﻿#!/usr/bin/env python3
"""
seamless_overlay.py — Seamlessly overlay Hungarian text on High Orbit corp cards.

Improvements over apply_corp_translations.py:
  1. Background color SAMPLED from card pixels (not hardcoded gray)
  2. Corp name area INPAINTED via OpenCV — original colored text removed cleanly
  3. Corp name uses MULTI-COLOR rendering: per-line fill + stroke color
  4. Font size AUTO-FITTED to name region height

Usage:
    python seamless_overlay.py                         # all 5 corps
    python seamless_overlay.py --card "Lagrange Collective"
    python seamless_overlay.py --card "Lagrange Collective" --test
"""

import os
import sys
import argparse
import numpy as np
from PIL import Image, ImageDraw, ImageFont

try:
    import cv2
    HAS_CV2 = True
except ImportError:
    HAS_CV2 = False
    print("Warning: opencv-python not found. Falling back to solid fill for name region.")

# Import gradient fill / body-text removal (separate module for clean context)
_gf_dir = os.path.join(os.path.dirname(__file__))
import importlib.util as _ilu
_gf_spec = _ilu.spec_from_file_location("gradient_fill",
              os.path.join(_gf_dir, "gradient_fill.py"))
_gf_mod = _ilu.module_from_spec(_gf_spec)
_gf_spec.loader.exec_module(_gf_mod)
inpaint_body_text       = _gf_mod.inpaint_body_text
inpaint_illustration_text = _gf_mod.inpaint_illustration_text
remove_all_body_text    = _gf_mod.remove_all_body_text

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
BASE_DIR   = r"C:\progik\forditsunk"
SOURCE_DIR = os.path.join(BASE_DIR, r"source\High_Orbit\High Orbit\High_Orbit_Professional_Print_Cards_3_1")
OUTPUT_DIR = os.path.join(BASE_DIR, "output", "corps")
FONTS_DIR  = r"C:\Windows\Fonts"

FONT_NAME   = os.path.join(FONTS_DIR, "arialbd.ttf")   # Arial Bold — heavy, clear
FONT_BODY   = os.path.join(FONTS_DIR, "calibri.ttf")
FONT_FLAVOR = os.path.join(FONTS_DIR, "georgiaz.ttf")  # Georgia Bold Italic

SZ_BODY   = 19
SZ_FLAVOR = 19

# CORPORATION banner position in landscape space (auto-detected below, this is a fallback)
CORP_BANNER = (395, 68, 720, 102)

# Reference image crop for the CÉGBIRODALOM badge
# (from source/ref/hu_corp_reference.jpg; card at x=21-441)
REF_IMG       = os.path.join(BASE_DIR, "source", "ref", "hu_corp_reference.jpg")
REF_BADGE     = (137, 27, 297, 44)   # (x1, y1, x2, y2) — y1=27 skips white photo background rows 12-26

# ---------------------------------------------------------------------------
# Corp data: coordinates in ROTATED LANDSCAPE space (1122 × 822 px)
# Corp names are NOT translated — they stay in English on the printed card.
# name_region is kept for reference (CÉGBIRODALOM badge detection), but the
# name text itself is left untouched in both inpaint-only and full-HU modes.
# ---------------------------------------------------------------------------

CORPS = {

"Lagrange Collective": {
    "file": "HO 077 Lagrange Collective.png",
    "name_region":      (278, 55, 942, 325),
    # Badge: measured from EN card using column-density gold detection (>10 gold rows/col)
    "badge_region":     (422, 74, 685, 104),
    "start_text":       ["(46 M€-vel kezdesz. Első akciódként ingyen kijátszhatsz 1 Pályamenti Főhadiszállást.)"],
    # x1=25 skip left card border; x2=336 covers full text width (text runs to ~x=334,
    # drops to <3% density by x=336); effect-box frame starts at ~x=338+.
    # y1=490 skip 46 MC badge; y2=608 stops before card contour at y=608-624
    "start_region":     (25, 490, 336, 608),
    "start_bg":         "body",   # gray gradient below the 46 badge, not illustration
    "effect":           ["(Hatás: Valahányszor Infrastruktúra jelzésű lapot játszol ki – beleértve ezt is –, M€ termelésed 1 szinttel nő.)"],
    "effect_region":    (340, 498, 940, 604),   # x margins skip the effect-box inner frame gradient
    "effect_label":     "HATÁS",
    "effect_label_region": (324, 330, 856, 372),  # blue EFFECT band in landscape
    "action":           None,
    "action_region":    None,
    "action_label":     None,
    "action_label_region": None,
    "flavor":           "A hatékonyan felépített rendszerek titka, hogy tudni kell, hová kerülnek az alkatrészek.",
    "flavor_region":    (25, 697, 942, 742),    # x1=25 to skip left card border
},

"Planetary Engineering Corps": {
    "file": "HO 078 Planetary Engineering Corps.png",
    "name_region":   (278, 55, 942, 325),
    "start_text":    ["(27 M€-vel, 10 Energiával és 2 M€ termeléssel kezdesz.)"],
    "start_region":  (15, 372, 345, 695),
    "effect":        ["(Hatás: Fizethetsz 5 Energiával és eldobhatsz 1 lapot a szokásos projektekre — ebben az esetben 15 M€-vel kevesebbet kell fizetned.)"],
    "effect_region": (300, 448, 960, 615),
    "action":        None,
    "action_region": None,
    "flavor":        "A hegyeket csak erő tudja megmozgatni.",
    "flavor_region": (15, 697, 942, 742),
},

"Trojan Space Logistics": {
    "file": "HO 079 Trojan Space Logistics.png",
    "name_region":   (278, 55, 942, 262),
    "start_text":    ["(35 M€-vel kezdesz.)"],
    "start_region":  (15, 372, 303, 510),
    "effect":        ["(Hatás: Valahányszor bármely játékos kék kártyát játszik ki, adj 1 Érc erőforrást BÁRMELY kártyára.)"],
    "effect_region": (300, 318, 960, 445),
    "action":        ["(Akció: Használj el 2 Érc erőforrást, hogy újra aktiválj egy Ezüst kártya akciót, amelyet ebben a generációban már használtál.)"],
    "action_region": (300, 452, 960, 660),
    "flavor":        "A megfelelő feladathoz a megfelelő eszköz kell.",
    "flavor_region": (15, 697, 942, 742),
},

"Spacetek": {
    "file": "HO 080 Spacetek.png",
    "name_region":   (278, 55, 942, 230),
    "start_text":    ["(39 M€-vel és 1 Titánium termeléssel kezdesz.)"],
    "start_region":  (15, 372, 303, 695),
    "effect":        ["(Hatás: Kapsz 1 Titániumot minden egyes lépésért, amellyel BÁRMELY játékos Titánium termelése nő.)"],
    "effect_region": (300, 318, 960, 445),
    "action":        ["(Akció: Költs el 13 M€-t és növeld 1 szinttel a Titánium termelésed. Bármely játékos végrehajthatja ezt az akciót.)"],
    "action_region": (300, 452, 960, 660),
    "flavor":        "Megvan bennünk a kellő anyag a feladathoz.",
    "flavor_region": (15, 697, 942, 742),
},

"Judges of Change": {
    "file": "HO 081 Judges of Change.png",
    "name_region":   (278, 55, 942, 262),
    "start_text":    ["(33 M€-vel és 22-es Terraformálási szinttel kezdesz.)"],
    "start_region":  (15, 560, 303, 700),
    "effect":        ["(Hatás: Minden páros Terraformálási szint elérésekor helyezz 1 Tudomány erőforrást ide. 1 GP minden Tudomány erőforrás után, ami itt van.)"],
    "effect_region": (300, 318, 960, 445),
    "action":        ["(Akció: Távolíts el 1 Tudomány erőforrást erről a kártyáról, és húzz annyi lapot, ahány játékos van. Tarts meg egyet, a többit dobd el.)"],
    "action_region": (300, 452, 960, 660),
    "flavor":        "Célunkhoz sok út vezet — a megfelelő kiválasztása megfontolást igényel.",
    "flavor_region": (15, 697, 1095, 742),
},

}


# ---------------------------------------------------------------------------
# Color sampling helpers
# ---------------------------------------------------------------------------

def sample_bg(arr, x1, y1, x2, y2, bright=185):
    """Median of the brightest pixels in a region — the card background color."""
    r = arr[y1:y2, x1:x2].astype(float)
    lum = r.mean(axis=2)
    mask = lum > bright
    if mask.sum() < 20:
        thr = np.percentile(lum.flatten(), 60)
        mask = lum > thr
    s = r[mask]
    return tuple(int(np.median(s[:, c])) for c in range(3))


def sample_fg(arr, x1, y1, x2, y2, dark=80):
    """Median of the darkest pixels — the text color."""
    r = arr[y1:y2, x1:x2].astype(float)
    lum = r.mean(axis=2)
    mask = lum < dark
    if mask.sum() < 5:
        thr = np.percentile(lum.flatten(), 15)
        mask = lum < max(thr, 20)
    if mask.sum() == 0:
        return (30, 30, 30)
    s = r[mask]
    return tuple(int(np.median(s[:, c])) for c in range(3))


# ---------------------------------------------------------------------------
# OpenCV inpainting: remove yellow/red text from name region
# ---------------------------------------------------------------------------

def inpaint_name(arr_rgb, x1, y1, x2, y2, logo_x=278):
    """
    Erase yellow+red corp-name text from the region using TELEA inpainting.
    Pixels left of logo_x are left untouched (that's the logo/illustration area).
    Returns the full modified array.
    """
    if not HAS_CV2:
        return arr_rgb   # caller should fill with sampled bg instead

    region = arr_rgb[y1:y2, x1:x2].copy().astype(np.uint8)
    R = region[:, :, 0].astype(float)
    G = region[:, :, 1].astype(float)
    B = region[:, :, 2].astype(float)

    # Yellow: R>150, G>100, B<120, R strongly dominant
    yellow = (R > 150) & (G > 100) & (B < 120) & ((R - B) > 60)
    # Red: R>130, R-G>55, R-B>55
    red = (R > 130) & ((R - G) > 55) & ((R - B) > 55)
    # Dark outline pixels between colored letters (near-black)
    dark_outline = (R < 60) & (G < 60) & (B < 60)

    mask = (yellow | red | dark_outline).astype(np.uint8) * 255

    # Exclude the logo side (left of logo boundary in this crop)
    local_logo_x = max(0, logo_x - x1)
    mask[:, :local_logo_x] = 0

    # 1px dilation to cover anti-aliased edges without spreading blur too far
    kernel = np.ones((3, 3), np.uint8)
    mask = cv2.dilate(mask, kernel, iterations=1)

    region_bgr = cv2.cvtColor(region, cv2.COLOR_RGB2BGR)
    inpainted  = cv2.inpaint(region_bgr, mask, inpaintRadius=5, flags=cv2.INPAINT_TELEA)
    inpainted_rgb = cv2.cvtColor(inpainted, cv2.COLOR_BGR2RGB)

    result = arr_rgb.copy()
    result[y1:y2, x1:x2] = inpainted_rgb
    return result


def inpaint_name_minimal(arr_rgb, x1, y1, x2, y2, logo_x=278):
    """
    Like inpaint_name but with the smallest mask and radius that still removes
    the colored text — prioritises preserving illustration texture over total
    erasure.  Leaves faint halos rather than spreading blur wide.
    """
    if not HAS_CV2:
        return arr_rgb

    region = arr_rgb[y1:y2, x1:x2].copy().astype(np.uint8)
    R = region[:, :, 0].astype(float)
    G = region[:, :, 1].astype(float)
    B = region[:, :, 2].astype(float)

    # Tighter thresholds → smaller mask → less blur spread
    yellow = (R > 160) & (G > 110) & (B < 100) & ((R - B) > 70)
    red    = (R > 140) & ((R - G) > 65) & ((R - B) > 65)
    # Only the darkest outline pixels (very tight)
    dark_outline = (R < 40) & (G < 40) & (B < 40)

    mask = (yellow | red | dark_outline).astype(np.uint8) * 255

    local_logo_x = max(0, logo_x - x1)
    mask[:, :local_logo_x] = 0

    # Single-pixel dilation — just enough to cover anti-aliased edges
    kernel = np.ones((2, 2), np.uint8)
    mask = cv2.dilate(mask, kernel, iterations=1)

    region_bgr = cv2.cvtColor(region, cv2.COLOR_RGB2BGR)
    inpainted  = cv2.inpaint(region_bgr, mask, inpaintRadius=3, flags=cv2.INPAINT_TELEA)
    inpainted_rgb = cv2.cvtColor(inpainted, cv2.COLOR_BGR2RGB)

    result = arr_rgb.copy()
    result[y1:y2, x1:x2] = inpainted_rgb
    return result


# ---------------------------------------------------------------------------
# Font helpers
# ---------------------------------------------------------------------------

def load_font(path, size):
    try:
        return ImageFont.truetype(path, size)
    except Exception:
        return ImageFont.load_default()


def fit_font_size(lines, font_path, region_w, region_h,
                  n_lines, line_gap=10, pad_x=12, pad_y=20,
                  min_sz=30, max_sz=130):
    """Binary-search for the largest font size where all lines fit the region."""
    avail_w = region_w - 2 * pad_x
    avail_h = region_h - 2 * pad_y

    lo, hi, best = min_sz, max_sz, min_sz
    while lo <= hi:
        mid = (lo + hi) // 2
        try:
            font = ImageFont.truetype(font_path, mid)
        except Exception:
            break
        # Measure a dummy draw
        tmp_img = Image.new("RGB", (region_w, region_h))
        draw = ImageDraw.Draw(tmp_img)
        widths, heights = [], []
        for text, _, _, sw in lines:
            bb = draw.textbbox((0, 0), text, font=font, stroke_width=sw)
            widths.append(bb[2] - bb[0])
            heights.append(bb[3] - bb[1])
        total_h = sum(heights) + line_gap * (n_lines - 1)
        max_w = max(widths) if widths else 0
        if total_h <= avail_h and max_w <= avail_w:
            best = mid
            lo = mid + 1
        else:
            hi = mid - 1
    return best


# ---------------------------------------------------------------------------
# Drawing helpers
# ---------------------------------------------------------------------------

def draw_name(img, region, name_lines, logo_x=278, line_gap=10, pad_x=12, pad_y=20):
    """Render multi-color corp name, centered in region (right of logo_x)."""
    x1, y1, x2, y2 = region
    # Only render right of the logo
    tx1 = max(x1, logo_x)
    region_w = x2 - tx1
    region_h = y2 - y1

    sz = fit_font_size(name_lines, FONT_NAME, region_w, region_h,
                       len(name_lines), line_gap=line_gap, pad_x=pad_x, pad_y=pad_y)
    font = load_font(FONT_NAME, sz)

    draw = ImageDraw.Draw(img)

    # Pre-measure
    tmp = ImageDraw.Draw(Image.new("RGB", (region_w, region_h)))
    line_data = []
    for text, fill, stroke, sw in name_lines:
        bb = tmp.textbbox((0, 0), text, font=font, stroke_width=sw)
        w, h = bb[2] - bb[0], bb[3] - bb[1]
        line_data.append((text, fill, stroke, sw, w, h))

    total_h = sum(d[5] for d in line_data) + line_gap * (len(line_data) - 1)
    y = y1 + (region_h - total_h) // 2

    for text, fill, stroke, sw, lw, lh in line_data:
        x = tx1 + (region_w - lw) // 2
        draw.text((x, y), text, font=font, fill=fill, stroke_width=sw, stroke_fill=stroke)
        y += lh + line_gap


def wrap_lines(text, font, max_px, draw):
    words = text.split()
    lines, cur = [], []
    for w in words:
        test = " ".join(cur + [w])
        bb = draw.textbbox((0, 0), test, font=font)
        if bb[2] <= max_px:
            cur.append(w)
        else:
            if cur:
                lines.append(" ".join(cur))
            cur = [w]
    if cur:
        lines.append(" ".join(cur))
    return lines


def draw_body(img, region, paragraphs, font, bg_color, fg_color,
              halign="center", valign="center", pad_x=10, pad_y=6, gap=3):
    """Fill region with sampled bg, render wrapped text."""
    x1, y1, x2, y2 = region
    draw = ImageDraw.Draw(img)
    draw.rectangle([x1, y1, x2, y2], fill=bg_color)

    if not paragraphs:
        return

    max_w = x2 - x1 - 2 * pad_x
    bb_ref = draw.textbbox((0, 0), "Ag", font=font)
    line_h = (bb_ref[3] - bb_ref[1]) + gap

    all_lines = []
    for i, para in enumerate(paragraphs):
        for sub in para.split("\n"):
            all_lines.extend(wrap_lines(sub, font, max_w, draw) if sub else [""])
        if i < len(paragraphs) - 1:
            all_lines.append("")

    total_h = len(all_lines) * line_h
    avail_h = y2 - y1 - 2 * pad_y
    y = y1 + pad_y + (max(0, avail_h - total_h) // 2 if valign == "center" else 0)

    for line in all_lines:
        if not line:
            y += line_h
            continue
        bb = draw.textbbox((0, 0), line, font=font)
        tw = bb[2] - bb[0]
        x = x1 + ((x2 - x1 - tw) // 2 if halign == "center" else pad_x)
        draw.text((x, y), line, font=font, fill=fg_color)
        y += line_h


# ---------------------------------------------------------------------------
# CORPORATION banner — pixel-perfect swap from reference image
# ---------------------------------------------------------------------------

def _detect_corp_badge(arr_rgb, name_x1=278, name_x2=942):
    """
    Detect CORPORATION badge in landscape corp card.

    Searches only within the name-area header strip (x=name_x1..name_x2,
    y=60..115) to avoid false gold positives from the illustration.
    The detected badge is CENTERED horizontally within the name area.
    Falls back to a centered version of CORP_BANNER if detection fails.
    """
    name_cx = (name_x1 + name_x2) // 2
    strip_y1, strip_y2 = 60, 115

    strip = arr_rgb[strip_y1:strip_y2, name_x1:name_x2, :]
    R = strip[:, :, 0].astype(float)
    G = strip[:, :, 1].astype(float)
    B = strip[:, :, 2].astype(float)
    gold = (R > 150) & (G > 100) & (B < 100)

    rows_g = np.where(gold.any(axis=1))[0]
    cols_g = np.where(gold.any(axis=0))[0]

    fb_w = CORP_BANNER[2] - CORP_BANNER[0]
    fb_y1, fb_y2 = CORP_BANNER[1], CORP_BANNER[3]

    if len(rows_g) < 2 or len(cols_g) < 20:
        # Fallback: center CORP_BANNER dimensions within name area
        return (name_cx - fb_w // 2, fb_y1,
                name_cx - fb_w // 2 + fb_w, fb_y2)

    # y-bounds with small margin
    y1 = strip_y1 + int(rows_g[0]) - 2
    y2 = strip_y1 + int(rows_g[-1]) + 3

    # x-bounds: detected width, but CENTERED in name area
    detected_w = int(cols_g[-1]) - int(cols_g[0]) + 1
    if detected_w < 50 or detected_w > 400:
        detected_w = fb_w
    x1 = name_cx - detected_w // 2
    x2 = x1 + detected_w

    return (max(0, x1), max(0, y1), x2, y2)


def _draw_spaced_text(draw, x, y, text, font, spacing, fill):
    """Draw text with extra pixel spacing between each character."""
    cx = x
    for ch in text:
        draw.text((cx, y), ch, font=font, fill=fill)
        bb = draw.textbbox((0, 0), ch, font=font)
        cx += (bb[2] - bb[0]) + spacing
    return cx - x - spacing  # total rendered width


def _spaced_text_width(draw, text, font, spacing):
    total = 0
    for i, ch in enumerate(text):
        bb = draw.textbbox((0, 0), ch, font=font)
        total += bb[2] - bb[0]
        if i < len(text) - 1:
            total += spacing
    return total


def replace_corp_badge(img, arr_rgb=None, badge_region=None):
    """
    Replace CORPORATION text with CÉGBIRODALOM.

    Strategy:
      1. Sample gold color per row from non-text pixels only.
      2. Fit a smooth linear gradient (polyfit per channel) through those samples.
      3. For each gold row, fill only the pixels WITHIN the detected badge boundary
         (left/right per-row) with the smooth gradient — border rows are untouched.
      4. Draw CÉGBIRODALOM centered with letter spacing (TODO — skipped until gradient ok).
    """
    if arr_rgb is None:
        arr_rgb = np.array(img)

    if badge_region is not None:
        x1, y1, x2, y2 = badge_region
    else:
        x1, y1, x2, y2 = _detect_corp_badge(arr_rgb)

    region = arr_rgb[y1:y2, x1:x2].copy().astype(np.uint8)
    H, W = region.shape[:2]
    br_map = region.mean(axis=2)

    # ── Step 1: per-row boundary + gold sample ──────────────────────────────
    # A pixel "belongs to the badge" if it's golden (R>160,G>130,B<130) or dark text
    row_bounds = {}   # row_idx -> (left_col, right_col)
    gold_samples = {}  # row_idx -> mean gold RGB (from non-text pixels only)

    for row_idx in range(H):
        row  = region[row_idx]
        br   = br_map[row_idx]
        R, G, B = row[:,0].astype(float), row[:,1].astype(float), row[:,2].astype(float)

        is_gold = (R > 160) & (G > 130) & (B < 130)
        is_text = br < 80
        is_badge = is_gold | is_text

        badge_cols = np.where(is_badge)[0]
        if len(badge_cols) == 0:
            continue
        row_bounds[row_idx] = (int(badge_cols[0]), int(badge_cols[-1]))

        gold_pix = row[is_gold]
        if len(gold_pix) >= 5:
            gold_samples[row_idx] = gold_pix.mean(axis=0).astype(float)

    # Only process rows that have both a boundary and a gold sample
    gold_rows = sorted(set(row_bounds) & set(gold_samples))
    if not gold_rows:
        return img

    # ── Step 2: fit smooth linear gradient per channel ──────────────────────
    y_pts  = np.array(gold_rows, dtype=float)
    colors = np.array([gold_samples[r] for r in gold_rows])  # (N, 3)

    gradient = np.zeros((H, 3), dtype=np.float32)
    for c in range(3):
        coeffs = np.polyfit(y_pts, colors[:, c], 1)
        for row_idx in gold_rows:
            gradient[row_idx, c] = np.clip(coeffs[0] * row_idx + coeffs[1], 0, 255)

    # ── Step 3: fill badge interior with gradient ────────────────────────────
    result = arr_rgb.copy()
    for row_idx in gold_rows:
        left_col, right_col = row_bounds[row_idx]
        fill = gradient[row_idx].astype(np.uint8)
        result[y1 + row_idx, x1 + left_col : x1 + right_col + 1] = fill

    return Image.fromarray(result, "RGB")


def replace_effect_label(img, label_region, label_text):
    """
    Erase the EN label ('EFFECT' / 'ACTION') from the blue gradient band and
    draw the Hungarian equivalent.  The blue background is preserved; only the
    dark text pixels are inpainted, then the HU text is drawn in the same dark
    color centered in the band.
    """
    if not HAS_CV2:
        return img
    x1, y1, x2, y2 = label_region
    band_w, band_h = x2 - x1, y2 - y1

    arr = np.array(img)
    region = arr[y1:y2, x1:x2, :].astype(np.uint8)

    # Mask: dark text pixels on the blue band (brightness < 80)
    brightness = region.mean(axis=2)
    mask = (brightness < 80).astype(np.uint8) * 255
    kernel = np.ones((3, 3), np.uint8)
    mask = cv2.dilate(mask, kernel, iterations=1)

    region_bgr = cv2.cvtColor(region, cv2.COLOR_RGB2BGR)
    inpainted = cv2.inpaint(region_bgr, mask, inpaintRadius=5, flags=cv2.INPAINT_TELEA)
    inpainted_rgb = cv2.cvtColor(inpainted, cv2.COLOR_BGR2RGB)
    arr[y1:y2, x1:x2] = inpainted_rgb
    img2 = Image.fromarray(arr, "RGB")

    # Draw HU label — fit font to band height, draw centered
    draw = ImageDraw.Draw(img2)
    font_sz = max(10, band_h - 16)
    font = load_font(FONT_NAME, font_sz)
    bb = draw.textbbox((0, 0), label_text, font=font)
    tw, th = bb[2] - bb[0], bb[3] - bb[1]
    tx = x1 + (band_w - tw) // 2
    ty = y1 + (band_h - th) // 2
    draw.text((tx, ty), label_text, font=font, fill=(15, 15, 15))
    return img2


# ---------------------------------------------------------------------------
# Inpaint-only processing (all text erased, only CÉGBIRODALOM placed)
# ---------------------------------------------------------------------------

def process_corp_inpaint_only(key, data, out_dir, review_dir=None, verbose=True):
    """
    Produce a clean-card image for user review:
      - Corp name:     LEFT AS-IS (corp names stay in English per task.txt)
      - Effect/action: text removed via gradient reconstruction
      - Start region:  text removed via illustration inpainting
      - Flavor:        text removed via gradient reconstruction
      - CÉGBIRODALOM: pixel-perfect badge replaced from reference
    """
    src = os.path.join(SOURCE_DIR, data["file"])
    if not os.path.exists(src):
        if verbose:
            print(f"  [WARN] Not found: {src}")
        return False

    pil_orig = Image.open(src).convert("RGB")
    pil_land = pil_orig.rotate(90, expand=True)
    arr = np.array(pil_land)

    # 1. Remove body text from all text boxes (NOT the name region)
    arr = remove_all_body_text(arr, data)
    if verbose:
        print(f"    body text inpainted (effect/action/flavor/start)")

    # 2. CÉGBIRODALOM badge — reconstruct gold gradient, draw HU label
    img = Image.fromarray(arr, "RGB")
    img = replace_corp_badge(img, arr_rgb=arr, badge_region=data.get("badge_region"))

    # 3. Replace EN effect/action label with HU equivalent in the blue band
    if data.get("effect_label") and data.get("effect_label_region"):
        img = replace_effect_label(img, data["effect_label_region"], data["effect_label"])
        if verbose:
            print(f"    effect label -> {data['effect_label']}")
    if data.get("action_label") and data.get("action_label_region"):
        img = replace_effect_label(img, data["action_label_region"], data["action_label"])
        if verbose:
            print(f"    action label -> {data['action_label']}")

    # 4. Save landscape review image
    if review_dir:
        os.makedirs(review_dir, exist_ok=True)
        rv_path = os.path.join(review_dir, "inpaint_" + data["file"])
        img.save(rv_path, "PNG")
        if verbose:
            print(f"    review -> {rv_path}")

    # 4. Rotate to portrait and save
    img_p = img.rotate(-90, expand=True)
    os.makedirs(out_dir, exist_ok=True)
    out_path = os.path.join(out_dir, data["file"])
    img_p.save(out_path, "PNG")
    if verbose:
        print(f"    saved -> {out_path}")
    return True


# ---------------------------------------------------------------------------
# Main processing
# ---------------------------------------------------------------------------

def process_corp(key, data, out_dir, verbose=True):
    """
    Full Hungarian overlay: remove EN body text, place HU translations.
    Corp names are left in English (not inpainted, not redrawn).
    Only the CÉGBIRODALOM badge is swapped.
    """
    src = os.path.join(SOURCE_DIR, data["file"])
    if not os.path.exists(src):
        print(f"  [WARN] Not found: {src}")
        return False

    # Load portrait → rotate to landscape (1122×822)
    pil_orig = Image.open(src).convert("RGB")
    pil_land = pil_orig.rotate(90, expand=True)
    arr = np.array(pil_land)

    # ── 1. Remove EN body text via gradient reconstruction ─────────────────
    arr = remove_all_body_text(arr, data)
    if verbose:
        print(f"    body text removed via gradient fill")

    # ── 2. Convert to PIL, swap CÉGBIRODALOM badge + label ───────────────
    img = Image.fromarray(arr, "RGB")
    img = replace_corp_badge(img, arr_rgb=arr, badge_region=data.get("badge_region"))
    if data.get("effect_label") and data.get("effect_label_region"):
        img = replace_effect_label(img, data["effect_label_region"], data["effect_label"])
    if data.get("action_label") and data.get("action_label_region"):
        img = replace_effect_label(img, data["action_label_region"], data["action_label"])

    # ── 3. Sample foreground color from original for HU text ──────────────
    arr_orig = np.array(pil_land)  # original pre-inpaint for color sampling
    fg_body  = sample_fg(arr_orig, *data["effect_region"])
    fn_body  = load_font(FONT_BODY,   SZ_BODY)
    fn_flavor = load_font(FONT_FLAVOR, SZ_FLAVOR)

    # ── 4. Render HU body text on clean background ─────────────────────────
    # For start/effect/action/flavor we sample bg from the now-clean region
    arr_clean = np.array(img)

    sr = data["start_region"]
    bg_s = sample_bg(arr_clean, *sr)
    draw_body(img, sr, data["start_text"], fn_body, bg_s, fg_body,
              halign="left", valign="top", pad_y=4)

    er = data["effect_region"]
    bg_e = sample_bg(arr_clean, *er)
    draw_body(img, er, data["effect"], fn_body, bg_e, fg_body)

    if data.get("action") and data.get("action_region"):
        ar = data["action_region"]
        bg_a = sample_bg(arr_clean, *ar)
        draw_body(img, ar, data["action"], fn_body, bg_a, fg_body)

    fr = data["flavor_region"]
    bg_f = sample_bg(arr_clean, *fr)
    draw_body(img, fr, [data["flavor"]], fn_flavor, bg_f, fg_body)

    # ── 5. Rotate back to portrait, save ──────────────────────────────────
    img = img.rotate(-90, expand=True)
    os.makedirs(out_dir, exist_ok=True)
    out_path = os.path.join(out_dir, data["file"])
    img.save(out_path, "PNG")
    if verbose:
        print(f"    saved -> {out_path}")
    return True


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--card", default=None, help="Process only this corp (partial name OK)")
    ap.add_argument("--test", action="store_true", help="Save to output/corps/test/")
    ap.add_argument("--quiet", action="store_true")
    ap.add_argument("--inpaint-only", action="store_true",
                    help="Clear all text regions; write only CEGBIRODALOM (no HU text drawn)")
    args = ap.parse_args()

    out_dir = os.path.join(OUTPUT_DIR, "test") if args.test else OUTPUT_DIR
    verbose = not args.quiet

    to_process = CORPS
    if args.card:
        needle = args.card.lower()
        to_process = {k: v for k, v in CORPS.items() if needle in k.lower()}
        if not to_process:
            print(f"No corp matching '{args.card}'. Available: {list(CORPS.keys())}")
            sys.exit(1)

    review_dir = os.path.join(OUTPUT_DIR, "review")
    total = 0
    for key, data in to_process.items():
        print(f"Processing: {key} ...")
        if args.inpaint_only:
            ok = process_corp_inpaint_only(key, data, out_dir,
                                           review_dir=review_dir, verbose=verbose)
        else:
            ok = process_corp(key, data, out_dir, verbose=verbose)
        if ok:
            total += 1

    print(f"\nDone. {total} card(s) saved to: {out_dir}")


if __name__ == "__main__":
    main()
