#!/usr/bin/env python3
"""Lower thirds pack: animated name / title straps burned into a video, or exported as transparent .mov overlays
for any editor. Eight designs, brand colours, free or brand fonts, several straps per video.

Designs: bar (accent bar + white card), pill (rounded capsule), split (two-colour blocks), underline (text with a
drawn rule), news (broadcast strap with a tag), glass (frosted card), neon (glow outline), minimal (thin rule).

Examples
  python lowerthirds.py --showcase --name "Maya Lin" --role "Head Roaster"       # every design as stills: pick one
  python lowerthirds.py --video talk.mp4 --item "Maya Lin|Head Roaster@1-5" --item "Brew & Bloom|Since 2014@8-12" --style bar
  python lowerthirds.py --item "Maya Lin|Head Roaster" --style glass --alpha --duration 5 --aspect 16:9   # overlay only
"""
from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path

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

STYLES = ("bar", "pill", "split", "underline", "news", "glass", "neon", "minimal")


def parse_item(s: str, D: float):
    m = re.match(r"^(.*?)(?:@(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?))?$", s.strip())
    body, a, b = m.group(1), m.group(2), m.group(3)
    name, _, role = body.partition("|")
    start = float(a) if a else min(1.0, D * 0.1)
    end = float(b) if b else min(D - 0.3, start + 4.5)
    return {"name": name.strip(), "role": role.strip(), "start": start, "end": max(end, start + 1.2)}


class Strap:
    """One lower third: draws the card at progress p_in (0..1 animate in) and p_out (0..1 animate out)."""

    def __init__(self, name: str, role: str, style: str, W: int, H: int, accent, color2, font, side: str, tag: str):
        self.W, self.H, self.style, self.side = W, H, style, side
        s = min(W, H)
        self.s = s
        self.accent, self.color2 = tuple(accent), tuple(color2)
        dark = (17, 17, 17, 255)
        white = (255, 255, 255, 255)
        name_col = dark if style in ("bar", "pill") else white
        role_col = (60, 60, 60, 255) if style in ("bar", "pill") else (225, 225, 225, 255)
        if style == "split":
            name_col = K.best_text_color(self.accent)[0] + (255,)
            role_col = K.best_text_color(self.color2)[0] + (255,)
        nsize = int(s * (0.052 if style != "news" else 0.046))
        self.name = K.text_img(name.upper() if style in ("news",) else name, nsize, name_col, "bold", font)
        self.role = K.text_img(role.upper() if style in ("split", "news") else role, int(s * 0.034), role_col, "regular" if not font else "bold", font) if role else None
        self.tag = K.text_img(tag.upper(), int(s * 0.03), K.best_text_color(self.accent)[0] + (255,), "bold", font) if tag else None
        self.pad = int(s * 0.024)

    def card(self, p: float):
        """Card image at reveal progress p (0..1): each style reveals its own way."""
        from PIL import Image, ImageDraw, ImageFilter
        st, pad, nm, rl = self.style, self.pad, self.name, self.role
        tw = max(nm.width, rl.width if rl else 0)
        th = nm.height + ((rl.height + pad // 2) if rl else 0)
        e = K.ease_out_cubic(p)
        if st == "split":
            h1, h2 = nm.height + pad, (rl.height + pad if rl else 0)
            w1, w2 = nm.width + pad * 2, (rl.width + pad * 2 if rl else 0)
            im = Image.new("RGBA", (max(w1, w2) + 4, h1 + h2), (0, 0, 0, 0))
            d = ImageDraw.Draw(im)
            d.rectangle((0, 0, int(w1 * e), h1), fill=self.accent + (255,))
            if rl:
                e2 = K.ease_out_cubic((p - 0.25) / 0.75)
                d.rectangle((0, h1, int(w2 * e2), h1 + h2), fill=self.color2 + (255,))
            im.alpha_composite(K.with_opacity(nm, K.clamp((p - 0.3) / 0.5)), (pad, pad // 2))
            if rl:
                im.alpha_composite(K.with_opacity(rl, K.clamp((p - 0.55) / 0.45)), (pad, h1 + pad // 2))
            return im
        w, h = tw + pad * 3, th + pad * 2
        extra = 0
        if st == "news" and self.tag is not None:
            extra = self.tag.width + pad * 2
        im = Image.new("RGBA", (w + extra + pad * 2, h + pad * 2), (0, 0, 0, 0))
        d = ImageDraw.Draw(im)
        x0, y0 = pad, pad
        wr = int(w * e)
        if st == "bar":
            d.rectangle((x0, y0, x0 + max(1, int(pad * 0.45)), y0 + h), fill=self.accent + (255,))
            if wr > pad:
                d.rectangle((x0 + int(pad * 0.45), y0, x0 + wr, y0 + h), fill=(255, 255, 255, 245))
        elif st == "pill":
            if wr > h // 2:
                d.rounded_rectangle((x0, y0, x0 + wr, y0 + h), radius=h // 2, fill=(255, 255, 255, 240))
            d.ellipse((x0 + pad * 0.6, y0 + h / 2 - pad * 0.35, x0 + pad * 1.3, y0 + h / 2 + pad * 0.35), fill=self.accent + (255,))
        elif st == "underline":
            d.rectangle((x0, y0 + nm.height + pad * 0.55, x0 + int(tw * e) + pad, y0 + nm.height + pad * 0.55 + max(3, self.s // 260)),
                        fill=self.accent + (255,))
        elif st == "news":
            if self.tag is not None:
                d.rectangle((x0, y0, x0 + extra, y0 + h), fill=self.accent + (255,))
                im.alpha_composite(K.with_opacity(self.tag, K.clamp((p - 0.2) / 0.4)), (x0 + pad, y0 + (h - self.tag.height) // 2))
            if wr > 2:
                d.rectangle((x0 + extra, y0, x0 + extra + wr, y0 + h), fill=(12, 18, 34, 235))
                d.rectangle((x0 + extra, y0 + h - max(3, pad // 4), x0 + extra + wr, y0 + h), fill=self.accent + (255,))
        elif st == "glass":
            if wr > pad:
                d.rounded_rectangle((x0, y0, x0 + wr, y0 + h), radius=pad, fill=(255, 255, 255, 46), outline=(255, 255, 255, 120), width=max(1, self.s // 500))
        elif st == "neon":
            if wr > pad:
                d.rounded_rectangle((x0, y0, x0 + wr, y0 + h), radius=pad, outline=self.accent + (255,), width=max(3, self.s // 300), fill=(5, 5, 20, 150))
        elif st == "minimal":
            d.rectangle((x0, y0 + int(h * (1 - e)), x0 + max(2, self.s // 400), y0 + h), fill=(255, 255, 255, 255))
        tx = x0 + extra + (pad * 2 if st in ("bar", "news", "pill") else pad)
        if st == "pill":
            tx = x0 + int(pad * 1.9)
        if st == "underline":
            tx = x0
        to = K.clamp((p - 0.35) / 0.5)
        slide = int((1 - K.ease_out_cubic(to)) * pad)
        im.alpha_composite(K.with_opacity(nm, to), (tx, y0 + pad - slide if st != "underline" else y0 - slide))
        if rl:
            ro = K.clamp((p - 0.5) / 0.5)
            ry = y0 + pad + nm.height + pad // 2 if st != "underline" else y0 + nm.height + pad
            im.alpha_composite(K.with_opacity(rl, ro), (tx, ry))
        if st == "neon":
            im = K.glow(im, max(4, self.s // 140), self.accent, 1.1)
        if st in ("glass", "underline", "minimal"):  # soft shadow keeps bare text readable on busy footage
            sh = Image.new("RGBA", im.size, (0, 0, 0, 0))
            sh.putalpha(im.getchannel("A").point(lambda v: int(v * (0.35 if st == "glass" else 0.7))).filter(ImageFilter.GaussianBlur(max(3, self.s // 150))))
            sh.alpha_composite(im)
            im = sh
        return im

    def frame(self, p_in: float, p_out: float, base=None):
        from PIL import Image
        canvas = base.copy().convert("RGBA") if base is not None else Image.new("RGBA", (self.W, self.H), (0, 0, 0, 0))
        if p_out >= 1 or p_in <= 0:
            return canvas
        c = self.card(K.clamp(p_in))
        m = int(self.s * 0.06)
        y = int(self.H * (0.80 if self.H > self.W else 0.84)) - c.height // 2
        x = m if self.side == "left" else self.W - m - c.width
        o = 1 - K.ease_in_cubic(p_out)
        dx = int(-K.ease_in_cubic(p_out) * self.s * 0.04) * (1 if self.side == "left" else -1)
        K.place(canvas, c, x + dx + c.width / 2, y + c.height / 2, 1.0, o)
        return canvas


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--video", help="video to burn the straps into (URL or file); omit for overlay-only output")
    ap.add_argument("--item", action="append", default=[], help="'Name|Role@start-end' (times optional)")
    ap.add_argument("--name", default="", help="shortcut for one strap (with --role)")
    ap.add_argument("--role", default="")
    ap.add_argument("--tag", default="", help="news style: tag text, e.g. LIVE or BREAKING")
    ap.add_argument("--style", choices=STYLES, default="bar")
    ap.add_argument("--accent", default="#FF5A1F")
    ap.add_argument("--color2", default="#111827", help="split style: second block colour")
    ap.add_argument("--side", choices=["left", "right"], default="left")
    ap.add_argument("--anim-in", type=float, default=0.7)
    ap.add_argument("--anim-out", type=float, default=0.45)
    ap.add_argument("--aspect", default="16:9", help="overlay-only output aspect")
    ap.add_argument("--res", type=int, default=1080)
    ap.add_argument("--duration", type=float, default=5.0, help="overlay-only length")
    ap.add_argument("--fps", type=int, default=30)
    ap.add_argument("--alpha", action="store_true", help="also write one transparent .mov per strap")
    ap.add_argument("--showcase", action="store_true", help="still sheet of every design with --name / --role, then stop")
    K.add_font_args(ap, "TTF/OTF for the straps")
    ap.add_argument("--out", default="kenerate-out/lower-thirds/lower_thirds.mp4")
    ap.add_argument("--keep-temp", action="store_true")
    a = K.parse_args(ap)

    st = K.Status(4, "lower-thirds")
    K.require(status=st)
    tmp = K.workdir(a.keep_temp)
    out = Path(a.out)
    out.parent.mkdir(parents=True, exist_ok=True)
    accent, color2 = K.hex_rgb(a.accent), K.hex_rgb(a.color2)

    st.step("Inputs")
    src, D = None, a.duration
    if a.video:
        src = K.fetch(a.video, tmp, "video", st)
        info = K.probe(src)
        W, H, D = info["width"] // 2 * 2, info["height"] // 2 * 2, info["duration"]
    else:
        W, H = K.size_for(a.aspect, a.res)
    raw = list(a.item) + ([f"{a.name}|{a.role}"] if a.name else [])
    if not raw:
        K.fail("No strap text.", "Pass --item 'Name|Role@1-5' (repeat) or --name / --role.")
    items = [parse_item(s, D) for s in raw]

    if a.showcase:
        st.step(f"Rendering {len(STYLES)} designs")
        bg = K.cover(K.open_image(K.extract_frame(src, D * 0.4, tmp / "bg.jpg")).convert("RGB"), W, H) if src else K.gradient(W, H, (40, 52, 70), (12, 16, 24))
        cells = []
        for sname in STYLES:
            sp = Strap(items[0]["name"], items[0]["role"], sname, W, H, accent, color2, a.font, a.side, a.tag or "LIVE")
            pth = tmp / f"{sname}.jpg"
            sp.frame(1.0, 0.0, bg).convert("RGB").save(pth, quality=88)
            cells.append(pth)
        sheet = K.contact_sheet(cells, out.with_name("lower_thirds_showcase.jpg"), cols=4, cell=420, labels=list(STYLES))
        K.finish({"showcase": str(sheet.resolve()), "styles": list(STYLES)})
        return

    st.step(f"Animating {len(items)} strap(s) ({a.style})")
    straps = []
    events = []
    for it in items:
        sp = Strap(it["name"], it["role"], a.style, W, H, accent, color2, a.font, a.side, a.tag)
        straps.append((it, sp))
        ain, aout = a.anim_in, a.anim_out
        events.append({"start": it["start"], "end": it["end"], "anim_in": ain, "anim_out": aout,
                       "draw": (lambda s_, i_, ai, ao: lambda t: s_.frame((t - i_["start"]) / ai, (t - (i_["end"] - ao)) / ao))(sp, it, ain, aout)})
        st.info(f"{it['name']} | {it['role']}  {it['start']:.1f}-{it['end']:.1f}s")
    st.step("Encoding")
    result = {}
    if src is not None:
        lst = K.timed_overlay_track(events, W, H, D, a.fps, tmp / "ovl", st)
        K.overlay_track(src, lst, out, st)
        result["output"] = K.file_info(out)
        times = [min(it["end"] - 0.1, it["start"] + a.anim_in + 0.3) for it in items][:6]
        result["preview"] = str(K.video_preview(out, out.with_name(out.stem + "_preview.jpg"), tmp=tmp / "pv", times=times).resolve())
    if a.alpha or src is None:
        movs = []
        for i, (it, sp) in enumerate(straps, 1):
            length = it["end"] - it["start"]
            n = int(round(length * a.fps))
            dst = out.with_name(f"{out.stem}_{i:02d}_{K.safe_name(it['name'])[:24]}.mov")
            K.encode_alpha_mov((sp.frame(k / a.fps / a.anim_in, (k / a.fps - (length - a.anim_out)) / a.anim_out) for k in range(n)),
                               dst, W, H, a.fps, n, st)
            movs.append(str(dst.resolve()))
        result["alpha_movs"] = movs
        if src is None:
            cells = []
            for i, (it, sp) in enumerate(straps):
                pth = tmp / f"still_{i}.jpg"
                sp.frame(1.0, 0.0, K.gradient(W, H, (40, 52, 70), (12, 16, 24))).convert("RGB").save(pth, quality=88)
                cells.append(pth)
            result["preview"] = str(K.contact_sheet(cells, out.with_name(out.stem + "_preview.jpg"), cols=min(3, len(cells)), cell=420).resolve())
    K.finish({**result, "style": a.style, "straps": [{k: v for k, v in it.items()} for it in items], "elapsed_s": st.elapsed()})


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