"""CameraView — QWidget that renders camera frames and composites overlay layers.

Responsibilities:
    - Receive QVideoFrame, convert to QImage, schedule repaint.
    - In paintEvent: draw the frame (letterboxed) then call paint() on each
      registered IOverlayLayer in order.
    - Know nothing about what specific overlays draw — that is entirely up to
      each IOverlayLayer implementation.

Adding a new overlay (e.g. YOLO bboxes):
    layer = YoloBboxOverlay()
    camera_view.add_overlay_layer(layer)
    yolo_processor.results_ready.connect(layer.on_results)

No modification to CameraView is needed.
"""

from __future__ import annotations

import logging

from PySide6.QtCore import QRect, Qt, Slot
from PySide6.QtGui import QImage, QPainter
from PySide6.QtMultimedia import QVideoFrame
from PySide6.QtWidgets import QWidget

from app.overlay.overlay_layer import IOverlayLayer

logger = logging.getLogger(__name__)


class CameraView(QWidget):
    """
    Camera preview widget.

    Frame pipeline:
        CameraService.frame_ready(QVideoFrame)
            → CameraView.on_frame()     — convert to QImage, schedule update()
            → paintEvent()              — draw image + all overlay layers
    """

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self.setStyleSheet("background: black;")

        self._current_image: QImage | None = None
        self._video_rect: QRect = QRect()
        self._overlay_layers: list[IOverlayLayer] = []

    # ------------------------------------------------------------------
    # Overlay layer registry
    # ------------------------------------------------------------------

    def add_overlay_layer(self, layer: IOverlayLayer) -> None:
        """Register an overlay layer. Layers are painted in registration order."""
        if layer not in self._overlay_layers:
            self._overlay_layers.append(layer)
            logger.debug("Overlay layer added: %s", layer.name)

    def remove_overlay_layer(self, layer: IOverlayLayer) -> None:
        """Unregister an overlay layer."""
        try:
            self._overlay_layers.remove(layer)
            logger.debug("Overlay layer removed: %s", layer.name)
        except ValueError:
            pass

    def set_all_overlays_visible(self, visible: bool) -> None:
        """Show or hide all registered overlay layers at once."""
        for layer in self._overlay_layers:
            layer.visible = visible
        self.update()

    # ------------------------------------------------------------------
    # Frame input
    # ------------------------------------------------------------------

    @Slot(QVideoFrame)
    def on_frame(self, frame: QVideoFrame) -> None:
        """Receive a camera frame, convert to QImage, schedule repaint."""
        if not frame.isValid():
            return

        image = frame.toImage()
        if image.isNull():
            return

        # Convert to Format_RGB32 — QPainter's fastest path on all platforms
        if image.format() != QImage.Format.Format_RGB32:
            image = image.convertToFormat(QImage.Format.Format_RGB32)

        self._current_image = image
        self.update()

    # ------------------------------------------------------------------
    # Qt paint
    # ------------------------------------------------------------------

    def paintEvent(self, event) -> None:  # noqa: N802
        painter = QPainter(self)

        # 1. Background
        painter.fillRect(self.rect(), Qt.GlobalColor.black)

        # 2. Camera frame — letterboxed
        if self._current_image is not None:
            img = self._current_image
            self._video_rect = _letterbox_rect(
                img.width(), img.height(), self.width(), self.height()
            )
            painter.drawImage(self._video_rect, img)
        else:
            self._video_rect = self.rect()

        # 3. Overlay layers — each gets a clean painter state
        for layer in self._overlay_layers:
            if not layer.visible:
                continue
            painter.save()
            try:
                layer.paint(painter, self._video_rect)
            except Exception:
                logger.exception("Error painting overlay layer %s", layer.name)
            finally:
                painter.restore()

        painter.end()


# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------

def _letterbox_rect(img_w: int, img_h: int, widget_w: int, widget_h: int) -> QRect:
    """Return the largest rect that fits the image while preserving aspect ratio."""
    if img_w <= 0 or img_h <= 0:
        return QRect(0, 0, widget_w, widget_h)

    scale = min(widget_w / img_w, widget_h / img_h)
    draw_w = int(img_w * scale)
    draw_h = int(img_h * scale)
    x = (widget_w - draw_w) // 2
    y = (widget_h - draw_h) // 2
    return QRect(x, y, draw_w, draw_h)
