import logging
from copy import deepcopy
from typing import List, Iterator
from acmenra_cv.tracker.timed_point import TimedPoint


logger = logging.getLogger(__name__)


class TimedPointQueue:
    """
    Fixed-length queue for tracking spatial TimedPoints with O(1) running average calculation.

    A specialized, memory-efficient queue designed for real-time trajectory management in
    computer vision pipelines. Maintains a chronological history of spatial points while
    incrementally computing centroid coordinates without storing full history for averaging.

    Key features:
    - Fixed-capacity FIFO queue with automatic eviction of oldest points
    - O(1) incremental running average calculation for X, Y, Z coordinates
    - Safe external data retrieval via deep-copy snapshot
    - Efficient memory usage with configurable max_length
    - Full iterator and indexing protocol support

    Designed for seamless integration with downstream processing pipelines such as:
    - Multi-object tracking (MOT) trajectory smoothing
    - Velocity and direction estimation from historical positions
    - Real-time motion analysis and predictive filtering
    - ADAS zone crossing and trajectory validation

    Attributes:
        _max_length (int): Maximum number of TimedPoints retained before automatic eviction.
        _coordinates (List[TimedPoint]): Internal chronological storage of points.
        _average_x, _average_y, _average_z (float): Incrementally updated running averages.

    Note:
        - All coordinate averages are updated incrementally (O(1) complexity)
        - Not inherently thread-safe for concurrent mutations; use external locks if needed
        - `get_values()` returns a deep copy to prevent external state mutation
        - Queue automatically discards oldest points when capacity is exceeded
    """

    def __init__(self, max_length: int) -> None:
        """
        Initializes a TimedPointQueue with a fixed maximum capacity.

        Args:
            max_length (int): Maximum number of TimedPoints to retain. Must be non-negative.

        Raises:
            TypeError: If max_length is not an integer or is a boolean.
            ValueError: If max_length is negative.

        Note:
            - Determines trajectory history length (older points are automatically evicted)
            - Affects memory footprint and average calculation precision
            - Default averages are initialized to 0.0
        """
        self.max_length = max_length
        self._coordinates: List[TimedPoint] = []
        self._average_x = 0.0
        self._average_y = 0.0
        self._average_z = 0.0

    def __len__(self) -> int:
        """
        Returns the current number of TimedPoints in the queue.

        Returns:
            int: Current queue size (0 to max_length).

        Note:
            - Maps to built-in `len()` function
            - O(1) operation
        """
        return len(self._coordinates)

    def __iter__(self) -> Iterator[TimedPoint]:
        """
        Returns an iterator over the TimedPoints in chronological order.

        Returns:
            Iterator[TimedPoint]: Iterator yielding points from oldest to newest.

        Note:
            - Enables usage in for-loops and comprehensions: `for point in queue:`
            - Iteration order: chronological (index 0 = oldest)
        """
        return iter(self._coordinates)

    def __getitem__(self, index: int) -> TimedPoint:
        """
        Gets the TimedPoint at the specified position in the queue.

        Args:
            index (int): Position in the queue. Supports negative indices (-1 = newest).

        Returns:
            TimedPoint: The TimedPoint at the requested index.

        Raises:
            IndexError: If index is out of valid range.

        Note:
            - Direct indexing without copying
            - Useful for accessing specific historical points: `queue[0]` (oldest), `queue[-1]` (newest)
        """
        return self._coordinates[index]

    @property
    def max_length(self) -> int:
        """
        int: Gets the maximum capacity of the queue.

        Returns:
            int: Maximum number of TimedPoints retained before automatic eviction.
        """
        return self._max_length

    @max_length.setter
    def max_length(self, max_length: int) -> None:
        """
         Sets the maximum queue length with validation.

         Args:
             max_length (int): New maximum capacity. Must be non-negative.

         Raises:
             TypeError: If max_length is not an integer.
             ValueError: If max_length is negative.

         Note:
             - Reducing capacity does NOT automatically truncate existing points
             - New limit applies to subsequent `enqueue()` operations
         """
        if not isinstance(max_length, int) or isinstance(max_length, bool):
            raise TypeError(f"'max_length' must be 'int', but got {type(max_length).__name__}")
        if max_length < 0:
            raise ValueError(f"'max_length' must be >= 0")
        self._max_length = max_length

    @property
    def average_x(self) -> float:
        """
        float: Gets the running average of X coordinates.

        Returns:
            float: Incrementally calculated X centroid (0.0 for empty queue).

        Note:
            - O(1) update complexity during enqueue/dequeue
            - Reflects centroid of currently retained points
        """
        return self._average_x

    @property
    def average_y(self) -> float:
        """
        float: Gets the running average of Y coordinates.

        Returns:
            float: Incrementally calculated Y centroid (0.0 for empty queue).

        Note:
            - O(1) update complexity during enqueue/dequeue
            - Reflects centroid of currently retained points
        """
        return self._average_y

    @property
    def average_z(self) -> float:
        """
        float: Gets the running average of Z coordinates.

        Returns:
            float: Incrementally calculated Z centroid (0.0 for empty queue).

        Note:
            - O(1) update complexity during enqueue/dequeue
            - Typically remains 0.0 for 2D image tracking
        """
        return self._average_z

    @property
    def count(self) -> int:
        """
        int: Gets the current number of TimedPoints in the queue.

        Returns:
            int: Alias for `len(self)`.
        """
        return len(self._coordinates)

    def enqueue(self, value: TimedPoint) -> None:
        """
        Adds a new TimedPoint to the queue, updating running averages.

        If the queue is at maximum capacity, the oldest TimedPoint is automatically
        evicted before adding the new one.

        Args:
            value (TimedPoint): The spatial-temporal point to add.

        Note:
            - Updates `_average_x/y/z` incrementally in O(1) time
            - Automatically triggers `dequeue()` if capacity is exceeded
            - FIFO order is maintained (newest appended to end)
        """
        self._average_x = (self._average_x * self.count + value.X) / (self.count + 1)
        self._average_y = (self._average_y * self.count + value.Y) / (self.count + 1)
        self._average_z = (self._average_z * self.count + value.Z) / (self.count + 1)
        self._coordinates.append(value)
        if len(self._coordinates) > self.max_length:
            self.dequeue()

    def dequeue(self) -> TimedPoint:
        """
        Removes and returns the oldest TimedPoint from the queue.

        Updates running averages to reflect the removal. If this is the last
        point in the queue, averages are reset to 0.0.

        Returns:
            TimedPoint: The oldest evicted TimedPoint.

        Raises:
            IndexError: If called on an empty queue.

        Note:
            - Automatically adjusts incremental averages to maintain accuracy
            - Called internally by `enqueue()` when `max_length` is exceeded
            - O(N) operation due to `list.pop(0)` shifting; acceptable for typical trajectory lengths (<50)
        """
        if not self._coordinates:
            raise IndexError("Cannot dequeue from empty queue")

        if self.count > 1:
            self._average_x = (self._average_x * self.count - self._coordinates[0].X) / (self.count - 1)
            self._average_y = (self._average_y * self.count - self._coordinates[0].Y) / (self.count - 1)
            self._average_z = (self._average_z * self.count - self._coordinates[0].Z) / (self.count - 1)
        else:
            self._average_x = 0.0
            self._average_y = 0.0
            self._average_z = 0.0
        return self._coordinates.pop(0)

    def get_values(self) -> List[TimedPoint]:
        """
        Returns a deep copy of all TimedPoints in chronological order.

        Returns:
            List[TimedPoint]: Snapshot of current queue contents (oldest to newest).

        Note:
            - Deep copy ensures external modifications do not affect internal state
            - Safe for external processing, serialization, or visualization
            - O(N) time and space complexity
        """
        return deepcopy(self._coordinates)

    def get_average(self) -> TimedPoint:
        """
        Calculates and returns the average position of all TimedPoints in the queue.

        Returns:
            TimedPoint: Centroid point with coordinates `(average_x, average_y, average_z)`.
                Timestamp is not set as it represents a statistical aggregate.

        Note:
            - Returns `TimedPoint(0.0, 0.0, 0.0)` for empty queues
            - Computed incrementally; O(1) retrieval complexity
            - Useful for trajectory smoothing or center-of-mass tracking
        """
        return TimedPoint.safe(x=self._average_x, y=self._average_y, z=self._average_z)
