# ocr/engines.py
from dataclasses import dataclass
from typing import List, Tuple, Optional, Dict, Any
import numpy as np

# Optional imports
try:
    from paddleocr import PaddleOCR
    _HAS_PADDLE = True
except Exception:
    _HAS_PADDLE = False

try:
    import pytesseract
    from pytesseract import Output
    _HAS_TESS = True
except Exception:
    _HAS_TESS = False

# Paddle valid tokens (subset used by paddleocr release)
_VALID_PADDLE_LANGS = {
    "ch", "en", "korean", "japan", "chinese_cht",
    "ta", "te", "ka", "latin"
}

def _normalize_paddle_lang(lang: Optional[str]) -> str:
    """
    Normalize a PADDLE_LANG value into a PaddleOCR-supported token.
    If user provided language hints like 'multi', 'hi', 'bn' -> fallback to 'en'
    (PaddleOCR stable release doesn't necessarily ship hi/bn models).
    """
    if not lang:
        return "en"
    l = str(lang).strip().lower()
    if l in ("multi", "all", "mixed"):
        return "en"
    if l in ("hi", "hindi", "bn", "bengali", "ben", "bn-in", "bn_in"):
        return "en"
    if "," in l:
        for candidate in [c.strip() for c in l.split(",")]:
            if candidate in _VALID_PADDLE_LANGS:
                return candidate
    if l in _VALID_PADDLE_LANGS:
        return l
    return "en"

def _tesseract_lang_token(user_hint: Optional[str]) -> str:
    """
    Build a tesseract language token string.
    If user hint includes 'bn' or 'bengali' include Bengali; always include English.
    Examples: 'eng+ben' or just 'eng'.
    """
    if not user_hint:
        return "eng+ben"
    u = str(user_hint).lower()
    wants_bn = any(x in u for x in ("bn", "bengali", "ben"))
    # Always include English first
    if wants_bn:
        return "eng+ben"
    return "eng"

@dataclass
class OcrResult:
    text: str
    boxes: Optional[List[Tuple[int,int,int,int]]]
    engine: str
    raw: Optional[Dict[str,Any]] = None

class OCREngine:
    """
    Hybrid OCR engine:
     - Prefer PaddleOCR (layout & rotation robust) when configured.
     - Always fallback to Tesseract using combined 'eng+ben' when Paddle returns nothing or fails.
     - Defensive around missing libs / unsupported paddle languages.
    """
    def __init__(self, prefer: str = "tesseract", lang: str = "en", tesseract_cmd: Optional[str] = None):
        self.prefer = (prefer or "tesseract").lower()
        # normalize paddle language spec
        self.lang = _normalize_paddle_lang(lang)
        # choose tesseract language token (eng + ben if hint present)
        self._tess_lang_hint = _tesseract_lang_token(lang)
        if tesseract_cmd:
            try:
                import pytesseract
                pytesseract.pytesseract.tesseract_cmd = tesseract_cmd
            except Exception:
                pass

        self._paddle = None
        if self.prefer == "paddle" and _HAS_PADDLE:
            try:
                # try to create PaddleOCR instance with normalized language
                self._paddle = PaddleOCR(lang=self.lang, use_angle_cls=True, rec=True)
            except AssertionError:
                # fallback to english if provided lang is not accepted
                try:
                    self.lang = "en"
                    self._paddle = PaddleOCR(lang="en", use_angle_cls=True, rec=True)
                except Exception:
                    self._paddle = None
            except Exception:
                self._paddle = None

    def _run_paddle(self, np_rgb: np.ndarray) -> OcrResult:
        if not _HAS_PADDLE or self._paddle is None:
            return OcrResult(text="", boxes=None, engine="paddle-unavailable")
        try:
            # Paddle accepts numpy arrays (RGB). Many versions handle RGB fine.
            res = self._paddle.ocr(np_rgb, cls=True)
            lines = []
            boxes = []
            for page in res:
                for det in page:
                    try:
                        txt = det[1][0] if det and len(det) > 1 and len(det[1]) > 0 else ""
                    except Exception:
                        txt = ""
                    if txt:
                        lines.append(txt)
                    pts = det[0]
                    xs = [int(p[0]) for p in pts]; ys = [int(p[1]) for p in pts]
                    boxes.append((min(xs), min(ys), max(xs), max(ys)))
            return OcrResult(text="\n".join(lines).strip(), boxes=boxes if boxes else None, engine="paddle", raw={"paddle_raw": res})
        except Exception as e:
            return OcrResult(text="", boxes=None, engine="paddle-error", raw={"error": str(e)})

    def _run_tesseract(self, pil_image, lang_token: Optional[str] = None) -> OcrResult:
        if not _HAS_TESS:
            return OcrResult(text="", boxes=None, engine="tesseract-unavailable")
        try:
            # pick language token: either provided or derived from hint
            tess_lang = lang_token or self._tess_lang_hint or "eng+ben"
            # pytesseract image_to_data returns bounding boxes and text
            d = pytesseract.image_to_data(pil_image, output_type=Output.DICT, lang=tess_lang)
            texts = []
            boxes = []
            n = len(d.get("text", []))
            for i in range(n):
                s = (d["text"][i] or "").strip()
                if not s:
                    continue
                texts.append(s)
                left = int(d.get("left", [0]*n)[i])
                top = int(d.get("top", [0]*n)[i])
                width = int(d.get("width", [0]*n)[i])
                height = int(d.get("height", [0]*n)[i])
                boxes.append((left, top, left + width, top + height))
            return OcrResult(text="\n".join(texts).strip(), boxes=boxes if boxes else None, engine=f"tesseract({tess_lang})", raw={"tesseract_data": d})
        except Exception as e:
            return OcrResult(text="", boxes=None, engine="tesseract-error", raw={"error": str(e)})

    def run(self, pil_rgb=None, pil_bw=None, np_rgb=None, np_gray=None) -> OcrResult:
        # Normalize inputs so both paddle and tesseract can use them
        if pil_rgb is None and np_rgb is not None:
            from PIL import Image
            pil_rgb = Image.fromarray(np_rgb)
        if pil_bw is None and np_gray is not None:
            from PIL import Image
            pil_bw = Image.fromarray(np_gray)

        # If Paddle preferred and available, try it first
        if self.prefer == "paddle" and _HAS_PADDLE and self._paddle is not None:
            # ensure numpy rgb for paddle
            if np_rgb is None and pil_rgb is not None:
                np_rgb = np.array(pil_rgb)
            res = self._run_paddle(np_rgb)
            # If paddle produced text, return it
            if res.text:
                return res
            # otherwise fallback to tesseract (with eng+ben)
            fallback = self._run_tesseract(pil_bw or pil_rgb, lang_token=self._tess_lang_hint)
            # label fallback origin
            if fallback.text:
                fallback.engine = f"{fallback.engine}|fallback_from_paddle"
            return fallback

        # Default: use tesseract (with eng+ben)
        return self._run_tesseract(pil_bw or pil_rgb, lang_token=self._tess_lang_hint)
