import os
from pathlib import Path
import re
import subprocess
import sys


def make_gnome_url(repo_name):
    return "https://gitlab.gnome.org/GNOME/{}.git".format(repo_name)


upstreams = {
    "meson": ("https://github.com/mesonbuild/meson.git", "master"),
    "termux-elf-cleaner": "https://github.com/termux/termux-elf-cleaner.git",
    "libiconv": "https://git.savannah.gnu.org/git/libiconv.git",
    "zlib": ("https://github.com/madler/zlib.git", "develop"),
    "brotli": "https://github.com/google/brotli.git",
    "minizip": "https://github.com/zlib-ng/minizip-ng.git",
    "libffi": "https://github.com/libffi/libffi.git",
    "libunwind": "https://github.com/libunwind/libunwind.git",
    "glib": make_gnome_url("glib"),
    "glib-networking": (make_gnome_url("glib-networking"), "master"),
    "libnice": "https://gitlab.freedesktop.org/libnice/libnice.git",
    "usrsctp": "https://github.com/sctplab/usrsctp.git",
    "libgee": make_gnome_url("libgee"),
    "json-glib": make_gnome_url("json-glib"),
    "libpsl": "https://github.com/rockdaboot/libpsl.git",
    "libxml2": make_gnome_url("libxml2"),
    "libsoup": make_gnome_url("libsoup"),
    "vala": make_gnome_url("vala"),
    "libdwarf": "https://github.com/davea42/libdwarf-code.git",
    "xz": "https://git.tukaani.org/xz.git",
    "pkg-config": "https://gitlab.freedesktop.org/pkg-config/pkg-config.git",
    "quickjs": ("https://github.com/bellard/quickjs.git", "master"),
    "gn": "https://gn.googlesource.com/gn",
    "v8": "https://chromium.googlesource.com/v8/v8",
    "capstone": ("https://github.com/capstone-engine/capstone.git", "v5"),
    "tinycc": "https://repo.or.cz/tinycc.git",
    "selinux": "https://github.com/SELinuxProject/selinux.git",
}


def sync(repo_path):
    repo_name = os.path.basename(repo_path)

    patches_path = os.path.join(str(Path.home()), ".frida-sync-" + re.sub(r"[^\w\d]", "-", repo_path.lower()).lstrip("-"))
    if os.path.exists(patches_path):
        patches = PendingPatches.load(patches_path)

        print("Applying {} pending patches".format(patches.count))
    else:
        entry = upstreams.get(repo_name, None)
        if entry is None:
            raise UnknownUpstreamError("Unknown upstream: {}".format(repo_name))
        if isinstance(entry, tuple):
            upstream_url, upstream_branch = entry
        else:
            upstream_url = entry
            upstream_branch = "main"
        upstream_target = "upstream/" + upstream_branch

        print("Synchronizing with {}".format(upstream_url))

        subprocess.run(["git", "checkout", "main"], cwd=repo_path, capture_output=True, check=True)
        subprocess.run(["git", "pull"], cwd=repo_path, capture_output=True, check=True)
        result = subprocess.run(["git", "status"], cwd=repo_path, capture_output=True, check=True, encoding='utf-8')
        if not "working tree clean" in result.stdout:
            raise WorkingTreeDirtyError("Working tree is dirty")

        subprocess.run(["git", "remote", "add", "upstream", upstream_url], cwd=repo_path, capture_output=True)
        subprocess.run(["git", "fetch", "upstream"], cwd=repo_path, check=True)

        patches, base = list_our_patches(repo_path)
        print("We have {} patches on top of upstream".format(patches.count))

        new_entries = list_upstream_changes(repo_path, upstream_target, base)
        if len(new_entries) == 0:
            print("Already up-to-date")
            return

        print("Upstream has {} new commits".format(len(new_entries)))

        print("Merging...")
        subprocess.run(["git", "merge", "-s", "ours", upstream_target], cwd=repo_path, capture_output=True, check=True)
        subprocess.run(["git", "checkout", "--detach", upstream_target], cwd=repo_path, capture_output=True, check=True)
        subprocess.run(["git", "reset", "--soft", "main"], cwd=repo_path, capture_output=True, check=True)
        subprocess.run(["git", "checkout", "main"], cwd=repo_path, capture_output=True, check=True)
        subprocess.run(["git", "commit", "--amend", "-C", "HEAD"], cwd=repo_path, capture_output=True, check=True)

        patches.save(patches_path)

    while True:
        index, cid, message = patches.try_pop()
        if index is None:
            break

        print("Cherry-picking {}/{}: {}".format(index + 1, patches.count, message))
        try:
            subprocess.run(["git", "cherry-pick", cid], cwd=repo_path, capture_output=True, encoding='utf-8', check=True)
        except subprocess.CalledProcessError as e:
            patches.save(patches_path)

            print("\n*** Unable to apply this patch:")
            print(e.stderr)
            print("Run `git cherry-pick --abort` and re-run script to skip it.")

            return

    os.remove(patches_path)
    print("Done!")

def list_our_patches(repo_path):
    items = []
    base = None
    entries = list_recent_commits(repo_path, "--max-count=1000")
    for index, entry in enumerate(entries):
        cid, message = entry
        if message.startswith("Merge"):
            base = entries[index + 1][0]
            break
        items.append(("pending", cid, message))
    items.reverse()
    return (PendingPatches(items), base)

def list_upstream_changes(repo_path, upstream_target, since):
    return list(reversed(list_recent_commits(repo_path, since + ".." + upstream_target)))

def list_recent_commits(repo_path, *args):
    result = subprocess.run(["git", "log", "--pretty=oneline", "--abbrev-commit", "--topo-order"] + list(args),
        cwd=repo_path, capture_output=True, check=True, encoding='utf-8', errors='surrogateescape')
    return [line.split(" ", 1) for line in result.stdout.rstrip().split("\n")]


class PendingPatches(object):
    def __init__(self, items):
        self._items = items

        offset = 0
        for status, cid, message in items:
            if status == "applied":
                offset += 1
            else:
                break
        self._offset = offset

    @property
    def count(self):
        return len(self._items)

    def try_pop(self):
        index = self._offset
        if index == len(self._items):
            return (None, None, None)

        _, cid, message = self._items[index]
        self._items[index] = ("applied", cid, message)
        self._offset += 1

        return (index, cid, message)

    @classmethod
    def load(cls, path):
        with open(path, "r", encoding='utf-8') as f:
            data = f.read()

        items = []
        for line in data.strip().split("\n"):
            status, cid, message = line.split(" ", maxsplit=2)
            items.append((status, cid, message))
        return PendingPatches(items)

    def save(self, path):
        data = "\n".join([" ".join(item) for item in self._items]) + "\n"
        with open(path, "w", encoding='utf-8') as f:
            f.write(data)


class WorkingTreeDirtyError(Exception):
    pass


class UnknownUpstreamError(Exception):
    pass


if __name__ == '__main__':
    sync(os.path.abspath(sys.argv[1]))
