#!/usr/bin/env python3
"""Convert old-distro palettes (colors.toml) into Nomarchy theme JSON presets.

Usage:  tools/import-palettes.py <old-palettes-dir> <output-themes-dir>

Old format (per theme directory):
    colors.toml     kitty-style keys: background, foreground, accent,
                    selection_background, color0..color15
    light.mode      marker file: theme is light
    icons.theme     icon theme name (informational, ignored for now)
    backgrounds/    wallpapers
    apps/           hand-made per-app theme files (btop.theme, ...)

Output, per theme:
    <slug>.json            the palette (Nomarchy theme schema)
    <slug>/backgrounds/    wallpapers, copied verbatim — NOTE: this repo's
                           own themes/ no longer carries backgrounds/ (moved
                           to the Nomarchy-Wallpapers repo, ROADMAP § "Faster
                           switches"); if <output-themes-dir> is this repo's
                           themes/, move the copied backgrounds/ over there
                           instead of committing them here
    <slug>/btop.theme      copied from apps/ when present

Hierarchy vs ANSI (#70):
    Nomarchy roles (surface/overlay/mantle/…) are first-class and may
    diverge from ansi[0]/ansi[8]. When color0==color8 (gruvbox, everforest,
    many light themes' black==bright-black), do NOT collapse surface and
    overlay — derive a raised step so the bg stack has hierarchy.

    Light themes: ANSI color0 is almost always "black" (dark), not a UI
    chip. Using it as surface paints sludge on a light canvas — detect
    that trap and derive surface/overlay from base instead.

    After import, hand-tune roles if needed. Do not bulk-reimport shipped
    themes/*.json without a hierarchy pass — this tool is for *future*
    imports / new palettes.
"""

import json
import shutil
import sys
import tomllib
from pathlib import Path


def darken(hex_color: str, factor: float = 0.85) -> str:
    """Scale a #rrggbb color toward black (used to derive 'mantle' / light chips)."""
    h = hex_color.lstrip("#")
    r, g, b = (int(h[i:i + 2], 16) for i in (0, 2, 4))
    return "#{:02x}{:02x}{:02x}".format(*(round(v * factor) for v in (r, g, b)))


def lighten(hex_color: str, amount: float = 0.14) -> str:
    """Move each channel toward 255 by `amount` (0..1) — raised dark-mode chip."""
    h = hex_color.lstrip("#")
    r, g, b = (int(h[i:i + 2], 16) for i in (0, 2, 4))
    return "#{:02x}{:02x}{:02x}".format(
        *(max(0, min(255, round(v + (255 - v) * amount))) for v in (r, g, b))
    )


def _norm(hex_color: str) -> str:
    return hex_color.strip().lower()


def _luma(hex_color: str) -> float:
    """Relative luminance (sRGB), for light-theme ANSI-black detection."""
    h = hex_color.lstrip("#")
    r, g, b = (int(h[i:i + 2], 16) / 255.0 for i in (0, 2, 4))

    def lin(c: float) -> float:
        return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4

    return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)


def hierarchy_roles(base: str, color0: str, color8: str, mode: str) -> dict:
    """Map ANSI → surface/overlay/mantle without collapsing when 0==8.

    Roles stay independent of the ANSI table: even when color0==color8 in
    the source palette, surface and overlay must differ so chips/menus
    can step up the stack.
    """
    # mantle: step "under" the canvas. Light bases use a gentler darken so
    # cream/off-white does not become muddy grey sludge.
    mantle = darken(base, 0.85 if mode == "dark" else 0.92)

    surface, overlay = color0, color8

    if mode == "light":
        # Light-theme trap: color0/color8 are ANSI black / bright-black
        # (dark), not raised UI chips. Derive chips off base instead.
        base_L = _luma(base)
        if _luma(surface) < base_L * 0.5:
            surface = darken(base, 0.94)
        if _luma(overlay) < base_L * 0.5:
            overlay = darken(base, 0.88)

    # When ANSI 0==8 (or the light-theme derive left them equal), force a
    # hierarchy step: dark mode lightens overlay; light mode darkens it.
    if _norm(surface) == _norm(overlay):
        if mode == "dark":
            overlay = lighten(surface, 0.14)
        else:
            overlay = darken(surface, 0.88)
        print(f"    note: color0==color8 — derived overlay hierarchy ({surface} → {overlay})")

    return {"mantle": mantle, "surface": surface, "overlay": overlay}


def convert(theme_dir: Path) -> dict:
    toml = tomllib.loads((theme_dir / "colors.toml").read_text())
    slug = theme_dir.name
    ansi = [toml[f"color{i}"] for i in range(16)]
    mode = "light" if (theme_dir / "light.mode").exists() else "dark"
    base = toml["background"]
    roles = hierarchy_roles(base, toml["color0"], toml["color8"], mode)

    # muted: prefer color8 when it is a mid grey distinct from surface;
    # otherwise derive so dim labels still read (roles ≠ ANSI when 0==8).
    muted = toml["color8"]
    if _norm(muted) == _norm(roles["surface"]) or (
        mode == "light" and _luma(muted) < _luma(base) * 0.5
    ):
        if mode == "dark":
            muted = darken(toml["foreground"], 0.55)
        else:
            # mid grey between text and base
            th, bh = toml["foreground"].lstrip("#"), base.lstrip("#")
            muted = "#{:02x}{:02x}{:02x}".format(
                *((int(th[i:i + 2], 16) + int(bh[i:i + 2], 16)) // 2 for i in (0, 2, 4))
            )

    return {
        "version": 1,
        "name": slug.replace("-", " ").title(),
        "slug": slug,
        "mode": mode,
        # Empty = auto: nomarchy-state-sync falls back to the first file in
        # the theme's backgrounds/ directory.
        "wallpaper": "",
        "colors": {
            "base": base,
            "mantle": roles["mantle"],
            "surface": roles["surface"],
            "overlay": roles["overlay"],
            "text": toml["foreground"],
            "subtext": toml["color7"],
            "muted": muted,
            "accent": toml["accent"],
            "accentAlt": toml["color5"],
            "good": toml["color2"],
            "warn": toml["color3"],
            "bad": toml["color1"],
        },
        "ansi": ansi,
    }


def main() -> None:
    if len(sys.argv) != 3:
        sys.exit(__doc__)
    src, out = Path(sys.argv[1]), Path(sys.argv[2])
    out.mkdir(parents=True, exist_ok=True)

    converted = 0
    for theme_dir in sorted(src.iterdir()):
        if not (theme_dir / "colors.toml").is_file():
            continue
        theme = convert(theme_dir)
        slug = theme["slug"]
        (out / f"{slug}.json").write_text(json.dumps(theme, indent=2) + "\n")

        # Assets: wallpapers and hand-made per-app theme files. Wallpapers
        # land in <out>/<slug>/backgrounds/ here for convenience, but if
        # <out> is this repo's themes/, move them to the Nomarchy-Wallpapers
        # repo instead of committing them (ROADMAP § "Faster switches") —
        # this repo's themes/*/backgrounds/ was removed on purpose.
        extras = []
        if (theme_dir / "backgrounds").is_dir():
            shutil.copytree(theme_dir / "backgrounds", out / slug / "backgrounds",
                            dirs_exist_ok=True)
            extras.append(f"{len(list((theme_dir / 'backgrounds').iterdir()))} wallpapers — move to Nomarchy-Wallpapers")
        if (theme_dir / "apps" / "btop.theme").is_file():
            (out / slug).mkdir(parents=True, exist_ok=True)
            shutil.copyfile(theme_dir / "apps" / "btop.theme", out / slug / "btop.theme")
            extras.append("btop")

        print(f"  {slug:<20} ({theme['mode']}{', ' + ', '.join(extras) if extras else ''})")
        converted += 1
    print(f"converted {converted} palettes -> {out}")


if __name__ == "__main__":
    main()
