import logging
from ctypes import create_unicode_buffer, Array, c_char, addressof
from datetime import datetime, timedelta
from typing import cast, Optional

from mwcapture.libmwcapture import (
    mw_capture,
    mwcap_video_buffer_info,
    mwcap_video_frame_info,
    mw_video_signal_status,
    MW_SUCCEEDED,
    mw_video_capture_status,
    mw_device_time,
    MWCAP_VIDEO_DEINTERLACE_BLEND,
    MWCAP_VIDEO_ASPECT_RATIO_CROPPING,
    MWCAP_VIDEO_COLOR_FORMAT_UNKNOWN,
    MWCAP_VIDEO_QUANTIZATION_UNKNOWN,
    MWCAP_VIDEO_SATURATION_UNKNOWN,
)
from pymagewell.pro_capture_device.device_status import (
    TransferStatus,
    SignalStatus,
    SignalState,
    OnDeviceBufferStatus,
    FrameInfo,
    DeviceInitTime,
)
from pymagewell.events.device_events import (
    RegisterableEvent,
    SignalChangeEvent,
    FrameBufferingEvent,
    FrameBufferedEvent,
    FrameTransferCompleteEvent,
    TransferCompleteEvent,
    PartialFrameTransferCompleteEvent,
    TimerEvent,
)
from pymagewell.events.notification import Notification
from pymagewell.pro_capture_device.device_settings import (
    TransferMode,
    ProCaptureSettings,
    DEVICE_CLOCK_TICK_PERIOD_IN_SECONDS,
)
from pymagewell.pro_capture_device.pro_capture_device_impl import ProCaptureDeviceImpl
from pymagewell.pro_capture_device.device_interface import ProCaptureEvents
from pymagewell.exceptions import ProCaptureError


logger = logging.getLogger(__name__)


class ProCaptureDevice(ProCaptureDeviceImpl, mw_capture):
    """`ProCaptureDevice` represents a physical Magewell ProCapture device. It is intended to be controlled by a
    `ProCaptureDeviceController`.

    ProCaptureDevice configures and initiates frame acquisition and transfer. It is responsible for constructing and
    registering events with the Magewell driver. It also provides methods for accessing information about the video
    source connected to the device. ProCaptureDevice inherits from the `mw_capture` class provided by Magwell's
    Windows SDK.
    """

    def __init__(self, settings: ProCaptureSettings):
        """
        Args:
            settings: A `ProCaptureSettings` object containing the settings for the device.
        """
        ProCaptureDeviceImpl.__init__(self, settings)
        mw_capture.__init__(self)  # type: ignore

        self.mw_capture_init_instance()  # type: ignore
        self.mw_refresh_device()  # type: ignore
        self._channel = _create_channel(self)

        self._device_init_time = DeviceInitTime(
            device_time_in_s=self._get_device_time_in_s(), system_time_as_datetime=datetime.now()
        )

        self._timer = _FrameTimer(self, self._channel, self._register_timer_event(TimerEvent()))

        self._signal_change_event = cast(SignalChangeEvent, self._register_event(SignalChangeEvent()))

        self._frame_buffered_event = FrameBufferedEvent()
        self._frame_buffering_event = FrameBufferingEvent()

        if self._settings.transfer_mode == TransferMode.NORMAL:
            self._frame_buffered_event = cast(FrameBufferedEvent, self._register_event(self._frame_buffered_event))
            self._transfer_complete_event: TransferCompleteEvent = FrameTransferCompleteEvent()

        elif self._settings.transfer_mode == TransferMode.LOW_LATENCY:
            self._frame_buffering_event = cast(FrameBufferingEvent, self._register_event(self._frame_buffering_event))
            self._transfer_complete_event = PartialFrameTransferCompleteEvent()

        elif self._settings.transfer_mode.TIMER:
            self._transfer_complete_event = FrameTransferCompleteEvent()

    @property
    def events(self) -> ProCaptureEvents:
        """events property
        Returns:
            A `ProCaptureEvents` object containing handles to the events generated by the device during frame grabbing.
        """
        return ProCaptureEvents(
            transfer_complete=self._transfer_complete_event,
            signal_change=self._signal_change_event,
            frame_buffered=self._frame_buffered_event,
            frame_buffering=self._frame_buffering_event,
            timer_event=self._timer.event,
        )

    def _register_event(self, event: RegisterableEvent) -> RegisterableEvent:
        notification_handle = self.mw_register_notify(self._channel, event.win32_event, event.registration_token)  # type: ignore
        event.register(Notification(notification_handle, self._channel))
        return event

    def _register_timer_event(self, event: TimerEvent) -> TimerEvent:
        """The _FrameTimer class handles constructing TimerEvents and registering them here."""
        notification_handle = self.mw_register_timer(self._channel, event.win32_event)  # type: ignore
        event.register(Notification(notification_handle, self._channel))
        return event

    def schedule_timer_event(self) -> None:
        """For use if the TransferMode is TransferMode.TIMER, this method schedules a timer event to trigger the transfer of the
        next frame."""
        self._timer.schedule_timer_event(self._get_device_time_in_ticks())

    @property
    def buffer_status(self) -> OnDeviceBufferStatus:
        """buffer_status property
        Returns:
            A `OnDeviceBufferStatus` object containing information about the device's buffer.
        """
        buffer_info = mwcap_video_buffer_info()
        self.mw_get_video_buffer_info(self._channel, buffer_info)  # type: ignore
        return OnDeviceBufferStatus.from_mwcap_video_buffer_info(buffer_info)

    @property
    def frame_info(self) -> FrameInfo:
        """frame_info property
        Returns:
            A `FrameInfo` object containing information about the most recent frame.
        """
        frame_info = mwcap_video_frame_info()
        self.mw_get_video_frame_info(self._channel, self.buffer_status.last_buffered_frame_index, frame_info)  # type: ignore
        return FrameInfo.from_mwcap_video_frame_info(frame_info, self._device_init_time)

    @property
    def signal_status(self) -> SignalStatus:
        """signal_status property
        Returns:
            A `SignalStatus` object containing information about the signal connected to the device.
        """
        mw_signal_status = mw_video_signal_status()
        self.mw_get_video_signal_status(self._channel, mw_signal_status)  # type: ignore
        return SignalStatus.from_mw_video_signal_status(mw_signal_status)

    def start_grabbing(self) -> None:
        """Starts the hardware acquiring frames."""
        start_capture_result = self.mw_start_video_capture(self._channel, self.events.transfer_complete.win32_event)  # type: ignore
        if start_capture_result != MW_SUCCEEDED:
            raise ProCaptureError(f"Start capture failed (error code {start_capture_result}).")
        if self.signal_status.state == SignalState.NONE:
            logger.warning("Input signal status: None")
        elif self.signal_status.state == SignalState.UNSUPPORTED:
            logger.warning("Input signal status: Unsupported")
        elif self.signal_status.state == SignalState.LOCKING:
            logger.info("Input signal status: Locking")
        elif self.signal_status.state == SignalState.LOCKED:
            logger.info("Input signal status: Locked")

        # Exit if signal not locked
        if self.signal_status.state != SignalState.LOCKED:
            self.mw_stop_video_capture(self._channel)  # type: ignore
            self.shutdown()
            raise ProCaptureError("Signal not locked. It is likely that no video signal is present.")

    def stop_grabbing(self) -> None:
        """Stops the hardware acquiring frames."""
        self.mw_stop_video_capture(self._channel)  # type: ignore

    @property
    def transfer_status(self) -> TransferStatus:
        """transfer_status property
        Returns:
            A `TransferStatus` object containing information about the transfer of frames from the device to the PC,
            including the number of frames transferred, and the number of lines of the current frame that have been
            transferred.
        """
        mw_capture_status = mw_video_capture_status()
        self.mw_get_video_capture_status(self._channel, mw_capture_status)  # type: ignore
        return TransferStatus.from_mw_video_capture_status(mw_capture_status)

    def _get_device_time_in_ticks(self) -> mw_device_time:
        """Read a timestamp from the device."""
        time = mw_device_time()  # type: ignore
        result = self.mw_get_device_time(self._channel, time)  # type: ignore
        if result != MW_SUCCEEDED:
            raise ProCaptureError("Failed to read time from device")
        else:
            return time

    def _get_device_time_in_s(self) -> float:
        return int(self._get_device_time_in_ticks().m_ll_device_time.value) * DEVICE_CLOCK_TICK_PERIOD_IN_SECONDS

    def start_a_frame_transfer(self, frame_buffer: Array[c_char]) -> datetime:
        """start_a_frame_transfer starts the transfer of lines from the device to a buffer in PC memory.
        Args:
            frame_buffer (Array[c_char]): A buffer in PC memory to which the lines of the current frame will be
            transferred.
        Returns:
            The time (datetime.datetime) at which the transfer of the first line of the current frame started.
        """
        in_low_latency_mode = self.transfer_mode == TransferMode.LOW_LATENCY
        notify_size = self._settings.num_lines_per_chunk if in_low_latency_mode else 0

        seconds_since_init = self._get_device_time_in_s() - self._device_init_time.device_time_in_s
        frame_timestamp = self._device_init_time.system_time_as_datetime + timedelta(seconds=seconds_since_init)
        result = self.mw_capture_video_frame_to_virtual_address_ex(  # type: ignore
            hchannel=self._channel,
            iframe=self.buffer_status.last_buffered_frame_index,
            pbframe=addressof(frame_buffer),
            cbframe=self._settings.image_size_in_bytes,
            cbstride=self._settings.min_stride,
            bbottomup=False,  # this is True in the C++ example, but false in python example,
            pvcontext=0,
            dwfourcc=self._settings.color_format.value,  # color format of captured frames
            cx=self._settings.dimensions.cols,
            cy=self._settings.dimensions.rows,
            dwprocessswitchs=0,
            cypartialnotify=notify_size,
            hosdimage=0,
            posdrects=0,
            cosdrects=0,
            scontrast=100,
            sbrightness=0,
            ssaturation=100,
            shue=0,
            deinterlacemode=MWCAP_VIDEO_DEINTERLACE_BLEND,
            aspectratioconvertmode=MWCAP_VIDEO_ASPECT_RATIO_CROPPING,
            prectsrc=0,  # 0 in C++ example, but configured using CLIP settings in python example,
            prectdest=0,
            naspectx=0,
            naspecty=0,
            colorformat=MWCAP_VIDEO_COLOR_FORMAT_UNKNOWN,
            quantrange=MWCAP_VIDEO_QUANTIZATION_UNKNOWN,
            satrange=MWCAP_VIDEO_SATURATION_UNKNOWN,
        )
        if result != MW_SUCCEEDED:
            raise ProCaptureError(f"Frame grab failed with error code {result}")
        else:
            return frame_timestamp

    def shutdown(self) -> None:
        """shutdown releases the hardware resources used by the device."""
        self._timer.shutdown()
        self._signal_change_event.destroy()
        self._transfer_complete_event.destroy()
        self._frame_buffered_event.destroy()
        self._frame_buffering_event.destroy()

        self.mw_close_channel(self._channel)  # type: ignore
        self.mw_capture_exit_instance()  # type: ignore


def _create_channel(capturer: mw_capture) -> int:
    t_path = create_unicode_buffer(128)
    capturer.mw_get_device_path(0, t_path)  # type: ignore
    return capturer.mw_open_channel_by_path(t_path)  # type: ignore


class _FrameTimer:
    """If the devices transfer mode is "Timer", this class is used to generate events triggering the transfer of frames
    from the device."""

    def __init__(self, device: ProCaptureDevice, channel: int, event: TimerEvent):
        self._device = device
        self._channel = channel
        self._frame_expire_time: Optional[mw_device_time] = None
        self._timer_event = event

    @property
    def event(self) -> TimerEvent:
        return self._timer_event

    def schedule_timer_event(self, device_time_now: mw_device_time) -> None:

        if self._frame_expire_time is None:
            self._frame_expire_time = device_time_now
        self._frame_expire_time.m_ll_device_time.value += int(
            self._device.signal_status.frame_period_s / DEVICE_CLOCK_TICK_PERIOD_IN_SECONDS
        )

        if self._timer_event.is_registered:
            result = self._device.mw_schedule_timer(
                self._channel,
                self._timer_event.notification.handle,  # type: ignore
                self._frame_expire_time.m_ll_device_time,
            )
        else:
            raise ProCaptureError("Timer event not registered with device.")
        if result != MW_SUCCEEDED:
            raise ProCaptureError("Failed to schedule frame timer")

    def shutdown(self) -> None:
        self._timer_event.destroy()
