"""
High-level SVG rendering API.

Provides the main render() and parse() functions for converting SVG to bitmaps.
"""

import io
import logging
import xml.etree.ElementTree as ET
from pathlib import Path as FilePath
from typing import Any

import numpy as np

from .canvas import BitmapCanvas
from .colors import Color
from .document import SVGDocument, ViewBox
from .elements import GroupElement, ShapeElement, SVGElement
from .parser import parse_shape_from_element, parse_svg, parse_transform_attribute
from .styles import Style, parse_style_attributes
from .transforms import Transform

logger = logging.getLogger(__name__)

# Type aliases for clarity
SupersampleFactor = int


def _validate_supersample_factor(supersample: SupersampleFactor) -> None:
    """
    Validate supersample factor parameter.

    Args:
        supersample: Supersampling factor to validate.

    Raises:
        TypeError: If supersample is not an integer.
        ValueError: If supersample is less than 1.
    """
    if not isinstance(supersample, int):
        raise TypeError(f"supersample must be an integer, got {type(supersample).__name__}")
    if supersample < 1:
        raise ValueError(f"supersample must be >= 1, got {supersample}")
    if supersample < 2:
        logger.warning(
            f"supersample={supersample} provides no anti-aliasing benefit. "
            "Use supersample >= 2 for anti-aliasing."
        )


def _calculate_intermediate_dimensions(
    width: int, height: int, supersample_factor: SupersampleFactor
) -> tuple[int, int]:
    """
    Calculate intermediate (high-resolution) canvas dimensions.

    Args:
        width: Target output width in pixels.
        height: Target output height in pixels.
        supersample_factor: Supersampling multiplier.

    Returns:
        Tuple of (intermediate_width, intermediate_height).
    """
    return (width * supersample_factor, height * supersample_factor)


def render(
    source: str | FilePath | io.IOBase,
    width: int | None = None,
    height: int | None = None,
    scale: float = 1.0,
    background: tuple[int, int, int, int] | None = None,
    dpi: float = 96.0,
    *,
    supersample: SupersampleFactor = 4,
) -> "np.ndarray[Any, np.dtype[np.uint8]]":
    """
    Render an SVG file to a numpy bitmap array.

    Args:
        source: SVG content as file path (str/Path), file-like object, or raw SVG string.
        width: Output bitmap width in pixels.
        height: Output bitmap height in pixels.
        scale: Scaling factor applied to SVG dimensions (default 1.0).
        background: Background color as (R, G, B, A) tuple with values 0-255.
        dpi: Dots per inch for unit conversion (default 96.0).
        supersample: Supersampling factor for anti-aliasing (default 4).
                    1=no AA, 2=2x, 4=4x (default), 8=8x. Higher values produce
                    smoother edges but use more memory and take longer.

    Returns:
        Numpy array with shape (height, width, 4) and dtype uint8.
        Channel order is RGBA.

    Raises:
        SVGParseError: If source contains malformed XML or invalid SVG structure.
        FileNotFoundError: If source is a file path that doesn't exist.
        ValueError: If width/height/scale/supersample are invalid.
        MemoryError: If supersampled dimensions exceed available memory.
    """
    # Validate parameters
    if width is not None and width <= 0:
        raise ValueError(f"Width must be positive, got {width}")
    if height is not None and height <= 0:
        raise ValueError(f"Height must be positive, got {height}")
    if scale <= 0:
        raise ValueError(f"Scale must be positive, got {scale}")

    # Validate supersample factor
    _validate_supersample_factor(supersample)

    # Parse SVG document
    doc = parse(source)

    # Render the document
    return render_document(doc, width, height, scale, background, dpi, supersample=supersample)


def parse(source: str | FilePath | io.IOBase) -> SVGDocument:
    """
    Parse SVG content into an SVGDocument object.

    Args:
        source: SVG content as file path (str/Path), file-like object, or raw SVG string.

    Returns:
        SVGDocument object representing the parsed SVG structure.

    Raises:
        SVGParseError: If source contains malformed XML or invalid SVG structure.
        FileNotFoundError: If source is a file path that doesn't exist.
    """
    # Parse XML
    root_xml = parse_svg(source)

    # Extract document attributes
    width = _parse_dimension(root_xml.get("width"))
    height = _parse_dimension(root_xml.get("height"))
    viewBox = _parse_viewbox(root_xml.get("viewBox"))

    # Extract CSS rules from <style> elements
    css_rules = _extract_css_rules(root_xml)

    # Build element tree
    root_element = _build_element_tree(root_xml, Transform.identity(), Style(), css_rules)

    # Create document
    doc = SVGDocument(
        width=width, height=height, viewBox=viewBox, root_element=root_element, metadata={}
    )

    logger.info(f"Parsed SVG document: {width}x{height}, viewBox={viewBox}")

    return doc

    return doc


def render_document(
    doc: SVGDocument,
    width: int | None = None,
    height: int | None = None,
    scale: float = 1.0,
    background: tuple[int, int, int, int] | None = None,
    dpi: float = 96.0,
    *,
    supersample: SupersampleFactor = 4,
) -> "np.ndarray[Any, np.dtype[np.uint8]]":
    """
    Render an SVGDocument to a bitmap array.

    Args:
        doc: SVGDocument to render.
        width: Output width in pixels.
        height: Output height in pixels.
        scale: Scaling factor.
        background: Background color tuple (R, G, B, A).
        dpi: DPI for unit conversion.
        supersample: Supersampling factor for anti-aliasing (default 4).

    Returns:
        Numpy array (height, width, 4) uint8.
    """
    from .rasterizer import _downsample_with_pillow

    # Determine output dimensions
    svg_width, svg_height = doc.get_dimensions()

    # Handle case where dimensions might be 0
    if svg_width == 0 or svg_height == 0:
        svg_width = svg_width or 100.0
        svg_height = svg_height or 100.0

    if width is None and height is None:
        # Use scale factor
        width = int(svg_width * scale)
        height = int(svg_height * scale)
    elif width is None:
        # Calculate width from height preserving aspect ratio
        assert height is not None, "height must be set when width is None"
        aspect = svg_width / svg_height if svg_height > 0 else 1.0
        width = int(height * aspect)
    elif height is None:
        # Calculate height from width preserving aspect ratio
        assert width is not None, "width must be set when height is None"
        aspect = svg_height / svg_width if svg_width > 0 else 1.0
        height = int(width * aspect)

    # Ensure dimensions are at least 1
    assert width is not None and height is not None
    width = max(1, width)
    height = max(1, height)

    logger.info(f"Rendering to {width}x{height} pixels (supersample={supersample})")

    # Determine rendering dimensions (potentially supersampled)
    if supersample == 1:
        # No supersampling - render at target size
        render_width = width
        render_height = height
        apply_downsampling = False
    else:
        # Supersampling - render at higher resolution
        render_width, render_height = _calculate_intermediate_dimensions(width, height, supersample)
        apply_downsampling = True
        logger.debug(
            f"Supersampling: rendering at {render_width}x{render_height}, will downsample to {width}x{height}"
        )

    try:
        # Create canvas (at potentially supersampled dimensions)
        bg_color = Color(*background) if background else Color(255, 255, 255, 0)
        canvas = BitmapCanvas(render_width, render_height, bg_color)
    except MemoryError as e:
        # Re-raise with helpful context
        intermediate_mb = (render_width * render_height * 4) / (1024 * 1024)
        raise MemoryError(
            f"Failed to allocate {render_width}x{render_height} canvas "
            f"(~{intermediate_mb:.1f} MB) with supersample={supersample}. "
            f"Try reducing supersample factor or output dimensions."
        ) from e

    # Calculate viewport transform (SVG space -> pixel space)
    viewport_transform = _calculate_viewport_transform(
        svg_width, svg_height, render_width, render_height, doc.viewBox
    )

    # Render element tree
    if doc.root_element:
        _render_element(canvas, doc.root_element, viewport_transform)

    # Get rendered array
    rendered_array = canvas.to_array()

    # Apply downsampling if supersampling was used
    if apply_downsampling:
        logger.debug(
            f"Downsampling with LANCZOS from {render_width}x{render_height} to {width}x{height}"
        )
        rendered_array = _downsample_with_pillow(rendered_array, width, height)

    return rendered_array


def _extract_css_rules(root_xml: ET.Element) -> dict[str, dict[str, str]]:
    """
    Extract CSS rules from <style> elements in the SVG.

    Currently supports simple tag selectors only (e.g., "ellipse { rx: 40px; }").

    Args:
        root_xml: Root XML element of the SVG.

    Returns:
        Dictionary mapping tag names to their CSS properties.
        Example: {"ellipse": {"rx": "40", "ry": "30"}}
    """
    css_rules: dict[str, dict[str, str]] = {}

    # Find all <style> elements
    for style_elem in root_xml.iter():
        if style_elem.tag.lower() == "style":
            css_text = style_elem.text or ""
            # Simple CSS parser for tag selectors
            # Pattern: "tag { prop: value; prop: value; }"
            import re

            pattern = r"(\w+)\s*\{([^}]+)\}"
            for match in re.finditer(pattern, css_text):
                tag = match.group(1).strip()
                properties_str = match.group(2).strip()

                # Parse properties
                properties = {}
                for prop_decl in properties_str.split(";"):
                    prop_decl = prop_decl.strip()
                    if ":" in prop_decl:
                        prop, value = prop_decl.split(":", 1)
                        prop = prop.strip()
                        value = value.strip()

                        # Strip common CSS units (px, pt, etc.) for numeric properties
                        # This allows CSS properties to work with SVG attribute parsers
                        # Keep "auto" values as-is for properties that support them
                        if value.lower() != "auto":
                            value = re.sub(r"([\d.]+)(px|pt|em|rem|%)", r"\1", value)

                        properties[prop] = value

                # Store rules for this tag
                if tag not in css_rules:
                    css_rules[tag] = {}
                css_rules[tag].update(properties)

    return css_rules


def _build_element_tree(
    xml_element: ET.Element,
    parent_transform: Transform,
    parent_style: Style,
    css_rules: dict[str, dict[str, str]],
) -> SVGElement:
    """
    Recursively build element tree from XML.

    Args:
        xml_element: XML element to process.
        parent_transform: Cumulative transform from parent.
        parent_style: Parent element's style.
        css_rules: Dictionary mapping CSS selectors to property dictionaries.

    Returns:
        SVGElement representing the element and its children.
    """
    tag = xml_element.tag.lower()
    attrs = xml_element.attrib

    # Apply CSS rules for this tag (if any)
    merged_attrs = dict(attrs)  # Make a copy
    if tag in css_rules:
        # CSS rules have lower priority than inline attributes
        for prop, value in css_rules[tag].items():
            if prop not in merged_attrs:
                merged_attrs[prop] = value

    # Parse transform
    transform_str = merged_attrs.get("transform", "")
    element_transform = parse_transform_attribute(transform_str)
    cumulative_transform = parent_transform.compose(element_transform)

    # Parse style
    element_style = parse_style_attributes(merged_attrs, parent_style)

    # Parse shape if this is a shape element
    # Temporarily update element's attrib to include CSS properties
    original_attrib = xml_element.attrib
    xml_element.attrib = merged_attrs
    try:
        shape = parse_shape_from_element(xml_element)
    finally:
        xml_element.attrib = original_attrib

    # Create appropriate element type
    element: ShapeElement | GroupElement
    if shape is not None:
        element = ShapeElement(
            shape=shape,
            element_type=tag,
            id=merged_attrs.get("id"),
            transform=cumulative_transform,
            style=element_style,
            attributes=merged_attrs,
        )
    elif tag == "g":
        element = GroupElement(
            id=merged_attrs.get("id"),
            transform=cumulative_transform,
            style=element_style,
            attributes=merged_attrs,
        )
    else:
        # Generic element (e.g., svg root)
        element = GroupElement(
            id=merged_attrs.get("id"),
            transform=cumulative_transform,
            style=element_style,
            attributes=merged_attrs,
        )
        element.element_type = tag

    # Recursively process children
    for child_xml in xml_element:
        if child_xml.tag:  # Skip text nodes
            try:
                child_element = _build_element_tree(
                    child_xml, cumulative_transform, element_style, css_rules
                )
                element.children.append(child_element)
            except Exception as e:
                logger.warning(f"Failed to parse child element <{child_xml.tag}>: {e}")

    return element


def _render_element(
    canvas: BitmapCanvas, element: SVGElement, viewport_transform: Transform
) -> None:
    """
    Recursively render an element and its children.

    Args:
        canvas: Canvas to draw on.
        element: Element to render.
        viewport_transform: Viewport transformation (SVG space → pixel space), applied once at root.
    """
    if not element.style.display:
        return

    # Apply viewport transform to element's cumulative transform (which already includes all ancestors)
    final_transform = viewport_transform.compose(element.transform)

    # Render this element if it's a shape
    if isinstance(element, ShapeElement) and element.shape:
        canvas.draw_shape(element.shape, final_transform, element.style)

    # Render children - pass same viewport_transform since children have cumulative transforms
    for child in element.children:
        _render_element(canvas, child, viewport_transform)


def _calculate_viewport_transform(
    svg_width: float,
    svg_height: float,
    pixel_width: int,
    pixel_height: int,
    viewBox: ViewBox | None,
) -> Transform:
    """
    Calculate transformation from SVG coordinates to pixel coordinates.

    Args:
        svg_width: SVG width in user units.
        svg_height: SVG height in user units.
        pixel_width: Output width in pixels.
        pixel_height: Output height in pixels.
        viewBox: ViewBox if specified.

    Returns:
        Transform mapping SVG space to pixel space.
    """
    if viewBox is not None:
        # ViewBox defines the coordinate system
        scale_x = pixel_width / viewBox.width
        scale_y = pixel_height / viewBox.height

        # Use uniform scaling (preserve aspect ratio)
        scale = min(scale_x, scale_y)

        # Center the content
        tx = (pixel_width - viewBox.width * scale) / 2 - viewBox.min_x * scale
        ty = (pixel_height - viewBox.height * scale) / 2 - viewBox.min_y * scale

        return Transform.translate(tx, ty).compose(Transform.scale(scale, scale))
    else:
        # Simple scaling based on SVG dimensions
        scale_x = pixel_width / svg_width if svg_width > 0 else 1.0
        scale_y = pixel_height / svg_height if svg_height > 0 else 1.0

        return Transform.scale(scale_x, scale_y)


def _parse_dimension(dim_str: str | None) -> float | None:
    """
    Parse SVG dimension attribute (width/height).

    Supports units: px, pt, pc, mm, cm, in, or unitless.

    Args:
        dim_str: Dimension string (e.g., "100", "10cm", "2in").

    Returns:
        Dimension in user units, or None if not specified.
    """
    if not dim_str:
        return None

    dim_str = dim_str.strip().lower()

    # Extract number and unit
    import re

    match = re.match(r"([-+]?[0-9]*\.?[0-9]+)([a-z%]*)", dim_str)
    if not match:
        return None

    value = float(match.group(1))
    unit = match.group(2) or "px"

    # Convert to user units (px)
    if unit == "px" or unit == "":
        return value
    elif unit == "pt":
        return value * 1.25  # 1pt = 1.25px
    elif unit == "pc":
        return value * 15  # 1pc = 15px
    elif unit == "mm":
        return value * 3.543307  # 1mm = 3.543307px at 96dpi
    elif unit == "cm":
        return value * 35.43307  # 1cm = 35.43307px at 96dpi
    elif unit == "in":
        return value * 96  # 1in = 96px at 96dpi
    else:
        logger.warning(f"Unknown unit: {unit}, treating as px")
        return value


def _parse_viewbox(viewbox_str: str | None) -> ViewBox | None:
    """
    Parse viewBox attribute.

    Args:
        viewbox_str: ViewBox string (e.g., "0 0 100 100").

    Returns:
        ViewBox object, or None if not specified.
    """
    if not viewbox_str:
        return None

    try:
        parts = [float(x) for x in viewbox_str.split()]
        if len(parts) == 4:
            return ViewBox(min_x=parts[0], min_y=parts[1], width=parts[2], height=parts[3])
    except (ValueError, IndexError):
        logger.warning(f"Invalid viewBox: {viewbox_str}")

    return None
