#!/usr/bin/env python
'''
@file device_io_module.py

@brief DeviceIoModule: combines DeviceIoEntry with DeviceIoReceiver for
       immediate byte pulling and observer broadcasting. Provides a higher-level
       interface than raw DeviceIoEntry while maintaining observer pattern.

Copyright (C) Atmosic 2025
'''
from __future__ import annotations
import time
import threading
from logging import getLogger

from deviceio.device_io_entry_base import DeviceIoEntryBase
from deviceio.device_io_receiver import DeviceIoReceiver
from deviceio.byte_stream_observer import ByteStreamObserver
from deviceio.device_io_read_detector_base import DeviceIoReadDetectorBase
from lib.bytes_utils import bytes_to_hex
from error.atmosic_error import DeviceIoModuleError
from error.errorcodes import DeviceIoModuleErrorCode

logger = getLogger(__name__)
LOG_INNER_ON_BYTE = "MIR"
LOG_READ = "MR"
LOG_WRITE = "MW"

class DeviceIoModule:
    """Higher-level I/O module that combines entry + receiver.

    - Wraps a DeviceIoEntry (e.g., SerialIoEntry) for basic I/O
    - Manages a DeviceIoReceiver for immediate byte pulling and broadcasting
    - Provides observer registration for byte stream events
    - Internal observer collects bytes into buffer for read() method
    """

    class _InternalObserver(ByteStreamObserver):
        """Internal observer that collects bytes into module's buffer."""

        def __init__(self, module:DeviceIoModule):
            super().__init__()
            self._module = module

        def on_bytes(self, data: bytes) -> None:
            """Collect received bytes into module's buffer."""
            logger.debug(f"[{LOG_INNER_ON_BYTE}] {bytes_to_hex(data)}")
            with self._module._buffer_lock:
                # Log both length and hex representation of received bytes
                self._module._received_buffer.extend(data)
                logger.debug(f"[{LOG_INNER_ON_BYTE}] {bytes_to_hex(data)},"
                             f" len: {len(data)}")

    def __init__(self, entry: DeviceIoEntryBase,
                 read_poll_interval: float = 0.001):
        self._entry = entry
        self._received_buffer = bytearray()
        self._buffer_lock = threading.Lock()
        self._read_poll_interval = read_poll_interval
        self._receiver = DeviceIoReceiver(entry)
        self._internal_observer = self._InternalObserver(self)
        self._receiver.register(self._internal_observer)

    def write(self, data) -> int:
        """Write data to the device."""
        logger.debug(f"[{LOG_WRITE}] {bytes_to_hex(data)}, len: {len(data)}")
        return self._entry.write(data)
    
    def _pop_msg(self, data: bytes, is_pop_buffer: bool) -> bytes:
        """Pop all bytes from the internal buffer."""
        pop_msg = ", not popped" if not is_pop_buffer else ""
        logger.debug(
            f"[{LOG_READ}] {bytes_to_hex(data)}, len: {len(data)} {pop_msg}")
        return data

    def read(self, size: int = 0, timeout: float = 0,
             is_pop_buffer: bool = True,
             read_detector: DeviceIoReadDetectorBase | None = None,
             ) -> bytes:
        # Validate parameter combinations
        if size != 0 and read_detector is not None:
            raise DeviceIoModuleError(
                "Cannot specify both size and read_detector parameters",
                DeviceIoModuleErrorCode.READ_AUGUMENT_ERROR)

        # Allow immediate return of available buffer without parameters
        if size == 0 and read_detector is None and timeout == 0:
            with self._buffer_lock:
                if not self._received_buffer:
                    raise DeviceIoModuleError(
                        "No data available in buffer for immediate read",
                        DeviceIoModuleErrorCode.READ_IMMEDIATE_NO_DATA)
                data = bytes(self._received_buffer)
                if is_pop_buffer:
                    self._received_buffer.clear()
                self._pop_msg(data, is_pop_buffer)
                return data

        # Handle timeout-only case: sleep then return all available data
        if size == 0 and read_detector is None:
            assert timeout is not None  # Guaranteed by validation above
            time.sleep(timeout)
            with self._buffer_lock:
                if not self._received_buffer:
                    raise DeviceIoModuleError(
                        "Timeout waiting for data",
                        DeviceIoModuleErrorCode.READ_TIMEOUT)
                data = bytes(self._received_buffer)
                if is_pop_buffer:
                    self._received_buffer.clear()
                self._pop_msg(data, is_pop_buffer)
                return data

        # Handle read_detector-based checking (with optional timeout)
        is_first_read = True
        if read_detector is not None:
            start_time = time.time()
            while True:
                # Check timeout first (if specified)
                if timeout != 0:
                    elapsed = time.time() - start_time
                    if elapsed >= timeout:
                        raise DeviceIoModuleError(
                            "Timeout waiting for data",
                            DeviceIoModuleErrorCode.READ_TIMEOUT)

                if not is_first_read:
                    time.sleep(self._read_poll_interval)
                is_first_read = False

                with self._buffer_lock:
                    drop_size = read_detector.detect_drop_size(
                        self._received_buffer)
                    if drop_size > 0:
                        del self._received_buffer[:drop_size]
                        logger.debug(
                            f"[{LOG_READ}] Dropped {drop_size} bytes,"
                            f" buffer has {len(self._received_buffer)} bytes")
                    count = read_detector.detect_read_size(
                        self._received_buffer)
                    if count == 0:
                        continue
                    if count <= len(self._received_buffer):
                        data = bytes(self._received_buffer[:count])
                        if is_pop_buffer:
                            del self._received_buffer[:count]
                        self._pop_msg(data, is_pop_buffer)
                        return data
                    size = count
                    break

        # Handle size-based reading (with optional timeout)
        start_time = time.time()
        is_first_read = True
        while True:
            # Check timeout first (if specified)
            if timeout != 0:
                elapsed = time.time() - start_time
                if elapsed >= timeout:
                    raise DeviceIoModuleError(
                        f"Timeout({timeout}s) waiting for data",
                        DeviceIoModuleErrorCode.READ_TIMEOUT)

            if not is_first_read:
                time.sleep(self._read_poll_interval)
            is_first_read = False

            # Check if we have enough data
            with self._buffer_lock:
                if len(self._received_buffer) < size:
                    continue
                data = bytes(self._received_buffer[:size])
                if is_pop_buffer:
                    del self._received_buffer[:size]
                self._pop_msg(data, is_pop_buffer)
                return data

    def register_observer(self, observer: ByteStreamObserver) -> None:
        """Register an observer for byte stream events."""
        self._receiver.register(observer)

    def unregister_observer(self, observer: ByteStreamObserver) -> None:
        """Unregister an observer from byte stream events."""
        self._receiver.unregister(observer)

    def clear_observers(self) -> None:
        """Clear all registered observers (except internal observer)."""
        # Note: We don't clear the internal observer, only external ones
        self._receiver.clear_observers()
        # Re-register our internal observer
        self._receiver.register(self._internal_observer)

    def _start_receiver(self) -> None:
        """Internal method to start the receiver."""
        self._receiver.start()

    def _stop_receiver(self) -> None:
        """Internal method to stop the receiver."""
        self._receiver.stop()

    def clear_read_buffer(self) -> None:
        """Clear the internal receive buffer."""
        self._entry.clear_read_buffer()
        with self._buffer_lock:
            self._received_buffer.clear()
            logger.debug(f"[{LOG_READ}] Clean the buffer in device IO")

    def can_read(self) -> bool:
        """Check if the serial port can be read from."""
        return self._entry.can_read()

    def can_write(self) -> bool:
        """Check if the serial port can be written to."""
        return self._entry.can_write()

    # Property access to underlying components (if needed)
    @property
    def entry(self) -> DeviceIoEntryBase:
        """Access to underlying entry for advanced use cases."""
        return self._entry

    @property
    def receiver(self) -> DeviceIoReceiver:
        """Access to underlying receiver for advanced use cases."""
        return self._receiver
