from PyQt5.QtCore import Qt, QSize, QTimer, pyqtSignal
from PyQt5.QtGui import QColor, QImage, QPainter, QPixmap
from PyQt5.QtMultimedia import (
    QAbstractVideoBuffer,
    QAbstractVideoSurface,
    QCamera,
    QCameraInfo,
    QCameraViewfinderSettings,
    QVideoFrame,
)
from PyQt5.QtWidgets import (
    QFrame,
    QLabel,
    QSizePolicy,
    QVBoxLayout,
)


class KinectVideoSurface(QAbstractVideoSurface):
    """Receives converted RGB frames without recording them."""

    frame_ready = pyqtSignal(QImage)

    def supportedPixelFormats(
        self,
        handle_type=QAbstractVideoBuffer.NoHandle,
    ):
        if handle_type != QAbstractVideoBuffer.NoHandle:
            return []

        return [
            QVideoFrame.Format_RGB32,
            QVideoFrame.Format_ARGB32,
            QVideoFrame.Format_RGB24,
        ]

    def present(self, frame):
        if not frame.isValid():
            return True

        formats = {
            QVideoFrame.Format_RGB32: QImage.Format_RGB32,
            QVideoFrame.Format_ARGB32: QImage.Format_ARGB32,
            QVideoFrame.Format_RGB24: QImage.Format_RGB888,
        }

        image_format = formats.get(frame.pixelFormat())

        if image_format is None:
            return False

        if not frame.map(QAbstractVideoBuffer.ReadOnly):
            return False

        try:
            pointer = frame.bits()
            pointer.setsize(frame.mappedBytes())

            image = QImage(
                pointer,
                frame.width(),
                frame.height(),
                frame.bytesPerLine(),
                image_format,
            ).copy()
        finally:
            frame.unmap()

        if image.isNull():
            return False

        self.frame_ready.emit(image)
        return True


class KinectCameraTile(QFrame):
    """Live Kinect preview. Frames are never recorded or retained."""

    def __init__(self, parent=None):
        super().__init__(parent)

        self.camera = None
        self.last_frame = None
        self._local_blink_closed = False

        self.setMinimumWidth(210)
        self.setMaximumWidth(280)
        self.setMinimumHeight(160)
        self.setMaximumHeight(176)
        self.setSizePolicy(
            QSizePolicy.Expanding,
            QSizePolicy.Fixed,
        )

        self.setStyleSheet(
            "QFrame {"
            "background:#03070d;"
            "border:2px solid #8f35ff;"
            "border-radius:7px;"
            "}"
        )

        layout = QVBoxLayout(self)
        layout.setContentsMargins(3, 2, 3, 3)
        layout.setSpacing(2)

        title = QLabel("KINECT VISION")
        title.setAlignment(Qt.AlignCenter)
        title.setStyleSheet(
            "color:#00ff88;"
            "border:none;"
            "font-size:10px;"
            "font-weight:bold;"
        )

        self.preview = QLabel("STARTING KINECT")
        self.preview.setAlignment(Qt.AlignCenter)
        self.preview.setMinimumHeight(125)
        self.preview.setStyleSheet(
            "background:#000;"
            "color:#70558f;"
            "border:1px solid #00ff88;"
            "font-size:9px;"
            "font-weight:bold;"
        )

        self.status = QLabel("STANDBY")
        self.status.setAlignment(Qt.AlignCenter)
        self.status.setStyleSheet(
            "color:#ffaa33;"
            "border:none;"
            "font-size:9px;"
            "font-weight:bold;"
        )

        self.surface = KinectVideoSurface(self)
        self.surface.frame_ready.connect(self._display_frame)

        layout.addWidget(title)
        layout.addWidget(self.preview, 1)
        layout.addWidget(self.status)

    def start_camera(self):
        if (
            self.camera
            and self.camera.state() == QCamera.ActiveState
        ):
            return

        camera_info = next(
            (
                item
                for item in QCameraInfo.availableCameras()
                if "Xbox NUI" in item.description()
            ),
            None,
        )

        if camera_info is None:
            self.status.setText("KINECT OFFLINE")
            return

        if self.camera is None:
            self.camera = QCamera(camera_info, self)
            self.camera.setCaptureMode(QCamera.CaptureVideo)

            settings = QCameraViewfinderSettings()
            settings.setResolution(QSize(640, 480))
            settings.setMinimumFrameRate(15.0)
            settings.setMaximumFrameRate(15.0)
            settings.setPixelFormat(QVideoFrame.Format_RGB32)

            self.camera.setViewfinderSettings(settings)
            self.camera.setViewfinder(self.surface)
            self.camera.stateChanged.connect(
                self._state_changed
            )
            self.camera.error.connect(
                self._camera_error
            )

        self.preview.setText("STARTING KINECT")
        self.status.setText("STARTING")
        self.camera.start()

    def stop_camera(self):
        if self.camera:
            self.camera.stop()

        self.status.setText("STANDBY")

    def begin_local_blink(self):
        """Close the local AIOS preview eyelids only."""
        self._local_blink_closed = True
        self._refresh_preview()

    def end_local_blink(self):
        """Reopen the local AIOS preview eyelids."""
        self._local_blink_closed = False
        self._refresh_preview()

    def _display_frame(self, image):
        self.last_frame = image
        self._refresh_preview()
        self.status.setText("LIVE • NO RECORDING")

    def _refresh_preview(self):
        if self.last_frame is None:
            return

        pixmap = QPixmap.fromImage(self.last_frame)
        pixmap = pixmap.scaled(
            self.preview.size(),
            Qt.KeepAspectRatio,
            Qt.SmoothTransformation,
        )

        if self._local_blink_closed:
            blinked = QPixmap(pixmap.size())
            blinked.fill(QColor("#010104"))

            painter = QPainter(blinked)
            painter.drawPixmap(0, 0, pixmap)

            width = pixmap.width()
            height = pixmap.height()
            centre = height // 2
            slit = max(2, height // 22)
            eyelid = QColor(1, 1, 4, 252)

            painter.fillRect(
                0,
                0,
                width,
                max(0, centre - slit),
                eyelid,
            )
            painter.fillRect(
                0,
                centre + slit,
                width,
                max(0, height - centre - slit),
                eyelid,
            )
            painter.end()

            pixmap = blinked

        self.preview.setText("")
        self.preview.setPixmap(pixmap)

    def _state_changed(self, state):
        if state == QCamera.ActiveState:
            self.status.setText("CAMERA ACTIVE")
        elif state == QCamera.UnloadedState:
            self.status.setText("STANDBY")

    def _camera_error(self, _error):
        message = (
            self.camera.errorString()
            if self.camera
            else "Camera error"
        )

        self.status.setText(
            f"ERROR: {message[:28]}"
        )

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self._refresh_preview()

    def showEvent(self, event):
        super().showEvent(event)
        QTimer.singleShot(250, self.start_camera)

    def hideEvent(self, event):
        self.stop_camera()
        super().hideEvent(event)
