import logging

import numpy as np

from acmenra_cv.instance.point import Point


logger = logging.getLogger(__name__)


class Box:
    """
    Represents an axis-aligned 3D bounding box defined by six planes.

    The box is designed to work with normalized coordinates (0.0-1.0) from YOLO
    detection models, supporting 2D image projections and 3D spatial calculations.
    Provides geometric properties (center, dimensions, area, volume) and
    transformation methods (scale, translate) for seamless integration with
    downstream processing pipelines.

    Key features:
    - Normalized coordinate system (0.0-1.0) relative to image dimensions
    - Automatic center and dimension calculations
    - Immutable transformation methods (scale, translate return new instances)
    - Conversion utilities for OpenCV and NumPy operations
    - Consistent interface with Polygon and Obb classes

    Coordinate System Note:
        - X axis: 0.0 = left, 1.0 = right
        - Y axis: 0.0 = top, 1.0 = bottom (increases downward)
        - Z axis: 0.0 = back, 1.0 = front (depth, typically 0.0 for 2D)
        - 'top' refers to minimum Y (closer to image top)
        - 'bottom' refers to maximum Y (closer to image bottom)

    Attributes:
        _left (float): Minimum X-coordinate.
        _right (float): Maximum X-coordinate.
        _top (float): Minimum Y-coordinate (closer to image top).
        _bottom (float): Maximum Y-coordinate (closer to image bottom).
        _front (float): Maximum Z-coordinate.
        _back (float): Minimum Z-coordinate.
    """

    def __init__(self,
                 left: float = 0.0, right: float = 0.0,
                 top: float = 0.0, bottom: float = 0.0,
                 front: float = 0.0, back: float = 0.0):
        """
        Initializes a 3D bounding box with the given boundaries.

        Args:
            left (float): Left boundary (X min). Defaults to 0.0.
            right (float): Right boundary (X max). Defaults to 0.0.
            top (float): Top boundary (Y min, closer to image top). Defaults to 0.0.
            bottom (float): Bottom boundary (Y max, closer to image bottom). Defaults to 0.0.
            front (float): Front boundary (Z max). Defaults to 0.0.
            back (float): Back boundary (Z min). Defaults to 0.0.

        Raises:
            TypeError: If any argument is not a float.

        Note:
            - Does not enforce `left < right`, `top < bottom`, etc.
            - Ensure coordinates are logically ordered before spatial calculations
            - For normalized coordinates, keep values in range [0.0, 1.0]
        """
        self.left = left
        self.right = right
        self.top = top
        self.bottom = bottom
        self.front = front
        self.back = back

    def __eq__(self, other: object) -> bool:
        """
        Checks if two boxes are geometrically equal.

        Args:
            other (object): Other object to compare.

        Returns:
            bool: True if boxes have identical boundaries.

        Note:
            Uses exact equality (==), not approximate.
            For floating-point tolerance, implement custom comparison.
        """
        if not isinstance(other, Box):
            return False
        return (self.left == other.left and self.right == other.right and
                self.top == other.top and self.bottom == other.bottom and
                self.front == other.front and self.back == other.back)

    def __str__(self) -> str:
        """
        Returns a human-readable string representation of the box.

        Returns:
            str: Formatted string with boundaries and center coordinates.
        """
        c = self.center
        return (f"Box(left={self.left:.3f}, right={self.right:.3f}, "
                f"top={self.top:.3f}, bottom={self.bottom:.3f}, "
                f"center=({c.X:.3f}, {c.Y:.3f}, {c.Z:.3f}))")

    def __repr__(self) -> str:
        """
        Returns a detailed string representation suitable for debugging.

        Returns:
            str: Exact representation that can recreate the object.
        """
        return (f"Box(left={self.left!r}, right={self.right!r}, "
                f"top={self.top!r}, bottom={self.bottom!r}, "
                f"front={self.front!r}, back={self.back!r})")

    @property
    def left(self) -> float:
        """float: Gets the left boundary (X min)."""
        return self._left

    @left.setter
    def left(self, left: float) -> None:
        """Sets the left boundary (X min).

        Args:
            left (float): The left boundary.

        Raises:
            TypeError: If left is not of type float.
        """
        if not isinstance(left, float):
            raise TypeError(f"'left' must be '<float>', but got {type(left).__name__}")
        self._left = left

    @property
    def right(self) -> float:
        """float: Gets the right boundary (X max)."""
        return self._right

    @right.setter
    def right(self, right: float) -> None:
        """Sets the right boundary (X max).

        Args:
            right (float): The right boundary.

        Raises:
            TypeError: If right is not of type float.
        """
        if not isinstance(right, float):
            raise TypeError(f"'right' must be '<float>', but got {type(right).__name__}")
        self._right = right

    @property
    def top(self) -> float:
        """float: Gets the top boundary (Y max)."""
        return self._top

    @top.setter
    def top(self, top: float) -> None:
        """Sets the top boundary (Y max).

        Args:
            top (float): The top boundary.

        Raises:
            TypeError: If top is not of type float.
        """
        if not isinstance(top, float):
            raise TypeError(f"'top' must be '<float>', but got {type(top).__name__}")
        self._top = top

    @property
    def bottom(self) -> float:
        """float: Gets the bottom boundary (Y min)."""
        return self._bottom

    @bottom.setter
    def bottom(self, bottom: float) -> None:
        """Sets the bottom boundary (Y min).

        Args:
            bottom (float): The bottom boundary.

        Raises:
            TypeError: If bottom is not of type float.
        """
        if not isinstance(bottom, float):
            raise TypeError(f"'bottom' must be '<float>', but got {type(bottom).__name__}")
        self._bottom = bottom

    @property
    def front(self) -> float:
        """float: Gets the front boundary (Z max)."""
        return self._front

    @front.setter
    def front(self, front: float) -> None:
        """Sets the front boundary (Z max).

        Args:
            front (float): The front boundary.

        Raises:
            TypeError: If front is not of type float.
        """
        if not isinstance(front, float):
            raise TypeError(f"'front' must be '<float>', but got {type(front).__name__}")
        self._front = front

    @property
    def back(self) -> float:
        """float: Gets the back boundary (Z min)."""
        return self._back

    @back.setter
    def back(self, back: float) -> None:
        """Sets the back boundary (Z min).

        Args:
            back (float): The back boundary.

        Raises:
            TypeError: If back is not of type float.
        """
        if not isinstance(back, float):
            raise TypeError(f"'back' must be '<float>', but got {type(back).__name__}")
        self._back = back

    @property
    def center(self) -> Point:
        """Point: Computes and returns the geometric center of the box.

        The center is recalculated every time it's accessed based on current boundaries.

        Returns:
            Point: The center point of the box.
        """
        return Point.safe(x=(self.left + self.right) / 2.0,
                          y=(self.top + self.bottom) / 2.0,
                          z=(self.front + self.back) / 2.0)

    @property
    def bottom_center(self) -> Point:
        """Point: Computes and returns the geometric center of the box.

        The center is recalculated every time it's accessed based on current boundaries.

        Returns:
            Point: The center point of the box.
        """
        return Point.safe(x=(self.left + self.right) / 2.0,
                          y=self.bottom,
                          z=(self.front + self.back) / 2.0)

    @property
    def width(self) -> float:
        """float: Gets the width of the box (right - left)."""
        return self.right - self.left

    @property
    def height(self) -> float:
        """float: Gets the height of the box (top - bottom)."""
        return self.top - self.bottom

    @property
    def depth(self) -> float:
        """float: Gets the depth of the box (front - back)."""
        return self.front - self.back

    def get_area(self) -> float:
        """float: Gets the 2D area of the box (width × height)."""
        return self.width * self.height

    def get_volume(self) -> float:
        """float: Gets the 3D volume of the box (width × height × depth)."""
        return self.width * self.height * self.depth

    def scale(self, sx: float, sy: float, sz: float = 1.0, normalized: bool = True) -> 'Box':
        """
        Returns a new Box scaled by the given factors.

        Args:
            sx, sy, sz: Scale factors. Accepts int or float.
            normalized: If True (default), clamps result to [0.0, 1.0].
                        If False, returns absolute coordinates without clamping.

        Returns:
            Box: Scaled box. Original instance remains unchanged.

        Note:
            When normalized=True and scale is negative, clamping may collapse
            boundaries. Use normalized=False for pixel-space or ROI transformations.
        """
        for val, name in [(sx, 'sx'), (sy, 'sy'), (sz, 'sz')]:
            if isinstance(val, bool) or not isinstance(val, (int, float)):
                raise TypeError(f"'{name}' must be numeric, got {type(val).__name__}")

        if normalized:
            return Box.safe(left=self.left * sx, right=self.right * sx,
                            top=self.top * sy, bottom=self.bottom * sy,
                            front=self.front * sz, back=self.back * sz)
        else:
            return self._from_raw(left=self.left * sx, right=self.right * sx,
                                  top=self.top * sy, bottom=self.bottom * sy,
                                  front=self.front * sz, back=self.back * sz)

    def translate(self, dx: float, dy: float, dz: float = 0.0, normalized: bool = True) -> 'Box':
        """
        Returns a new Box translated by the given offsets.

        Args:
            dx, dy, dz: Translation offsets (can be negative/positive).
            normalized: If True (default), clamps result to [0.0, 1.0].
                        If False, allows coordinates to exceed bounds (e.g., ROI padding).

        Returns:
            Box: Translated box. Original instance remains unchanged.
        """
        for val, name in [(dx, 'dx'), (dy, 'dy'), (dz, 'dz')]:
            if isinstance(val, bool) or not isinstance(val, (int, float)):
                raise TypeError(f"'{name}' must be numeric, got {type(val).__name__}")

        if normalized:
            return Box.safe(left=self.left + dx, right=self.right + dx,
                            top=self.top + dy, bottom=self.bottom + dy,
                            front=self.front + dz, back=self.back + dz)
        else:
            return self._from_raw(left=self.left + dx, right=self.right + dx,
                                  top=self.top + dy, bottom=self.bottom + dy,
                                  front=self.front + dz, back=self.back + dz)

    def to_array(self) -> np.ndarray:
        """Converts the Box to a NumPy array.

        Returns:
            np.ndarray: Array of shape (6,) [left, right, top, bottom, front, back]
        """
        return np.array([self.left, self.right, self.top, self.bottom, self.front, self.back])

    def to_absolute_array(self, width: float, height: float) -> np.ndarray:
        """Converts normalized coordinates (0..1) to absolute pixel coordinates.

        Useful for drawing boxes on original images or extracting ROIs.

        Args:
            width (float): Width of the target image in pixels.
            height (float): Height of the target image in pixels.

        Returns:
            np.ndarray: Array of shape (4, 2) with absolute coordinates:
                [[left, top], [right, top], [right, bottom], [left, bottom]]

        Note:
            For 2D images, Z coordinates (front/back) are ignored.
            All values are rounded to integers for pixel alignment.
        """
        return np.array([[self.left * width, self.top * height],
                         [self.right * width, self.top * height],
                         [self.right * width, self.bottom * height],
                         [self.left * width, self.bottom * height]])

    @classmethod
    def safe(cls, left: float = 0.0, right: float = 0.0, top: float = 0.0,
             bottom: float = 0.0, front: float = 0.0, back: float = 0.0,
             min_val: float = 0.0, max_val: float = 1.0) -> 'Box':
        """
        Creates a Box with coordinates clamped to [min_val, max_val] range.

        Bypasses __init__ validation to avoid double-checking and support custom ranges.

        Note:
            - No exceptions raised. Coordinates are clamped independently per axis.
            - Recommended for geometric transformations that may exit normalized space.
        """
        def clamp(v: float) -> float:
            return float(min_val if v < min_val else (max_val if v > max_val else v))

        box = cls.__new__(cls)
        box._left = clamp(left)
        box._right = clamp(right)
        box._top = clamp(top)
        box._bottom = clamp(bottom)
        box._front = clamp(front)
        box._back = clamp(back)
        return box

    @classmethod
    def _from_raw(cls, left: float, right: float, top: float,
                  bottom: float, front: float, back: float) -> 'Box':
        """Internal factory to create Box without __init__ validation or clamping."""
        box = cls.__new__(cls)
        box._left = float(left)
        box._right = float(right)
        box._top = float(top)
        box._bottom = float(bottom)
        box._front = float(front)
        box._back = float(back)
        return box


