#!/usr/bin/env python3
"""Draws the paper's SVG figures from out/summary.json and out/mutants.json."""
import json, os, sys
R = sys.argv[1]; DST = sys.argv[2]
os.makedirs(DST, exist_ok=True)
S = json.load(open(os.path.join(R, "out/summary.json")))
M = json.load(open(os.path.join(R, "out/mutants.json")))

INK, MUTED, RULE, BG = "#1b1f24", "#57606a", "#d0d7de", "#ffffff"
DART, SWIFT, KOTLIN = "#0b6bcb", "#c4410c", "#6741d9"
FONT = "font-family=\"Inter, 'Helvetica Neue', Arial, sans-serif\""

def svg(w, h, body, title):
    return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" height="{h}" role="img" aria-label="{title}">'
            f'<title>{title}</title><rect width="{w}" height="{h}" fill="{BG}"/>{body}</svg>\n')

def text(x, y, s, size=12, fill=INK, anchor="start", weight="400"):
    s = s.replace("&", "&amp;").replace("<", "&lt;")
    return f'<text x="{x}" y="{y}" {FONT} font-size="{size}" fill="{fill}" text-anchor="{anchor}" font-weight="{weight}">{s}</text>'

# ---------- Fig 1: pipeline ----------
def box(x, y, w, h, title, lines, color):
    b = f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="8" fill="#f6f8fa" stroke="{color}" stroke-width="1.6"/>'
    b += text(x + 12, y + 22, title, 13, color, weight="600")
    for i, l in enumerate(lines):
        b += text(x + 12, y + 42 + 17 * i, l, 11.5, MUTED)
    return b
def arrow(x1, y1, x2, y2):
    return (f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{MUTED}" stroke-width="1.4" marker-end="url(#a)"/>')
defs = f'<defs><marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="{MUTED}"/></marker></defs>'
b = defs
b += box(16, 20, 190, 112, "Golden vectors", ["7,658 publications", "29 IANA zones", "550,420 query instants", "seeded, deterministic"], INK)
b += box(232, 20, 196, 112, "Dart (TZ=zone)", ["publishes payload as app", "answer 1: app reconcile", "answer 2: currentPoolIndex", "verbatim functions"], DART)
b += box(454, 20, 170, 52, "Swift (TZ=zone)", ["rule + WidgetKit timeline"], SWIFT)
b += box(454, 80, 170, 52, "Kotlin/JVM (TZ=zone)", ["rule + alarm chain"], KOTLIN)
b += arrow(206, 76, 230, 76) + arrow(428, 50, 452, 46) + arrow(428, 100, 452, 106)
b += box(160, 168, 330, 92, "Oracle and analysis", ["pairwise agreement on pool index at each instant", "instant rule vs displayed entry, per platform", "mechanical root-cause rules; residue reported"], INK)
b += arrow(539, 132, 420, 166) + arrow(330, 132, 330, 166)
b += text(16, 290, "Each runtime reads the same payload; no implementation sees another's answers.", 11.5, MUTED)
open(os.path.join(DST, "fig1-pipeline.svg"), "w").write(svg(640, 304, b, "Differential test pipeline: generator, Dart publisher, Swift and Kotlin harnesses, oracle"))

# ---------- Fig 2 (hero): root causes ----------
rows = [
    ("D1", "Anchor truncated to whole seconds", "Dart publisher", DART, "D1 sub-second anchor truncation"),
    ("D2", "Today override adds 24 h, not one day", "Dart app", DART, "D2 Today-override +24h across DST (app)"),
    ("D7", "Elapsed count vs own list in a gap", "Dart app", DART, "D7 Dart elapsed count vs its own published list (gap)"),
    ("D4a", "Skipped minute resolved to end of gap", "Swift", SWIFT, "D4a Swift tail boundary (gap)"),
    ("D4b", "Day count after a midnight transition", "Swift", SWIFT, "D4b Swift tail day count after a midnight transition"),
    ("D5", "Timeline entry dates vs step rule", "Swift", SWIFT, "D5 Swift timeline shows stale entry"),
    ("D3", "Repeated minute resolved to later hour", "Kotlin", KOTLIN, "D3 Kotlin tail boundary (overlap)"),
    ("D6", "Next alarm keeps shifted wall time", "Kotlin", KOTLIN, "D6 Kotlin alarm shows stale render"),
]
W, top, rh = 640, 58, 34
b = text(16, 24, "Eight divergences in the shipped code, by the runtime that deviates", 14, INK, weight="600")
b += text(16, 43, "Bar: time zones (of 29) where it appeared. Label: zones · publications affected.", 11.5, MUTED)
x0, bw = 330, 190
for i, (cid, label, who, col, key) in enumerate(rows):
    y = top + i * rh
    c = S["classes"][key]
    nz, ns = len(c["zones"]), c["scenarios"]
    b += text(16, y + 20, cid, 12, col, weight="600")
    b += text(52, y + 14, label, 11.5, INK)
    b += text(52, y + 28, who, 11, MUTED)
    b += f'<rect x="{x0}" y="{y + 7}" width="{bw}" height="16" fill="#eef1f4"/>'
    b += f'<rect x="{x0}" y="{y + 7}" width="{bw * nz / 29:.1f}" height="16" fill="{col}"/>'
    b += text(x0 + bw * nz / 29 + 6, y + 20, f"{nz} · {ns:,}", 11, INK).replace("<text ", '<text stroke="#ffffff" stroke-width="4" paint-order="stroke" ', 1)
for k in (0, 10, 20, 29):
    xx = x0 + bw * k / 29
    b += f'<line x1="{xx:.1f}" y1="{top - 4}" x2="{xx:.1f}" y2="{top + len(rows) * rh}" stroke="{RULE}" stroke-width="0.6" stroke-dasharray="2 3"/>'
    b += text(xx, top + len(rows) * rh + 14, str(k), 11, MUTED, "middle")
b += text(16, top + len(rows) * rh + 34, "None of the eight made any of the 22 string-matching contract tests fail.", 11.5, INK, weight="600")
open(os.path.join(DST, "fig2-divergences.svg"), "w").write(svg(W, top + len(rows) * rh + 48, b, "Eight divergences found across Dart, Swift and Kotlin, with the number of time zones and publications each affected"))

# ---------- Fig 3: DST night strips (illustrative, from measured examples) ----------
# Fall-back night, New York, boundary 01:30 (D3), and spring-forward, Los Angeles, boundary 02:30 (D4a).
def strip(y, title, ticks, segs, note):
    out = text(16, y, title, 12.5, INK, weight="600")
    x0, x1 = 110, 560
    for (lab, t) in ticks:
        xx = x0 + (x1 - x0) * t
        out += f'<line x1="{xx:.1f}" y1="{y + 10}" x2="{xx:.1f}" y2="{y + 96}" stroke="{RULE}" stroke-width="0.8"/>'
        out += text(xx, y + 110, lab, 11, MUTED, "middle")
    for r, (name, col, changes) in enumerate(segs):
        yy = y + 18 + r * 26
        out += text(16, yy + 12, name, 11.5, col, weight="600")
        prev = x0
        for k, (t, q) in enumerate(changes):
            xx = x0 + (x1 - x0) * t
            nxt = x0 + (x1 - x0) * (changes[k + 1][0] if k + 1 < len(changes) else 1)
            shade = col if q == "new" else "#eef1f4"
            out += f'<rect x="{xx:.1f}" y="{yy}" width="{nxt - xx:.1f}" height="16" fill="{shade}" opacity="{0.85 if q == "new" else 1}"/>'
        out += text(x1 + 4, yy + 12, "", 11)
    out += text(16, y + 128, note, 11, MUTED)
    return out
b = ""
b += strip(22, "Fall-back night, New York, change set for 01:30 (D3, 96+ days after last open)",
           [("00:30 EDT", 0.0), ("01:30 EDT", 0.25), ("01:30 EST", 0.5), ("02:30 EST", 0.75)],
           [("Dart app", DART, [(0, "old"), (0.25, "new")]), ("Swift", SWIFT, [(0, "old"), (0.25, "new")]), ("Kotlin", KOTLIN, [(0, "old"), (0.5, "new")])],
           "Java resolves the repeated 01:30 to its second (standard-time) instance: Kotlin is an hour late.")
b += strip(182, "Spring-forward night, Los Angeles, change set for 02:30 (D4a)",
           [("01:00 PST", 0.0), ("03:00 PDT", 0.3), ("03:30 PDT", 0.55), ("04:30 PDT", 0.8)],
           [("Dart app", DART, [(0, "old"), (0.55, "new")]), ("Swift", SWIFT, [(0, "old"), (0.3, "new")]), ("Kotlin", KOTLIN, [(0, "old"), (0.55, "new")])],
           "02:30 does not exist. Dart and Java move it to 03:30; Foundation returns 03:00: Swift is 30 min early.")
b += f'<rect x="110" y="330" width="14" height="10" fill="#eef1f4"/>' + text(130, 339, "previous quote", 11, MUTED)
b += f'<rect x="230" y="330" width="14" height="10" fill="{DART}"/><rect x="244" y="330" width="14" height="10" fill="{SWIFT}"/><rect x="258" y="330" width="14" height="10" fill="{KOTLIN}"/>' + text(280, 339, "next quote", 11, MUTED)
open(os.path.join(DST, "fig3-dst-nights.svg"), "w").write(svg(640, 352, b, "Two daylight-saving nights showing when each runtime switches to the next quote"))

# ---------- Fig 4: mutant kill matrix ----------
rows = M["mutants"]
W, top, rh = 640, 64, 22
b = text(16, 24, "Which oracle notices a change to the native rotation code?", 14, INK, weight="600")
b += text(16, 43, "Filled = mutant detected (a contract test failed / an answer changed). 14 single-line mutants.", 11.5, MUTED)
cols = [("22 contract tests", 420), ("Differential test", 540)]
for name, x in cols:
    b += text(x, top - 6, name, 11.5, INK, "middle", "600")
for i, m in enumerate(rows):
    y = top + i * rh
    col = SWIFT if m["platform"] == "swift" else KOTLIN
    b += text(16, y + 14, m["id"], 11.5, col, weight="600")
    b += text(52, y + 14, m["description"], 11.5, INK if m["kind"] == "behaviour" else MUTED)
    for (name, x), hit in zip(cols, (m["contractKilled"], m["differentialKilled"])):
        fill = INK if hit else BG
        b += f'<circle cx="{x}" cy="{y + 10}" r="6" fill="{fill}" stroke="{INK}" stroke-width="1.2"/>'
y = top + len(rows) * rh + 18
b += text(16, y, f'Behaviour mutants detected: contract tests {M["contractBehaviourKilled"]} of {M["behaviourCount"]}, differential {M["differentialBehaviourKilled"]} of {M["behaviourCount"]}.', 11.5, INK, weight="600")
b += text(16, y + 18, f'Behaviour-preserving refactors flagged: contract tests {M["contractRefactorKilled"]} of {M["refactorCount"]}, differential {M["differentialRefactorKilled"]} of {M["refactorCount"]}.', 11.5, INK)
open(os.path.join(DST, "fig4-mutants.svg"), "w").write(svg(W, y + 30, b, "Mutant detection by string-matching contract tests versus the differential test"))
print("figures written to", DST)
