import os
import git
import shutil
import sys
if sys.version_info.major > 2:
    int_type = int
else:
    int_type = (int, long)

##################################################

def gen_gitpath(path):
    return os.path.relpath(path, '/')

def git_hex2int(hexsha, digits=7):
    """Convert git hexidecimal commit id into an integer.

    Take the first 'digits' of the hex and return an int.  The default
    'digits' is 7, which is a "standard" short git hex length that
    should be relatively unique, and is 28 bits, which fortunately
    fits into a standard EPICS 32-bit signed int.

    """
    return int(hexsha[:digits], 16)

def git_int2hex(intsha, digits=7):
    """Convert integer into hex.

    The 'intsha' is converted to a hex string.  If hex is less than
    'digits' in length (default 7), the string is zero padded up to
    'digits'.

    """
    return hex(intsha)[2:].zfill(digits)

##################################################

class SystemArchiveGitError(Exception):
    def __init__(self, e):
        error = 'Not a valid object name'
        if error in str(e):
            super(SystemArchiveGitError, self).__init__(error)

class SystemArchiveGit(object):
    def __init__(self, root, rw=False):
        self.root = os.path.abspath(root)
        self.repo = None
        # config = repo.config_writer()
        # config.set_value("user", "email", "")
        # config.set_value("user", "name", "Guardian Daemon")

    def __enter__(self):
        self.repo = git.Repo.init(self.root)
        return self

    def __exit__(self, *args):
        del self.repo
        self.repo = None

    def commit(self, system, message):
        """Commit system code base to archive git repo.

        Arguments are system to be archived and a commit message.

        Returns SHA1 hex commit id of commit.

        """
        if not self.repo:
            raise SystemArchiveGitError("repo not open, use context manager")
        repo = self.repo
        root = repo.working_dir

        # clean up the working tree, in case a previous commit was
        # aborted or hung.
        # reset the index and working tree to the HEAD.
        try:
            repo.head.reset(index=True, working_tree=True)
        except:
            pass
        # remove any stale index locks
        # FIXME: this should not be necessary, and undermines the
        # purpose of the lock
        try:
            os.remove(os.path.join(root, '.git', 'index.lock'))
        except:
            pass

        # completely remove all files from the index, so we commit
        # only a clean set of files (to handle code removes)
        if repo.head.is_valid():
            for entry in repo.head.commit.tree.traverse():
                fullpath = os.path.join(root, entry.path)
                if os.path.isfile(fullpath) or os.path.islink(fullpath):
                    repo.index.remove([entry.path])
        # FIXME: should remove *all* files, not just those known to
        # index, e.g.:
        # for root, dirs, files in os.walk(root):
        #     dirs[:] = [d for d in dirs if d != '.git']
        #     ...

        # copy and add all usercode files
        for path in [system.path] + system.usercode:
            # store all files in the repo at their current global
            # system path location, with the git working directory as
            # the filesystem root
            gitpath = gen_gitpath(path)
            outpath = os.path.join(root, gitpath)
            outdir = os.path.dirname(outpath)
            if not os.path.exists(outdir):
                os.makedirs(outdir)
            shutil.copyfile(path, outpath)
            repo.index.add([gitpath])

        # add the guardian path as a PATH description in the
        # GUARD_MODULE_PATH file
        guardpath_file = os.path.join(root, 'GUARD_MODULE_PATH')
        with open(guardpath_file, 'w') as f:
            path = ':'.join([path[1:] for path in system.guardpath])
            f.write(path)
            f.write('\n')
        repo.index.add([guardpath_file])

        # if there are no differences detected, return None
        if repo.head.is_valid() and not repo.index.diff(repo.head.commit):
            return

        # commit the index
        repo.index.commit(message)

        # return the current commit hex SHA1
        return self.get_hexsha()

    def get_hexsha(self):
        return git.Repo.init(self.root).head.commit.hexsha

    def get_intsha(self, digits=7):
        return git_hex2int(self.get_hexsha(), digits=digits)

    def _resolve_id(self, id):
        if isinstance(id, int_type):
            return git_int2hex(id)
        else:
            return id

    def get_guardpath(self):
        with open(os.path.join(self.root, 'GUARD_MODULE_PATH')) as f:
            data = f.read()
        return [os.path.join(self.root, path) for path in data.split(':')]
