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

@brief Shared runner for all DeviceIoReceiver instances. Manages a single
       polling thread that calls read_once() and notify_once() for all
       registered receivers.

Copyright (C) Atmosic 2025
'''
from __future__ import annotations

import threading
from typing import Set
from logging import getLogger

from lib.polling_thread_base import PollingThreadBase

logger = getLogger(__name__)

class DeviceIoReceiverReadRunner(PollingThreadBase):
    """Shared runner for all DeviceIoReceiver instances.
    
    This is a module-level shared instance that manages a single polling
    thread for all receivers. Receivers can add/remove themselves and
    the runner automatically starts/stops as needed.
    """
    
    def __init__(self):
        PollingThreadBase.__init__(self)
        self._receivers: Set = set()
        self._receivers_lock = threading.Lock()
    
    def add_receiver(self, receiver) -> None:
        """Add a receiver to the polling loop."""
        is_first_receiver = False
        with self._receivers_lock:
            if receiver in self._receivers:
                return
            if len(self._receivers) == 0:
                is_first_receiver = True
            self._receivers.add(receiver)
            logger.debug(
                f"[ReadRunner {id(self)}] added receiver id={id(receiver)}"
                f" class={receiver.__class__.__name__}")
                
                # Auto-start if this is the first receiver
        if is_first_receiver and not self.is_alive():
            self.start()
            logger.debug(f"[ReadRunner {id(self)}] auto-started"
                         f" thread={self.name}")
    
    def remove_receiver(self, receiver) -> None:
        """Remove a receiver from the polling loop."""
        is_last_receiver = False

        with self._receivers_lock:
            if receiver not in self._receivers:
                return
            self._receivers.remove(receiver)
            logger.debug(
                f"[ReadRunner {id(self)}] removed receiver"
                f" id={id(receiver)} class={receiver.__class__.__name__}")
            if len(self._receivers) == 0:
                is_last_receiver = True

        # Call stop() outside the lock to avoid deadlock
        if is_last_receiver and self.is_alive():
            self.stop()
            logger.debug(f"[ReadRunner {id(self)}]"
                         f" DeviceIoReceiverRunner: auto-stopped")
    
    def polling(self) -> None:
        """Called by PollingThreadBase - poll all registered receivers once."""
        # Copy receivers to avoid holding lock during polling
        with self._receivers_lock:
            receivers = list(self._receivers)

        # Poll each receiver
        for receiver in receivers:
            try:
                receiver.read_once()
            except Exception as e:
                logger.error(
                    f"[ReadRunner {id(self)}] receiver id={id(receiver)}"
                    f" class={receiver.__class__.__name__} failed: {e}",
                    exc_info=True)



# Module-level shared instance
_shared_runner: DeviceIoReceiverReadRunner | None = None
_runner_lock = threading.Lock()


def get_shared_read_runner() -> DeviceIoReceiverReadRunner:
    """Get the shared DeviceIoReceiverRunner instance."""
    global _shared_runner
    if _shared_runner is None:
        with _runner_lock:
            if _shared_runner is None:  # Double-check locking
                _shared_runner = DeviceIoReceiverReadRunner()
    return _shared_runner
