#!/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/passport/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/passport/scripts")
from mrz import build_line1, build_line2

def gen_mrz(data, dob, expiry):
    dob_str = dob.strftime("%y%m%d")
    exp_str = expiry.strftime("%y%m%d")
    l1 = build_line1(data["surname"], data["given_name"],
                     data.get("type", "PV"), data.get("country_code", "MMR"))
    l2 = build_line2(data["passport_no"], 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},
}

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)
        angles.append(angle)
    
    if len(angles) > 5:
        median_angle = np.median(angles)
        if abs(median_angle) > 0.0087:
            H, W = img.shape[:2]
            M = cv2.getRotationMatrix2D((W/2, H/2), np.degrees(median_angle), 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/passport/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):
    img_cv = cv2.imread(img_path)
    if img_cv is None:
        raise FileNotFoundError(img_path)
    
    # Perspective correction
    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)
    
    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 8 variable fields using affine transform
    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 = datetime.strptime(data["dob"], "%d %b %Y")
    expiry = datetime.strptime(data["expiry_date"], "%d %b %Y")
    l1, l2 = gen_mrz(data, dob, expiry)
    
    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})")

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")
    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)
