"""Turn an uploaded frame folder into the hero sequence the site loads.

Frames dropped in by hand arrive with whatever naming the exporter used and
still carry the burnt-in watermark. This normalises the numbering to
frame_0001.jpg… and patches the watermark, writing into public/frames/.

The originals in the input folder are never modified, so this can be re-run.

Note: scripts/extract_frames.py writes to the same output folder and wipes it
first. Run one or the other, not both.

Each sequence has its own watermark in its own place, so the delogo box is per
folder — measure it before adding a new one rather than reusing another's.

Usage:
    python scripts/prepare_frames.py                       # hero sequence
    python scripts/prepare_frames.py --set services        # services sequence
    python scripts/prepare_frames.py --delogo ''           # keep the watermark
"""

from __future__ import annotations

import argparse
import re
import shutil
import subprocess
import sys
from pathlib import Path

import imageio_ffmpeg

ROOT = Path(__file__).resolve().parent.parent

SETS = {
    "hero": {
        "input": "solar-frames",
        "output": "frames",
        # sparkle mark, bottom-right of a 1920x1080 frame
        "always": ["1690:846:100:104"],
        "timed": [],
    },
    "services": {
        "input": "services-frame",
        "output": "frames-services",
        # "Veo" wordmark, tucked into the bottom-right corner
        "always": ["1845:1020:60:42"],
        # Burnt-in captions. Each is on screen for only part of the clip, so
        # each box is gated to its own frame range — patching a caption band
        # in frames that never carried one would smear them for nothing.
        # Ranges are 0-based filter frame numbers.
        # Boxes hug the glyphs deliberately. delogo interpolates from the box
        # border, so anything else caught inside — here the falling sparkle
        # rays — gets smeared into streaks. Tighter box, smaller artefact.
        "timed": [
            ("440:425:1185:155", 0, 80),    # "Power from the Sun"
            ("88:138:1235:135", 74, 239),   # "Seamless Installation" / "Clean Energy Flow"
        ],
    },
}


def build_vf(cfg, disable: bool) -> tuple[str, list[str]]:
    """Filter chain plus human-readable lines describing it."""
    if disable:
        return "", ["Delogo  : disabled"]
    parts, notes = [], []
    for box in cfg["always"]:
        x, y, w, h = box.split(":")
        parts.append("delogo=x={}:y={}:w={}:h={}".format(x, y, w, h))
        notes.append("Delogo  : {} (all frames)".format(box))
    for box, a, b in cfg["timed"]:
        x, y, w, h = box.split(":")
        # Commas inside an expression must be escaped or ffmpeg reads them as
        # filter separators.
        parts.append(
            "delogo=x={}:y={}:w={}:h={}:enable='between(n\\,{}\\,{})'".format(x, y, w, h, a, b)
        )
        notes.append("Delogo  : {} (frames {}-{})".format(box, a + 1, b + 1))
    return ",".join(parts), notes


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--set", choices=sorted(SETS), default="hero")
    ap.add_argument("--input", type=Path, default=None)
    ap.add_argument("--output", type=Path, default=None)
    ap.add_argument("--quality", type=int, default=2, help="ffmpeg -q:v, 2=best")
    ap.add_argument(
        "--keep-overlays",
        action="store_true",
        help="skip watermark and caption patching entirely",
    )
    args = ap.parse_args()

    cfg = SETS[args.set]
    in_dir = args.input or (ROOT / "public" / cfg["input"])
    out_dir = args.output or (ROOT / "public" / cfg["output"])
    vf, vf_notes = build_vf(cfg, args.keep_overlays)

    if not in_dir.is_dir():
        print("Input folder not found: {}".format(in_dir), file=sys.stderr)
        return 1

    frames = sorted(in_dir.glob("*.jpg"))
    if not frames:
        print("No .jpg frames in {}".format(in_dir), file=sys.stderr)
        return 1

    # Derive the exporter's printf pattern from the first filename, so this
    # works whatever zero-padding the source used.
    m = re.match(r"^(.*?)(\d+)(\.jpg)$", frames[0].name)
    if not m:
        print("Cannot parse numbering from {}".format(frames[0].name), file=sys.stderr)
        return 1
    prefix, digits, ext = m.group(1), m.group(2), m.group(3)
    pattern = "{}%0{}d{}".format(prefix, len(digits), ext)
    start = int(digits)

    print("Set     : {}".format(args.set))
    print("Input   : {}  ({} frames, pattern {})".format(in_dir.name, len(frames), pattern))
    print("Output  : public/{}".format(out_dir.name))

    if out_dir.exists():
        shutil.rmtree(out_dir)
    out_dir.mkdir(parents=True)

    cmd = [
        imageio_ffmpeg.get_ffmpeg_exe(), "-hide_banner", "-loglevel", "error", "-y",
        "-start_number", str(start),
        "-i", str(in_dir / pattern),
    ]
    for note in vf_notes:
        print(note)
    if vf:
        cmd += ["-vf", vf]
    cmd += ["-q:v", str(args.quality), "-start_number", "1",
            str(out_dir / "frame_%04d.jpg")]

    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(result.stderr.strip(), file=sys.stderr)
        return result.returncode

    written = sorted(out_dir.glob("frame_*.jpg"))
    total_mb = sum(f.stat().st_size for f in written) / (1024 * 1024)
    print("Wrote   : {} frames, {:.1f} MB".format(len(written), total_mb))

    # A gap would make the canvas request a 404 mid-scroll.
    missing = [i + 1 for i in range(len(written))
               if not (out_dir / "frame_{:04d}.jpg".format(i + 1)).exists()]
    if missing:
        print("Gaps at: {}".format(missing[:5]), file=sys.stderr)
        return 1
    if len(written) != len(frames):
        print("Expected {} frames, wrote {}".format(len(frames), len(written)), file=sys.stderr)
        return 1

    print("FRAME_COUNT = {}".format(len(written)))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
