#!/usr/bin/env python3
"""YouTube thumbnail maker: 1280x720 JPGs (under 2 MB) from a video frame or an image, with huge outlined text,
an optional cut-out subject (remove_background PNG) with a glow, a face / subject zoom, and an A/B set of layouts.

Examples
  python thumb.py --source https://.../video.mp4 --pick-best --text "I TRIED IT FOR 30 DAYS" --variants 4
  python thumb.py --source frame.jpg --subject cutout.png --text "DON'T BUY THIS" --text-b "WORTH IT?" --accent "#FFD60A"
  python thumb.py --source shot.jpg --focus 0.7,0.35 --zoom 1.6 --text "WOW"     # punch in on a face at 70% / 35%
"""
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

W, H = 1280, 720
LAYOUTS = ["text-left", "text-right", "text-top", "text-bottom"]
SCHEMES = [((255, 255, 255), (0, 0, 0)), ((255, 214, 10), (0, 0, 0)), ((255, 255, 255), (220, 30, 60)), ((0, 0, 0), (255, 255, 255))]


def sharpness(path: Path) -> float:
    from PIL import ImageFilter, ImageStat
    im = K.open_image(path).convert("L")
    im.thumbnail((480, 480))
    return ImageStat.Stat(im.filter(ImageFilter.FIND_EDGES)).var[0]


def background(src, focus, zoom: float):
    from PIL import ImageEnhance
    img = K.open_image(src).convert("RGB")
    if zoom > 1.0:
        cw, ch = img.width / zoom, img.height / zoom
        x0 = min(max(img.width * focus[0] - cw / 2, 0), img.width - cw)
        y0 = min(max(img.height * focus[1] - ch / 2, 0), img.height - ch)
        img = img.crop((int(x0), int(y0), int(x0 + cw), int(y0 + ch)))
    img = K.cover(img, W, H, focus)
    img = ImageEnhance.Contrast(img).enhance(1.12)
    img = ImageEnhance.Color(img).enhance(1.25)  # thumbnails read better a bit punchier
    return img


def compose(bg, subject, text: str, layout: str, scheme, accent, font, arrow: bool):
    from PIL import Image, ImageDraw, ImageFilter
    img = bg.convert("RGBA").copy()
    # darken behind the text side
    shade = Image.new("L", (W, H), 0)
    d = ImageDraw.Draw(shade)
    if layout == "text-left":
        for x in range(W // 2 + 120):
            d.line((x, 0, x, H), fill=int(170 * max(0, 1 - x / (W / 2 + 120))))
    elif layout == "text-right":
        for x in range(W // 2 - 120, W):
            d.line((x, 0, x, H), fill=int(170 * max(0, (x - (W / 2 - 120)) / (W / 2 + 120))))
    elif layout == "text-top":
        for y in range(H // 2):
            d.line((0, y, W, y), fill=int(160 * (1 - y / (H / 2))))
    else:
        for y in range(H // 2, H):
            d.line((0, y, W, y), fill=int(160 * ((y - H / 2) / (H / 2))))
    dark = Image.new("RGBA", (W, H), (0, 0, 0, 255))
    dark.putalpha(shade)
    img.alpha_composite(dark)
    if subject is not None:
        s = subject.copy()
        s.thumbnail((int(W * 0.55), int(H * 1.0)))
        x = {"text-left": W - s.width - 10, "text-right": 10}.get(layout, (W - s.width) // 2)
        y = H - s.height
        halo = Image.new("RGBA", s.size, tuple(accent) + (0,))
        halo.putalpha(s.getchannel("A").filter(ImageFilter.MaxFilter(9)).filter(ImageFilter.GaussianBlur(14)))
        img.alpha_composite(halo, (x, y))
        img.alpha_composite(halo, (x, y))
        img.alpha_composite(s, (x, y))
    fill, stroke = scheme
    box = {"text-left": (40, 40, int(W * 0.56), H - 40), "text-right": (int(W * 0.44), 40, W - 40, H - 40),
           "text-top": (60, 24, W - 60, int(H * 0.45)), "text-bottom": (60, int(H * 0.55), W - 60, H - 24)}[layout]
    r = K.draw_block(img, text, box, fill=fill, max_size=200, min_size=40, kind="bold", font_override=font,
                     stroke_ratio=0.085, stroke_fill=stroke, upper=True, shadow=True,
                     align="left" if layout == "text-left" else ("right" if layout == "text-right" else "center"), max_lines=3)
    if arrow and layout in ("text-left", "text-right"):
        d = ImageDraw.Draw(img)
        bx = r["bbox"]
        y = min(H - 70, bx[3] + 40)
        if layout == "text-left":
            x0, x1 = bx[0] + 20, min(bx[2] + 120, int(W * 0.66))
            d.line((x0, y, x1 - 30, y), fill=tuple(accent) + (255,), width=18)
            d.polygon([(x1, y), (x1 - 50, y - 36), (x1 - 50, y + 36)], fill=tuple(accent) + (255,))
        else:
            x0, x1 = bx[2] - 20, max(bx[0] - 120, int(W * 0.34))
            d.line((x1 + 30, y, x0, y), fill=tuple(accent) + (255,), width=18)
            d.polygon([(x1, y), (x1 + 50, y - 36), (x1 + 50, y + 36)], fill=tuple(accent) + (255,))
    return img.convert("RGB"), r


def save_under(img, dst: Path, limit=2 * 1024 * 1024) -> int:
    for q in (92, 88, 84, 80, 75, 70, 62):
        img.save(dst, quality=q, optimize=True, progressive=True)
        if dst.stat().st_size <= limit:
            return q
    return q


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--source", required=True, help="video or image (URL or file)")
    ap.add_argument("--time", type=float, default=None, help="video: frame time in seconds")
    ap.add_argument("--pick-best", action="store_true", help="video: sample 12 frames and use the sharpest")
    ap.add_argument("--text", required=True, help="3-5 words read best")
    ap.add_argument("--text-b", default="", help="alternate text for the B variants")
    ap.add_argument("--subject", default="", help="transparent PNG cut-out (Kenerate remove_background result)")
    ap.add_argument("--focus", default="0.5,0.4", help="x,y 0..1 of the face / subject to keep in frame")
    ap.add_argument("--zoom", type=float, default=1.0, help="punch-in on --focus (1.0 = none, 1.5 = 150%%)")
    ap.add_argument("--accent", default="#FFD60A")
    ap.add_argument("--variants", type=int, default=4, help="how many layouts / colour schemes (1-8)")
    ap.add_argument("--no-arrow", action="store_true")
    K.add_font_args(ap)
    ap.add_argument("--name", default="thumb")
    ap.add_argument("--out-dir", default="kenerate-out/thumbnail-maker")
    ap.add_argument("--keep-temp", action="store_true")
    a = K.parse_args(ap)

    st = K.Status(4, "thumbnail-maker")
    K.require(status=st)
    tmp = K.workdir(a.keep_temp)
    out = Path(a.out_dir)
    out.mkdir(parents=True, exist_ok=True)
    focus = tuple(float(v) for v in a.focus.split(","))

    st.step("Fetching inputs")
    src = K.fetch(a.source, tmp, "source", st)
    subject = K.open_image(K.fetch(a.subject, tmp, "subject", st)).convert("RGBA") if a.subject else None
    if subject is not None and subject.getchannel("A").getextrema()[0] == 255:
        st.warn("--subject has no transparency; run Kenerate remove_background on it first for a clean cut-out")

    st.step("Choosing the frame")
    frame = src
    if K.kind_of(str(src)) != "image":
        info = K.probe(src)
        D = info["duration"]
        if a.pick_best and D > 0:
            cands = []
            for i in range(12):
                t = D * (0.05 + 0.9 * i / 11)
                p = K.extract_frame(src, t, tmp / f"cand_{i:02d}.jpg")
                cands.append((sharpness(p), t, p))
                st.progress((i + 1) * 100 / 12, f"frame {i + 1}/12")
            score, t, frame = max(cands)
            st.info(f"sharpest frame at {t:.2f} s")
        else:
            t = a.time if a.time is not None else D * 0.3
            frame = K.extract_frame(src, t, tmp / "frame.jpg")
            st.info(f"frame at {t:.2f} s")
    bg = background(frame, focus, a.zoom)

    n = max(1, min(8, a.variants))
    st.step(f"Rendering {n} variant(s)")
    accent = K.hex_rgb(a.accent)
    files, rows = [], []
    for i in range(n):
        layout = LAYOUTS[i % len(LAYOUTS)]
        if subject is not None and layout in ("text-top", "text-bottom") and i < 2:
            layout = "text-left"
        scheme = SCHEMES[(i // 2) % len(SCHEMES)] if i else SCHEMES[0]
        if i == 1:
            scheme = ((accent[0], accent[1], accent[2]), (0, 0, 0))
        text = a.text_b if (a.text_b and i % 2 == 1) else a.text
        img, r = compose(bg, subject, text, layout, scheme, accent, a.font, not a.no_arrow)
        ab = "A" if (not a.text_b or i % 2 == 0) else "B"
        dst = out / f"{K.safe_name(a.name)}_{ab}{i + 1}_{layout}.jpg"
        q = save_under(img, dst)
        files.append(dst)
        rows.append({"file": str(dst.resolve()), "text": text, "layout": layout, "font_size": r["font_size"], "jpeg_quality": q,
                     "bytes": dst.stat().st_size})
        st.progress((i + 1) * 100 / n, dst.name)

    st.step("Small-size preview (how it looks in the feed)")
    from PIL import Image
    tiles = []
    for p in files:
        im = Image.open(p).convert("RGB")
        tiles.append(im.resize((336, 189), Image.LANCZOS))  # 2x the 168x94 sidebar size
    sheet = Image.new("RGB", (len(tiles) * 352 + 16, 189 + 32), (15, 15, 15))
    for i, tl in enumerate(tiles):
        sheet.paste(tl, (16 + i * 352, 16))
    sp = out / f"{K.safe_name(a.name)}_feed_preview.jpg"
    sheet.save(sp, quality=90)
    K.finish({"thumbnails": rows, "feed_preview": str(sp.resolve()), "size": f"{W}x{H}", "elapsed_s": st.elapsed()})


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