#!/usr/bin/env python3
"""ss_carousel_lib.py — ENGINE STANDAR carousel StudioSoft (FIXED 16 Sep 2026).

Mengikuti standar kartu Threads SS yang sudah di-approve Bos (gen_uky_center.py):
  • RATA KIRI
  • SATU BLOK DI TENGAH VERTIKAL (jarak atas = jarak bawah) -> tidak ada void
    besar di bawah seperti versi lama
  • TANPA garis separator (tick "-----" dihapus)
  • Judul Poppins-Bold 85 / body Poppins-Regular 36 / sub 30 — konsisten semua slide
  • Line advance = standar Bos: judul size+8, body size+12 (JANGAN pakai metrics
    penuh 1.41×size — bikin kendur & overflow ke footer)
  • Judul auto-fit: ukuran diturunkan otomatis supaya tidak melebihi lebar kanvas

WARNA (perintah Bos 16 Sep 2026): BIRU TUA #122B63 sebagai background (atau sebagai
teks kalau background putih — versi bergantian).

AUTO-FIT: kalau isi slide kepanjangan, engine mengecilkan jarak (air) lalu ukuran
huruf bertahap sampai pasti tidak menabrak footer.

Cara pakai:
    from ss_carousel_lib import Slide, SKY
    s = Slide(num=1, total=4, tag="", bg=(18, 43, 99))
    s.title(["CEPAT =", "RANKING"])
    s.para("Kecepatan website penentu halaman pertama.", size=30)
    s.bullets(["LCP: loading konten utama", "INP: respons terhadap klik"])
    s.render("01.png", out_dir="...")
"""
import os

from PIL import Image, ImageDraw, ImageFont

FONT_DIR = os.path.expanduser("~/.fonts")

NAVY = (13, 71, 97)              # arsip (teal-navy lama)
BIRU_TUA = (18, 43, 99)         # #122B63 — warna baku sekarang
SKY = (56, 189, 248)
WHITE = (255, 255, 255)
AKSEN_PUTIH = (37, 99, 235)     # #2563EB — aksen di atas latar putih (SKY terlalu pucat)

W, H = 1080, 1350
M = 70                      # margin kiri/kanan
TITLE = 85                  # judul utama (standar Bos)
LEAD = 36                   # body utama (standar Bos)
SUB = 30                    # body sekunder
H2 = 46                     # sub-judul
NAME = 40                   # nama item pada list
DESC = 30                   # keterangan item
BULLET_INDENT = 52
DOT_R = 8
TOP_LIM, BOT_LIM = 150, 1220   # area isi (footer di y=1292)

_cache = {}


def F(name, size):
    key = (name, size)
    if key not in _cache:
        _cache[key] = ImageFont.truetype(os.path.join(FONT_DIR, name), size)
    return _cache[key]


def wrap(text, font, max_w):
    lines, cur = [], ""
    for word in text.split():
        test = ("%s %s" % (cur, word)).strip()
        if font.getlength(test) <= max_w:
            cur = test
        else:
            if cur:
                lines.append(cur)
            cur = word
    if cur:
        lines.append(cur)
    return lines


class Slide:
    """Satu kartu. Semua blok dikumpulkan dulu, baru digambar terpusat vertikal."""

    def __init__(self, num, total=5, tag="#edukasikesehatan", bg=BIRU_TUA,
                 brand="STUDIOSOFT", footer_left="www.studiosoft.id",
                 fg=WHITE, dot_color=None):
        self.bg = bg
        self.fg = fg
        self.dot_color = dot_color or fg
        self.num, self.total, self.tag = str(num), str(total), tag
        self.brand, self.footer_left = brand, footer_left
        self.items = []

    # ── blok-blok ────────────────────────────────────────────────────────
    def title(self, parts, size=TITLE, after=48, color=None):
        """parts: str atau list; tiap elemen jadi satu baris (boleh (teks, warna))."""
        if isinstance(parts, str):
            parts = [parts]
        self.items.append({"kind": "lines", "parts": parts, "size": size,
                           "default_color": color or self.fg, "after": after})
        return self

    def h2(self, parts, size=H2, color=SKY, after=24):
        if isinstance(parts, str):
            parts = [parts]
        self.items.append({"kind": "lines", "parts": parts, "size": size,
                           "default_color": color, "after": after})
        return self

    def para(self, text, size=SUB, color=None, after=0, bold=False):
        self.items.append({"kind": "para", "text": text, "size": size,
                           "color": color or self.fg, "bold": bold, "after": after})
        return self

    def lead(self, text, size=LEAD, color=None, after=0):
        return self.para(text, size=size, color=color, after=after, bold=True)

    def bullets(self, pairs, after=0, gap=26, name_size=NAME, desc_size=DESC):
        """pairs: [(nama, keterangan)] atau [nama] — keterangan boleh ""."""
        norm = [(p, "") if isinstance(p, str) else (p[0], p[1]) for p in pairs]
        self.items.append({"kind": "bullets", "pairs": norm, "name_size": name_size,
                           "desc_size": desc_size, "gap": gap, "after": after})
        return self

    def gap(self, px):
        self.items.append({"kind": "gap", "px": px})
        return self

    # ── ukur + gambar ────────────────────────────────────────────────────
    def _build(self, air=1.0, shrink=1.0):
        out = []
        for it in self.items:
            k = it["kind"]
            if k == "gap":
                out.append({"kind": "gap", "h": int(it["px"] * air), "after": 0})
                continue
            after = int(it.get("after", 0) * air)
            if k == "lines":
                raw = [(p[0], p[1]) if isinstance(p, tuple) else (p, it["default_color"])
                       for p in it["parts"]]
                size = max(12, int(it["size"] * shrink))
                while size > 24:                     # auto-fit lebar
                    f = F("Poppins-Bold.ttf", size)
                    if all(f.getlength(t) <= W - 2 * M for t, _ in raw):
                        break
                    size -= 2
                f = F("Poppins-Bold.ttf", size)
                step = size + 8
                out.append({"kind": "lines", "lines": raw, "font": f,
                            "step": step, "after": after, "h": step * len(raw)})
            elif k == "para":
                size = max(12, int(it["size"] * shrink))
                f = F("Poppins-SemiBold.ttf" if it.get("bold") else "Poppins-Regular.ttf", size)
                lines = wrap(it["text"], f, W - 2 * M)
                step = size + 12
                out.append({"kind": "para", "lines": lines, "font": f, "color": it["color"],
                            "step": step, "after": after, "h": step * len(lines)})
            elif k == "bullets":
                nsz = max(12, int(it["name_size"] * shrink))
                dsz = max(12, int(it["desc_size"] * shrink))
                fn, fd = F("Poppins-SemiBold.ttf", nsz), F("Poppins-Regular.ttf", dsz)
                ns, ds = nsz + 8, dsz + 12
                g = int(it["gap"] * air)
                avail = W - M - BULLET_INDENT - M
                blocks = []
                for name, desc in it["pairs"]:
                    nl = wrap(name, fn, avail)
                    dl = wrap(desc, fd, avail) if desc else []
                    blocks.append((nl, dl, ns * len(nl) + ds * len(dl)))
                h = sum(b[2] for b in blocks) + g * max(0, len(blocks) - 1)
                out.append({"kind": "bullets", "blocks": blocks, "fn": fn, "fd": fd,
                            "ns": ns, "ds": ds, "gap": g, "after": after, "h": h})
        return out

    def _fit(self):
        area = BOT_LIM - TOP_LIM
        best = None
        for shrink in (1.0, 0.94, 0.88, 0.82, 0.76):
            for air in (1.0, 0.85, 0.72, 0.60, 0.50):
                items = self._build(air=air, shrink=shrink)
                total = sum(it["h"] for it in items) + sum(it["after"] for it in items)
                if total <= area:
                    best = (items, total, air, shrink)
                    break
            if best:
                break
        if best is None:
            items = self._build(air=0.5, shrink=0.76)
            total = sum(it["h"] for it in items) + sum(it["after"] for it in items)
            best = (items, total, 0.5, 0.76)
        return best

    def render(self, filename=None, out_dir=".", quiet=True):
        items, block_h, air, shrink = self._fit()
        img = Image.new("RGB", (W, H), self.bg)
        d = ImageDraw.Draw(img)

        y = int((TOP_LIM + BOT_LIM - block_h) / 2)
        y = max(TOP_LIM, min(y, BOT_LIM - block_h))

        for it in items:
            if it["kind"] == "gap":
                y += it["h"]
                continue
            if it["kind"] == "lines":
                for txt, col in it["lines"]:
                    d.text((M, y), txt, font=it["font"], fill=col)
                    y += it["step"]
            elif it["kind"] == "para":
                for ln in it["lines"]:
                    d.text((M, y), ln, font=it["font"], fill=it["color"])
                    y += it["step"]
            elif it["kind"] == "bullets":
                for i, (nl, dl, hh) in enumerate(it["blocks"]):
                    cy = y + it["ns"] // 2 - 2
                    d.ellipse([M + 8 - DOT_R, cy - DOT_R, M + 8 + DOT_R, cy + DOT_R],
                              fill=self.dot_color)
                    yy = y
                    for ln in nl:
                        d.text((M + BULLET_INDENT, yy), ln, font=it["fn"], fill=self.fg)
                        yy += it["ns"]
                    for ln in dl:
                        d.text((M + BULLET_INDENT, yy), ln, font=it["fd"], fill=self.fg)
                        yy += it["ds"]
                    y = y + hh + (it["gap"] if i < len(it["blocks"]) - 1 else 0)
            y += it["after"]

        d.text((M, 34), self.brand, font=F("Poppins-Bold.ttf", 30), fill=self.fg)
        d.text((W - M, 38), "%s/%s" % (self.num, self.total),
               font=F("Poppins-Regular.ttf", 24), fill=self.fg, anchor="rt")
        d.text((M, H - 58), self.footer_left, font=F("Poppins-Regular.ttf", 20), fill=self.fg)
        if self.tag:
            d.text((W - M, H - 58), self.tag, font=F("Poppins-Regular.ttf", 20),
                   fill=self.fg, anchor="rt")

        if filename:
            os.makedirs(out_dir, exist_ok=True)
            path = os.path.join(out_dir, filename)
            img.save(path, "PNG")
            if not quiet:
                print("   air=%.2f shrink=%.2f tinggi=%dpx sisa=%dpx"
                      % (air, shrink, block_h, BOT_LIM - TOP_LIM - block_h))
            return path
        return img


# ── POLA WARNA HARIAN (perintah Bos 2 Okt 2026 12:2x WIB; ritual reels DIPERBAIKI 3 Okt 2026) ──
#   "Atur polanya. Begitupun reels. Hari ini putih n biru. Besoknya biru n putih. Begitu terus."
# Patokan: tanggal GENAP → PUTIH dulu lalu biru; tanggal GANJIL → kebalikannya.
# 2 Okt (genap) = putih→biru, 3 Okt (ganjil) = biru→putih, 4 Okt = putih→biru, dst.
# Dipakai untuk SELURUH post feed IG SS dalam satu urutan waktu:
#   urutan 1 = carousel feed 08:30 · urutan 2 = reel 11:00 · urutan 3 = reel 19:00.
# Reels BUKAN pasangan baru per hari — lanjutkan urutan warna carousel hari itu
# (2-3 Okt sempat keliru: reel 11:00 dihitung urutan 1 → dua BIRU berurutan di feed).
# Jangan dihitung per-urutan-jadwal, hitung dari TANGGAL.

def mulai_putih(tgl) -> bool:
    """True kalau tanggal genap (warna pertama hari itu = putih)."""
    return tgl.day % 2 == 0


def warna_ke(tgl, urutan: int) -> str:
    """Warna untuk posisi `urutan` (1 = pertama) pada tanggal `tgl`."""
    a = "putih" if mulai_putih(tgl) else "biru"
    b = "biru" if a == "putih" else "putih"
    return a if urutan % 2 == 1 else b


def slide_pola(num, total, tag, tgl, **kw):
    """Slide ke-`num` (1-based) dengan BG ikut pola harian + warna aksen yang cocok.

    Contoh: for i in 1..10: s, aksen = slide_pola(i, 10, "#seo", tanggal)
    """
    if warna_ke(tgl, num) == "putih":
        return Slide(num, total, tag, bg=WHITE, fg=BIRU_TUA, dot_color=BIRU_TUA, **kw), AKSEN_PUTIH
    return Slide(num, total, tag, bg=BIRU_TUA, fg=WHITE, **kw), SKY
