"""Render synthetic detection silhouettes for vendor text marks.

Committed assets must be font-rendered and contain no source-image pixels. Local
evaluation inputs may be used only to learn glyphs, weight, layout, and detector
thresholds. Candidate assets stay outside the installed package until calibrated.

Regenerate with:
    uv run python scripts/render_vendor_silhouettes.py
"""

from __future__ import annotations

import sys
from pathlib import Path
from typing import Any

import numpy as np
from PIL import Image, ImageDraw, ImageFont

_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(_ROOT / "src"))

from remove_ai_watermarks.watermark_registry import mark_keys  # noqa: E402

_PACKAGE_ASSETS = _ROOT / "src" / "remove_ai_watermarks" / "assets"
_CANDIDATE_ASSETS = _ROOT / "scripts" / "assets" / "visible-mark-candidates"
# STHeiti Medium approximates the semibold CJK sans these marks are set in; the exact
# family is unpublished for every vendor (GB 45438-2025 only requires a legible face).
_FONT = "/System/Library/Fonts/STHeiti Medium.ttc"

MARKS = {
    "qwen_alpha.png": "千问AI生成",
    "xinghui_alpha.png": "星绘AI生成",
    # Yuanbao's stamp is a TWO-LINE block (元宝 over AI生成), left-aligned, tightly
    # stacked and ITALIC-SLANTED. A rare one-line variant exists, but the stacked block
    # is dominant.
    "yuanbao_alpha.png": "元宝\nAI生成",
    # Kling (可灵) stamps a thin light-gray one-line "可灵AI 3.0" bottom-right (an
    # "Omni" suffix variant and a latin "KlingAI 3.0" variant also exist; the CJK
    # run without the suffix is the common core). The leading spiral logo is NOT
    # rendered (logos vary; the text run discriminates).
    "kling_alpha.png": "可灵AI 3.0",
    # The "cat-logo" candidate stamps an outline cat-head plus bold "AI生成",
    # bottom-right. It remains unregistered pending sufficient calibration coverage.
    "catlogo_alpha.png": "CATLOGO",  # sentinel: drawn by draw_catlogo(), not font-rendered
    # RunningHub top-left text mark.
    "runninghub_alpha.png": "RunningHub AI生成",
    # LibLibAI bottom-center wordmark.
    "liblib_alpha.png": "LibLibAI",
    # Zhipu Qingyan candidate text mark.
    "qingyan_alpha.png": "清言·AI生成",
    # MiniMax / Hailuo candidate wordmark.
    "hailuo_alpha.png": "Hailuo AI",
    # Baidu bottom-right text run.
    "baidu_alpha.png": "百度",
}
_REGISTERED = {f"{key}_alpha.png" for key in mark_keys()} & MARKS.keys()

# Per-mark post-processing for the multi-line / slanted stamps (see render()).
MARK_OPTS: dict[str, dict[str, Any]] = {
    # Hiragino Sans GB W6, tight leading, dilation, and negative shear match the
    # standard Yuanbao stamp without clipping the lower line.
    "yuanbao_alpha.png": {
        "gap_frac": 0.05,
        "dilate": 2,
        "shear": -0.60,
        "font": "/System/Library/Fonts/Hiragino Sans GB.ttc",
        "font_index": 2,
    },
    # Qingyan uses a heavier weight than STHeiti Medium.
    "qingyan_alpha.png": {"font": "/System/Library/Fonts/Hiragino Sans GB.ttc", "font_index": 2},
    # LibLibAI uses an Arial-class grotesque.
    "liblib_alpha.png": {"font": "/System/Library/Fonts/Supplemental/Arial.ttf"},
}


def render(text: str, width: int = 335, opts: dict[str, Any] | None = None) -> np.ndarray:
    """Binary glyph silhouette (255 = glyph), sized to the doubao asset's convention.

    Matching doubao's 335px asset width keeps the `alpha_*_frac` numbers transferable,
    since these marks are the same house style and scale. A "\n" in ``text`` renders a
    multi-line block: lines drawn left-aligned at one shared font size with a tight
    gap, then optional stroke dilation and an italic shear (see MARK_OPTS).
    """
    opts = opts or {}
    gap_frac = float(opts.get("gap_frac", 0.15))
    dilate = int(opts.get("dilate", 0))
    shear_k = float(opts.get("shear", 0.0))
    font_path = str(opts.get("font", _FONT))
    font_index = int(opts.get("font_index", 0))
    probe = Image.new("L", (10, 10))
    d0 = ImageDraw.Draw(probe)
    lines = text.split("\n")
    size = 8
    while size < 200:  # grow until the LONGEST line fills the target width
        f = ImageFont.truetype(font_path, size, index=font_index)
        if max(d0.textbbox((0, 0), ln, font=f)[2] for ln in lines) >= width * 0.98:
            break
        size += 1
    font = ImageFont.truetype(font_path, size, index=font_index)
    boxes = [d0.textbbox((0, 0), ln, font=font) for ln in lines]
    line_h = max(bb[3] - bb[1] for bb in boxes)
    gap = max(1, int(line_h * gap_frac))
    w = max(bb[2] - bb[0] for bb in boxes)
    h = line_h * len(lines) + gap * (len(lines) - 1)
    pad = max(2, int(line_h * 0.12))
    im = Image.new("L", (w + 2 * pad, h + 2 * pad), 0)
    draw = ImageDraw.Draw(im)
    y = pad
    for ln, bb in zip(lines, boxes, strict=True):
        draw.text((pad - bb[0], y - bb[1]), ln, font=font, fill=255)
        y += line_h + gap
    sil = np.array(im)
    if dilate or shear_k:
        import cv2

        if dilate:
            sil = cv2.dilate(sil, np.ones((dilate, dilate), np.uint8))
        if shear_k:
            hh, ww = sil.shape
            extra = int(abs(shear_k) * hh)
            offset = extra if shear_k < 0 else 0
            sil = cv2.warpAffine(
                sil,
                np.float32([[1, shear_k, offset], [0, 1, 0]]),
                (ww + extra, hh),
            )
            ys, xs = np.where(sil > 0)
            if xs.size:
                sil = sil[ys.min() : ys.max() + 1, xs.min() : xs.max() + 1]
    return sil


def draw_catlogo(width: int = 335) -> np.ndarray:
    """The cat-logo mark: an outline cat-head (integrated pointy ears, two dot eyes)
    + a bold "AI生成" run, drawn synthetically from the calibrated layout. The outline
    form is the parked candidate described in MARKS."""
    probe = Image.new("L", (10, 10))
    d0 = ImageDraw.Draw(probe)
    text = "AI生成"
    size = 8
    while size < 200:
        f = ImageFont.truetype(_FONT, size)
        if d0.textbbox((0, 0), text, font=f)[2] >= width * 0.60:
            break
        size += 1
    font = ImageFont.truetype(_FONT, size)
    bb = d0.textbbox((0, 0), text, font=font)
    tw, th = bb[2] - bb[0], bb[3] - bb[1]
    cs = int(th * 1.08)
    stroke = max(2, int(th * 0.09))
    gap = int(th * 0.35)

    def head(s: int) -> Image.Image:
        im = Image.new("L", (s, s), 0)
        d = ImageDraw.Draw(im)
        f = float(s)
        pts = [
            (0.12 * f, 0.95 * f),
            (0.10 * f, 0.45 * f),
            (0.12 * f, 0.30 * f),
            (0.20 * f, 0.05 * f),  # left ear tip
            (0.40 * f, 0.24 * f),  # left ear valley
            (0.60 * f, 0.24 * f),  # right ear valley
            (0.80 * f, 0.05 * f),  # right ear tip
            (0.88 * f, 0.30 * f),
            (0.90 * f, 0.45 * f),
            (0.88 * f, 0.95 * f),
        ]
        d.line([*pts, pts[0]], fill=255, width=stroke, joint="curve")
        r = max(1.5, stroke * 0.7)
        d.ellipse([0.35 * f - r, 0.60 * f - r, 0.35 * f + r, 0.60 * f + r], fill=255)
        d.ellipse([0.65 * f - r, 0.60 * f - r, 0.65 * f + r, 0.60 * f + r], fill=255)
        return im

    w = cs + gap + tw
    h = max(th, cs)
    pad = max(2, int(h * 0.12))
    im = Image.new("L", (w + 2 * pad, h + 2 * pad), 0)
    im.paste(head(cs), (pad, pad + (h - cs) // 2))
    ImageDraw.Draw(im).text((pad + cs + gap - bb[0], pad + (h - th) // 2 - bb[1]), text, font=font, fill=255)
    return np.array(im)


def main() -> None:
    try:
        for name, text in MARKS.items():
            sil = draw_catlogo() if text == "CATLOGO" else render(text, opts=MARK_OPTS.get(name))
            output_dir = _PACKAGE_ASSETS if name in _REGISTERED else _CANDIDATE_ASSETS
            output_dir.mkdir(parents=True, exist_ok=True)
            output = output_dir / name
            Image.fromarray(sil).save(output)
            print(f"wrote {output}  ({sil.shape[1]}x{sil.shape[0]})  text={text!r}")
    except OSError as e:
        print(f"Font not found ({e}); install a CJK font or edit _FONT.", file=sys.stderr)
        raise SystemExit(1) from e


if __name__ == "__main__":
    main()
