# acmenra_cv/inference/result.py

import logging
from enum import Enum, EnumType, EnumMeta
from typing import Any, List, Dict, Optional, Iterator

from acmenra_cv.inference.timing import Timing
from acmenra_cv.instance import Point, Box, Obb, Polygon, Instance,  DeviceType, TaskType


logger = logging.getLogger(__name__)


class Result:
    """
    Universal result container for 2D/3D computer vision inference backends.

    This class provides a standardized output format that all inference backends
    (YOLO, custom models, future depth-aware models) must return. By enforcing a common
    interface, downstream components (Tracker, Drawer, business logic) can work
    with any backend without modification.

    Key design decisions (contracts):
    - Spatial dimensions (width, height, depth) are strictly normalized [0.0, 1.0].
    - Timing data supports partial measurement (None) for backends without profiling.
    - Strict type validation on all property assignments to prevent silent pipeline failures.
    - Full JSON serialization support (to_dict/from_dict) for Outbox/Analytics pipelines.

    All coordinates within contained `Instance` objects are maintained in normalized
    space (0.0-1.0) relative to these dimensions, making results resolution-independent.

    Attributes:
        _instances (List[Instance]): Detected objects with normalized coordinates.
        _timing (Timing): Execution time breakdown for pipeline stages.
        _width (float): Normalized frame width (typically 1.0).
        _height (float): Normalized frame height (typically 1.0).
        _depth (float): Normalized frame depth (1.0 for 2D, <1.0 for 3D/depth cameras).
        _device (DeviceType): Hardware backend used for inference.
        _category (Optional[EnumType]): Semantic category enumeration class.
    """

    def __init__(self,
                 instances: List[Instance],
                 timing: Optional[Timing],
                 width: int,
                 height: int,
                 depth: int = 1,
                 device: DeviceType = DeviceType.CPU,
                 category: Optional[EnumType] = None): # tested (good)
        """
        Initializes an inference result with detections and spatial/temporal metadata.

        Args:
            instances (List[Instance]): List of detected object instances.
            timing (Optional[Timing]): Execution time breakdown. If None, defaults to
                a Timing object with all None values.
            width (float): Normalized frame width (0.0, 1.0].
            height (float): Normalized frame height (0.0, 1.0].
            depth (float, optional): Normalized frame depth [0.0, 1.0]. Defaults to 1.0.
            device (DeviceType, optional): Hardware backend used. Defaults to CPU.
            category (Optional[EnumType], optional): Semantic category enum. Defaults to None.

        Raises:
            TypeError: If any parameter has incorrect type.
            ValueError: If spatial dimensions are out of valid ranges.

        Note:
            - Assignment is delegated to property setters to ensure immediate validation.
            - `width` and `height` must be > 0.0 to prevent division by zero during denormalization.
        """
        self.instances = instances
        self.timing = timing if timing is not None else Timing(preprocess=None, prediction=None, postprocess=None)
        self.width = width
        self.height = height
        self.depth = depth
        self.device = device
        self.category = category

    def __len__(self) -> int: # tested (good)
        """
        Returns the number of detected instances.

        Returns:
            int: Number of instances in the result.

        Note:
            Allows usage of built-in len() function: len(result)
        """
        return len(self._instances)

    def __iter__(self) -> Iterator[Instance]: # tested (good)
        """
        Returns an iterator over the detected instances.

        Returns:
            Iterator[Instance]: Iterator yielding each Instance in order.

        Note:
            Allows direct iteration: for instance in result:
        """
        return iter(self._instances)

    def __getitem__(self, index: int) -> Instance: # tested (good)
        """
        Gets a detected instance by index.

        Args:
            index (int): Index of the instance to retrieve. Supports negative indices.

        Returns:
            Instance: The instance at the specified index.

        Raises:
            IndexError: If index is out of range.

        Note:
            Allows index-based access: result[0] or result[-1]
        """
        return self._instances[index]

    def __str__(self) -> str: # tested (good)
        """
        Returns a human-readable string representation of the inference result.

        Returns:
            str: Concise summary with instance count, total timing, device, and dimensions.
        """
        total_time = self.timing.total if self.timing.total is not None else 0.0
        return (f"Result(instances={len(self._instances)}, "
                f"timing={total_time:.1f}ms, "
                f"device={self.device.value}, "
                f"dim=({self.width}px, {self.height}px, {self.depth}px)")

    def __repr__(self) -> str: # tested (good)
        """
        Returns a detailed string representation suitable for debugging and reproduction.

        Returns:
            str: Exact representation including all canonical parameters.
        """
        return (f"Result(instances=[...{len(self._instances)} items], "
                f"timing={self.timing!r}, "
                f"width={self.width!r}, height={self.height!r}, depth={self.depth!r}, "
                f"device={self.device!r}, category={self.category!r})")

    @property
    def instances(self) -> List[Instance]: # tested (good)
        """
        List[Instance]: Gets the list of detected object instances.

        Returns:
            List[Instance]: All detections with normalized coordinates [0.0, 1.0].
                May be empty if no objects were detected.

        Note:
            - Each Instance contains box, polygon, or obb depending on model type.
            - Coordinates are normalized relative to width/height/depth.
            - Order is typically by descending confidence (backend-specific).
        """
        return self._instances

    @instances.setter
    def instances(self, instances: List[Instance]) -> None: # tested (good)
        """
        Sets the list of detected instances with strict validation.

        Args:
            instances (List[Instance]): List of Instance objects.

        Raises:
            TypeError: If instances is not a list or contains non-Instance elements.

        Note:
            - Empty list is valid (represents no detections).
            - All elements must be Instance objects (strict validation).
        """
        if not isinstance(instances, list):
            raise TypeError(f"'instances' must be '<list>', but got {type(instances).__name__}")
        for i, instance in enumerate(instances):
            if not isinstance(instance, Instance):
                raise TypeError(f"'instances[{i}]' must be '<Instance>', but got {type(instance).__name__}")
        self._instances = instances

    @property
    def timing(self) -> Timing: # tested (good)
        """
        Timing: Gets the execution time breakdown for pipeline stages.

        Returns:
            Timing: Object containing preprocess, prediction, postprocess times in ms.

        Note:
            - Use timing.total for FPS calculation: fps = 1000.0 / result.timing.total.
            - Useful for identifying performance bottlenecks in the pipeline.
        """
        return self._timing

    @timing.setter
    def timing(self, timing: Timing) -> None: # tested (good)
        """
        Sets the timing information with validation.

        Args:
            timing (Timing): Timing object with stage durations.

        Raises:
            TypeError: If timing is not a Timing instance.

        Note:
            - Timing must be pre-validated (non-negative values) by the Timing class itself.
        """
        if not isinstance(timing, Timing):
            raise TypeError(f"'timing' must be '<Timing>', but got {type(timing).__name__}")
        self._timing = timing

    @property
    def width(self) -> int: # tested (good)
        """
        int: Gets the absolute frame width in pixels.

        Returns:
            int: Width in pixels (must be > 0).
        """
        return self._width

    @width.setter
    def width(self, width: int) -> None: # tested (good)
        """
        Sets the absolute frame width with validation.

        Args:
            width (int): Frame width in pixels.

        Raises:
            TypeError: If width is not an integer.
            ValueError: If width is <= 0.

        Note:
            - Must be strictly > 0 to prevent division by zero during coordinate denormalization.
            - Stored as int to represent exact pixel dimensions of the source frame.
        """
        if not isinstance(width, int) or isinstance(width, bool):
            raise TypeError(f"'width' must be '<int>', but got {type(width).__name__}")
        if width <= 0:
            raise ValueError(f"'width' must be > 0, got {width}")
        self._width = width

    @property
    def height(self) -> int: # tested (good)
        """
        int: Gets the absolute frame height in pixels.

        Returns:
            int: Height in pixels (must be > 0).
        """
        return self._height

    @height.setter
    def height(self, height: int) -> None: # tested (good)
        """
        Sets the absolute frame height with validation.

        Args:
            height (int): Frame height in pixels.

        Raises:
            TypeError: If height is not an integer.
            ValueError: If height is <= 0.

        Note:
            - Must be strictly > 0 to prevent division by zero during coordinate denormalization.
        """
        if not isinstance(height, int) or isinstance(height, bool):
            raise TypeError(f"'height' must be '<int>', but got {type(height).__name__}")
        if height <= 0:
            raise ValueError(f"'height' must be > 0, got {height}")
        self._height = height

    @property
    def depth(self) -> int: # tested (good)
        """
        int: Gets the absolute frame depth in pixels (or 1 for standard 2D).

        Returns:
            int: Depth in pixels (must be >= 1).
        """
        return self._depth

    @depth.setter
    def depth(self, depth: int) -> None: # tested (good)
        """
        Sets the absolute frame depth with validation.

        Args:
            depth (int): Frame depth in pixels.

        Raises:
            TypeError: If depth is not an integer.
            ValueError: If depth is < 1.

        Note:
            - Defaults to 1 for standard 2D images.
            - For 3D/depth cameras, represents the maximum depth resolution or bound.
        """
        if not isinstance(depth, int) or isinstance(depth, bool):
            raise TypeError(f"'depth' must be '<int>', but got {type(depth).__name__}")
        if depth < 1:
            raise ValueError(f"'depth' must be >= 1, got {depth}")
        self._depth = depth

    @property
    def device(self) -> DeviceType: # tested (good)
        """
        DeviceType: Gets the hardware backend used for instance inference.

        Returns:
            DeviceType: Current device (`CPU`, `CUDA`, `MPS`, etc.).

        Note:
            - Must match available hardware resources on the target system.
            - `CUDA` requires NVIDIA GPU with compatible drivers.
            - `MPS` requires Apple Silicon (M1/M2/M3/M4 series).
        """
        return self._device

    @device.setter
    def device(self, device: DeviceType) -> None: # tested (good)
        """
        Sets the hardware backend for instance inference with validation.

        Args:
            device (DeviceType): Target device for inference.

        Raises:
            TypeError: If device is not a `DeviceType` enum member.

        Note:
            - This is a pure data container; it does NOT trigger model reloading.
            - Model reloading logic belongs in the Tracker/Inference backend, not the Result DTO.
        """
        if not isinstance(device, DeviceType):
            raise TypeError(f"'device' must be '<DeviceType>', but got {type(device).__name__}")
        self._device = device

    @property
    def category(self) -> EnumType: # tested (good)
        """
        Optional[EnumType]: Gets the semantic category enumeration class.

        Returns:
            Optional[EnumType]: Category enum (e.g., `ADASClass`, `CocoClass`) or None.

        Note:
            - Provides human-readable semantic meaning to numeric `class_id`.
            - May be None if the backend does not provide semantic mapping.
        """
        return self._category

    @category.setter
    def category(self, category: EnumType) -> None: # tested (good)
        """
        Sets the semantic category enumeration with type validation.

        Args:
            category (Optional[EnumType]): New category enum class or None.

        Raises:
            TypeError: If category is not an `EnumType` instance or None.

        Note:
            - Category enum must match the instance's output class indices.
            - Used for mapping `class_id` to semantic labels in business logic.
        """
        if not isinstance(category, EnumType):
            raise TypeError(f"'category' must be '<EnumType>', but got {type(category).__name__}")
        self._category = category




