#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Generator slide carousel gaya TS Law (krem kertas + headline bold + aksen oranye).

Niru desain kiriman Bos (`tslaw_car_ts2/1..6.png`): 1080x1350, latar kertas krem bertekstur,
header "Tito Suprianto Law Firm." / "www.tslaw.id", headline SF Pro Display Bold rata kiri,
goresan oranye + panah putus-putus, footer "@tslaw_firm" / "#hukumpidana".

Contoh:  python3 gen_tslaw_slide.py --demo     # 3 slide contoh ke /tmp/tslaw_gen/
         python3 gen_tslaw_slide.py --deck my.json --outdir out/
"""
import argparse
import json
import math
import os
import random
import sys

from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageFont

W, H = 1080, 1350
BASE = (238, 229, 215)      # krem kertas (sampel dari file Bos)
INK = (26, 24, 22)          # hitam teks
ORANGE = (249, 95, 52)      # #F95F34
MARGIN = 95

FONT_DIR = "/home/mantapman/.fonts"
F_DISPLAY_B = f"{FONT_DIR}/SFProDisplay-Bold.otf"
F_DISPLAY_R = f"{FONT_DIR}/SFProDisplay-Regular.otf"
F_TEXT_B = f"{FONT_DIR}/SFProText-Bold.otf"
F_TEXT_R = f"{FONT_DIR}/SFProText-Regular.otf"


# ---------------------------------------------------------------- latar kertas
def paper_bg(seed=7):
    rnd = random.Random(seed)
    base = Image.new("RGB", (W, H), BASE)

    # grain halus (tekstur kertas)
    raw = bytes(rnd.getrandbits(8) for _ in range(W * H))
    grain = Image.frombytes("L", (W, H), raw).filter(ImageFilter.GaussianBlur(0.5))
    grey = Image.new("L", (W, H), 128)
    grain = Image.blend(grey, grain, 0.16)
    noisy = ImageChops.add(base, Image.merge("RGB", (grain, grain, grain)), scale=1.0, offset=-128)

    # bercak besar (serat kertas) — bikin tidak rata seperti kertas daur ulang
    blot_raw = bytes(rnd.getrandbits(8) for _ in range((W // 8 + 1) * (H // 8 + 1)))
    blot = Image.frombytes("L", (W // 8 + 1, H // 8 + 1), blot_raw)
    blot = blot.resize((W, H), Image.BICUBIC).filter(ImageFilter.GaussianBlur(28))
    blot = Image.blend(Image.new("L", (W, H), 128), blot, 0.10)
    noisy = ImageChops.add(noisy, Image.merge("RGB", (blot, blot, blot)), scale=1.0, offset=-128)

    # gradasi hangat ke sudut kanan bawah (sampel Bos: pojok bawah lebih kuning/gelap)
    grad = Image.new("L", (W, H), 0)
    gd = ImageDraw.Draw(grad)
    for i in range(H):
        gd.line([(0, i), (W, i)], fill=int(26 * (i / H) ** 2))
    warm = Image.merge("RGB", (grad, (grad.point(lambda v: int(v * 0.75))), (grad.point(lambda v: int(v * 0.4)))))
    noisy = ImageChops.subtract(noisy, warm)
    return noisy


# ---------------------------------------------------------------- teks
def wrap(draw, text, font, maxw):
    words, lines, cur = text.split(), [], ""
    for w in words:
        uji = f"{cur} {w}".strip()
        if draw.textlength(uji, font=font) <= maxw:
            cur = uji
        else:
            if cur:
                lines.append(cur)
            cur = w
    if cur:
        lines.append(cur)
    return lines


def blok_teks(draw, text, font, maxw):
    lines, cur = [], ""
    for par in text.split("\n"):
        if not par.strip():
            lines.append("")
            cur = ""
            continue
        words = par.split()
        cur = cur.strip()
        for w in words:
            uji = f"{cur} {w}".strip()
            if draw.textlength(uji, font=font) <= maxw:
                cur = uji
            else:
                lines.append(cur)
                cur = w
        if cur:
            lines.append(cur)
            cur = ""
    return lines


def ukuran_pas(text, font_path, maxw, start, min_size):
    """Cari ukuran font terbesar yang membuat teks muat dalam maxw."""
    tmp = ImageDraw.Draw(Image.new("RGB", (10, 10)))
    size = start
    while size > min_size:
        f = ImageFont.truetype(font_path, size)
        if all(tmp.textlength(l, font=f) <= maxw for l in text.split("\n")):
            return size
        size -= 2
    return min_size


def tulis(draw, xy, text, font, fill=INK, spacing=10):
    x, y = xy
    for line in text.split("\n"):
        draw.text((x, y), line, font=font, fill=fill)
        y += font.size + spacing
    return y


def tulis_blok(draw, xy, text, font, maxw, fill=INK, spacing=10):
    """Tulis teks dengan WRAPPING otomatis (tidak melampaui maxw)."""
    x, y = xy
    for par in text.split("\n"):
        if not par.strip():
            y += int(font.size * 0.62)
            continue
        for line in blok_teks(draw, par, font, maxw):
            draw.text((x, y), line, font=font, fill=fill)
            y += font.size + spacing
    return y


def ukuran_blok(text, font_path, maxw, start, min_size, maks_baris):
    """Ukuran font terbesar yang membuat teks (setelah di-wrap) maksimal `maks_baris` baris."""
    tmp = ImageDraw.Draw(Image.new("RGB", (10, 10)))
    size = start
    while size > min_size:
        f = ImageFont.truetype(font_path, size)
        n = sum(len(blok_teks(tmp, p, f, maxw)) for p in text.split("\n") if p.strip())
        if n <= maks_baris:
            return size
        size -= 2
    return min_size


# ---------------------------------------------------------------- aksen
def goresan_oranye(draw, x0, y0, x1, y1=None, tebal_awal=17, tebal_akhir=4, lengkung=14):
    """Goresan kuas oranye (polygon menirus) — pengganti garis biasa."""
    if y1 is None:
        y1 = y0
    n = 60
    atas, bawah = [], []
    for i in range(n + 1):
        t = i / n
        x = x0 + (x1 - x0) * t
        y = y0 + (y1 - y0) * t + math.sin(math.pi * t) * -lengkung
        th = tebal_awal + (tebal_akhir - tebal_awal) * t
        atas.append((x, y - th / 2 + 2 * t))
        bawah.append((x, y + th / 2))
    draw.polygon(atas + bawah[::-1], fill=ORANGE)


def panah_putus(draw, p0, p1, p2, dash=34, gap=26, width=13):
    """Panah melengkung putus-putus (kuadratik bezier) + mata panah segitiga."""
    pts = []
    n = 240
    for i in range(n + 1):
        t = i / n
        x = (1 - t) ** 2 * p0[0] + 2 * (1 - t) * t * p1[0] + t ** 2 * p2[0]
        y = (1 - t) ** 2 * p0[1] + 2 * (1 - t) * t * p1[1] + t ** 2 * p2[1]
        pts.append((x, y))
    # panjang kumulatif
    seg = [0.0]
    for i in range(1, len(pts)):
        seg.append(seg[-1] + math.dist(pts[i - 1], pts[i]))
    total = seg[-1]
    pos, on = 0.0, True
    while pos < total:
        panjang = dash if on else gap
        a, b = pos, min(pos + panjang, total)
        if on:
            potong = [p for p, s in zip(pts, seg) if a - 0.01 <= s <= b + 0.01]
            if len(potong) > 1:
                draw.line(potong, fill=ORANGE, width=width, joint="curve")
                for e in (potong[0], potong[-1]):
                    r = width / 2
                    draw.ellipse([e[0] - r, e[1] - r, e[0] + r, e[1] + r], fill=ORANGE)
        pos += panjang
        on = not on
    # mata panah di ujung p2 mengikuti arah akhir kurva
    ax, ay = pts[-1]
    bx, by = pts[-14]
    sudut = math.atan2(ay - by, ax - bx)
    L, lebar = 44, 30
    tip = (ax + math.cos(sudut) * L * 0.55, ay + math.sin(sudut) * L * 0.55)
    kiri = (ax - math.cos(sudut) * L * 0.45 + math.cos(sudut + math.pi / 2) * lebar / 2,
            ay - math.sin(sudut) * L * 0.45 + math.sin(sudut + math.pi / 2) * lebar / 2)
    kanan = (ax - math.cos(sudut) * L * 0.45 - math.cos(sudut + math.pi / 2) * lebar / 2,
             ay - math.sin(sudut) * L * 0.45 - math.sin(sudut + math.pi / 2) * lebar / 2)
    draw.polygon([tip, kiri, kanan], fill=ORANGE)


def ikon_bookmark(draw, x, y, w=92, h=126, width=9, color=ORANGE):
    """Ikon bookmark garis (dipakai di slide CTA)."""
    draw.line([(x, y), (x + w, y)], fill=color, width=width)
    draw.line([(x, y), (x, y + h)], fill=color, width=width)
    draw.line([(x + w, y), (x + w, y + h)], fill=color, width=width)
    draw.line([(x, y + h), (x + w / 2, y + h - w * 0.55)], fill=color, width=width, joint="curve")
    draw.line([(x + w / 2, y + h - w * 0.55), (x + w, y + h)], fill=color, width=width, joint="curve")


# ---------------------------------------------------------------- slide
def render(slide, seed=7):
    im = paper_bg(seed)
    d = ImageDraw.Draw(im)
    maks = slide.get("maxw", W - 2 * MARGIN)

    # header
    d.text((MARGIN, 118), slide.get("header_kiri", "Tito Suprianto Law Firm."),
           font=ImageFont.truetype(F_TEXT_R, 27), fill=(38, 36, 33))
    hd = "www.tslaw.id"
    f_text_b = ImageFont.truetype(F_TEXT_B, 27)
    d.text((W - MARGIN - d.textlength(hd, font=f_text_b), 118), hd, font=f_text_b, fill=(38, 36, 33))

    # headline
    hx, hy = slide.get("headline_xy", (MARGIN, 340))

    if slide.get("mitos"):
        f_lab = ImageFont.truetype(F_TEXT_B, slide.get("label_size", 30))
        d.text((hx, hy), slide.get("label_mitos", "MITOS"), font=f_lab, fill=ORANGE)
        uk = slide.get("mitos_size") or ukuran_blok(slide["mitos"], F_DISPLAY_B, maks, 56, 34,
                                                    slide.get("mitos_baris", 3))
        f_m = ImageFont.truetype(F_DISPLAY_B, uk)
        y = tulis_blok(d, (hx, hy + 50), slide["mitos"], f_m, maks, INK, slide.get("mitos_spacing", 6))
        goresan_oranye(d, hx, y + 40, hx + 240, y + 40, 13, 3, 9)
        y += 92
        d.text((hx, y), slide.get("label_fakta", "FAKTANYA"), font=f_lab, fill=ORANGE)
        uf = slide.get("fakta_size") or ukuran_blok(slide["fakta"], F_TEXT_R, maks, 36, 24,
                                                    slide.get("fakta_baris", 7))
        f_f = ImageFont.truetype(F_TEXT_R, uf)
        tulis_blok(d, (hx, y + 56), slide["fakta"], f_f, maks, (30, 28, 26), slide.get("fakta_spacing", 15))
    else:
        ukuran = slide.get("headline_size") or ukuran_pas(slide["headline"], F_DISPLAY_B, maks, 110, 54)
        f_head = ImageFont.truetype(F_DISPLAY_B, ukuran)
        y = tulis(d, (hx, hy), slide["headline"], f_head, INK, slide.get("headline_spacing", 8))

    # subteks
    if slide.get("sub"):
        f_sub = ImageFont.truetype(F_TEXT_R, slide.get("sub_size", 34))
        y += 26
        y = tulis_blok(d, (hx, y), slide["sub"], f_sub, maks, (30, 28, 26), slide.get("sub_spacing", 14))

    # daftar poin (bullet pakai titik • — hindari U+2192 yang jadi tofu di Poppins)
    if slide.get("poin"):
        f_p = ImageFont.truetype(F_TEXT_R, slide.get("poin_size", 36))
        y += slide.get("poin_gap", 40)
        for p in slide["poin"]:
            for i, line in enumerate(blok_teks(d, p, f_p, maks - 30)):
                d.text((hx + (0 if i == 0 else 30), y), line, font=f_p, fill=(30, 28, 26))
                y += f_p.size + 16
            y += 28

    # aksen oranye
    for ik in slide.get("ikon", []):
        if ik.get("jenis") == "bookmark":
            ikon_bookmark(d, ik["x"], ik["y"], ik.get("w", 92), ik.get("h", 126))
    for g in slide.get("goresan", []):
        goresan_oranye(d, *g)
    for a in slide.get("panah", []):
        panah_putus(d, *a)

    # footer
    ff = ImageFont.truetype(F_TEXT_B, 27)
    d.text((MARGIN, H - 140), slide.get("footer_kiri", "@tslaw_firm"), font=ff, fill=(38, 36, 33))
    tag = slide.get("footer_kanan", "#hukumpidana")
    d.text((W - MARGIN - d.textlength(tag, font=ff), H - 140), tag, font=ff, fill=(38, 36, 33))
    return im


DEMO = [
    {   # 1 — cover
        "headline": "Sudah divonis\nbersalah dalam\nperkara Narkotika,\napakah masih bisa\ndiperjuangkan?",
        "headline_xy": (MARGIN, 300),
        "maxw": 760,
        "goresan": [(120, 985, 700, 985, 17, 5, 16)],
        "panah": [((820, 1060), (1075, 900), (950, 1050))],
    },
    {   # 2 — jawaban
        "headline": "Tentunya,\nbisa.",
        "headline_xy": (250, 265),
        "sub": "Upaya Hukum Peninjauan Kembali\nmerupakan upaya hukum luar biasa\nyang dapat diajukan terhadap putusan\nyang telah berkekuatan hukum tetap.",
        "sub_size": 34, "sub_spacing": 14,
        "panah": [((0, 700), (60, 700), (170, 690)), ((650, 1050), (950, 1010), (1070, 900))],
    },
    {   # 3 — daftar alasan
        "headline": "Alasan Peninjauan\nKembali yakni",
        "headline_xy": (MARGIN, 430),
        "poin": ["Ditemukan keadaan atau bukti baru",
                 "Terdapat pertentangan antara putusan-putusan"],
        "poin_size": 36,
        "panah": [((820, 350), (1000, 340), (1075, 430)), ((0, 1010), (30, 1010), (150, 995))],
    },
]


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--demo", action="store_true")
    ap.add_argument("--deck")
    ap.add_argument("--outdir", default="/tmp/tslaw_gen")
    ap.add_argument("--ext", default="png")
    a = ap.parse_args()
    os.makedirs(a.outdir, exist_ok=True)
    slides = DEMO if a.demo or not a.deck else json.load(open(a.deck))
    for i, s in enumerate(slides, 1):
        im = render(s, seed=7 + i)
        out = f"{a.outdir}/tslaw_gen_{i:02d}.{a.ext}"
        if a.ext.lower() in ("jpg", "jpeg"):
            im.convert("RGB").save(out, "JPEG", quality=90, optimize=True)
        else:
            im.save(out)
        print("tulis", out, im.size, os.path.getsize(out) // 1024, "KB")


if __name__ == "__main__":
    sys.exit(main())
