#!/usr/bin/env -S uv run --quiet python
"""Politick "Broadcast · Soft red" — the MARK (parametric vector generator).

The mark is a lowercase **"i"** (the "i" of Polit*i*ck) that doubles as a broadcast icon:

  - a soft-red **dot** = the tittle of the "i" AND the origin of the broadcast;
  - a vertical ink **stem** = the body of the "i", a clean flat-capped bar (no rounded
    corners), centred under the dot;
  - a set of concentric **broadcast arcs** = true right-opening semicircles centred on the
    dot, radiating outward. Their lower endpoints tuck behind the stem, so the left side of
    the mark is just dot + stem and all the "signal" bulges to the right.

GEOMETRY (single source of truth = the Params dataclass)
--------------------------------------------------------
Everything is a ratio of one base `unit` = the DOT RADIUS. Proportions were read off the
decided render (`design/brand/broadcast-softred/key-visual.png`) — NOT traced — then made
exact and re-tunable:

  dot radius      1.00 u          arc stroke      ~0.44 u
  stem width      1.00 u          dot->stem gap   ~0.45 u
  inner arc r     ~2.50 u         outer arc r     ~7.30 u   (rings evenly spaced between)

All curves are TRUE elliptical-arc (`A`) path commands — never sampled polylines. Strokes
use `currentColor` (so the mark inherits text colour and works on paper AND ink); the accent
dot uses `fill="var(--pk-accent, #B84A38)"`. Re-runnable: `uv run mark.py`.
"""
from __future__ import annotations

import math
from dataclasses import dataclass
from pathlib import Path

OUT = Path(__file__).parent

# --------------------------------------------------------------------------- brand colours
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. `unit` = the dot radius; every dimension is a ratio of it."""
    unit: float = 40.0            # base = dot radius, in user-space px

    # --- the dot (tittle + broadcast origin) ---
    dot: float = 1.0             # dot radius / unit

    # --- the stem (body of the "i") ---
    stem_w: float = 1.0          # stem width / unit (a flat-capped bar; ~2.6x the arc stroke)
    stem_gap: float = 0.45       # gap between dot bottom and stem top / unit (the "i" break)
    stem_foot: float = 0.25      # how far the stem extends past the outer arc baseline / unit
                                 # (covers the outer arc's stroke + matches the reference foot)

    # --- the broadcast arcs (concentric right-opening semicircles, centred on the dot) ---
    rings: int = 5               # number of arcs in the full mark (the key-visual reads 5)
    inner: float = 1.95          # innermost arc radius / unit (hugs the dot, ~one pitch out)
    outer: float = 7.3           # outermost arc radius / unit
    stroke: float = 0.38         # arc stroke width / unit
    arc_deg: float = 180.0       # angular extent of each arc, centred on +x (180 = semicircle)

    # --- layout ---
    pad: float = 0.4             # artboard padding around the tight bbox / unit
    round_caps: bool = False     # arc stroke caps: False = butt (editorial), True = round

    def ring_radii(self) -> list[float]:
        """Evenly spaced concentric arc radii from inner*unit to outer*unit (rings of them)."""
        if self.rings <= 1:
            return [self.outer * self.unit]
        return [
            (self.inner + (self.outer - self.inner) * k / (self.rings - 1)) * self.unit
            for k in range(self.rings)
        ]


# --------------------------------------------------------------------------- favicon params
# A separate, OPTICALLY RETUNED build for 16-32 px: fewer + heavier arcs, a bigger dot, a
# shorter stem. NOT a shrunk full mark — redrawn so it survives tiny.
FAVICON = Params(
    unit=40.0,
    dot=1.25,        # larger dot — the one thing that must read at 16px
    stem_w=1.15,     # heavier stem
    stem_gap=0.42,
    stem_foot=0.45,  # heavier favicon stroke needs a deeper foot to cover the outer arc
    rings=2,         # only two arcs survive small
    inner=2.6,
    outer=4.7,       # tighter — keep the icon compact / nearly square
    stroke=0.72,     # much heavier so the arcs don't vanish
    arc_deg=180.0,
    pad=0.55,
    round_caps=False,
)


# --------------------------------------------------------------------------- svg helpers
def f(x: float) -> str:
    """Compact float formatting."""
    return f"{x:.2f}".rstrip("0").rstrip(".")


def arc_path(cx: float, cy: float, r: float, deg: float) -> str:
    """A true circular arc of angular extent `deg`, centred on the +x axis through (cx,cy).

    Endpoints are placed symmetrically above/below the +x axis at angle +-deg/2, so the
    arc opens to the right. deg=180 -> a clean right semicircle with endpoints on the
    vertical line x=cx (top and bottom)."""
    half = math.radians(deg / 2)
    x0, y0 = cx + r * math.cos(-half), cy + r * math.sin(-half)   # top endpoint
    x1, y1 = cx + r * math.cos(half), cy + r * math.sin(half)     # bottom endpoint
    large = 1 if deg > 180 else 0
    return f"M{f(x0)} {f(y0)}A{f(r)} {f(r)} 0 {large} 1 {f(x1)} {f(y1)}"


# --------------------------------------------------------------------------- build
def build_mark(p: Params) -> dict:
    """Compute all geometry for the mark in a coordinate system with the DOT at the origin.

    Returns the arc path data, the stem rect, the dot, the tight bounding box and stroke.
    The caller translates everything into a positive viewBox.
    """
    u = p.unit
    radii = p.ring_radii()
    r_out = radii[-1]
    sw = p.stroke * u
    dot_r = p.dot * u
    stem_w = p.stem_w * u

    # arcs: concentric, centred on the dot (origin), opening right
    arcs = [arc_path(0.0, 0.0, r, p.arc_deg) for r in radii]

    # stem: a flat-capped bar centred under the dot, from just below the dot down to the
    # outermost arc's baseline (its lower endpoints tuck inside the stem).
    stem_top = dot_r + p.stem_gap * u
    stem_bot = r_out + p.stem_foot * u        # extends past the outer arc -> flat clean foot
    stem = dict(x=-stem_w / 2, y=stem_top, w=stem_w, h=stem_bot - stem_top)

    # tight bbox (account for stroke half-width on the arcs and dot radius on the left)
    left = -max(dot_r, stem_w / 2)
    right = r_out + sw / 2
    top = -(r_out + sw / 2)
    bot = stem_bot
    return dict(
        arcs=arcs, stem=stem, dot_r=dot_r, stroke=sw,
        bbox=(left, top, right - left, bot - top), radii=radii,
    )


# --------------------------------------------------------------------------- emit
def _mark_group(m: dict, p: Params, accent: str, ink: str = "currentColor") -> str:
    """The mark's inner SVG markup (arcs + stem in `ink`, dot in `accent`), at the dot=origin
    coordinate system. The caller wraps it in <svg> with a translate to a positive viewBox."""
    cap = "round" if p.round_caps else "butt"
    arcs_d = " ".join(m["arcs"])
    s = m["stem"]
    return (
        f'<g fill="none" stroke="{ink}" stroke-width="{f(m["stroke"])}" stroke-linecap="{cap}">'
        f'<path d="{arcs_d}"/></g>'
        f'<rect x="{f(s["x"])}" y="{f(s["y"])}" width="{f(s["w"])}" height="{f(s["h"])}" '
        f'fill="{ink}"/>'
        f'<circle cx="0" cy="0" r="{f(m["dot_r"])}" fill="{accent}"/>'
    )


def emit_mark(p: Params, accent: str = ACCENT_VAR, ink: str = "currentColor",
              pad: float | None = None, extra_pad: float = 0.0, label: str = "Politick") -> str:
    """A standalone, themeable SVG of just the mark, with a tight viewBox.

    `pad` overrides p.pad; `extra_pad` adds clear-space (for the clear-space diagram)."""
    m = build_mark(p)
    bx, by, bw, bh = m["bbox"]
    pad = (p.pad if pad is None else pad) * p.unit + extra_pad * p.unit
    vb = (bx - pad, by - pad, bw + 2 * pad, bh + 2 * pad)
    return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="{f(vb[0])} {f(vb[1])} {f(vb[2])} {f(vb[3])}"
     role="img" aria-label="{label}" style="color:{INK}">
  {_mark_group(m, p, accent, ink)}
</svg>
'''


def emit_clearspace(p: Params) -> str:
    """Clear-space + min-size diagram: the mark inside its safe area (= stem-width margin)
    with guide rules and a caption."""
    m = build_mark(p)
    bx, by, bw, bh = m["bbox"]
    cs = p.stem_w * p.unit            # clear space = one stem width on every side
    pad = cs + 0.6 * p.unit
    vb = (bx - pad, by - pad, bw + 2 * pad, bh + 2 * pad)
    # safe-area rectangle = tight bbox grown by the clear space
    sx, sy, sw, sh = bx - cs, by - cs, bw + 2 * cs, bh + 2 * cs
    guide = '#B0A99A'
    return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="{f(vb[0])} {f(vb[1])} {f(vb[2])} {f(vb[3])}"
     role="img" aria-label="Politick mark clear space" style="color:{INK}">
  <rect x="{f(sx)}" y="{f(sy)}" width="{f(sw)}" height="{f(sh)}" fill="none"
        stroke="{guide}" stroke-width="{f(0.06 * p.unit)}" stroke-dasharray="{f(0.3 * p.unit)} {f(0.3 * p.unit)}"/>
  {_mark_group(m, p, ACCENT_VAR)}
</svg>
'''


def emit_lockup(p: Params, layout: str = "horizontal", accent: str = ACCENT_VAR,
                ink: str = "currentColor") -> str:
    """A logo lockup: mark + the "Politick" wordmark as LIVE TEXT (system serif stand-in;
    the production face is var(--pk-font-display)). layout = 'horizontal' | 'stacked'."""
    m = build_mark(p)
    bx, by, bw, bh = m["bbox"]
    u = p.unit
    # wordmark metrics (serif, optical): cap height keyed to the mark height
    cap = bh * 0.46
    font_px = cap / 0.70                      # serif cap-height is ~0.7 em
    # Production face = var(--pk-font-display). The rest are stand-ins so this SVG also
    # renders as a newspaper-of-record serif outside the app (and in the cairosvg proofs).
    font = ("var(--pk-font-display, 'Iowan Old Style', 'Palatino Linotype', Georgia, "
            "'Noto Serif Display', 'PT Serif', 'Liberation Serif', 'Times New Roman', serif)")
    word = "Politick"
    if layout == "horizontal":
        gap = 1.1 * u
        word_x = bx + bw + gap
        word_baseline = by + bh * 0.5 + cap * 0.5     # vertically centred on the mark
        text = (f'<text x="{f(word_x)}" y="{f(word_baseline)}" fill="{ink}" '
                f'font-family="{font}" font-size="{f(font_px)}" '
                f'letter-spacing="{f(-0.01 * font_px)}">{word}</text>')
        # rough advance width of "Politick" in a serif ~ 0.52em/char
        word_w = font_px * 0.52 * len(word)
        pad = 0.5 * u
        vb = (bx - pad, by - pad, (word_x + word_w) - bx + 2 * pad, bh + 2 * pad)
    else:  # stacked
        gap = 0.7 * u
        word_baseline = by + bh + gap + cap
        word_w = font_px * 0.52 * len(word)
        # centre the wordmark under the mark's optical centre
        mark_cx = bx + bw * 0.5
        word_x = mark_cx - word_w * 0.5
        text = (f'<text x="{f(word_x)}" y="{f(word_baseline)}" fill="{ink}" '
                f'font-family="{font}" font-size="{f(font_px)}" '
                f'letter-spacing="{f(-0.01 * font_px)}">{word}</text>')
        pad = 0.5 * u
        total_bottom = word_baseline + font_px * 0.22
        left = min(bx, word_x)
        right = max(bx + bw, word_x + word_w)
        vb = (left - pad, by - pad, (right - left) + 2 * pad, (total_bottom - by) + 2 * pad)
    return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="{f(vb[0])} {f(vb[1])} {f(vb[2])} {f(vb[3])}"
     role="img" aria-label="Politick" style="color:{INK}">
  {_mark_group(m, p, accent, ink)}
  {text}
</svg>
'''


# --------------------------------------------------------------------------- main
def main():
    p = Params()

    (OUT / "mark.svg").write_text(emit_mark(p))
    (OUT / "mark-mono.svg").write_text(emit_mark(p, accent="currentColor"))
    (OUT / "favicon.svg").write_text(emit_mark(FAVICON, label="Politick"))
    (OUT / "favicon-mono.svg").write_text(emit_mark(FAVICON, accent="currentColor", label="Politick"))
    (OUT / "mark-clearspace.svg").write_text(emit_clearspace(p))
    (OUT / "mark-horizontal.svg").write_text(emit_lockup(p, "horizontal"))
    (OUT / "mark-stacked.svg").write_text(emit_lockup(p, "stacked"))

    radii = p.ring_radii()
    print("wrote mark.svg, mark-mono.svg, favicon.svg, favicon-mono.svg,")
    print("      mark-clearspace.svg, mark-horizontal.svg, mark-stacked.svg")
    print(f"full mark: rings={p.rings} radii(u)={[round(r/p.unit,2) for r in radii]}")
    print(f"favicon:   rings={FAVICON.rings} radii(u)={[round(r/FAVICON.unit,2) for r in FAVICON.ring_radii()]}")


if __name__ == "__main__":
    main()
