"""
SyncQueue allows for 2 threads to share data between them with threadsafety
ConditionVariables are used for synchronization to reducing polling and latency between the 
producer and the consumer

NOTE: This is meant to be used in a single producer single consumer setup.
"""

import copy
from collections import deque
from threading import Condition
from typing import List, TypeVar, Generic, Optional
from uuid import uuid4

# Default maximum queue size use -1 to set to "infinite"
DEFAULT_MAX_Q_SIZE = 1000

_T = TypeVar("_T")


class SyncQueueStopped(RuntimeError): pass


class SyncQueue(Generic[_T]):
    """
    Single Producer - Single Consumer synchronized data queue
    Producer should only call `push`
    Consumer should only call `sync_get_all`

    The SyncQueue has roughly <0.0002s of latency with ~0.1% CPU usage
    To achieve similar latency with a NonSyncQueue (polling), we need roughly ~10% CPU usage
    """

    def __init__(self, max_queue_size: int = DEFAULT_MAX_Q_SIZE):
        self._q = deque(maxlen=max_queue_size)
        self._condition = Condition()
        self.id = uuid4()
        self._stopped = False

    def __enter__(self):
        return self
    
    def __exit__(self, exception_type, exception_value, exception_traceback):
        self.stop()

    def stop(self):
        self._stopped = True

    def push(self, data: _T) -> None:
        with self._condition:
            if self._stopped:
                raise SyncQueueStopped()
            
            self._q.append(data)
            self._condition.notify_all()

    def sync_get_all(self, timeout: float = 1) -> Optional[List[_T]]:
        with self._condition:
            res = self._condition.wait_for(lambda: len(self._q) > 0 or self._stopped, timeout)
            if not res:
                return None
            
            if self._stopped:
                raise SyncQueueStopped()
            
            ret = [copy.deepcopy(ii) for ii in self._q]
            self._q.clear()
            return ret