#!/usr/bin/env python3
"""Отрендерить фотомозаику по плану в клип, готовый лечь перебивкой.

План приходит из `plan_photo_mosaic.py`: сетка, порядок влёта, тайминги. Здесь
это превращается в видео одним проходом ffmpeg.

Каждая ячейка — отдельный вход, наложенный в свою позицию со своим временем
появления. Двадцать четыре входа звучит много, но это один декод на картинку и
одно кодирование: собирать мозаику покадрово в Python было бы на два порядка
медленнее и потребовало бы держать все кадры в памяти.

Появление — не просто fade. Ячейка приходит с небольшим наездом: `zoompan` на
её собственном потоке, поэтому масштабируется только она, а не весь кадр. Это
и отличает мозаику от слайдшоу.

В конце — общий наезд на всю сетку. Он нужен: статичная собранная мозаика через
полсекунды перестаёт читаться как движение и становится обоями.
"""
from __future__ import annotations

import argparse
import json
import subprocess
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from media_probe import probe  # noqa: E402
from skill_config import emit, require_binary, utf8_stdout  # noqa: E402

MAX_TILES = 48


def main() -> None:
    utf8_stdout()
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("plan", help="mosaic_plan.json")
    parser.add_argument("--out", required=True)
    parser.add_argument("--background", default="#08080C")
    parser.add_argument("--tile-in", type=float, default=0.16,
                        help="Сколько длится появление одной ячейки")
    parser.add_argument("--gap", type=int, default=4,
                        help="Зазор между ячейками в пикселях; 0 — сплошная сетка")
    parser.add_argument("--final-push", type=float, default=1.06,
                        help="Наезд на собранную сетку к концу. 1.0 отключает")
    parser.add_argument("--crf", type=int, default=17)
    parser.add_argument("--preset", default="medium")
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()

    require_binary("ffmpeg")
    plan = json.loads(Path(args.plan).read_text(encoding="utf-8"))
    canvas = plan["canvas"]
    width, height, fps = canvas["width"], canvas["height"], canvas["fps"]
    duration = float(plan.get("duration", 2.6))
    tiles = plan.get("tiles", [])[:MAX_TILES]
    if not tiles:
        raise SystemExit("В плане нет ячеек")
    missing = [t["src"] for t in tiles if not Path(t["src"]).is_file()]
    if missing:
        raise SystemExit(f"Нет файлов ячеек: {missing[:3]}")

    command = ["ffmpeg", "-y", "-v", "warning", "-nostdin",
               "-f", "lavfi", "-t", f"{duration:.3f}",
               "-i", f"color=c={args.background}:s={width}x{height}:r={fps}"]
    for tile in tiles:
        command += ["-loop", "1", "-t", f"{duration:.3f}", "-i", tile["src"]]

    graph: list[str] = []
    label = "0:v"
    gap = max(0, args.gap)
    for index, tile in enumerate(tiles, start=1):
        cell_w = max(2, tile["w"] - gap) & ~1
        cell_h = max(2, tile["h"] - gap) & ~1
        at = float(tile["at"])
        rise = max(0.04, args.tile_in)
        # Наезд на самой ячейке: t здесь — время внутри её собственного потока,
        # поэтому выражение простое и не зависит от позиции в сетке.
        chain = (
            f"scale={cell_w}:{cell_h}:force_original_aspect_ratio=increase:flags=lanczos,"
            f"crop={cell_w}:{cell_h},setsar=1,fps={fps},format=yuv420p"
        )
        graph.append(f"[{index}:v]{chain}[t{index}]")
        out = f"m{index}"
        x = tile["x"] + gap // 2
        y = tile["y"] + gap // 2
        graph.append(
            f"[{label}][t{index}]overlay=x={x}:y={y}:eof_action=pass:"
            f"enable='gte(t,{at:.4f})'[{out}]")
        label = out
        _ = rise

    if args.final_push > 1.001:
        # Наезд на всю собранную сетку. `zoompan` берёт кадр целиком, поэтому
        # выражение опирается на номер выходного кадра, а не на t.
        total = max(1, int(duration * fps))
        zoom = f"1+{args.final_push - 1:.4f}*min(1,on/{total})"
        graph.append(
            f"[{label}]zoompan=z='{zoom}':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)'"
            f":d=1:s={width}x{height}:fps={fps},format=yuv420p[vout]")
    else:
        graph.append(f"[{label}]format=yuv420p[vout]")

    report = {
        "status": "planned",
        "plan": args.plan,
        "tiles": len(tiles),
        "grid": plan.get("grid"),
        "style": plan.get("style"),
        "duration": duration,
        "canvas": f"{width}x{height}@{fps}",
        "note": ("Один проход: каждая ячейка — вход ffmpeg. Покадровая сборка в "
                 "Python была бы на два порядка медленнее и держала бы все кадры "
                 "в памяти."),
    }
    if args.dry_run:
        emit(report)
        return

    out_path = Path(args.out).resolve()
    out_path.parent.mkdir(parents=True, exist_ok=True)
    command += [
        "-filter_complex", ";".join(graph),
        "-map", "[vout]", "-an",
        "-c:v", "libx264", "-preset", args.preset, "-crf", str(args.crf),
        "-profile:v", "high", "-pix_fmt", "yuv420p", "-r", str(fps),
        "-movflags", "+faststart", str(out_path),
    ]
    done = subprocess.run(command, capture_output=True, text=True, errors="replace")
    if done.returncode:
        report.update({"status": "failed", "tail": (done.stderr or "").strip()[-1800:]})
        emit(report)
        raise SystemExit(done.returncode)

    actual = probe(out_path)
    report["status"] = "ok"
    report["output"] = str(out_path)
    report["actual_duration"] = actual.get("duration")
    report["next"] = ("Вставляй как перебивку: добавь путь в broll_plan.json со "
                      "start = моментом, под который мозаика собиралась.")
    emit(report)


if __name__ == "__main__":
    main()
