"""Image → RGB565 conversion for ``push_widget_image`` and the emulator.

Off-app fallback using Pillow. ``source`` may be an http(s) URL, a local path,
raw image bytes, or a PIL ``Image``. Output is ``w*h*2`` bytes, RGB565
big-endian (high byte first) — the M1X widget image channel format. Pillow is an
optional dependency; without it this returns ``b""`` and callers no-op.
"""
from __future__ import annotations

import io
from typing import Optional


def _load_pil(source):
    from PIL import Image  # type: ignore
    if hasattr(source, "convert") and hasattr(source, "size"):
        return source  # already a PIL Image
    if isinstance(source, (bytes, bytearray)):
        return Image.open(io.BytesIO(bytes(source)))
    if isinstance(source, str):
        if source.startswith(("http://", "https://")):
            import urllib.request
            with urllib.request.urlopen(source, timeout=8) as r:  # nosec B310
                return Image.open(io.BytesIO(r.read()))
        return Image.open(source)
    raise TypeError(f"unsupported image source: {type(source)!r}")


def source_to_rgb565(source, w: int, h: int) -> Optional[bytes]:
    """Decode + resize + pack to RGB565 big-endian. ``b""`` if Pillow missing."""
    try:
        from PIL import Image  # noqa: F401
    except Exception:
        return b""
    try:
        img = _load_pil(source).convert("RGB").resize(
            (int(w), int(h)))
    except Exception:
        return b""
    out = bytearray()
    for r, g, b in img.getdata():
        v = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
        out.append((v >> 8) & 0xFF)   # big-endian: high byte first
        out.append(v & 0xFF)
    return bytes(out)


def rgb565_to_image(data: bytes, w: int, h: int):
    """Inverse of :func:`source_to_rgb565` — for emulator previews. PIL Image."""
    from PIL import Image
    img = Image.new("RGB", (int(w), int(h)))
    px = img.load()
    i = 0
    for y in range(int(h)):
        for x in range(int(w)):
            if i + 1 >= len(data):
                break
            v = (data[i] << 8) | data[i + 1]
            i += 2
            r = (v >> 11) & 0x1F
            g = (v >> 5) & 0x3F
            b = v & 0x1F
            px[x, y] = ((r * 255) // 31, (g * 255) // 63, (b * 255) // 31)
    return img


__all__ = ["source_to_rgb565", "rgb565_to_image"]


# ── Plugin icon image ────────────────────────────────────────────────────────
#: The one standard size every published plugin's icon is normalized to. Square,
#: so the app can cover-crop it into the wide Discover hero AND scale it down to a
#: small icon (installed list, compact views) from a single source.
ICON_SIZE = (512, 512)
CARD_IMAGE_SIZE = ICON_SIZE   # back-compat alias


def normalize_icon(source, size=ICON_SIZE) -> bytes:
    """Cover-crop + resize any image to a standard **square** PNG for the plugin
    icon. Returns PNG bytes, or ``b""`` if Pillow is missing or the source can't
    be decoded. Keeps every plugin's icon identical in dimensions so the store
    stays uniform, and one square source serves both the big card and a small
    scaled-down icon.
    """
    try:
        from PIL import Image  # type: ignore
        img = _load_pil(source).convert("RGB")
    except Exception:
        return b""
    tw, th = size
    sw, sh = img.size
    if sw <= 0 or sh <= 0:
        return b""
    scale = max(tw / sw, th / sh)                 # fill (cover), don't letterbox
    nw, nh = max(tw, int(sw * scale + 0.5)), max(th, int(sh * scale + 0.5))
    img = img.resize((nw, nh), Image.LANCZOS)
    left, top = (nw - tw) // 2, (nh - th) // 2    # center-crop
    img = img.crop((left, top, left + tw, top + th))
    buf = io.BytesIO()
    img.save(buf, format="PNG", optimize=True)
    return buf.getvalue()


#: Back-compat alias.
normalize_card_image = normalize_icon
