#!/usr/bin/env python3
"""Stasis as withheld difference: stacked mistimed reprints of one motif.

Writes render-1.png beside this file. No network, no extra inputs.
"""

from __future__ import annotations

import math
import random
from pathlib import Path

from PIL import Image, ImageDraw

# Flexoki, inlined (https://stephango.com/flexoki)
PAPER = (255, 252, 240)
BLACK = (16, 15, 15)
BASE_200 = (206, 205, 195)
BASE_300 = (183, 181, 172)
BASE_600 = (111, 110, 105)
BASE_700 = (87, 86, 83)
BASE_800 = (64, 62, 60)
BASE_850 = (52, 51, 49)
BASE_900 = (40, 39, 38)
BASE_950 = (28, 27, 26)
RED_600 = (175, 48, 41)
RED_400 = (209, 77, 65)
ORANGE_600 = (188, 82, 21)
ORANGE_400 = (218, 112, 44)

SIZE = 1200
SEED = 20260930
N_LAYERS = 22
OUT = Path(__file__).resolve().parent / "render-1.png"


def xf(x, y, ox, oy, scale, rot, jx, jy):
    cx, cy = 0.5, 0.5
    dx, dy = x - cx, y - cy
    c, s = math.cos(rot), math.sin(rot)
    rx, ry = c * dx - s * dy, s * dx + c * dy
    return ox + (rx + cx) * scale + jx, oy + (ry + cy) * scale + jy


def ribbon(draw, pts, width, color):
    """Filled strip along pts so edges stay exact palette (no AA blend)."""
    if len(pts) < 2:
        return
    # perpendicular offsets
    left, right = [], []
    for i, (x, y) in enumerate(pts):
        if i == 0:
            dx, dy = pts[1][0] - x, pts[1][1] - y
        elif i == len(pts) - 1:
            dx, dy = x - pts[i - 1][0], y - pts[i - 1][1]
        else:
            dx, dy = pts[i + 1][0] - pts[i - 1][0], pts[i + 1][1] - pts[i - 1][1]
        L = math.hypot(dx, dy) or 1.0
        nx, ny = -dy / L * width, dx / L * width
        left.append((x + nx, y + ny))
        right.append((x - nx, y - ny))
    poly = left + list(reversed(right))
    draw.polygon(poly, fill=color)


def motif_paths():
    """One arrangement: bowed rails, irregular uprights, two hooks."""
    paths = []
    # seven bowed rails, uneven spacing
    ys = [0.10, 0.21, 0.34, 0.47, 0.58, 0.73, 0.88]
    bows = [0.035, -0.018, 0.048, 0.012, -0.03, 0.04, 0.022]
    widths = [0.016, 0.011, 0.019, 0.013, 0.017, 0.012, 0.015]
    xspans = [
        (0.07, 0.93),
        (0.11, 0.90),
        (0.05, 0.95),
        (0.09, 0.91),
        (0.06, 0.94),
        (0.10, 0.92),
        (0.08, 0.88),
    ]
    for y, bow, w, (x0, x1) in zip(ys, bows, widths, xspans):
        pts = []
        for t in range(36):
            u = t / 35.0
            x = x0 + (x1 - x0) * u
            yy = y + bow * math.sin(u * math.pi)
            pts.append((x, yy))
        paths.append(("rail", pts, w, 0 if (y * 10) % 2 < 1 else 1))

    # five uprights, not a regular grate
    xs = [0.18, 0.33, 0.49, 0.64, 0.81]
    for i, x in enumerate(xs):
        lean = (-0.02, 0.015, -0.01, 0.025, -0.018)[i]
        y0, y1 = 0.07 + (i % 2) * 0.03, 0.93 - (i % 3) * 0.02
        pts = []
        for t in range(20):
            u = t / 19.0
            pts.append((x + lean * u, y0 + (y1 - y0) * u))
        paths.append(("up", pts, 0.009 + (i % 3) * 0.002, 2 if i % 2 else 1))

    def arc(cx, cy, r, a0, a1, n=24):
        pts = []
        for t in range(n + 1):
            a = a0 + (a1 - a0) * (t / n)
            pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
        return pts

    paths.append(("hook", arc(0.72, 0.29, 0.12, 0.15, 3.5), 0.013, 0))
    paths.append(("hook", arc(0.28, 0.76, 0.10, 3.4, 6.1), 0.011, 2))
    paths.append(("hook", arc(0.55, 0.52, 0.07, 1.0, 4.2), 0.008, 1))
    return paths


def stamp(draw, paths, ox, oy, scale, rot, jx, jy, inks, width_mul=1.0):
    for kind, pts, w, ink in paths:
        mapped = [xf(x, y, ox, oy, scale, rot, jx, jy) for x, y in pts]
        ribbon(draw, mapped, w * scale * width_mul, inks[ink % len(inks)])


def fault_polyline():
    # Off-axis incomplete cut, stops short of the plate edges
    x0, y0, x1, y1 = 280, 210, 940, 980
    pts = []
    n = 80
    for t in range(n + 1):
        u = t / n
        x = x0 + (x1 - x0) * u
        y = y0 + (y1 - y0) * u
        wob = 11 * math.sin(u * 7.4) + 5 * math.sin(u * 19.0 + 0.6)
        nx, ny = (y1 - y0), -(x1 - x0)
        L = math.hypot(nx, ny) or 1
        nx, ny = nx / L, ny / L
        pts.append((x + nx * wob, y + ny * wob))
    return pts


def cut_fault(draw, pts):
    """Open a continuous paper slit with a red lip; width dies at both ends."""
    n = len(pts) - 1
    left_p, right_p, lip_l, lip_r = [], [], [], []
    for i, (x, y) in enumerate(pts):
        u = i / n
        end = min(u, 1 - u) / 0.16
        end = max(0.0, min(1.0, end))
        half = 2.2 + 7.5 * end
        if i == 0:
            dx, dy = pts[1][0] - x, pts[1][1] - y
        elif i == n:
            dx, dy = x - pts[i - 1][0], y - pts[i - 1][1]
        else:
            dx, dy = pts[i + 1][0] - pts[i - 1][0], pts[i + 1][1] - pts[i - 1][1]
        L = math.hypot(dx, dy) or 1
        nx, ny = -dy / L, dx / L
        left_p.append((x + nx * half, y + ny * half))
        right_p.append((x - nx * half, y - ny * half))
        lip_l.append((x + nx * (half + 3.2), y + ny * (half + 3.2)))
        lip_r.append((x - nx * (half + 3.2), y - ny * (half + 3.2)))
    draw.polygon(lip_l + list(reversed(lip_r)), fill=RED_600)
    draw.polygon(left_p + list(reversed(right_p)), fill=PAPER)


def main():
    rng = random.Random(SEED)
    img = Image.new("RGB", (SIZE, SIZE), PAPER)
    draw = ImageDraw.Draw(img)

    margin = 44
    plate = [margin, margin + 14, SIZE - margin - 8, SIZE - margin]
    draw.rectangle(plate, fill=BASE_950)

    paths = motif_paths()
    scale = SIZE - 2 * margin - 56
    ox = margin + 28
    oy = margin + 34
    base_rot = -0.028

    palettes = [
        (BASE_800, BASE_700, BASE_600),
        (BASE_850, BASE_800, BASE_700),
        (BLACK, BASE_800, BASE_700),
        (BASE_900, BASE_700, ORANGE_600),
        (BASE_850, BLACK, BASE_600),
        (BASE_800, BASE_900, BASE_700),
    ]

    # Fused reprints: tiny mistiming so edges thicken into a plate
    for i in range(N_LAYERS):
        inks = palettes[i % len(palettes)]
        amp = 1.1 + 0.22 * (i % 6)
        jx = rng.uniform(-amp, amp)
        jy = rng.uniform(-amp, amp)
        rot = base_rot + rng.uniform(-0.0035, 0.0035)
        wm = 1.0 + 0.08 * (i % 4)
        stamp(draw, paths, ox, oy, scale, rot, jx, jy, inks, wm)

    # One misregistered reprint — the withheld difference made visible
    stamp(
        draw,
        paths,
        ox + 21,
        oy - 13,
        scale,
        base_rot + 0.016,
        0,
        0,
        (ORANGE_600, RED_600, ORANGE_400),
        0.95,
    )

    cut_fault(draw, fault_polyline())

    # A few late grey reprints over the plate (not across the slit) to keep
    # the field fused. They sit on the same registration as the stack.
    for i in range(3):
        stamp(
            draw,
            paths,
            ox,
            oy,
            scale,
            base_rot + rng.uniform(-0.002, 0.002),
            rng.uniform(-0.8, 0.8),
            rng.uniform(-0.8, 0.8),
            (BASE_800, BASE_700, BASE_600),
            0.85,
        )

    draw.rectangle(plate, outline=BASE_800, width=2)
    img.save(OUT, "PNG")
    print(f"wrote {OUT} {img.size}")


if __name__ == "__main__":
    main()
