"""ServiceSim のパネル付き動画を作る（サイトに出す動画・報告に使う動画はすべてこの形）。
使い方: python3 make_panel_video.py rec.mp4 frames.csv timeline.csv out.mp4 --title "③ 標準ファミレス・ロボット2台"
        [--from 店の秒] [--to 店の秒] [--speed 倍] [--trial]

決まった形（2026-11-03 エイイチ了承。変えるときはエイイチに確かめてから）
  画面   1920x1080・30fps。左 1360px が録画、右 560px がパネル
  色     立体と同じ。お客さん＝黄・スタッフ＝青・ロボット＝白。録画の左上に色の説明（お客さん／スタッフ／ロボット）
  パネル 上から：題（店・台数）→ 開店からの時間（「1時間54分」の形。1:54 とは書かない） → スタッフ N人がしていること（案内・料理・呼び出し・会計・片付け・
         ロボットの助け・手すき。1人＝青い四角1つ）→ ロボット N台（運んでいる＝白・黒縁／待機＝薄い）→
         入口で待つ組・席にいる組・片付けを待つ卓 → 入口で待つ組の一日の線（いまの時刻に縦線・映っている範囲を薄く）
  言葉   「客」と書かずに「お客さん」。数字の欄は「〜組」「〜卓」
  数字   録画と同じ回の timeline.csv を使う（違う回の数字を重ねるのは --trial のときだけ。題に（試作）が付く）
"""
import csv, argparse, subprocess, tempfile, pathlib, json
from PIL import Image, ImageDraw, ImageFont

W, H, PW = 1920, 1080, 560
VW = W - PW
FPS = 30
BG, INK, QUIET, LINE, SHADE = "#faf9f5", "#1d1d1b", "#6b6a64", "#d9d6cc", "#eeece4"
BAD, WARN = "#d85a30", "#c98a1a"
STAFF_C, ROBOT_C, GUEST_C = "#2a55c8", "#ffffff", "#e8c520"   # 立体の色
FONT = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
FONTB = "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc"
F = lambda s, b=False: ImageFont.truetype(FONTB if b else FONT, s, index=0)

STAFF = [("staff_guide", "案内"), ("staff_serve", "料理"), ("staff_call", "呼び出し"), ("staff_pay", "会計"),
         ("staff_clear", "片付け"), ("staff_rescue", "ロボットの助け"), ("staff_idle", "手すき")]
COUNTS = [("waiting_groups", "入口で待つ組", BAD), ("seated_groups", "席にいる組", INK), ("tables_dirty_untaken", "片付けを待つ卓", WARN)]
NEED = ["t_s", "robots_busy"] + [k for k, _ in STAFF] + [k for k, _, _ in COUNTS]

def hm(t):
    t = int(t); h, m = t // 3600, t % 3600 // 60
    return f"{h}時間{m:02d}分" if h else f"{m}分"

def fit(d, text, size, maxw, bold=False):
    """幅に収まるまで字を小さくする（崩れ防止）"""
    while size > 14 and d.textlength(text, font=F(size, bold)) > maxw:
        size -= 1
    return F(size, bold)

def panel(row, rows, robots, staff_n, t_from, t_to, title):
    im = Image.new("RGB", (PW, H), BG); d = ImageDraw.Draw(im)
    x0, xr = 40, PW - 40; inner = xr - x0
    d.text((x0, 36), title, font=fit(d, title, 26, inner), fill=QUIET)
    clock = f"開店から {hm(row['t_s'])}"
    d.text((x0, 76), clock, font=fit(d, clock, 52, inner, True), fill=INK)
    y = 170
    d.text((x0, y), f"スタッフ {staff_n}人 がしていること", font=F(28, True), fill=INK); y += 52
    bx = x0 + 210; step = min(46, (xr - bx) // max(staff_n, 1)); box = step - 10
    for k, name in STAFF:
        v = int(float(row[k]))
        d.text((x0, y), name, font=F(26), fill=INK if v else QUIET)
        for i in range(v):
            d.rounded_rectangle((bx + i * step, y + 4, bx + i * step + box, y + 4 + min(box, 32)), 6, fill=STAFF_C)
        y += 44
    y += 16
    rb = int(float(row["robots_busy"]))
    d.text((x0, y), f"ロボット {robots}台", font=F(28, True), fill=INK); y += 52
    if robots == 0:
        d.text((x0, y + 8), "入れていない", font=F(26), fill=QUIET)
    else:
        rstep = min(150, inner // robots); rw = rstep - 14
        for i in range(robots):
            busy = i < rb; lab = "運んでいる" if busy else "待機"
            d.rounded_rectangle((x0 + i * rstep, y, x0 + i * rstep + rw, y + 50), 10,
                                fill=ROBOT_C if busy else BG, outline=INK if busy else LINE, width=3)
            d.text((x0 + i * rstep + rw / 2, y + 25), lab, font=fit(d, lab, 24, rw - 12, True), fill=INK if busy else QUIET, anchor="mm")
    y += 86
    for k, name, c in COUNTS:
        v = int(float(row[k]))
        d.text((x0, y + 8), name, font=F(28), fill=INK)
        d.text((xr - 10, y), str(v), font=F(48, True), fill=c if v else QUIET, anchor="ra")
        y += 66
    y += 14; gh = 120
    d.text((x0, y), "入口で待つ組（営業の始めから終わり）", font=F(22), fill=QUIET); y += 36
    ts = [r["t_s"] for r in rows]; vs = [int(float(r["waiting_groups"])) for r in rows]
    vmax = max(max(vs), 1); tmax = ts[-1] or 1
    X = lambda t: x0 + inner * min(max(t, 0), tmax) / tmax; Y = lambda v: y + gh - gh * v / vmax
    d.rectangle((X(t_from), y, X(t_to), y + gh), fill=SHADE)
    d.line([(X(t), Y(v)) for t, v in zip(ts, vs)], fill=BAD, width=3)
    d.line((x0, y + gh, xr, y + gh), fill=LINE, width=2)
    cx = X(row["t_s"]); d.line((cx, y, cx, y + gh), fill=INK, width=3)
    assert y + gh <= H - 20, "パネルが下にはみ出す"
    return im

def legend(path):
    items = [(GUEST_C, "お客さん"), (STAFF_C, "スタッフ"), ("#f2f2f2", "ロボット")]
    d0 = ImageDraw.Draw(Image.new("RGB", (1, 1))); f = F(26)
    w = 20 + sum(38 + d0.textlength(n, font=f) + 34 for _, n in items)
    im = Image.new("RGBA", (int(w), 64), (250, 249, 245, 230)); d = ImageDraw.Draw(im); x = 20
    for c, name in items:
        d.ellipse((x, 18, x + 28, 46), fill=c, outline=QUIET, width=2)
        d.text((x + 38, 32), name, font=f, fill=INK, anchor="lm")
        x += 38 + d.textlength(name, font=f) + 34
    im.save(path)

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("rec"); ap.add_argument("frames"); ap.add_argument("timeline"); ap.add_argument("out")
    ap.add_argument("--title", required=True)
    ap.add_argument("--from", dest="t_from", type=float, default=0); ap.add_argument("--to", dest="t_to", type=float, default=None)
    ap.add_argument("--speed", type=float, default=1.0)
    ap.add_argument("--trial", action="store_true", help="録画と数字が別の回のとき（題に（試作）を付ける）")
    a = ap.parse_args()
    title = a.title + ("（試作）" if a.trial else "")
    fr = [(int(r["frame"]), float(r["t_s"])) for r in csv.DictReader(open(a.frames))]
    dt = fr[1][1] - fr[0][1]                       # 1コマが店の何秒か
    rows = []
    for r in csv.DictReader(open(a.timeline)):
        r = dict(r); r["t_s"] = float(r["t_s"]); rows.append(r)
    miss = [k for k in NEED if k not in rows[0]]
    assert not miss, f"timeline.csv に列がない: {miss}"
    cid = rows[0].get("condition_id", "")
    robots = int(cid.split("_r")[1].split("_")[0]) if "_r" in cid else 0
    staff_n = max(sum(int(float(r[k])) for k, _ in STAFF) for r in rows)
    probe = json.loads(subprocess.run(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height",
                                       "-of", "json", a.rec], capture_output=True, text=True, check=True).stdout)["streams"][0]
    assert probe["width"] * 9 == probe["height"] * 16, "録画は 16:9 にする"
    t_to = a.t_to if a.t_to is not None else fr[-1][1]
    f_from = next(f for f, t in fr if t >= a.t_from); f_to = max(f for f, t in fr if t <= t_to)
    v_from, v_dur = f_from / FPS, (f_to - f_from) / FPS
    tmp = pathlib.Path(tempfile.mkdtemp())
    sel = [r for r in rows if a.t_from - (rows[1]["t_s"] - rows[0]["t_s"]) < r["t_s"] <= t_to]
    lst, p = [], None
    for i, r in enumerate(sel):
        t0 = max(r["t_s"], a.t_from); t1 = sel[i + 1]["t_s"] if i + 1 < len(sel) else t_to
        if t1 <= t0: continue
        p = tmp / f"p{i:04d}.png"; panel(r, rows, robots, staff_n, a.t_from, t_to, title).save(p)
        lst.append(f"file '{p}'\nduration {(t1 - t0) / dt / FPS / a.speed:.4f}\n")
    lst.append(f"file '{p}'\n")
    (tmp / "p.txt").write_text("".join(lst))
    legend(tmp / "lg.png")
    vf = (f"[0:v]trim=start={v_from}:duration={v_dur},setpts=(PTS-STARTPTS)/{a.speed},"
          f"scale=-2:{H},crop={VW}:{H}[v0];[2:v]format=rgba[lg];[v0][lg]overlay=32:32[v];"
          f"[1:v]fps={FPS},setpts=PTS-STARTPTS[p];[v][p]hstack=inputs=2,format=yuv420p")
    subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", a.rec, "-f", "concat", "-safe", "0", "-i", str(tmp / "p.txt"),
                    "-loop", "1", "-i", str(tmp / "lg.png"), "-filter_complex", vf, "-t", f"{v_dur / a.speed:.3f}",
                    "-r", str(FPS), "-c:v", "libx264", "-preset", "veryfast", "-crf", "22", a.out], check=True)
    out = json.loads(subprocess.run(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height",
                                     "-of", "json", a.out], capture_output=True, text=True, check=True).stdout)["streams"][0]
    assert (out["width"], out["height"]) == (W, H), "出来た動画の大きさが違う"
    print(a.out, f"{v_dur / a.speed:.1f}s")

if __name__ == "__main__":
    main()
