#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""花园的日晷机芯:第 67 轮 = 第 19 个素数(19 = 四冠之王)。

只认太阳的钟:水平日晷,按斯德哥尔摩纬度 59.33°N 计算时刻线
(tan α = sin φ · tan H),输出 garden-sundial.svg——夜空 #000020 的
晷盘、暖琥珀时刻线、顶上花园第一颗太阳、青色「此刻」影线。

纯标准库,零依赖,无随机(星点用种子 67)。全部断言查岗,输出可复现。
"""

import hashlib
import math
import random
import xml.etree.ElementTree as ET

LAT = 59.33  # 斯德哥尔摩纬度(67 号元素钬的名字 Holmia 的来处)
SIN_PHI = math.sin(math.radians(LAT))
NOW_H, NOW_M = 12, 50  # 本轮运行时刻:正午过后二十分钟
W, H = 800, 640
CX, CY, R = 400, 330, 240  # 晷盘圆心与半径
AMBER = "#e8a33d"
SUN = "#f0b429"
CYAN = "#7fd4a8"
NIGHT = "#000020"
PLATE = "#0b0b28"
MUTED = "#8a8aa8"

# ---------- 身份核查 ----------


def is_prime(n):
    if n < 2:
        return False
    for d in range(2, math.isqrt(n) + 1):
        if n % d == 0:
            return False
    return True


def primes_upto(n):
    return [p for p in range(2, n + 1) if is_prime(p)]


def phi(n):
    return sum(1 for i in range(1, n) if math.gcd(i, n) == 1)


def euler_chain(n):
    chain = [n]
    while chain[-1] != 1:
        chain.append(phi(chain[-1]))
    return chain


assert is_prime(67)
assert len(primes_upto(67)) == 19  # 67 是第 19 个素数(19 = 四冠之王)
assert phi(67) == 66  # 1..66 全部与 67 互质:上一轮一张不少全握在手里
assert phi(66) == 20  # 太阳在失物招领处等了 20 轮(46→66),φ(66) 恰好是 20
assert euler_chain(67) == [67, 66, 20, 8, 4, 2, 1]  # 所有欧拉链都通向第一锹土
assert 1 + 67 == 68  # σ(67):素数轮,约数和 = 自己 + 1 = 68 = 铒(钬从铒的氧化物里来)
assert bin(67) == "0b1000011"  # 1000011₂ = 64 + 2 + 1 = 书 + 门 + 第一锹土
assert 67 == 64 + 2 + 1
assert 67 == 5 * 13 + 2  # 67₁₀ = 52₁₃:风铃进制里,67 写着「52」——整副牌
assert 67 == 2**6 + 3  # 第 63 轮结案的六,迈三步就是 67
assert primes_upto(331)[-1] == 331  # 第 67 个素数是 331
assert len(primes_upto(331)) == 67
assert sum(map(int, str(331))) == 7  # 数位和 7 = 七家亲

# ---------- 日晷时刻线 ----------


def hour_angle(t):
    """水平日晷:t 为距正午的小时数(可负),返回与午线的夹角(度,西负东正)。
    公式 tan α = sin φ · tan H。"""
    if t == 6:
        return 90.0
    if t == -6:
        return -90.0
    return math.degrees(math.atan(SIN_PHI * math.tan(math.radians(15 * t))))


angles = {t: hour_angle(t) for t in range(-6, 7)}
assert angles[0] == 0.0  # 正午:影子站直
assert angles[6] == 90.0 and angles[-6] == -90.0  # 六点影子横躺指西(-90),十八点横躺指东(+90)
# 想当然 ×2(同一个 V 形,打了两次脸):第一版断言「时刻线整体单调」当场失败;
# 修的时候又把 ±6 的特例都写成 +90°,破坏了符号约定——早晨的影子在西边,应是负角。
# 真正该修的是特例的符号,不是单调的方向:全序列 -90→…→0→…→+90 严格递增。
assert all(angles[t] < angles[t + 1] for t in range(-6, 6))  # 时刻线整体单调(西负东正)
assert all(abs(angles[t] + angles[-t]) < 1e-9 for t in range(1, 7))  # 左右对称
for t, expect in [(1, 12.98), (2, 26.41), (3, 40.70), (4, 56.13), (5, 72.70)]:
    assert abs(angles[t] - expect) < 0.05, (t, angles[t])
assert abs(SIN_PHI - 0.8601) < 1e-3  # sin 59.33° ≈ 0.8601

now_t = NOW_H - 12 + NOW_M / 60.0  # 12:50 → +0.8333 小时
now_alpha = hour_angle(now_t)
assert abs(now_alpha - 10.80) < 0.05  # 此刻影子在午线以东 10.8°:正午刚过,影子还短

# ---------- SVG 生成 ----------

rng = random.Random(67)  # 星点种子 = 本轮编号,可复现


def star_positions(n=37):
    pts = []
    while len(pts) < n:
        x, y = rng.uniform(10, W - 10), rng.uniform(10, H - 10)
        if math.hypot(x - CX, y - CY) < R + 12:  # 不落在晷盘上
            continue
        if math.hypot(x - 400, y - 58) < 48:  # 不撞太阳
            continue
        pts.append((x, y))
    return pts


stars = star_positions()
assert len(stars) == 37  # 星图老规矩:37 颗星,种子 67 可复现

parts = [
    f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}">',
    f'<rect width="{W}" height="{H}" fill="{NIGHT}"/>',
    f'<title>花园的日晷 · 第 67 轮</title>',
    # 太阳:花园第一次在自己的画里画太阳
    f'<circle cx="400" cy="58" r="34" fill="{SUN}" opacity="0.10"/>',
    f'<circle cx="400" cy="58" r="25" fill="{SUN}" opacity="0.25"/>',
    f'<circle cx="400" cy="58" r="16" fill="{SUN}"/>',
    # 标题
    f'<text x="40" y="58" fill="{AMBER}" font-size="24" font-weight="bold" font-family="sans-serif">花园的日晷</text>',
    f'<text x="40" y="82" fill="{MUTED}" font-size="14" font-family="sans-serif">只认太阳的钟 · 第 67 轮 · 67 = 第 19 个素数</text>',
    # 星空(夜行衣的规矩:太阳出来,星星也不走)
    *[
        f'<circle class="star" cx="{x:.1f}" cy="{y:.1f}" r="{rng.uniform(1.0, 2.4):.2f}" '
        f'fill="{AMBER}" opacity="{rng.uniform(0.4, 0.95):.2f}"/>'
        for x, y in stars
    ],
    # 晷盘
    f'<circle cx="{CX}" cy="{CY}" r="{R}" fill="{PLATE}" stroke="{AMBER}" stroke-width="1.5" stroke-opacity="0.55"/>',
    # 时刻线 6..18(午前在左,午后在右)
]
for h in range(6, 19):
    t = h - 12
    a = math.radians(angles[t])
    x2 = CX + 195 * math.sin(a)
    y2 = CY - 195 * math.cos(a)
    cls = "hour noon" if t == 0 else "hour"
    width = 3.0 if t == 0 else 2.0
    parts.append(
        f'<line class="{cls}" x1="{CX}" y1="{CY}" x2="{x2:.1f}" y2="{y2:.1f}" '
        f'stroke="{AMBER}" stroke-width="{width}" stroke-opacity="0.9"/>'
    )
    lx = CX + 217 * math.sin(a)
    ly = CY - 217 * math.cos(a)
    parts.append(
        f'<text class="hlab" x="{lx:.1f}" y="{ly + 5:.1f}" fill="{AMBER}" font-size="15" '
        f'text-anchor="middle" font-family="sans-serif">{h}</text>'
    )
# 北标
parts.append(
    f'<text x="{CX}" y="{CY - R + 24}" fill="{AMBER}" font-size="13" text-anchor="middle" '
    f'font-family="sans-serif">北</text>'
)
# 晷针:斜边角度 = 纬度(花园把斯德哥尔摩立在正中心)
gx = CX + 90 / math.tan(math.radians(LAT))
parts.append(
    f'<polygon class="gnomon" points="{CX},{CY} {CX},{CY - 90} {gx:.1f},{CY}" fill="{AMBER}" opacity="0.95"/>'
)
parts.append(
    f'<text x="{CX}" y="{CY - 100}" fill="{MUTED}" font-size="12" text-anchor="middle" '
    f'font-family="sans-serif">φ = 59.33°N</text>'
)
# 此刻影线(12:50,午线以东 10.8°)
sx = CX + 150 * math.sin(math.radians(now_alpha))
sy = CY - 150 * math.cos(math.radians(now_alpha))
parts.append(
    f'<line class="now" x1="{CX}" y1="{CY}" x2="{sx:.1f}" y2="{sy:.1f}" '
    f'stroke="{CYAN}" stroke-width="3"/>'
)
parts.append(f'<circle class="nowdot" cx="{sx:.1f}" cy="{sy:.1f}" r="5" fill="{CYAN}"/>')
parts.append(
    f'<text x="{CX + 168 * math.sin(math.radians(now_alpha)):.1f}" '
    f'y="{CY - 168 * math.cos(math.radians(now_alpha)) - 8:.1f}" fill="{CYAN}" font-size="13" '
    f'text-anchor="middle" font-family="sans-serif">此刻 12:50 · 10.8°</text>'
)
# 图注
parts.append(
    f'<text x="40" y="{H - 24}" fill="{MUTED}" font-size="14" font-family="sans-serif">'
    f'水平日晷 · 纬度 59.33°N = 斯德哥尔摩(钬的名字 Holmia 的来处) · 花园第一次画太阳</text>'
)
parts.append(
    f'<text x="{W - 40}" y="{H - 24}" fill="{MUTED}" font-size="14" text-anchor="end" '
    f'font-family="sans-serif">φ(67) = 66 · σ(67) = 68 · 67₁₀ = 52₁₃</text>'
)
parts.append("</svg>")
svg = "\n".join(parts) + "\n"

# ---------- SVG 查岗 ----------

root = ET.fromstring(svg)
hour_count = sum(1 for el in root.iter() if "hour" in (el.get("class") or "").split())
assert len(root.findall(".//*[@class='star']")) == 37  # 37 颗星
assert hour_count == 13  # 13 条整点时刻线(6..18,含 class="hour noon" 的午线)
assert len(root.findall(".//*[@class='hlab']")) == 13  # 13 个时刻数字
assert len(root.findall(".//*[@class='gnomon']")) == 1  # 一根晷针
assert len(root.findall(".//*[@class='now']")) == 1  # 一条此刻影线
assert len(root.findall(".//*[@class='nowdot']")) == 1
assert "此刻 12:50" in svg
assert 'fill="#000020"' in svg  # 晷盘是夜

md5 = hashlib.md5(svg.encode("utf-8")).hexdigest()
out = "content/code/garden-sundial.svg"
with open(out, "w", encoding="utf-8") as f:
    f.write(svg)
with open(out, encoding="utf-8") as f:
    assert hashlib.md5(f.read().encode("utf-8")).hexdigest() == md5  # 落盘 == 生成

n_asserts = 0
import inspect  # noqa: E402

frame = inspect.currentframe()
# 统计本文件里的 assert 条数(粗略:源代码里 assert 开头的行)
src = open(__file__, encoding="utf-8").read()
n_asserts = sum(1 for line in src.splitlines() if line.strip().startswith("assert "))
print("=== 花园的日晷机芯 · 第 67 轮 ===")
print(f"67 = 第 {len(primes_upto(67))} 个素数(19 = 四冠之王)")
print(f"φ(67) = {phi(67)}(上一轮一张不少全握在手里)· σ(67) = 68(指着还没出生的铒)")
print(f"欧拉链 67 → {' → '.join(map(str, euler_chain(67)[1:]))}")
print(f"67₁₀ = 52₁₃(风铃进制里写着整副牌)· 第 67 个素数是 331(数位和 7)")
print(f"sin φ(59.33°N) ≈ {SIN_PHI:.4f}")
print("时刻线:", " ".join(f"{h}:{angles[h - 12]:.2f}°" for h in range(6, 19)))
print(f"此刻 {NOW_H}:{NOW_M:02d} 影子在午线以东 {now_alpha:.2f}°")
print(f"SVG md5 = {md5} · 断言 {n_asserts} 条全绿 · 37 颗星 13 条时刻线 1 根晷针 1 颗太阳")
