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

@brief Read raw bytes from a DeviceIoEntry and broadcast to observers.
       Implements an observer pattern for byte streams with manual control.
       Threading is handled by DeviceIoReceiverRunner.

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

from deviceio.device_io_entry_base import DeviceIoEntryBase
from deviceio.byte_stream_observer import ByteStreamObserver
from deviceio.device_io_receiver_read_runner import get_shared_read_runner
from deviceio.device_io_receiver_notify_runner import get_shared_notify_runner
from lib.bytes_utils import bytes_to_hex

logger = getLogger(__name__)
LOG_READ = "MRR"
LOG_NOTIFY = "MRN"
class DeviceIoReceiver:
    """Broadcasts any bytes read from the IO entry to observers.

    - register()/unregister() to manage observers
    - read_once(): single read, append to internal buffer
    - notify_once(): drain buffer and broadcast to all observers
    - No threading - managed externally by DeviceIoReceiverRunner
    """

    def __init__(self, io_entry: DeviceIoEntryBase) -> None:
        self._io = io_entry
        self._observers: List[ByteStreamObserver] = []
        self._buf = bytearray()
        self._buf_lock = threading.Lock()  # 保護 buffer
        self._is_running = False

    def register(self, observer: ByteStreamObserver) -> None:
        if observer not in self._observers:
            self._observers.append(observer)
            logger.debug("DeviceIoReceiver: registered "
                           f"{observer.__class__.__name__}")

    def unregister(self, observer: ByteStreamObserver) -> None:
        if observer in self._observers:
            self._observers.remove(observer)
            logger.debug("DeviceIoReceiver: unregistered "
                           f"{observer.__class__.__name__}")

    def clear_observers(self) -> None:
        self._observers.clear()

    def _notify(self, data: bytes) -> None:
        for obs in self._observers:
            try:
                # Skip observers that are paused
                if not obs.is_receiving():
                    continue
                obs.on_bytes(data)
            except Exception as e:  # keep other observers running
                logger.error(f"Observer {obs.__class__.__name__} failed: "
                               f"{e}", exc_info=True)

    def read_once(self) -> None:
        """Read from IO and append to internal buffer. Return bytes read."""
        if not self._io.can_read():
            return
        data = self._io.read()
        if not data:
            return
        if not isinstance(data, (bytes, bytearray)):
            logger.warning("DeviceIoReceiver: non-bytes data ignored")
            return
        with self._buf_lock:
            self._buf.extend(data)
            logger.debug(f"[{LOG_READ}] {bytes_to_hex(data)}")

    def notify_once(self) -> None:
        """Drain accumulated bytes and broadcast; return bytes broadcasted."""
        if not self._buf:
            return
        with self._buf_lock:
            chunk = bytes(self._buf)
            self._buf.clear()
        logger.debug(f"[{LOG_NOTIFY}] {bytes_to_hex(chunk)}")
        self._notify(chunk)

    def start(self) -> None:
        """Start receiving by registering with the shared runner."""
        if self._is_running:
            return

        read_runner = get_shared_read_runner()
        read_runner.add_receiver(self)
        notify_runner = get_shared_notify_runner()
        notify_runner.add_receiver(self)
        self._is_running = True
        logger.debug(f"DeviceIoReceiver {id(self)}: started")

    def stop(self) -> None:
        """Stop receiving by unregistering from the shared runner."""
        if not self._is_running:
            return

        read_runner = get_shared_read_runner()
        read_runner.remove_receiver(self)
        notify_runner = get_shared_notify_runner()
        notify_runner.remove_receiver(self)
        self._is_running = False
        logger.debug(f"DeviceIoReceiver {id(self)}: stopped")

    def is_running(self) -> bool:
        """Check if this receiver is currently running."""
        return self._is_running



