"""texto.py — desenha os textos do editor de vídeo em PNG transparente.

    python3 texto.py estados job.json   → um png por estado (lib/video/texto.js)
    python3 texto.py grade job.json     → a folha do inspect: quadros com grade
    python3 texto.py conferir           → {"ok": true} se o Pillow está aqui

── POR QUE PNG E NÃO drawtext ─────────────────────────────────────────────
O ffmpeg do Homebrew saiu sem drawtext e sem libass (ffmpeg 9, set/2026).
Desenhar aqui funciona igual em qualquer ffmpeg, e a animação vem pronta do
JavaScript: este arquivo só desenha o que recebe, não decide nada.

Responde sempre UMA linha de JSON no stdout.
"""
from __future__ import annotations

import json
import os
import sys

try:
    from PIL import Image, ImageDraw, ImageFont
except Exception:  # sem Pillow: o node avisa com o comando de instalar
    Image = None

FONTES = {
    "bold": [
        "/System/Library/Fonts/Supplemental/Arial Bold.ttf", "/Library/Fonts/Arial Bold.ttf",
        "C:/Windows/Fonts/arialbd.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
        "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
    ],
    "black": [
        "/System/Library/Fonts/Supplemental/Arial Black.ttf", "C:/Windows/Fonts/ariblk.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
    ],
    "regular": [
        "/System/Library/Fonts/Supplemental/Arial.ttf", "/Library/Fonts/Arial.ttf", "C:/Windows/Fonts/arial.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/TTF/DejaVuSans.ttf",
    ],
}

_cache = {}


def fonte(nome, tamanho):
    tamanho = max(4, int(round(tamanho)))
    chave = (nome, tamanho)
    if chave in _cache:
        return _cache[chave]
    caminhos = [nome] if nome and os.path.isfile(str(nome)) else FONTES.get(nome or "bold", FONTES["bold"]) + FONTES["bold"]
    f = None
    for c in caminhos:
        if os.path.isfile(c):
            try:
                f = ImageFont.truetype(c, tamanho)
                break
            except Exception:
                pass
    if f is None:
        try:
            f = ImageFont.load_default(size=tamanho)
        except TypeError:
            f = ImageFont.load_default()
    _cache[chave] = f
    return f


def cor(hexa, alpha=1.0):
    h = str(hexa or "#FFFFFF").lstrip("#")
    if len(h) == 3:
        h = "".join(ch * 2 for ch in h)
    a = int(h[6:8], 16) / 255 if len(h) == 8 else 1.0
    return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), int(255 * max(0.0, min(1.0, a * alpha))))


def largura(f, t):
    return f.getlength(t) if hasattr(f, "getlength") else f.getsize(t)[0]


def desenhar_item(img, it):
    escala = float(it.get("scale", 1))
    alpha = float(it.get("alpha", 1))
    tam = it["size"] * escala
    f = fonte(it.get("font"), tam)
    espaco = largura(f, " ")
    maxw = max(10, it["w"] * escala)
    # Quebra de linha pela largura, com a palavra no tamanho BASE: a palavra
    # em destaque cresce sem empurrar as outras (senão a legenda treme).
    linhas, atual, w_atual = [], [], 0.0
    for w in it["words"]:
        lw = largura(f, w["t"])
        if atual and w_atual + espaco + lw > maxw * 0.92:
            linhas.append((atual, w_atual))
            atual, w_atual = [], 0.0
        w_atual = lw if not atual else w_atual + espaco + lw
        atual.append((w, lw))
    if atual:
        linhas.append((atual, w_atual))
    altura_linha = tam * 1.18
    total_h = altura_linha * len(linhas)
    cx, cy = it["cx"], it["cy"]
    camada = Image.new("RGBA", img.size, (0, 0, 0, 0))
    d = ImageDraw.Draw(camada)
    if it.get("bg"):
        pad = tam * 0.35
        larg = max(l for _, l in linhas)
        d.rounded_rectangle([cx - larg / 2 - pad, cy - total_h / 2 - pad * 0.6, cx + larg / 2 + pad, cy + total_h / 2 + pad * 0.6],
                            radius=tam * 0.3, fill=cor(it["bg"], alpha))
    sw = int(round(it.get("sw", 0) * escala))
    for n, (palavras, _) in enumerate(linhas):
        y = cy - total_h / 2 + altura_linha * (n + 0.5)
        # A linha é quebrada no tamanho base, mas posicionada com a largura
        # REAL de cada palavra: a palavra em destaque abre espaço para si.
        medidas = []
        for w, lw in palavras:
            s = float(w.get("s", 1))
            fw = fonte(it.get("font"), tam * s) if s != 1 else f
            medidas.append((w, largura(fw, w["t"]) if s != 1 else lw, fw, s))
        larg = sum(m[1] for m in medidas) + espaco * (len(medidas) - 1)
        alinhar = it.get("align", "center")
        x = cx - larg / 2 if alinhar == "center" else (cx - maxw / 2 if alinhar == "left" else cx + maxw / 2 - larg)
        for w, lw, fw, s in medidas:
            a = alpha * float(w.get("a", 1))
            texto = w.get("show", w["t"])
            if a > 0.01 and texto:
                meio = x + lw / 2
                # "show" (typewriter) escreve a partir da esquerda da palavra.
                if "show" in w:
                    d.text((x, y), texto, font=fw, anchor="lm", fill=cor(w.get("c", it["color"]), a),
                           stroke_width=int(sw * s), stroke_fill=cor(it.get("stroke") or "#000000", a))
                else:
                    d.text((meio, y), texto, font=fw, anchor="mm", fill=cor(w.get("c", it["color"]), a),
                           stroke_width=int(sw * s) if it.get("stroke") else 0, stroke_fill=cor(it.get("stroke") or "#000000", a))
            x += lw + espaco
    if it.get("rot"):
        camada = camada.rotate(-float(it["rot"]), center=(cx, cy), resample=Image.BICUBIC)
    img.alpha_composite(camada)


def estados(job):
    W, H = int(job["width"]), int(job["height"])
    feitos = 0
    for e in job["estados"]:
        img = Image.new("RGBA", (W, H), (0, 0, 0, 0))
        for it in e["items"]:
            desenhar_item(img, it)
        img.save(e["file"], optimize=False, compress_level=3)
        feitos += 1
    vazio = job.get("vazio")
    if vazio and not os.path.exists(vazio):
        Image.new("RGBA", (W, H), (0, 0, 0, 0)).save(vazio)
    return {"ok": True, "feitos": feitos}


def grade(job):
    """Cada quadro com a grade 0–1 e o número do quadro; todos numa folha."""
    quadros = []
    for q in job["frames"]:
        im = Image.open(q["png"]).convert("RGBA")
        W, H = im.size
        cam = Image.new("RGBA", im.size, (0, 0, 0, 0))
        d = ImageDraw.Draw(cam)
        f = fonte("bold", max(12, H // 60))
        for k in range(1, 10):
            forte = k == 5
            c = (255, 255, 255, 150 if forte else 80)
            d.line([(W * k / 10, 0), (W * k / 10, H)], fill=c, width=3 if forte else 1)
            d.line([(0, H * k / 10), (W, H * k / 10)], fill=c, width=3 if forte else 1)
            rot = "%.1f" % (k / 10)
            d.text((W * k / 10 + 4, 4), rot, font=f, fill=(255, 255, 0, 230), stroke_width=2, stroke_fill=(0, 0, 0, 230))
            d.text((4, H * k / 10 + 2), rot, font=f, fill=(0, 255, 255, 230), stroke_width=2, stroke_fill=(0, 0, 0, 230))
        rotulo = "frame %d  %.2fs" % (q["frame"], q["tempo"])
        fg = fonte("bold", max(16, H // 34))
        d.rectangle([0, H - H // 20, largura(fg, rotulo) + 24, H], fill=(0, 0, 0, 190))
        d.text((12, H - H // 40), rotulo, font=fg, anchor="lm", fill=(255, 255, 255, 255))
        im.alpha_composite(cam)
        quadros.append(im.convert("RGB"))
    alvo = int(job.get("altura", 640))
    reduzidos = [q.resize((max(1, int(q.width * alvo / q.height)), alvo)) for q in quadros]
    cols = min(len(reduzidos), int(job.get("cols", 4)))
    linhas = (len(reduzidos) + cols - 1) // cols
    cw = max(r.width for r in reduzidos)
    folha = Image.new("RGB", (cw * cols + 8 * (cols - 1), alvo * linhas + 8 * (linhas - 1)), (40, 40, 40))
    for i, r in enumerate(reduzidos):
        folha.paste(r, ((i % cols) * (cw + 8), (i // cols) * (alvo + 8)))
    folha.save(job["out"])
    return {"ok": True, "out": job["out"], "size": [folha.width, folha.height]}


def main():
    acao = sys.argv[1] if len(sys.argv) > 1 else "conferir"
    if Image is None:
        print(json.dumps({"ok": False, "error": "o Pillow não está instalado", "comoResolver": "python3 -m pip install pillow"}))
        return 1
    if acao == "conferir":
        print(json.dumps({"ok": True}))
        return 0
    with open(sys.argv[2], encoding="utf-8") as fh:
        job = json.load(fh)
    try:
        r = estados(job) if acao == "estados" else grade(job) if acao == "grade" else {"ok": False, "error": "ação " + acao}
    except Exception as e:
        r = {"ok": False, "error": "%s: %s" % (type(e).__name__, e)}
    print(json.dumps(r))
    return 0 if r.get("ok") else 1


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