import logging
from enum import EnumType, EnumMeta
from typing import Optional
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 (category enum)
    - 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 instance categories.
        category (EnumType): Semantic class label from enumeration.
        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)
    """

    def __init__(self,
                 id: Optional[int],
                 class_id: int,
                 category: EnumType,
                 conf: float,
                 box: Box,
                 polygon: Optional[Polygon] = None,
                 obb: Optional[Obb] = 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.
            category (EnumType): Semantic class label from COCO enumeration.
            conf (float): Detection confidence score in range [0.0, 1.0].
            box (Box): Axis-aligned bounding box in normalized coordinates.
            polygon (Optional[Polygon]): Segmentation mask. Defaults to None.
                Only set for segmentation models (task='segment').
            obb (Optional[Obb]): Oriented bounding box. Defaults to None.
                Only set for OBB models (task='obb').

        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
        """
        self.id = id
        self.class_id = class_id
        self.category = category
        self.conf = conf
        self.polygon = polygon
        self.obb = obb
        self.box = box

    @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.

        Note:
            Negative IDs are not validated (some trackers use -1 for lost tracks).
            None is valid for detection-only mode.
        """
        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)
        """
        Gets the numeric class identifier (0-79 for COCO dataset).

        Returns:
            int: Numeric class ID corresponding to instance categories.
                Always non-negative.

        Note:
            This is the raw numeric ID used internally by YOLO.
            For semantic meaning, use the 'category' property instead.
            Example: class_id=0 might represent 'person' in COCO dataset.
        """
        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 instance's output categories.
            For custom models, ensure class_id matches training configuration.
        """
        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 category(self) -> EnumMeta: # tested (good)
        """
        Gets the semantic category of the object.

        Returns:
            EnumMeta: Enum member representing the object's semantic category
                (e.g., ADASClass.car, ADASClass.person).

        Note:
            This provides human-readable semantic meaning to the numeric class_id.
            Always corresponds to the current class_id property.
            Example: ADASClass.car.value == 2 (if car is class 2 in instance)
        """
        return self._category

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

        Args:
            category (EnumMeta): Semantic category to set.

        Note:
            Category should match the class_id value.
            No type validation enforced (enum types vary by project).
        """
        # if not isinstance(category, enum):
        #     raise TypeError(f"'category' must be '<EnumType>', but got {type(category)}")
        self._category = category

    @property
    def conf(self) -> float: # tested (good)
        """
        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 negative.

        Note:
            Values > 1.0 are allowed (some models output unnormalized scores).
            Values < 0.0 are rejected as invalid.
        """
        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)
        """
        Sets the bounding box with validation.

        Args:
            box (Box): Box instance representing the object's bounding box.
                Must contain valid normalized coordinates (0..1).

        Raises:
            TypeError: If box is not a Box instance.

        Note:
            Box is the primary spatial representation for tracking.
            All trajectory calculations use box.bottom_center.
        """
        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)
        """
        Gets the oriented bounding box (rotated rectangle).

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

        Note:
            Only set for OBB models (task='obb').
            Contains exactly 4 points in clockwise order:
            [top-left, top-right, bottom-right, bottom-left]

            Provides rotation angle via obb.angle property.
            For detection/segmentation models, this will be None.
        """
        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.
                Must contain exactly 4 points in clockwise order.
                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 corners.
        """
        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)
        """
        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.
        """
        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).
        """
        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


