"""
Shape rasterization using scanline algorithm.

Converts vector shapes to pixels without antialiasing.
Antialiasing is achieved through supersampling in the renderer.
"""

import logging
from typing import TYPE_CHECKING

import numpy as np

# Import Pillow with helpful error message
try:
    from PIL import Image
except ImportError as e:
    raise ImportError(
        "Pillow is required for anti-aliasing features. "
        "Install it with: pip install Pillow>=10.0.0"
    ) from e

from .colors import Color

if TYPE_CHECKING:
    from .canvas import BitmapCanvas

logger = logging.getLogger(__name__)


def _numpy_to_pillow(array: np.ndarray) -> Image.Image:
    """
    Convert numpy RGBA array to Pillow Image.

    Args:
        array: Numpy array with shape (height, width, 4) and dtype uint8.

    Returns:
        PIL Image in RGBA mode.
    """
    if array.dtype != np.uint8:
        raise ValueError(f"Array must have dtype uint8, got {array.dtype}")
    if array.ndim != 3 or array.shape[2] != 4:
        raise ValueError(f"Array must have shape (height, width, 4), got {array.shape}")

    return Image.fromarray(array, mode="RGBA")


def _pillow_to_numpy(pil_image: Image.Image) -> np.ndarray:
    """
    Convert Pillow Image to numpy RGBA array.

    Args:
        pil_image: PIL Image (will be converted to RGBA if needed).

    Returns:
        Numpy array with shape (height, width, 4) and dtype uint8.
    """
    # Ensure RGBA mode
    if pil_image.mode != "RGBA":
        pil_image = pil_image.convert("RGBA")

    # Convert to numpy array
    return np.array(pil_image, dtype=np.uint8)


def _downsample_with_pillow(
    highres_array: np.ndarray, target_width: int, target_height: int
) -> np.ndarray:
    """
    Downsample high-resolution array using Pillow's LANCZOS filter.

    Args:
        highres_array: High-resolution numpy array (height, width, 4) uint8.
        target_width: Target output width in pixels.
        target_height: Target output height in pixels.

    Returns:
        Downsampled numpy array with shape (target_height, target_width, 4).
    """
    # Convert to PIL Image
    pil_image = _numpy_to_pillow(highres_array)

    # Resize using LANCZOS (highest quality)
    resized = pil_image.resize((target_width, target_height), resample=Image.Resampling.LANCZOS)

    # Convert back to numpy
    return _pillow_to_numpy(resized)


def rasterize_shape(
    canvas: "BitmapCanvas",
    points: list[tuple[float, float]],
    color: Color,
    fill: bool = True,
    stroke_width: float = 1.0,
    dash_segments: list[tuple[float, float]] | None = None,
) -> None:
    """
    Rasterize a shape defined by points onto a canvas.

    Args:
        canvas: BitmapCanvas to draw on.
        points: List of (x, y) points defining the shape.
        color: Color to use for drawing.
        fill: Whether to fill the shape (True) or just stroke (False).
        stroke_width: Width of stroke in pixels (if fill=False).
        dash_segments: Optional list of (start_distance, end_distance) tuples for dashed strokes.
                      Only used when fill=False.
    """
    if not points:
        return

    if fill:
        _rasterize_filled_polygon(canvas, points, color)
    else:
        _rasterize_stroked_path(canvas, points, color, stroke_width, dash_segments)


def _rasterize_filled_polygon(
    canvas: "BitmapCanvas", points: list[tuple[float, float]], color: Color
) -> None:
    """
    Fill a polygon using scanline algorithm.

    Uses even-odd fill rule for simplicity.

    Args:
        canvas: BitmapCanvas to draw on.
        points: Polygon vertices.
        color: Fill color.
    """
    if len(points) < 3:
        return  # Need at least 3 points for a polygon

    # Get bounding box
    xs = [p[0] for p in points]
    ys = [p[1] for p in points]
    min_x = max(0, int(np.floor(min(xs))))
    max_x = min(canvas.width - 1, int(np.ceil(max(xs))))
    min_y = max(0, int(np.floor(min(ys))))
    # For max_y, use ceil-1 to match Inkscape's pixel selection for non-antialiased rendering
    # A rectangle from y1 to y2 fills pixels floor(y1) through ceil(y2)-1
    max_y = min(canvas.height - 1, int(np.ceil(max(ys))) - 1)

    # Scan each row
    for y in range(min_y, max_y + 1):
        # Find intersections of scanline with polygon edges
        intersections = []

        for i in range(len(points)):
            p1 = points[i]
            p2 = points[(i + 1) % len(points)]

            # Check if edge crosses scanline at y + 0.5 (pixel center)
            # This matches Inkscape's non-antialiased rendering behavior
            y_scanline = y + 0.5
            if (p1[1] <= y_scanline < p2[1]) or (p2[1] <= y_scanline < p1[1]):
                # Calculate x intersection
                if p2[1] != p1[1]:
                    x = p1[0] + (y_scanline - p1[1]) * (p2[0] - p1[0]) / (p2[1] - p1[1])
                    intersections.append(x)

        # Sort intersections
        intersections.sort()

        # Fill between pairs of intersections (even-odd rule)
        for i in range(0, len(intersections) - 1, 2):
            x_start = max(min_x, int(np.floor(intersections[i])))
            x_end = min(max_x, int(np.ceil(intersections[i + 1])))

            for x in range(x_start, x_end):
                canvas.set_pixel(x, y, color)


def _rasterize_stroked_path(
    canvas: "BitmapCanvas",
    points: list[tuple[float, float]],
    color: Color,
    stroke_width: float,
    dash_segments: list[tuple[float, float]] | None = None,
) -> None:
    """
    Draw a stroked path using outline polygon technique.

    Creates a filled polygon representing the stroke outline by computing
    offset curves on both sides of the path.

    Args:
        canvas: BitmapCanvas to draw on.
        points: Path points.
        color: Stroke color.
        stroke_width: Line width in pixels.
        dash_segments: Optional list of (start_distance, end_distance) tuples for dashed strokes.
                      If None, renders solid stroke. If provided, only renders visible dash segments.
    """
    if len(points) < 2:
        return

    # If no dash segments, render as solid stroke (existing behavior)
    if dash_segments is None:
        _rasterize_solid_stroked_path(canvas, points, color, stroke_width)
    else:
        _rasterize_dashed_stroked_path(canvas, points, color, stroke_width, dash_segments)


def _rasterize_solid_stroked_path(
    canvas: "BitmapCanvas", points: list[tuple[float, float]], color: Color, stroke_width: float
) -> None:
    """
    Draw a solid (non-dashed) stroked path.

    This is the original implementation extracted for clarity.

    Args:
        canvas: BitmapCanvas to draw on.
        points: Path points.
        color: Stroke color.
        stroke_width: Line width in pixels.
    """
    if len(points) < 2:
        return

    half_width = stroke_width / 2.0

    # Check if path is closed (first and last points are the same or very close)
    is_closed = False
    if len(points) >= 3:
        dx = points[-1][0] - points[0][0]
        dy = points[-1][1] - points[0][1]
        if (dx * dx + dy * dy) < 0.01:  # Within 0.1 pixel
            is_closed = True

    # Compute the offset curves on both sides of the path
    left_points = []
    right_points = []

    for i in range(len(points)):
        x, y = points[i]

        if i == 0 and not is_closed:
            # First point of open path: use direction to next point
            if len(points) > 1:
                dx = points[i + 1][0] - x
                dy = points[i + 1][1] - y
                length = (dx * dx + dy * dy) ** 0.5
                if length > 0.001:
                    px = -dy / length
                    py = dx / length
                    left_points.append((x + px * half_width, y + py * half_width))
                    right_points.append((x - px * half_width, y - py * half_width))
        elif i == len(points) - 1 and not is_closed:
            # Last point of open path: use direction from previous point
            dx = x - points[i - 1][0]
            dy = y - points[i - 1][1]
            length = (dx * dx + dy * dy) ** 0.5
            if length > 0.001:
                px = -dy / length
                py = dx / length
                left_points.append((x + px * half_width, y + py * half_width))
                right_points.append((x - px * half_width, y - py * half_width))
        else:
            # Middle point (or closed path point): compute miter joint
            # Get direction vectors for segments before and after this point

            # For closed paths at i==0, wrap around to get previous point
            prev_idx = i - 1
            if is_closed and i == 0:
                prev_idx = len(points) - 2  # Skip the duplicate last point

            dx1 = x - points[prev_idx][0]
            dy1 = y - points[prev_idx][1]
            len1 = (dx1 * dx1 + dy1 * dy1) ** 0.5

            # For next point, handle wrapping for closed paths
            next_idx = (i + 1) % len(points)
            if is_closed and i == len(points) - 1:
                # This is the last point, so next point is the second point
                next_idx = 1

            dx2 = points[next_idx][0] - x
            dy2 = points[next_idx][1] - y
            len2 = (dx2 * dx2 + dy2 * dy2) ** 0.5

            if len1 > 0.001 and len2 > 0.001:
                # Normalize direction vectors
                dx1 /= len1
                dy1 /= len1
                dx2 /= len2
                dy2 /= len2

                # Compute perpendiculars (must match endpoint perpendicular direction)
                # Use left perpendicular (-dy, dx) to match endpoint calculation
                px1 = -dy1
                py1 = dx1
                px2 = -dy2
                py2 = dx2

                # Average perpendicular for miter
                px = (px1 + px2) / 2.0
                py = (py1 + py2) / 2.0

                # Normalize miter vector
                miter_len = (px * px + py * py) ** 0.5
                if miter_len > 0.001:
                    # Normalize the miter direction
                    px_unit = px / miter_len
                    py_unit = py / miter_len

                    # Calculate proper miter distance
                    # miter_len represents how much the averaged perpendicular is shortened
                    # For a 90° corner: miter_len ≈ 0.707, so we need factor ≈ 1.414
                    # The miter distance should be: half_width / miter_len
                    miter_distance = half_width / miter_len

                    # Limit miter extension to avoid excessive spikes at sharp angles
                    miter_distance = min(miter_distance, half_width * 4.0)

                    left_points.append((x + px_unit * miter_distance, y + py_unit * miter_distance))
                    right_points.append(
                        (x - px_unit * miter_distance, y - py_unit * miter_distance)
                    )

    # Create closed polygon: left side forward, right side backward
    if left_points and right_points:
        if is_closed:
            # For closed paths: all left points + all reversed right points
            outline_points = left_points + list(reversed(right_points))
        else:
            # For open paths: connect endpoints properly
            outline_points = left_points + list(reversed(right_points))
        _rasterize_filled_polygon(canvas, outline_points, color)


def _rasterize_dashed_stroked_path(
    canvas: "BitmapCanvas",
    points: list[tuple[float, float]],
    color: Color,
    stroke_width: float,
    dash_segments: list[tuple[float, float]],
) -> None:
    """
    Draw a dashed stroked path by extracting subsections for each dash segment.

    Args:
        canvas: BitmapCanvas to draw on.
        points: Path points along the full path.
        color: Stroke color.
        stroke_width: Line width in pixels.
        dash_segments: List of (start_distance, end_distance) tuples for visible dashes.
    """
    if len(points) < 2 or not dash_segments:
        return

    # Calculate cumulative distances along path
    distances = [0.0]
    for i in range(len(points) - 1):
        x0, y0 = points[i]
        x1, y1 = points[i + 1]
        dx = x1 - x0
        dy = y1 - y0
        segment_length = (dx * dx + dy * dy) ** 0.5
        distances.append(distances[-1] + segment_length)

    # For each dash segment, extract points within that range
    for start_dist, end_dist in dash_segments:
        # Extract subsection of path for this dash
        dash_points = _extract_path_subsection(points, distances, start_dist, end_dist)

        if len(dash_points) >= 2:
            # Render this dash segment as a solid stroke with caps
            _rasterize_dash_segment_with_caps(canvas, dash_points, color, stroke_width)


def _extract_path_subsection(
    points: list[tuple[float, float]],
    distances: list[float],
    start_dist: float,
    end_dist: float,
) -> list[tuple[float, float]]:
    """
    Extract a subsection of a path between two distances.

    Args:
        points: Full path points.
        distances: Cumulative distances at each point.
        start_dist: Start distance along path.
        end_dist: End distance along path.

    Returns:
        List of points representing the subsection (includes interpolated start/end points).
    """
    result_points = []

    for i in range(len(points) - 1):
        seg_start_dist = distances[i]
        seg_end_dist = distances[i + 1]

        # Skip segments completely before start
        if seg_end_dist < start_dist:
            continue

        # Stop if segment starts after end
        if seg_start_dist > end_dist:
            break

        x0, y0 = points[i]
        x1, y1 = points[i + 1]

        # Determine which part of this segment to include
        # Start point: interpolate if needed
        if seg_start_dist < start_dist <= seg_end_dist:
            # Interpolate start point
            t = (
                (start_dist - seg_start_dist) / (seg_end_dist - seg_start_dist)
                if seg_end_dist > seg_start_dist
                else 0.0
            )
            x_start = x0 + t * (x1 - x0)
            y_start = y0 + t * (y1 - y0)
            result_points.append((x_start, y_start))
        elif seg_start_dist >= start_dist:
            # Include full start point
            result_points.append((x0, y0))

        # End point: interpolate if needed
        if seg_start_dist < end_dist <= seg_end_dist:
            # Interpolate end point
            t = (
                (end_dist - seg_start_dist) / (seg_end_dist - seg_start_dist)
                if seg_end_dist > seg_start_dist
                else 0.0
            )
            x_end = x0 + t * (x1 - x0)
            y_end = y0 + t * (y1 - y0)
            result_points.append((x_end, y_end))
            break  # Done with this dash segment
        elif seg_end_dist <= end_dist:
            # Include full end point
            result_points.append((x1, y1))

    return result_points


def _rasterize_dash_segment_with_caps(
    canvas: "BitmapCanvas",
    points: list[tuple[float, float]],
    color: Color,
    stroke_width: float,
) -> None:
    """
    Render a single dash segment with butt caps (no extension beyond endpoints).

    For dashed strokes, SVG spec uses butt caps by default to prevent dashes
    from overlapping into gaps.

    Args:
        canvas: BitmapCanvas to draw on.
        points: Points for this dash segment.
        color: Stroke color.
        stroke_width: Line width in pixels.
    """
    if len(points) < 2:
        return

    half_width = stroke_width / 2.0

    # Draw line segments (rectangles) without extending caps
    for i in range(len(points) - 1):
        x0, y0 = points[i]
        x1, y1 = points[i + 1]

        # Calculate perpendicular offset vector
        dx = x1 - x0
        dy = y1 - y0
        length = (dx * dx + dy * dy) ** 0.5

        if length < 0.001:  # Skip zero-length segments
            continue

        # Unit perpendicular vector
        px = -dy / length * half_width
        py = dx / length * half_width

        # Create rectangle points for this segment
        rect_points = [
            (x0 + px, y0 + py),
            (x1 + px, y1 + py),
            (x1 - px, y1 - py),
            (x0 - px, y0 - py),
        ]

        # Fill the rectangle
        _rasterize_filled_polygon(canvas, rect_points, color)

    # NOTE: For dashed strokes, we use butt caps (no caps) to prevent overlapping.
    # Round caps would extend half_width beyond the dash boundaries, filling gaps.
    # If stroke-linecap support is added in the future, this should be conditional.


def _draw_line(
    canvas: "BitmapCanvas", x0: float, y0: float, x1: float, y1: float, color: Color, width: float
) -> None:
    """
    Draw a line segment using Bresenham-like algorithm.

    Args:
        canvas: BitmapCanvas to draw on.
        x0, y0: Start point.
        x1, y1: End point.
        color: Line color.
        width: Line width in pixels.
    """
    # Convert to integer coordinates
    ix0, iy0 = int(round(x0)), int(round(y0))
    ix1, iy1 = int(round(x1)), int(round(y1))

    dx = abs(ix1 - ix0)
    dy = abs(iy1 - iy0)

    sx = 1 if ix0 < ix1 else -1
    sy = 1 if iy0 < iy1 else -1

    err = dx - dy

    x, y = ix0, iy0

    # Bresenham's line algorithm
    while True:
        # Draw pixel (with thickness if width > 1)
        if width <= 1:
            canvas.set_pixel(x, y, color)
        else:
            # Draw thicker line by drawing circle around each point
            radius = int(width / 2)
            for dy_offset in range(-radius, radius + 1):
                for dx_offset in range(-radius, radius + 1):
                    if dx_offset * dx_offset + dy_offset * dy_offset <= radius * radius:
                        canvas.set_pixel(x + dx_offset, y + dy_offset, color)

        if x == ix1 and y == iy1:
            break

        e2 = 2 * err

        if e2 > -dy:
            err -= dy
            x += sx

        if e2 < dx:
            err += dx
            y += sy


def _draw_circle(canvas: "BitmapCanvas", cx: float, cy: float, radius: float, color: Color) -> None:
    """
    Draw a filled circle for stroke caps and joins.

    Args:
        canvas: BitmapCanvas to draw on.
        cx, cy: Center point.
        radius: Circle radius.
        color: Circle color.
    """
    # Create circle points
    num_segments = max(8, int(radius * 4))
    points = []

    import math

    for i in range(num_segments):
        angle = 2 * math.pi * i / num_segments
        x = cx + radius * math.cos(angle)
        y = cy + radius * math.sin(angle)
        points.append((x, y))

    # Fill the circle as a polygon
    if len(points) >= 3:
        _rasterize_filled_polygon(canvas, points, color)


def flatten_cubic_bezier(
    x0: float,
    y0: float,
    x1: float,
    y1: float,
    x2: float,
    y2: float,
    x3: float,
    y3: float,
    tolerance: float = 0.5,
) -> list[tuple[float, float]]:
    """
    Flatten a cubic Bezier curve into line segments.

    Uses recursive subdivision until segments are within tolerance.

    Args:
        x0, y0: Start point.
        x1, y1: First control point.
        x2, y2: Second control point.
        x3, y3: End point.
        tolerance: Maximum distance between curve and approximation.

    Returns:
        List of (x, y) points approximating the curve.
    """
    # Simple flattening using fixed subdivision for now
    # More sophisticated adaptive subdivision can be added later
    steps = max(10, int(tolerance * 20))
    points = []

    for i in range(steps + 1):
        t = i / steps

        # Cubic Bezier formula
        x = (1 - t) ** 3 * x0 + 3 * (1 - t) ** 2 * t * x1 + 3 * (1 - t) * t**2 * x2 + t**3 * x3
        y = (1 - t) ** 3 * y0 + 3 * (1 - t) ** 2 * t * y1 + 3 * (1 - t) * t**2 * y2 + t**3 * y3

        points.append((x, y))

    return points


def flatten_quadratic_bezier(
    x0: float, y0: float, x1: float, y1: float, x2: float, y2: float, tolerance: float = 0.5
) -> list[tuple[float, float]]:
    """
    Flatten a quadratic Bezier curve into line segments.

    Args:
        x0, y0: Start point.
        x1, y1: Control point.
        x2, y2: End point.
        tolerance: Maximum distance between curve and approximation.

    Returns:
        List of (x, y) points approximating the curve.
    """
    steps = max(10, int(tolerance * 20))
    points = []

    for i in range(steps + 1):
        t = i / steps

        # Quadratic Bezier formula
        x = (1 - t) ** 2 * x0 + 2 * (1 - t) * t * x1 + t**2 * x2
        y = (1 - t) ** 2 * y0 + 2 * (1 - t) * t * y1 + t**2 * y2

        points.append((x, y))

    return points
