#!/usr/bin/env python
import os
import sys
import termenu

GITBIN = "/usr/bin/git"

def show_menu(options, multiselect, precolored=False):
    plugins = [termenu.FilterPlugin(), termenu.HeaderPlugin()]
    if precolored:
        plugins.append(termenu.Precolored())
    return termenu.Termenu(options, multiselect=multiselect, height=15, plugins=plugins).show()

class RemoteBranch(str):
    pass

class LocalBranch(str):
    pass

class GitRunner(object):
    def __init__(self, args):
        self._debug = False
        if args and args[0] == "--gitter-debug":
            self._debug = True
            args = args[1:]
        self.args = args

    def run(self):
        if not self.args or "--help" in self.args:
            return self._exec_git(self.args)
        self.command = self.args[0]
        self.args = self.args[1:]
        self.freeArgs = [arg for arg in self.args if not arg.startswith("-")]
        self.flags = set([arg for arg in self.args if arg.startswith("-")])

        # If free argument provided, run git as is
        if self.freeArgs:
            return self._exec_default()

        # Dispatch the git command to a specified handler
        func = "_do_" + self.command.replace("-", "_")
        if hasattr(self, func):
            return getattr(self, func)()
        else:
            self._exec_default()

    def _exec_default(self):
        return self._exec_git([self.command] + self.args)

    def _exec_git(self, args):
        args = ['"%s"' % arg if " " in arg else arg for arg in args]
        command = " ".join([GITBIN] + args)
        if self._debug:
            print command
        else:
            return os.system(command)

    # Lists of items to show in a menu

    def _with_header(self, header, lines):
        return [termenu.Header(header)] + lines if lines else lines

    def _status_files(self, workspaceStatuses="?M ", indexStatuses="?M "):
        lines = os.popen("%s status --porcelain" % GITBIN).readlines()
        files = [line[2:].strip() for line in lines if line[1] in workspaceStatuses or line[0] in indexStatuses]
        root = os.popen("%s rev-parse --show-toplevel" % GITBIN).read().strip()
        files = [os.path.relpath(os.path.os.path.join(root, file)) for file in files]
        return list(files)

    def _list_local_branches(self):
        branches = os.popen("%s branch" % GITBIN).readlines()
        branches = [b.strip("*").strip() for b in branches]
        branches = [LocalBranch(b) for b in branches]
        return self._with_header("Local Branches", sorted(branches))

    def _list_remote_branches(self):
        branches = os.popen("%s branch -r" % GITBIN).readlines()
        branches = [b.strip() for b in branches]
        branches = [b for b in branches if "->" not in b]
        branches = [RemoteBranch(b) for b in branches]
        return self._with_header("Remote Branches", sorted(branches))

    def _list_modified(self):
        return self._with_header("Modified", self._status_files("M", ""))

    def _list_deleted(self):
        return self._with_header("Deleted", self._status_files("D", ""))

    def _list_untracked(self):
        return self._with_header("Untracked", self._status_files("?", ""))

    def _list_to_be_committed(self):
        return self._with_header("To be committed", self._status_files("", "MA"))

    def _list_both_modified(self):
        return self._with_header("Both modified", self._status_files("", "U"))

    def _list_tracked(self):
        lines = os.popen("%s ls-files" % GITBIN).readlines()
        return self._with_header("Tracked", map(str.strip, lines))

    def _list_commits(self, branch):
        cmd = "log --color --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)%cn%Creset' --abbrev-commit --date=relative"
        lines = os.popen("%s %s %s -100" % (GITBIN, cmd, branch)).readlines()
        lines = map(str.strip, lines)
        return lines

    # Command abstractions

    def _command_with_menu(self, options, multiselect):
        if not options:
            return
        selected = show_menu(options, multiselect=multiselect)
        if not selected:
            return
        return self._exec_git([self.command] + self.args + selected)

    def _command_with_hash(self, branch=""):
        options = self._list_commits(branch)
        if not options:
            return
        selected = show_menu(options, multiselect=False, precolored=True)
        if not selected:
            return
        selected = selected[0].split(" ", 1)[0]
        return self._exec_git([self.command] + self.args + [selected])

    # Command handlers

    def _do_checkout(self):
        options = self._list_modified() + self._list_deleted() + self._list_local_branches() + self._list_remote_branches()
        selected = show_menu(options, multiselect=True)
        if not selected:
            return
        for s in selected:
            if isinstance(s, (RemoteBranch, LocalBranch)) and len(selected) > 1:
                raise Exception("more than one branch selected")
        if isinstance(selected[0], RemoteBranch):
            selected = ["-b", selected[0].split("/")[-1], selected[0]]
        return self._exec_git([self.command] + self.args + selected)

    def _do_add(self):
        if "-i" in self.flags or "--interactive" in self.flags:
            return self._exec_default()
        return self._command_with_menu(self._list_modified() + self._list_both_modified() + self._list_untracked(), multiselect=True)

    def _do_reset(self):
        return self._command_with_menu(self._list_to_be_committed(), multiselect=True)

    def _do_show(self):
        return self._command_with_hash()

    def _do_branch(self):
        if "-D" in self.flags or "-d" in self.flags:
            return self._command_with_menu(self._list_local_branches(), multiselect=True)
        else:
            return self._exec_default()

    def _do_merge(self):
        return self._command_with_menu(self._list_local_branches(), multiselect=False)

    def _do_rm(self):
        return self._command_with_menu(self._list_deleted() + self._list_tracked(), multiselect=True)

    def _do_clean(self):
        return self._command_with_menu(self._list_untracked(), multiselect=True)

    def _do_cherry_pick(self):
        branches = self._list_local_branches()
        branch = show_menu(branches, multiselect=False)
        return self._command_with_hash(branch[0])

if __name__ == "__main__":
    try:
        ret = GitRunner(sys.argv[1:]).run()
    except Exception, e:
        print "gitter error: " + str(e)
        sys.exit(1)
    sys.exit(ret or 127)
