#!/usr/bin/env python3
"""
gradient_fill.py — Remove dark body text from TFM card text boxes.

All HO corp card text boxes (effect, action, flavor, start) have a light
gray gradient background (~RGB 200-235) with near-black body text.
This module reconstructs the background by inpainting masked text pixels.

Strategy:
  1. Per-row brightness percentile to threshold text vs background.
  2. Small dilation to cover anti-aliased glyph edges.
  3. OpenCV INPAINT_TELEA fills the mask from surrounding background.

For the illustration overlay region (start_region), larger inpaint radius
and looser threshold account for the more complex background pattern.
"""

import numpy as np

try:
    import cv2
    HAS_CV2 = True
except ImportError:
    HAS_CV2 = False


def inpaint_body_text(arr_rgb, x1, y1, x2, y2, radius=10, bg_percentile=55, text_gap=45):
    """
    Remove dark text from a light-background text box using OpenCV inpainting.

    Parameters:
        arr_rgb      : full card image as numpy uint8 RGB array
        x1,y1,x2,y2 : region bounding box in the array
        radius       : INPAINT_TELEA radius — increase for larger fonts / wider gaps
        bg_percentile: row brightness percentile to estimate background level
        text_gap     : pixels below (bg_level - text_gap) are considered text

    Returns the full array with the region inpainted.
    """
    if not HAS_CV2:
        return arr_rgb

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

    mask = np.zeros(brightness.shape, dtype=np.uint8)
    for row_idx in range(region.shape[0]):
        row_br = brightness[row_idx]
        # Estimate background level: upper percentile (ignores text minima)
        bg_level = np.percentile(row_br, bg_percentile)
        mask[row_idx, row_br < (bg_level - text_gap)] = 255

    # Dilate 2px to cover antialiased glyph edges and thin strokes
    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=radius,
                            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_illustration_text(arr_rgb, x1, y1, x2, y2, radius=18, bg_percentile=60, text_gap=50):
    """
    Remove text from regions where text overlays a complex illustration.

    Uses a larger inpaint radius so the algorithm can "reach around" text
    characters and sample from the illustration background further away.
    Also accepts a looser threshold since illustration colors vary more.

    The approach:
      - Estimate per-column background level (not per-row) for vertically
        aligned caption text common in illustration panels.
      - Combine per-row and per-column thresholds to avoid masking
        dark illustration details (shadows, lines) that aren't text.
    """
    if not HAS_CV2:
        return arr_rgb

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

    mask_row = np.zeros(brightness.shape, dtype=np.uint8)
    mask_col = np.zeros(brightness.shape, dtype=np.uint8)

    # Row-based threshold
    for row_idx in range(region.shape[0]):
        row_br = brightness[row_idx]
        bg_level = np.percentile(row_br, bg_percentile)
        mask_row[row_idx, row_br < (bg_level - text_gap)] = 255

    # Column-based threshold (catches vertical strokes that span multiple rows)
    for col_idx in range(region.shape[1]):
        col_br = brightness[:, col_idx]
        bg_level = np.percentile(col_br, bg_percentile)
        mask_col[col_br < (bg_level - text_gap), col_idx] = 255

    # Union: a pixel is text if flagged by BOTH row and column threshold
    # (intersection = requires agreement, reduces false masking of dark illustration)
    mask = (mask_row & mask_col)

    # Dilate to close gaps between strokes
    kernel = np.ones((3, 3), np.uint8)
    mask = cv2.dilate(mask, kernel, iterations=2)

    region_bgr = cv2.cvtColor(region, cv2.COLOR_RGB2BGR)
    inpainted = cv2.inpaint(region_bgr, mask, inpaintRadius=radius,
                            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 background_row_fill(arr_rgb, x1, y1, x2, y2, bg_percentile=85, text_gap=55):
    """
    Remove text by filling masked pixels with per-row background median.

    Unlike TELEA inpainting, this has zero boundary artifacts because each
    fill pixel gets the exact background gradient value from that row.

    Vertical structural lines (box borders) are detected and excluded from
    the mask so they are not erased.
    """
    region = arr_rgb[y1:y2, x1:x2].copy().astype(np.uint8)
    H, W = region.shape[:2]
    brightness = region.mean(axis=2)

    # 1. Build per-row text mask
    mask = np.zeros((H, W), dtype=bool)
    for row_idx in range(H):
        row_br = brightness[row_idx]
        bg_level = np.percentile(row_br, bg_percentile)
        mask[row_idx] = row_br < (bg_level - text_gap)

    # 2. Dilate 3 iterations to cover anti-aliased glyph edges
    if HAS_CV2:
        kernel = np.ones((3, 3), np.uint8)
        mask_u8 = mask.astype(np.uint8) * 255
        mask_u8 = cv2.dilate(mask_u8, kernel, iterations=3)
        mask = mask_u8 > 0

    # 3. Exclude persistent vertical stripes (box borders): columns where
    #    more than 85% of rows are masked — that's a structural line, not text.
    #    Using 0.85 (not 0.60) because dense text columns can reach 60-75%,
    #    while true card-border stripes are 100% masked after dilation.
    #    Also protect ±5px around each structural column so horizontal frame
    #    lines between border stripes are not filled (prevents gray rectangles).
    col_masked_fraction = mask.mean(axis=0)   # fraction of rows masked per col
    border_cols = col_masked_fraction > 0.85
    border_cols_extended = np.zeros(W, dtype=bool)
    for col in np.where(border_cols)[0]:
        lo_col = max(0, col - 5)
        hi_col = min(W, col + 6)
        border_cols_extended[lo_col:hi_col] = True
    mask[:, border_cols_extended] = False

    # 4. Compute per-row background median from non-masked pixels
    #    Smooth across rows with moving-average window to get a gradient
    row_bg = np.zeros((H, 3), dtype=np.float32)
    for row_idx in range(H):
        bg_pixels = region[row_idx, ~mask[row_idx], :]
        if len(bg_pixels) >= 3:
            row_bg[row_idx] = np.median(bg_pixels, axis=0)
        elif row_idx > 0:
            row_bg[row_idx] = row_bg[row_idx - 1]
        else:
            row_bg[row_idx] = region[row_idx].mean(axis=0)

    # Moving-average smoothing (window=11) across rows for gradient continuity
    win = 11
    pad = win // 2
    row_bg_pad = np.pad(row_bg, ((pad, pad), (0, 0)), mode='edge')
    row_bg_smooth = np.zeros_like(row_bg)
    for i in range(H):
        row_bg_smooth[i] = row_bg_pad[i:i+win].mean(axis=0)

    # 5. Fill masked pixels with their row background estimate
    result = region.copy().astype(np.float32)
    for row_idx in range(H):
        if mask[row_idx].any():
            result[row_idx, mask[row_idx]] = row_bg_smooth[row_idx]

    arr_out = arr_rgb.copy()
    arr_out[y1:y2, x1:x2] = result.astype(np.uint8)
    return arr_out


def remove_all_body_text(arr_rgb, data):
    """
    Apply the correct inpaint strategy to each text region defined in `data`.

    `data` is one of the CORPS entries from seamless_overlay.py.
    The name_region is intentionally skipped — corp names stay in English.

    Key: data["start_bg"] = "body" uses standard body-text removal for the
    start region (uniform gray bg); "illustration" (default) uses the more
    conservative illustration strategy.

    Returns the modified array.
    """
    arr = arr_rgb.copy()

    # Effect box — uniform gradient, standard removal
    if data.get("effect_region"):
        arr = inpaint_body_text(arr, *data["effect_region"])

    # Action box — same gradient style
    if data.get("action_region"):
        arr = inpaint_body_text(arr, *data["action_region"])

    # Flavor box — narrow strip, same approach
    if data.get("flavor_region"):
        arr = inpaint_body_text(arr, *data["flavor_region"])

    # Start/initial ability — route based on background type
    if data.get("start_region"):
        if data.get("start_bg") == "body":
            arr = background_row_fill(arr, *data["start_region"])
        else:
            arr = inpaint_illustration_text(arr, *data["start_region"])

    return arr
