#!/usr/bin/env python3
"""
Passport Generation API - m02/m03 Template Version
支持：m02 和 m03 两种模板，8 个可编辑字段 + 4 个固定字段 + MRZ 自动生成
使用方法: python3 passport_m03.py --img <template> --out <output> --data <json> [--template m02|m03]
"""

import json
import argparse
import sys
from datetime import datetime
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import numpy as np

# ============================================================
# 模板尺寸
# ============================================================
M02_WIDTH = 1672
M02_HEIGHT = 1840
M03_WIDTH = 1672
M03_HEIGHT = 1840

# ============================================================
# m02 模板字段位置常量 (OCR 校准)
# ============================================================
# m02 实测校准：坐标为字段"墨迹左上角"（值是印刷值行），右侧可编辑字段左对齐。
# 字体 = Liberation Sans Narrow Bold（Arial Narrow Bold 度量，与 m02 印刷字形一致）；
# 字号分两档：顶部行(type/ccode/passport_no) 30px，数据行 33px（对应墨迹高度≈21/23，扫描柔和后≈22/26）。
M02_FIELD_POS = {
    "type":          (606, 1056),
    "nationality":   (601, 1189),
    "authority":     (643, 1387),
    "passport_no":   (1025, 1054),
    "name":          (604, 1123),
    "country_code":  (745, 1055),
    "sex":           (600, 1322),
    "dob":           (601, 1256),
    "birth_place":   (911, 1320),
    "issue_date":    (598, 1389),
    "expiry_date":   (596, 1463),
}

# 字段值字体现定为 Times 系衬线粗体(Liberation Serif Bold, TNR Bold 度量克隆)：
#   - 顶行(type/ccode/passport_no) 墨迹高 21-23px  → size 32
#   - 数据行 墨迹高 23-26px                          → size 36
# (m02 实测：PV≈21.6, passport≈22.8, 数据行 23(字母)~26.2(数字)@100%)
M02_FIELD_SIZE = {
    "type": 32, "country_code": 32, "passport_no": 32,
    "nationality": 36, "authority": 36, "name": 36, "sex": 36, "dob": 36,
    "birth_place": 36, "issue_date": 36, "expiry_date": 36,
}

M02_MRZ_POS = {
    "prefix": (45, 1569),
    "line1":  (268, 1572),
    "line2":  (264, 1639),
}
# m02 实测 OCR-B size~36，pitch=25.744（ICAO 规定 10字符/英寸=25.4mm）
M02_MRZ_SIZES = {"line1": 36, "line2": 36}
M02_MRZ_PITCH = {"line1": 25.535, "line2": 25.744}
# m02 的 `0` 比 OCR-B 原生更宽更圆(实测 w20-21 vs 17-18)，横向拉伸 1.15 匹配
M02_MRZ_ZERO_STRETCH = 1.15
# m02 油墨比 OCR-B 略重(笔划 5.73 vs 5.48)：3x 超采样 + 亚像素加粗 0.34 匹配
M02_MRZ_SCALE = 3
M02_MRZ_STROKE = 0.34

# ============================================================
# m03 模板字段位置常量 (OCR 校准)
# ============================================================
M03_FIELD_POS = {
    "type":          (626, 1065),
    "nationality":   (600, 1200),
    "authority":     (1091, 1397),
    "passport_no":   (1642, 46),
    "name":          (802, 1039),
    "country_code":  (784, 1066),
    "sex":           (632, 1105),
    "dob":           (649, 1170),
    "birth_place":   (655, 1240),
    "issue_date":    (618, 1305),
    "expiry_date":   (661, 1371),
}

M03_FIELD_SIZE = {k: 24 for k in M03_FIELD_POS}

M03_MRZ_POS = {
    "prefix": (45, 1110),
    "line1":  (836, 1126),
    "line2":  (836, 1170),
}

# ============================================================
# 字体大小和颜色
# ============================================================
FONT_DIR = Path(__file__).parent.parent / "fonts"
FONT_FILES = {
    "title": "DejaVuSans-Bold.ttf",
    "pass":  "DejaVuSans-Bold.ttf",
    "label": "DejaVuSans.ttf",
    "val":   "LiberationSerif-Bold.ttf",
    "mrz":   "OCR-B.otf",
    "tiny":  "DejaVuSans.ttf",
}

COLORS = {
    "title": "#1a1a1a", "pass": "#6B0000", "gold": "#C4A35A",
    "label": "#666", "val": "#1f1c1a", "mrz": "#1a1a18",
}

# ============================================================
# MRZ 生成 (ICAO 9303)
# ============================================================
WEIGHTS = (7, 3, 1)

def to_val(ch: str) -> int:
    ch = ch.upper()
    if ch == "<": return 0
    if ch.isdigit(): return int(ch)
    return ord(ch) - ord("A") + 10

def check_digit(data: str) -> int:
    return sum(to_val(c) * WEIGHTS[i % 3] for i, c in enumerate(data)) % 10

def _padded(raw: str, n: int) -> str:
    return raw.upper().ljust(n, "<")[:n]

def build_line1(surname: str, given: str = "", type_code: str = "PV", country: str = "MMR") -> str:
    s = surname.upper().replace(" ", "<")
    g = given.upper().replace(" ", "<") if given else ""
    t = type_code[1] if len(type_code) > 1 and type_code[1] != "<" else "<"
    c = country.upper()[:3]
    id_part = s + "<<" + g if g else s
    base = ("P" + t + c + "<" + id_part).ljust(43, "<")[:43]
    return base + str(check_digit(base))

def build_line2(passport_no: str, birth_date: str, expiry_date: str,
                sex: str = "M", issuing_state: str = "MMR", personal_no: str = "") -> str:
    p = _padded(passport_no, 9)
    b = _padded(birth_date, 6)
    e = _padded(expiry_date, 6)
    n = _padded(personal_no, 14)
    
    ps = f"{p}{check_digit(p)}"
    bs = f"{b}{check_digit(b)}"
    es = f"{e}{check_digit(e)}"
    ns = n + str(check_digit(n)) if personal_no.strip("<") else n + "<"
    
    l43 = ps + issuing_state + bs + sex + es + ns
    return l43 + str(check_digit(ps + bs + es + ns))

def gen_mrz(data: dict) -> tuple[str, str]:
    dob = datetime.strptime(data["dob"], "%d %b %Y")
    expiry = datetime.strptime(data["expiry_date"], "%d %b %Y")
    dob_str = dob.strftime("%y%m%d")
    exp_str = expiry.strftime("%y%m%d")
    name = data.get("name", "")
    surname = data.get("surname", name)
    given_name = data.get("given_name", "")
    return build_line1(surname, given_name, data.get("type", "PV"), data.get("country_code", "MMR")), \
           build_line2(data["passport_no"], dob_str, exp_str, data.get("sex", "M"), "MMR")


# ============================================================
# 绘制工具
# ============================================================
def load_font(name: str, size: int) -> ImageFont.FreeTypeFont:
    if name not in FONT_FILES:
        raise ValueError(f"Unknown font name: {name}")
    path = FONT_DIR / FONT_FILES[name]
    if not path.exists():
        raise FileNotFoundError(f"Font not found: {path}")
    return ImageFont.truetype(str(path), size)

def hex_to_rgb(hex_color: str) -> tuple:
    h = hex_color.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_mask(img: Image.Image, xy: tuple, text: str, font: ImageFont.FreeTypeFont,
                   fill: str, anchor: str = "lt", debug: bool = False, label: str = "",
                   texture: bool = False) -> None:
    mask = font.getmask(text, mode='L')
    if mask.size[0] <= 0 or mask.size[1] <= 0:
        return
    mask_img = Image.frombytes('L', mask.size, bytes(mask))
    mw, mh = mask_img.size
    x, y = xy
    
    anchors = {
        "lt": (x, y), "mt": (x - mw//2, y), "rt": (x - mw, y),
        "mm": (x - mw//2, y - mh//2), "lb": (x, y - mh),
        "mb": (x - mw//2, y - mh), "rb": (x - mw, y - mh)
    }
    tl_x, tl_y = anchors.get(anchor, (x, y))
    
    if texture:
        # 印刷质感：轻微高斯模糊(省纸软边) + 噪声(墨迹晕染)，模拟 m02 扫描件观感
        f = hex_to_rgb(fill) if isinstance(fill, str) else fill
        text_layer = Image.new('RGB', (mw, mh), f)
        text_layer = text_layer.filter(ImageFilter.GaussianBlur(radius=0.8))
        arr = np.array(text_layer).astype(np.float32)
        # 噪声只扰动明度，且聚焦在墨迹区，避免把深墨整体泛白
        arr[...] += np.random.normal(0, 8, arr.shape)
        text_layer = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
        img.paste(text_layer, (tl_x, tl_y), mask_img)
    else:
        text_layer = Image.new('RGB', (mw, mh), hex_to_rgb(fill) if isinstance(fill, str) else fill)
        img.paste(text_layer, (tl_x, tl_y), mask_img)
    
    if debug:
        draw = ImageDraw.Draw(img)
        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")
        draw.text((tl_x, tl_y - 14), f"{label} {mw}x{mh}", fill="#FF0000")


def render_ss(img: Image.Image, xy: tuple, text: str, size: int, fill: str,
              pitch: float = None, stroke: int = 0, scale: int = 2,
              blur: float = 0.9, noise: float = 8.0, round_zero: bool = False,
              zero_stretch: float = 1.0, zero_stretch_y: float = 1.0) -> None:
    """超采样渲染 MRZ：2x 画布上**逐字符**按自定义 pitch 排布(自然字形+收紧字距，
    复刻 m02 扫描件的"全宽字形+紧凑字距"特征)，高斯模糊+噪声后下采样回原尺寸。
    round_zero=True 用字母 O 字形渲染数字 0；zero_stretch/zero_stretch_y 对 0 字形
    横向/纵向拉伸(m02 的零是圆形、数字高度，而 OCR-B 的 O 是大写高度且 M 同宽)。"""
    s = scale
    W, H = img.size
    fpath = str(FONT_DIR / FONT_FILES["mrz"])
    f2 = ImageFont.truetype(fpath, int(size * s))
    col = hex_to_rgb(fill)
    if pitch is None:
        pitch = f2.getlength("M") / s
    layer = Image.new('RGBA', (W * s, H * s), (0, 0, 0, 0))
    d = ImageDraw.Draw(layer)
    pad = int(size * s)
    for i, ch in enumerate(text):
        glyph = "O" if (round_zero and ch == "0") else ch
        px = int(xy[0] * s + i * pitch * s)
        py = int(xy[1] * s)
        if ch == "0" and (zero_stretch != 1.0 or zero_stretch_y != 1.0):
            tmp = Image.new('RGBA', (pad * 3, pad * 3), (0, 0, 0, 0))
            ImageDraw.Draw(tmp).text((pad, pad), glyph, font=f2, fill=col + (255,))
            bb = tmp.getbbox()
            if bb:
                p = tmp.crop(bb)
                ow, oh = p.size
                nw = max(1, int(round(ow * zero_stretch)))
                nh = max(1, int(round(oh * zero_stretch_y)))
                p = p.resize((nw, nh), Image.LANCZOS)
                # 以字形中心为基准拉伸，保持字符网格位置不变
                cx = px + (bb[0] - pad) + ow / 2.0
                cy = py + (bb[1] - pad) + oh / 2.0
                layer.alpha_composite(p, (int(round(cx - nw / 2.0)), int(round(cy - nh / 2.0))))
        else:
            d.text((px, py), glyph, font=f2, fill=col + (255,),
                   stroke_width=int(stroke * s), stroke_fill=col + (255,))
    alpha = np.array(layer.split()[3])
    ys, xs = np.where(alpha > 0)
    if len(ys) == 0:
        return
    x0, x1 = max(0, xs.min() - 12), min(W * s, xs.max() + 12)
    y0, y1 = max(0, ys.min() - 12), min(H * s, ys.max() + 12)
    crop = layer.crop((x0, y0, x1, y1))
    # 颜色层：均匀墨色 + 噪声；对 alpha 蒙版轻度模糊产生扫描软边
    a = crop.split()[3].filter(ImageFilter.GaussianBlur(radius=blur * s * 0.5))
    rgb = np.zeros((crop.size[1], crop.size[0], 3), np.float32)
    rgb[...] = col
    rgb = rgb + np.random.normal(0, noise, rgb.shape)
    rgb = Image.fromarray(np.clip(rgb, 0, 255).astype(np.uint8))
    a_layer = rgb.convert('RGBA')
    a_layer.putalpha(a)
    a_layer = a_layer.resize((crop.size[0] // s, crop.size[1] // s), Image.LANCZOS)
    img.paste(a_layer, (x0 // s, max(0, y0) // s), a_layer)


# ============================================================
# 主写入函数
# ============================================================
def write_passport(img_path: str, out_path: str, data: dict, template: str = "m03",
                   debug: bool = False, skip_header: bool = False,
                   skip_fixed: bool = False) -> None:
    img = Image.open(img_path).convert("RGB")
    W, H = img.size
    
    # 根据模板选择配置
    if template == "m02":
        FIELD_POS = M02_FIELD_POS
        FIELD_SIZE = M02_FIELD_SIZE
        MRZ_POS = M02_MRZ_POS
        scale_f = min(W / 900, H / 630)
    else:
        FIELD_POS = M03_FIELD_POS
        FIELD_SIZE = M03_FIELD_SIZE
        MRZ_POS = M03_MRZ_POS
        scale_f = min(W / 900, H / 630)
    
    sf = lambda v: max(1, int(v * scale_f))
    F = {k: load_font(k, v) for k, v in {
        "title": max(12, int(24*scale_f)),
        "pass":  max(10, int(17*scale_f)),
        "label": max(8,  int(11*scale_f)),
        "val":   max(9,  int(13*scale_f)),
        "mrz":   max(10, int(19.4*scale_f)),
        "tiny":  max(6,  int(8*scale_f)),
    }.items()}
    if template == "m02":
        # 复刻 m02 实物：L1 字距 25px(35)、L2 26px(36)，右端对齐
        F["mrz1"] = load_font("mrz", max(10, int(M02_MRZ_SIZES["line1"] * scale_f / 1.858)))
        F["mrz2"] = load_font("mrz", max(10, int(M02_MRZ_SIZES["line2"] * scale_f / 1.858)))
    else:
        F["mrz1"] = F["mrz2"] = F["mrz"]
    C = COLORS
    
    if debug:
        draw = ImageDraw.Draw(img)
    
    if not skip_header:
        # 1. 页眉
        draw_text_mask(img, (W//2, 55), "REPUBLIC OF THE UNION OF MYANMAR", F["title"], C["title"], "mt", debug, "title1")
        draw_text_mask(img, (W//2, 55 + sf(66)), "P A S S P O R T", F["pass"], C["pass"], "mt", debug, "title2")
        
        # 2. 右上区域
        draw_text_mask(img, (W - sf(30), sf(25)), f"Passport No  {data['passport_no']}", F["label"], "#1a1a1a", "rt", debug, "passport_no")
        draw_text_mask(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")
    
# 3. 固定字段 (3个)；值左对齐、字号逐字段
    if not skip_fixed:
        for fk in ("type", "nationality", "authority"):
            fx, fy = FIELD_POS[fk]
            fnt = load_font("val", FIELD_SIZE.get(fk, 24))
            draw_text_mask(img, (fx, fy), str(data.get(fk, "")), fnt, C["val"],
                           "lt", debug, f"{fk}_val")

    # 4. 可编辑字段 (8个)；skip_fixed 时排除网站 readonly 的 country_code，保留模板印刷
    editable = [
        ("name", FIELD_POS["name"][0], FIELD_POS["name"][1]),
        ("country_code", FIELD_POS["country_code"][0], FIELD_POS["country_code"][1]),
        ("sex", FIELD_POS["sex"][0], FIELD_POS["sex"][1]),
        ("dob", FIELD_POS["dob"][0], FIELD_POS["dob"][1]),
        ("birth_place", FIELD_POS["birth_place"][0], FIELD_POS["birth_place"][1]),
        ("issue_date", FIELD_POS["issue_date"][0], FIELD_POS["issue_date"][1]),
        ("expiry_date", FIELD_POS["expiry_date"][0], FIELD_POS["expiry_date"][1]),
        ("passport_no", FIELD_POS["passport_no"][0], FIELD_POS["passport_no"][1]),
    ]
    if skip_fixed:
        editable = [e for e in editable if e[0] != "country_code"]

    for key, x, y in editable:
        val = data.get(key, "")
        fnt = load_font("val", FIELD_SIZE.get(key, 24))
        draw_text_mask(img, (x, y), str(val), fnt, C["val"], "lt", debug, f"field_{key}")
    
    # 5. MRZ 渲染 (OCR-B 标准字体，2x超采样，pitch匹配m02实测)
    l1, l2 = gen_mrz(data)
    render_ss(img, MRZ_POS["line1"], l1, M02_MRZ_SIZES["line1"], C["mrz"],
              pitch=M02_MRZ_PITCH["line1"], zero_stretch=M02_MRZ_ZERO_STRETCH,
              stroke=M02_MRZ_STROKE, scale=M02_MRZ_SCALE)
    render_ss(img, MRZ_POS["line2"], l2, M02_MRZ_SIZES["line2"], C["mrz"],
              pitch=M02_MRZ_PITCH["line2"], zero_stretch=M02_MRZ_ZERO_STRETCH,
              stroke=M02_MRZ_STROKE, scale=M02_MRZ_SCALE)
    
    img.save(out_path, dpi=(300, 300))
    print(f"✓ Generated: {out_path} ({W}x{H}) template={template}")


# ============================================================
# CLI 入口
# ============================================================
if __name__ == "__main__":
    p = argparse.ArgumentParser(description="Passport generator - m02/m03 templates")
    p.add_argument("--img", required=True)
    p.add_argument("--out", required=True)
    p.add_argument("--data", required=True, help="JSON or @file.json")
    p.add_argument("--template", choices=["m02", "m03"], default="m03", help="Template type")
    p.add_argument("--debug", action="store_true")
    p.add_argument("--skip-header", action="store_true")
    p.add_argument("--skip-fixed", action="store_true")
    args = p.parse_args()
    
    if args.data.startswith("@"):
        with open(args.data[1:]) as f:
            data = json.load(f)
    else:
        data = json.loads(args.data)
    
    required = ["name", "nationality", "dob", "sex",
                "birth_place", "issue_date", "expiry_date", "authority",
                "passport_no", "type", "country_code"]
    missing = [k for k in required if k not in data]
    if missing:
        sys.exit(f"Missing fields: {missing}")
    
    write_passport(args.img, args.out, data, args.template, args.debug,
                   args.skip_header, args.skip_fixed)