# overlay classes for repositories
# unifies access to unimported git objects and committed hg objects
# designed to support incoming
#
# incomplete, implemented on demand

from __future__ import generator_stop

from mercurial import (
    ancestor,
    changelog,
    context,
    exthelper,
    manifest,
    match as matchmod,
    namespaces,
    node,
    util,
)
from mercurial.utils import stringutil
from mercurial.node import bin, hex, nullid

from dulwich.refs import (
    LOCAL_BRANCH_PREFIX,
    LOCAL_TAG_PREFIX,
)

from . import compat

eh = exthelper.exthelper()


def _maybehex(n):
    if len(n) == 20:
        return hex(n)
    return n


class overlaymanifest(object):
    def __init__(self, repo, sha):
        self.repo = repo
        self.tree = repo.handler.git.get_object(sha)
        self._map = None
        self._flags = None

    def withflags(self):
        self.load()
        return {path for path, flag in self._flags.items() if flag != b''}

    def copy(self):
        return overlaymanifest(self.repo, self.tree.id)

    def keys(self):
        self.load()
        return self._map.keys()

    def iterkeys(self):
        return iter(self.keys())

    def load(self):
        if self._map is not None:
            return

        self._map = {}
        self._flags = {}

        def hgflag(gitflag):
            if gitflag & 0o100:
                return b'x'
            elif gitflag & 0o20000:
                return b'l'
            else:
                return b''

        def addtree(tree, dirname):
            for entry in tree.items():
                if entry.mode & 0o40000:
                    # expand directory
                    subtree = self.repo.handler.git.get_object(entry.sha)
                    addtree(subtree, dirname + entry.path + b'/')
                else:
                    path = dirname + entry.path
                    self._map[path] = bin(entry.sha)
                    self._flags[path] = hgflag(entry.mode)

        addtree(self.tree, b'')

    def matches(self, match):
        '''generate a new manifest filtered by the match argument'''
        if match.always():
            return self.copy()

        mf = self.copy()
        for fn in mf.keys():
            if not match(fn):
                del mf[fn]
        return mf

    def iteritems(self):
        self.load()
        return self._map.items()

    def __iter__(self):
        self.load()
        return self._map.__iter__()

    def __getitem__(self, path):
        self.load()
        return self._map[path]

    def __contains__(self, path):
        self.load()
        return path in self._map

    def get(self, path, default=None):
        self.load()
        return self._map.get(path, default)

    def flags(self, key, default=''):
        return self._flags.get(key, default)

    def diff(self, m2, match=None, clean=False):
        self.load()
        if isinstance(m2, overlaymanifest):
            m2.load()

        # below code copied from manifest.py:manifestdict.diff
        diff = {}

        if match is None:
            match = matchmod.always(b'', b'')
        for fn, n1 in self.iteritems():
            if not match(fn):
                continue
            fl1 = self._flags.get(fn)
            n2 = m2.get(fn, None)
            fl2 = m2.flags(fn)
            if n2 is None:
                fl2 = b''
            if n1 != n2 or fl1 != fl2:
                diff[fn] = ((n1, fl1), (n2, fl2))
            elif clean:
                diff[fn] = None

        for fn, n2 in m2.iteritems():
            if fn not in self:
                if not match(fn):
                    continue
                fl2 = m2.flags(fn)
                diff[fn] = ((None, b''), (n2, fl2))

        return diff

    def __delitem__(self, path):
        del self._map[path]


@eh.wrapfunction(manifest.manifestdict, b'diff')
def wrapmanifestdictdiff(orig, self, m2, match=None, clean=False):
    '''avoid calling into lazymanifest code if m2 is an overlaymanifest'''
    # Older mercurial clients used diff(m2, clean=False). If a caller failed
    # to specify clean as a keyword arg, it might get passed as match here.
    if isinstance(match, bool):
        clean = match
        match = None

    kwargs = {'clean': clean}
    # Older versions of mercurial don't support the match arg, so only add it
    # if it exists.
    if match is not None:
        kwargs['match'] = match
    if isinstance(m2, overlaymanifest):
        diff = m2.diff(self, **kwargs)
        # since we calculated the diff with m2 vs m1, flip it around
        for fn in diff:
            c1, c2 = diff[fn]
            diff[fn] = c2, c1
        return diff
    else:
        return orig(self, m2, **kwargs)


class overlayfilectx(object):
    def __init__(self, repo, path, fileid=None):
        self._repo = repo
        self._path = path
        self.fileid = fileid

    def repo(self):
        return self._repo

    # this is a hack to skip copy detection
    def ancestors(self):
        return [self, self]

    def filenode(self):
        return nullid

    def rev(self):
        return -1

    def path(self):
        return self._path

    def filelog(self):
        return self.fileid

    def data(self):
        blob = self._repo.handler.git.get_object(_maybehex(self.fileid))
        return blob.data

    def isbinary(self):
        return stringutil.binary(self.data())


class overlaychangectx(context.changectx):
    def __init__(self, repo, sha, maybe_filtered=True):
        # Can't store this in self._repo because the base class uses that field
        self._hgrepo = repo
        if isinstance(sha, bytes):
            pass
        elif isinstance(sha, str):
            sha = sha.encode('ascii')
        else:
            sha = sha.hex()
        self.commit = repo.handler.git.get_object(_maybehex(sha))
        self._overlay = getattr(repo, 'gitoverlay', repo)
        self._rev = self._overlay.rev(bin(self.commit.id))
        self._maybe_filtered = maybe_filtered

    def __getattr__(self, name):
        # Since hg 4.6, context.changectx doesn't define self._repo,
        # but it is still used by self.obsolete() (and friends)
        # So, if the attribute wasn't found, fallback to _hgrepo
        if name == '_repo':
            return self._hgrepo
        return super().__getattr__(name)

    def repo(self):
        return self._hgrepo

    def node(self):
        return bin(self.commit.id)

    def rev(self):
        return self._rev

    def date(self):
        return self.commit.author_time, self.commit.author_timezone

    def branch(self):
        return b'default'

    def user(self):
        return self.commit.author

    def files(self):
        return []

    def extra(self):
        return {}

    def description(self):
        return self.commit.message

    @util.propertycache
    def _parents(self):
        cl = self.repo().changelog
        parents = cl.parents(cl.node(self._rev))
        if not parents:
            return [self.repo()[b'null']]
        if parents[1] == nullid:
            parents = parents[:-1]
        return [self.repo()[sha] for sha in parents]

    def manifestnode(self):
        return bin(self.commit.tree)

    def hex(self):
        return self.commit.id

    def tags(self):
        return []

    def bookmarks(self):
        return []

    def manifest(self):
        return overlaymanifest(self._overlay, self.commit.tree)

    def filectx(self, path, filelog=None):
        mf = self.manifest()
        return overlayfilectx(self._overlay, path, mf[path])

    def flags(self, path):
        mf = self.manifest()
        return mf.flags(path)

    def __bool__(self):
        return True

    def phase(self):
        try:
            from mercurial import phases

            return phases.draft
        except (AttributeError, ImportError):
            return 1

    def totuple(self):
        return (
            self.commit.tree,
            self.user(),
            self.date(),
            self.files(),
            self.description(),
            self.extra(),
        )


class overlayrevlog(object):
    def __init__(self, repo, base):
        self.repo = repo
        self.base = base

    def parents(self, n):
        gitrev = self.repo.revmap.get(n)
        if gitrev is None:
            # we've reached a revision we have
            return self.base.parents(n)
        commit = self.repo.handler.git.get_object(_maybehex(n))

        if not commit.parents:
            return [nullid, nullid]

        def gitorhg(n):
            hn = self.repo.handler.map_hg_get(hex(n))
            if hn is not None:
                return bin(hn)
            return n

        # currently ignores the octopus
        p1 = gitorhg(bin(commit.parents[0]))
        if len(commit.parents) > 1:
            p2 = gitorhg(bin(commit.parents[1]))
        else:
            p2 = nullid

        return [p1, p2]

    def ancestor(self, a, b):
        anode = self.repo.nodemap.get(a)
        bnode = self.repo.nodemap.get(b)
        if anode is None and bnode is None:
            return self.base.ancestor(a, b)
        ancs = ancestor.ancestors(self.parentrevs, a, b)
        if ancs:
            return min(map(self.node, ancs))
        return nullid

    def parentrevs(self, rev):
        return [self.rev(p) for p in self.parents(self.node(rev))]

    def node(self, rev):
        gitnode = self.repo.nodemap.get(rev)
        if gitnode is None:
            return self.base.node(rev)
        return gitnode

    def rev(self, n):
        gitrev = self.repo.revmap.get(n)
        if gitrev is None:
            return self.base.rev(n)
        return gitrev

    def __len__(self):
        return len(self.repo.handler.repo) + len(self.repo.revmap)


class overlaymanifestrevlog(overlayrevlog):
    pass


class overlaymanifestctx(object):
    def __init__(self, repo, node):
        self._repo = repo
        self._node = node

    def read(self):
        return overlaymanifest(self._repo, self._node)


class overlaymanifestlog(manifest.manifestlog):
    def __init__(self, repo):
        self._repo = repo

    def get(self, dir, node):
        if dir:
            raise RuntimeError(b"hggit doesn't support treemanifests")
        if node == nullid:
            return manifest.manifestctx()
        return overlaymanifestctx(self._repo, node)


class overlaychangelog(overlayrevlog):
    def read(self, sha):
        if isinstance(sha, int):
            sha = self.node(sha)
        if sha == nullid:
            return (nullid, b"", (0, 0), [], b"", {})
        try:
            return self.base.read(sha)
        except LookupError:
            return overlaychangectx(self.repo, sha).totuple()

    def changelogrevision(self, noderev):
        values = self.read(noderev)
        return changelog._changelogrevision(
            manifest=values[0],
            user=values[1],
            date=values[2],
            files=values[3],
            description=values[4],
            extra=values[5],
        )


class overlaygithandler(object):
    # This was added to support the {gitnode} keyword in templates for incoming.
    # For this, falling back to the underlying repository is not necessary.

    def __init__(self, repo):
        self.repo = repo

    def map_git_get(self, sha):
        # Normally, the parameter contains the hg hash. However, for incoming
        # changesets, it contains the git hash, so we can return it as-is
        # (after sanity-checking that it is the hash of an incoming commit).
        assert node.bin(sha) in self.repo.revmap
        return sha


class overlayrepo(object):
    def __init__(self, handler, commits, refs):
        self.handler = handler
        self._activebookmark = None

        self.changelog = overlaychangelog(self, handler.repo.changelog)
        self.manifestlog = overlaymanifestlog(self)
        self.nodeconstants = compat.sha1nodeconstants

        # for incoming -p
        self.root = handler.repo.root
        self.getcwd = handler.repo.getcwd
        # self.status = handler.repo.status
        self.ui = handler.repo.ui

        self.revmap = None
        self.nodemap = None
        self.refmap = None
        self.tagmap = None

        self._makemaps(commits, refs)

        self.names = namespaces.namespaces()

        self.githandler = overlaygithandler(self)

    def _constructmanifest(self):
        return overlaymanifestrevlog(
            self, self.handler.repo._constructmanifest()
        )

    def __getitem__(self, n):
        if isinstance(n, int):
            n = self.changelog.node(n)
        if n not in self.revmap:
            return self.handler.repo[n]
        return overlaychangectx(self, n)

    def _handlerhack(self, method, *args, **kwargs):
        nothing = object()
        r = self.handler.repo
        oldhandler = getattr(r, 'handler', nothing)
        oldoverlay = getattr(r, 'gitoverlay', nothing)
        r.handler = self.handler
        r.gitoverlay = self
        try:
            return getattr(r, method)(*args, **kwargs)
        finally:
            if oldhandler is nothing:
                del r.handler
            else:
                r.handler = oldhandler
            if oldoverlay is nothing:
                del r.gitoverlay
            else:
                r.gitoverlay = oldoverlay

    def status(self, *args, **kwargs):
        return self._handlerhack(b'status', *args, **kwargs)

    def node(self, n):
        """Returns an Hg or Git hash for the specified Git hash"""
        if bin(n) in self.revmap:
            return n
        return self.handler.map_hg_get(n)

    def nodebookmarks(self, n):
        return self.refmap.get(n, [])

    def nodetags(self, n):
        return self.tagmap.get(n, [])

    def rev(self, n):
        return self.revmap[n]

    def filectx(self, path, fileid=None):
        return overlayfilectx(self, path, fileid=fileid)

    def unfiltered(self):
        return self.handler.repo.unfiltered()

    def _makemaps(self, commits, refs):
        baserev = self.handler.repo[b'tip'].rev()
        self.revmap = {}
        self.nodemap = {}
        for i, n in enumerate(commits):
            rev = baserev + i + 1
            self.revmap[n] = rev
            self.nodemap[rev] = n

        self.refmap = {}
        self.tagmap = {}
        for ref in refs:
            if ref.startswith(LOCAL_BRANCH_PREFIX):
                refname = ref[len(LOCAL_BRANCH_PREFIX) :]
                self.refmap.setdefault(bin(refs[ref]), []).append(refname)
            elif ref.startswith(LOCAL_TAG_PREFIX):
                tagname = ref[len(LOCAL_TAG_PREFIX) :]
                self.tagmap.setdefault(bin(refs[ref]), []).append(tagname)

    def narrowmatch(self, *args, **kwargs):
        return self.handler.repo.narrowmatch(*args, **kwargs)
