#!/usr/bin/env python3
"""Build prototype/assets/media: muted, seamlessly looping background films with posters.

Sources are free Pexels clips. Pexels License: free for commercial use, no attribution
required, and the people shown must not appear to endorse the product (so no names or
quotes next to them). Credit is kept here anyway:
  try-on-stripes  "Woman fitting clothes"          Alena Darmel  https://www.pexels.com/video/8994266/
  try-on-blazer   "A woman putting on a blazer"    Alena Darmel  https://www.pexels.com/video/8992079/
  try-on-shoes    "Woman choosing shoes"           Alena Darmel  https://www.pexels.com/video/8993959/
  mirror-shirts   "Woman checking out new clothes" Liza Summer   https://www.pexels.com/video/6342221/
  rack-choosing   "Woman checking clothes"         Pexels        https://www.pexels.com/video/12908968/
  friends-try-on  "Young woman trying on clothes"  Pexels        https://www.pexels.com/video/7951318/

Each film is cropped to 3:4, scaled to 768x1024 and cut into a loop whose end dissolves into
its start. The loop is rotated so that its first frame, which is also the poster that
reduced-motion visitors see, is a smiling one.

Usage:  python3 tools/build_media.py [--cache DIR]   (needs ffmpeg; downloads ~23 MB once)
"""
from __future__ import annotations

import argparse
import subprocess
import tempfile
import urllib.request
from dataclasses import dataclass
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
OUT_DIR = ROOT / "prototype" / "assets" / "media"
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) SKAP-build-media"
# 3:4 windows of the sources, head room kept
PORTRAIT_1080 = "crop=1080:1440:0:288"
PORTRAIT_720 = "crop=720:960:0:192"
SIZE = "768:1024"
FPS = 25
CRF = 28
DOWNLOAD_TIMEOUT_S = 120


@dataclass(frozen=True)
class Film:
    name: str
    url: str
    start: float  # loop segment in source seconds
    end: float
    fade: float  # length of the dissolve from end into start
    entry: float  # source second where the loop, and so the poster, begins
    crop: str = PORTRAIT_1080


FILMS = (
    Film("try-on-stripes", "https://videos.pexels.com/video-files/8994266/8994266-hd_1080_1920_25fps.mp4",
         start=5.0, end=18.4, fade=1.0, entry=15.9),
    Film("try-on-blazer", "https://videos.pexels.com/video-files/8992079/8992079-hd_1080_1920_25fps.mp4",
         start=2.5, end=15.0, fade=1.0, entry=10.4),
    Film("try-on-shoes", "https://videos.pexels.com/video-files/8993959/8993959-hd_1080_1920_25fps.mp4",
         start=12.8, end=18.2, fade=0.8, entry=15.6),
    Film("mirror-shirts", "https://videos.pexels.com/video-files/6342221/6342221-hd_720_1280_24fps.mp4",
         start=0.3, end=9.8, fade=1.0, entry=4.4, crop=PORTRAIT_720),
    Film("rack-choosing", "https://videos.pexels.com/video-files/12908968/12908968-hd_1080_1920_24fps.mp4",
         start=0.4, end=9.8, fade=1.0, entry=5.6),
    Film("friends-try-on", "https://videos.pexels.com/video-files/7951318/7951318-hd_1920_1080_30fps.mp4",
         start=0.5, end=12.6, fade=1.0, entry=8.9, crop="crop=810:1080:960:0"),
)


def fetch(film: Film, cache: Path) -> Path:
    target = cache / f"{film.name}-source.mp4"
    if target.is_file():
        return target
    request = urllib.request.Request(film.url, headers={"User-Agent": USER_AGENT})
    with urllib.request.urlopen(request, timeout=DOWNLOAD_TIMEOUT_S) as response:
        target.write_bytes(response.read())
    return target


def seconds(value: float) -> str:
    """Filter-safe timestamp: 18.4 - 15.9 - 1.0 is 1.4999999999999982 in floating point."""
    return f"{value:.3f}"


def loop_filter(film: Film) -> str:
    """[entry, end] dissolves into [start, start + fade], then [start + fade, entry] closes the loop."""
    start, end, entry, fade = map(seconds, (film.start, film.end, film.entry, film.fade))
    head_end = seconds(film.start + film.fade)
    offset = seconds(film.end - film.entry - film.fade)
    return (
        f"[0:v]{film.crop},scale={SIZE}:flags=lanczos,fps={FPS},format=yuv420p,split=3[a][b][c];"
        f"[a]trim=start={entry}:end={end},setpts=PTS-STARTPTS[tail];"
        f"[b]trim=start={start}:end={head_end},setpts=PTS-STARTPTS[head];"
        f"[c]trim=start={head_end}:end={entry},setpts=PTS-STARTPTS[rest];"
        f"[tail][head]xfade=transition=fade:duration={fade}:offset={offset}[seam];"
        "[seam][rest]concat=n=2:v=1:a=0[v]"
    )


def ffmpeg(*args: str) -> None:
    subprocess.run(["ffmpeg", "-v", "error", "-y", *args], check=True)


def build(film: Film, cache: Path) -> None:
    if not film.start < film.start + film.fade < film.entry < film.end - film.fade:
        raise ValueError(f"{film.name}: entry must sit inside the loop, away from the dissolve")
    source = fetch(film, cache)
    video = OUT_DIR / f"{film.name}.mp4"
    ffmpeg("-i", str(source), "-filter_complex", loop_filter(film), "-map", "[v]", "-an",
           "-c:v", "libx264", "-preset", "slow", "-crf", str(CRF), "-profile:v", "high",
           "-pix_fmt", "yuv420p", "-movflags", "+faststart", str(video))
    ffmpeg("-i", str(video), "-frames:v", "1", "-q:v", "5", str(OUT_DIR / f"{film.name}.jpg"))
    print(f"wrote {video.name} ({video.stat().st_size // 1024} KB) and its poster")


def main() -> None:
    parser = argparse.ArgumentParser(description="Cut the prototype's background films.")
    parser.add_argument("--cache", type=Path, default=Path(tempfile.gettempdir()) / "skap-media",
                        help="where the downloaded sources are kept between runs")
    args = parser.parse_args()
    args.cache.mkdir(parents=True, exist_ok=True)
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    for film in FILMS:
        build(film, args.cache)


if __name__ == "__main__":
    main()
