#!/usr/bin/env python

import argparse
import os
import re
import subprocess

from enum import Enum
from jinja2 import Template
from os import path
from pygerrit2 import Anonymous, GerritRestAPI

EXCLUDED_SUBJECTS = {
    "annotat",
    "assert",
    "AutoValue",
    "avadoc",  # Javadoc &co.
    "avaDoc",
    "ava-doc",
    "baz",  # bazel, bazlet(s)
    "Baz",
    "circular",
    "class",
    "common.ts",
    "construct",
    "controls",
    "debounce",
    "Debounce",
    "decorat",
    "efactor",  # Refactor &co.
    "format",
    "Format",
    "getter",
    "gr-",
    "hide",
    "icon",
    "ignore",
    "immutab",
    "import",
    "inject",
    "iterat",
    "IT",
    "js",
    "label",
    "licence",
    "license",
    "lint",
    "listener",
    "Listener",
    "lock",
    "method",
    "metric",
    "mock",
    "module",
    "naming",
    "nits",
    "nongoogle",
    "prone",  # error prone &co.
    "Prone",
    "register",
    "Register",
    "remove",
    "Remove",
    "rename",
    "Rename",
    "Revert",
    "serializ",
    "Serializ",
    "server.go",
    "setter",
    "spell",
    "Spell",
    "test",  # testing, tests; unit or else
    "Test",
    "thread",
    "tsetse",
    "type",
    "Type",
    "typo",
    "util",
    "variable",
    "version",
    "warning",
}

COMMIT_SHA1_PATTERN = r"^commit ([a-z0-9]+)$"
DATE_HEADER_PATTERN = r"Date: .+"
SUBJECT_SUBMODULES_PATTERN = r"^Update git submodules$"
ISSUE_ID_PATTERN = r"[a-zA-Z]+: [Ii]ssue ([0-9]+)"
CHANGE_ID_PATTERN = r"^Change-Id: [I0-9a-z]+$"
PLUGIN_PATTERN = r"plugins/([a-z\-]+)"
RELEASE_VERSIONS_PATTERN = r"v([0-9\.\-rc]+)\.\.v([0-9\.\-rc]+)"
RELEASE_MAJOR_PATTERN = r"^([0-9]+\.[0-9]+).+"
RELEASE_DOC_PATTERN = r"^([0-9]+\.[0-9]+\.[0-9]+).*"

CHANGE_URL = "/c/gerrit/+/"
COMMIT_URL = "/changes/?q=commit%3A"
GERRIT_URL = "https://gerrit-review.googlesource.com"
ISSUE_URL = "https://bugs.chromium.org/p/gerrit/issues/detail?id="

MARKDOWN = "release_noter"
GIT_COMMAND = "git"
GIT_PATH = "../.."
PLUGINS = "plugins/"
UTF8 = "UTF-8"


def parse_args():
    parser = argparse.ArgumentParser(
        description="Generate an initial release notes markdown file.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument(
        "-l",
        "--link",
        dest="link",
        required=False,
        default=False,
        action="store_true",
        help="link commits to change in Gerrit; slower as it gets each _number from gerrit",
    )
    parser.add_argument("range", help="git log revision range")
    return parser.parse_args()


def list_submodules():
    submodule_names = [
        GIT_COMMAND,
        "submodule",
        "foreach",
        "--quiet",
        "echo $name",
    ]
    return subprocess.check_output(submodule_names, cwd=f"{GIT_PATH}", encoding=UTF8)


def open_git_log(options, cwd=os.getcwd()):
    git_log = [
        GIT_COMMAND,
        "log",
        "--no-merges",
        options.range,
    ]
    return subprocess.check_output(git_log, cwd=cwd, encoding=UTF8)


class Component:
    name = None
    sentinels = set()

    def __init__(self, name, sentinels):
        self.name = name
        self.sentinels = sentinels


class Components(Enum):
    plugin_ce = Component("Codemirror-editor", {PLUGINS})
    plugin_cm = Component("Commit-message-length-validator", {PLUGINS})
    plugin_dp = Component("Delete-project", {PLUGINS})
    plugin_dc = Component("Download-commands", {PLUGINS})
    plugin_gt = Component("Gitiles", {PLUGINS})
    plugin_ho = Component("Hooks", {PLUGINS})
    plugin_pm = Component("Plugin-manager", {PLUGINS})
    plugin_re = Component("Replication", {PLUGINS})
    plugin_rn = Component("Reviewnotes", {PLUGINS})
    plugin_su = Component("Singleusergroup", {PLUGINS})
    plugin_wh = Component("Webhooks", {PLUGINS})

    ui = Component(
        "Polygerrit UI",
        {"poly", "gwt", "button", "dialog", "icon", "hover", "menu", "ux"},
    )
    doc = Component("Documentation", {"document"})
    jgit = Component("JGit", {"jgit"})
    elastic = Component("Elasticsearch", {"elastic"})
    deps = Component("Other dependency", {"upgrade", "dependenc"})
    otherwise = Component("Other core", {})


class Task(Enum):
    start_commit = 1
    finish_headers = 2
    capture_subject = 3
    finish_commit = 4


class Commit:
    sha1 = None
    subject = None
    component = None
    issues = set()

    def reset(self, signature, task):
        if signature is not None:
            self.sha1 = signature.group(1)
            self.subject = None
            self.component = None
            self.issues = set()
            return Task.finish_headers
        return task


def parse_log(process, gerrit, options, commits, cwd=os.getcwd()):
    commit = Commit()
    task = Task.start_commit
    for line in process.splitlines():
        line = line.strip()
        if not line:
            continue
        if task == Task.start_commit:
            task = commit.reset(re.search(COMMIT_SHA1_PATTERN, line), task)
        elif task == Task.finish_headers:
            if re.match(DATE_HEADER_PATTERN, line):
                task = Task.capture_subject
        elif task == Task.capture_subject:
            commit.subject = line
            task = Task.finish_commit
        elif task == Task.finish_commit:
            commit_issue = re.search(ISSUE_ID_PATTERN, line)
            if commit_issue is not None:
                commit.issues.add(commit_issue.group(1))
            else:
                commit_end = re.match(CHANGE_ID_PATTERN, line)
                if commit_end is not None:
                    commit = finish(commit, commits, gerrit, options, cwd)
                    task = Task.start_commit
        else:
            raise RuntimeError("FIXME")


def finish(commit, commits, gerrit, options, cwd):
    if re.match(SUBJECT_SUBMODULES_PATTERN, commit.subject):
        return Commit()
    if len(commit.issues) == 0:
        for exclusion in EXCLUDED_SUBJECTS:
            if exclusion in commit.subject:
                return Commit()
        for component in commits:
            for noted_commit in commits[component]:
                if noted_commit.subject == commit.subject:
                    return Commit()
    set_component(commit, commits, cwd)
    link_subject(commit, gerrit, options, cwd)
    escape_these(commit)
    return Commit()


def set_component(commit, commits, cwd):
    component_found = None
    for component in Components:
        for sentinel in component.value.sentinels:
            if component_found is None:
                if re.match(f"{GIT_PATH}/{PLUGINS}{component.value.name.lower()}", cwd):
                    component_found = component
                elif sentinel.lower() in commit.subject.lower():
                    component_found = component
                if component_found is not None:
                    commits[component].append(commit)
    if component_found is None:
        commits[Components.otherwise].append(commit)
    commit.component = component_found


def init_components():
    components = dict()
    for component in Components:
        components[component] = []
    return components


def link_subject(commit, gerrit, options, cwd):
    if options.link:
        gerrit_change = gerrit.get(f"{COMMIT_URL}{commit.sha1}")
        if not gerrit_change:
            return
        change_number = gerrit_change[0]["_number"]
        plugin_wd = re.search(f"{GIT_PATH}/({PLUGINS}.+)", cwd)
        if plugin_wd is not None:
            change_address = f"{GERRIT_URL}/c/{plugin_wd.group(1)}/+/{change_number}"
        else:
            change_address = f"{GERRIT_URL}{CHANGE_URL}{change_number}"
        short_sha1 = commit.sha1[0:7]
        commit.subject = f"[{short_sha1}]({change_address})\n  {commit.subject}"


def escape_these(in_change):
    in_change.subject = in_change.subject.replace("<", "\\<")
    in_change.subject = in_change.subject.replace(">", "\\>")


def print_commits(commits, md):
    for component in commits:
        if len(commits[component]) > 0:
            if PLUGINS in component.value.sentinels:
                md.write(f"\n### {component.value.name}\n")
            else:
                md.write(f"\n## {component.value.name} changes\n")
            for commit in commits[component]:
                print_from(commit, md)


def print_from(this_change, md):
    md.write("\n*")
    for issue in sorted(this_change.issues):
        md.write(f" [Issue {issue}]({ISSUE_URL}{issue});\n ")
    md.write(f" {this_change.subject}\n")


def print_template(md, options):
    previous = "0.0.0"
    new = "0.1.0"
    versions = re.search(RELEASE_VERSIONS_PATTERN, options.range)
    if versions is not None:
        previous = versions.group(1)
        new = versions.group(2)
    data = {
        "previous": previous,
        "new": new,
        "major": re.search(RELEASE_MAJOR_PATTERN, new).group(1),
        "doc": re.search(RELEASE_DOC_PATTERN, new).group(1),
    }
    template = Template(open(f"{MARKDOWN}.md.template").read())
    md.write(f"{template.render(data=data)}\n")


def print_notes(commits, options):
    markdown = f"{MARKDOWN}.md"
    next_md = 2
    while path.exists(markdown):
        markdown = f"{MARKDOWN}-{next_md}.md"
        next_md += 1
    with open(markdown, "w") as md:
        print_template(md, options)
        print_commits(commits, md)
        md.write("\n## Bugfix releases\n")


def plugin_changes():
    plugin_commits = init_components()
    for submodule_name in list_submodules().splitlines():
        plugin_name = re.search(PLUGIN_PATTERN, submodule_name)
        if plugin_name is not None:
            plugin_wd = f"{GIT_PATH}/{PLUGINS}{plugin_name.group(1)}"
            plugin_log = open_git_log(script_options, plugin_wd)
            parse_log(
                plugin_log,
                gerrit_api,
                script_options,
                plugin_commits,
                plugin_wd,
            )
    return plugin_commits


if __name__ == "__main__":
    gerrit_api = GerritRestAPI(url=GERRIT_URL, auth=Anonymous())
    script_options = parse_args()
    if script_options.link:
        print("Link option used; slower.")
    noted_changes = plugin_changes()
    change_log = open_git_log(script_options)
    parse_log(change_log, gerrit_api, script_options, noted_changes)
    print_notes(noted_changes, script_options)
