"""Single-owner Kinect RGB service for SHiRE Vision consumers."""

from PyQt5.QtCore import QObject, QSize, pyqtSignal
from PyQt5.QtGui import QImage
from PyQt5.QtMultimedia import (
    QAbstractVideoBuffer,
    QAbstractVideoSurface,
    QCamera,
    QCameraInfo,
    QCameraViewfinderSettings,
    QVideoFrame,
)


class SharedKinectSurface(QAbstractVideoSurface):
    """Converts Kinect frames into detached RGB QImages."""

    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 KinectVisionService(QObject):
    """Owns the Kinect once and distributes frames to subscribers."""

    frame_ready = pyqtSignal(QImage)
    status_changed = pyqtSignal(dict)
    camera_error = pyqtSignal(str)

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

        self.camera = None
        self.surface = SharedKinectSurface(self)
        self.surface.frame_ready.connect(
            self._receive_frame
        )

        self.consumers = set()
        self.last_frame = None
        self.frame_sequence = 0
        self.last_error = None

    @staticmethod
    def _consumer_id(value):
        identifier = " ".join(
            str(value).strip().split()
        )

        if not identifier:
            raise ValueError(
                "Vision consumer ID is required."
            )

        return identifier

    def acquire(self, consumer_id, start=True):
        identifier = self._consumer_id(consumer_id)
        self.consumers.add(identifier)

        if start:
            self.start_camera()

        self.status_changed.emit(self.snapshot())
        return self.snapshot()

    def release(self, consumer_id):
        identifier = self._consumer_id(consumer_id)
        self.consumers.discard(identifier)

        if not self.consumers:
            self.stop_camera()

        self.status_changed.emit(self.snapshot())
        return self.snapshot()

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

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

        if camera_info is None:
            self.last_error = "Kinect camera not found"
            self.camera_error.emit(self.last_error)
            self.status_changed.emit(self.snapshot())
            return self.snapshot()

        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._camera_state_changed
            )
            self.camera.error.connect(
                self._camera_failed
            )

        self.last_error = None
        self.camera.start()
        return self.snapshot()

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

        # Idle frames must not be retained.
        self.last_frame = None
        self.status_changed.emit(self.snapshot())
        return self.snapshot()

    def _receive_frame(self, image):
        self.last_frame = image
        self.frame_sequence += 1
        self.frame_ready.emit(image)

    def _camera_state_changed(self, _state):
        self.status_changed.emit(self.snapshot())

    def _camera_failed(self, _error):
        self.last_error = (
            self.camera.errorString()
            if self.camera
            else "Kinect camera error"
        )
        self.camera_error.emit(self.last_error)
        self.status_changed.emit(self.snapshot())

    def snapshot(self):
        state = (
            self.camera.state()
            if self.camera
            else QCamera.UnloadedState
        )

        return {
            "camera_owned": self.camera is not None,
            "camera_active": (
                state == QCamera.ActiveState
            ),
            "consumer_count": len(self.consumers),
            "consumers": sorted(self.consumers),
            "frame_sequence": self.frame_sequence,
            "frame_retained": self.last_frame is not None,
            "last_error": self.last_error,
            "resolution": [640, 480],
            "frame_rate": 15,
            "recording": False,
        }


_shared_kinect_vision_service = None


def get_kinect_vision_service():
    """Return the one process-wide Kinect camera owner."""

    global _shared_kinect_vision_service

    if _shared_kinect_vision_service is None:
        _shared_kinect_vision_service = KinectVisionService()

    return _shared_kinect_vision_service
