#!/usr/bin/env python3
"""
Passport text writing tool - Fixed version
Fixes 4 technical issues:
1. Hardcoded coordinates -> runtime affine transform detection
2. LaMa fails on guilloche -> OpenCV INPAINT_TELEA
3. No perspective correction -> getPerspectiveTransform
4. Perfect digital text -> texture blending + noise
"""
import json, sys, argparse
from datetime import datetime
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import numpy as np
import cv2

FONT_DIR = Path("/data/port/fonts")
FONT_FILES = {
    "sans":      "DejaVuSans.ttf",
    "sans_bold": "DejaVuSans-Bold.ttf",
    "mono":      "DejaVuSansMono.ttf",
}

def load_font(name, size):
    path = FONT_DIR / FONT_FILES[name]
    if not path.exists():
        raise FileNotFoundError(f"Font not found: {path}")
    return ImageFont.truetype(str(path), size)

# MRZ generation
sys.path.insert(0, "/data/port/scripts")
from mrz import build_line1, build_line2

def _parse_date(s):
    for fmt in ("%d %b %Y", "%d %B %Y", "%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y"):
        try:
            return datetime.strptime((s or "").strip(), fmt)
        except Exception:
            pass
    return None

def gen_mrz(data, dob, expiry):
    dob_str = dob.strftime("%y%m%d")
    exp_str = expiry.strftime("%y%m%d")
    if data.get("name"):
        surname = data["name"].upper().replace(" ", "<")
        given = ""
    else:
        surname = (data.get("surname", "") or "").upper().replace(" ", "<")
        given = (data.get("given_name", "") or "").upper().replace(" ", "<")
    l1 = build_line1(surname, given,
                     data.get("type", "PV"), data.get("country_code", "MMR"))
    l2 = build_line2((data.get("passport_no", "") or "MK000000").upper(),
                     dob_str, exp_str, data.get("sex", "M"), "MMR")
    assert len(l1) == 44 and len(l2) == 44
    return l1, l2

# Runtime anchor detection
def find_anchor_points(img):
    """Detect PASSPORT (top-left) and MOHA (bottom-right) anchor points"""
    H, W = img.shape[:2]
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    refs = {}
    
    # PASSPORT: y=1000-1100, x=250-600
    pass_region = gray[1000:1100, 250:600]
    _, binary = cv2.threshold(pass_region, 100, 255, cv2.THRESH_BINARY_INV)
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if contours:
        largest = max(contours, key=cv2.contourArea)
        x, y, w, h = cv2.boundingRect(largest)
        refs["passport"] = (250 + x + w//2, 1000 + y + h//2)
    else:
        refs["passport"] = (432, 1063)
    
    # MOHA: y=1380-1450, x=900-1300
    moha_region = gray[1380:1450, 900:1300]
    _, binary_m = cv2.threshold(moha_region, 100, 255, cv2.THRESH_BINARY_INV)
    contours_m, _ = cv2.findContours(binary_m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if contours_m:
        largest = max(contours_m, key=cv2.contourArea)
        xm, ym, wm, hm = cv2.boundingRect(largest)
        refs["moha"] = (900 + xm + wm//2, 1380 + ym + hm//2)
    else:
        refs["moha"] = (1090, 1397)
    
    # REPUBLIC title
    rep_region = gray[880:970, 500:1200]
    _, binary_r = cv2.threshold(rep_region, 100, 255, cv2.THRESH_BINARY_INV)
    contours_r, _ = cv2.findContours(binary_r, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if contours_r:
        largest = max(contours_r, key=cv2.contourArea)
        xr, yr, wr, hr = cv2.boundingRect(largest)
        refs["republic"] = (500 + xr + wr//2, 880 + yr + hr//2)
    else:
        refs["republic"] = (845, 932)
    
    # Portrait right edge
    portrait_region = gray[118:318, 150:520]
    non_white = np.any(portrait_region < 220, axis=1)
    xs_p = np.where(non_white)[0]
    refs["portrait_right"] = 150 + xs_p.max() if len(xs_p) > 0 else 500
    
    return refs

# m02 field reference coordinates (label x + content offset)
M02_FIELDS = {
    "surname":     {"lx": 600, "ly": 1020, "dx": 140},
    "given_name":  {"lx": 906, "ly": 1020, "dx": 110},
    "sex":         {"lx": 600, "ly": 1092, "dx": 140},
    "dob":         {"lx": 600, "ly": 1160, "dx": 140},
    "birth_place": {"lx": 600, "ly": 1226, "dx": 140},
    "issue_date":  {"lx": 600, "ly": 1300, "dx": 140},
    "expiry_date": {"lx": 600, "ly": 1361, "dx": 140},
    "authority":   {"lx": 908, "ly": 1380, "dx": 20},
}

# m02/m03 模板 (1672x1840) 精确坐标：来自模板/m02文字坐标.json + OCR 实测。
# (x0, y0) = 值文字左上角, (w, h) = 值区域尺寸, 字号按区域高度推导。
M02_FIXED = {
    "type":          (603, 1060, 45, 15),
    "country_code":  (743, 1060, 80, 16),
    "passport_no":   (1022, 1059, 145, 14),
    "name":          (605, 1128, 160, 15),
    "nationality":   (603, 1196, 170, 15),
    "dob":           (602, 1263, 170, 15),
    "sex":           (596, 1326, 18, 22),
    "birth_place":   (908, 1326, 150, 20),
    "issue_date":    (600, 1394, 190, 17),
    "authority":     (909, 1391, 350, 18),
    "expiry_date":   (598, 1470, 190, 17),
}
M02_FIXED_MRZ = {
    "line1": (274, 1584, 1100, 20),
    "line2": (274, 1647, 1100, 24),
}

def compute_affine_transform(refs):
    """Compute affine transform from m02 reference to current image"""
    m02_pass = np.array([432.0, 1063.0], dtype=np.float32)
    m02_moha = np.array([1090.0, 1397.0], dtype=np.float32)
    m02_rep  = np.array([845.0, 932.0], dtype=np.float32)
    
    pts_src = np.array([m02_pass, m02_moha, m02_rep], dtype=np.float32)
    pts_dst = np.array([refs["passport"], refs["moha"], refs["republic"]], dtype=np.float32)
    
    M = cv2.getAffineTransform(pts_src, pts_dst)
    return M

def transform_point(M, x, y):
    pt = np.array([x, y, 1.0], dtype=np.float32)
    out = M @ pt
    return (out[0], out[1])

# Perspective correction
def detect_and_correct_perspective(img):
    """Detect and correct perspective skew"""
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    edges = cv2.Canny(gray, 50, 150)
    lines = cv2.HoughLinesP(edges, 1, np.pi/180, 200, minLineLength=300, maxLineGap=20)
    if lines is None:
        return img
    
    angles = []
    for line in lines:
        x1, y1, x2, y2 = line[0]
        angle = np.arctan2(y2 - y1, x2 - x1)
        a = angle % np.pi
        if a > np.pi / 2:
            a -= np.pi
        if a > np.pi / 4:
            a -= np.pi / 2
        elif a < -np.pi / 4:
            a += np.pi / 2
        angles.append(a)
    
    if len(angles) > 5:
        median_angle = np.median(angles)
        deg = np.degrees(median_angle)
        if 0.15 < abs(deg) < 2.0:
            H, W = img.shape[:2]
            M = cv2.getRotationMatrix2D((W/2, H/2), deg, 1.0)
            corrected = cv2.warpAffine(img, M, (W, H), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
            return corrected
    return img

# Texture-blended text drawing
def hex_to_rgb(h):
    h = h.lstrip('#')
    if len(h) == 3: h = ''.join(c*2 for c in h)
    return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))

def draw_text_with_texture(draw, img, xy, text, font, fill, anchor, debug=False, label=""):
    """Draw text with edge feathering using background texture"""
    mask = font.getmask(text, mode='L')
    mask_img = Image.frombytes('L', mask.size, bytes(mask))
    mw, mh = mask_img.size
    x, y = xy
    
    anchor_map = {"lt": (x, y), "rt": (x-mw, y), "mm": (x-mw//2, y-mh//2),
                  "lb": (x, y-mh)}
    tl_x, tl_y = anchor_map.get(anchor, (x, y))
    
    text_color = hex_to_rgb(fill) if isinstance(fill, str) else fill
    text_layer = Image.new('RGBA', (mw, mh), (*text_color, 0))
    alpha = mask_img.filter(ImageFilter.GaussianBlur(radius=0.8))
    text_layer.putalpha(alpha)
    
    pad = 3
    bg_patch = img.crop((tl_x-pad, tl_y-pad, tl_x+mw+pad, tl_y+mh+pad)).convert('RGBA')
    bg_with_text = bg_patch.copy()
    bg_with_text.paste(text_layer, (pad, pad), text_layer)
    img.paste(bg_with_text.convert('RGB'), (tl_x-pad, tl_y-pad))
    
    if debug:
        draw.rectangle([tl_x, tl_y, tl_x+mw, tl_y+mh], outline="#FF0000", width=2)
        draw.ellipse([x-3, y-3, x+3, y+3], fill="#00FF00")

# INPAINT_TELEA replacement for LaMa
def inpaint_text_regions(img, debug=False):
    """Use INPAINT_TELEA to erase template text for clean background"""
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY_INV)
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    mask = np.zeros_like(gray)
    for cnt in contours:
        area = cv2.contourArea(cnt)
        if 20 < area < 5000:
            x, y, w, h = cv2.boundingRect(cnt)
            if 800 < y < 1500 and 500 < x < 1300:
                cv2.drawContours(mask, [cnt], -1, 255, -1)
    
    if debug and np.any(mask > 0):
        cv2.imwrite('/data/port/output/debug_inpaint_mask.png', mask)
    
    if np.any(mask > 0):
        return cv2.inpaint(img, mask, 3, cv2.INPAINT_TELEA)
    return img

# Main function
def write_passport_fixed(img_path, out_path, data, debug=False, skip_header=False, no_affine=False):
    if data.get("name") and not (data.get("surname") or data.get("given_name")):
        parts = str(data.get("name", "")).strip().upper().split()
        if parts:
            data["surname"] = parts[0]
            data["given_name"] = " ".join(parts[1:]) if len(parts) > 1 else ""
    img_cv = cv2.imread(img_path)
    if img_cv is None:
        raise FileNotFoundError(img_path)

    if no_affine:
        # 模板直出模式：跳过透视校正/锚点检测/仿射变换，
        # 直接使用 m02/m03 模板 (1672x1840) 的精确校准坐标。
        refs = {"passport": (432, 1063), "moha": (1090, 1397),
                "republic": (845, 932), "portrait_right": 349}
        M = np.eye(2, 3, dtype=np.float32)
        sf = lambda v: max(1, int(v))
    else:
        img_cv = detect_and_correct_perspective(img_cv)

        # Optional: inpaint old text (disabled - template has background texture we want to preserve)
        # img_cv = inpaint_text_regions(img_cv, debug)

        # Detect anchors and compute transform
        refs = find_anchor_points(img_cv)
        M = compute_affine_transform(refs)

        print(f"[Fixed] PASSPORT: {refs['passport']}")
        print(f"[Fixed] MOHA: {refs['moha']}")
        print(f"[Fixed] Republic: {refs['republic']}")
        print(f"[Fixed] Portrait right: {refs['portrait_right']}")

    img = Image.fromarray(cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB))
    W, H = img.size
    draw = ImageDraw.Draw(img)

    if no_affine:
        scale_f = 1.0
    else:
        scale_f = min(W / 900, H / 630)
    sf = lambda v: max(1, int(v * scale_f))
    F = {
        "title": load_font("sans_bold", max(12, int(24*scale_f))),
        "val": load_font("sans_bold", max(9, int(13*scale_f))),
        "mrz": load_font("mono", max(10, int(18*scale_f))),
        "label": load_font("sans", max(8, int(11*scale_f))),
        "tiny": load_font("sans", max(6, int(8*scale_f))),
    }
    C = {"title": "#1a1a1a", "pass": "#6B0000", "val": "#111", "mrz": "#111", "label": "#666"}

    # Write header
    if not skip_header:
        rep_x, rep_y = refs["republic"]
        draw_text_with_texture(draw, img, (rep_x, rep_y - sf(15)),
                               "REPUBLIC OF THE UNION OF MYANMAR", F["title"], C["title"], "mm", debug, "title")
        draw_text_with_texture(draw, img, (rep_x, rep_y + sf(21)),
                               "P A S S P O R T", F["title"], C["pass"], "mm", debug, "passport")

        draw_text_with_texture(draw, img, (W - sf(30), sf(25)),
                               f"Passport No  {data['passport_no']}", F["label"], "#1a1a1a", "rt", debug, "passport_no")
        draw_text_with_texture(draw, img, (W - sf(30), sf(45)),
                               f"Type  {data.get('type','PV')}    Code  {data.get('country_code','MMR')}",
                               F["tiny"], "#555", "rt", debug, "type_code")

    # Write variable fields
    if no_affine:
        _draw_fields_fixed(draw, img, data, F, C, debug)
    else:
        for key, ref in M02_FIELDS.items():
            val = str(data.get(key, ""))
            if not val:
                continue
            lx, ly, dx = ref["lx"], ref["ly"], ref["dx"]
            vx, vy = transform_point(M, lx + dx, ly)
            vx = max(10, min(W - 100, vx))
            vy = max(10, min(H - 10, vy))
            draw_text_with_texture(draw, img, (int(vx), int(vy)), val, F["val"], C["val"], "lt", debug, f"val_{key}")

    # Write MRZ
    dob = _parse_date(data.get("dob")) or datetime(2000, 2, 6)
    expiry = _parse_date(data.get("expiry_date")) or datetime(2030, 11, 23)
    l1, l2 = gen_mrz(data, dob, expiry)

    if no_affine:
        # 模板直出：MRZ 用固定行区。
        for i, line in enumerate((l1, l2), start=1):
            x, y, w, h = M02_FIXED_MRZ[f"line{i}"]
            fs = int(round(w / (44 * 0.73)))
            draw_text_with_texture(draw, img, (x, y), line, F["mrz"], C["mrz"], "lt", debug, f"mrz_l{i}")
    else:
        pass_cy = refs["passport"][1]
        moha_cy = refs["moha"][1]
        mrz_y1 = int(pass_cy + (moha_cy - pass_cy) * 0.17)
        mrz_y2 = mrz_y1 + 65
        mrz_x = refs["portrait_right"] + 10

        draw_text_with_texture(draw, img, (mrz_x, mrz_y1 - sf(6)), "P<", F["mrz"], C["mrz"], "lt", debug, "mrz_prefix")
        draw_text_with_texture(draw, img, (mrz_x + sf(20), mrz_y1), l1, F["mrz"], C["mrz"], "lt", debug, "mrz_l1")
        draw_text_with_texture(draw, img, (mrz_x + sf(20), mrz_y2), l2, F["mrz"], C["mrz"], "lt", debug, "mrz_l2")

    # Add scan-like noise
    arr = np.array(img)
    rng = np.random.default_rng(42)
    noise = rng.normal(0, 0.8, arr.shape).astype(np.float32)
    arr = np.clip(arr.astype(np.float32) + noise, 0, 255).astype(np.uint8)
    img = Image.fromarray(arr)

    img.save(out_path, dpi=(300, 300))
    print(f"[Fixed] Output: {out_path} ({W}x{H})")


def _draw_fields_fixed(draw, img, data, F, C, debug=False):
    """M02_M03 模板直出：按精确坐标绘制字段值。
    surname+given_name 合并到单行 name。"""
    merged_name = ""
    if data.get("name"):
        merged_name = str(data["name"]).strip().upper()
    else:
        s = str(data.get("surname", "") or "").strip().upper()
        g = str(data.get("given_name", "") or "").strip().upper()
        merged_name = " ".join(x for x in (s, g) if x)

    fields = {}
    for key, spec in M02_FIXED.items():
        val = ""
        if key == "name":
            val = merged_name
        else:
            val = str(data.get(key, "") or "").strip().upper()
        if val:
            fields[key] = (val, spec)

    for key, (val, (x, y, w, h)) in fields.items():
        # 按区域高度决定字号，尽量贴近模板原字
        fs = M02_FIELD_FONT.get(key, max(10, int(h * 0.9)))
        font = load_font("sans_bold", fs)
        tw = draw.textlength(val, font=font)
        # 超宽则等比缩到区内再画，保持可读（不换行）
        if tw > w:
            font = load_font("sans_bold", max(8, int(fs * w / tw)))
            tw = draw.textlength(val, font=font)
        draw_text_with_texture(draw, img, (x, y + max(0, (h - fs) // 2)), val,
                               font, C["val"], "lt", debug, f"fixed_{key}")


# 模板值区域高度 → 字号映射（m02/m03, 1672x1840）
M02_FIELD_FONT = {
    "type": 18, "country_code": 20, "passport_no": 16, "name": 18,
    "nationality": 18, "dob": 18, "sex": 22, "birth_place": 22,
    "issue_date": 20, "authority": 20, "expiry_date": 20,
}

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--img", required=True)
    parser.add_argument("--out", required=True)
    parser.add_argument("--data", required=True)
    parser.add_argument("--debug", action="store_true")
    parser.add_argument("--skip-header", action="store_true")
    parser.add_argument("--no-affine", action="store_true")
    args = parser.parse_args()
    
    if args.data.startswith("@"):
        with open(args.data[1:], "r", encoding="utf-8") as f:
            data = json.load(f)
    else:
        data = json.loads(args.data)
    
    write_passport_fixed(args.img, args.out, data, args.debug, args.skip_header, args.no_affine)
