"""ServiceSim の録画に、店の様子のパネル（timeline.csv）を横に付ける。
使い方: python3 make_panel_video.py rec.mp4 frames.csv timeline.csv out.mp4 [--from 秒] [--to 秒] [--speed 倍] [--title 文字]
録画は 1コマ = frames.csv の t_s（店の時刻・秒）。パネルは timeline.csv の t_s ごと（60秒おき）に描き直す。
"""
import csv, sys, argparse, subprocess, tempfile, pathlib, bisect
from PIL import Image, ImageDraw, ImageFont

W, H, PW = 1920, 1080, 560          # 全体・パネルの幅
VW = W - PW                          # 録画の幅
BG, INK, QUIET, LINE = "#faf9f5", "#1d1d1b", "#6b6a64", "#d9d6cc"
ACC, GOOD, BAD, WARN = "#2f6fdf", "#1d9e75", "#d85a30", "#c98a1a"
import os
# 字は Noto Sans CJK JP。Linux の場所が無ければ Mac（Homebrew の font-noto-sans-cjk-jp）の場所（2026-11-04・依頼87）
_MAC = os.path.expanduser("~/Library/Fonts")
FONT = next((f for f in ("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", _MAC + "/NotoSansCJKjp-Regular.otf") if os.path.exists(f)), None)
FONTB = next((f for f in ("/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc", _MAC + "/NotoSansCJKjp-Bold.otf") if os.path.exists(f)), None)
F = lambda s, b=False: ImageFont.truetype(FONTB if b else FONT, s, index=0)

PAD = "#66605b"                       # --fit の余白（Unity の背景に近い色）
STAFF_C, ROBOT_C = "#2a55c8", "#ffffff"   # 立体の色に合わせる（スタッフ青・ロボット白）
STAFF = [("staff_guide", "案内", ACC), ("staff_serve", "料理", GOOD), ("staff_call", "呼び出し", "#7a5cc7"),
         ("staff_pay", "会計", WARN), ("staff_clear", "片付け", "#8a6d3b"), ("staff_rescue", "ロボットの助け", BAD),
         ("staff_idle", "手すき", "#b8b5ab")]

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

def panel(row, rows, robots, t_from, t_to, title):
    im = Image.new("RGB", (PW, H), BG); d = ImageDraw.Draw(im)
    x0 = 40
    d.text((x0, 36), title, font=F(26), fill=QUIET)
    d.text((x0, 76), f"開店から {hm(row['t_s'])}", font=F(52, True), fill=INK)
    y = 170
    # スタッフ
    n = sum(int(row[k]) for k, _, _ in STAFF)
    d.text((x0, y), f"スタッフ {n}人 がしていること", font=F(28, True), fill=INK); y += 52
    for k, name, c in STAFF:
        v = int(row[k])
        d.text((x0, y), name, font=F(26), fill=INK if v else QUIET)
        for i in range(v):
            d.rounded_rectangle((x0 + 210 + i * 46, y + 4, x0 + 246 + i * 46, y + 38), 6, fill=STAFF_C)
        y += 46
    y += 20
    # ロボット
    rb = int(row["robots_busy"])
    # 下げ膳にも使う回（timeline.csv に robots_bussing の列がある回）だけ、動いている台のうち下げ膳の台を「下げ膳」と出す（2026-11-04・依頼91）
    bus = min(int(row.get("robots_bussing") or 0), rb)
    d.text((x0, y), f"ロボット {robots}台", font=F(28, True), fill=INK); y += 52
    for i in range(robots):
        busy = i < rb
        d.rounded_rectangle((x0 + i * 150, y, x0 + 136 + i * 150, y + 50), 10, fill=ROBOT_C if busy else BG, outline=INK if busy else LINE, width=3)
        d.text((x0 + 68 + i * 150, y + 25), ("下げ膳" if i >= rb - bus else "運んでいる") if busy else "待機", font=F(24, True), fill=INK if busy else QUIET, anchor="mm")
    if robots == 0:
        d.text((x0, y + 8), "入れていない", font=F(26), fill=QUIET)
    y += 86
    # 数字
    for k, name, c in [("waiting_groups", "入口で待つ組", BAD), ("seated_groups", "席にいる組", INK),
                       ("tables_dirty_untaken", "片付けを待つ卓", WARN)]:
        v = int(row[k])
        d.text((x0, y + 8), name, font=F(28), fill=INK)
        d.text((PW - 50, y), str(v), font=F(48, True), fill=c if v else QUIET, anchor="ra")
        y += 66
    # 入口で待つ組の一日
    y += 14; gh = 120; gx0, gx1 = x0, PW - 40
    d.text((x0, y), "入口で待つ組（営業の始めから終わり）", font=F(22), fill=QUIET); y += 36
    ts = [r["t_s"] for r in rows]; vs = [int(r["waiting_groups"]) for r in rows]
    vmax = max(max(vs), 1); tmax = ts[-1] or 1
    X = lambda t: gx0 + (gx1 - gx0) * t / tmax; Y = lambda v: y + gh - gh * v / vmax
    d.rectangle((X(t_from), y, X(t_to), y + gh), fill="#eeece4")
    d.line([(X(t), Y(v)) for t, v in zip(ts, vs)], fill=BAD, width=3)
    d.line((gx0, y + gh, gx1, y + gh), fill=LINE, width=2)
    cx = X(row["t_s"]); d.line((cx, y, cx, y + gh), fill=INK, width=3)
    return im

def legend(path):
    im = Image.new("RGBA", (560, 64), (250, 249, 245, 230)); d = ImageDraw.Draw(im); x = 20
    for c, name in [("#e8c520", "お客さん"), ("#2a55c8", "スタッフ"), ("#f2f2f2", "ロボット")]:
        d.ellipse((x, 18, x + 28, 46), fill=c, outline="#6b6a64", width=2); d.text((x + 38, 32), name, font=F(26), fill=INK, anchor="lm")
        x += 70 + 30 * len(name)
    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("--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("--title", default="ServiceSim")
    ap.add_argument("--fit", action="store_true", help="録画を切らずに横幅に合わせて縮め、上下に余白を足す（字幕の入った寄りの部品用）")
    a = ap.parse_args()
    fr = [(int(r["frame"]), float(r["t_s"])) for r in csv.DictReader(open(a.frames))]
    rows = []
    for r in csv.DictReader(open(a.timeline)):
        r = dict(r); r["t_s"] = float(r["t_s"]); rows.append(r)
    robots = int(rows[0]["condition_id"].split("_r")[1].split("_")[0]) if "_r" in rows[0]["condition_id"] else 0
    t_to = a.t_to if a.t_to is not None else fr[-1][1]
    fps = 30
    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())
    # パネル：timeline の行ごと。その行が見える録画の長さ（速さで割る）だけ出す
    sel = [r for r in rows if a.t_from - 60 < r["t_s"] <= t_to]
    lst = []
    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
        dur = (t1 - t0) / 0.5 / fps / a.speed
        p = tmp / f"p{i:04d}.png"; panel(r, rows, robots, a.t_from, t_to, a.title).save(p)
        lst.append(f"file '{p}'\nduration {dur:.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={VW}:-2,pad={VW}:{H}:0:(oh-ih)/2:color={PAD}[v0];" if a.fit else f"scale=-2:{H},crop={VW}:{H}[v0];") +
          f"[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)
    print(a.out, f"{v_dur / a.speed:.1f}s")

if __name__ == "__main__":
    main()
