"""PP-OCRv4 ONNX 推理 + bbox 缓存。

det 模型: pp-ocrv4_mobile_det.onnx  (DBNet 文本检测)
rec 模型: en_mobile_rec.onnx          (CRNN 英文识别, 95 字符)
dict:     en_mobile_rec.yml 内的 character_dict (95 字符)

对 mm_girl.jpg 启动时跑一次 det，结果缓存到全局 _TEMPLATE_BBOXES。
后续 generate_passport_image 直接用缓存的 bbox 抹除原文 + 重绘。
"""
import os
import yaml
import cv2
import numpy as np
import onnxruntime as ort

OCR_DIR = "/data/port/models/ocr"
DET_PATH = os.path.join(OCR_DIR, "pp-ocrv4_mobile_det.onnx")
REC_PATH = os.path.join(OCR_DIR, "en_mobile_rec.onnx")
REC_YML  = os.path.join(OCR_DIR, "en_mobile_rec.yml")

_DET_SESS = None
_REC_SESS = None
_REC_CHARS = None
_TEMPLATE_BBOXES = []  # list of (x0,y0,x1,y1)


def _load_yaml_dict():
    with open(REC_YML) as f:
        cfg = yaml.safe_load(f)
    return cfg["PostProcess"]["character_dict"]


def init_ocr():
    """启动时调用一次：加载 det+rec 模型 + 跑 mm_girl.jpg det。"""
    global _DET_SESS, _REC_SESS, _REC_CHARS
    _DET_SESS = ort.InferenceSession(DET_PATH, providers=["CPUExecutionProvider"])
    _REC_SESS = ort.InferenceSession(REC_PATH, providers=["CPUExecutionProvider"])
    _REC_CHARS = _load_yaml_dict()
    print(f"[OCR] det+rec loaded, dict size={len(_REC_CHARS)}", flush=True)


def _preprocess_det(img, limit=1280):
    h, w = img.shape[:2]
    scale = limit / max(h, w)
    nw, nh = int(w * scale) // 32 * 32, int(h * scale) // 32 * 32
    nw = max(nw, 32); nh = max(nh, 32)
    resized = cv2.resize(img, (nw, nh))
    blob = resized.astype(np.float32) / 255.0
    blob = (blob - np.array([0.485, 0.456, 0.406], dtype=np.float32)) \
         / np.array([0.229, 0.224, 0.225], dtype=np.float32)
    return blob.transpose(2, 0, 1)[None], scale


def _det_boxes(img):
    blob, scale = _preprocess_det(img)
    out = _DET_SESS.run(None, {_DET_SESS.get_inputs()[0].name: blob})[0]
    pred = out[0, 0]
    binary = (pred > 0.3).astype(np.uint8)
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    boxes = []
    for c in contours:
        if cv2.contourArea(c) < 30:
            continue
        x, y, w, h = cv2.boundingRect(c)
        boxes.append((int(x / scale), int(y / scale),
                      int((x + w) / scale), int((y + h) / scale)))
    boxes.sort(key=lambda b: (b[1], b[0]))
    return boxes


def _preprocess_rec(crop, max_w=320):
    h, w = crop.shape[:2]
    if h == 0:
        return None
    ratio = 48 / h
    nw = max(int(w * ratio), 1)
    nw = min(nw, max_w)
    resized = cv2.resize(crop, (nw, 48))
    blob = resized.astype(np.float32) / 255.0
    blob = (blob - 0.5) / 0.5
    return blob.transpose(2, 0, 1)[None]


def _rec_text(crop):
    b = _preprocess_rec(crop)
    if b is None:
        return ""
    out = _REC_SESS.run(None, {_REC_SESS.get_inputs()[0].name: b})[0]
    preds = out[0]
    prev = 0
    text = []
    for t in range(preds.shape[0]):
        idx = int(np.argmax(preds[t]))
        if idx != prev and idx != 0 and idx - 1 < len(_REC_CHARS):
            text.append(_REC_CHARS[idx - 1])
        prev = idx
    return "".join(text)


def cache_template_bboxes(template_path):
    """对模板跑 det，把每个文字框缓存到 _TEMPLATE_BBOXES。"""
    global _TEMPLATE_BBOXES
    img = cv2.imread(template_path)
    if img is None:
        raise FileNotFoundError(template_path)
    boxes = _det_boxes(img)
    _TEMPLATE_BBOXES = []
    for (x0, y0, x1, y1) in boxes:
        crop = img[max(y0, 0):y1, max(x0, 0):x1]
        text = _rec_text(crop)
        # 扩 bbox 上下左右各 2px 防文字边缘残留
        h, w = img.shape[:2]
        ex = ((max(0, x0 - 2), max(0, y0 - 2),
               min(w, x1 + 2), min(h, y1 + 2)))
        _TEMPLATE_BBOXES.append({"bbox": ex, "text": text})
    print(f"[OCR] template cached: {len(_TEMPLATE_BBOXES)} text boxes", flush=True)


def get_template_bboxes():
    return list(_TEMPLATE_BBOXES)
