# acmenra_cv/render/draw.py

import cv2
import math
import logging
import numpy as np
from functools import wraps
from typing import List, Tuple, Optional

from acmenra_cv.render.style import Style
from acmenra_cv.tracker.tracker import TrackedObject
from acmenra_cv.instance import Point, Box, Obb, Polygon, Instance, TaskType, DeviceType


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.stroke.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 all 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 configuration to apply.

        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, canvas: np.ndarray, x: int, y: int,  text: str, style: Optional[Style] = None) -> np.ndarray:
        """
        Renders text on the frame at specified pixel coordinates.

        Args:
            canvas (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 and active_style.font is not None:
            return canvas

        return cv2.putText(canvas,
                           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
        stroke_canvas = np.zeros_like(frame)
        fill_canvas = np.zeros_like(frame)
        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:
                fill_canvas = self.draw_box_fill(frame=fill_canvas,
                                                 instance=tracked_object.instance,
                                                 style=style,
                                                 color=color)
                stroke_canvas = self.draw_box_stroke(frame=stroke_canvas,
                                                     instance=tracked_object.instance,
                                                     style=style,
                                                     color=color)
            if is_obb:
                fill_canvas = self.draw_obb_fill(frame=fill_canvas,
                                                 instance=tracked_object.instance,
                                                 style=style,
                                                 color=color)
                stroke_canvas = self.draw_obb_stroke(frame=stroke_canvas,
                                                     instance=tracked_object.instance,
                                                     style=style,
                                                     color=color)
            if is_polygon:
                fill_canvas = self.draw_polygon_fill(frame=fill_canvas,
                                                     instance=tracked_object.instance,
                                                     style=style,
                                                     color=color)
                stroke_canvas = self.draw_polygon_stroke(frame=stroke_canvas,
                                                         instance=tracked_object.instance,
                                                         style=style,
                                                         color=color)
            # if is_pill:
            #     angle = 15.0
            #     fill_canvas = self.draw_pill_fill(frame=fill_canvas,
            #                                       instance=tracked_object.instance, angle=angle,
            #                                       style=style,
            #                                       color=color)
            #     stroke_canvas = self.draw_pill_stroke(frame=stroke_canvas,
            #                                           instance=tracked_object.instance, angle=angle,
            #                                           style=style,
            #                                           color=color)

            if is_trajectory:
                stroke_canvas = self.draw_trajectory(frame=stroke_canvas,
                                                     instance=tracked_object.instance,
                                                     trajectory=tracked_object.trajectory.get_values(),
                                                     style=style,
                                                     color=color)


        gray = cv2.cvtColor(fill_canvas, cv2.COLOR_BGR2GRAY)
        if cv2.countNonZero(gray) > 0:
            combined_alpha = active_style.fill.alpha * active_style.alpha
            _, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
            blended = cv2.addWeighted(frame, 1.0 - combined_alpha, fill_canvas, combined_alpha, 0.0)
            cv2.copyTo(blended, mask, frame)

        gray = cv2.cvtColor(stroke_canvas, cv2.COLOR_BGR2GRAY)
        if cv2.countNonZero(gray) > 0:
            combined_alpha = active_style.stroke.alpha * active_style.alpha
            _, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
            blended = cv2.addWeighted(frame, 1.0 - combined_alpha, stroke_canvas, combined_alpha, 0.0)
            cv2.copyTo(blended, mask, frame)
        return frame

    @validate_frame
    def draw_box_fill(self,
                      canvas: np.ndarray,
                      instance: Instance,
                      style: Optional[Style] = None,
                      color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
        """
        Renders the filled interior of an axis-aligned bounding box with rounded corners.

        Args:
            canvas (np.ndarray): Input frame to draw on.
            instance (Instance): Detected object instance containing box geometry.
            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: Canvas with the filled bounding box rendered.

        Raises:
            TypeError: If style is provided but not a Style instance, or instance is not an Instance.

        Note:
            - Uses a combination of rectangles and circles to create rounded corners
            - Automatically calculates visual inset based on stroke thickness to prevent overlap
            - Returns original canvas if `style.show` is False or `style.fill` is None
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        if not isinstance(instance, Instance):
            raise TypeError(f"'instance' must be '<Instance>', but got {type(instance).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show and active_style.fill is None:
            return canvas
        color = color if color is not None else active_style.palette[instance.class_id]

        left, top, right, bottom, width, height, radius, center_x, center_y = self._get_box_coords(frame=canvas,
                                                                                                   instance=instance,
                                                                                                   style=active_style)
        inset = max(active_style.stroke.thickness, 5)
        inner = max(1, radius - inset)

        max_reasonable_inset = min(width, height) * 0.04

        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(canvas, (left + inset + inner, top + inset), (right - inner - inset, bottom - inset), color, -1)
        cv2.rectangle(canvas, (left + inset, top + inset + inner), (right - inset, bottom - inner - inset), color, -1)

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

        return canvas

    @validate_frame
    def draw_box_stroke(self,
                        canvas: np.ndarray,
                        instance: Instance,
                        style: Optional[Style] = None,
                        color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
        """
        Renders the outline (stroke) of an axis-aligned bounding box with segmented corners.

        Args:
            canvas (np.ndarray): Input frame to draw on.
            instance (Instance): Detected object instance containing box geometry.
            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: Canvas with the bounding box outline rendered.

        Raises:
            TypeError: If style is provided but not a Style instance, or instance is not an Instance.

        Note:
            - Draws "firmware-style" segmented corners using ellipses and lines
            - Includes a center point indicator for precise spatial reference
            - Returns original canvas if `style.show` is False or `style.stroke` is None
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        if not isinstance(instance, Instance):
            raise TypeError(f"'instance' must be '<Instance>', but got {type(instance).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show and active_style.stroke:
            return canvas
        color = color if color is not None else active_style.palette[instance.class_id]

        left, top, right, bottom, width, height, radius, center_x, center_y = self._get_box_coords(frame=canvas,
                                                                                                   instance=instance,
                                                                                                   style=active_style)
        segment = active_style.stroke.segment
        thickness = active_style.stroke.thickness
        gline = int(width * segment)
        vline = int(height * segment)

        # LeftTop segment
        cv2.ellipse(canvas, (left + radius, top + radius), (radius, radius), 180 % 360, 0, 90, color, thickness, cv2.LINE_AA)
        cv2.line(canvas, (left + radius, top),(left + radius + gline, top), color, thickness) # goris
        cv2.line(canvas, (left, top + radius), (left, top + radius + vline), color, thickness) # vert

        # RightTop segment
        cv2.ellipse(canvas, (right - radius, top + radius), (radius, radius), 270 % 360, 0, 90, color, thickness, cv2.LINE_AA)
        cv2.line(canvas, (right - radius, top), (right - radius - gline, top), color, thickness) # goris
        cv2.line(canvas, (right, top + radius), (right, top + radius + vline), color, thickness) # vert

        # RightBottom segment
        cv2.ellipse(canvas, (right - radius, bottom - radius), (radius, radius), 0 % 360, 0, 90, color, thickness, cv2.LINE_AA)
        cv2.line(canvas, (right - radius, bottom), (right - radius - gline, bottom), color, thickness)  # goris
        cv2.line(canvas, (right, bottom - radius), (right, bottom - radius - vline), color, thickness)  # vert

        # LeftBottom segment
        cv2.ellipse(canvas, (left + radius, bottom - radius), (radius, radius), 90 % 360, 0, 90, color, thickness, cv2.LINE_AA)
        cv2.line(canvas, (left + radius, bottom), (left + radius + gline, bottom), color, thickness)  # goris
        cv2.line(canvas, (left, bottom - radius), (left, bottom - radius - vline), color, thickness) # vert
        cv2.circle(canvas, (center_x, center_y), radius, color, thickness)
        return canvas

    @validate_frame
    def draw_obb_fill(self,
                      canvas: np.ndarray,
                      instance: Instance,
                      style: Optional[Style] = None,
                      color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
        """
        Renders the filled interior of an oriented bounding box (OBB) with rounded corners.

        Args:
            canvas (np.ndarray): Input frame to draw on.
            instance (Instance): Detected object instance containing OBB geometry.
            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: Canvas with the filled oriented bounding box rendered.

        Raises:
            TypeError: If style is provided but not a Style instance, or instance is not an Instance.

        Note:
            - Uses an octagon and 4 ellipses to create smooth, rotated rounded corners
            - Automatically calculates visual inset based on stroke thickness
            - Returns original canvas if `style.show` is False or `style.fill` is None
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        if not isinstance(instance, Instance):
            raise TypeError(f"'instance' must be '<Instance>', but got {type(instance).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show or active_style.fill is None:
            return canvas

        color = color if color is not None else active_style.palette[instance.class_id]
        center_x, center_y, width, height, yaw, radius = self._get_obb_coords(frame=canvas,
                                                                              instance=instance,
                                                                              style=active_style)

        inset = max(active_style.stroke.thickness if active_style.stroke else 5, 5)
        inner = max(1, radius - inset)
        max_reasonable_inset = int(min(width, height) * 0.04)

        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 = max_reasonable_inset
            inner = max(1, radius - inset)

        cos_a, sin_a = math.cos(math.radians(yaw)), math.sin(math.radians(yaw))

        hw_i = width / 2 - inset
        hh_i = height / 2 - inset

        oct_pts = np.array([
            [int(center_x + (-hw_i + inner) * cos_a + hh_i * sin_a), int(center_y + (-hw_i + inner) * sin_a - hh_i * cos_a)],
            [int(center_x + (hw_i - inner) * cos_a + hh_i * sin_a), int(center_y + (hw_i - inner) * sin_a - hh_i * cos_a)],

            [int(center_x + hw_i * cos_a + (hh_i - inner) * sin_a), int(center_y + hw_i * sin_a - (hh_i - inner) * cos_a)],
            [int(center_x + hw_i * cos_a - (hh_i - inner) * sin_a), int(center_y + hw_i * sin_a + (hh_i - inner) * cos_a)],

            [int(center_x + (hw_i - inner) * cos_a - hh_i * sin_a), int(center_y + (hw_i - inner) * sin_a + hh_i * cos_a)],
            [int(center_x + (-hw_i + inner) * cos_a - hh_i * sin_a), int(center_y + (-hw_i + inner) * sin_a + hh_i * cos_a)],

            [int(center_x - hw_i * cos_a - (hh_i - inner) * sin_a), int(center_y - hw_i * sin_a + (hh_i - inner) * cos_a)],
            [int(center_x - hw_i * cos_a + (hh_i - inner) * sin_a), int(center_y - hw_i * sin_a - (hh_i - inner) * cos_a)]
        ], dtype=np.int32)

        cv2.fillPoly(canvas, [oct_pts], color)


        cv2.ellipse(canvas,
                    (int(center_x + (-hw_i + inner) * cos_a + (-hh_i + inner) * sin_a),
                     int(center_y + (-hw_i + inner) * sin_a - (-hh_i + inner) * cos_a)),
                    (inner, inner), (90 + yaw) % 360, 0, 90, color, -1, cv2.LINE_AA)

        cv2.ellipse(canvas,
                    (int(center_x + (hw_i - inner) * cos_a + (hh_i - inner) * sin_a),
                     int(center_y + (hw_i - inner) * sin_a - (hh_i - inner) * cos_a)),
                    (inner, inner), (270 + yaw) % 360, 0, 90, color, -1, cv2.LINE_AA)

        cv2.ellipse(canvas,
                    (int(center_x + (-hw_i + inner) * cos_a + (hh_i - inner) * sin_a),
                     int(center_y + (-hw_i + inner) * sin_a - (hh_i - inner) * cos_a)),
                    (inner, inner), (180 + yaw) % 360, 0, 90, color, -1, cv2.LINE_AA)

        cv2.ellipse(canvas,
                    (int(center_x + (hw_i - inner) * cos_a + (-hh_i + inner) * sin_a),
                     int(center_y + (hw_i - inner) * sin_a - (-hh_i + inner) * cos_a)),
                    (inner, inner), (0 + yaw) % 360, 0, 90, color, -1, cv2.LINE_AA)

        return canvas

    @validate_frame
    def draw_obb_stroke(self,
                        canvas: np.ndarray,
                        instance: Instance,
                        style: Optional[Style] = None,
                        color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
        """
        Renders the outline (stroke) of an oriented bounding box (OBB) with segmented corners.

        Args:
            canvas (np.ndarray): Input frame to draw on.
            instance (Instance): Detected object instance containing OBB geometry.
            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: Canvas with the oriented bounding box outline rendered.

        Raises:
            TypeError: If style is provided but not a Style instance, or instance is not an Instance.

        Note:
            - Draws rotated "firmware-style" segmented corners using trigonometric transformations
            - Includes a center point indicator for precise spatial reference
            - Returns original canvas if `style.show` is False or `style.stroke` is None
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        if not isinstance(instance, Instance):
            raise TypeError(f"'instance' must be '<Instance>', but got {type(instance).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show or active_style.stroke is None:
            return canvas

        color = color if color is not None else active_style.palette[instance.class_id]
        center_x, center_y, width, height, yaw, radius = self._get_obb_coords(frame=canvas,
                                                                              instance=instance,
                                                                              style=active_style)

        segment = active_style.stroke.segment
        thickness = active_style.stroke.thickness
        cos_a, sin_a = math.cos(math.radians(yaw)), math.sin(math.radians(yaw))
        dx, dy = width / 2 - radius, height / 2 - radius
        gline = int(width * segment)
        vline = int(height * segment)

        # =========================================================================
        # TopLeft segment
        # =========================================================================
        center = (int(center_x - dx * cos_a + dy * sin_a), int(center_y - dx * sin_a - dy * cos_a))
        cv2.ellipse(canvas, center, (radius, radius), (180 + yaw) % 360, 0, 90, color, thickness, cv2.LINE_AA)

        h_start = (int(center[0] - radius * -sin_a), int(center[1] - radius * cos_a))
        h_end = (int(h_start[0] + gline * cos_a), int(h_start[1] + gline * sin_a))
        cv2.line(canvas, h_start, h_end, color, thickness)

        v_start = (int(center[0] - radius * cos_a), int(center[1] - radius * sin_a))
        v_end = (int(v_start[0] + vline * -sin_a), int(v_start[1] + vline * cos_a))
        cv2.line(canvas, v_start, v_end, color, thickness)

        # =========================================================================
        # RightTop segment
        # =========================================================================
        center = (int(center_x + dx * cos_a + dy * sin_a), int(center_y + dx * sin_a - dy * cos_a))
        cv2.ellipse(canvas, center, (radius, radius), (270 + yaw) % 360, 0, 90, color, thickness, cv2.LINE_AA)

        h_start = (int(center[0] - radius * -sin_a), int(center[1] - radius * cos_a))
        h_end = (int(h_start[0] - gline * cos_a), int(h_start[1] - gline * sin_a))
        cv2.line(canvas, h_start, h_end, color, thickness)

        v_start = (int(center[0] + radius * cos_a), int(center[1] + radius * sin_a))
        v_end = (int(v_start[0] + vline * -sin_a), int(v_start[1] + vline * cos_a))
        cv2.line(canvas, v_start, v_end, color, thickness)

        # =========================================================================
        # RightBottom segment
        # =========================================================================
        center = (int(center_x + dx * cos_a - dy * sin_a), int(center_y + dx * sin_a + dy * cos_a))
        cv2.ellipse(canvas, center, (radius, radius), (0 + yaw) % 360, 0, 90, color, thickness, cv2.LINE_AA)

        h_start = (int(center[0] + radius * -sin_a), int(center[1] + radius * cos_a))
        h_end = (int(h_start[0] - gline * cos_a), int(h_start[1] - gline * sin_a))
        cv2.line(canvas, h_start, h_end, color, thickness)

        v_start = (int(center[0] + radius * cos_a), int(center[1] + radius * sin_a))
        v_end = (int(v_start[0] - vline * -sin_a), int(v_start[1] - vline * cos_a))
        cv2.line(canvas, v_start, v_end, color, thickness)

        # =========================================================================
        # LeftBottom segment
        # =========================================================================
        center = (int(center_x - dx * cos_a - dy * sin_a), int(center_y - dx * sin_a + dy * cos_a))
        cv2.ellipse(canvas, center, (radius, radius), (90 + yaw) % 360, 0, 90, color, thickness, cv2.LINE_AA)

        h_start = (int(center[0] - radius * sin_a), int(center[1] + radius * cos_a))
        h_end = (int(h_start[0] + gline * cos_a), int(h_start[1] + gline * sin_a))
        cv2.line(canvas, h_start, h_end, color, thickness)

        v_start = (int(center[0] - radius * cos_a), int(center[1] - radius * sin_a))
        v_end = (int(v_start[0] - vline * -sin_a), int(v_start[1] - vline * cos_a))
        cv2.line(canvas, v_start, v_end, color, thickness)

        cv2.circle(canvas, (center_x, center_y), radius, color, thickness)
        return canvas

    @validate_frame
    def draw_polygon_fill(self,
                          canvas: np.ndarray,
                          instance: Instance,
                          style: Optional[Style] = None,
                          color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
        """
        Renders the filled interior of a segmentation polygon.

        Args:
            canvas (np.ndarray): Input frame to draw on.
            instance (Instance): Detected object instance containing polygon geometry.
            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: Canvas with the filled segmentation polygon rendered.

        Raises:
            TypeError: If style is provided but not a Style instance, or instance is not an Instance.

        Note:
            - Applies polygon smoothing based on `style.smooth` before rendering
            - Converts normalized coordinates to absolute pixel space automatically
            - Returns original canvas if `style.show` is False or `style.fill` is None
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        if not isinstance(instance, Instance):
            raise TypeError(f"'instance' must be '<Instance>', but got {type(instance).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show or active_style.fill is None:
            return canvas
        color = color if color is not None else active_style.palette[instance.class_id]

        h, w = canvas.shape[:2]
        pts = instance.polygon.smooth(n=active_style.smooth).to_absolute_array(w, h).astype(np.int32).reshape(-1, 1, 2)
        cv2.fillPoly(canvas, [pts], color, cv2.LINE_AA)
        return canvas

    @validate_frame
    def draw_polygon_stroke(self,
                            canvas: np.ndarray,
                            instance: Instance,
                            style: Optional[Style] = None,
                            color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
        """
        Renders the outline (stroke) of a segmentation polygon.

        Args:
            canvas (np.ndarray): Input frame to draw on.
            instance (Instance): Detected object instance containing polygon geometry.
            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: Canvas with the segmentation polygon outline rendered.

        Raises:
            TypeError: If style is provided but not a Style instance, or instance is not an Instance.

        Note:
            - Applies polygon smoothing based on `style.smooth` before rendering
            - Uses anti-aliased lines (`cv2.LINE_AA`) for smooth edges
            - Returns original canvas if `style.show` is False or `style.stroke` is None
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        if not isinstance(instance, Instance):
            raise TypeError(f"'instance' must be '<Instance>', but got {type(instance).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show or active_style.stroke is None:
            return canvas
        color = color if color is not None else active_style.palette[instance.class_id]

        h, w = canvas.shape[:2]
        thickness = active_style.stroke.thickness
        pts = instance.polygon.smooth(n=active_style.smooth).to_absolute_array(w, h).astype(np.int32).reshape(-1, 1, 2)
        cv2.polylines(canvas, [pts], isClosed=True, color=color, thickness=thickness, lineType=cv2.LINE_AA)
        return canvas

    @validate_frame
    def draw_trajectory(self,
                        canvas: np.ndarray,
                        instance: Instance,
                        trajectory: List[Point],
                        style: Optional[Style] = None,
                        color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
        """
        Renders the movement trajectory of a tracked object as a connected line sequence.

        Args:
            canvas (np.ndarray): Input frame to draw on.
            instance (Instance): Detected object instance (used for validation and context).
            trajectory (List[Point]): List of historical points representing movement.
            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: Canvas with the object trajectory rendered.

        Raises:
            TypeError: If style is provided but not a Style instance, or instance is not an Instance.

        Note:
            - Automatically filters out None values from the trajectory list
            - Converts normalized point coordinates to absolute pixel space
            - Returns original canvas if `style.show` is False or `style.stroke` is None
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        if not isinstance(instance, Instance):
            raise TypeError(f"'instance' must be '<Instance>', but got {type(instance).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show and active_style.stroke:
            return canvas
        color = color if color is not None else active_style.palette[instance.class_id]

        h, w = canvas.shape[:2]
        thickness = active_style.stroke.thickness
        valid_points = [p for p in trajectory if p is not None]

        for i in range(len(valid_points) - 1):
            cv2.line(canvas,
                     (int(valid_points[i].X * w), int(valid_points[i].Y * h)),
                     (int(valid_points[i + 1].X * w), int(valid_points[i + 1].Y * h)),
                     color=color,
                     thickness=thickness,
                     lineType=cv2.LINE_AA)

        return canvas

    @validate_frame
    def draw_pill_fill(self,
                       canvas: np.ndarray,
                       instance: Instance,
                       angle: float = 0.0,
                       style: Optional[Style] = None,
                       color: Optional[Tuple[int, int, int]] = None) -> np.ndarray: # TODO needs correction on next versions
        """
        Renders the filled background of an adaptive pill-shaped badge.

        Args:
            canvas (np.ndarray): Input frame to draw on.
            instance (Instance): Detected object instance containing class and confidence data.
            angle (float): Rotation angle for the badge (reserved for future use).
            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: Canvas with the pill badge background rendered.

        Raises:
            TypeError: If style is provided but not a Style instance, or instance is not an Instance.

        Note:
            - Badge structure: [circle]—[rectangle]—[circle] (perfect rounded ends)
            - Automatically calculates badge size based on text dimensions
            - Returns original canvas if `style.show` is False or `style.fill` is None
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        if not isinstance(instance, Instance):
            raise TypeError(f"'instance' must be '<Instance>', but got {type(instance).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show and active_style.stroke:
            return canvas
        color = color if color is not None else active_style.palette[instance.class_id]

        text = f"{instance.class_id}:{instance.category} {instance.conf}%"
        (tw, th), baseline = cv2.getTextSize(text,
                                             active_style.font.font,
                                             active_style.font.font_scale,
                                             active_style.font.thickness)

        h, w = canvas.shape[:2]
        badge_x = int(instance.box.center.X * w)
        badge_y = int(instance.box.center.Y * h)

        cv2.rectangle(canvas,
                      (badge_x - tw // 2 - th, badge_y - th),
                      (badge_x - tw // 2 + tw + th, badge_y + 3 * th - baseline // 2),
                      color, -1, cv2.LINE_8)
        cv2.circle(canvas,
                   (badge_x - tw // 2 - th, badge_y + th - baseline // 4),
                   (2 * th - baseline // 4), color, -1, cv2.LINE_8)
        cv2.circle(canvas,
                   (badge_x + tw // 2 + th, badge_y + th - baseline // 4),
                   (2 * th - baseline // 4), color, -1, cv2.LINE_8)
        return canvas

    @validate_frame
    def draw_pill_stroke(self,
                         canvas: np.ndarray,
                         instance: Instance,
                         angle: float = 0.0,
                         style: Optional[Style] = None,
                         color: Optional[Tuple[int, int, int]] = None) -> np.ndarray: # TODO needs correction on next versions
        """
        Renders the text content inside an adaptive pill-shaped badge.

        Args:
            canvas (np.ndarray): Input frame to draw on.
            instance (Instance): Detected object instance containing class and confidence data.
            angle (float): Rotation angle for the badge (reserved for future use).
            style (Optional[Style]): Optional style override. Defaults to None.
            color (Optional[Tuple[int, int, int]]): Optional BGR color tuple (ignored for text).

        Returns:
            np.ndarray: Canvas with the pill badge text rendered.

        Raises:
            TypeError: If style is provided but not a Style instance, or instance is not an Instance.

        Note:
            - Text is always rendered horizontally for maximum readability
            - Uses high-contrast color (white/black) regardless of palette
            - Returns original canvas if `style.show` is False or `style.stroke` is None
        """
        if style is not None and not isinstance(style, Style):
            raise TypeError(f"'style' must be '<Style>', but got {type(style).__name__}")
        if not isinstance(instance, Instance):
            raise TypeError(f"'instance' must be '<Instance>', but got {type(instance).__name__}")
        active_style = style if style is not None else self.style
        if not active_style.show and active_style.stroke:
            return canvas
        color = color if color is not None else active_style.palette[instance.class_id]

        text = f"{instance.class_id}:{instance.category} {instance.conf}%"
        (tw, th), baseline = cv2.getTextSize(text,
                                             active_style.font.font,
                                             active_style.font.font_scale,
                                             active_style.font.thickness)
        h, w = canvas.shape[:2]
        badge_x = int(instance.box.center.X * w)
        badge_y = int(instance.box.center.Y * h)

        text_color = (1, 1, 1)  # чёрный для контраста
        cv2.putText(canvas,
                    text,
                    (badge_x- tw // 2, badge_y + th + baseline // 2),
                    active_style.font.font,
                    active_style.font.font_scale,
                    text_color,
                    active_style.font.thickness, cv2.LINE_8
        )
        return canvas

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

        Args:
            frame (np.ndarray): Input frame for dimension reference.
            instance (Instance): Bounding box with normalized coordinates (0.0-1.0).
            style (Optional[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__}")
        if not isinstance(instance, Instance):
            raise TypeError(f"'instance' must be '<Instance>', but got {type(instance).__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(instance.box.left * w),
            int(instance.box.top * h),
            int(instance.box.right * w),
            int(instance.box.bottom * h),
            int(instance.box.center.X * w),
            int(instance.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


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

        Args:
            frame (np.ndarray): Input frame for dimension reference.
            instance (Instance): Oriented bounding box with normalized coordinates.
            style (Optional[Style]): Style object for rounding parameter.

        Returns:
            tuple: (center_x, center_y, width, height, yaw, radius).

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

        Note:
            - Converts normalized center and dimensions to pixel space
            - Calculates corner radius based on minimum dimension and style.rounding
            - 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__}")
        if not isinstance(instance, Instance):
            raise TypeError(f"'instance' must be '<Instance>', but got {type(instance).__name__}")
        active_style = style if style is not None else self.style
        h, w = frame.shape[:2]
        center_x, center_y, width, height, yaw = (int(instance.obb.x * w),
                                                  int(instance.obb.y * h),
                                                  int(instance.obb.width * w),
                                                  int(instance.obb.height * h),
                                                  instance.obb.yaw)
        radius = int(min(width, height) * active_style.rounding)
        return  center_x, center_y, width, height, yaw, radius


    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 is empty: {w}x{h}")
                return False
            return True

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


