"""Wivexx mascot items — PIL renderer for the SHARED item spec.

Reads ui/mascot_items.js (the single source of truth that the control room's
ui/accessories.js also draws from) and paints each accessory / gadget with PIL, so
the floating tk orb and the control room render the SAME designs from ONE file.
Edit ui/mascot_items.js and both surfaces update.

Public API:  draw_item(base_img, name, cx, cy, R, t, look=(0,0))  -> alpha-composites
the item onto `base_img` (an RGBA PIL image). Coordinates match the spec: origin at
the orb centre, +x right, +y DOWN, units of the orb radius R (all in px here).
"""
from __future__ import annotations

import json
import math
import os
import re
from typing import Optional

from PIL import Image, ImageDraw

_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ui", "mascot_items.js")
_ITEMS: Optional[dict] = None
_PAL: dict = {}


def _load() -> dict:
    """Parse the JS spec file's object literal as JSON (cached)."""
    global _ITEMS, _PAL
    if _ITEMS is not None:
        return _ITEMS
    try:
        with open(_SPEC_PATH, "r", encoding="utf-8") as f:
            txt = f.read()
        # anchor on the real assignment `window.MASCOT_ITEMS = {` (the header comment
        # also MENTIONS the name, so a bare .index() would match that instead)
        m = re.search(r"window\.MASCOT_ITEMS\s*=\s*\{", txt)
        start = txt.index("{", m.start())
        body = txt[start:].rstrip()
        if body.endswith(";"):
            body = body[:-1].rstrip()
        _ITEMS = json.loads(body)
    except Exception:
        _ITEMS = {}
    _PAL = _ITEMS.get("_palette", {}) if isinstance(_ITEMS, dict) else {}
    return _ITEMS


def _col(tok, mul_a: float = 1.0):
    """Resolve a color token ('volt', 'volt@0.55', '#rrggbb') → (r,g,b,a 0..255)."""
    if not tok or tok == "none":
        return None
    a = 1.0
    hexs = tok
    if "@" in tok:
        hexs, av = tok.split("@", 1)
        try:
            a = float(av)
        except ValueError:
            a = 1.0
    hexs = _PAL.get(hexs, hexs)
    if not hexs.startswith("#"):
        hexs = "#000000"
    r = int(hexs[1:3], 16)
    g = int(hexs[3:5], 16)
    b = int(hexs[5:7], 16)
    return (r, g, b, max(0, min(255, int(255 * a * mul_a))))


_TAU = math.pi * 2


def _anim(an, t):
    """Per-prim animation → (alpha_mul, dx, dy) in R-units for dx/dy."""
    if not an:
        return 1.0, 0.0, 0.0
    dur = an.get("dur", 1.0) or 1.0
    ph = an.get("ph", 0.0)
    k = an.get("k")
    if k == "blink":
        lo = an.get("lo", 0.3)
        return lo + (1 - lo) * (0.5 + 0.5 * math.sin(_TAU * (t / dur))), 0.0, 0.0
    if k == "flash":
        return (1.0 if math.sin(_TAU * (t / dur)) > an.get("thr", 0.0) else 0.0), 0.0, 0.0
    if k == "bob":
        s = an.get("amp", 0.05) * math.sin(_TAU * (t / dur + ph))
        return (1.0, s, 0.0) if an.get("ax") == "x" else (1.0, 0.0, s)
    return 1.0, 0.0, 0.0


def draw_item(base: Image.Image, name: str, cx: float, cy: float, R: float,
              t: float, look=(0.0, 0.0)) -> None:
    """Alpha-composite the named item onto `base` (RGBA), matching ui/accessories.js."""
    items = _load()
    item = items.get(name)
    if not item:
        return
    lx, ly = (look[0], look[1]) if look else (0.0, 0.0)
    ov = Image.new("RGBA", base.size, (0, 0, 0, 0))
    dr = ImageDraw.Draw(ov)

    # group transform: translate with the gaze and/or rotate about a pivot
    gtx = gty = 0.0
    ang = 0.0
    piv = None
    g = item.get("grp")
    if g:
        tr = g.get("tr")
        if tr:
            gtx = tr.get("gx", 0.0) * lx
            gty = tr.get("gy", 0.0) * ly
            cl = tr.get("clamp")
            if cl:
                m = math.hypot(gtx, gty)
                if m > cl:
                    gtx, gty = gtx * cl / m, gty * cl / m
        rot = g.get("rot")
        if rot:
            ang = math.radians(rot.get("deg", 0.0) * math.sin(_TAU * (t / (rot.get("dur", 1.0) or 1.0)))
                               + rot.get("gaze", 0.0) * lx)
            pv = rot.get("piv", [0, 0])
            piv = (cx + pv[0] * R + gtx * R, cy + pv[1] * R + gty * R)
    ca, sa = math.cos(ang), math.sin(ang)

    def pt(x, y):
        """spec (x,y) in R-units → device px, with group translate + rotation."""
        px = cx + (x + gtx) * R
        py = cy + (y + gty) * R
        if piv is not None and ang:
            dx, dy = px - piv[0], py - piv[1]
            px, py = piv[0] + dx * ca - dy * sa, piv[1] + dx * sa + dy * ca
        return (px, py)

    def lwid(w):
        return max(1, int(round((w or 0) * R)))

    for p in item.get("prims", []):
        amul, ox, oy = _anim(p.get("an"), t)
        if amul <= 0.001:
            continue
        fill = _col(p.get("f"), amul)
        stroke = _col(p.get("s"), amul)
        typ = p.get("t")

        if typ in ("poly", "line"):
            pts = [pt(px + ox, py + oy) for (px, py) in p["p"]]
            if typ == "poly":
                if fill:
                    dr.polygon(pts, fill=fill)
                if stroke:
                    dr.line(pts + [pts[0]], fill=stroke, width=lwid(p.get("sw")),
                            joint="curve")
            else:
                dr.line(pts, fill=stroke, width=lwid(p.get("sw")), joint="curve")
        elif typ == "circ":
            x0, y0 = pt(p["x"] + ox, p["y"] + oy)
            r = p["r"] * R
            bb = [x0 - r, y0 - r, x0 + r, y0 + r]
            dr.ellipse(bb, fill=fill, outline=stroke, width=lwid(p.get("sw")) if stroke else 1)
        elif typ == "ell":
            x0, y0 = pt(p["x"] + ox, p["y"] + oy)
            rx, ry = p["rx"] * R, p["ry"] * R
            dr.ellipse([x0 - rx, y0 - ry, x0 + rx, y0 + ry], fill=fill,
                       outline=stroke, width=lwid(p.get("sw")) if stroke else 1)
        elif typ == "arc":
            x0, y0 = pt(p["x"] + ox, p["y"] + oy)
            rx, ry = p["rx"] * R, p["ry"] * R
            dr.arc([x0 - rx, y0 - ry, x0 + rx, y0 + ry], p["a0"], p["a1"],
                   fill=stroke, width=lwid(p.get("sw")))
        elif typ in ("rect", "rr"):
            x0, y0 = pt(p["x"] + ox, p["y"] + oy)
            w, h = p["w"] * R, p["h"] * R
            box = [x0 - w / 2, y0 - h / 2, x0 + w / 2, y0 + h / 2]
            # clamp the corner radius safely — PIL errors if radius approaches half the
            # box's SHORTER side; keep a 1.5px margin and fall back to a plain rect
            _mr = min(w, h) / 2.0 - 1.5
            rad = min(p.get("rad", 0) * R, _mr) if (typ == "rr" and _mr > 0) else 0
            if rad >= 1.0:
                dr.rounded_rectangle(box, radius=rad, fill=fill, outline=stroke,
                                     width=lwid(p.get("sw")) if stroke else 1)
            else:
                dr.rectangle(box, fill=fill, outline=stroke,
                             width=lwid(p.get("sw")) if stroke else 1)

    base.alpha_composite(ov)


def item_names():
    return [k for k in _load().keys() if not k.startswith("_")]
