#!/usr/bin/env python3
"""Storyboard to film: cut the generated shots into one short film with a title card, an end card, transitions,
a colour grade, optional cinematic letterbox, per-shot captions / dialogue subtitles and a music bed.

shots.json:
  {"title": "The Paper Boat", "subtitle": "a Kenerate short", "end": "Made with Kenerate",
   "music": "https://.../score.mp3",
   "shots": [{"src": "https://.../shot1.mp4", "in": 0, "out": 4.5, "caption": "Rain on Elm Street"},
             {"src": "https://.../shot2.mp4", "caption": "The boat sets off"}, ...]}

Examples
  python film.py --shots shots.json --aspect 16:9 --grade teal-orange --letterbox 2.39
  python film.py --shot a.mp4 --shot b.mp4@0-3 --title "Launch" --music score.mp3 --transition dissolve
"""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import kmedia as K  # noqa: E402

GRADES = {
    "none": "",
    "warm": "colorbalance=rs=0.05:gs=0.01:bs=-0.06:rm=0.03:bm=-0.03,eq=saturation=1.06",
    "cool": "colorbalance=rs=-0.05:bs=0.06:rm=-0.02:bm=0.03,eq=saturation=0.98",
    "teal-orange": "colorbalance=rs=-0.07:gs=0.0:bs=0.08:rh=0.08:gh=0.02:bh=-0.08,eq=contrast=1.05:saturation=1.1",
    "film": "eq=contrast=1.08:saturation=0.9:gamma=0.97,noise=alls=5:allf=t",
    "mono": "hue=s=0,eq=contrast=1.12",
}


def card(text: str, sub: str, W: int, H: int, dst: Path, font, bg=(8, 8, 10)) -> Path:
    from PIL import Image
    img = Image.new("RGBA", (W, H), tuple(bg) + (255,))
    short = min(W, H)
    K.draw_block(img, text, (int(W * 0.1), int(H * 0.3), int(W * 0.9), int(H * (0.55 if sub else 0.7))), fill=(245, 245, 245),
                 max_size=int(short * 0.12), min_size=20, kind="bold", font_override=font, valign="bottom" if sub else "center", max_lines=3)
    if sub:
        K.draw_block(img, sub, (int(W * 0.15), int(H * 0.58), int(W * 0.85), int(H * 0.68)), fill=(170, 170, 175),
                     max_size=int(short * 0.045), min_size=14, kind="regular", valign="top", max_lines=2)
    img.convert("RGB").save(dst)
    return dst


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--shots", help="shots.json (see above)")
    ap.add_argument("--shot", action="append", default=[], help="URL or file, optional @IN-OUT (repeat, in order)")
    ap.add_argument("--title", default="")
    ap.add_argument("--subtitle", default="")
    ap.add_argument("--end", default="", help="end card text")
    ap.add_argument("--card-seconds", type=float, default=2.5)
    ap.add_argument("--music", default="")
    ap.add_argument("--music-db", type=float, default=-12.0)
    ap.add_argument("--aspect", default="16:9")
    ap.add_argument("--res", type=int, default=1080)
    ap.add_argument("--fps", type=int, default=24, help="24 for a film look (default)")
    ap.add_argument("--transition", default="fade")
    ap.add_argument("--xfade", type=float, default=0.6)
    ap.add_argument("--grade", choices=list(GRADES), default="none")
    ap.add_argument("--letterbox", type=float, default=0.0, help="e.g. 2.39 for cinemascope bars (0 = off)")
    ap.add_argument("--caption-style", choices=K.CAPTION_STYLES, default="minimal")
    K.add_font_args(ap)
    ap.add_argument("--out", default="kenerate-out/storyboard-to-film/film.mp4")
    ap.add_argument("--keep-temp", action="store_true")
    a = K.parse_args(ap)

    spec = json.loads(Path(a.shots).read_text(encoding="utf-8")) if a.shots else {}
    shots = [{"src": s["src"], "in": s.get("in"), "out": s.get("out"), "caption": s.get("caption", "")} for s in spec.get("shots", [])]
    for s in a.shot:
        src, i, o = K.parse_span(s)
        shots.append({"src": src, "in": i, "out": o, "caption": ""})
    if not shots:
        K.fail("No shots.", "Pass --shots shots.json or --shot URL (repeat).")
    title = a.title or spec.get("title", "")
    subtitle = a.subtitle or spec.get("subtitle", "")
    endt = a.end or spec.get("end", "")
    music_src = a.music or spec.get("music", "")

    st = K.Status(7, "storyboard-to-film")
    K.require(status=st)
    W, H = K.size_for(a.aspect, a.res)
    tmp = K.workdir(a.keep_temp)
    out = Path(a.out)
    out.parent.mkdir(parents=True, exist_ok=True)

    st.step(f"Fetching {len(shots)} shot(s)" + (" + music" if music_src else ""))
    paths = K.fetch_all([s["src"] for s in shots], tmp / "in", st, prefix="shot")
    music = K.fetch(music_src, tmp / "in", "music", st) if music_src else None

    st.step("Title / end cards")
    parts = []
    if title:
        p = card(title, subtitle, W, H, tmp / "title.png", a.font)
        d = K.still_to_clip(p, tmp / "title.mp4", W, H, a.card_seconds, a.fps, move=1)
        parts.append(("title", tmp / "title.mp4", d, ""))

    st.step(f"Normalizing shots to {W}x{H} @ {a.fps} fps")
    for i, (p, s) in enumerate(zip(paths, shots), 1):
        start = s["in"] or 0.0
        dur = (s["out"] - start) if s["out"] is not None else None
        dst = tmp / f"shot_{i:02d}.mp4"
        d = K.normalize_clip(p, dst, W, H, a.fps, start, dur, "cover", move=i)
        parts.append(("shot", dst, d, s["caption"]))
        st.progress(i * 100 / len(shots), f"shot {i}/{len(shots)} ({d:.1f}s)")
    if endt:
        p = card(endt, "", W, H, tmp / "end.png", a.font)
        d = K.still_to_clip(p, tmp / "end.mp4", W, H, a.card_seconds, a.fps, move=0)
        parts.append(("end", tmp / "end.mp4", d, ""))

    st.step(f"Editing ({a.transition})")
    joined = tmp / "joined.mp4"
    xf = 0 if a.transition == "cut" else a.xfade
    total = K.join_clips([(p, d) for _, p, d, _ in parts], joined, a.transition, xf, st)
    xf_eff = min(xf, min(d for _, _, d, _ in parts) / 2.5) if xf else 0
    cues, t = [], 0.0
    for i, (_, _, d, cap) in enumerate(parts):
        if cap:
            cues.append((t + (xf_eff if i else 0) + 0.15, t + d - xf_eff - 0.1, cap))
        t += d - xf_eff

    st.step(f"Grade ({a.grade}) + letterbox" if a.letterbox else f"Grade ({a.grade})")
    vf = [GRADES[a.grade]] if GRADES[a.grade] else []
    if a.letterbox and W / H < a.letterbox:
        bar = int((H - W / a.letterbox) / 2)
        if bar > 0:
            vf.append(f"drawbox=x=0:y=0:w=iw:h={bar}:color=black:t=fill,drawbox=x=0:y=ih-{bar}:w=iw:h={bar}:color=black:t=fill")
    cur = joined
    if vf:
        graded = tmp / "graded.mp4"
        K.run_ffmpeg(["-i", str(cur), "-vf", ",".join(vf) + ",format=yuv420p"] + K.video_codec_args(19) + ["-c:a", "copy", str(graded)],
                     duration=total, status=st)
        cur = graded

    st.step(f"Captions ({len(cues)})" if cues else "Captions (none)")
    if cues:
        lst = K.caption_track(cues, W, H, total, tmp / "caps", a.caption_style, "bottom", a.font)
        capped = tmp / "captioned.mp4"
        K.overlay_track(cur, lst, capped, st)
        cur = capped
        K.write_srt(cues, out.with_suffix(".srt"))

    st.step("Music + export" if music else "Export")
    if music:
        K.mix_music(cur, music, out, a.music_db, duck=True, keep_voice=True, fade_out=2.5, status=st)
    else:
        import shutil
        shutil.copyfile(cur, out)
    K.finish({"output": K.file_info(out), "shots": len(shots), "seconds": round(total, 2), "frame": f"{W}x{H}",
              "grade": a.grade, "letterbox": a.letterbox or None, "elapsed_s": st.elapsed()})


if __name__ == "__main__":
    K.main_guard(main)
