#!/usr/bin/env python3
"""Transitions pack: join clips with designed transitions (whip pan, flash, zoom punch, glitch, blur dissolve,
light leak, luma wipes ...) - one style for every cut or one per cut - and render a showcase reel so the user can
pick before the final edit. Optional whoosh / hit sound per transition (only a file the user supplied or approved).

Examples
  python transitions.py --showcase --clips a.mp4 b.mp4                    # every transition, labelled, + contact sheet
  python transitions.py --clips a.mp4 b.mp4 c.mp4 --transition whip-left --aspect 9:16
  python transitions.py --clips a.mp4 b.mp4 c.mp4 --transitions zoom-punch,flash --sfx whoosh.mp3
  python transitions.py --list
"""
from __future__ import annotations

import argparse
import sys
from pathlib import Path

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

# name: (xfade transition, default seconds, post effect over the transition window or "", description)
PACK = {
    "crossfade": ("fade", 0.5, "", "soft dissolve"),
    "dip-black": ("fadeblack", 0.6, "", "fade through black"),
    "flash": ("fadewhite", 0.3, "eq=brightness=0.25:contrast=1.2", "white flash cut (beat hits)"),
    "whip-left": ("slideleft", 0.28, "dblur=angle=0:radius=40", "fast whip pan to the left with motion blur"),
    "whip-right": ("slideright", 0.28, "dblur=angle=180:radius=40", "fast whip pan to the right with motion blur"),
    "whip-up": ("slideup", 0.28, "dblur=angle=90:radius=40", "vertical whip (great on 9:16)"),
    "zoom-punch": ("zoomin", 0.35, "gblur=sigma=6", "punch-in zoom with a blur kick"),
    "blur-dissolve": ("dissolve", 0.6, "gblur=sigma=18", "dreamy blur through"),
    "glitch": ("pixelize", 0.35, "rgbashift=rh=14:bh=-14:gv=6,noise=alls=35:allf=t", "digital glitch: RGB split + noise"),
    "light-leak": ("fade", 0.7, "colorbalance=rs=0.5:gs=0.2:bs=-0.35,eq=brightness=0.28:saturation=1.35", "warm film light leak"),
    "circle-reveal": ("circleopen", 0.6, "", "iris opens on the next shot"),
    "wipe-left": ("wipeleft", 0.5, "", "hard wipe"),
    "smooth-left": ("smoothleft", 0.5, "", "soft-edged wipe"),
    "slide-push": ("slideleft", 0.45, "", "push the shot out"),
    "radial": ("radial", 0.6, "", "clock wipe"),
    "cut": ("", 0.0, "", "hard cut"),
}


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--clips", nargs="*", default=[], help="videos / images in order (URLs or files)")
    ap.add_argument("--transition", default="crossfade", help="one style for every cut (see --list)")
    ap.add_argument("--transitions", default="", help="comma list, one per cut (cycled if shorter)")
    ap.add_argument("--seconds", type=float, default=0, help="override every transition's length")
    ap.add_argument("--aspect", default="16:9")
    ap.add_argument("--res", type=int, default=1080)
    ap.add_argument("--fps", type=int, default=30)
    ap.add_argument("--still-seconds", type=float, default=2.5)
    ap.add_argument("--sfx", help="sound for each transition (whoosh / hit) - only with the user's OK")
    ap.add_argument("--sfx-db", type=float, default=-8.0)
    ap.add_argument("--showcase", action="store_true", help="demo every transition between the first two clips")
    ap.add_argument("--list", action="store_true", help="print the transitions and exit")
    ap.add_argument("--out", default="kenerate-out/transitions-pack/joined.mp4")
    ap.add_argument("--keep-temp", action="store_true")
    a = ap.parse_args()

    if a.list:
        for k, v in PACK.items():
            print(f"{k:14s} {v[1]:.2f}s  {v[3]}")
        K.finish({"transitions": {k: v[3] for k, v in PACK.items()}})
        return
    if len(a.clips) < 2:
        K.fail("Need at least two clips.", "Pass --clips a.mp4 b.mp4 [c.mp4 ...]")
    names = [x.strip() for x in a.transitions.split(",") if x.strip()] or [a.transition]
    bad = [n for n in names if n not in PACK]
    if bad:
        K.fail(f"Unknown transition(s): {', '.join(bad)}", "Run with --list to see the pack.")

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

    st.step(f"Fetching {len(a.clips)} clip(s)")
    srcs = K.fetch_all(a.clips, tmp / "in", st, prefix="clip")
    sfx = K.fetch(a.sfx, tmp / "in", "sfx", st) if a.sfx else None

    st.step(f"Normalizing to {W}x{H}")
    norm = []
    if a.showcase:  # A, B, A, B ... with one transition each, 2 s per shot
        shown = [n for n in PACK if n != "cut"]
        base = []
        for i, p in enumerate(srcs[:2]):
            d = tmp / f"n{i}.mp4"
            K.normalize_clip(p, d, W, H, a.fps, 0, 2.2, "cover", still_seconds=2.2, move=i)
            base.append(d)
        for i in range(len(shown) + 1):
            norm.append((base[i % 2], K.probe(base[i % 2])["duration"]))
        names = shown
    else:
        for i, p in enumerate(srcs, 1):
            d = tmp / f"n{i:02d}.mp4"
            norm.append((d, K.normalize_clip(p, d, W, H, a.fps, still_seconds=a.still_seconds, move=i, status=st)))

    st.step("Joining with transitions")
    cuts = len(norm) - 1
    seq = [names[i % len(names)] for i in range(cuts)]
    inputs = []
    for p, _ in norm:
        inputs += ["-i", str(p)]
    parts, offs = [], []
    vprev, aprev, offset = "[0:v]", "[0:a]", 0.0
    for i in range(1, len(norm)):
        tr, dflt, post, _ = PACK[seq[i - 1]]
        xf = a.seconds or dflt
        xf = min(xf, norm[i - 1][1] / 2.5, norm[i][1] / 2.5) if tr else 0.0
        if not tr or xf < 0.04:
            offset += norm[i - 1][1]
            parts.append(f"{vprev}[{i}:v]concat=n=2:v=1:a=0[v{i}]")
            parts.append(f"{aprev}[{i}:a]concat=n=2:v=0:a=1[a{i}]")
            offs.append((offset, 0.0, ""))
        else:
            offset += norm[i - 1][1] - xf
            parts.append(f"{vprev}[{i}:v]xfade=transition={tr}:duration={xf:.3f}:offset={offset:.3f}[v{i}]")
            parts.append(f"{aprev}[{i}:a]acrossfade=d={xf:.3f}:c1=tri:c2=tri[a{i}]")
            offs.append((offset, xf, post))
        vprev, aprev = f"[v{i}]", f"[a{i}]"
    total = offset + norm[-1][1]
    chain = []
    for (o, xf, post) in offs:
        if post and xf > 0:
            for f in post.split(","):  # each filter of the effect only inside its transition window
                chain.append(f"{f}:enable='between(t,{o:.3f},{o + xf:.3f})'")
    graph = ";".join(parts) + f";{vprev}{(','.join(chain) + ',') if chain else ''}format=yuv420p[v]"
    amap = aprev
    if sfx is not None:
        sfx_in = len(norm)
        inputs += ["-i", str(sfx)]
        vol = 10 ** (a.sfx_db / 20)
        k = len(offs)
        graph += f";[{sfx_in}:a]aresample=48000,aformat=channel_layouts=stereo,volume={vol:.3f},asplit={k}" + "".join(f"[s{j}]" for j in range(k))
        for j, (o, xf, _) in enumerate(offs):
            ms = int(max(0.0, o + xf / 2 - 0.15) * 1000)
            graph += f";[s{j}]adelay={ms}|{ms}[sd{j}]"
        graph += f";{aprev}" + "".join(f"[sd{j}]" for j in range(k)) + f"amix=inputs={k + 1}:duration=first:normalize=0,alimiter=limit=0.95[am]"
        amap = "[am]"
    if a.showcase:  # label each transition
        from PIL import Image
        events = []
        for name, (o, xf, _) in zip(seq, offs):
            spr = K.text_img(name, int(min(W, H) * 0.06), (255, 255, 255, 255), "bold", stroke=max(2, min(W, H) // 200))
            img = Image.new("RGBA", (W, H), (0, 0, 0, 0))
            img.alpha_composite(spr, (int(W * 0.04), int(H * 0.05)))
            events.append({"start": max(0.0, o - 0.6), "end": o + xf + 0.6, "draw": (lambda im: lambda t: im)(img)})
        lst = K.timed_overlay_track(events, W, H, total, a.fps, tmp / "labels")
        joined = tmp / "joined.mp4"
        K.run_ffmpeg(inputs + ["-filter_complex", graph, "-map", "[v]", "-map", amap] + K.video_codec_args(19, "veryfast")
                     + K.AUDIO_ARGS + [str(joined)], duration=total, status=st)
        K.overlay_track(joined, lst, out, st)
    else:
        K.run_ffmpeg(inputs + ["-filter_complex", graph, "-map", "[v]", "-map", amap] + K.video_codec_args(19)
                     + K.AUDIO_ARGS + [str(out)], duration=total, status=st)

    st.step("Preview (mid-transition frames)")
    times = [o + xf / 2 for (o, xf, _) in offs][:18] or [total / 2]
    sheet = K.video_preview(out, out.with_name(out.stem + "_preview.jpg"), tmp=tmp / "pv", times=times)
    st.step("Done")
    K.finish({"output": K.file_info(out), "preview": str(sheet.resolve()), "seconds": round(total, 2),
              "transitions": [{"at": round(o, 2), "style": n, "seconds": round(xf, 2)} for n, (o, xf, _) in zip(seq, offs)],
              "sfx": bool(sfx), "elapsed_s": st.elapsed()})


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