# -*- coding: utf-8 -*-
"""
SuperMovie SE — seData.ts 自動検証スクリプト（supermovie-0609で実証済みの品質基準）

SEの「選曲」は内容判断なのでモデルが行う。このスクリプトは配置ルール違反を機械検証する。
seData.ts を書いたら必ずこれを実行し、エラー0にしてから完了報告する。

使い方:
  python3 check_se.py <PROJECT_DIR>            # 一人語り（supermovie 標準）
  python3 check_se.py <PROJECT_DIR> --taidan   # 対談（supermovie2）: 密度下限5%・末尾SEは警告のみ
      （対談は normal 会話テロップが大半で SE 密度が自然に低い。実績 10.7%）

検証項目（エラー = exit 1）:
  E1: SEは emphasis/warning/success テロップの startFrame か タイトルの startFrame にだけ置く
      （normal テロップや無関係なフレームへの配置は禁止）
  E2: ★冒頭クリーン: 最初のテロップ・frame 0 のタイトルにSEを置かない
  E3: フレーム重複なし・直前のSEから10フレーム以上空ける
  E4: 同一SEファイルの連続使用なし
  E5: 0 <= startFrame < DURATION_FRAMES
  E6: 全ファイルが public/se/ に存在
  E7: volume が 0.15〜0.40 の範囲
  E8: 動画の最後5秒にSEなし（余韻）
  E9: 密度: SE数/テロップ数 が 15〜45%、かつ尺3分割の各密度差が20ポイント以内（均一品質）
"""
import re
import sys
from pathlib import Path


def parse_entries(txt, pattern):
    return [m.groupdict() for m in re.finditer(pattern, txt)]


def main():
    proj = Path(sys.argv[1])
    taidan = "--taidan" in sys.argv
    min_density = 5 if taidan else 15
    vc = (proj / "src" / "videoConfig.ts").read_text()
    FPS = int(re.search(r"export const FPS = (\d+)", vc).group(1))
    TOTAL = int(re.search(r"export const DURATION_FRAMES = (\d+)", vc).group(1))

    se_txt = (proj / "src" / "SoundEffects" / "seData.ts").read_text()
    ses = [
        {"id": int(m.group(1)), "frame": int(m.group(2)), "file": m.group(3), "vol": float(m.group(4))}
        for m in re.finditer(
            r"\{\s*id:\s*(\d+),\s*startFrame:\s*(\d+),\s*file:\s*'([^']+)',\s*volume:\s*([\d.]+)", se_txt
        )
    ]
    tel_txt = (proj / "src" / "テロップテンプレート" / "telopData.ts").read_text()
    # speaker フィールド（対談）の有無どちらにも対応
    telops = [
        {"frame": int(m.group(1)), "style": m.group(2)}
        for m in re.finditer(
            r"startFrame:\s*(\d+),\s*endFrame:\s*\d+,\s*text:\s*'[^']*',"
            r"(?:\s*speaker:\s*'\w+',)?\s*style:\s*'(\w+)'",
            tel_txt,
        )
    ]
    ttl_path = proj / "src" / "Title" / "titleData.ts"
    titles = []
    if ttl_path.exists():
        titles = [int(m.group(1)) for m in re.finditer(r"startFrame:\s*(\d+)", ttl_path.read_text())]

    if not ses:
        print("★seData.ts からSEを読めませんでした")
        sys.exit(1)
    if not telops:
        print("★telopData.ts からテロップを読めませんでした")
        sys.exit(1)

    anim_frames = {t["frame"] for t in telops if t["style"] in ("emphasis", "warning", "success")}
    normal_frames = {t["frame"] for t in telops if t["style"] == "normal"}
    title_frames = set(titles)
    first_telop = min(t["frame"] for t in telops)

    errors = []
    ses_sorted = sorted(ses, key=lambda s: s["frame"])
    for i, s in enumerate(ses_sorted):
        f = s["frame"]
        if f not in anim_frames and f not in title_frames:
            where = "normalテロップ" if f in normal_frames else "テロップ/タイトル外"
            errors.append(f"E1: SE#{s['id']} frame {f} は {where}（emphasis/warning/success/タイトルのstartFrameに置く）")
        if f <= first_telop or (titles and f == 0):
            errors.append(f"E2: SE#{s['id']} frame {f} 冒頭クリーン違反（最初のテロップ frame {first_telop} 以前）")
        if i > 0:
            gap = f - ses_sorted[i - 1]["frame"]
            if gap == 0:
                errors.append(f"E3: SE#{s['id']} frame {f} 重複")
            elif gap < 10:
                errors.append(f"E3: SE#{s['id']} frame {f} 直前SEと{gap}fしか空いていない（10f以上）")
            if s["file"] == ses_sorted[i - 1]["file"]:
                errors.append(f"E4: SE#{s['id']}「{s['file']}」連続使用")
        if not (0 <= f < TOTAL):
            errors.append(f"E5: SE#{s['id']} frame {f} 範囲外")
        if not (proj / "public" / "se" / s["file"]).exists():
            errors.append(f"E6: SE#{s['id']}「{s['file']}」が public/se/ にない")
        if not (0.15 <= s["vol"] <= 0.40):
            errors.append(f"E7: SE#{s['id']} volume {s['vol']} が 0.15〜0.40 外")
        if f > TOTAL - 5 * FPS:
            msg = f"E8: SE#{s['id']} frame {f} 最後5秒内（余韻のため置かない）"
            if taidan:
                print(f"  警告(対談・締めSEは許容): {msg}")
            else:
                errors.append(msg)

    # E9 密度
    ratio = len(ses) / len(telops) * 100
    thirds_se = [0, 0, 0]
    thirds_tel = [0, 0, 0]
    for s in ses:
        thirds_se[min(2, s["frame"] * 3 // TOTAL)] += 1
    for t in telops:
        thirds_tel[min(2, t["frame"] * 3 // TOTAL)] += 1
    dens = [thirds_se[k] / max(1, thirds_tel[k]) * 100 for k in range(3)]
    if not (min_density <= ratio <= 45):
        errors.append(f"E9: SE密度 {ratio:.1f}%（許容{min_density}〜45%）")
    if max(dens) - min(dens) > 20:
        errors.append(f"E9: 尺3分割の密度差 {max(dens) - min(dens):.1f}pt > 20pt（前半だけ賑やか/後半雑 = NG）")

    files = {}
    for s in ses:
        files[s["file"]] = files.get(s["file"], 0) + 1
    print(f"SE {len(ses)}個 / テロップ {len(telops)}個 = 密度 {ratio:.1f}%")
    print(f"尺3分割の密度: {[f'{d:.1f}%' for d in dens]}")
    print(f"使用SE種類: {len(files)}種 / 音量レンジ {min(s['vol'] for s in ses)}〜{max(s['vol'] for s in ses)}")
    if errors:
        print("\n★検証エラー:")
        for e in errors[:30]:
            print(" -", e)
        sys.exit(1)
    print("検証OK（E1〜E9 全パス）")


if __name__ == "__main__":
    main()
