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

GITBIN = os.popen("which git").read().strip()

def show_menu(options, multiselect, precolored=False):
    return termenu.show_menu("", options, multiselect=multiselect, precolored=precolored, height=15)

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:
            print >>sys.stderr, termenu.ansi.colorize("Running via gitter v%s (http://github/gooli/gitter)\n" % version.version, "white", bright=True)
            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 _assert_repo(self):
        result = os.system("%s status > /dev/null 2> /dev/null" % GITBIN)
        if result != 0:
            raise Exception("not a git repository")

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

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

    # Lists of items to show in a menu

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

    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
        if not multiselect:
            selected = [selected]
        return self._exec_git([self.command] + self.args + selected)

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

    # Command handlers

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

    def _do_add(self):
        self._assert_repo()
        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):
        self._assert_repo()
        return self._command_with_menu(self._list_to_be_committed(), multiselect=True)

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

    def _do_branch(self):
        self._assert_repo()
        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):
        self._assert_repo()
        return self._command_with_menu(self._list_local_branches(), multiselect=False)

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

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

    def _do_cherry_pick(self):
        self._assert_repo()
        branches = self._list_local_branches() + self._list_remote_branches()
        branch = show_menu(branches, multiselect=False)
        if branch:
            return self._command_with_hash(branch, multiselect=True)

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)
