#!/usr/bin/env -S uv run --with cairosvg --quiet python
"""Politick "Broadcast · Soft red" — production vector MOTIF / supporting-asset kit.

The working visual vocabulary derived from the broadcast language (an origin dot + concentric
arcs): dividers, status/activity dots, the broadcast pulse indicator, bullets, arc ticks, and
reading/citation marks. Built as TRUE VECTOR — every shape is an explicit <path>/<circle> with
intentional geometry, all curves are true elliptical-arc (A) commands (no sampled polylines).

House style (matches tilings-prod/c-python-rigorous/generate.py)
---------------------------------------------------------------
  * A single PARAMS dataclass is the source of truth; every dimension is a ratio of a base
    unit `u` (no magic numbers strewn around). Re-runnable.
  * Ink strokes/fills use `currentColor` (inherits text colour -> works on light AND dark).
  * The accent uses `var(--pk-accent, #B84A38)`.
  * Compact float formatting; tight per-primitive viewBox.

Two broadcast forms (both = concentric arcs from an origin dot)
--------------------------------------------------------------
  * FAN  : a wifi-style quarter-fan (90 deg) with the origin dot at the corner. This is the
           small-UI density seen across the assets sheet (dividers, icons, reading marks).
           Used by: pulse, tick, cite, divider-arc.
  * (The MARK / logo uses the larger RIGHT-FACING SEMICIRCLE form. See report: the kit adopts
    the icon-density FAN; mark.py owns the lockup semicircle. Shared stroke ratio ~0.34*dot.)

Standalone .svg files fix `style="color:#1C2024"` for light/paper preview (like the tilings
SVGs); the themeable API is the Svelte components (currentColor + --pk-accent). verify.py
re-themes for the dark proofs.
"""
from __future__ import annotations

from dataclasses import dataclass
from math import cos, sin, radians
from pathlib import Path

OUT = Path(__file__).parent / "motifs"

# --------------------------------------------------------------------------- brand
INK = "#1C2024"      # ink
PAPER = "#F6F3EC"    # warm paper
ACCENT = "#B84A38"   # soft red
ACCENT_VAR = f"var(--pk-accent, {ACCENT})"


# --------------------------------------------------------------------------- params
@dataclass
class Params:
    """Single source of truth. `u` is the base module (~ one UI line / icon box); every
    dimension below is a ratio of `u`, except the broadcast-fan internals which are ratios of
    their own origin dot `g_dot` (so the fan's curvature feel is scale-independent)."""
    u: float = 24.0               # base module

    # --- standalone status / activity dots (ratios of u) ---
    dot: float = 0.21             # status dot radius            -> 5.04
    party: float = 0.15           # party dot radius (diam <= 8) -> 3.6 (d 7.2)
    bullet: float = 0.14          # list-bullet radius           -> 3.36
    ring_sw: float = 0.083        # hollow-ring dot stroke width -> 2.0

    # --- broadcast fan (origin dot + concentric quarter-arcs); internals ratio of g_dot ---
    g_dot: float = 0.060          # fan origin dot radius (ratio of u) -> 1.44 (tiny)
    g_stroke: float = 0.36        # arc stroke / g_dot  (matches the mark's ~0.36)
    g_ring0: float = 2.6          # innermost arc radius / g_dot
    g_pitch: float = 2.0          # arc pitch / g_dot
    g_rings: int = 3              # number of concentric arcs

    # --- rules / dividers (ratios of u) ---
    rule_sw: float = 0.045        # hairline stroke width        -> 1.08
    rule_op: float = 0.22         # hairline opacity on currentColor (see README: tokens grey)
    rule_w: float = 12.0          # reference divider width (multiples of u) -> 288
    rule_gap: float = 0.5         # gap between hairline and centre cluster  -> 12

    # --- dotted leader (ratios of u) ---
    lead_r: float = 0.05          # leader dot radius            -> 1.2
    lead_pitch: float = 0.30      # leader dot spacing           -> 7.2

    # ---- derived helpers -------------------------------------------------------------
    @property
    def gdot_px(self) -> float:
        return self.g_dot * self.u

    def fan_radii(self) -> list[float]:
        """Concentric quarter-arc radii for the broadcast fan (ratios of the origin dot)."""
        gd = self.gdot_px
        return [gd * (self.g_ring0 + k * self.g_pitch) for k in range(self.g_rings)]

    @property
    def fan_stroke(self) -> float:
        return self.g_stroke * self.gdot_px


# --------------------------------------------------------------------------- svg helpers
def f(x: float) -> str:
    """Compact float formatting (matches house style)."""
    s = f"{x:.2f}".rstrip("0").rstrip(".")
    return s if s != "-0" else "0"


def pt(cx: float, cy: float, r: float, deg: float) -> tuple[float, float]:
    """Point on a circle. `deg` measured clockwise from east in SCREEN space (y-down)."""
    return cx + r * cos(radians(deg)), cy + r * sin(radians(deg))


def arc_seg(cx: float, cy: float, r: float, a0: float, a1: float, sweep: int) -> str:
    """A single elliptical arc from angle a0 to a1 (screen-space degrees, y-down).

    `sweep`=1 draws in the direction of increasing screen angle (clockwise on screen).
    `large` is derived from the angular span so spans >180 still render correctly.
    """
    x0, y0 = pt(cx, cy, r, a0)
    x1, y1 = pt(cx, cy, r, a1)
    large = 1 if abs(a1 - a0) > 180 else 0
    return f"M{f(x0)} {f(y0)}A{f(r)} {f(r)} 0 {large} {sweep} {f(x1)} {f(y1)}"


def circle(cx: float, cy: float, r: float, fill: str) -> str:
    return f'<circle cx="{f(cx)}" cy="{f(cy)}" r="{f(r)}" fill="{fill}"/>'


def svg(view_w: float, view_h: float, body: str, *, themeable_preview: bool = True) -> str:
    """Wrap body in a tight-viewBox SVG. `style="color:INK"` gives a correct standalone
    light/paper preview while the embedded use still inherits via currentColor."""
    style = f' style="color:{INK}"' if themeable_preview else ""
    return (
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{f(view_w)}" height="{f(view_h)}" '
        f'viewBox="0 0 {f(view_w)} {f(view_h)}" role="img"{style}>\n  {body}\n</svg>\n'
    )


# --------------------------------------------------------------------------- broadcast fan
def fan_body(p: Params, cx: float, cy: float, *, mirror: bool = False,
            accent_dot: bool = False) -> tuple[str, float]:
    """A wifi-style quarter-fan: origin dot at (cx,cy) + concentric quarter-arcs in the
    upper-right quadrant (east -> north). `mirror=True` flips to the upper-left quadrant
    (a NW fan). Returns (svg_body, outer_radius). The origin dot is ink unless accent_dot."""
    # NE fan: arc from east (deg 0) up to north (deg -90), screen-space -> sweep=0.
    # NW fan: arc from west (deg 180) up to north (deg 270 i.e. -90), sweep=1.
    a0, a1, sweep = (0.0, -90.0, 0) if not mirror else (180.0, 270.0, 1)
    arcs = "".join(arc_seg(cx, cy, r, a0, a1, sweep) for r in p.fan_radii())
    outer = p.fan_radii()[-1]
    dot_fill = ACCENT_VAR if accent_dot else "currentColor"
    body = (
        f'<g fill="none" stroke="currentColor" stroke-width="{f(p.fan_stroke)}" '
        f'stroke-linecap="butt"><path d="{arcs}"/></g>'
        f'{circle(cx, cy, p.gdot_px, dot_fill)}'
    )
    return body, outer


# --------------------------------------------------------------------------- dots
def emit_dot(p: Params, name: str, kind: str):
    """Status / activity dot family, all in a shared 0..box square so they swap cleanly."""
    box = round(2 * (p.dot * p.u) + 2 * (p.ring_sw * p.u) + 4, 2)  # room for ring stroke
    c = box / 2
    if kind == "filled":
        body = circle(c, c, p.dot * p.u, ACCENT_VAR)
    elif kind == "ink":
        body = circle(c, c, p.dot * p.u, "currentColor")
    elif kind == "ring":
        r = p.dot * p.u
        body = (f'<circle cx="{f(c)}" cy="{f(c)}" r="{f(r)}" fill="none" '
                f'stroke="currentColor" stroke-width="{f(p.ring_sw * p.u)}"/>')
    elif kind == "party":
        body = circle(c, c, p.party * p.u, "currentColor")  # colour set by `color`/currentColor
    elif kind == "bullet":
        body = circle(c, c, p.bullet * p.u, ACCENT_VAR)
    else:
        raise ValueError(f"unknown dot kind: {kind}")
    (OUT / f"{name}.svg").write_text(svg(box, box, body))
    return name


# --------------------------------------------------------------------------- pulse / tick / cite
def emit_fan_glyph(p: Params, name: str, rings: int, *, accent_dot: bool = True):
    """A standalone broadcast fan glyph (pulse / tick / cite) in a tight viewBox.

    The fan occupies the upper-right quadrant; the origin dot sits at the lower-left so the
    glyph reads as 'signal radiating up-and-out'. pad accounts for the arc stroke + dot."""
    pp = Params(**{**p.__dict__, "g_rings": rings})
    outer = pp.fan_radii()[-1]
    pad = pp.fan_stroke / 2 + pp.gdot_px + 1.0
    w = outer + 2 * pad
    h = outer + 2 * pad
    cx = pad                 # origin dot near lower-left
    cy = h - pad
    body, _ = fan_body(pp, cx, cy, accent_dot=accent_dot)
    (OUT / f"{name}.svg").write_text(svg(w, h, body))
    return name


# --------------------------------------------------------------------------- dividers
def emit_divider(p: Params, name: str, kind: str):
    """Horizontal hairline rules with a broadcast accent. `kind`:
       plain : a bare hairline.
       dot   : hairline broken around a centred accent dot.
       arc   : hairline broken around a centred accent dot flanked by two mirrored fans.
    """
    W = p.rule_w * p.u
    sw = p.rule_sw * p.u
    hairline = (f'<g stroke="currentColor" stroke-width="{f(sw)}" stroke-opacity="{f(p.rule_op)}" '
                f'stroke-linecap="butt">')

    if kind == "plain":
        H = round(2 * sw + 2, 2)
        cy = H / 2
        body = f'{hairline}<line x1="0" y1="{f(cy)}" x2="{f(W)}" y2="{f(cy)}"/></g>'
        (OUT / f"{name}.svg").write_text(svg(W, H, body))
        return name

    if kind == "dot":
        dr = p.dot * p.u
        H = round(2 * dr + 2, 2)
        cy = H / 2
        cx = W / 2
        g = p.rule_gap * p.u
        body = (f'{hairline}'
                f'<line x1="0" y1="{f(cy)}" x2="{f(cx - dr - g)}" y2="{f(cy)}"/>'
                f'<line x1="{f(cx + dr + g)}" y1="{f(cy)}" x2="{f(W)}" y2="{f(cy)}"/></g>'
                f'{circle(cx, cy, dr, ACCENT_VAR)}')
        (OUT / f"{name}.svg").write_text(svg(W, H, body))
        return name

    if kind == "arc":
        # central red dot, flanked by an NE fan (left) and an NW fan (right). Fans sit ON the
        # baseline (their origin dots on the line); arcs rise above it.
        fp = Params(**{**p.__dict__, "g_rings": 2})  # divider fans = 2 rings (calmer)
        outer = fp.fan_radii()[-1]
        dr = p.dot * p.u
        pad = fp.fan_stroke / 2 + 2.0
        H = round(outer + pad + dr + 2, 2)
        baseline = H - dr - 1.0           # fan origin dots + red dot sit here
        cx = W / 2
        g = p.rule_gap * p.u
        sep = dr + 0.6 * p.u              # horizontal offset of each fan origin from centre
        left_x = cx - sep
        right_x = cx + sep
        lbody, _ = fan_body(fp, left_x, baseline)                 # NE fan (opens up-right)
        rbody, _ = fan_body(fp, right_x, baseline, mirror=True)   # NW fan (opens up-left)
        cluster_half = sep + outer
        line = (f'{hairline}'
                f'<line x1="0" y1="{f(baseline)}" x2="{f(cx - cluster_half - g)}" y2="{f(baseline)}"/>'
                f'<line x1="{f(cx + cluster_half + g)}" y1="{f(baseline)}" x2="{f(W)}" y2="{f(baseline)}"/></g>')
        body = f'{line}{lbody}{rbody}{circle(cx, baseline, dr, ACCENT_VAR)}'
        (OUT / f"{name}.svg").write_text(svg(W, H, body))
        return name

    raise ValueError(f"unknown divider kind: {kind}")


# --------------------------------------------------------------------------- leader
def emit_leader(p: Params, name: str):
    """A dotted leader (TOC / citation 'date . page . column' row): evenly spaced ink dots."""
    W = p.rule_w * p.u
    r = p.lead_r * p.u
    pitch = p.lead_pitch * p.u
    H = round(2 * r + 2, 2)
    cy = H / 2
    n = int(W // pitch)
    x0 = (W - (n - 1) * pitch) / 2
    dots = "".join(circle(x0 + k * pitch, cy, r, "currentColor") for k in range(n))
    (OUT / f"{name}.svg").write_text(svg(W, H, dots))
    return name


# --------------------------------------------------------------------------- main
def main():
    OUT.mkdir(parents=True, exist_ok=True)
    p = Params()
    written = []
    # dots
    written += [emit_dot(p, "dot-filled", "filled"),
                emit_dot(p, "dot-ink", "ink"),
                emit_dot(p, "dot-ring", "ring"),
                emit_dot(p, "dot-party", "party"),
                emit_dot(p, "bullet", "bullet")]
    # broadcast fan glyphs
    written += [emit_fan_glyph(p, "pulse", rings=3),
                emit_fan_glyph(p, "tick", rings=1),
                emit_fan_glyph(p, "cite", rings=2)]
    # dividers
    written += [emit_divider(p, "divider-plain", "plain"),
                emit_divider(p, "divider-dot", "dot"),
                emit_divider(p, "divider-arc", "arc")]
    # leader
    written += [emit_leader(p, "leader")]
    for name in written:
        print(f"wrote {name}.svg")


if __name__ == "__main__":
    main()
