#!/usr/bin/env python3
"""
adom-footprint 1.0.20 emits empty <g class="adom-layer-{fab,silk,courtyard}"> groups —
it only ever populates copper/drill/padnum. The viewer's own LAYER_DEFS already define
those three layers and `groupHasGeo()` shows a toggle row as soon as a group holds
geometry, so filling the groups is enough to light up the layers panel. No JS changes.

Parses the graphics items out of a .kicad_mod and injects matching SVG into the groups,
widening the viewBox when the courtyard/fab outline sits outside the copper extents.
"""
import re, sys, math

COLORS = {"fab": "#6e7681", "silk": "#00cccc", "courtyard": "#FF26E2"}
LAYER_OF = {"F.Fab": "fab", "F.SilkS": "silk", "F.CrtYd": "courtyard"}


def sexp(text):
    """Minimal s-expression reader -> nested lists of tokens."""
    toks = re.findall(r'\(|\)|"(?:[^"\\]|\\.)*"|[^\s()]+', text)
    stack, cur = [], []
    for t in toks:
        if t == "(":
            stack.append(cur); cur = []
        elif t == ")":
            done, cur = cur, stack.pop(); cur.append(done)
        else:
            cur.append(t[1:-1] if t.startswith('"') else t)
    return cur


def find(node, key):
    return [c for c in node if isinstance(c, list) and c and c[0] == key]


def one(node, key):
    r = find(node, key)
    return r[0] if r else None


def xy(node, key):
    n = one(node, key)
    return (float(n[1]), float(n[2])) if n else None


def layer_of(item):
    n = one(item, "layer")
    return n[1] if n else None


def width_of(item):
    st = one(item, "stroke")
    if st:
        w = one(st, "width")
        if w:
            return float(w[1])
    w = one(item, "width")
    return float(w[1]) if w else 0.12


def filled(item):
    f = one(item, "fill")
    if f and len(f) > 1:
        v = f[1] if not isinstance(f[1], list) else (f[1][1] if len(f[1]) > 1 else "no")
        return v in ("yes", "solid", "true")
    return False


def arc_path(s, m, e):
    """Three-point arc -> SVG A command."""
    (x1, y1), (xm, ym), (x2, y2) = s, m, e
    d = 2 * (x1 * (ym - y2) + xm * (y2 - y1) + x2 * (y1 - ym))
    if abs(d) < 1e-12:
        return f"M {x1:.4f} {y1:.4f} L {x2:.4f} {y2:.4f}"
    ux = ((x1**2 + y1**2) * (ym - y2) + (xm**2 + ym**2) * (y2 - y1) + (x2**2 + y2**2) * (y1 - ym)) / d
    uy = ((x1**2 + y1**2) * (x2 - xm) + (xm**2 + ym**2) * (x1 - x2) + (x2**2 + y2**2) * (xm - x1)) / d
    r = math.hypot(x1 - ux, y1 - uy)
    cross = (xm - x1) * (y2 - y1) - (ym - y1) * (x2 - x1)
    sweep = 0 if cross > 0 else 1
    a1 = math.atan2(y1 - uy, x1 - ux); a2 = math.atan2(y2 - uy, x2 - ux)
    da = (a2 - a1) % (2 * math.pi) if sweep else (a1 - a2) % (2 * math.pi)
    large = 1 if da > math.pi else 0
    return f"M {x1:.4f} {y1:.4f} A {r:.4f} {r:.4f} 0 {large} {sweep} {x2:.4f} {y2:.4f}"


def collect(fp):
    """-> {layer_key: [svg_element, ...]}, plus the bbox of everything drawn."""
    out = {k: [] for k in COLORS}
    xs, ys = [], []

    def note(*pts):
        for x, y in pts:
            xs.append(x); ys.append(y)

    for item in fp:
        if not isinstance(item, list) or not item:
            continue
        kind = item[0]
        if not kind.startswith("fp_"):
            continue
        key = LAYER_OF.get(layer_of(item) or "")
        if not key:
            continue
        col, w = COLORS[key], width_of(item)
        common = f'stroke="{col}" stroke-width="{w:.3f}" stroke-linecap="round" fill="none"'

        if kind == "fp_line":
            s, e = xy(item, "start"), xy(item, "end")
            if s and e:
                note(s, e)
                out[key].append(f'<line x1="{s[0]:.4f}" y1="{s[1]:.4f}" x2="{e[0]:.4f}" y2="{e[1]:.4f}" {common}/>')
        elif kind == "fp_rect":
            s, e = xy(item, "start"), xy(item, "end")
            if s and e:
                note(s, e)
                x, y = min(s[0], e[0]), min(s[1], e[1])
                fillv = col if filled(item) else "none"
                out[key].append(f'<rect x="{x:.4f}" y="{y:.4f}" width="{abs(e[0]-s[0]):.4f}" '
                                f'height="{abs(e[1]-s[1]):.4f}" stroke="{col}" stroke-width="{w:.3f}" fill="{fillv}"/>')
        elif kind == "fp_circle":
            c, e = xy(item, "center"), xy(item, "end")
            if c and e:
                r = math.hypot(e[0] - c[0], e[1] - c[1])
                note((c[0] - r, c[1] - r), (c[0] + r, c[1] + r))
                fillv = col if filled(item) else "none"
                out[key].append(f'<circle cx="{c[0]:.4f}" cy="{c[1]:.4f}" r="{r:.4f}" '
                                f'stroke="{col}" stroke-width="{w:.3f}" fill="{fillv}"/>')
        elif kind == "fp_arc":
            s, m, e = xy(item, "start"), xy(item, "mid"), xy(item, "end")
            if s and m and e:
                note(s, m, e)
                out[key].append(f'<path d="{arc_path(s, m, e)}" {common}/>')
        elif kind == "fp_poly":
            pts = one(item, "pts")
            if pts:
                p = [(float(c[1]), float(c[2])) for c in find(pts, "xy")]
                if p:
                    note(*p)
                    fillv = col if filled(item) else "none"
                    d = " ".join(f"{x:.4f},{y:.4f}" for x, y in p)
                    out[key].append(f'<polygon points="{d}" stroke="{col}" stroke-width="{w:.3f}" fill="{fillv}"/>')

    bbox = (min(xs), min(ys), max(xs), max(ys)) if xs else None
    return out, bbox


def inject(html_path, mod_path, out_path):
    fp = sexp(open(mod_path).read())[0]
    layers, bbox = collect(fp)
    html = open(html_path).read()

    for key, els in layers.items():
        if not els:
            continue
        tag = f'<g class="adom-layer-{key}"></g>'
        if tag not in html:
            print(f"  ! no empty {key} group to fill", file=sys.stderr); continue
        html = html.replace(tag, f'<g class="adom-layer-{key}">{"".join(els)}</g>', 1)

    # widen the viewBox if the outline geometry falls outside the copper extents
    m = re.search(r'viewBox="(-?[\d.]+) (-?[\d.]+) ([\d.]+) ([\d.]+)"', html)
    if m and bbox:
        vx, vy, vw, vh = (float(g) for g in m.groups())
        nx0, ny0 = min(vx, bbox[0]), min(vy, bbox[1])
        nx1, ny1 = max(vx + vw, bbox[2]), max(vy + vh, bbox[3])
        pad = 0.15
        nx0 -= pad; ny0 -= pad; nx1 += pad; ny1 += pad
        if (nx0, ny0, nx1 - nx0, ny1 - ny0) != (vx, vy, vw, vh):
            html = html.replace(m.group(0),
                                f'viewBox="{nx0:.3f} {ny0:.3f} {nx1-nx0:.3f} {ny1-ny0:.3f}"', 1)

    open(out_path, "w").write(html)
    print(f"  {out_path}: " + ", ".join(f"{k}={len(v)}" for k, v in layers.items() if v))


if __name__ == "__main__":
    inject(*sys.argv[1:4])
