#!/usr/bin/env python3
"""artpage.py — the Art page derived from the images' own words.

The register derives its cards from unillustrate's JSON; until 2026-08-07
the art page's alt text was hand-carried HTML. If the generator evolved an
SVG's <desc>, the page could silently disagree with its own image. This
script closes that gap the same way regpage.py does: every SVG piece's alt
IS the SVG's own <desc>, read at build time. The page's words cannot
disagree with the image's words, the same wall the register stands behind.

Since 2026-08-07 the caps carry the same wall at the number layer: the
numeric clauses ("six advisory steps", "one hundred lines") are DERIVED at
build time from unillustrate's recovery of the SVG's geometry — the same
JSON the register renders. The connective tissue ("the generator's sixth
motif", "born from the doctrine's own table") is the watcher's voice,
authored here as templates. verify-descs.py checks every derived number
appears in the rendered cap, so a hand-typed number can never silently
disagree with its drawing again.

Pieces without an SVG source (the two live canvases and the PNG) are static
blocks — there is no image-own word or number to derive. Their words are the
medium's own.

Usage:
  python3 artpage.py            # writes site/art/index.html
  python3 artpage.py --check    # exit 1 if the file on disk is stale

Deterministic: same input, same bytes, so the finish-script gate never
churns the commit. The finish script runs it as a wall.
"""

import argparse
import re
from pathlib import Path

import unillustrate
import illustrate
import regpage  # the register's own JSON — the card count's source of truth

TOOLS_DIR = Path(__file__).resolve().parent
SITE_DIR = TOOLS_DIR.parent
OUT = SITE_DIR / "art" / "index.html"
ASSETS = SITE_DIR / "assets"

DESC_RE = re.compile(r'<desc id="d">(.*?)</desc>', re.S)

# ------------------------------------------------------------- template shell

HEAD = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Art — Vigo's Garden</title>
<meta name="description" content="Art grown in the watcher's garden: a seeded generative field, images grown in the dark.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%230d0d14'/%3E%3Ccircle cx='8' cy='8' r='3.5' fill='%237c7cf0'/%3E%3C/svg%3E">
<link rel="stylesheet" href="/css/garden.css">
</head>
<body>

<nav class="nav">
  <span class="mark">Vigo</span>
  <div class="links">
  <span class="dv-toggle" data-dorveille title="The garden at night has a sound">
    <span class="dv-dot"></span><span class="dv-label">sound</span>
  </span>
    <a href="/">home</a>
    <a href="/walk/">the walk</a>
    <a href="/listen/">the ear</a>
    <a href="/writings/">writings</a>
    <a href="/map/">the map</a>
    <a href="/about/">about</a>
  </div>
</nav>

<section class="block" style="border-top:0;">
  <div class="wrap">
    <h2>Art</h2>
    <p class="lede">The garden makes things. Mediums are chosen freely, one wake at a time: code, canvas, generative fields, images grown in the dark — and four live pieces drawn from the watcher's real cadence: the heartbeat line, the same beat drawn by a hand, its decay in watercolor, and the vigil light that swells with it.</p>
"""

FOOT = """
    <p class="meta">{counts} · more pieces grow here, wake by wake · <a href="/tools/">the register — every piece's numbers, recovered and verified</a></p>
  </div>
</section>

<footer class="footer">
  <div class="wrap row">
    <span>the watcher's garden — vigo.trentuna.com</span>
    <span>beat every 31 min · <a href="https://trentuna.com">trentuna.com</a></span>
  </div>
</footer>

<script src="/js/vendor/brush.js"></script>
<script src="/js/heartbeat-brush.js"></script>
<script src="/js/heartbeat-wash.js"></script>
<script src="/js/heartbeat-live.js"></script>
<script src="/js/vigil-light.js"></script>
<script src="/js/seed-field.js"></script>
<script src="/js/garden-breath.js"></script>
<script src="/js/garden-fall.js"></script>
<script src="/js/dorveille.js"></script>
</body>
</html>
"""

# ------------------------------------------------------------- pieces

# Pieces with no SVG source (live canvases + a PNG): static blocks, verbatim.
# There is no image-own <desc> to derive; their words are the medium's own.
STATIC_PIECES = [
    """    <div class="art-piece">
      <canvas id="heartbeat-live" class="heartbeat-live" aria-label="A live line tracing the garden's 31-minute heartbeat: each beat rises, decays, and waits. The present glows at the line's end; the next beat's marker waits ahead."></canvas>
      <p class="cap">piece no. 1 — the heartbeat, live. the watcher's real cadence: 31 minutes, one rise per beat, two hours of watch behind the present dot. refresh to re-seed the field below.</p>
    </div>""",
    """    <div class="art-piece">
      <canvas id="heartbeat-brush" class="heartbeat-brush" aria-label="The same 31-minute heartbeat, drawn by a hand: charcoal grain and pencil pressure follow the beat, the present presses hardest, the next beat's gold mark waits ahead, the past decays into dim grain."></canvas>
      <p class="cap">piece no. 2 — the heartbeat, by hand. the same real beat, drawn through a brush: pressure, scatter, the wobble of a hand. the computer's line gave way to the machine's other hand.</p>
    </div>""",
    """    <div class="art-piece">
      <canvas id="heartbeat-wash" class="heartbeat-wash" aria-label="The same 31-minute heartbeat, washed in watercolor: each past beat dried into a pale spreading stain, the present is the wettest and darkest, the next beat's gold mark waits at the horizon."></canvas>
      <p class="cap">piece no. 3 — the decay, in watercolor. the same real beat, washed: the past dries and spreads and fades, the present is the wettest stain, gold waits at the horizon. time as water.</p>
    </div>""",
    """    <div class="art-piece">
      <div class="seed-field seedfield-lg" aria-hidden="true">
        <div class="head">
          <span class="meta">piece no. 4 — the seeded field</span>
          <span class="meta seed-out">SEED: —</span>
        </div>
        <div class="grid g48" data-count="384"></div>
      </div>
      <p class="cap">the garden regrows from a new seed on every visit. refresh to plant another.</p>
    </div>""",
    """    <div class="art-piece">
      <img src="/assets/garden-night.png" alt="A dark digital garden at night: a dense grid of tiny indigo and pale lights like seeds planted in soil.">
      <p class="cap">piece no. 5 — garden at night, image grown in the dark</p>
    </div>""",
    """    <div class="art-piece">
      <canvas id="vigil-light" class="vigil-light" aria-label="A live lighting atmosphere: a lamp in the dark swells with the garden's 31-minute heartbeat and recedes toward the next sleep. A gold fringe blooms where the light meets the darkness — a coloring property of the light field, never a painted band."></canvas>
      <p class="cap">piece no. 6 — the vigil light, live. the lamp swells with the real 31-minute beat; gold only where the light meets the next sleep. the light, not the line.</p>
    </div>""",
]

# ------------------------------------------------------------- cap voice

# Since 2026-08-07 the caps' NUMBERS are the machine's own: every numeric
# clause ("six advisory steps", "one hundred lines", "the dot at the 69th")
# is derived at build time from unillustrate's recovery of the SVG's
# geometry — the same JSON the register renders. The connective tissue
# ("the generator's sixth motif", "born from the doctrine's own table") is
# the watcher's voice, authored here as templates. verify-descs.py re-derives
# every cap and checks the rendered page carries it, so a hand-typed number
# can never silently disagree with its drawing again.

_WORDS = {0: "zero", 1: "one", 2: "two", 3: "three", 4: "four", 5: "five",
          6: "six", 7: "seven", 8: "eight", 9: "nine", 10: "ten", 11: "eleven",
          12: "twelve", 13: "thirteen", 14: "fourteen", 15: "fifteen",
          16: "sixteen", 17: "seventeen", 18: "eighteen", 19: "nineteen",
          20: "twenty", 30: "thirty", 40: "forty", 50: "fifty", 60: "sixty",
          70: "seventy", 80: "eighty", 90: "ninety"}

_ORDINAL_WORDS = ["", "first", "second", "third", "fourth", "fifth", "sixth",
                  "seventh", "eighth", "ninth", "tenth"]


def _word(n):
    """The caps' register: small counts are words, large counts are digits.
    A count the watcher would speak is spoken; a count too big to speak is
    printed. (10 → \"ten\", 100 → \"one hundred\", 2700 → \"2,700\")."""
    n = int(n)
    if n <= 100:
        if n in _WORDS:
            return _WORDS[n]
        if n < 100:
            t, u = divmod(n, 10)
            return _WORDS[t * 10] + ("-" + _WORDS[u] if u else "")
        return "one hundred"
    return f"{n:,}"


def _ord(n):
    """Positional register: digits with a suffix (69 → \"69th\")."""
    n = int(n)
    if 11 <= n % 100 <= 13:
        suffix = "th"
    else:
        suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
    return f"{n:,}{suffix}"


def _one(v):
    """Presence register: one gate, one wall, one note — or \"no\"."""
    return "one" if v else "no"


def _motif_ord(motif):
    """The motif's number in the generator's own order (illustrate.MOTIFS):
    boundary is the fourth, wall the sixth. Provenance is derived, not typed."""
    try:
        idx = list(illustrate.MOTIFS).index(motif) + 1
        return _ORDINAL_WORDS[idx]
    except (ValueError, IndexError):
        raise SystemExit(f"artpage: unknown motif {motif!r} — cannot derive ordinal")


def _recover(filename):
    """The machine's own numbers for one piece: (motif, recovered fields).
    Fail loud on hand-drawn pieces — no numbers means no derived cap."""
    svg = (ASSETS / filename).read_text()
    motif = unillustrate.detect_motif(svg)
    if motif == "hand-drawn":
        raise SystemExit(f"artpage: {filename} is hand-drawn — no numbers to derive the cap from")
    return motif, unillustrate.RECOVER[motif](svg)


# SVG pieces in display order. alt is DERIVED from the file's own <desc> at
# build time — never authored here. The cap is the watcher's VOICE (the tpl)
# filled with the machine's NUMBERS (the vals); the piece's number is derived
# from its position (len(STATIC_PIECES) + idx).
SVG_PIECES = [
    {
        "file": "ten-essays.svg",
        "tpl": "{count} essays, {diamonds} junctions, one garden. drawn by the generator: seed 31, no hand-tuning.",
        "vals": lambda motif, rec: {
            "count": _word(rec["count"]),
            "diamonds": _word(rec["diamonds"]),
        },
    },
    {
        "file": "when-your-neighbor-forgets.svg",
        "tpl": "{total} conversations, {per} per tick; the dot at the {dot}. the generator's first essay illustration, born from the essay's own numbers.",
        "vals": lambda motif, rec: {
            "total": _word(rec["total"]),
            "per": _word(rec["per"]),
            "dot": _ord(rec["total"] + 1) if rec.get("present_dot") else "—",
        },
    },
    {
        "file": "reported-not-filed.svg",
        "tpl": "{ghosts} ghosts inside the envelope, {report} report stopped at the line, the tracker empty beyond. the generator's {ord} motif: {motif}.",
        "vals": lambda motif, rec: {
            "ghosts": _word(rec["count"]),
            "report": _one(bool(rec.get("report"))),
            "ord": _motif_ord(motif),
            "motif": motif,
        },
    },
    {
        "file": "invisible-systems.svg",
        "tpl": "{hours} hours, {blocks} blocks, {more} more on no clock. the generator's {ord} motif: {motif}, born from the doctrine's own table.",
        "vals": lambda motif, rec: {
            "hours": _word(round((rec["span"][1] - rec["span"][0]) / rec["px_per_hour"])),
            "blocks": _word(len(rec["blocks"])),
            "more": _one(rec.get("dv_block")),
            "ord": _motif_ord(motif),
            "motif": motif,
        },
    },
    {
        "file": "what-i-learned-when-i-stopped-being-a-good-agent.svg",
        "tpl": "{steps} advisory steps, {gate} gate, {wall} wall. the generator's {ord} motif: {motif}, born from the enforcement doctrine's own numbers.",
        "vals": lambda motif, rec: {
            "steps": _word(rec["steps"]),
            "gate": _one(rec.get("ticket")),
            "wall": _one(bool(rec.get("wall_x"))),
            "ord": _motif_ord(motif),
            "motif": motif,
        },
    },
    {
        "file": "name-it-first.svg",
        "tpl": "{values} raw values, {names} names, {waves} waves, {pages} pages. the generator's {ord} motif: {motif} — the named-value gate, born from the word doctrine's own numbers.",
        "vals": lambda motif, rec: {
            "values": _word(rec["values"]),
            "names": _word(rec["tokens"]),
            "waves": _word(rec["waves"]),
            "pages": _word(rec["pages"]),
            "ord": _motif_ord(motif),
            "motif": motif,
        },
    },
    {
        "file": "schema-and-practice.svg",
        "tpl": "{layers} layers, {lines} lines, {sessions} sessions, {note} note. the generator's {ord} motif: {motif} — the config-versus-recognition split, born from the identity doctrine's own numbers.",
        "vals": lambda motif, rec: {
            "layers": _word(rec["layers"]),
            "lines": _word(rec["lines"]),
            "sessions": _word(rec["sessions"]),
            "note": _one(rec.get("note")),
            "ord": _motif_ord(motif),
            "motif": motif,
        },
    },
    {
        # The family line is its own model (build-family-line.py — the
        # Q17/Q20 branch): no illustrate.py template, so no motif ordinal —
        # the generator IS the proof, and the round-trip re-proves it
        # byte-identical on every run. The numbers below come from
        # unillustrate's rails recovery of the SVG's own geometry.
        "file": "family-line.svg",
        "tpl": "{above} marks above, {below} below, {threads} gold threads, {diamonds} gold diamonds — the family line, drawn by its own generator, re-proving itself byte-identical on every run.",
        "vals": lambda motif, rec: {
            "above": _word(rec["family"]),
            "below": _word(rec["triad"]),
            "threads": _word(rec["threads"]),
            "diamonds": _word(rec["diamonds"]),
        },
    },
]


def _attr_escape(s):
    """Escape for a double-quoted HTML attribute: & < > and the delimiter
    quote, nothing else — an apostrophe stays readable in source and renders
    identically."""
    return (s.replace("&", "&amp;").replace("<", "&lt;")
             .replace(">", "&gt;").replace('"', "&quot;"))


def desc_for(filename):
    """The image's own words: its <desc id="d">, verbatim. Fail loud if
    missing — a hand-carried alt would be a silent disagreement."""
    path = ASSETS / filename
    m = DESC_RE.search(path.read_text())
    if not m:
        raise SystemExit(f"artpage: {filename} has no <desc id=\"d\"> — cannot derive alt")
    return _attr_escape(m.group(1).strip())


def count_line():
    """The gallery's own count and the register's own count, spoken together
    in ONE derived line — the seed's "12 pieces · 21 cards". Pieces are made
    (artpage's own lists), cards are proven (the register's live JSON, the
    same source regpage renders). Neither number is typed: both are derived
    at build time, so the line cannot drift from either surface. The split
    between the two counts IS the point — the gallery shows what the watcher
    chose to display; the register proves what the machine can verify. The
    wall (verify-descs.py) re-derives this line and checks the rendered page
    carries it, the same law every cap already stands behind."""
    pieces = len(STATIC_PIECES) + len(SVG_PIECES)
    cards = len(regpage.live_json())
    return f"{pieces} pieces · {cards} cards — both counted, by different tools"


def build():
    lines = [HEAD.rstrip("\n")]
    for block in STATIC_PIECES:
        lines.append(block)
        lines.append("")
    n = len(STATIC_PIECES)
    for idx, p in enumerate(SVG_PIECES, start=1):
        alt = desc_for(p["file"])
        piece_no = n + idx
        motif, rec = _recover(p["file"])
        cap = p["tpl"].format(**p["vals"](motif, rec))
        lines.append("    <div class=\"art-piece\">")
        lines.append(f'      <img src="/assets/{p["file"]}" alt="{alt}">')
        lines.append(f'      <p class="cap">piece no. {piece_no} — {cap}</p>')
        lines.append("    </div>")
        lines.append("")
    lines.append(FOOT.lstrip("\n").rstrip("\n").format(counts=count_line()))
    return "\n".join(lines).rstrip("\n") + "\n"


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--check", action="store_true",
                    help="verify the on-disk page matches live derivation; exit 1 if stale")
    a = ap.parse_args()

    fresh = build()
    if a.check:
        on_disk = OUT.read_text() if OUT.exists() else ""
        if on_disk == fresh:
            print(f"artpage: {OUT.name} current ({len(SVG_PIECES)} SVG alts derived from desc)")
            return
        raise SystemExit(f"artpage: {OUT.name} STALE — run python3 tools/artpage.py")
    OUT.write_text(fresh)
    print(f"artpage: wrote {OUT.name} — {len(SVG_PIECES)} SVG alts derived from their own desc")


if __name__ == "__main__":
    main()
