# acmenra_cv/render/style/style.py

import logging
from typing import Optional, List, Tuple, Dict, Any

from acmenra_cv.render.style.font import Font
from acmenra_cv.render.style.fill import Fill
from acmenra_cv.render.style.stroke import Stroke
from acmenra_cv.render.style.enums import LabelPosition


logger = logging.getLogger(__name__)


class Style:
    """
    Centralized configuration container for visualization and rendering parameters.

    This class manages all stylistic aspects of drawing operations, including color palettes,
    geometry smoothing, transparency compositing, text rendering, and stroke/fill toggles.
    Designed to provide a consistent, type-safe interface for configuring visual elements
    across computer vision pipelines and ADAS monitoring systems.

    Key features:
    - Strict type and range validation for all visualization parameters
    - Unique color palette enforcement with RGB tuple validation
    - Optional stroke/fill layers with independent alpha control
    - Global alpha overlay applied post-composition (100% drawing -> scaled by global alpha)
    - Configurable label positioning enum
    - Global show/hide toggle for resource optimization on embedded devices

    Designed for seamless integration with downstream rendering utilities such as:
    - Drawer class for frame overlay operations
    - Real-time visualization pipelines
    - Multi-camera monitoring systems
    - Performance-optimized embedded deployments (e.g., Raspberry Pi, Jetson)

    Attributes:
        _font (Font): Font configuration for text rendering elements.
        _palette (List[Tuple[int, int, int]]): List of unique RGB color tuples.
        _stroke (Optional[Stroke]): Stroke outline configuration. None = skip strokes.
        _fill (Optional[Fill]): Interior fill configuration. None = skip fills.
        _smooth (int): Global smoothing factor for polygon vertices.
        _alpha (float): Global overlay opacity [0.0, 1.0] applied after stroke+fill composition.
        _show (bool): Global toggle for visualization rendering.
        _label (LabelPosition): Positioning for rendered labels/badges.

    Note:
        - `show=False` completely disables rendering for performance optimization
        - Global `alpha` scales the final composite (stroke=100%, fill=100% -> final=alpha%)
        - All numeric parameters are validated on assignment
        - Palette colors must be unique RGB tuples within [0, 255] range
    """

    def __init__(self,
                 palette: List[tuple],
                 font: Font,
                 stroke: Optional[Stroke] = None,
                 fill: Optional[Fill] = None,
                 smooth: int = 2,
                 rounding: float = 0.04,
                 alpha: float = 0.65,
                 show: bool = False,
                 label: LabelPosition = LabelPosition.TOP): # tested (good)
        """
        Initializes a Style object with comprehensive visualization parameters.

        Args:
            palette (List[Tuple[int, int, int]]): List of unique RGB color tuples for object visualization.
            font (Font): Font configuration for text rendering elements.
            stroke (Optional[Stroke], optional): Stroke configuration. Defaults to None.
            fill (Optional[Fill], optional): Fill configuration. Defaults to None.
            smooth (int, optional): Global smoothing factor for polygon vertices. Defaults to 2.
            rounding (float, optional): Corner rounding factor for bounding boxes. Defaults to 0.04.
            alpha (float, optional): Global overlay opacity [0.0, 1.0]. Defaults to 0.65.
            show (bool, optional): Global toggle for visualization rendering.
                Defaults to False (disabled for resource optimization on embedded devices).
            label (LabelPosition, optional): Positioning for labels/badges. Defaults to TOP.

        Raises:
            TypeError: If any parameter has incorrect type.
            ValueError: If numeric parameters are outside valid ranges or palette contains duplicate colors.

        Note:
            - `show=False` bypasses all drawing operations for maximum performance
            - Palette colors must be unique to avoid class ID mapping conflicts
            - All validation is delegated to property setters
        """
        self.palette = palette
        self.font = font
        self.stroke = stroke
        self.fill = fill
        self.smooth = smooth
        self.rounding = rounding
        self.alpha = alpha
        self.show = show
        self.label = label

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

        Returns:
            str: Concise summary with key rendering parameters.
                 Format: "Style(show=..., alpha=..., rounding=..., label=...)"

        Note:
            - Optimized for console logging, operator dashboards, and alert systems
            - Omits complex objects (palette, font instances) to keep output clean
            - Uses fixed-point precision for immediate visual clarity
        """
        return (f"Style(show={self.show}, alpha={self.alpha:.2f}, "
                f"rounding={self.rounding:.2f}, label={self.label.value})")

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

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

        Note:
            - Includes palette length to avoid massive output in logs
            - All styling objects and flags printed with full precision
            - Essential for debugging pipeline style overrides and state traceability
        """
        return (f"Style(palette_len={len(self._palette)}, font={self.font!r}, "
                f"stroke={self.stroke!r}, fill={self.fill!r}, "
                f"smooth={self.smooth!r}, rounding={self.rounding!r}, "
                f"alpha={self.alpha!r}, show={self.show!r}, label={self.label!r})")

    @property
    def font(self) -> Font: # tested (good)
        """
        Font: Gets the font configuration used for text rendering.

        Returns:
            Font: Active font object containing face, scale, thickness, and color.

        Note:
            - Inherited by all drawing operations unless explicitly overridden per-call.
            - Changes affect all subsequent text and badge rendering immediately.
        """
        return self._font

    @font.setter
    def font(self, font: Font) -> None: # tested (good)
        """
        Sets the font configuration for text rendering.

        Args:
            font (Font): Font object to set.

        Note:
            - Validation is currently deferred to the Font class constructor.
            - Accepts only valid Font instances to maintain rendering consistency.
        """
        # Validation commented out as per current architecture; Font class handles it internally.
        if font is not None and not isinstance(font, Font):
            raise TypeError(f"'font' must be '<Font>', but got {type(font).__name__}")
        self._font = font

    @property
    def palette(self) -> List[Tuple[int, int, int]]: # tested (good)
        """
        List[Tuple[int, int, int]]: Gets the color palette for object visualization.

        Returns:
            List[Tuple[int, int, int]]: Unique RGB tuples mapped to class IDs.

        Note:
            - Index 0 corresponds to class_id=0, index 1 to class_id=1, etc.
            - Length should match or exceed the number of object classes in your model.
            - Colors are validated for RGB range and uniqueness on assignment.
        """
        return self._palette

    @palette.setter
    def palette(self, palette: List[Tuple[int, int, int]]) -> None: # tested (good)
        """
        Sets the color palette with comprehensive validation.

        Args:
            palette (List[Tuple[int, int, int]]): New color palette to set.

        Raises:
            TypeError: If palette is not a list or contains invalid color formats.
            ValueError: If RGB values are outside [0, 255] or colors are not unique.

        Note:
            - Duplicate colors are rejected to prevent class ID visual collisions.
            - Changes apply globally to all instances using this style.
        """
        if not isinstance(palette, list):
            raise TypeError(f"'palette' must be '<List[Tuple[int, int, int]]>', but got {type(palette).__name__}")

        for color in palette:
            if not isinstance(color, (tuple, list)) or len(color) != 3:
                raise TypeError(f"'color' must be '<Tuple[int, int, int]>', but got {type(color).__name__}")
            for value in color:
                if not isinstance(value, int) or isinstance(value, bool):
                    raise TypeError(f"All elements in 'color' must be '<int>', but got {type(value).__name__}")
                if not 0 <= value <= 255:
                    raise ValueError(f"'value' should be in [0, 255], but got '{value}'")

        if len(palette) != len(set(tuple(c) for c in palette)):
            raise ValueError(f"'colors' should have unique identifiers, but duplicates found in '{palette}'")
        self._palette = palette

    @property
    def stroke(self) -> Optional[Stroke]: # tested (good)
        """
        Optional[Stroke]: Gets the stroke outline configuration.

        Returns:
            Optional[Stroke]: Stroke settings if enabled, None to disable outlines.

        Note:
            - When None, only fill layers are rendered (if configured).
            - Controls thickness, corner segments, and independent stroke transparency.
        """
        return self._stroke

    @stroke.setter
    def stroke(self, stroke: Optional[Stroke]) -> None: # tested (good)
        """
        Sets the stroke configuration with validation.

        Args:
            stroke (Optional[Stroke]): Stroke settings to set. Pass None to disable.

        Raises:
            TypeError: If stroke is not None and not a Stroke instance.

        Note:
            - Toggling to None disables all outline rendering for performance.
            - Must be a valid Stroke object to maintain layer compositing logic.
        """
        if stroke is not None and not isinstance(stroke, Stroke):
            raise TypeError(f"'stroke' must be '<Stroke>' or None, but got {type(stroke).__name__}")
        self._stroke = stroke

    @property
    def rounding(self) -> float: # tested (good)
        """
        float: Gets the corner rounding factor for bounding boxes.

        Returns:
            float: Rounding factor in range [0.0, 0.5].

        Note:
            - 0.0 = sharp 90° corners (standard rectangle).
            - 0.5 = maximum curvature (capsule/pill shape at box ends).
            - Applied proportionally to box dimensions before segment calculation.
        """
        return self._rounding

    @rounding.setter
    def rounding(self, rounding: float) -> None: # tested (good)
        """
        Sets the corner rounding factor with range validation.

        Args:
            rounding (float): New rounding factor to set.

        Raises:
            TypeError: If rounding is not a float.
            ValueError: If rounding is outside [0.0, 0.5].

        Note:
            - Values outside range cause visual artifacts or OpenCV assertion errors.
            - Affects both Box and OBB corner radius calculations.
        """
        if not isinstance(rounding, float):
            raise TypeError(f"'rounding' must be '<float>', but got {type(rounding).__name__}")
        if not 0.0 <= rounding <= 0.5:
            raise ValueError(f"'rounding' must be [0, 0.5]")
        self._rounding = rounding

    @property
    def fill(self) -> Optional[Fill]: # tested (good)
        """
         Optional[Fill]: Gets the interior fill configuration.

         Returns:
             Optional[Fill]: Fill settings if enabled, None to disable fills.

         Note:
             - When None, only stroke layers are rendered (if configured).
             - Controls independent fill transparency for ghosting/overlay effects.
         """
        return self._fill

    @fill.setter
    def fill(self, fill: Optional[Fill]) -> None: # tested (good)
        """
        Sets the fill configuration with validation.

        Args:
            fill (Optional[Fill]): Fill settings to set. Pass None to disable.

        Raises:
            TypeError: If fill is not None and not a Fill instance.

        Note:
            - Toggling to None disables all interior rendering for performance.
            - Must be a valid Fill object to maintain layer compositing logic.
        """
        if fill is not None and not isinstance(fill, Fill):
            raise TypeError(f"'fill' must be '<Fill>' or None, but got {type(fill).__name__}")
        self._fill = fill

    @property
    def smooth(self) -> int: # tested (good)
        """
        int: Gets the global smoothing factor for polygon vertices.

        Returns:
            int: Smoothing iterations for polygon contour approximation.

        Note:
            - Higher values produce smoother, more organic mask boundaries.
            - Value 0 applies raw detection polygon without interpolation.
            - Applied via cv2.approxPolyDP or similar smoothing algorithms.
        """
        return self._smooth

    @smooth.setter
    def smooth(self, smooth: int) -> None: # tested (good)
        """
        Sets the polygon smoothing factor with validation.

        Args:
            smooth (int): Smoothing level to set. Must be >= 0.

        Raises:
            TypeError: If smooth is not an integer or is a boolean.
            ValueError: If smooth is negative.

        Note:
            - Negative values are mathematically invalid for smoothing iterations.
            - Large values may slightly impact rendering latency on embedded devices.
        """
        if not isinstance(smooth, int) or isinstance(smooth, bool):
            raise TypeError(f"'smooth' must be '<int>', but got {type(smooth).__name__}")
        if smooth < 0:
            raise ValueError(f"'smooth' must be >= 0, got {smooth}")
        self._smooth = smooth

    @property
    def alpha(self) -> float: # tested (good)
        """
        float: Gets the global overlay opacity applied post-composition.

        Returns:
            float: Global alpha multiplier in range [0.0, 1.0].

        Note:
            - Applied AFTER individual stroke/fill alphas are composited.
            - 1.0 = fully opaque final render. 0.0 = completely transparent.
            - Allows unified dimming/brightening of all visual elements simultaneously.
        """
        return self._alpha

    @alpha.setter
    def alpha(self, alpha: float) -> None: # tested (good)
        """
        Sets the global overlay opacity with range validation.

        Args:
            alpha (float): Global alpha multiplier to set. Must be in [0.0, 1.0].

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

        Note:
            - Multiplied with layer-specific alphas during frame blending.
            - Changes affect all subsequent drawing operations globally.
        """
        if not isinstance(alpha, float):
            raise TypeError(f"'alpha' must be '<float>', but got {type(alpha).__name__}")
        if not 0.0 <= alpha <= 1.0:
            raise ValueError(f"'alpha' must be in [0.0, 1.0], got {alpha}")
        self._alpha = alpha

    @property
    def label(self) -> LabelPosition: # tested (good)
        """
        LabelPosition: Gets the positioning for rendered labels/badges.

        Returns:
            LabelPosition: Enum member defining badge attachment point.

        Note:
            - Controls where class/confidence pills attach to bounding geometry.
            - Defaults to TOP for standard CV/ADAS dashboard layouts.
        """
        return self._label

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

        Args:
            label (LabelPosition): Enum position to set.

        Raises:
            TypeError: If label is not a LabelPosition enum member.

        Note:
            - Invalid types are rejected to prevent runtime layout errors.
            - Use LabelPosition.OFF to completely disable badge rendering.
        """
        if not isinstance(label, LabelPosition):
            raise TypeError(f"'label' must be '<LabelPosition>', but got {type(label).__name__}")
        self._label = label

    @property
    def show(self) -> bool: # tested (good)
        """
        bool: Gets whether visualization rendering is globally enabled.

        Returns:
            bool: True if rendering is active, False to bypass all drawing.

        Note:
            - Primary performance toggle for headless or resource-constrained modes.
            - When False, all drawing methods return the original frame immediately.
        """
        return self._show

    @show.setter
    def show(self, show: bool) -> None: # tested (good)
        """
        Sets the global toggle for visualization rendering.

        Args:
            show (bool): Whether to enable rendering.

        Raises:
            TypeError: If show is not a boolean.

        Note:
            - Setting False bypasses CPU-intensive alpha blending and OpenCV calls.
            - Recommended for production inference when only tracking data is needed.
        """
        if not isinstance(show, bool):
            raise TypeError(f"'show' must be '<bool>', but got {type(show).__name__}")
        self._show = show

    def to_dict(self) -> Dict[str, Any]: # tested (good)
        """
        Serializes the Style configuration to a JSON-compatible dictionary.

        Returns:
            Dict[str, Any]: Dictionary containing all style rendering parameters.

        Note:
            - Nested objects (Font, Stroke, Fill) are serialized via their own to_dict() methods.
            - None values for optional stroke/fill are preserved.
            - LabelPosition enum is serialized by its value (string).
            - Palette is preserved as list of RGB tuples.
            - Ready for direct DB insertion or API transmission.
            - Maintains perfect round-trip fidelity with from_dict().
        """
        return {
            "palette": self.palette,
            "font": self.font.to_dict() if self.font else None,
            "stroke": self.stroke.to_dict() if self.stroke else None,
            "fill": self.fill.to_dict() if self.fill else None,
            "smooth": self.smooth,
            "rounding": self.rounding,
            "alpha": self.alpha,
            "show": self.show,
            "label": self.label.value
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'Style':
        """
        Reconstructs a Style instance from a serialized dictionary.

        Args:
            data: Dictionary with style configuration. Required: palette, font, smooth, rounding, alpha, show, label.
                  Optional: stroke, fill (may be None).

        Returns:
            Style: Fully initialized and validated instance.

        Raises:
            KeyError: If any required field is missing from input dictionary.
            TypeError: If any parameter has incorrect type.
            ValueError: If any parameter is outside valid range.

        Note:
            - Delegates validation to property setters.
            - Reconstructs nested objects (Font, Stroke, Fill) via their from_dict() methods.
            - Handles None values for optional stroke/fill correctly.
            - Converts label value back to LabelPosition enum.
            - Maintains perfect round-trip fidelity with to_dict().
        """
        required_fields = ['palette', 'font', 'smooth', 'rounding', 'alpha', 'show', 'label']
        for field in required_fields:
            if field not in data:
                raise KeyError(f"Required field '{field}' is missing from input dictionary: {data}")

        return cls(
            palette=data.get("palette"),
            font=Font.from_dict(data.get("font")) if data.get("font") else None,
            stroke=Stroke.from_dict(data.get("stroke")) if data.get("stroke") else None,
            fill=Fill.from_dict(data.get("fill")) if data.get("fill") else None,
            smooth=int(data.get("smooth")),
            rounding=float(data.get("rounding")),
            alpha=float(data.get("alpha")),
            show=bool(data.get("show")),
            label=LabelPosition(str(data.get("label")).lower())
        )


