#!/usr/bin/env python
from __future__ import print_function

import os
import sys
import termenu

__version__ = "0.3.5"

def popen(command):
    with os.popen(command) as f:
        lines = f.readlines()
    return lines

GITBIN = popen("which git")[0].strip()

class RemoteBranch(str): pass
class LocalBranch(str): pass
class ModifiedFile(str): pass
class DeletedFile(str): pass
class UntrackedFile(str): pass
class CachedFile(str): pass
class CacheDeletedFile(str): pass
class BothModifiedFile(str): pass
class BothAddedFile(str): pass

if sys.version_info[0] >= 3:
    import builtins
    def map(func, *iterable):
        return list(builtins.map(func, *iterable))

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:
            message = "Running via gitter v%s (http://github/elifiner/gitter)\n" % __version__
            print(termenu.ansi.colorize(message, "white", bright=True), file=sys.stderr)
            return self._exec_git(self.args)
        self._run()

    def _run(self):
        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("-") and arg != "--"])

        # 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 self._system(command)

    def _system(self, command):
        return os.system(command)

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

    # 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 ", both=False):
        lines = popen("%s status --porcelain" % GITBIN)
        if both:
            files = [line[2:].strip() for line in lines if line[1] in workspaceStatuses and line[0] in indexStatuses]
        else:
            files = [line[2:].strip() for line in lines if line[1] in workspaceStatuses or line[0] in indexStatuses]
        files = [file.split("->")[1].strip() if "->" in file else file for file in files]
        root = popen("%s rev-parse --show-toplevel" % GITBIN)[0].strip()
        files = [os.path.relpath(os.path.os.path.join(root, file)) for file in files]
        return list(files)

    def _list_local_branches(self, sort_by='-creatordate'):
        branches = popen("%s branch --sort %s" % (GITBIN, sort_by))
        branches = [b.strip("*").strip() for b in branches]
        branches = [LocalBranch(b) for b in branches]
        return self._with_header("Local Branches", branches)

    def _list_remote_branches(self, sort_by='-creatordate'):
        branches = popen("%s branch -r --sort %s" % (GITBIN, sort_by))
        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", branches)

    def _list_modified(self):
        files = map(ModifiedFile, self._status_files("MT", ""))
        return self._with_header("Modified", files)

    def _list_deleted(self):
        files = map(DeletedFile, self._status_files("D", ""))
        return self._with_header("Deleted", files)

    def _list_untracked(self):
        files = map(UntrackedFile, self._status_files("?", ""))
        return self._with_header("Untracked", files)

    def _list_cached(self):
        files = map(CachedFile, self._status_files("", "MA"))
        return self._with_header("Cached", files)

    def _list_cache_deleted(self):
        files = map(CacheDeletedFile, self._status_files("", "D"))
        return self._with_header("Cache Deleted", files)

    def _list_both_modified(self):
        files = map(BothModifiedFile, self._status_files("U", "U", True))
        return self._with_header("Both Modified", files)

    def _list_both_added(self):
        files = map(BothAddedFile, self._status_files("A", "A", True))
        return self._with_header("Both Added", files)

    def _list_tracked(self):
        lines = popen("%s ls-files" % GITBIN)
        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 = popen("%s %s %s -100" % (GITBIN, cmd, branch))
        lines = map(str.strip, lines)
        return lines

    def _get_selected_types(self, selected):
        return list(set(type(s) for s in selected))

    # Command abstractions

    def _command_with_menu(self, options, multiselect):
        if not options:
            return
        selected = self._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 = self._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]
        selected = list(reversed(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 = self._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", "/".join(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_both_added() + self._list_untracked(), multiselect=True)

    def _do_rebase(self):
        self._assert_repo()
        if "-i" in self.flags or "--interactive" in self.flags:
            return self._command_with_hash()
        elif not self.args:
            branches = self._list_local_branches() + self._list_remote_branches()
            return self._command_with_menu(branches, multiselect=False)
        else:
            return self._exec_default()

    def _do_reset(self):
        self._assert_repo()
        if self.args:
            return self._exec_default()
        self.args += ['HEAD']
        return self._command_with_menu(self._list_cached() + self._list_cache_deleted() + self._list_both_modified(), 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()
        if self.flags:
            return self._exec_default()
        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()
        if self.flags:
            return self._exec_default()
        branches = self._list_local_branches() + self._list_remote_branches()
        branch = self._show_menu(branches, multiselect=False)
        if branch:
            return self._command_with_hash(branch, multiselect=True)

    def _do_revert(self):
        self._assert_repo()
        if self.flags:
            return self._exec_default()
        return self._command_with_hash()

    def _do_diff(self):
        self._assert_repo()
        if self.args:
            return self._exec_default()
        options = self._list_cached() + self._list_modified() + self._list_both_modified()
        if not options:
            return
        selected = self._show_menu(options, multiselect=True)
        if not selected:
            return
        types = self._get_selected_types(selected)
        if len(types) > 1:
            raise Exception("can show diffs for cached files or modified files, but not both")
        if types[0] == CachedFile:
            self.args += ["--cached"]
        return self._exec_git([self.command] + self.args + selected)

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