"""
Bitmap canvas for rendering SVG shapes.

The canvas maintains a numpy array buffer and provides methods for drawing
shapes, compositing layers, and managing the output bitmap.
"""

import logging
from typing import Any

import numpy as np

from .colors import Color
from .shapes import Shape
from .styles import Style
from .transforms import Transform

logger = logging.getLogger(__name__)


class BitmapCanvas:
    """
    Bitmap canvas for rasterizing SVG shapes.

    Attributes:
        width: Canvas width in pixels.
        height: Canvas height in pixels.
        buffer: Numpy array (height, width, 4) uint8 for RGBA.
        background: Background color.
    """

    def __init__(self, width: int, height: int, background: Color | None = None):
        """
        Initialize canvas with given dimensions.

        Args:
            width: Canvas width in pixels (must be positive).
            height: Canvas height in pixels (must be positive).
            background: Background color (default transparent).

        Raises:
            ValueError: If width or height are not positive.
        """
        if width <= 0 or height <= 0:
            raise ValueError(f"Canvas dimensions must be positive, got {width}x{height}")

        self.width = width
        self.height = height
        self.buffer = np.zeros((height, width, 4), dtype=np.uint8)
        self.background = background or Color(255, 255, 255, 0)  # Transparent white

        # Initialize with background
        self.clear()

    def clear(self, color: Color | None = None) -> None:
        """
        Fill canvas with solid color.

        Args:
            color: Color to fill with (default uses background color).
        """
        fill_color = color or self.background
        self.buffer[:, :, 0] = fill_color.r
        self.buffer[:, :, 1] = fill_color.g
        self.buffer[:, :, 2] = fill_color.b
        self.buffer[:, :, 3] = fill_color.a

    def draw_shape(self, shape: Shape, transform: Transform, style: Style) -> None:
        """
        Rasterize and composite a shape onto the canvas.

        Args:
            shape: Shape geometry to draw.
            transform: Transformation to apply to shape.
            style: Styling (fill, stroke, opacity, etc.).
        """
        # Import here to avoid circular dependency
        import logging

        from .rasterizer import rasterize_shape

        logger = logging.getLogger(__name__)

        if not style.display:
            return

        # Convert shape to path for unified handling
        path = shape.to_path()

        # Flatten path to line segments
        points = path.flatten()

        if not points:
            return

        # Apply transform to points
        transformed_points = [transform.apply_to_point(x, y) for x, y in points]

        # Rasterize filled shape
        if style.fill is not None:
            fill_color = style.fill
            fill_opacity = style.get_effective_fill_opacity()

            # Create color with effective opacity
            effective_color = Color(
                fill_color.r, fill_color.g, fill_color.b, int(fill_color.a * fill_opacity)
            )

            # Rasterize and draw
            rasterize_shape(self, transformed_points, effective_color, fill=True)

        # Rasterize stroke
        if style.stroke is not None and style.stroke_width > 0:
            stroke_color = style.stroke
            stroke_opacity = style.get_effective_stroke_opacity()

            effective_color = Color(
                stroke_color.r, stroke_color.g, stroke_color.b, int(stroke_color.a * stroke_opacity)
            )

            # Check for dash pattern
            dash_pattern = style.get_dash_pattern()
            dash_segments_list = None

            if dash_pattern is not None:
                # Calculate path length from transformed points (not original shape)
                # This ensures dash pattern scales correctly with viewport transform and supersampling
                try:
                    import math

                    # Calculate BOTH original and transformed path lengths to get scale factor
                    if len(transformed_points) >= 2:
                        # Original path length from the shape
                        original_length = shape.get_path_length()

                        # Transformed path length
                        path_length = 0.0
                        for i in range(1, len(transformed_points)):
                            x1, y1 = transformed_points[i - 1]
                            x2, y2 = transformed_points[i]
                            segment_length = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
                            path_length += segment_length

                        # Calculate scale factor
                        scale_factor = path_length / original_length if original_length > 0 else 1.0

                        # Scale the dash pattern to match transformed coordinates
                        scaled_dash_lengths = tuple(
                            length * scale_factor for length in dash_pattern.dash_lengths
                        )
                        scaled_offset = dash_pattern.offset * scale_factor
                        scaled_total_length = sum(scaled_dash_lengths)

                        # Create a scaled dash pattern
                        from puresvg.dash import DashPattern

                        scaled_pattern = DashPattern(
                            dash_lengths=scaled_dash_lengths,
                            offset=scaled_offset,
                            total_length=scaled_total_length,
                        )

                        # Generate dash segments using the scaled pattern
                        dash_segments_list = list(scaled_pattern.generate_segments(path_length))

                        logger.debug(
                            f"Generated {len(dash_segments_list)} dash segments for path length {path_length:.2f}"
                        )
                    else:
                        dash_segments_list = None
                except Exception as e:
                    # Fall back to solid stroke on any error
                    logger.warning(
                        f"Failed to generate dash pattern: {e}. Falling back to solid stroke."
                    )
                    dash_segments_list = None

            # Rasterize stroke (solid or dashed)
            # Scale stroke width by transform scale factor
            # This ensures stroke widths are correct when supersampling
            transform_scale = transform.get_scale()
            scaled_stroke_width = style.stroke_width * transform_scale

            # Rasterize stroke (simplified - draws line segments)
            rasterize_shape(
                self,
                transformed_points,
                effective_color,
                fill=False,
                stroke_width=scaled_stroke_width,
                dash_segments=dash_segments_list,
            )

    def composite(self, other: "BitmapCanvas", opacity: float = 1.0) -> None:
        """
        Composite another canvas on top of this one.

        Args:
            other: Canvas to composite on top.
            opacity: Overall opacity for the composited layer (0.0-1.0).
        """
        if other.width != self.width or other.height != self.height:
            raise ValueError("Canvas dimensions must match for compositing")

        # Alpha blend each pixel
        for y in range(self.height):
            for x in range(self.width):
                fg = Color(
                    other.buffer[y, x, 0],
                    other.buffer[y, x, 1],
                    other.buffer[y, x, 2],
                    int(other.buffer[y, x, 3] * opacity),
                )
                bg = Color(
                    self.buffer[y, x, 0],
                    self.buffer[y, x, 1],
                    self.buffer[y, x, 2],
                    self.buffer[y, x, 3],
                )

                blended = fg.blend_with(bg)
                self.buffer[y, x] = blended.to_tuple()

    def to_array(self) -> "np.ndarray[Any, np.dtype[np.uint8]]":
        """
        Export canvas as numpy array.

        Returns:
            Numpy array with shape (height, width, 4) and dtype uint8.
        """
        return self.buffer.copy()

    def get_pixel(self, x: int, y: int) -> Color:
        """
        Read pixel color at coordinates.

        Args:
            x: Pixel x coordinate.
            y: Pixel y coordinate.

        Returns:
            Color at the specified pixel.

        Raises:
            IndexError: If coordinates are outside canvas bounds.
        """
        if not (0 <= x < self.width and 0 <= y < self.height):
            raise IndexError(f"Pixel coordinates ({x}, {y}) outside canvas bounds")

        return Color(
            self.buffer[y, x, 0], self.buffer[y, x, 1], self.buffer[y, x, 2], self.buffer[y, x, 3]
        )

    def set_pixel(self, x: int, y: int, color: Color, opacity: float = 1.0) -> None:
        """
        Write pixel with alpha blending.

        Args:
            x: Pixel x coordinate.
            y: Pixel y coordinate.
            color: Color to set.
            opacity: Additional opacity multiplier (0.0-1.0).
        """
        if not (0 <= x < self.width and 0 <= y < self.height):
            return  # Silently skip out-of-bounds pixels

        # Apply opacity
        fg = Color(color.r, color.g, color.b, int(color.a * opacity))
        bg = self.get_pixel(x, y)

        # Blend and set
        blended = fg.blend_with(bg)
        self.buffer[y, x] = blended.to_tuple()
