"""ODF XML manipulation helpers using lxml.

This module provides ODF-specific functions for generating draw effects,
gradients, and custom paths. Unlike OOXML's rich DrawingML, ODF has a
simpler effect model with many features unsupported.

OOXML → ODF Feature Mapping
===========================

MAPS CLEANLY:
- Gradients (linear/radial) → <draw:gradient> (2 stops only)
- Custom geometry paths → SVG d="" attribute on <draw:path>

EXISTS BUT SIMPLER:
- Outer shadow → draw:shadow attrs (no blur, x/y offset instead of angle+distance)

NOT REPRESENTABLE IN ODF:
- Glow effect
- Reflection effect
- Soft edge (feather)
- Inner shadow
- 3D bevels (top/bottom)
- 3D scene control (camera, lighting)
- Compound effects (multiple per shape)

FUNDAMENTALLY DIFFERENT:
- Theme colors → ODF has NO color semantics, only explicit RGB values
- 3D → ODF dr3d: namespace is chart-only, very limited

Design Implications:
====================
When exporting from OOXML to ODF:
1. Advanced effects are silently dropped (use get_unsupported_effects() to warn)
2. Theme color references must be resolved to literal RGB
3. Multi-stop gradients collapse to 2 stops (first + last)
4. Shadow blur is lost; only offset and color preserved

References:
- ODF 1.3 spec: https://docs.oasis-open.org/office/OpenDocument/v1.3/os/
- Draw shadows: https://wiki.openoffice.org/wiki/Documentation/OOoAuthors_User_Manual/Draw_Guide/Shadows_and_transparency
"""

from __future__ import annotations

import math
from typing import TYPE_CHECKING, List, Optional

from lxml import etree

if TYPE_CHECKING:
    from tokenmoulds.ir.style_primitives import (
        CustomGeometry,
        Effects,
        GradientFill,
        Shadow,
    )


from tokenmoulds.design.units import EMU_PER_INCH as _EMU_PER_INCH

# =============================================================================
# ODF NAMESPACES
# =============================================================================

DRAW_NS = "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
STYLE_NS = "urn:oasis:names:tc:opendocument:xmlns:style:1.0"
FO_NS = "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
SVG_NS = "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
OFFICE_NS = "urn:oasis:names:tc:opendocument:xmlns:office:1.0"

TEXT_NS = "urn:oasis:names:tc:opendocument:xmlns:text:1.0"
TABLE_NS = "urn:oasis:names:tc:opendocument:xmlns:table:1.0"

# Default table styling constants (shared across ODF emitters)
DEFAULT_TABLE_BORDER_WIDTH_PT = 0.75  # ~15 twips
DEFAULT_TABLE_BORDER_COLOR = "#E5E7EB"
DEFAULT_TABLE_PADDING_X_PT = "6.00pt"  # ~120 twips
DEFAULT_TABLE_PADDING_Y_PT = "3.00pt"  # ~60 twips

ODF_NSMAP = {
    "draw": DRAW_NS,
    "style": STYLE_NS,
    "fo": FO_NS,
    "svg": SVG_NS,
    "office": OFFICE_NS,
    "text": TEXT_NS,
    "table": TABLE_NS,
}


def q(ns: str, tag: str) -> str:
    """Create qualified name for ODF namespace."""
    ns_uri = ODF_NSMAP.get(ns)
    if ns_uri is None:
        raise ValueError(f"Unknown ODF namespace: {ns}")
    return f"{{{ns_uri}}}{tag}"


# =============================================================================
# STYLE FINDING HELPERS
# =============================================================================


def find_style(root: etree._Element, style_name: str) -> Optional[etree._Element]:
    """Find a style:style element by name."""
    for style in root.iter(q("style", "style")):
        if style.get(q("style", "name")) == style_name:
            return style
    return None


def find_font_face(root: etree._Element, font_name: str) -> Optional[etree._Element]:
    """Find a style:font-face element by name."""
    for font in root.iter(q("style", "font-face")):
        if font.get(q("style", "name")) == font_name:
            return font
    return None


def find_default_style(root: etree._Element, family: str) -> Optional[etree._Element]:
    """Find a style:default-style element by family."""
    for style in root.iter(q("style", "default-style")):
        if style.get(q("style", "family")) == family:
            return style
    return None


def find_list_style(root: etree._Element, style_name: str) -> Optional[etree._Element]:
    """Find a text:list-style element by name."""
    for style in root.iter(q("text", "list-style")):
        if style.get(q("style", "name")) == style_name:
            return style
    return None


def set_font_family(font_face: etree._Element, font_family: str) -> None:
    """Set svg:font-family on a font-face element."""
    font_face.set(q("svg", "font-family"), font_family)


def set_text_props(
    text_props: etree._Element,
    *,
    font_family: Optional[str] = None,
    font_size_pt: Optional[float] = None,
    color: Optional[str] = None,
    bold: Optional[bool] = None,
) -> None:
    """Set common text properties on a style:text-properties element."""
    if font_family is not None:
        text_props.set(q("fo", "font-family"), font_family)
    if font_size_pt is not None:
        text_props.set(q("fo", "font-size"), f"{font_size_pt:.1f}pt")
    if color is not None:
        text_props.set(q("fo", "color"), color)
    if bold is not None:
        text_props.set(q("fo", "font-weight"), "bold" if bold else "normal")


# =============================================================================
# SHADOW EFFECTS
# =============================================================================


def _compute_shadow_attrs(shadow: "Shadow") -> tuple[float, float, str, float]:
    """Compute ODF shadow offset, color, and opacity from an OOXML Shadow primitive.

    Converts OOXML direction (60000ths of degree) + distance (EMU) to x/y
    offsets in cm, resolves the color to an RGB hex string, and converts
    the alpha modifier to an ODF opacity percentage.

    Returns:
        (offset_x_cm, offset_y_cm, color_hex, opacity_percent)
    """
    # Convert direction + distance to x/y offsets (cm)
    angle_deg = shadow.direction / 60000.0
    distance_cm = shadow.distance / _EMU_PER_INCH * 2.54  # EMU -> inches -> cm

    # OOXML: 0° = right, 90° = down, 180° = left, 270° = up
    angle_rad = math.radians(angle_deg)
    offset_x = distance_cm * math.cos(angle_rad)
    offset_y = distance_cm * math.sin(angle_rad)

    # Resolve color
    if shadow.color.hex_rgb:
        color = f"#{shadow.color.hex_rgb.upper()}"
    else:
        from tokenmoulds.design.color_defaults import SHADOW_COLOR

        color = SHADOW_COLOR

    # Opacity (ODF uses 0-100%, OOXML uses 0-100000)
    if shadow.color.modifiers and shadow.color.modifiers.alpha is not None:
        opacity = shadow.color.modifiers.alpha / 1000.0
    else:
        opacity = 100.0

    return offset_x, offset_y, color, opacity


def apply_shadow_to_graphic_props(
    graphic_props: etree._Element,
    shadow: "Shadow",
) -> None:
    """Apply shadow properties to a graphic-properties element.

    ODF shadows are simpler than OOXML:
    - No blur radius (always sharp-edged)
    - No inner shadow support
    - Offset is in cm, not EMU

    Args:
        graphic_props: The style:graphic-properties element
        shadow: Shadow primitive (blur_radius is ignored in ODF)

    Note:
        ODF shadow direction is handled differently - uses x/y offsets
        rather than angle + distance. We convert from the OOXML model.
    """
    offset_x, offset_y, color, opacity = _compute_shadow_attrs(shadow)

    graphic_props.set(q("draw", "shadow"), "visible")
    graphic_props.set(q("draw", "shadow-offset-x"), f"{offset_x:.3f}cm")
    graphic_props.set(q("draw", "shadow-offset-y"), f"{offset_y:.3f}cm")
    graphic_props.set(q("draw", "shadow-color"), color)
    graphic_props.set(q("draw", "shadow-opacity"), f"{opacity:.0f}%")


def create_odf_shadow_style(
    shadow: "Shadow",
    style_name: str = "ShadowStyle",
) -> str:
    """Create ODF style attributes for shadow.

    Returns a dict-like string of attributes to add to a style element.
    This is for use with template substitution.

    Args:
        shadow: Shadow primitive
        style_name: Name for the style

    Returns:
        Attribute string for embedding in template
    """
    offset_x, offset_y, color, opacity = _compute_shadow_attrs(shadow)

    return (
        f'draw:shadow="visible" '
        f'draw:shadow-offset-x="{offset_x:.3f}cm" '
        f'draw:shadow-offset-y="{offset_y:.3f}cm" '
        f'draw:shadow-color="{color}" '
        f'draw:shadow-opacity="{opacity:.0f}%"'
    )


# =============================================================================
# GRADIENT DEFINITIONS
# =============================================================================


def create_gradient_element(
    parent: etree._Element,
    gradient: "GradientFill",
    gradient_name: str,
) -> etree._Element:
    """Create a draw:gradient element for ODF styles.

    ODF gradients are defined as named styles in office:styles,
    then referenced by name in graphic-properties.

    Args:
        parent: Parent element (usually office:styles)
        gradient: GradientFill primitive
        gradient_name: Unique name for this gradient

    Returns:
        The created draw:gradient element

    Example:
        >>> create_gradient_element(styles, gradient, "Gradient1")
        >>> # Produces: <draw:gradient draw:name="Gradient1" draw:style="linear" .../>
    """
    from tokenmoulds.ir.style_primitives import GradientType

    grad = etree.SubElement(parent, q("draw", "gradient"))
    grad.set(q("draw", "name"), gradient_name)
    grad.set(q("draw", "display-name"), gradient_name)

    # Gradient style
    if gradient.gradient_type == GradientType.LINEAR:
        grad.set(q("draw", "style"), "linear")
        # Angle in ODF is in degrees (0-360), same as our stored value / 60000
        angle = gradient.angle / 60000.0 if gradient.angle else 0
        grad.set(q("draw", "angle"), f"{int(angle)}")
    elif gradient.gradient_type == GradientType.PATH:
        grad.set(q("draw", "style"), "radial")
        # ODF radial doesn't support offset center like OOXML fillToRect
        grad.set(q("draw", "cx"), "50%")
        grad.set(q("draw", "cy"), "50%")

    # Start and end colors (ODF only supports 2-stop gradients natively)
    if gradient.stops and len(gradient.stops) >= 2:
        start_stop = gradient.stops[0]
        end_stop = gradient.stops[-1]

        if start_stop.color.hex_rgb:
            grad.set(q("draw", "start-color"), f"#{start_stop.color.hex_rgb.upper()}")
        if end_stop.color.hex_rgb:
            grad.set(q("draw", "end-color"), f"#{end_stop.color.hex_rgb.upper()}")

        # Opacity (ODF uses intensity, 0-100%)
        grad.set(q("draw", "start-intensity"), "100%")
        grad.set(q("draw", "end-intensity"), "100%")

    return grad


def apply_gradient_to_graphic_props(
    graphic_props: etree._Element,
    gradient_name: str,
) -> None:
    """Apply a gradient fill reference to graphic-properties.

    The gradient must already be defined in office:styles.

    Args:
        graphic_props: The style:graphic-properties element
        gradient_name: Name of the gradient defined in styles
    """
    graphic_props.set(q("draw", "fill"), "gradient")
    graphic_props.set(q("draw", "fill-gradient-name"), gradient_name)


# =============================================================================
# CUSTOM GEOMETRY (SVG Paths)
# =============================================================================


def _arc_to_cubics(
    rx: float,
    ry: float,
    start_angle_60k: int,
    sweep_angle_60k: int,
) -> list[tuple[tuple[float, float], tuple[float, float], tuple[float, float]]]:
    """Convert an OOXML arcTo to cubic bezier control-point triples.

    OOXML arcs use center parameterization with angles in 60000ths of a degree.
    This converts them to cubic bezier segments suitable for SVG ``C`` commands.

    Each returned tuple is ``(cp1, cp2, endpoint)`` — the three points after the
    implicit current point that form a cubic bezier curve.

    Algorithm adapted from svg2ooxml's ``_arc_to_cubic_segments``.
    """
    if rx == 0 or ry == 0 or sweep_angle_60k == 0:
        return []

    start_rad = math.radians(start_angle_60k / 60000.0)
    sweep_rad = math.radians(sweep_angle_60k / 60000.0)

    # Split into segments of at most 90°
    n_segments = max(1, int(math.ceil(abs(sweep_rad) / (math.pi / 2))))
    delta = sweep_rad / n_segments
    t = 8.0 / 3.0 * math.sin(delta / 4.0) ** 2 / math.sin(delta / 2.0)

    result: list[
        tuple[tuple[float, float], tuple[float, float], tuple[float, float]]
    ] = []
    angle = start_rad
    for _ in range(n_segments):
        cos_s = math.cos(angle)
        sin_s = math.sin(angle)
        end_angle = angle + delta
        cos_e = math.cos(end_angle)
        sin_e = math.sin(end_angle)

        # Control points relative to arc center (0, 0)
        p0x = rx * cos_s
        p0y = ry * sin_s
        p3x = rx * cos_e
        p3y = ry * sin_e

        cp1 = (p0x - rx * sin_s * t, p0y + ry * cos_s * t)
        cp2 = (p3x + rx * sin_e * t, p3y - ry * cos_e * t)
        end = (p3x, p3y)

        result.append((cp1, cp2, end))
        angle = end_angle

    return result


def geometry_to_svg_path(geometry: "CustomGeometry") -> str:
    """Convert CustomGeometry to SVG path data string.

    ODF uses SVG path syntax for custom shapes via the svg:d attribute
    on draw:path elements.

    Args:
        geometry: CustomGeometry primitive with paths

    Returns:
        SVG path data string (M, L, C, Q, A, Z commands)

    Example:
        >>> geometry_to_svg_path(rect_geometry)
        >>> # Returns: "M 0 0 L 100 0 L 100 100 L 0 100 Z"
    """
    from tokenmoulds.ir.style_primitives import PathCommand

    path_data = []

    for geom_path in geometry.paths:
        for segment in geom_path.segments:
            if segment.command == PathCommand.MOVE_TO:
                pt = segment.points[0]
                path_data.append(f"M {pt.x} {pt.y}")

            elif segment.command == PathCommand.LINE_TO:
                pt = segment.points[0]
                path_data.append(f"L {pt.x} {pt.y}")

            elif segment.command == PathCommand.CUBIC_BEZIER_TO:
                if len(segment.points) >= 3:
                    p1, p2, p3 = segment.points[:3]
                    path_data.append(f"C {p1.x} {p1.y} {p2.x} {p2.y} {p3.x} {p3.y}")

            elif segment.command == PathCommand.QUAD_BEZIER_TO:
                if len(segment.points) >= 2:
                    p1, p2 = segment.points[:2]
                    path_data.append(f"Q {p1.x} {p1.y} {p2.x} {p2.y}")

            elif segment.command == PathCommand.ARC_TO:
                if segment.arc_params:
                    arc = segment.arc_params
                    beziers = _arc_to_cubics(
                        float(arc.width_radius),
                        float(arc.height_radius),
                        arc.start_angle,
                        arc.sweep_angle,
                    )
                    for cp1, cp2, end in beziers:
                        path_data.append(
                            f"C {cp1[0]:.6g} {cp1[1]:.6g} "
                            f"{cp2[0]:.6g} {cp2[1]:.6g} "
                            f"{end[0]:.6g} {end[1]:.6g}"
                        )

            elif segment.command == PathCommand.CLOSE:
                path_data.append("Z")

    return " ".join(path_data)


def create_path_element(
    parent: etree._Element,
    geometry: "CustomGeometry",
    x: str = "0cm",
    y: str = "0cm",
    width: str = "10cm",
    height: str = "10cm",
) -> etree._Element:
    """Create a draw:path element with custom geometry.

    Args:
        parent: Parent element
        geometry: CustomGeometry primitive
        x, y: Position
        width, height: Size

    Returns:
        The created draw:path element
    """
    path = etree.SubElement(parent, q("draw", "path"))
    path.set(q("svg", "x"), x)
    path.set(q("svg", "y"), y)
    path.set(q("svg", "width"), width)
    path.set(q("svg", "height"), height)
    path.set(q("svg", "d"), geometry_to_svg_path(geometry))
    path.set(
        q("svg", "viewBox"),
        f"0 0 {width.removesuffix('cm')} {height.removesuffix('cm')}",
    )

    return path


# =============================================================================
# EFFECTS CONTAINER (Limited ODF Support)
# =============================================================================


def apply_effects_to_graphic_props(
    graphic_props: etree._Element,
    effects: "Effects",
    gradient_name: Optional[str] = None,
) -> None:
    """Apply effects to graphic-properties element.

    Only shadow is supported in ODF. Other effects are silently ignored.

    Args:
        graphic_props: The style:graphic-properties element
        effects: Effects primitive (only shadow is used)
        gradient_name: If set, apply this gradient fill

    Note:
        The following OOXML effects are NOT supported in ODF:
        - glow
        - reflection
        - soft_edge
        - inner_shadow
        - bevel_top/bottom
        - scene_3d
    """
    # Shadow (the only effect ODF supports)
    if effects.shadow is not None and not effects.shadow.inner:
        apply_shadow_to_graphic_props(graphic_props, effects.shadow)

    # Gradient fill (if provided)
    if gradient_name:
        apply_gradient_to_graphic_props(graphic_props, gradient_name)


def get_unsupported_effects(effects: "Effects") -> List[str]:
    """Get list of effect types that won't render in ODF.

    Useful for logging warnings when exporting to ODF.

    Args:
        effects: Effects primitive to check

    Returns:
        List of unsupported effect names
    """
    unsupported = []

    if effects.glow is not None:
        unsupported.append("glow")
    if effects.reflection is not None:
        unsupported.append("reflection")
    if effects.soft_edge is not None:
        unsupported.append("soft_edge")
    if effects.inner_shadow is not None:
        unsupported.append("inner_shadow")
    if effects.bevel_top is not None:
        unsupported.append("bevel_top")
    if effects.bevel_bottom is not None:
        unsupported.append("bevel_bottom")
    if effects.scene_3d is not None:
        unsupported.append("scene_3d")

    return unsupported


__all__ = [
    # Table defaults
    "DEFAULT_TABLE_BORDER_WIDTH_PT",
    "DEFAULT_TABLE_BORDER_COLOR",
    "DEFAULT_TABLE_PADDING_X_PT",
    "DEFAULT_TABLE_PADDING_Y_PT",
    # Namespaces
    "DRAW_NS",
    "STYLE_NS",
    "FO_NS",
    "SVG_NS",
    "OFFICE_NS",
    "TEXT_NS",
    "TABLE_NS",
    "ODF_NSMAP",
    "q",
    # Style finding helpers
    "find_style",
    "find_font_face",
    "find_default_style",
    "find_list_style",
    "set_font_family",
    "set_text_props",
    # Shadow
    "apply_shadow_to_graphic_props",
    "create_odf_shadow_style",
    # Gradient
    "create_gradient_element",
    "apply_gradient_to_graphic_props",
    # Custom geometry
    "geometry_to_svg_path",
    "create_path_element",
    # Effects container
    "apply_effects_to_graphic_props",
    "get_unsupported_effects",
]
