"""ServiceSim の報告動画を作る。spec.json（スライドの並び）→ 1920x1080 の mp4。
使い方: python3 make_video.py spec.json out.mp4
スライドの種類: title / chart / rows / steps
"""
import json, sys, subprocess, html, pathlib, tempfile, math, os
from playwright.sync_api import sync_playwright
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import style as st   # 色と字は style.py（決まりは STYLE.md）。ここで色を決めない

W, H = 1920, 1080
INK, QUIET, LINE, BG = st.INK, st.MUTED, st.RULE, st.PAPER
E = html.escape


def tag_color(c):
    c = st.TAG_ALIAS.get(c or "next", c or "next")
    if c not in st.TAG:
        raise SystemExit("札の色 %r は使えない。good / bad / quiet / next のどれか" % c)
    return st.TAG[c]


def series_color(c):
    c = st.SERIES_ALIAS.get(c or "unity", c or "unity")
    if c not in st.SERIES:
        raise SystemExit("線の色 %r は使えない。unity（main）/ site / bad のどれか" % c)
    return st.SERIES[c], c == "site"


def nice_step(v):
    """v 以上で一番小さい 1・2・2.5・5 ×10^n"""
    if v <= 0:
        return 1.0
    e = 10 ** math.floor(math.log10(v))
    for m in (1, 2, 2.5, 5, 10):
        if v <= m * e + 1e-12:
            return m * e


def ticks(top):
    """目盛りはきりのいい数で 4〜5 本（0 から）。上端は目盛りの数に切り上げる"""
    step = nice_step(top / 4)
    n = math.ceil(top / step - 1e-9)
    return [round(step * k, 6) for k in range(n + 1)]

CSS = f"""
*{{margin:0;padding:0;box-sizing:border-box}}
{st.CSS_FACE}
body{{width:{W}px;height:{H}px;background:{BG};font-family:{st.CSS_FONT};color:{INK};position:relative;overflow:hidden}}
.top{{position:absolute;left:96px;top:64px;font-size:30px;color:{QUIET}}}
h1{{position:absolute;left:96px;top:120px;right:96px;font-size:60px;font-weight:700;line-height:1.3}}
.main{{position:absolute;left:96px;right:96px;top:260px;bottom:220px}}
.sub{{position:absolute;left:0;right:0;bottom:0;height:180px;background:{INK};color:#fff;font-size:40px;line-height:1.5;padding:30px 120px;display:flex;align-items:center;justify-content:center;text-align:center}}
.row{{display:flex;align-items:center;gap:28px;font-size:38px;line-height:1.4;margin-bottom:30px}}
.tag{{flex:none;min-width:150px;padding:8px 20px;border-radius:10px;font-size:30px;font-weight:700;text-align:center;color:#fff}}
.note{{font-size:28px;color:{QUIET};margin-top:16px}}
"""

def svg_chart(s):
    xs = s["x"]; series = s["series"]; unit = s.get("unit", "")
    cw, ch = 1728, 500; l, r, t, b = 120, 360, 56, 70
    vals = [v for se in series for v in se["values"] if v is not None]
    tk = ticks(s.get("ymax") or max(vals) * 1.1)
    vmax = tk[-1]
    pad = 48   # 最初の点を目盛りの字から離す
    X = lambda i: l + pad + i * (cw - l - r - pad) / (len(xs) - 1)
    Y = lambda v: t + (ch - t - b) * (1 - v / vmax)
    out = [f'<svg width="{cw}" height="{ch}" xmlns="http://www.w3.org/2000/svg" font-family="{st.CSS_FONT.replace(chr(39), "")}">']
    for v in tk:
        y = Y(v)
        out.append(f'<line x1="{l}" x2="{cw-r}" y1="{y}" y2="{y}" stroke="{LINE}"/>')
        out.append(f'<text x="{l-16}" y="{y+10}" font-size="26" fill="{QUIET}" text-anchor="end">{v:g}</text>')
    for i, x in enumerate(xs):
        out.append(f'<text x="{X(i)}" y="{ch-24}" font-size="28" fill="{QUIET}" text-anchor="middle">{E(x)}</text>')
    for se in series:
        c, is_site = series_color(se.get("color"))
        dash = ' stroke-dasharray="12 10"' if (se.get("dash") or is_site) else ""
        pts = [(X(i), Y(v)) for i, v in enumerate(se["values"]) if v is not None]
        out.append(f'<polyline points="{" ".join(f"{a},{b2}" for a, b2 in pts)}" fill="none" stroke="{c}" stroke-width="6"{dash}/>')
        for i, v in enumerate(se["values"]):
            if v is None: continue
            out.append(f'<circle cx="{X(i)}" cy="{Y(v)}" r="10" fill="{c}"/>')
            if se.get("labels", True):
                out.append(f'<text x="{X(i)}" y="{Y(v)+(46 if se.get("below") else -22)}" font-size="30" font-weight="700" fill="{c}" text-anchor="middle">{v:.{s.get("dp",1)}f}</text>')
        lx, ly = pts[-1]
        out.append(f'<text x="{lx+30}" y="{ly+(44 if se.get("below") else -6)}" font-size="32" font-weight="700" fill="{c}">{E(se["name"])}</text>')
    out.append(f'<text x="{l}" y="{t-28}" font-size="26" fill="{QUIET}">{E(unit)}</text></svg>')
    return "".join(out)

def body(s):
    if s["kind"] == "title":
        return f'<h1 style="top:300px;font-size:76px">{E(s["title"])}</h1><div class="main" style="top:520px;font-size:40px;color:{QUIET};line-height:1.6">{"<br>".join(E(x) for x in s.get("lines", []))}</div>'
    h = f'<h1>{E(s["title"])}</h1>'
    if s["kind"] == "chart":
        note = f'<div class="note">{E(s["note"])}</div>' if s.get("note") else ""
        return h + f'<div class="main">{svg_chart(s)}{note}</div>'
    if s["kind"] in ("rows", "steps"):
        rows = "".join(f'<div class="row"><span class="tag" style="background:{tag_color(r.get("c"))}">{E(r["tag"])}</span><span>{E(r["text"])}</span></div>' for r in s["rows"])
        return h + f'<div class="main">{rows}</div>'
    raise ValueError(s["kind"])

def main(spec_path, out):
    spec = json.load(open(spec_path, encoding="utf-8"))
    tmp = pathlib.Path(tempfile.mkdtemp())
    segs = []
    with sync_playwright() as p:
        br = p.chromium.launch(); pg = br.new_page(viewport={"width": W, "height": H})
        for i, s in enumerate(spec["slides"]):
            doc = f'<html><head><meta charset="utf-8"><style>{CSS}</style></head><body><div class="top">{E(spec["header"])}　{i+1}/{len(spec["slides"])}</div>{body(s)}<div class="sub">{E(s["sub"])}</div></body></html>'
            pg.set_content(doc); pg.wait_for_timeout(150)
            png = tmp / f"s{i:02d}.png"; pg.screenshot(path=str(png))
            seg = tmp / f"s{i:02d}.mp4"
            subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-loop", "1", "-i", str(png), "-t", str(s.get("sec", 10)),
                            "-vf", "fade=in:0:8,format=yuv420p", "-r", "30", "-c:v", "libx264", "-preset", "veryfast", str(seg)], check=True)
            segs.append(seg)
        br.close()
    lst = tmp / "list.txt"; lst.write_text("".join(f"file '{x}'\n" for x in segs))
    subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-f", "concat", "-safe", "0", "-i", str(lst), "-c", "copy", out], check=True)
    print(out)

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])
