import os
from six.moves import cStringIO as StringIO
from fnmatch import fnmatch

from . import vcs
from .log import get_logger
from .utils import is_blacklisted, rel_path_to_url

def chunks(data, n):
    for i in range(0, len(data) - 1, n):
        yield data[i:i+n]

class TestTree(object):
    def __init__(self, tests_root, url_base):
        self.tests_root = tests_root
        self.url_base = url_base
        self.logger = get_logger()

    def current_rev(self):
        pass

    def local_changes(self):
        pass

    def committed_changes(self, base_rev=None):
        pass


class GitTree(TestTree):
    def __init__(self, tests_root, url_base):
        TestTree.__init__(self, tests_root, url_base)
        self.git = self.setup_git()

    def setup_git(self):
        assert vcs.is_git_repo(self.tests_root)
        return vcs.get_git_func(self.tests_root)

    def current_rev(self):
        return self.git("rev-parse", "HEAD").strip()

    def local_changes(self, path=None):
        # -z is stable like --porcelain; see the git status documentation for details
        cmd = ["status", "-z", "--ignore-submodules=all"]
        if path is not None:
            cmd.extend(["--", path])

        rv = {}

        data = self.git(*cmd)
        if data == "":
            return rv

        assert data[-1] == "\0"
        f = StringIO(data)

        while f.tell() < len(data):
            # First two bytes are the status in the stage (index) and working tree, respectively
            staged = f.read(1)
            worktree = f.read(1)
            assert f.read(1) == " "

            if staged == "R":
                # When a file is renamed, there are two files, the source and the destination
                files = 2
            else:
                files = 1

            filenames = []

            for i in range(files):
                filenames.append("")
                char = f.read(1)
                while char != "\0":
                    filenames[-1] += char
                    char = f.read(1)

            if not is_blacklisted(rel_path_to_url(filenames[0], self.url_base)):
                rv.update(self.local_status(staged, worktree, filenames))

        return rv

    def committed_changes(self, base_rev=None):
        if base_rev is None:
            self.logger.debug("Adding all changesets to the manifest")
            return [(item, "modified") for item in self.paths()]

        self.logger.debug("Updating the manifest from %s to %s" % (base_rev, self.current_rev()))
        rv = []
        data  = self.git("diff", "-z", "--name-status", base_rev + "..HEAD")
        items = data.split("\0")
        for status, filename in chunks(items, 2):
            if is_blacklisted(rel_path_to_url(filename, self.url_base)):
                continue
            if status == "D":
                rv.append((filename, "deleted"))
            else:
                rv.append((filename, "modified"))
        return rv

    def paths(self):
        data = self.git("ls-tree", "--name-only", "--full-tree", "-r", "HEAD")
        return [item for item in data.split("\n") if not item.endswith(os.path.sep)]

    def local_status(self, staged, worktree, filenames):
        # Convert the complex range of statuses that git can have to two values
        # we care about; "modified" and "deleted" and return a dictionary mapping
        # filenames to statuses

        rv = {}

        if (staged, worktree) in [("D", "D"), ("A", "U"), ("U", "D"), ("U", "A"),
                                  ("D", "U"), ("A", "A"), ("U", "U")]:
            raise Exception("Can't operate on tree containing unmerged paths")

        if staged == "R":
            assert len(filenames) == 2
            dest, src = filenames
            rv[dest] = "modified"
            rv[src] = "deleted"
        else:
            assert len(filenames) == 1

            filename = filenames[0]

            if staged == "D" or worktree == "D":
                # Actually if something is deleted in the index but present in the worktree
                # it will get included by having a status of both "D " and "??".
                # It isn't clear whether that's a bug
                rv[filename] = "deleted"
            elif staged == "?" and worktree == "?":
                # A new file. If it's a directory, recurse into it
                if os.path.isdir(os.path.join(self.tests_root, filename)):
                    if filename[-1] != '/':
                        filename += '/'
                    rv.update(self.local_changes(filename))
                else:
                    rv[filename] = "modified"
            else:
                rv[filename] = "modified"

        return rv

class NoVCSTree(TestTree):
    """Subclass that doesn't depend on git"""

    ignore = ["*.py[c|0]", "*~", "#*"]

    def current_rev(self):
        return None

    def local_changes(self):
        # Put all files into local_changes and rely on Manifest.update to de-dupe
        # changes that in fact committed at the base rev.

        rv = []
        for dir_path, dir_names, filenames in os.walk(self.tests_root):
            for filename in filenames:
                if any(fnmatch(filename, pattern) for pattern in self.ignore):
                    continue
                rel_path = os.path.relpath(os.path.join(dir_path, filename),
                                           self.tests_root)
                if is_blacklisted(rel_path_to_url(rel_path, self.url_base)):
                    continue
                rv.append((rel_path, "modified"))
        return dict(rv)

    def committed_changes(self, base_rev=None):
        return None
