#!/usr/bin/python3

##########################################################################
# git lab porcelain
##########################################################################

import git
import sys
import os
try:
    import configparser
except ImportError:
    import ConfigParser as configparser
import pycurl
try:
    from urllib.parse import urlencode
except ImportError:
    from urllib import urlencode

try:
    from urllib.parse import urlparse
except ImportError:
    from urlparse import urlparse
import urllib3
import io
import json
import argparse
import gitlab
import tempfile
import datetime
import subprocess
import time
import curses
from curses.textpad import Textbox, rectangle
from curses import wrapper
import weakref
from tabulate import tabulate
import logging
import argcomplete

#
# Classes representing git repo, gitlab objects and raw rest commands
#

##########################################################################
# Class to read in and validate our repository with needed gitlab info
# returns a gitpython object or None if validation fails
##########################################################################


class GitRepoLoader():
    @staticmethod
    def getUnvalidatedRepo():
        try:
            repo = git.Repo(".", search_parent_directories=True)
        except git.exc.InvalidGitRepositoryError as e:
            raise Exception("%s is not a git repo" % e)
        return repo
    @staticmethod
    def getRepo():
        repo = GitRepoLoader.getUnvalidatedRepo()
        return GitRepoLoader.validate(repo)
    @staticmethod
    def validate(repo):
        # Make sure the gitlab config is present here
        config = repo.config_reader()

        if not config.has_section('gitlab'):
            print(
                "You need a gitlab section in your git config specifying the "
                "resturl and apikey items")
            return None
        try:
            config.get('gitlab', 'apikey')
        except configparser.NoOptionError:
            print("You need to specify your apikey in the gitlab section of "
                  "your git config")
            return None
        try:
            config.get('gitlab', 'hosturl')
        except configparser.NoOptionError:
            print("You need to specify your hosturl in the gitlab section of "
                  "your git config")
            return None
        return repo

##########################################################################
# Wrapper class to instantiate a python-gitlab object using
# information fetched from our local git repository
##########################################################################


class RepoGitLab(gitlab.Gitlab):
    def __init__(self, repo):
        self.repo = repo
        reader = repo.config_reader()
        host = reader.get('gitlab', 'hosturl')
        key = reader.get('gitlab', 'apikey')

        try:
            sslverify = reader.get('gitlab', 'sslVerify')
            if sslverify == 'false':
                urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
                sslverify = False
            else:
                sslverify = True
        except:
            sslverify = True
        gitlab.Gitlab.__init__(self, host, key, ssl_verify=sslverify)

        try:
            debug = reader.get('gitlab', 'debug')
            if debug == 'true':
                gitlab.Gitlab.enable_debug(self)
        except:
            pass

##########################################################################
# Generic REST command for raw gitlab usage
##########################################################################
class RESTCommand():
    def __init__(self, repo):
        authheader = ["Private-Token: " +
                      repo.config_reader().get('gitlab', 'apikey')]
        self.curl = pycurl.Curl()
        self.response = io.BytesIO()
        self.curl.setopt(self.curl.WRITEDATA, self.response)
        self.curl.setopt(self.curl.HTTPHEADER, authheader)
        self.url = repo.config_reader().get('gitlab', 'hosturl')
        self.url = self.url + "/api/v4"

    def execute(self):
        self.curl.setopt(self.curl.URL, self.url)
        self.curl.perform()
        self.curl.close()
        self.jsonresponse = json.loads(self.response.getvalue())

##########################################################################
# Class to create any REST command for the gitlab api
# Assumes gitlab v4 api
##########################################################################


class RawRestCommand(RESTCommand):
    def __init__(self, repo, presult):
        # TODO: I think super is used differently in python2
        super().__init__(repo, presult)
        self.url = self.url + "/api/v4/" + presult.endpoint
        if presult.post:
            self.curl.setopt(self.curl.POST, True)
            if presult.arguments is not None:
                self.curl.setopt(self.curl.POSTFIELDS,
                                 urlencode(presult.arguments))
        else:
            if presult.arguments is not None:
                self.url = self.url + '?' + \
                    urlencode(presult.arguments)


#
# HELPER FUNCTIONS
#
def get_current_branch(strip=False):
    branch = git.cmd.Git().symbolic_ref("HEAD")
    if strip:
        branch = branch.replace("refs/heads/", "")
    return branch

def get_default_remote(repo, remote):
    if remote is not None:
        return remote
    remote = repo.config_reader().get('branch "' +
            get_current_branch(True) + '"', "remote")
    return remote

# Do some sanity checking on the branch availability in the remote
def validate_remote_branch(project, branch):
    try:
        head = project.branches.get(branch)
    except (gitlab.exceptions.GitlabHttpError,
            gitlab.exceptions.GitlabGetError) as e:
        raise Exception("Unable to find remote branch %s" % branch)

def get_message_parts(message):
    title, sep, body = message.partition('\n')
    return title, body


##########################################################################
# Helper function to get ther gitlab project of the your forkrepo
# as listed in gitconfig
##########################################################################
def getForkProject(repo, remote):
    lab = RepoGitLab(repo)
    # Parse the local repo config to find our project fork remote
    # Use that info to guess the gitlab project name
    config = repo.remotes[remote].config_reader
    try:
        project = config.get('projectid')
        project = lab.projects.get(project)
    except Exception:
        print(
            "you do not have a project id configured for the remote %s" %
            remote)
        print("please configure projectid setting for this remote")
        return None
    return project

##########################################################################
# Helper function to get parent project
##########################################################################
def getParentProject(lab, project):
    try:
        parent_id = project.forked_from_project['id']
        parent = lab.projects.get(parent_id)
    except AttributeError:
        # This project is not forked from anything, just use the passed
        # in project instead
        parent = project
    return parent

##########################################################################
# Helper function to get post description
#
# Create a temp file and open an editor to enter a MR description
##########################################################################
# TODO: pull commit template from git config; commit.template
# TODO: bug, comments in the file are not filtered out - remember to query
#         git config for the character that is treated a the comment char
def get_post_description(template=None):
    # Create a temp file and open an editor to enter a MR description
    temp = tempfile.NamedTemporaryFile(mode="w", delete=True)
    temp.write(
        "#Enter your Merge description here - this line will be auto removed\n")
    if (template is not None):
        temp.write(template)
    temp.flush()
    os.system(git.cmd.Git().var("GIT_EDITOR") + " " + temp.name)
    with open(temp.name) as x:
        description = x.readlines()
    temp.close()
    description = "".join(description)
    return description

##########################################################################
# Helper function to block execution until an MR's CI pipelines complete
##########################################################################


def wait_for_ci_pipelines(lab, project, parentproject, mr, headsha):
    pipelines = None

    # Need to refresh the merge request to get the latest head sha
    while mr.sha != headsha:
        print("Synchronizing merge request")
        mr = parentproject.mergerequests.get(mr.iid)
        time.sleep(5)

    print("Getting pipelines")
    while pipelines is None:
        try:
            pipelines = project.pipelines.list()
        except gitlab.exceptions.GitlabListError as e:
            time.sleep(1)
    print("Waiting for CI pipeline to complete")
    for pipeline in pipelines:
        if pipeline.sha == mr.sha:
            pipeobj = project.pipelines.get(pipeline.id)
            while pipeobj.finished_at is None:
                time.sleep(5)
                pipeobj.refresh()
            print("Pipeline %d completed with status %s" %
                  (pipeobj.id, pipeobj.status))
            if pipeobj.status == "failed":
                return False
    return True

##########################################################################
# Function to execute a raw gitlab REST command from porcelain
##########################################################################


def rawcommand(presult):
    try:
        cmd = RawRestCommand(GitRepoLoader.getRepo(), presult)
    except Exception as e:
        print(e)
        return
    cmd.execute()
    print(cmd.jsonresponse)

##########################################################################
# Function to execute a fork command to gitlab
# Specifies a project to fork, and optionally
# a remote to add to the local git repo pointing
# to the new fork
##########################################################################


def forkcommand(presult):
    repo = GitRepoLoader.getRepo()
    remote = get_default_remote(repo, presult.remote)
    lab = RepoGitLab(repo)
    if presult.project is None:
        presult.project = urlparse(repo.remotes[remote].url).path
        presult.project, extension = os.path.splitext(presult.project)
        presult.project = presult.project[1:]
        print(
            "Guessing project from upstream tracking branch url as %s" %
            presult.project)

    lab.auth()

    try:
        print("Getting project " + presult.project)
        project = lab.projects.get(presult.project)
    except gitlab.exceptions.GitlabGetError as e:
        raise Exception("Unable to find project %s" % presult.project)

    fork = None
    try:
        #fork = project.forks.create({'namespace': presult.namespace})
        fork = project.forks.create({})
    except gitlab.GitlabCreateError as e:
        print("project %s is already created\n" % presult.project)
        for p in project.forks.list():
            print(
                "Comparing %s to %s" %
                (p.namespace['path'], lab.user.username))
            if p.namespace['path'] == lab.user.username:
                fork = p
                break

    print("Project %s forked to %s/%s" %
          (fork.path, fork.namespace['path'], fork.path))
    if presult.remote is not None:
        cloneurl = "ssh://git@gitlab.com/" + \
            fork.namespace['path'] + "/" + fork.path + ".git"
        repo.create_remote(presult.remote, cloneurl)
        config = repo.remotes[presult.remote].config_writer
        config.set('projectid', fork.id)
        print(
            "Remote for forked project created with name %s" %
            presult.remote)

    return None


#
# GIT LAB COMMANDS
#

##########################################################################
# Function to create a pull request
##########################################################################
def createmrcommand(presult):
    repo = GitRepoLoader.getRepo()
    remote = get_default_remote(repo, presult.remote)
    lab = RepoGitLab(repo)

    message = presult.message
    if not message:
        message = get_post_description()

    project = getForkProject(repo, remote)
    if project is None:
        raise Exception("No such remote %s" % remote)
    parentproject = getParentProject(lab, project)

    if presult.refspec is None:
        source = get_current_branch(True)
        dest = [i for i in parentproject.branches.list()
                if i.default == True][0].name
    else:
        source, sep, dest = presult.refspec.partition(':')
        if len(source) == 0:
            source = get_current_branch(True)
        if len(dest) == 0:
            dest = [i for i in parentproject.branches.list()
                    if i.default == True][0].name

    if presult.push:
        try:
            repo.remotes[remote].push(refspec='{}:{}'.format(
                source, source), force=presult.force)
        except Exception as e:
            raise Exception("Failed to push references: %s" % e)

    # Validate source and dest branches
    try:
        validate_remote_branch(project, source)
    except Exception as e:
        raise Exception(str(e) + ", try pushing first")

    validate_remote_branch(parentproject, dest)
    title, body = get_message_parts(message)

    # And finally, create the MR
    request = {
        'source_branch': source,
        'target_branch': dest,
        'title': title,
        'target_project_id': parentproject.id,
    }

    if body is not None and len(body) > 0:
        request['description'] = body
    mr = project.mergerequests.create(request)
    print("Created merge-request %d" % mr.iid)

##########################################################################
# Update a merge request with new commits
##########################################################################
def updatemrcommand(presult):
    repo = GitRepoLoader.getRepo()
    remote = get_default_remote(repo, presult.remote)
    lab = RepoGitLab(repo)
    project = getForkProject(repo, remote)
    parentproject = getParentProject(lab, project)

    mr = parentproject.mergerequests.get(presult.id)
    for head in repo.heads:
        if str(head) == mr.source_branch:
            break
    if str(head) != mr.source_branch:
        raise Exception("Branch %s doesn't exist" % mr.source_branch)

    try:
        repo.remotes[remote].push(refspec='{}:{}'.format(
            mr.source_branch, mr.source_branch), force=True)
    except Exception as e:
        raise Exception("Failed to push references: %s" % e)

    for head in repo.heads:
        if str(head) == mr.source_branch:
            break

    print("Merge request %d updated" % mr.iid)
    if presult.wait:
        if wait_for_ci_pipelines(
                lab, project, parentproject, mr, head.commit) == False:
            raise Exception("CI Pipeline failed")

##########################################################################
# Function to list merge requests
##########################################################################


def listmrcommand(presult):
    repo = GitRepoLoader.getRepo()
    remote = get_default_remote(repo, presult.remote)
    lab = RepoGitLab(repo)
    project = getForkProject(repo, remote)
    if project is None:
        raise Exception("No project found for remote %s" % remote)

    lab.auth()
    if presult.author is not None:
        author = lab.users.list(search=presult.author)
        if len(author) == 0:
            print("User %s not found" % presult.author)
            return
        if len(author) != 1:
            print("Your author specification matches multiple users:")
            for a in author:
                print(a.username)
            raise Exception("Please refine your user search string")
        presult.author = author[0].id

    if presult.parent:
        # Get the parent project of our fork
        project = getParentProject(lab, project)
    try:
        labellist = presult.labels.split(',')
    except:
        labellist = []
   
    mrs = project.mergerequests.list(
            state=presult.state,
            author_id=presult.author,
            search=presult.keyword,
            labels=labellist)

    if presult.usejson:
        outputjson = {}
        mrlist = []
        for mr in mrs:
            mrlist.append(mr.iid)
        outputjson[project.id] = mrlist
        print(json.dumps(outputjson))
        return None

    table = []
    for mr in mrs: 
        row = [mr.iid, mr.title, mr.web_url]
        pipestatus = "none"
        highestid=0

        for pipeline in mr.pipelines():
            if pipeline['sha'] == mr.sha and highestid < int(pipeline['id']):
                pipestatus = pipeline['status']
                highestid = int(pipeline['id'])
        row.append(pipestatus)
        try:
            approval = mr.approvals.get()
            if approval.approved == True:
                row.append('YES')
            else:
                row.append('NO')
        except gitlab.exceptions.GitlabGetError as e:
            row.append('N/A')

        table.append(row)
        # print("%d: %s: %s: CI status: [%s] %s" % (
        #    mr.iid, mr.title, mr.web_url, pipestatus, ackstring))

    print(
        tabulate(
            table,
            headers=[
                'ID',
                'TITLE',
                'URL',
                'CI STATUS',
                'APPROVED']))
    return None


##########################################################################
# Function to filter mr list output according to policy 
##########################################################################
def filtermrcommand(presult):
    repo = GitRepoLoader.getRepo()
    lab = RepoGitLab(repo)
    if not os.path.isdir(os.environ['HOME']+'/.gitlabporcelain/filters'):
        sys.stderr.write(os.environ['HOME']+'/.gitlabporcelain/filters not present, no filters\n')
        return None

    if presult.list:
        import glob
        modules = glob.glob(os.environ['HOME']+'/.gitlabporcelain/filters/*.py')
        mnames = [os.path.basename(f)[:-3] for f in modules if os.path.isfile(f)]
        print(mnames)
        return None

    runfilter = None
    if not presult.filter:
        sys.stderr.write("You must specify a filter name\n")
        return None

    sys.path.append(os.environ['HOME']+'/.gitlabporcelain/filters')
    if presult.info:
        try:
            filterinfo = getattr(__import__(presult.filter,fromlist=['filterinfo']), 'filterinfo')
            output = filterinfo()
            sys.stderr.write(output + "\n")
        except Exception as e:
            sys.stderr.write("Error getting filter information for %s: %s\n" % (presult.filter, str(e)))

        return None
    try:
        runfilter = getattr(__import__(presult.filter, fromlist=['runfilter']), 'runfilter')
    except Exception as e:
        sys.stderr.write("Error loading filter %s: %s\n" % (presult.filter, str(e)))
        runfilter = None

    if runfilter == None:
        return None
    # fetch json from stdin, and convert it to python array
    inputjson = json.loads(sys.stdin.read())
    output = runfilter(repo, lab, inputjson)
    if output:
        print(json.dumps(output)) 


##########################################################################
# Function to close an mr
##########################################################################


def closemrcommand(presult):
    repo = GitRepoLoader.getRepo()
    remote = get_default_remote(repo, presult.remote)
    lab = RepoGitLab(repo)
    project = getForkProject(repo, remote)
    if presult.parent:
        parentproject = getParentProject(lab, project)
    else:
        parentproject = project

    try:
        mr = parentproject.mergerequests.get(presult.id)
        mr.state_event = 'close'
        mr.save()
    except Exception as e:
        raise Exception("Failed to close MR %s: %s" % (presult.id, e))
    print("Closed merge request %s" % presult.id)

##########################################################################
# Function to list all projects on the gitlab server
##########################################################################


def listprojects(presult):
    lab = RepoGitLab(GitRepoLoader.getRepo())
    i = 1

    while True:
        try:
            if presult.keyword is None:
                projects = lab.projects.list(
                    page=i, per_page=20, owned=presult.owned)
            else:
                projects = lab.projects.list(
                    page=i, per_page=20, owned=presult.owned, search=presult.keyword)
        except Exception as e:
            raise Exception("Error while listing projects")

        for p in projects:
            print("name: %s\nid: %d\n" % (p.name, p.id))
        i = i + 1
        if (len(projects) < 20):
            return None
    return None

##########################################################################
# Git lab review window code
##########################################################################
##########################################################################
# Window Classes
##########################################################################
class Window:
    instances = []

    def __init__(self, repo, lab, stdscr, numpads, pattr=None):
        self.repo = repo
        self.stdscr = stdscr
        self.lab = lab
        self.height, self.width = self.stdscr.getmaxyx()
        self.ystart = 0
        self.__class__.instances.append(weakref.proxy(self))
        self.needResize = False

        self.customkeys = [
            {'inputs': [curses.KEY_UP],
             'keystroke': '<UP ARROW>',
             'op': self.doKeyUp,
             'help': 'Move up one entry'},
            {'inputs': [curses.KEY_DOWN],
             'keystroke': '<DOWN ARROW>',
             'op': self.doKeyDown,
             'help': 'Move down one entry'},
            {'inputs': [curses.KEY_LEFT],
             'op': self.doKeyLeft,
             'keystroke': 'LEFT ARROW',
             'help': 'Move one pane left'},
            {'inputs': [curses.KEY_RIGHT],
             'keystroke': '<RIGHT ARROW>',
             'op': self.doKeyRight,
             'help': 'Move one pane right'},
            {'inputs': [ord('f'), ord('F'), curses.KEY_REFRESH],
             'keystroke': 'f|F',
             'op': self.doKeyRefresh,
             'help': 'Refresh the current screen'},
            {'inputs': [ord('q'), ord('Q')],
             'keystroke': 'q|Q',
             'op': self.doKeyPrevious,
             'help': 'Go to the previous screen'},
            {'inputs': [ord('h'), ord('H')],
             'keystroke': 'h|H',
             'op': self.doKeyHelp,
             'help': 'Get help on nav commands (this screen)'}]

        self.numpads = numpads
        self.pads = []
        for i in range(numpads):
            # default padsize to 5000 lines if no attribute exists for it
            try:
                padsize = pattr[i]['padsize']
            except:
                padsize = 5000

            pad = {'pad': curses.newpad(padsize, int(self.width)),
                   'header': 0,
                   'footer': 0,
                   'selectablerows': 0}
            pad['pad'].keypad(True)
            pad['pad'].nodelay(False)
            pad['pad'].scrollok(True)
            pad['pad'].idlok(1)
            self.pads.append(pad)
            try:
                pad['attr'] = pattr[i]
            except Exception:
                pad['attr'] = {}
                pass
        self.activepad = 0

    def __del__(self):
        self.clearWindow()

    def _getKey(self, key):
        if key in self.pads[self.activepad]['attr'].keys():
            return self.pads[self.activepad]['attr'][key]
        else:
            return None

    def getHelp(self):
        helpstring = ''
        for c in self.customkeys:
            helpstring = helpstring + c['keystroke'] + " : " + c['help'] + '\n'
        return helpstring

    def setActivePad(self, idx):
        self.activepad = idx

    def moveCursor(self, y, x):
        self.pads[self.activepad]['pad'].move(y, x)

    def getCursor(self):
        return self.pads[self.activepad]['pad'].getyx()

    def addstr(self, y, x, content):
        self.pads[self.activepad]['pad'].addstr(y, x, content)

    def addHeader(self, content):
        if (len(self.pads) > 1):
            if self.activepad == 0:
                content = content + " >>"
            elif self.activepad == len(self.pads) - 1:
                content = "<< " + content
            else:
                content = "<< " + content + " >>"
        self.addstr(0, int(self.width / 2 - (len(content) / 2)), content)

    def clearWindow(self):
        for pad in self.pads:
            pad['pad'].clear()

    def chgAt(self, y, x, attr):
        self.pads[self.activepad]['pad'].chgat(y, x, self.width, attr)

    def getLine(self, y, x):
        return self.pads[self.activepad]['pad'].instr(y, x).decode("utf-8")

    def resizeWindow(self):
        self.height, self.width = self.stdscr.getmaxyx()
        for pad in self.pads:
            pad['pad'].resize(self.height, self.width)

    def refreshWindow(self):
        self.pads[self.activepad]['pad'].refresh(
            self.ystart, 0, 0, 0, self.height - 1, self.width)

    def setPadRowInfo(self, header, footer, selectable):
        self.pads[self.activepad]['header'] = header
        self.pads[self.activepad]['footer'] = footer
        self.pads[self.activepad]['selectablerows'] = selectable

    def registerNavInput(self, inputinfo):
        # input info is of the form
        # {inputs:[ key, key, ...], keystroke: string, op: callable(y, x), help: helptext }
        self.customkeys = self.customkeys + inputinfo

    def clearNavInput(self):
        self.customkeys = []

    def doKeyUp(self, y, x):
        logging.debug("GOT KEY UP with x,y at %d,%d and ystart %d\n" % (x, y, self.ystart))
        logging.debug("WIN HEIGHT IS %d\n" % self.height)
        highlight = self._getKey('highlight')
        if highlight is None:
            highlight = True
        if y <= self.pads[self.activepad]['header']:
            return False
        if self.ystart != 0 and y >= self.height:
            self.ystart = self.ystart - 1
        if highlight:
            self.chgAt(y, x, curses.A_NORMAL)
        y = y - 1
        if highlight:
            self.chgAt(y, x, curses.A_STANDOUT)
        self.moveCursor(y, x)
        logging.debug("REFRESH WITH YSTART %d\n" % self.ystart)
        self.refreshWindow()
        return False

    def doKeyDown(self, y, x):
        logging.debug("GOT KEY UP with x,y at %d,%d and ystart %d\n" % (x, y, self.ystart))
        logging.debug("WIN HEIGHT IS %d\n" % self.height)
        logging.debug("SELECTABLEROWS IS %d\n" % self.pads[self.activepad]['selectablerows'])
        highlight = self._getKey('highlight')
        if highlight is None:
            highlight = True
        if (y >= self.pads[self.activepad]['selectablerows'] + self.pads[self.activepad]['header'] - 1):
            return False
        if highlight:
            self.chgAt(y, x, curses.A_NORMAL)
        if (y >= self.height-1):
            self.ystart = self.ystart+1
        y = y + 1
        if highlight:
            self.chgAt(y, x, curses.A_STANDOUT)
        self.moveCursor(y, x)
        logging.debug("REFRESH WITH YSTART %d\n" % self.ystart)
        self.refreshWindow()
        return False

    def doKeyLeft(self, y, x):
        highlight = self._getKey('highlight')
        if highlight is None:
            highlight = True

        if self.activepad == 0:
            self.setActivePad((len(self.pads) - 1))
        else:
            self.setActivePad(self.activepad - 1)
        self.refreshWindow()
        self.moveCursor(self.pads[self.activepad]['header'], 0)
        if highlight:
            self.chgAt(self.pads[self.activepad]
                       ['header'], 0, curses.A_STANDOUT)
        return False

    def doKeyRight(self, y, x):
        highlight = self._getKey('highlight')
        if highlight is None:
            highlight = True

        self.setActivePad((self.activepad + 1) % len(self.pads))
        self.refreshWindow()
        self.moveCursor(self.pads[self.activepad]['header'], 0)
        if highlight:
            self.chgAt(self.pads[self.activepad]
                       ['header'], 0, curses.A_STANDOUT)
        return False

    def doKeyRefresh(self, y, x):
        self.pads[self.activepad]['pad'].clear()
        self.display()
        return False

    def doKeyPrevious(self, y, x):
        return True

    def doKeyHelp(self, y, x):
        helpmsg = self.getHelp()
        if helpmsg:
            helpwindow = HelpWindow(helpmsg, self.stdscr)
            helpwindow.display()
            helpwindow.handleNavInput()
            self.refreshWindow()
        return False

    def handleNavInput(self):
        while True:
            key = self.pads[self.activepad]['pad'].getch()
            if key == curses.KEY_RESIZE:
                for i in Window.instances:
                    i.needResize = True
                    self.resizeWindow()
                    self.needResize = False
                continue

            (y, x) = self.getCursor()
            for c in self.customkeys:
                if key in c['inputs']:
                    ret = c['op'](y, x)
                    if ret == True:
                        return
                    break
            if self.needResize == True:
                self.needResize = False
                self.resizeWindow()


class HelpWindow(Window):
    def __init__(self, msg, stdscr):
        self.msg = msg.splitlines()
        Window.__init__(self, None, None, stdscr, 1)
        self.clearNavInput()
        self.registerNavInput([{'inputs': [ord('q'), ord('Q')],
                                'kerystroke': 'q|Q',
                                'op': self.doKeyPrevious,
                                'help': 'Go to previous screen'}])
        longestx = 0
        ysize = len(self.msg)
        for l in self.msg:
            if len(l) > longestx:
                longestx = len(l)
        self.yoffset = int((self.height - ysize) / 2)
        self.xoffset = int((self.width - longestx) / 2)

    def display(self):
        y = 0
        for l in self.msg:
            self.addstr(self.yoffset + y, self.xoffset, l)
            y = y + 1
        self.refreshWindow()


class WaitMsgWindow(Window):
    def __init__(self, msg, stdscr):
        self.msg = msg
        self.stdscr = stdscr
        Window.__init__(self, None, None, stdscr, 1)

    def updateText(self, msg):
        self.msg = msg
        self.stdscr.clear()
        self.display()

    def display(self):
        self.addstr(int(self.height / 2),
                    int((self.width / 2) - (len(self.msg) / 2)),
                    self.msg)
        self.refreshWindow()

class FilterWindow():
    def __init__(self, filterinfo, stdscr):
        self.filterinfo = filterinfo
        self.stdscr = stdscr

    def display(self):
        editpad = curses.newwin(5, curses.COLS, 0, 0)
        editpad.addstr(1,1, "Enter search terms in as key/value pairs, for example")
        editpad.addstr(2,1, "label=label1,label2 author=user@host.com")
        editpad.addstr(3,1, "ctrl-G to apply filter to move between fields and apply filters")
        editpad.border()

        labeltitle = curses.newwin(1,curses.COLS, 8, 0)
        labeltitle.addstr(0, 0, "Labels")
        labelwin = curses.newwin(3,curses.COLS, 9, 0)
        labelbox = Textbox(labelwin)
        labelwin.border()

        targettitle = curses.newwin(1,curses.COLS, 14, 0)
        targettitle.addstr(0, 0, "Target Branch")
        targetwin = curses.newwin(3, curses.COLS, 15, 0)
        targetbox = Textbox(targetwin)
        targetwin.border()

        labelwin.move(1,1)
        self.stdscr.refresh()
        editpad.refresh()
        targettitle.refresh()
        targetwin.refresh()
        labeltitle.refresh()
        labelbox.edit() 
        targetwin.move(1,1)
        targetbox.edit()

        # note, drawing a border on textboxes produces ascii lines in the textbox output
        # that is returned from gather, so the splitlines and slice below trims that out
        message = labelbox.gather().splitlines()[1][1:][:-1].rstrip()
        value=message.split(',')
        self.filterinfo['labels'] = value

        message = targetbox.gather().splitlines()[1][1:][:-1].rstrip()
        value = message.split(',')
        self.filterinfo['targets'] = value

class DiscViewWindow(Window):
    def __init__(self, repo, stdscr, mr, note, discussion):
        self.note = note
        self.mr = mr
        self.discussion = discussion
        attrs = [{'highlight': False}]
        Window.__init__(self, repo, None, stdscr, 1, attrs)
        self.registerNavInput([{'inputs': [ord('c'), ord('C')],
                                'keystroke': 'c|C',
                                'op': self.doComment,
                                'help': 'Reply to a comment'}])

    def display(self):
        y = 0
        text = self.note.body.splitlines()
        for t in text:
            self.addstr(y, 0, t)
            y = y + 1
        self.setPadRowInfo(0, 0, y)
        self.refreshWindow()
        self.moveCursor(0, 0)
        self.handleNavInput()
        self.refreshWindow()

    def _getComment(self):
            # Create a file for our comment
        cfile = tempfile.NamedTemporaryFile('w+')
        # Add the context we're commenting on
        comment = '>' + self.note.body.replace('\n', '>\n')
        cfile.write(comment)
        cfile.flush()
        # Turn off curses
        curses.endwin()
        # Run our editor process
        subprocess.run(["vi", cfile.name])
        # Go back to get the comment
        cfile.seek(0)
        # Get the comment lines
        cfl = cfile.readlines()
        # Close the temp file
        cfile.close()
        # Remove the context lines and rejoin the string
        if cfl[0].startswith('>'):
            cfl = cfl[1:]
        fullstring = ''
        fullstring = fullstring.join(cfl)
        # re-enable curses
        curses.raw()
        curses.noecho()
        # and redraw the window
        self.refreshWindow()
        return fullstring

    def doComment(self, y, x):
        comment = self._getComment()
        if len(comment) != 0:
            self.discussion.notes.create({'body': comment})
        return False


class DiffViewWindow(Window):
    def __init__(self, repo, stdscr, mr, commit, diff):
        self.diff = diff
        self.difflines = diff.splitlines()
        self.mr = mr
        self.repo = repo
        self.commit = self.repo.commit(commit)
        self.comments = []
        attrs=[{'padsize': len(self.difflines)}]
        Window.__init__(self, repo, None, stdscr, 1, attrs)
        self.registerNavInput([{'inputs': [ord('c'), ord('C')],
                                'keystroke': 'c|C',
                                'op': self.doComment,
                                'help': 'Create a comment on a chunk in a commit'}])

    def display(self):
        y = 0
        for l in self.difflines:
            self.addstr(y, 0, l)
            y = y + 1
        self.setPadRowInfo(0, 0, len(self.difflines))
        self.moveCursor(0, 0)
        self.refreshWindow()

    def _getLineOffset(self, y):
        # we need to backtrack from y, to the nearest line that is a diff
        # context (@@)
        delta = 0
        findfile = False
        for l in self.difflines[y::-1]:
            if findfile == True:
                if l.startswith('diff'):
                    newfilename = (l.split(' ')[3])[2:]
                    oldfilename = (l.split(' ')[2])[2:]
                    linecount = self.repo.git.show(
                        str(self.commit) + '^:' + oldfilename).count('\n')
                    break
            else:
                if l[0] == '@' and l[1] == '@':
                    # delta is from the last line before we found this, sub 1
                    delta = delta - 1
                    # This is a context line, get the from line information
                    oldoffset = l.split(' ')[1].split(',')[0].strip('-')
                    newoffset = l.split(' ')[2].split(',')[0].strip('+')
                    # We need to know if this is a line that didn't exist in
                    # the old file
                    findfile = True
                else:
                    delta = delta + 1

        newoffset = int(newoffset) + delta
        if linecount < newoffset:
            oldoffset = ''
        else:
            oldfoffset = int(oldoffset) + delta
        if not (self.difflines[y].startswith('+') or self.difflines[y].startswith('-')):
            oldoffset = None

        return (newoffset, oldoffset, oldfilename, newfilename)

    def _getComment(self, y):
            # Create a file for our comment
        cfile = tempfile.NamedTemporaryFile('w+')
        # Add the context we're commenting on
        cfile.write(">" + self.difflines[y] + "\n")
        cfile.flush()
        # Turn off curses
        curses.endwin()
        # Run our editor process
        subprocess.run(["vi", cfile.name])
        # Go back to get the comment
        cfile.seek(0)
        # Get the comment lines
        cfl = cfile.readlines()
        # Close the temp file
        cfile.close()
        # Remove the context lines and rejoin the string
        if cfl[0].startswith('>'):
            cfl = cfl[1:]
        fullstring = ''
        fullstring = fullstring.join(cfl)
        # re-enable curses
        curses.raw()
        curses.noecho()
        # and redraw the window
        self.refreshWindow()
        return fullstring

    def doComment(self, y, x):
        comment = {}
        # Get our start/base/head sha values
        comment['position'] = self.mr.diff_refs
        (comment['position']['new_line'],
         comment['position']['old_line'],
         comment['position']['old_path'],
         comment['position']['new_path']) = self._getLineOffset(y)
        comment['position']['position_type'] = 'text'
        logging.debug(comment)
        body = self._getComment(y)
        if len(body) == 0:
            return
        comment['body'] = '>commit: ' + self.mr.diff_refs['head_sha'][0:8] + ' path: ' + \
            comment['position']['new_path'] + '\n>' + \
            self.difflines[y] + '\n' + body
        self.mr.discussions.create(comment)
        return False


class MRReviewWindow(Window):
    def __init__(self, repo, lab, stdscr, project, mr):
        self.project = project
        self.mr = mr
        self.stdscr = stdscr
        self.notemap = {}
        Window.__init__(self, repo, lab, stdscr, 2)
        self.registerNavInput([{'inputs': [ord('r'), ord('R')],
                                'keystroke': 'r|R',
                                'op': self.doResolve,
                                'help': 'Toggle Issue as (un)resolved'},
                               {'inputs': [10, 13, curses.KEY_ENTER],
                                'keystroke': '<ENTER>',
                                'op': self.doEnter,
                                'help': 'Select the merge request to review'}])

    def display(self):
        waitmsg = "Gathering Discussions for Merge request " + \
            self.mr.title + "(" + str(self.mr.iid) + ")"
        waitwindow = WaitMsgWindow(waitmsg, self.stdscr)
        waitwindow.display()
        # refresh our project and merge request
        self.project = self.lab.projects.get(self.project.id)
        # Gather our general discussions and place them in the first active pad
        discussions = self.mr.discussions.list()
        headers = ['Note ID', 'Type', 'Comment', 'Resolved']
        table = []
        i = 1
        for d in discussions:
            waitmsg = "Gathering Discussions for Merge request " + \
                self.mr.title + "(" + str(self.mr.iid) + ")(" + str(i) + "/" + str(len(discussions)) + ")"
            waitwindow.updateText(waitmsg)
            i = i + 1
            indent = ''
            for n in d.attributes['notes']:
                # Skip system notifications
                if n['system'] == True:
                    continue
                self.notemap[n['id']] = d.id
                firstnl = n['body'].find('\n')
                if firstnl != -1:
                    maxidx = min(40, firstnl)
                else:
                    maxidx = min(40, len(n['body']))

                row = [n['id'], n['type'], indent +
                       n['body'][0:maxidx+1]]
                if n['resolvable'] == True:
                    if n['resolved'] == True:
                        row.append('Yes')
                    else:
                        row.append('No')
                else:
                    # Unresolvable notes are always marked resolved
                    row.append('Yes')

                table.append(row)
                if not indent:
                    indent = '|-'

        rows = tabulate(table, headers).splitlines()

        self.clearWindow()
        headerline = "GENERAL DISCUSSIONS FOR MERGE REQUEST " + \
            self.mr.title + "(" + str(self.mr.iid) + ")"
        self.addHeader(headerline)
        ridx = 1
        for r in rows:
            self.addstr(ridx, 0, r)
            ridx = ridx + 1
        self.setPadRowInfo(3, 1, len(table))

        # Move to the last pad and add our commits to start a new review
        self.setActivePad(1)
        headerline = "COMMITS FOR MERGE REQUEST " + \
            self.mr.title + "(" + str(self.mr.iid) + ")"
        self.addHeader(headerline)
        table = []
        headers = ['COMMIT', 'SUBJECT']
        headref = 'refs/merge-requests/' + str(self.mr.iid) + '/head'
        self.repo.remotes.origin.fetch(headref + ':' + headref)
        commits = self.mr.commits()
        i = 1
        for c in commits:
            waitmsg = "Gathering Commits for Merge request " + \
                self.mr.title + "(" + str(self.mr.iid) + ")(" + str(i) + "/" + str(len(commits)) + ")"
            waitwindow.updateText(waitmsg)
            i = i + 1
            row = [c.id, c.title]
            table.append(row)
        rows = tabulate(table, headers).splitlines()
        ridx = 1
        for r in rows:
            self.addstr(ridx, 0, r)
            ridx = ridx + 1

        self.setPadRowInfo(3, 1, len(table))

        # close the wait window
        del waitwindow

        # Go back to the first pad and display it
        self.setActivePad(0)
        self.chgAt(3, 0, curses.A_STANDOUT)
        self.moveCursor(3, 0)
        self.refreshWindow()

    def doEnter(self, y, x):
        if self.activepad == 0:
            line = self.getLine(y, x)
            nid = line.split(' ')[0]
            try:
                note = self.mr.notes.get(nid)
            except:
                # If we can't find a note (i.e. if there are none), do nothing
                return False
            discussion = self.mr.discussions.get(self.notemap[note.id])
            discview = DiscViewWindow(
                self.repo, self.stdscr, self.mr, note, discussion)
            discview.display()
            discview.handleNavInput()
        elif self.activepad == 1:
            line = self.getLine(y, x)
            commit = line.split(' ', 1)[0]
            diff = self.repo.git.diff(commit + "~.." + commit)
            diffview = DiffViewWindow(
                self.repo, self.stdscr, self.mr, commit, diff)
            diffview.display()
            diffview.handleNavInput()
        self.refreshWindow()
        return False

    def doResolve(self, y, x):
        config = self.repo.config_reader()
        line = self.getLine(y, x)
        nid = line.split(' ')[0]
        note = self.mr.notes.get(nid)
        discussion = self.mr.discussions.get(self.notemap[note.id])
        if note.resolved == True:
            state = 'false'
        else:
            state = 'true'

        # Sigh, python-gitlab has a bug in which it can't resolve discussions,
        # so we need to do this with curl right now
        cmd = 'curl -o /dev/null -s -X PUT -H"Private-Token: ' + config.get(
            'gitlab', 'apikey') + '" ' + config.get(
            'gitlab', 'hosturl') + '/api/v4/projects/' + str(
            self.project.id) + '/merge_requests/' + str(
                self.mr.iid) + '/discussions/' + str(
                    discussion.id) + '?resolved=' + state
        os.system(cmd)
        curses.ungetch(curses.KEY_REFRESH)

class TopReviewWindow(Window):
    def __init__(self, repo, lab, project, stdscr):
        self.filters = {}
        self.mrcount = 0
        self.project = project
        Window.__init__(self, repo, lab, stdscr, 1)
        self.registerNavInput([{'inputs': [10, 13, curses.KEY_ENTER],
                                'keystroke': '<ENTER>',
                                'op': self.doEnter,
                                'help': 'Select the project to review merges for'},
                               {'inputs': [ord('a'), ord('A')],
                                'keystroke': 'a|A',
                                'op': self.doApproveToggle,
                                'help': 'Toggle Approval status of merge request'},
                               {'inputs': [ord('s'), ord('S')],
                                'keystroke': 's|S',
                                'op': self.filterMRs,
                                'help': 'Filter presented merge requests'}])

    def display(self):
        waitmsg = "Gathering Merge requests for project " + self.project.name
        waitwindow = WaitMsgWindow(waitmsg, self.stdscr)
        waitwindow.display()
        # Find the id of the parent, so we can open the MR there
        try:
            labels = self.filters['labels']
        except:
            labels = []
        try:
            targets = self.filters['targets']
        except:
            targets = []
        mrs = self.project.mergerequests.list(
            state='opened', labels=labels, targets=targets, order_by='created_at')
        headers = [
            'ID',
            'TITLE',
            'TARGET BRANCH',
            'CI STATUS',
            'DISCUSSIONS RESOLVED',
            'APPROVALS']
        table = []
        self.mrcount = len(mrs)
        self.setPadRowInfo(3, 1, self.mrcount)
        i = 0
        for m in mrs:
            waitmsg = "Gathering Merge requests for project " + self.project.name + "(" + str(i) + "/" + str(len(mrs)) + ")"
            i = i + 1
            waitwindow.updateText(waitmsg)
            m = self.project.mergerequests.get(str(m.iid))

            try:
                approval = m.approvals.get()

                if len(approval.approved_by) != 0:
                    approvers = ''
                    for u in approval.approved_by:
                        approvers = approvers + " " + u['user']['username']
                    approvers = approvers.strip()
                    approvers = str(len(approval.approved_by)) + \
                        '(' + approvers + ')'
                else:
                    approvers = '0'

                approved = 'No'
                if approval.approved:
                    approved = 'Yes'
            except gitlab.exceptions.GitlabGetError as e:
                approvers = 'N/A'
                approved = 'Yes'

            discussions = m.discussions.list()
            discResolved = 'Yes'
            for d in discussions:
                if discResolved == 'No':
                    break
                for n in d.attributes['notes']:
                    if n['system'] == True:
                        continue
                    if n['resolvable'] == True:
                        if n['resolved'] == False:
                            discResolved = 'No'
                        break
            try:
                head_pipeline = m.head_pipeline['status']
            except:
                head_pipeline = "None"

            row = [
                m.iid,
                m.title,
                m.target_branch,
                head_pipeline,
                discResolved,
                approvers]
            table.append(row)

        rows = tabulate(table, headers).splitlines()
        del waitwindow
        self.clearWindow()
        headerline = "MERGE REQUESTS FOR PROJECT " + self.project.name
        self.addHeader(headerline)
        ridx = 1
        for r in rows:
            self.addstr(ridx, 0, r)
            ridx = ridx + 1

        # Set line one to be reverse video
        self.chgAt(3, 0, curses.A_STANDOUT)
        # Set the cursor to be the first MR
        self.moveCursor(3, 0)
        self.refreshWindow()

    def filterMRs(self, y, x):
        self.filters = {}
        filtwin = FilterWindow(self.filters, self.stdscr)
        filtwin.display()
        self.refreshWindow()
        curses.ungetch(curses.KEY_REFRESH)

    def doEnter(self, y, x):
        line = self.getLine(y, x)
        iid = int(line.split()[0])
        mr = self.project.mergerequests.get(iid)
        reviewwindow = MRReviewWindow(
            self.repo, self.lab, self.stdscr, self.project, mr)
        reviewwindow.display()
        reviewwindow.handleNavInput()
        self.refreshWindow()
        return False

    def doApproveToggle(self, y, x):
        config = self.repo.config_reader()
        line = self.getLine(y, x)
        iid = int(line.split()[0])
        mr = self.project.mergerequests.get(iid)
        try:
            approval = mr.approvals.get()
        except gitlab.exceptions.GitlabGetError as e:
            # TODO: Display a modal window with "Approvals are not supported [OK]"
            return False
        # Need to authorize ourselves to get the current user object
        self.lab.auth()
        approved = 'approve'
        for u in approval.approved_by:
            if u['user']['id'] == self.lab.user.id:
                approved = 'unapprove'
                break
        # Approvals api in python-gitlab is missing at the moment
        cmd = 'curl -s -o /dev/null -X POST -H"Private-Token: ' + config.get('gitlab', 'apikey') + '" ' + config.get(
            'gitlab', 'hosturl') + '/api/v4/projects/' + str(self.project.id) + '/merge_requests/' + str(mr.iid) + '/' + approved
        os.system(cmd)
        # Force a screen rebuild
        curses.ungetch(curses.KEY_REFRESH)
        return False


class ProjectWindow(Window):
    def __init__(self, repo, lab, stdscr):
        Window.__init__(self, repo, lab, stdscr, 1)
        self.registerNavInput([{'inputs': [10, 13, curses.KEY_ENTER],
                                'keystroke': '<ENTER>',
                                'op': self.doEnter,
                                'help': 'Select the project to review merges for'}])

    def display(self):
        waitmsg = "Gathering Available projects "
        waitwindow = WaitMsgWindow(waitmsg, self.stdscr)

        projects = []
        i = 0
        for r in self.repo.remotes:
            waitmsg = "Gathering Available projects (" + str(i) + "/" +str(len(self.repo.remotes)) + ")"
            waitwindow.updateText(waitmsg)
            i = i + 1
            try:
                projid = r.config_reader.get('projectid')
                project = self.lab.projects.get(projid)
                projects.append({'project': project})
            except Exception:
                continue

        # Find the id of the parent, so we can open the MR there
        headers = ['ID', 'PROJECT']
        table = []
        self.setPadRowInfo(3, 1, len(projects))
        for p in projects:
            row = [
                p['project'].id,
                p['project'].path_with_namespace]
            table.append(row)

        rows = tabulate(table, headers).splitlines()
        del waitwindow
        self.clearWindow()
        headerline = "AVAILABLE PROJECTS"
        self.addHeader(headerline)
        ridx = 1
        for r in rows:
            self.addstr(ridx, 0, r)
            ridx = ridx + 1

        # Set line one to be reverse video
        self.chgAt(3, 0, curses.A_STANDOUT)
        # Set the cursor to be the first MR
        self.moveCursor(3, 0)
        self.refreshWindow()

    def doEnter(self, y, x):
        line = self.getLine(y, x)
        pid = line.split()[0]
        project = self.lab.projects.get(pid)
        projectwindow = TopReviewWindow(
            self.repo, self.lab, project, self.stdscr)
        projectwindow.display()
        projectwindow.handleNavInput()
        self.refreshWindow()
        return False

##########################################################################
# Main routine
##########################################################################


def runReviewUI(stdscr):
    repo = GitRepoLoader.getRepo()
    if repo is None:
        sys.exit(1)
    lab = RepoGitLab(repo)
    if lab is None:
        sys.exit(1)

    newwin = ProjectWindow(repo, lab, stdscr)
    newwin.display()
    newwin.handleNavInput()


def reviewcommand(presult):
    wrapper(runReviewUI)
    return


##########################################################################
# Setup the local repo to work with gitlab
##########################################################################
def setupcommand(presult):

    # Use the right version of input for python 2 vs 3
    myinput = input
    if sys.version_info[0] < 3:
        myinput = raw_input

    repo = GitRepoLoader.getUnvalidatedRepo()
    config = repo.config_writer()

    if presult.show:
        if not config.has_section('gitlab'):
            print("GitLab instance is not configured for this repository")
            return

        print("GitLab instance config:")
        for pair in config.items('gitlab'):
            print("%s = \"%s\"" % pair)

        # Check each remote for a possible project id
        for r in repo.remotes:
            if config.has_option('remote "'+str(r)+'"', 'projectid'):
                projectid = config.get('remote "'+str(r)+'"', 'projectid')
                print('remote "%s" projectid = %s' % (str(r), projectid))
        return

    if presult.clear or presult.reset:
        # Check each remote for a possible project id and remove it
        for r in repo.remotes:
            config.remove_option('remote "'+str(r)+'"', 'projectid')

        config.remove_section('gitlab')
        config.write()
        print("Cleared the current GitLab instance config")

        if presult.reset == False:
            return

    # Check the gitlab section for proper settings
    try:
        config.add_section('gitlab')
    except:
        pass

    if presult.debug is not None:
        config.set('gitlab', 'debug', 'true' if presult.debug else 'false')
        config.write()

    if presult.sslverify is not None:
        config.set('gitlab', 'sslVerify', 'true' if presult.sslverify else
                'false')
        config.write()

    try:
        hosturl = config.get('gitlab', 'hosturl')
    except:
        defaulturl = 'https://gitlab.com'
        print("I need a gitlab hosturl to communicate with")
        sys.stdout.write("Host URL (default: %s): " % (defaulturl))
        sys.stdout.flush()
        hosturl = myinput()
        if not hosturl:
            hosturl = defaulturl
        config.set('gitlab', 'hosturl', hosturl)
        config.write()

    try:
        config.get('gitlab', 'apikey')
    except:
        print("I need an api key to access your gitlab instance")
        print("You can create one here: " + hosturl +
              "/profile/personal_access_tokens")
        apikey = ''
        while not apikey:
            sys.stdout.write("API KEY: ")
            sys.stdout.flush()
            apikey = myinput()
        config.set('gitlab', 'apikey', apikey)
        config.write()

    lab = RepoGitLab(repo)

    # Check each remote for a possible project id
    for r in repo.remotes:
        buildid = False
        try:
            config.get('remote "'+str(r)+'"','projectid')
        except:
            buildid = True

        if buildid:
            url = urlparse(config.get('remote "'+str(r)+'"','url'))
            projname = os.path.splitext(url.path)[0].strip('/')
            # check to see if this is an ssh url, and adjust it accordingly
            try:
                idx = projname.index(':')
                projname = projname[idx+1:]
            except ValueError:
                pass

            print("Looking for project id for %s" % projname)
            try:
                project = lab.projects.get(projname)
            except gitlab.exceptions.GitlabGetError as e:
                print("Skipping id lookup for remote \"%s\": %s" %
                        (str(r), e))
                continue

            print("Adding project id %d to remote \"%s\"" %
                    (project.id, str(r)))
            config.set('remote "'+str(r)+'"', 'projectid', project.id)

##########################################################################
# Main routine
##########################################################################


def main():
    from  signal import signal, SIGPIPE, SIG_DFL
    signal(SIGPIPE, SIG_DFL)

    #logging.basicConfig(filename='/tmp/gitlab.log',level=logging.DEBUG)

    # common parsers
    waitparser = argparse.ArgumentParser(add_help = False)
    waitparser.add_argument('-w', '--wait',
            dest = 'wait',
            action = 'store_true',
            help = 'Wait for ci pipeline to complete')
    remoteparser = argparse.ArgumentParser(add_help = False)
    remoteparser.add_argument('remote',
            default = None,
            nargs = "?",
            help = 'The remote to use (default: upstream tracking remote)')
    parentparser = argparse.ArgumentParser(add_help = False)
    parentparser.add_argument('-p', '--parent',
            dest = 'parent',
            action = 'store_true',
            help="Use the remote's parent project, rather than the " +
                "remote's project")
    searchparser = argparse.ArgumentParser(add_help = False)
    searchparser.add_argument('-k', '--keyword', '--search',
            dest = 'keyword',
            help = 'Filter results based on search KEYWORDs provided.')
    idparser = argparse.ArgumentParser(add_help = False)
    idparser.add_argument('id', help = 'Merge request id to use')


    # primary parser
    parser = argparse.ArgumentParser(
            description = "This git porcelain provides access to the " +
                "gitlab workflow via its REST API.",
            epilog = "TODO epilog")
    subcmds = parser.add_subparsers(
            title = "subcommands",
            description = "use --help on each subcommand to get usage",
            metavar = "CMD",
            )

    # setup command subparser
    parser_setup = subcmds.add_parser("setup",
            help = "Set up a git repository to work with a GitLab instance",
            description = "Execute gitlab setup on the local repo. " +
                "The porcelain will query the local repo config and " +
                "prompt for missing config items. It will also scan " +
                "all remotes that are configured and lookup their " +
                "project ids for use by the rest of the tool suite.")
    parser_setup.add_argument('-c', '--clear',
            dest = 'clear',
            default = False,
            action = 'store_true',
            help = "Clear GitLab config, i.e. unlink this repository " +
                "from a GitLab instance")
    parser_setup.add_argument('-r', '--reset',
            dest = 'reset',
            default = False,
            action = 'store_true',
            help = 'Perform a new set up overwriting the previous one')
    parser_setup.add_argument('-s', '--show',
            dest = 'show',
            default = False,
            action = 'store_true',
            help = 'Display the current GitLab config')
    parser_setup.add_argument('-v', '--ssl-verify',
            dest = 'sslverify',
            default = 0,
            type = int,
            choices = [0, 1],
            help = "Set SSL verification")
    parser_setup.add_argument('-d', '--debug',
            dest = 'debug',
            default = 0,
            type = int,
            choices = [0, 1],
            help = "Set debug mode")
    parser_setup.set_defaults(func = setupcommand)

    # fork command subparser
    # TODO: what do 'remote' and 'project' mean in the context
    # of forking a project on gitlab?
    parser_fork = subcmds.add_parser("fork",
            parents = [remoteparser],
            help = "Fork a project",
            description = "Execute a fork operation on your gitlab "+
                "instance with the following options")
    parser_fork.add_argument('project',
            help = 'Project name to fork')
    parser_fork.set_defaults(func = forkcommand)

    # raw command subparser
    parser_raw = subcmds.add_parser("raw",
            help = "Process raw command args",
            description = "Execute a raw REST api endpoint using the "+
                "following options.")
    parser_raw.add_argument('-p', '--post',
            dest = 'post',
            action = 'store_true',
            help='Indicate this is a POST operation')
    parser_raw.add_argument('-a', '--arguments',
            dest = 'arguments',
            type = json.loads,
            help = 'json encoded dict of arguments i.e. --arguments '+
                '\'{"state":"opened", "author_id":"user"}\'')
    parser_raw.add_argument('endpoint',
            help = 'specify the REST api endpoint to call')
    parser_raw.set_defaults(func = rawcommand)

    # listprojects command subparser
    parser_listprojects = subcmds.add_parser("listprojects",
            parents = [searchparser],
            help = "List/Search Projects",
            description = "TODO description")
    # lets by default not print every repo in the forge and only print
    # projects the user owns
    parser_listprojects.add_argument('-a', '--all',
            dest = 'owned',
            action = 'store_false',
            help = 'Display all projects in the forge, not just the ones'+
                ' you own')
    parser_listprojects.set_defaults(func = listprojects)

    # createmr command subparser
    parser_createmr = subcmds.add_parser("createmr",
            parents = [waitparser, remoteparser],
            help = "Create merge-request",
            description = "Create a merge-request to the parent project "+
                "of your fork. This command will open an editor to enter "+
                "your description of the merge-request.")
    parser_createmr.add_argument('--push',
            dest = 'push',
            action = 'store_true',
            help = 'Push local branch to remote fork before creating'+
                'merge-request')
    parser_createmr.add_argument('-f', '--force',
            dest = 'force',
            action = 'store_true',
            help = 'Override checks for branch sync')
    parser_createmr.add_argument('-m', '--message',
            dest = 'message',
            help = 'Merge-request message')
    parser_createmr.add_argument('refspec',
            default = None,
            nargs = "?",
            help = 'git-push like refspec')
    parser_createmr.set_defaults(func = createmrcommand)

    # updatemr command subparser
    # TODO: what are we updating about the merge-request? When one
    # pushes to their fork the merge-request is updated
    parser_updatemr = subcmds.add_parser("updatemr",
            parents = [waitparser, remoteparser, idparser],
            help = "Update merge-request",
            description = "Update an existing merge-request with the "+
                "latest local version of the branch. This is usefull "+
                "for posting subsequent versions of a patch after "+
                "reviews result in needed changes.")
    parser_updatemr.set_defaults(func = updatemrcommand)

    # listmr command subparser
    parser_listmr = subcmds.add_parser("listmr",
            parents = [parentparser, searchparser, remoteparser],
            help = "List merge-requests",
            description = "List merge-requests of the parent project "+
                "of your fork.")
    parser_listmr.add_argument('-s', '--state',
            dest = 'state',
            default = "opened",
            choices = ["opened", "closed", "locked", "merged"],
            help = 'State of the requests to list')
    parser_listmr.add_argument('-a', '--author',
            dest = 'author',
            default = None,
            help = 'List MRs opened by this user')
    parser_listmr.add_argument('-l', '--labels',
        dest = 'labels',
            default = None,
            help = 'list MRs containing this list of labels')
    parser_listmr.add_argument('-j', '--json',
        dest = 'usejson', action='store_true',
        default = False,
        help = 'output results as json file')
    parser_listmr.set_defaults(func = listmrcommand)

    # filter command subparser
    parser_filtermr = subcmds.add_parser("filtermr",
            parents = [],
            help = "Filter merge requests",
            description = "Policy filter merge requests as produced by listmr based"+
                " on specified policy")
    parser_filtermr.add_argument('-f', '--filter',
            dest = 'filter',
            default = None,
            help = 'specify filter module')
    parser_filtermr.add_argument('-l', '--list',
            dest = 'list',
            help = 'list available policy filters',
            default = False,
            action='store_true')
    parser_filtermr.add_argument('-i', '--info',
            dest = 'info',
            help = 'detailed information on policy filter',
            default = False,
            action='store_true')
  
    parser_filtermr.set_defaults(func = filtermrcommand)

    # closemr command subparser
    parser_closemr = subcmds.add_parser("closemr",
            parents = [parentparser, remoteparser, idparser],
            help = "Close a merge-request",
            description = "Close a merge-request that you have opened.")
    parser_closemr.set_defaults(func = closemrcommand)

    # review command subparser
    parser_review = subcmds.add_parser("review",
            help = "Conduct merge-request review",
            description = """Open the gitlab review ncurses tool. This tool provides a text user interface to browse and comment on open merge-requests. Navigation consists of:
    <-, ->, ^, v - Navigate columns/rows on a given screen
    <ENTER> - Select an issue/comment/commit to view details of
    <c|C> - Comment/Reply to a commit/discussion
    <f|F> - refresh screen, reloading from server
    <q|Q> - Go up one screen
    <r|R> - resolve discussion
    <u|U> - unresolve discussion
    <a|A> - Toggle approval status of an MR from the Top MR list screen
    <h|H> - screen context help""")
    parser_review.set_defaults(func = reviewcommand)

    argcomplete.autocomplete(parser)
    args = parser.parse_args()
    try:
        args.func(args)
    # TODO: handle keyboard exceptions
    except Exception as e:
        print(type(e), e)
        sys.exit(1)
    sys.exit(0)

if __name__ == '__main__':
    main()
