import cv2
import logging
import numpy as np
from functools import wraps
from typing import List, Tuple, Optional
from acmenra_cv.instance.box import Box
from acmenra_cv.instance.obb import Obb
from acmenra_cv.instance.point import Point
from acmenra_cv.instance.polygon import Polygon
from acmenra_cv.tracker.tracker import TrackedObject
from acmenra_cv.render.style import Style


logger = logging.getLogger(__name__)


def validate_frame(f):
    """
    Decorator that validates input frames before executing drawing operations.

    Checks if the frame is valid (non-None, non-empty, valid dimensions) before
    proceeding. If validation fails, logs a warning and returns the original frame
    unmodified, preventing OpenCV assertion errors during rendering.

    Note:
        - Applied to all public drawing methods via @validate_frame
        - Ensures safe in-place frame modification
        - Preserves original frame on validation failure
    """
    @wraps(f)
    def wrapper(self, frame: np.ndarray, *args, **kwargs):
        if not self._validate_frame(frame):
            logger.warning(f"[{f.__name__}] Пропускаем из-за невалидного frame")
            return frame
        return f(self, frame, *args, **kwargs)
    return wrapper


class Drawer:
    """
    Comprehensive renderer for visual elements on video frames with configurable styling.

    This class provides a type-safe, style-driven interface for drawing bounding boxes,
    oriented bounding boxes (OBB), segmentation polygons, trajectories, and text overlays
    in computer vision pipelines. All operations respect normalized coordinate systems,
    support alpha compositing, and include robust frame validation to prevent OpenCV errors.

    Key features:
    - Configurable rounded bounding boxes with segment-based corner styling
    - Alpha-blended overlay rendering for transparent visualizations
    - Polygon smoothing and anti-aliased line drawing (OpenCV LINE_AA)
    - Trajectory visualization from normalized Point histories
    - Style inheritance with per-operation override capability
    - Strict frame validation and graceful degradation on invalid inputs

    Designed for seamless integration with downstream processing pipelines such as:
    - Real-time ADAS monitoring and alert visualization
    - Multi-camera object tracking overlays
    - Performance-optimized embedded deployments (e.g., Raspberry Pi, Jetson)
    - Custom rendering engines built on OpenCV/Numpy

    Attributes:
        _style (Style): Default rendering configuration for all drawing operations.

    Note:
        - All spatial coordinates (Box, Polygon, Obb, Trajectory) are normalized [0.0, 1.0]
        - Text coordinates (x, y) are in absolute pixel space
        - Drawing operations modify the input frame in-place for performance
        - `style.show=False` globally bypasses all rendering for resource optimization
    """

    def __init__(self, style: Style):
        """
        Initializes the Drawer with a default rendering style.

        Args:
            style (Style): Style object containing default rendering parameters.

        Raises:
            TypeError: If style is not a Style instance.

        Note:
            - The style object defines default appearance for all drawing operations
            - Style can be modified later via the style property
            - Individual drawing calls can still override specific parameters
        """
        self.style = style

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

        Returns:
            str: Formatted string with key rendering parameters.

        Note:
            - Includes current style configuration (show, alpha, rounding, segment)
            - Useful for logging and debugging pipeline state
        """
        return (f"Drawer(style=Style(show={self.style.show}, alpha={self.style.alpha}, "
                f"rounding={self.style.rounding}, segment={self.style.segment}))")

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

        Returns:
            str: Exact representation of the Drawer instance.

        Note:
            - Can be used to recreate the object in interactive sessions
            - Includes full style configuration via repr
        """
        return f"Drawer(style={self.style!r})"

    @property
    def style(self) -> Style:
        """
        Style: Gets the current default rendering style.

        Returns:
            Style: Active style configuration for drawing operations.
        """
        return self._style

    @style.setter
    def style(self, style: Style) -> None:
        """
        Sets the default rendering style with validation.

        Args:
            style (Style): New style to use as default.

        Raises:
            TypeError: If style is not a Style instance.

        Note:
            - Changing style affects all subsequent drawing operations
            - Individual drawing calls can still override specific parameters
            - Does not affect already rendered frames
        """
        if not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        self._style = style

    @validate_frame
    def draw_text(self, frame: np.ndarray, x: int, y: int,  text: str, style: Optional[Style] = None) -> np.ndarray:
        """
        Renders text on the frame at specified pixel coordinates.

        Args:
            frame (np.ndarray): Input frame to draw on.
            x (int): X-coordinate of text origin (in pixels).
            y (int): Y-coordinate of text origin (in pixels).
            text (str): Text string to render.
            style (Optional[Style]): Optional style override. Defaults to None.

        Returns:
            np.ndarray: Frame with text rendered.

        Raises:
            TypeError: If style is provided but not a Style instance.

        Note:
            - Coordinates are in absolute pixel space (not normalized)
            - Font settings inherited from active style (font, scale, color, thickness)
            - Maps directly to OpenCV's `cv2.putText()` parameters
            - Returns original frame if `style.show` is False
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show:
            return frame

        return cv2.putText(frame,
                           text,
                           (x, y),
                           active_style.font.font,
                           active_style.font.font_scale,
                           active_style.font.color,
                           active_style.font.thickness)

    @validate_frame
    def draw_instances(self,
                       frame: np.ndarray,
                       tracked_objects: List[TrackedObject],
                       is_box: bool = False,
                       is_obb: bool = False,
                       is_polygon: bool = False,
                       is_trajectory: bool = False,
                       style: Optional[Style] = None) -> np.ndarray:
        """
        Renders multiple tracked objects with selected visualization elements.

        Args:
            frame (np.ndarray): Input frame to draw on.
            tracked_objects (List[TrackedObject]): List of tracked objects to visualize.
            is_box (bool): Whether to draw axis-aligned bounding boxes. Defaults to False.
            is_obb (bool): Whether to draw oriented bounding boxes. Defaults to False.
            is_polygon (bool): Whether to draw segmentation polygons. Defaults to False.
            is_trajectory (bool): Whether to draw movement trajectories. Defaults to False.
            style (Optional[Style]): Optional style override. Defaults to None.

        Returns:
            np.ndarray: Frame with visualized objects blended via alpha compositing.

        Raises:
            TypeError: If style is provided but not a Style instance.

        Note:
            - Objects are colored using `style.palette[class_id]` with fallback to index 0
            - Alpha blending controlled by `style.alpha` (0.0=transparent, 1.0=opaque)
            - Each visualization type can be independently enabled/disabled
            - Returns original frame if `style.show` is False
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show:
            return frame

        img_copy = frame.copy()
        for tracked_object in tracked_objects:
            cid = tracked_object.instance.class_id if tracked_object.instance.class_id < len(active_style.palette) else 0
            color = active_style.palette[cid]
            if is_box:
                img_copy = self.draw_box(frame=img_copy,
                                         box=tracked_object.instance.box,
                                         style=style,
                                         color=color)
            if is_obb:
                img_copy = self.draw_obb(frame=img_copy,
                                         obb=tracked_object.instance.obb,
                                         style=style,
                                         color=color)

            if is_polygon:
                img_copy = self.draw_polygon(frame=img_copy,
                                             polygon=tracked_object.instance.polygon,
                                             style=style,
                                             color=color)
            if is_trajectory:
                img_copy = self.draw_trajectory(frame=img_copy,
                                                trajectory=tracked_object.trajectory.get_values(),
                                                style=style,
                                                color=color)
        return cv2.addWeighted(frame, 1 - active_style.alpha, img_copy, active_style.alpha, 0)

    @validate_frame
    def draw_box(self,
                 frame: np.ndarray,
                 box: Box, style: Optional[Style] = None,
                 color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
        """
        Renders a bounding box with rounded corners on the frame.

        Args:
            frame (np.ndarray): Input frame to draw on.
            box (Box): Bounding box to render (normalized coordinates 0.0-1.0).
            style (Optional[Style]): Optional style override. Defaults to None.
            color (Optional[Tuple[int, int, int]]): Optional BGR color tuple. Defaults to None.

        Returns:
            np.ndarray: Frame with bounding box rendered.

        Raises:
            TypeError: If style is provided but not a Style instance.

        Note:
            - Corner radius proportional to box size (`style.rounding`)
            - Segment length controlled by `style.segment` for partial edge drawing
            - Interior filled if `style.fill=True`
            - Returns original frame if `style.show` is False
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show:
            return frame

        left, top, right, bottom, width, height, radius, center_x, center_y = self._get_box_coords(frame=frame,
                                                                                                   box=box,
                                                                                                   style=active_style)

        color = color if color is not None else active_style.palette[0]
        segment = active_style.segment
        thickness = active_style.thickness

        # LeftTop segment
        cv2.ellipse(frame, (left + radius, top + radius), (radius, radius), 180, 0, 90, color, thickness, cv2.LINE_AA)
        cv2.line(frame, (left + radius, top),(left + radius + int(width * segment), top), color, thickness) # goris
        cv2.line(frame, (left, top + radius), (left, top + radius + int(height * segment)), color, thickness) # vert

        # RightTop segment
        cv2.ellipse(frame, (right - radius, top + radius), (radius, radius), 270, 0, 90, color, thickness, cv2.LINE_AA)
        cv2.line(frame, (right - radius, top), (right - radius - int(width * segment), top), color, thickness) # goris
        cv2.line(frame, (right, top + radius), (right, top + radius + int(height * segment)), color, thickness) # vert

        # LeftBottom segment
        cv2.ellipse(frame, (left + radius, bottom - radius), (radius, radius), 90, 0, 90, color, thickness, cv2.LINE_AA)
        cv2.line(frame, (left + radius, bottom), (left + radius + int(width * segment), bottom), color, thickness)  # goris
        cv2.line(frame, (left, bottom - radius), (left, bottom - radius - int(height * segment)), color, thickness) # vert

        cv2.ellipse(frame, (right - radius, bottom - radius), (radius, radius), 0, 0, 90, color, thickness, cv2.LINE_AA)
        cv2.line(frame, (right - radius, bottom), (right - radius - int(width * segment), bottom), color, thickness)  # goris
        cv2.line(frame, (right, bottom - radius), (right, bottom - radius - int(height * segment)), color, thickness)  # vert

        if style.fill if style is not None else self.style.fill:
            frame = self._draw_rounded_rect(frame=frame, box=box, style=style, color=color)
        return cv2.circle(frame, (center_x, center_y), radius, color, thickness)

    @validate_frame
    def draw_obb(self,
                 frame: np.ndarray,
                 obb: Obb,
                 style: Optional[Style] = None,
                 color: Optional[Tuple[int, int, int]] = None,
                 draw_angle: bool = False) -> np.ndarray:
        """
        Renders an oriented bounding box (rotated rectangle) on the frame.

        Args:
            frame (np.ndarray): Input frame to draw on.
            obb (Obb): Oriented bounding box to render (normalized coordinates 0.0-1.0).
            style (Optional[Style]): Optional style override. Defaults to None.
            color (Optional[Tuple[int, int, int]]): Optional BGR color tuple. Defaults to None.
            draw_angle (bool): Whether to display rotation angle near the center. Defaults to False.

        Returns:
            np.ndarray: Frame with OBB rendered.

        Raises:
            TypeError: If style is provided but not a Style instance.

        Note:
            - Draws all 4 edges using anti-aliased lines (`LINE_AA`)
            - Angle text rendered at center if `draw_angle=True`
            - Center point marked with a filled circle
            - Returns original frame if `style.show` is False
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show:
            return frame

        h, w = frame.shape[:2]
        color = color if color is not None else active_style.palette[0]

        points = [(int(p.X * w), int(p.Y * h)) for p in obb.points]

        for i in range(4):
            cv2.line(frame, points[i], points[(i + 1) % 4], color, active_style.thickness, cv2.LINE_AA)

        if draw_angle:
            center = (int(obb.center.X * w), int(obb.center.Y * h))
            angle_text = f"{obb.angle:.1f}°"
            cv2.putText(frame, angle_text, (center[0] - 20, center[1] - 10),
                       cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1, cv2.LINE_AA)
        center = (int(obb.center.X * w), int(obb.center.Y * h))

        return cv2.circle(frame, center, 3, color, -1)

    @validate_frame
    def draw_polygon(self,
                     frame: np.ndarray,
                     polygon: Polygon,
                     style: Optional[Style] = None,
                     color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
        """
        Renders a filled polygon with outline on the frame.

        Args:
            frame (np.ndarray): Input frame to draw on.
            polygon (Polygon): Polygon to render (normalized coordinates 0.0-1.0).
            style (Optional[Style]): Optional style override. Defaults to None.
            color (Optional[Tuple[int, int, int]]): Optional BGR color tuple. Defaults to None.

        Returns:
            np.ndarray: Frame with polygon rendered.

        Raises:
            TypeError: If style is provided but not a Style instance.

        Note:
            - Applies vertex smoothing via `polygon.smooth(style.smooth)`
            - Converts normalized coordinates to absolute pixels internally
            - Fills interior and draws anti-aliased outline
            - Returns original frame if `style.show` is False
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show:
            return frame

        h, w = frame.shape[:2]
        mask_cv = polygon.smooth(n=active_style.smooth).to_absolute_array(w, h).astype(np.int32).reshape(-1, 1, 2)
        cv2.fillPoly(frame, [mask_cv], color=color if color is not None else active_style.palette[0])
        cv2.polylines(frame,
                      [mask_cv],
                      isClosed=True,
                      thickness=active_style.thickness,
                      color=color if color is not None else active_style.palette[0])
        return frame

    @validate_frame
    def draw_trajectory(self,
                        frame: np.ndarray,
                        trajectory: List[Point],
                        style: Optional[Style] = None,
                        color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
        """
        Renders object movement trajectory on the frame.

        Args:
            frame (np.ndarray): Input frame to draw on.
            trajectory (List[Point]): List of points representing movement history (normalized coordinates).
            style (Optional[Style]): Optional style override. Defaults to None.
            color (Optional[Tuple[int, int, int]]): Optional BGR color tuple. Defaults to None.

        Returns:
            np.ndarray: Frame with trajectory rendered.

        Raises:
            TypeError: If style is provided but not a Style instance.

        Note:
            - Connects consecutive points with straight lines
            - Filters out `None` values automatically
            - Line thickness inherited from active style
            - Returns original frame if `style.show` is False
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show:
            return frame

        height, width = frame.shape[:2]
        trajectory = [traj for traj in trajectory if traj is not None]
        for i in range(len(trajectory) - 1):
            cv2.line(frame,
                     (int(trajectory[i].X * width), int(trajectory[i].Y * height)),
                     (int(trajectory[i + 1].X * width), int(trajectory[i + 1].Y * height)),
                     color=color if color is not None else active_style.palette[0],
                     thickness=active_style.thickness)
        return frame

    @validate_frame
    def _draw_rounded_rect(self,
                           frame: np.ndarray,
                           box: Box,
                           style: Optional[Style] = None,
                           color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
        """
        Internal method for drawing filled rounded rectangles.

        Args:
            frame (np.ndarray): Input frame to draw on.
            box (Box): Bounding box defining rectangle area (normalized coordinates).
            style (Optional[Style]): Optional style override. Defaults to None.
            color (Optional[Tuple[int, int, int]]): Optional BGR color tuple. Defaults to None.

        Returns:
            np.ndarray: Frame with filled rounded rectangle.

        Raises:
            TypeError: If style is provided but not a Style instance.

        Note:
            - Uses composite shapes (rectangles + circles) to simulate rounded fill
            - Dynamically clamps inset/radius to prevent visual artifacts on small boxes
            - Not intended for direct public use; called internally by `draw_box()`
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show:
            return frame

        left, top, right, bottom, width, height, radius, center_x, center_y = self._get_box_coords(frame=frame,
                                                                                                   box=box,
                                                                                                   style=active_style)

        color = color if color is not None else active_style.palette[0]
        inset = max(active_style.thickness, 5)
        inner = max(1, radius - inset)

        max_reasonable_inset = min(width, height) * 0.04  # Максимум 15% размера бокса

        if inset > max_reasonable_inset:
            logger.warning(f"Inset={inset}px is too big for {width}x{height}! Inset set to {max_reasonable_inset}!")
            inset = int(max_reasonable_inset)
            inner = max(1, radius - inset)

        cv2.rectangle(frame, (left + inset + inner, top + inset), (right - inner - inset, bottom - inset), color, -1)
        cv2.rectangle(frame, (left + inset, top + inset + inner), (right - inset, bottom - inner - inset), color, -1)

        cv2.circle(frame, (left + inset + inner, top + inset + inner), inner, color, -1)
        cv2.circle(frame, (right - inset - inner, top + inset + inner), inner, color, -1)
        cv2.circle(frame, (left + inset + inner, bottom - inset - inner), inner, color, -1)
        cv2.circle(frame, (right - inset - inner, bottom - inset - inner), inner, color, -1)

        return frame

    @validate_frame
    def _get_box_coords(self,
                        frame: np.ndarray,
                        box: Box,
                        style: Optional[Style] = None) -> tuple:
        """Internal method converting normalized box coordinates to pixel coordinates.

        Args:
            frame: Input frame for dimension reference
            box: Bounding box with normalized coordinates (0.0-1.0)
            style: Style object for rounding parameter

        Returns:
            tuple: (left, top, right, bottom, width, height, radius, center_x, center_y)

        Raises:
            TypeError: If style is provided but not a Style instance

        Note:
            - Converts normalized coordinates to pixel space based on frame dimensions
            - Calculates corner radius based on box size and style.rounding
            - Returns all necessary coordinates for rounded box rendering
            - Not intended for direct public use
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        active_style = style if style is not None else self.style
        h, w = frame.shape[:2]
        left, top, right, bottom,center_x, center_y = (
            int(box.left * w),
            int(box.top * h),
            int(box.right * w),
            int(box.bottom * h),
            int(box.center.X * w),
            int(box.center.Y * h)
        )
        width, height = right - left, bottom - top
        radius = int(min(width, height) * active_style.rounding)
        return left, top, right, bottom, width, height, radius, center_x, center_y


    def _validate_frame(self, frame: np.ndarray) -> bool:
        """
        Internal method validating input frames for drawing operations.

        Args:
            frame (np.ndarray): Frame to validate.

        Returns:
            bool: True if frame is valid, False otherwise.

        Note:
            - Checks for None, empty dimensions, or zero size
            - Logs appropriate error messages for different failure cases
            - Used by `@validate_frame` decorator across all drawing methods
            - Not intended for direct public use
        """
        if frame is not None:
            h, w = frame.shape[:2]
            if h == 0 or w == 0 or frame.size == 0:
                logger.error(f"Frame пустой: {w}x{h}")
                return False
            return True

        logger.error("Frame is None!")
        return False



