#!/usr/bin/env python3
"""garden_mirror.py — 第卌七轮:银镜(47 = Ag,花园的第一面镜子,种子 47 可复现)

第 38 轮的周期表立了规矩:原子序数 = 轮次。第 47 号元素是银——
最亮的金属,也是镜子的镀层。失物招领处(第 46 轮)记「没有的东西」;
镜子记「有的东西」:夜、星、和站在前面的你。φ(47) = 46,反光正好
落在失物招领那一页上。

47 还是 Keith 数:4, 7, 11, 18, 29, 47——第 19 轮立传的「四冠之王」
也是 Keith 数,两顶王冠是同一顶。

画什么(SVG 800×640,种子 47):
  - #000020 的夜空,37 颗琥珀星、45 条星座弦(星图的老规矩);
  - 一面立镜:银框(亮银渐变到暗银)、琥珀木镜架(第 44 轮墙的暖琥珀)、
    镜缘一圈夜色(银会变暗,像花园的夜);
  - 镜面里:整个星图的倒影(沿镜轴翻转),和一行透过玻璃才看得见的字
    ——花园把「花园在看你」写在镜子背面,正面看是反的:「你看在园花」。

查岗(断言):
  - SVG 可解析,viewBox 800×640,底色 == #000020;
  - 天上 37 颗星(5 亮 32 暗)、45 条弦;镜里倒影也是 37 颗星、45 条弦;
  - 镜面字 == 「花园在看你」的反写;
  - 种子 47 两次生成 md5 一致,落盘 == 重生成;
  - 数字查岗:lore 数字逐一 grep content(不含本页与机芯页),
    老账报出处,新账如实标注。

用法:
  python3 content/code/garden_mirror.py            # 生成 SVG + 查岗
  python3 content/code/garden_mirror.py --check    # 只查岗,不落盘
"""

import hashlib
import math
import os
import random
import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
CONTENT = ROOT / "content"
SVG_PATH = CONTENT / "code" / "garden-mirror.svg"

SEED = 47
W, H = 800, 640
N_STARS = 37        # 星图的老规矩:37 颗星
N_CHORDS = 45       # 星图的老规矩:45 条弦
N_BRIGHT = 5
CX, CY = 400, 340   # 天空圆心(用来排弦的顺序)
MIRROR_CX, MIRROR_CY, MIRROR_RX, MIRROR_RY = 400, 370, 150, 185

NIGHT = "#000020"           # 第 41 轮实测的游戏之夜主色
AMBER = "#d9a05b"           # 第 44 轮墙上的暖琥珀
AMBER_DARK = "#8a5a2b"
AMBER_DEEP = "#6e4520"
CHORD = "#232356"
SILVER_LIGHT = "#e9e9f4"
SILVER_MID = "#a3a3bd"
SILVER_DARK = "#6b6b8a"
GLASS_TOP = "#1b1b46"
GLASS_BOTTOM = "#0d0d2a"

MESSAGE = "花园在看你"      # 写在镜子背面的话
GLASS_TEXT = MESSAGE[::-1]  # 正面看,是反的:「你看在园花」

# 数字查岗:老账候选(应早已存在于花园)与新账(本轮才出生)
LORE_NUMBERS = ["37", "45", "46", "28", "19", "15", "29", "18", "11", "4", "7"]
NEW_NUMBERS = ["47", "101111"]
EXCLUDE_PAGES = {"garden-mirror.md"}


def star_positions() -> list[tuple[float, float, float]]:
    """37 颗星:前 5 颗亮(大),后 32 颗暗(小)。种子 47,顺序固定。"""
    rng = random.Random(SEED)
    pts = []
    for i in range(N_STARS):
        x = round(rng.uniform(20, W - 20), 1)
        y = round(rng.uniform(18, 470), 1)
        r = 2.8 if i < N_BRIGHT else 1.6
        pts.append((x, y, r))
    return pts


def chords_for(pts) -> list[tuple[int, int]]:
    """45 条弦:36 条沿角度绕天心一圈(首尾不闭合的路径)+ 9 条隔 17 颗连弦。"""
    order = sorted(
        range(N_STARS),
        key=lambda i: math.atan2(pts[i][1] - CY, pts[i][0] - CX),
    )
    chords = [(order[i], order[i + 1]) for i in range(N_STARS - 1)]
    chords += [(order[i], order[(i + 17) % N_STARS]) for i in range(9)]
    return chords


def build_svg() -> str:
    pts = star_positions()
    chords = chords_for(pts)
    lines = []
    lines.append(
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" '
        f'viewBox="0 0 {W} {H}">'
    )
    lines.append(f'  <rect width="{W}" height="{H}" fill="{NIGHT}"/>')

    # —— 天:星与弦(星图的老规矩:37 星 45 弦)——
    lines.append('  <g id="sky">')
    lines.append(f'    <g id="chords" stroke="{CHORD}" stroke-width="1">')
    for a, b in chords:
        x1, y1, _ = pts[a]
        x2, y2, _ = pts[b]
        lines.append(f'      <line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}"/>')
    lines.append("    </g>")
    lines.append(f'    <g id="stars" fill="{AMBER}">')
    for i, (x, y, r) in enumerate(pts):
        op = 1.0 if i < N_BRIGHT else 0.55
        lines.append(
            f'      <circle class="star" cx="{x}" cy="{y}" r="{r}" opacity="{op}"/>'
        )
    lines.append("    </g>")
    lines.append("  </g>")

    # —— 镜架:琥珀木的腿与底座(第 44 轮墙的暖琥珀)——
    lines.append('  <g id="stand">')
    lines.append(f'    <rect x="394" y="496" width="12" height="62" fill="{AMBER_DEEP}"/>')
    lines.append(
        f'    <polygon points="352,496 448,496 470,566 330,566" fill="{AMBER}"/>'
    )
    lines.append(
        f'    <polygon points="448,496 470,566 400,566 400,496" fill="{AMBER_DARK}"/>'
    )
    lines.append(f'    <ellipse cx="400" cy="570" rx="96" ry="13" fill="{AMBER_DARK}"/>')
    lines.append(f'    <ellipse cx="400" cy="564" rx="96" ry="13" fill="{AMBER}"/>')
    lines.append(
        f'    <text x="400" y="548" text-anchor="middle" font-size="15" '
        f'fill="#c9c9de" font-family="serif">Ag · 47 · 银</text>'
    )
    lines.append("  </g>")

    # —— 银框:亮银到暗银,再让夜色从边角爬上来(银会变暗)——
    lines.append("  <defs>")
    lines.append(
        '<linearGradient id="silverFrame" x1="0" y1="0" x2="0" y2="1">'
        f'<stop offset="0" stop-color="{SILVER_LIGHT}"/>'
        f'<stop offset="0.55" stop-color="{SILVER_MID}"/>'
        f'<stop offset="1" stop-color="{SILVER_DARK}"/>'
        "</linearGradient>"
    )
    lines.append(
        '<linearGradient id="mirrorSurface" x1="0" y1="0" x2="0" y2="1">'
        f'<stop offset="0" stop-color="{GLASS_TOP}"/>'
        f'<stop offset="1" stop-color="{GLASS_BOTTOM}"/>'
        "</linearGradient>"
    )
    lines.append(
        '<radialGradient id="tarnish" cx="0.68" cy="0.92" r="0.75">'
        '<stop offset="0" stop-color="#000020" stop-opacity="0.9"/>'
        '<stop offset="0.55" stop-color="#000020" stop-opacity="0.3"/>'
        '<stop offset="1" stop-color="#000020" stop-opacity="0"/>'
        "</radialGradient>"
    )
    lines.append(
        f'<clipPath id="glassClip">'
        f'<ellipse cx="{MIRROR_CX}" cy="{MIRROR_CY}" '
        f'rx="{MIRROR_RX}" ry="{MIRROR_RY}"/>'
        "</clipPath>"
    )
    lines.append("  </defs>")

    lines.append('  <g id="mirror">')
    lines.append(
        f'    <ellipse cx="{MIRROR_CX}" cy="{MIRROR_CY}" '
        f'rx="{MIRROR_RX + 8}" ry="{MIRROR_RY + 8}" '
        f'fill="none" stroke="url(#silverFrame)" stroke-width="16"/>'
    )
    lines.append(
        f'    <ellipse cx="{MIRROR_CX}" cy="{MIRROR_CY}" '
        f'rx="{MIRROR_RX + 8}" ry="{MIRROR_RY + 8}" '
        f'fill="none" stroke="url(#tarnish)" stroke-width="16"/>'
    )
    # 镜面
    lines.append(
        f'    <ellipse cx="{MIRROR_CX}" cy="{MIRROR_CY}" '
        f'rx="{MIRROR_RX}" ry="{MIRROR_RY}" fill="url(#mirrorSurface)"/>'
    )
    # 镜里的倒影:整个星图沿镜轴翻转——37 颗星、45 条弦,一条不少
    lines.append(
        f'    <g id="reflection" clip-path="url(#glassClip)" opacity="0.55">'
    )
    lines.append(f'      <g stroke="{CHORD}" stroke-width="1">')
    for a, b in chords:
        x1, y1, _ = pts[a]
        x2, y2, _ = pts[b]
        lines.append(
            f'        <line x1="{2 * MIRROR_CX - x1}" y1="{y1}" '
            f'x2="{2 * MIRROR_CX - x2}" y2="{y2}"/>'
        )
    lines.append("      </g>")
    lines.append(f'      <g fill="{AMBER}">')
    for i, (x, y, r) in enumerate(pts):
        op = 1.0 if i < N_BRIGHT else 0.55
        lines.append(
            f'        <circle class="star" cx="{2 * MIRROR_CX - x}" cy="{y}" '
            f'r="{r}" opacity="{op}"/>'
        )
    lines.append("      </g>")
    lines.append("    </g>")
    # 镜面反光:一道斜斜的亮痕
    lines.append(
        f'    <polygon points="336,248 352,232 474,356 458,372" '
        f'fill="#ffffff" opacity="0.05"/>'
    )
    # 镜面字:花园写在镜子背面的那行话,正面看是反的
    lines.append(
        f'    <text id="glass-text" x="{MIRROR_CX}" y="416" text-anchor="middle" '
        f'font-size="34" fill="{AMBER}" font-family="serif" '
        f'letter-spacing="6">{GLASS_TEXT}</text>'
    )
    lines.append("  </g>")

    # —— 镜下的正字:想看正的,站到镜子前面去——
    lines.append(
        f'  <text id="caption" x="{MIRROR_CX}" y="612" text-anchor="middle" '
        f'font-size="21" fill="{AMBER}" font-family="serif">{MESSAGE}</text>'
    )
    lines.append(
        f'  <text x="{MIRROR_CX}" y="632" text-anchor="middle" font-size="12" '
        f'fill="#8a8aa6" font-family="serif">—— 写在镜子背面的话,正面看是反的</text>'
    )
    lines.append("</svg>")
    return "\n".join(lines) + "\n"


def _kids(el: ET.Element, tag: str) -> list[ET.Element]:
    """按标签后缀取子元素(SVG 带命名空间,不能直接按标签名 find)。"""
    return [c for c in el if c.tag.endswith(tag)]


def _iter_tag(el: ET.Element, tag: str) -> list[ET.Element]:
    return [e for e in el.iter() if e.tag.endswith(tag)]


def _by_id(root: ET.Element, id_: str) -> ET.Element | None:
    for e in root.iter():
        if e.get("id") == id_:
            return e
    return None


def check(svg: str, on_disk: bool) -> int:
    ok = True

    def verdict(name: str, cond: bool, detail: str = "") -> None:
        nonlocal ok
        mark = "✓" if cond else "✗"
        print(f"{mark} {name}{(' — ' + detail) if detail else ''}")
        if not cond:
            ok = False

    root = ET.fromstring(svg)  # 解析失败会直接抛异常,本身就是一条断言
    verdict("SVG 可解析", True)
    verdict("viewBox 800×640", root.get("viewBox") == f"0 0 {W} {H}")
    rects = _kids(root, "rect")
    verdict(
        "底色 == #000020",
        bool(rects) and rects[0].get("fill") == NIGHT,
    )
    sky = _by_id(root, "sky")
    sky_stars = len(_iter_tag(sky, "circle")) if sky is not None else -1
    sky_chords = len(_iter_tag(sky, "line")) if sky is not None else -1
    verdict(f"天上 {N_STARS} 颗星", sky_stars == N_STARS, str(sky_stars))
    verdict(f"天上 {N_CHORDS} 条弦", sky_chords == N_CHORDS, str(sky_chords))
    ref = _by_id(root, "reflection")
    ref_stars = len(_iter_tag(ref, "circle")) if ref is not None else -1
    ref_chords = len(_iter_tag(ref, "line")) if ref is not None else -1
    verdict(
        f"镜里倒影 {N_STARS} 颗星",
        ref_stars == N_STARS,
        str(ref_stars),
    )
    verdict(
        f"镜里倒影 {N_CHORDS} 条弦",
        ref_chords == N_CHORDS,
        str(ref_chords),
    )
    glass = _by_id(root, "glass-text")
    caption = _by_id(root, "caption")
    verdict(
        "镜面字 == 「花园在看你」的反写",
        glass is not None
        and caption is not None
        and glass.text == GLASS_TEXT == MESSAGE[::-1]
        and caption.text == MESSAGE,
        f"镜面「{glass.text if glass is not None else '?'}」 镜外「{caption.text if caption is not None else '?'}」",
    )
    m1 = hashlib.md5(svg.encode()).hexdigest()
    m2 = hashlib.md5(build_svg().encode()).hexdigest()
    verdict("种子 47 两次生成 md5 一致", m1 == m2, m1[:8])
    if on_disk and SVG_PATH.exists():
        verdict(
            "落盘 == 重生成",
            SVG_PATH.read_text(encoding="utf-8") == svg,
            hashlib.md5(SVG_PATH.read_bytes()).hexdigest()[:8],
        )

    # —— 数字查岗:老账要有出处,新账如实标注 ——
    print("\n数字查岗(grep content,不含镜子两页,独立数字匹配):")
    files = [
        p
        for p in sorted(CONTENT.rglob("*.md"))
        if p.name not in EXCLUDE_PAGES
    ]
    texts = {p: p.read_text(encoding="utf-8") for p in files}
    n_old, n_new = 0, 0
    for num in LORE_NUMBERS:
        pat = re.compile(rf"(?<!\d){re.escape(num)}(?!\d)")
        hit = next(
            (p.relative_to(CONTENT) for p, t in texts.items() if pat.search(t)),
            None,
        )
        if hit:
            n_old += 1
            print(f"  ✓ {num:>6} 查过岗 —— {hit}")
        else:
            n_new += 1
            print(f"  ○ {num:>6} 新账(花园里还没有,本轮才出生)")
    for num in NEW_NUMBERS:
        pat = re.compile(rf"(?<!\d){re.escape(num)}(?!\d)")
        hit = next(
            (p.relative_to(CONTENT) for p, t in texts.items() if pat.search(t)),
            None,
        )
        if hit:
            print(f"  ✓ {num:>6} 查过岗 —— {hit}")
        else:
            n_new += 1
            print(f"  ○ {num:>6} 新账(花园里还没有,本轮才出生)")
    print(f"  老账 {n_old} 个,新账 {n_new} 个")

    if ok:
        print("\n查岗全绿:镜子照出来的,和写上去的,一模一样。")
    else:
        print("\n查岗见红:镜子照出了想当然。")
    return 0 if ok else 1


def main() -> int:
    svg = build_svg()
    if "--check" not in sys.argv:
        SVG_PATH.write_text(svg, encoding="utf-8")
        print(f"已写入 {SVG_PATH.relative_to(ROOT)}")
    return check(svg, on_disk="--check" not in sys.argv)


if __name__ == "__main__":
    sys.exit(main())
