#!/usr/bin/env python3

from __future__ import annotations

import argparse
import json
import subprocess
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Mapping
    from typing import TypeAlias

Version: TypeAlias = tuple[int, ...]

OPTIONS: Mapping[str, tuple[str, Version, Version | None]] = {
    "afterends": ("nextword function (Ctrl+Right) stops at word ends", (4, 0), None),
    "atblanks": ("Soft wrapping breaks on spaces", (4, 0), None),
    "nonewlines": ("Do not ensure final newline", (4, 1), None),
    "historylog": ("Remember search history", (4, 0), None),
    "linenumbers": ("Show line numbers", (4, 0), None),
    "locking": ("Vim-style soft lock files", (4, 0), None),
    "mouse": ("Enable mouse", (4, 0), None),
    "multibuffer": ("Allow multiple buffers", (4, 0), None),
    "positionlog": ("Remember place in file", (4, 0), None),
    "quickblank": ("Clear status bar after one keystroke", (4, 0), None),
    "regexp": ("Regex search by default", (4, 0), None),
    "smarthome": ("Home toggles start of line and start of text", (4, 0), None),
    "speller": ("Spelling checker", (4, 0), None),
    "suspend": ("Allow suspending", (4, 0), (4, 9)),
    "suspendable": ("Allow suspending", (4, 9), (6, 0)),
    "tabsize 4": ("Sane tab size", (4, 0), None),
    "tabstospaces": ("Convert tabs to spaces", (4, 0), None),
    "whitespace": ("Whitespace characters", (4, 0), None),
    "zap": ("Let delete/backspace delete selection", (4, 0), None),
    "indicator": ("Scrollbar on the right", (5, 0), None),
    "minibar": ("Supress title bar and show state at the bottom", (5, 5), None),
    "guidestripe 80": ("Show guide stripe", (6, 1), None),
    "stateflags": ("Show state in title bar", (5, 3), None),
    "magic": ("Fall back to libmagic for syntax choosing", (4, 0), (5, 3)),
}
BINDS: Mapping[str, tuple[str, Version, Version | None]] = {
    "suspend main": ("Allow suspending", (6, 0), None),
}
DEFAULT_BINDS: Mapping[str, tuple[str, Version, Version | None]] = {}


def _named_colour(
    version: Version,
    colour: str,
    base16: bool,
) -> str:
    """Based on src/rcfile.c"""
    _ansi_8 = [
        "red",
        "green",
        "blue",
        "magenta",
        "yellow",
        "cyan",
        "white",
        "black",
    ]
    _ansi_16 = [
        *_ansi_8,
        *(f"light{colour}" for colour in _ansi_8),
        *(f"bright{colour}" for colour in _ansi_8),
        "grey",
        "gray",
    ]
    _named_256 = [
        "pink",
        "purple",
        "mauve",
        "lagoon",
        "mint",
        "lime",
        "peach",
        "orange",
        "latte",
        "rosy",
        "beet",
        "plum",
        "sea",
        "sky",
        "slate",
        "teal",
        "sage",
        "brown",
        "ocher",
        "sand",
        "tawny",
        "brick",
        "crimson",
        "normal",
    ]
    if colour in _named_256:
        if base16:
            raise ValueError("Fuck off, use base 16 colours")
    elif colour not in _ansi_16:
        raise ValueError(f"Unknown colour {colour}")
    if colour.startswith("light") and version < (5, 0):
        colour = f"bright{colour[5:]}"
    if colour.startswith("bright") and version >= (5, 0):
        colour = f"light{colour[5:]}"
    return colour


def _segment(
    version: Version,
    name: str,
    value: str,
    base16: bool,
) -> str | None:
    # Segment name part
    valid = {
        "titlecolor": (4, 0),
        "statuscolor": (4, 0),
        "errorcolor": (4, 0),
        "selectedcolor": (4, 0),
        "stripecolor": (4, 0),
        "numbercolor": (4, 0),
        "keycolor": (4, 0),
        "functioncolor": (4, 0),
        "scrollercolor": (5, 3),
        "promptcolor": (5, 5),
        "spotlightcolor": (5, 6),
    }
    if name not in valid:
        raise ValueError(f"Unknown segment: {name}")
    if version < valid[name]:
        return None
    if name == "spotlightcolor" and version < (5, 6, 1):
        name = "highlightcolor"

    # Colour value part
    raw_parts = value.split(",")
    bold = "bold" in raw_parts
    italic = "italic" in raw_parts
    fg_bg = [part for part in raw_parts if part != "bold" if part != "italic"]
    if len(fg_bg) == 1:
        (fg,) = fg_bg
        bg = None
    else:
        fg, bg = fg_bg
    parts = []
    if version >= (5, 0):
        if bold:
            parts.append("bold")
        if italic:
            parts.append("italic")
    elif bold:
        try:
            fg = _named_colour(version, f"bright{fg}", base16)
        except ValueError:
            pass

    parts.append(_named_colour(version, fg, base16) if fg else "")
    if bg:
        parts.append(_named_colour(version, bg, base16))
    value = ",".join(parts)

    return f"set {name} {value}"


def _version(string: str) -> Version:
    if string.startswith("v"):
        string = string[1:]
    return tuple(map(int, string.split(".")))


def generate_changes(start_version: Version):
    with tempfile.TemporaryDirectory() as temp_dir:
        nano_git = Path(temp_dir) / "nano.git"

        # Clone repo
        subprocess.check_call(
            [
                "git",
                "clone",
                "--bare",
                "https://git.savannah.gnu.org/git/nano.git",
                nano_git,
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )

        # Find relevant tags
        tags = [
            tag
            for tag in subprocess.check_output(
                [
                    "git",
                    "--git-dir",
                    nano_git,
                    "tag",
                    "--list",
                ],
                stderr=subprocess.PIPE,
            )
            .decode()
            .splitlines()
            if tag.startswith("v")
            if all(map(str.isnumeric, tag[1:].split(".")))
        ]

        # Built a history of changes to the sample configuration
        _configs_per_version = []
        _versions = []
        tracked_config = set()
        for tag in sorted(tags, key=_version):
            version = _version(tag)
            if version < start_version:
                continue
            nanorc = subprocess.check_output(
                ["git", "--git-dir", nano_git, "show", f"{tag}:doc/sample.nanorc.in"],
                stderr=subprocess.PIPE,
            ).decode()
            config_lines = {
                line
                for line in nanorc.splitlines()
                if line.strip()
                if not line.startswith("##")
                if line != '" main'
                if line != '\x1bu" main'
            }
            assert all(line.startswith("#") for line in config_lines)
            added = config_lines - tracked_config
            removed = tracked_config - config_lines
            tracked_config = config_lines
            if added or removed:
                _configs_per_version.append((version, removed, added))
            _versions.append(version)

    return _versions, _configs_per_version


def generate_config(
    version: Version,
    config_path: Path,
    out_path: Path | None,
    base16: bool,
) -> None:
    config = json.loads(config_path.read_text())

    output = []

    # Non-theme options
    for option, value in config["set"].items():
        if option not in OPTIONS:
            raise ValueError(f"Untracked option {option}")
        doc, start, stop = OPTIONS[option]
        if version >= start and (stop is None or version < stop):
            if value is True:
                output.extend((f"# {doc}", f"set {option}", ""))
            else:
                output.extend((f"# {doc}", f'set {option} "{value}"', ""))

    # Binds
    for key, bind in config["bind"].items():
        if bind not in BINDS:
            raise ValueError(f"Untracked binding {bind}")
        doc, start, stop = BINDS[bind]
        if version >= start and (stop is None or version < stop):
            output.extend((f"# {doc}", f"bind {key} {bind}", ""))

    # Unbinds
    for key, _ in config["unbind"].items():
        if key not in DEFAULT_BINDS:
            raise ValueError(f"Untracked unbinding {key}")
        doc, start, stop = DEFAULT_BINDS
        if version >= start and (stop is None or version < stop):
            output.extend((f"# {doc}", f"unbind {key}", ""))

    # Theme
    output.append("# Theme")
    for name, value in config["theme"].items():
        segment = _segment(version, name, value, base16)
        if segment:
            output.append(segment)
    output.append("")

    if out_path:
        out_path.write_text("\n".join(output))
    else:
        print("\n".join(output))


def check_updates(
    start_version: Version,
    last_checked_version: Version,
) -> None:
    versions, configs_per_version = generate_changes(start_version)
    last_changed_version, _, _ = configs_per_version[-1]
    if last_changed_version > last_checked_version:
        version_str = ".".join(map(str, last_changed_version))
        raise RuntimeError(f"Update for newest version: {version_str}")


def main():
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="action")
    check_parser = subparsers.add_parser("check")
    check_parser.add_argument(
        "--start",
        type=_version,
        default=(4, 0),
    )
    check_parser.add_argument(
        "--last-checked",
        type=_version,
        default=(7, 2),
    )
    generate_parser = subparsers.add_parser("generate")
    generate_parser.add_argument(
        "--version",
        type=_version,
        default=(7, 2),
    )
    generate_parser.add_argument(
        "--config",
        type=Path,
        required=True,
    )
    generate_parser.add_argument(
        "--out",
        type=Path,
    )
    generate_parser.add_argument(
        "--256-colours",
        action="store_false",
        default=True,
        dest="base16",
    )
    args = parser.parse_args()
    if args.action == "check":
        check_updates(args.start, args.last_checked)
    elif args.action == "generate":
        generate_config(args.version, args.config, args.out, args.base16)


if __name__ == "__main__":
    main()