import os
import re
import difflib
from datetime import datetime

class VCS(object):
    def __init__(self, path):
        self._path = path
        self._ref = None
    def branches(self):
        return []
    def tags(self):
        return []
    def ref(self):
        return self._ref
    def log(self, commit=None, path=None, max=50, offset=0):
        return []

def create(type, path):
    cls = {
            0: Git,
    }.get(type, None)
    if not cls:
        raise Exception('Unknown VCS Type')
    return cls(path)

class Blob(object):
    def __init__(self, path, data):
        self.path = path
        self.data = data

class Diff(object):
    Types = {
            'Add': 0,
            'Delete': 1,
            'Rename': 2,
            'Modify': 3,
            }
    def __init__(self):
        self.a = None
        self.b = None
        self.type = 0
    def unified(self, context=3):
        a = self.a.data.split('\n') if self.a else []
        b = self.b.data.split('\n') if self.b else []
        diff = difflib.unified_diff(
                a,
                b,
                fromfile=self.a.path if self.a else '/dev/null',
                tofile=self.b.path if self.b else '/dev/null',
                n=context,
                lineterm='')
        return "\n".join(diff)
    def changes(self, context=3):
        "Parses the unified diff into a data structure for easy display"
        changes = []
        line_a = 0
        line_b = 0
        if context == None:
            context = max(
                    len(self.a.data.split('\n')) if self.a else 0,
                    len(self.b.data.split('\n')) if self.b else 0)
        for line in self.unified(context).split('\n')[2:]:
            if line.startswith('@@'):
                pattern = r'\-(\d+)(,\d+)? \+(\d+)(,\d+)?'
                info = re.findall(pattern, line)
                line_a = int(info[0][0])
                line_b = int(info[0][2])
                change = {
                        'type': '@',
                        'text': line,
                        'line_a': line_a,
                        'line_b': line_b,
                        }
                changes.append(change)
                continue
            type = line[0]
            text = line[1:]
            change = {
                    'type': type,
                    'text': text,
                    'line_a': line_a,
                    'line_b': line_b,
                    }
            if type == '+':
                line_b += 1
            elif type == '-':
                line_a += 1
            else:
                line_a += 1
                line_b += 1
            changes.append(change)
        return changes

class Commit(object):
    def __init__(self):
        self.id = None
        self.tree = None
        self.message = None
        self.author = None
        self.author_email = None
        self.committer = None
        self.committer_email = None
        self.authored_date = None
        self.committed_date = None
        self.parents = []

import git
class Git(VCS):
    type = 'Git'
    def __init__(self, path):
        super(Git, self).__init__(path);
        self._repo = git.Repo(self._path)
        self._branches = None
        self._tags = None

        # Set default branch ref
        if 'master' in self.branches():
            self._ref = 'master'
        else:
            self._ref = self.branches()[0]
    def branches(self):
        if not self._branches:
            self._branches = dict([(head.name, self.commit(head.commit)) for head in self._repo.heads])
        return self._branches
    def tags(self):
        if not self._tags:
            self._tags = dict([(tag.name, self.commit(tag.commit)) for tag in self._repo.tags])
        return self._tags
    def commit(self, commit):
        if type(commit) in [str, unicode]:
            commit = self._repo.commit(commit)
        c = Commit()
        c.id = commit.hexsha
        c.tree = commit.tree.hexsha
        c.message = commit.message
        c.author = commit.author.name
        c.author_email = commit.author.email
        c.committer = commit.committer.name
        c.committer_email = commit.committer.email
        c.authored_date = datetime.fromtimestamp(commit.authored_date)
        c.committed_date = datetime.fromtimestamp(commit.committed_date)
        c.parents = [parent.hexsha for parent in commit.parents]
        return c
    def log(self, commit=None, path=None, max=50, offset=0):
        commit = commit if commit else self._ref
        result = []
        for c in self._repo.iter_commits(rev=commit, paths=path, max_count=max,
                skip=offset):
            result.append(self.commit(c))
        return result
    def diff(self, b, a=None):
        """Get the diff for ref b from ref a

        If ref a is not provided, b's parent refs will be used.

        *** Note: The parameter order is backwards, since we default the origin
        ref to the target's parents.
        """
        result = []
        b = self._repo.commit(b)
        if not a:
            # FIXME:
            # Only using the first parent for now. Some merge commits seem to be
            # causing nasty problems, while others diff just fine.
            a = b.parents[:1]
        else:
            a = self._repo.commit(a)
        if a:
            diffs = b.diff(a)
        else:
            # No parents, use the default behaviour (safe for bare repos)
            diffs = b.diff()
        for diff in diffs:
            # b and a are swapped so the parent diff will work as a list of
            # parents. Therefore, we'll swap them back when we put them into our
            # Diff object.
            d = Diff()
            if diff.a_blob: d.b = Blob(diff.a_blob.path,
                    diff.a_blob.data_stream.read())
            if diff.b_blob: d.a = Blob(diff.b_blob.path,
                    diff.b_blob.data_stream.read())
            result.append(d)
        return result
    def browse(self, commit=None, path=''):
        if not commit:
            commit = self.ref()
        files = []
        dirs = []

        # Locate the tree matching the requested path
        tree = self._repo.commit(commit).tree
        if path:
            for i in tree.traverse():
                if type(i) == git.objects.Tree and i.path == path:
                    tree = i
        if path != tree.path:
            raise Exception('Path not found')

        for node in tree:
            if type(node) == git.objects.Blob:
                files.append(node.path)
            elif type(node) == git.objects.Tree:
                dirs.append(node.path)
        return dirs, files
    def blob(self, commit, path):
        tree = self._repo.commit(commit).tree
        dir = os.path.dirname(path)
        if dir:
            for i in tree.traverse():
                if type(i) == git.objects.Tree and i.path == dir:
                    tree = i
        if dir != tree.path:
            raise Exception('Path not found')
        for node in tree:
            if type(node) == git.objects.Blob and node.path == path:
                return Blob(node.path, node.data_stream.read())
        raise Exception('Blob Path not found')
if __name__ == '__main__':
    g = Git('/home/correlr/code/voiceaxis')