"""Image encoding/decoding/resizing utilities.

Uses OpenCV (cv2) when available for best performance, falls back to
Pillow on platforms where cv2 is unavailable (e.g. Android via Chaquopy).

All functions work with numpy RGB arrays (H, W, 3) uint8.
"""

import base64
import io
from pathlib import Path
from typing import Tuple

import numpy as np

try:
    import cv2

    _HAS_CV2 = True
except ImportError:
    _HAS_CV2 = False

try:
    from PIL import Image

    _HAS_PIL = True
except ImportError:
    _HAS_PIL = False


def _require_backend() -> None:
    if not _HAS_CV2 and not _HAS_PIL:
        raise ImportError(
            "Neither opencv-python-headless nor Pillow is installed. "
            "At least one is required for image operations."
        )


# ---------------------------------------------------------------------------
# Encode
# ---------------------------------------------------------------------------


def encode_jpeg(image: np.ndarray, quality: int = 85) -> bytes:
    """Encode an RGB numpy array as JPEG bytes."""
    if _HAS_CV2:
        bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
        ok, buf = cv2.imencode(".jpg", bgr, [cv2.IMWRITE_JPEG_QUALITY, quality])
        if not ok:
            raise RuntimeError("JPEG encoding failed")
        return buf.tobytes()

    if _HAS_PIL:
        img = Image.fromarray(image, "RGB")
        buf = io.BytesIO()
        img.save(buf, format="JPEG", quality=quality)
        return buf.getvalue()

    _require_backend()


def encode_jpeg_data_uri(image: np.ndarray, quality: int = 85) -> str:
    """Encode an RGB numpy array as a JPEG base64 data URI."""
    raw = encode_jpeg(image, quality)
    b64 = base64.b64encode(raw).decode("utf-8")
    return f"data:image/jpeg;base64,{b64}"


# ---------------------------------------------------------------------------
# Resize
# ---------------------------------------------------------------------------


def resize_image(image: np.ndarray, width: int, height: int) -> np.ndarray:
    """Resize an image to exact dimensions."""
    if _HAS_CV2:
        h, w = image.shape[:2]
        shrinking = (width * height) < (w * h)
        interp = cv2.INTER_AREA if shrinking else cv2.INTER_LANCZOS4
        return cv2.resize(image, (width, height), interpolation=interp)

    if _HAS_PIL:
        img = Image.fromarray(image, "RGB")
        resized = img.resize((width, height), Image.LANCZOS)
        return np.asarray(resized)

    _require_backend()


def thumbnail(image: np.ndarray, max_width: int) -> np.ndarray:
    """Create a thumbnail that fits within max_width, preserving aspect ratio."""
    h, w = image.shape[:2]
    if w <= max_width:
        return image.copy()
    scale = max_width / w
    new_w = max_width
    new_h = max(1, int(h * scale))
    return resize_image(image, new_w, new_h)


def resize_down(image: np.ndarray, max_width: int) -> np.ndarray:
    """Downscale if wider than max_width; return as-is otherwise."""
    h, w = image.shape[:2]
    if w <= max_width:
        return image
    scale = max_width / w
    new_w = max_width
    new_h = max(1, int(h * scale))
    return resize_image(image, new_w, new_h)


# ---------------------------------------------------------------------------
# Load
# ---------------------------------------------------------------------------


def load_image_file(path: str | Path) -> np.ndarray:
    """Load an image file and return as RGB numpy array."""
    path = str(path)

    if _HAS_CV2:
        bgr = cv2.imread(path, cv2.IMREAD_COLOR)
        if bgr is None:
            raise FileNotFoundError(f"Cannot load image: {path}")
        return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)

    if _HAS_PIL:
        img = Image.open(path).convert("RGB")
        return np.asarray(img)

    _require_backend()


def load_image_bytes(data: bytes) -> np.ndarray:
    """Decode image bytes (JPEG, PNG, etc.) and return as RGB numpy array."""
    if _HAS_CV2:
        arr = np.frombuffer(data, dtype=np.uint8)
        bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR)
        if bgr is None:
            raise ValueError("Cannot decode image data")
        return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)

    if _HAS_PIL:
        img = Image.open(io.BytesIO(data)).convert("RGB")
        return np.asarray(img)

    _require_backend()


# ---------------------------------------------------------------------------
# Info
# ---------------------------------------------------------------------------


def image_size(image: np.ndarray) -> Tuple[int, int]:
    """Return (width, height) of an image array."""
    return image.shape[1], image.shape[0]
