# acmenra_cv/instance/instance.py

import logging
from typing import Optional
from enum import Enum, EnumType, EnumMeta

from acmenra_cv.instance.box import Box
from acmenra_cv.instance.obb import Obb
from acmenra_cv.instance.polygon import Polygon


logger = logging.getLogger(__name__)


class Instance:
    """
    Represents a single detected object instance from YOLO detection models.

    Contains comprehensive information about a detected object including:
    - Semantic classification (label enum member)
    - Segmentation mask as a Polygon (optional, for segmentation models)
    - Oriented bounding box as Obb (optional, for OBB models)
    - Axis-aligned bounding box as Box (always present)
    - Tracking ID (if available from tracking mode)

    Designed for seamless integration with downstream processing pipelines such as:
    - ADAS zone analysis and collision detection
    - Multi-object tracking systems
    - Visualization and rendering utilities
    - Business logic for event detection

    All coordinates are maintained in normalized space (0.0-1.0) relative to the
    original image dimensions, making instances resolution-independent.

    Attributes:
        id (Optional[int]): Tracking ID if available (None for detection-only mode).
            Must be non-negative when present.
        class_id (int): Numeric class identifier corresponding to model output indices.
        label (Enum): Semantic class label member from enumeration (e.g., Coco.person).
            Provides human-readable meaning to the numeric class_id.
        conf (float): Detection confidence score (0.0-1.0).
        box (Box): Axis-aligned bounding box in normalized coordinates.
        polygon (Optional[Polygon]): Segmentation mask (None for detection/OBB models).
        obb (Optional[Obb]): Oriented bounding box (None for detection/segmentation models).

    Note:
        - Only one of polygon/obb should be set depending on instance task type
        - Box is always available (computed from polygon/obb if needed)
        - All spatial data uses normalized coordinates (0.0-1.0)
        - The 'label' field holds a specific enum member (e.g., Coco.person),
          while Backend/Result hold the enum class itself (e.g., Coco) as 'category'
    """

    def __init__(self,
                 id: Optional[int],
                 class_id: int,
                 label: Enum,
                 conf: float,
                 box: Box,
                 obb: Optional[Obb] = None,
                 polygon: Optional[Polygon] = None): # tested (good)
        """
        Initializes a Instance with complete object detection information.

        Args:
            id (Optional[int]): Tracking ID (can be None for detection-only mode).
                Must be non-negative when provided.
            class_id (int): Numeric class identifier (0-79 for COCO dataset).
                Must be non-negative.
            label (Enum): Semantic class label member from enumeration
                (e.g., Coco.person, ADASClass.car).
            conf (float): Detection confidence score in range [0.0, 1.0].
            box (Box): Axis-aligned bounding box in normalized coordinates.
            obb (Optional[Obb]): Oriented bounding box. Defaults to None.
                Only set for OBB models (task='obb').
            polygon (Optional[Polygon]): Segmentation mask. Defaults to None.
                Only set for segmentation models (task='segment').

        Raises:
            TypeError: If any parameter has incorrect type.
            ValueError: If numeric parameters have invalid values (negative).

        Note:
            - polygon and obb are mutually exclusive (set based on instance task)
            - box is always required (axis-aligned fallback for all modes)
            - id is None in detection mode, assigned by tracker in tracking mode
            - label must be an Enum member (not an Enum class)
        """
        self.id = id
        self.class_id = class_id
        self.label = label
        self.conf = conf
        self.box = box
        self.obb = obb
        self.polygon = polygon

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

        Returns:
            str: Concise summary with tracking ID, semantic label, and confidence.
                 Format: "Instance(id=..., label=..., conf=...)"

        Note:
            - Optimized for console logging, operator dashboards, and alert systems
            - Omits complex geometry (Box/Polygon) to keep output clean
            - Handles both Enum members and Enum classes gracefully
        """
        return f"Instance(id={self.id!r}, label={self.label}, conf={self.conf:.2f})"

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

        Returns:
            str: Exact representation including all canonical parameters and spatial objects.
                 Uses repr-safe formatting (!r) for unambiguous parsing.

        Note:
            - Includes nested representations of Box, Polygon, and Obb
            - All floats and IDs are printed with full precision
            - Essential for debugging pipeline failures and verifying detection state
        """
        return (f"Instance(id={self.id!r}, "
                f"class_id={self.class_id!r}, "
                f"label={self.label}, "
                f"conf={self.conf!r}, "
                f"box={self.box!r}, "
                f"obb={self.obb!r}, "
                f"polygon={self.polygon!r})")

    @property
    def id(self) -> Optional[int]: # tested (good)
        """
        Optional[int]: Gets the tracking ID of the instance.

        Returns:
            Optional[int]: Tracking ID if available (None for detection-only mode).
                When present, always non-negative.

        Note:
            Tracking IDs are maintained across frames in tracking mode.
            In detection-only mode, this will always be None.
            IDs are assigned by the tracking system (e.g., BoT-SORT, YOLO tracker).
        """
        return self._id

    @id.setter
    def id(self, id: Optional[int]) -> None: # tested (good)
        """
        Sets the tracking ID with validation.

        Args:
            id (Optional[int]): Tracking ID to set. Must be non-negative when provided.
                Pass None for detection-only mode.

        Raises:
            TypeError: If id is not None and not an integer.
            ValueError: If id is negative.

        Note:
            None is valid for detection-only mode.
            Negative IDs are rejected (some trackers use -1 for lost tracks,
            but acmenra-cv uses None instead for clarity).
        """
        if id is not None and not isinstance(id, int) or isinstance(id, bool):
            raise TypeError(f"'id' must be '<int>', but got {type(id).__name__}")
        if id is not None and id < 0:
            raise ValueError(f"'id' must be >= 0")
        self._id = id

    @property
    def class_id(self) -> int: # tested (good)
        """
        int: Gets the numeric class identifier (0-79 for COCO dataset).

        Returns:
            int: Numeric class ID corresponding to model output indices.
                Always non-negative.

        Note:
            This is the raw numeric ID used internally by YOLO.
            For semantic meaning, use the 'label' property instead.
            Example: class_id=0 might represent 'person' in COCO dataset.
            The label property provides the corresponding Enum member.
        """
        return self._class_id

    @class_id.setter
    def class_id(self, class_id: int) -> None: # tested (good)
        """
        Sets the numeric class identifier with validation.

        Args:
            class_id (int): Numeric class ID to set. Must be non-negative.

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

        Note:
            Class ID must match the model's output indices.
            For custom models, ensure class_id matches training configuration.
            Boolean values are explicitly rejected (bool is subclass of int).
        """
        if not isinstance(class_id, int) or isinstance(class_id, bool):
            raise TypeError(f"'class_id' must be '<int>', but got {type(class_id).__name__}")
        if class_id < 0:
            raise ValueError(f"'class_id' must be >= 0")
        self._class_id = class_id

    @property
    def label(self) -> Enum: # tested (good)
        """
        Enum: Gets the semantic label of the object.

        Returns:
            Enum: Enum member representing the object's semantic label
                (e.g., Coco.car, Coco.person, ADASClass.truck).

        Note:
            This provides human-readable semantic meaning to the numeric class_id.
            Always corresponds to the current class_id property.
            Example: Coco.car.value == 2 (if car is class 2 in the model).

            Important distinction:
            - Instance.label holds a specific enum MEMBER (e.g., Coco.person)
            - Backend.category / Result.category hold the enum CLASS (e.g., Coco)

            This naming convention avoids confusion between the class-level
            category (which enum to use) and instance-level label (which class).
        """
        return self._label

    @label.setter
    def label(self, label: Enum) -> None: # tested (good)
        """
        Sets the semantic label with validation.

        Args:
            label (Enum): Semantic label member to set (e.g., Coco.person).

        Raises:
            TypeError: If label is not an Enum member.

        Note:
            Label must be an Enum member, not an Enum class.
            Should correspond to the class_id value (label.value == class_id).
            Strict type validation ensures only proper enum members are accepted.
        """
        if not isinstance(label, Enum):
            raise TypeError(f"'label' must be '<Enum>', but got {type(label)}")
        self._label = label

    @property
    def conf(self) -> float: # tested (good)
        """
        float: Gets the detection confidence score.

        Returns:
            float: Confidence score in range [0.0, 1.0].
                Higher values indicate more confident detections.

        Note:
            Typical thresholds:
            - 0.25-0.35: Standard detection threshold
            - 0.5+: High confidence detections only
            - 0.1-0.2: Low-light/night mode (more false positives)

            Confidence is instance-dependent and may need calibration per use case.
        """
        return self._conf

    @conf.setter
    def conf(self, conf: float) -> None: # tested (good)
        """
        Sets the detection confidence score with validation.

        Args:
            conf (float): Confidence score to set. Must be in range [0.0, 1.0].

        Raises:
            TypeError: If conf is not a float.
            ValueError: If conf is outside range [0.0, 1.0].

        Note:
            Values outside [0.0, 1.0] are rejected as invalid probabilities.
            Some models output unnormalized scores, but acmenra-cv enforces
            the normalized range for consistency across all backends.
        """
        if not isinstance(conf, float):
            raise TypeError(f"'conf' must be '<float>', but got {type(conf).__name__}")
        if not 0.0 <= conf <= 1.0:
            raise ValueError(f"'conf' must be in range [0.0, 1.0], got {conf}")
        self._conf = conf

    @property
    def box(self) -> Box: # tested (good)
        """
        Gets the axis-aligned bounding box in normalized coordinates.

        Returns:
            Box: Bounding box with coordinates in normalized space (0..1).

        Note:
            Box coordinates follow the convention:
            - left/right: X-axis (0=left edge, 1=right edge)
            - top/bottom: Y-axis (0=top edge, 1=bottom edge)
            - front/back: Z-axis (0.0 for 2D images)

            This is always available, even for segmentation/OBB models.
            For OBB models, this is the axis-aligned envelope of the rotated box.
        """
        return self._box

    @box.setter
    def box(self, box: Box) -> None: # tested (good)
        """
        Box: Gets the axis-aligned bounding box in normalized coordinates.

        Returns:
            Box: Bounding box with coordinates in normalized space (0..1).

        Note:
            Box coordinates follow the convention:
            - left/right: X-axis (0=left edge, 1=right edge)
            - top/bottom: Y-axis (0=top edge, 1=bottom edge)
            - front/back: Z-axis (0.0 for 2D images)

            This is always available, even for segmentation/OBB models.
            For OBB models, this is the axis-aligned envelope of the rotated box.
            For segmentation models, this is the bounding box of the polygon mask.
        """
        if not isinstance(box, Box):
            raise TypeError(f"'box' must be '<Box>', but got {type(box).__name__}")
        self._box = box

    @property
    def obb(self) -> Optional[Obb]: # tested (good)
        """
        Optional[Obb]: Gets the oriented bounding box (rotated rectangle).

        Returns:
            Optional[Obb]: OBB with rotation parameters in normalized space (0..1).
                None if OBB is not available.

        Note:
            Only set for OBB models (task='obb').
            Contains center coordinates, dimensions, and rotation angle.
            Provides rotation angle via obb.yaw property.
            For detection/segmentation models, this will be None.

            Mutually exclusive with polygon (only one should be set).
        """
        return self._obb

    @obb.setter
    def obb(self, obb: Optional[Obb]) -> None: # tested (good)
        """
        Sets the oriented bounding box with validation.

        Args:
            obb (Optional[Obb]): Obb instance representing the rotated box.
                Pass None for detection/segmentation models.

        Raises:
            TypeError: If obb is not None and not an Obb instance.

        Note:
            Should only be set for OBB models.
            Mutually exclusive with polygon (don't set both).
            Box is automatically computed from OBB parameters.
        """
        if obb is not None and not isinstance(obb, Obb):
            raise TypeError(f"'obb' must be '<Obb>', but got {type(obb).__name__}")
        self._obb = obb

    @property
    def polygon(self) -> Optional[Polygon]: # tested (good)
        """
        Optional[Polygon]: Gets the segmentation mask as a Polygon.

        Returns:
            Optional[Polygon]: Object's mask boundary with vertices in normalized
                space (0..1). None if segmentation is not available.

        Note:
            Only set for segmentation models (task='segment').
            Contains variable number of points (typically 10-100+).
            Use to_absolute_array() to convert to pixel coordinates.

            For detection/OBB models, this will be None.

            Mutually exclusive with obb (only one should be set).
        """
        return self._polygon

    @polygon.setter
    def polygon(self, polygon: Polygon) -> None: # tested (good)
        """
        Sets the segmentation mask with validation.

        Args:
            polygon (Optional[Polygon]): Polygon instance representing the
                object's mask. Must contain valid normalized coordinates (0..1).
                Pass None for detection/OBB models.

        Raises:
            TypeError: If polygon is not None and not a Polygon instance.

        Note:
            Should only be set for segmentation models.
            Mutually exclusive with obb (don't set both).
            Box is automatically computed from polygon bounds.
        """
        if polygon is not None and not isinstance(polygon, Polygon):
            raise TypeError(f"'polygon' must be '<Polygon>', but got {type(polygon).__name__}")
        self._polygon = polygon


