"""Classes for rendering in the SVG format"""

import dataclasses
import typing
import math
import xml.sax.saxutils

import momapy.drawing
import momapy.geometry
import momapy.rendering.core


@dataclasses.dataclass
class SVGElement(object):
    """Class for SVG elements.

    This class represents an SVG element with a name, optional text value,
    attributes, and child elements.

    Attributes:
        name: The tag name of the SVG element (e.g., 'svg', 'rect', 'path')
        value: Optional text content of the element
        attributes: Dictionary of attribute names and values
        elements: List of child SVGElement instances

    Example:
        >>> element = SVGElement(
        ...     name="rect",
        ...     attributes={"x": "0", "y": "0", "width": "100", "height": "100"}
        ... )
        >>> print(element)
        <rect x="0" y="0" width="100" height="100">
        </rect>
    """

    name: str
    value: typing.Optional[str] = None
    attributes: dict = dataclasses.field(default_factory=dict)
    elements: list["SVGElement"] = dataclasses.field(default_factory=list)

    def to_string(self, indent: int = 0):
        """Return the SVG string representing the element.

        Args:
            indent: The indentation level (number of tabs)

        Returns:
            The SVG markup as a string
        """
        s_indent = "\t" * indent
        s_value = f"{s_indent}{self.value}\n" if self.value is not None else ""
        if self.attributes:
            l_s_attributes = []
            for attr_name, attr_value in self.attributes.items():
                s_attr_name = attr_name
                s_attr_value = f'"{attr_value}"'
                s_attribute = f"{s_attr_name}={s_attr_value}"
                l_s_attributes.append(s_attribute)
            s_attributes = f" {' '.join(l_s_attributes)}"
        else:
            s_attributes = ""
        if self.elements:
            s_elements = "\n".join(
                [child.to_string(indent + 1) for child in self.elements]
            )
            s_elements += "\n"
        else:
            s_elements = ""
        return f"{s_indent}<{self.name}{s_attributes}>\n{s_value}{s_elements}{s_indent}</{self.name}>"

    def __str__(self):
        return self.to_string()

    def add_element(self, element):
        """Add a sub-element to the SVG element.

        Args:
            element: The child SVGElement to add
        """
        self.elements.append(element)


@dataclasses.dataclass
class SVGNativeRenderer(momapy.rendering.core.Renderer):
    """Renderer implementation for generating native SVG output.

    This renderer creates SVG markup directly without external dependencies.
    It supports all standard SVG features including filters, transformations,
    and presentation attributes.

    Attributes:
        svg: The root SVGElement that will contain all rendered content

    Example:
        >>> from momapy.meta.nodes import Rectangle
        >>> import momapy.geometry
        >>> # Create a layout element to render
        >>> node = Rectangle(
        ...     position=momapy.geometry.Point(100, 100),
        ...     width=200,
        ...     height=100
        ... )
        >>> # Create renderer and render the element
        >>> renderer = SVGNativeRenderer.from_file("output.svg", 800, 600, "svg")
        >>> renderer.begin_session()
        >>> renderer.render_layout_element(node)
        >>> renderer.end_session()
    """

    supported_formats: typing.ClassVar[list[str]] = ["svg"]
    _de_class_func_mapping: typing.ClassVar[dict] = {
        momapy.drawing.Group: "_make_group_element",
        momapy.drawing.Path: "_make_path_element",
        momapy.drawing.Text: "_make_text_element",
        momapy.drawing.Ellipse: "_make_ellipse_element",
        momapy.drawing.Rectangle: "_make_rectangle_element",
    }
    _pa_class_func_mapping: typing.ClassVar[dict] = {
        momapy.drawing.MoveTo: "_make_move_to_value",
        momapy.drawing.LineTo: "_make_line_to_value",
        momapy.drawing.CurveTo: "_make_curve_to_value",
        momapy.drawing.QuadraticCurveTo: "_make_quadratic_curve_to_value",
        momapy.drawing.ClosePath: "_make_close_value",
        momapy.drawing.EllipticalArc: "_make_elliptical_arc_value",
    }
    _tr_class_func_mapping: typing.ClassVar[dict] = {
        momapy.geometry.Translation: "_make_translation_value",
        momapy.geometry.Rotation: "_make_rotation_value",
        momapy.geometry.Scaling: "_make_scaling_value",
        momapy.geometry.MatrixTransformation: "_make_matrix_transformation_value",
    }
    _fe_class_func_mapping: typing.ClassVar[dict] = {
        momapy.drawing.DropShadowEffect: "_make_drop_shadow_effect_element",
        momapy.drawing.CompositeEffect: "_make_composite_effect_element",
        momapy.drawing.GaussianBlurEffect: "_make_gaussian_blur_effect_element",
        momapy.drawing.OffsetEffect: "_make_offset_effect_element",
        momapy.drawing.FloodEffect: "_make_flood_effect_element",
    }
    _fe_composite_comp_op_value_mapping: typing.ClassVar[dict] = {
        momapy.drawing.CompositionOperator.OVER: "over",
        momapy.drawing.CompositionOperator.IN: "in",
        momapy.drawing.CompositionOperator.OUT: "out",
        momapy.drawing.CompositionOperator.ATOP: "atop",
        momapy.drawing.CompositionOperator.XOR: "xor",
        momapy.drawing.CompositionOperator.LIGHTER: "lighter",
        momapy.drawing.CompositionOperator.ARTIHMETIC: "arithmetic",
    }
    _fe_gaussian_blur_edgemode_value_mapping: typing.ClassVar[dict] = {
        momapy.drawing.EdgeMode.WRAP: "wrap",
        momapy.drawing.EdgeMode.DUPLICATE: "duplicate",
        momapy.drawing.NoneValue: "none",
    }
    _fe_filter_unit_value_mapping: typing.ClassVar[dict] = {
        momapy.drawing.FilterUnits.USER_SPACE_ON_USE: "UserSpaceOnUse",
        momapy.drawing.FilterUnits.OBJECT_BOUNDING_BOX: "objectBoundingBox",
    }
    _fe_input_value_mapping: typing.ClassVar[dict] = {
        momapy.drawing.FilterEffectInput.SOURCE_GRAPHIC: "SourceGraphic",
        momapy.drawing.FilterEffectInput.SOURCE_ALPHA: "SourceAlpha",
        momapy.drawing.FilterEffectInput.BACKGROUND_IMAGE: "BackgroundImage",
        momapy.drawing.FilterEffectInput.BACKGROUND_ALPHA: "BackgroundAlpha",
        momapy.drawing.FilterEffectInput.FILL_PAINT: "FillPaint",
        momapy.drawing.FilterEffectInput.STROKE_PAINT: "StrokePaint",
    }
    _te_font_style_value_mapping: typing.ClassVar[dict] = {
        momapy.drawing.FontStyle.NORMAL: "normal",
        momapy.drawing.FontStyle.ITALIC: "italic",
        momapy.drawing.FontStyle.OBLIQUE: "oblique",
    }
    _te_font_weight_value_mapping: typing.ClassVar[dict] = {
        momapy.drawing.FontWeight.NORMAL: "normal",
        momapy.drawing.FontWeight.BOLD: "bold",
        momapy.drawing.FontWeight.BOLDER: "bolder",
        momapy.drawing.FontWeight.LIGHTER: "lighter",
    }
    _te_text_anchor_value_mapping: typing.ClassVar[dict] = {
        momapy.drawing.TextAnchor.START: "start",
        momapy.drawing.TextAnchor.MIDDLE: "middle",
        momapy.drawing.TextAnchor.END: "end",
    }
    _de_fill_rule_value_mapping: typing.ClassVar[dict] = {
        momapy.drawing.FillRule.NONZERO: "nonzero",
        momapy.drawing.FillRule.EVENODD: "evenodd",
    }

    svg: SVGElement
    config: dict = dataclasses.field(default_factory=dict)
    _filter_elements: list[SVGElement] = dataclasses.field(default_factory=list)

    @classmethod
    def from_file(cls, output_file, width, height, format_, config=None):
        """Create an SVGNativeRenderer instance from a file path.

        Args:
            output_file: The output file path
            width: The width of the SVG canvas
            height: The height of the SVG canvas
            format_: The output format (must be "svg")
            config: Optional configuration dictionary

        Returns:
            A new SVGNativeRenderer instance

        Raises:
            ValueError: If the format is not supported

        Example:
            >>> renderer = SVGNativeRenderer.from_file("output.svg", 800, 600, "svg")
        """
        if format_ not in cls.supported_formats:
            raise ValueError(f"Unsupported format: {format_}")
        if config is None:
            config = {}
        config["output_file"] = output_file
        config["width"] = width
        config["height"] = height
        config["format"] = format_
        svg = SVGElement(
            name="svg",
            attributes={
                "xmlns": "http://www.w3.org/2000/svg",
                "viewBox": f"0 0 {width} {height}",
            },
        )
        return cls(svg=svg, config=config)

    def begin_session(self):
        """Begin a rendering session.

        This method initializes the rendering context. For SVGNativeRenderer,
        no explicit initialization is needed as the SVG element is created
        during instantiation.
        """
        pass

    def end_session(self):
        """End the rendering session and save the output.

        This method finalizes the SVG document, adds any filter definitions
        to the defs section, and writes the output to the file.
        """
        if self._filter_elements:
            defs = SVGElement(name="defs", elements=self._filter_elements)
            self.svg.add_element(defs)
        if self.config.get("output_file") is not None:
            with open(self.config["output_file"], "w", encoding="utf-8") as f:
                f.write(str(self.svg))

    def new_page(self, width, height):
        """Create a new page in the output document.

        Args:
            width: The width of the new page
            height: The height of the new page

        Note:
            SVG format does not support multiple pages. This method is a no-op.
        """
        pass

    def render_map(self, map_):
        """Render a map to the output.

        Args:
            map_: The map to render
        """
        self.render_layout_element(map_.layout)

    def render_layout_element(self, layout_element):
        """Render a layout element to the output.

        Args:
            layout_element: The layout element to render
        """
        drawing_elements = layout_element.drawing_elements()
        for drawing_element in drawing_elements:
            self.render_drawing_element(drawing_element)

    def render_drawing_element(self, drawing_element):
        """Render a drawing element to the output.

        Args:
            drawing_element: The drawing element to render

        This method converts the drawing element to an SVG element
        and adds it to the SVG document.
        """
        element = self._make_drawing_element_element(drawing_element)
        self.svg.add_element(element)

    def _make_color_value(self, color):
        return f"rgb({color.red}, {color.green}, {color.blue})"

    def _make_opacity_value(self, color):
        return str(color.alpha)

    def _make_transform_value(self, transform):
        value = " ".join(
            [
                self._make_transformation_value(transformation)
                for transformation in transform
            ]
        )
        return value

    def _make_transformation_value(self, transformation):
        class_ = type(transformation)
        if issubclass(class_, momapy.builder.Builder):
            class_ = class_._cls_to_build
        tr_func = getattr(self, self._tr_class_func_mapping[class_])
        return tr_func(transformation)

    def _make_drawing_element_element_id_class_atrributes(self, drawing_element):
        attributes = {}
        id_ = getattr(drawing_element, "id_", None)
        if id_ is not None:
            attributes["id"] = id_
        class_ = getattr(drawing_element, "class_", None)
        if class_ is not None:
            attributes["class"] = class_
        return attributes

    def _make_drawing_element_presentation_attributes(self, drawing_element):
        attributes = {}
        for attr_name in momapy.drawing.PRESENTATION_ATTRIBUTES:
            attr_value = getattr(drawing_element, attr_name)
            if attr_value is not None:
                if attr_value == momapy.drawing.NoneValue:
                    attr_value = "none"
                else:
                    if attr_name == "stroke" or attr_name == "fill":
                        opacity_value = self._make_opacity_value(attr_value)
                        attributes[f"{attr_name}-opacity"] = opacity_value
                        attr_value = self._make_color_value(attr_value)
                    elif attr_name == "transform":
                        attr_value = self._make_transform_value(attr_value)
                    elif attr_name == "filter":
                        filter_element = self._make_filter_element(attr_value)
                        if filter_element not in self._filter_elements:
                            self._filter_elements.append(filter_element)
                        attr_value = f"url(#{attr_value.id_})"
                    elif attr_name == "font_style":
                        attr_value = self._te_font_style_value_mapping[attr_value]
                    elif attr_name == "font_weight":
                        if isinstance(attr_value, momapy.drawing.FontWeight):
                            attr_value = self._te_font_weight_value_mapping[attr_value]
                    elif attr_name == "text_anchor":
                        attr_value = self._te_text_anchor_value_mapping[attr_value]
                    elif attr_name == "fill_rule":
                        attr_value = self._de_fill_rule_value_mapping[attr_value]
                    elif attr_name == "stroke_dasharray":
                        attr_value = " ".join(
                            [
                                str(attr_value_element)
                                for attr_value_element in attr_value
                            ]
                        )
                attr_name = attr_name.replace("_", "-")
                attributes[attr_name] = attr_value
        return attributes

    def _make_filter_element(self, filter_):
        name = "filter"
        attributes = {}
        attributes["id"] = filter_.id_
        attributes["filterUnits"] = self._fe_filter_unit_value_mapping[
            filter_.filter_units
        ]
        attributes["x"] = filter_.x
        attributes["y"] = filter_.y
        attributes["width"] = filter_.width
        attributes["height"] = filter_.height
        subelements = []
        for filter_effect in filter_.effects:
            subelement = self._make_filter_effect_element(filter_effect)
            subelements.append(subelement)
        element = SVGElement(name=name, attributes=attributes, elements=subelements)
        return element

    def _make_filter_effect_element(self, filter_effect):
        class_ = type(filter_effect)
        if issubclass(class_, momapy.builder.Builder):
            class_ = class_._cls_to_build
        fe_func = getattr(self, self._fe_class_func_mapping[class_])
        element = fe_func(filter_effect)
        return element

    def _make_drop_shadow_effect_element(self, filter_effect):
        name = "feDropShadow"
        attributes = {}
        attributes["dx"] = filter_effect.dx
        attributes["dy"] = filter_effect.dy
        attributes["stdDeviation"] = filter_effect.std_deviation
        attributes["flood-opacity"] = filter_effect.flood_opacity
        attributes["flood-color"] = self._make_color_value(filter_effect.flood_color)
        if filter_effect.result is not None:
            attributes["result"] = filter_effect.result
        element = SVGElement(
            name=name,
            attributes=attributes,
        )
        return element

    def _make_composite_effect_element(self, filter_effect):
        name = "feComposite"
        attributes = {}
        if isinstance(filter_effect.in_, momapy.drawing.FilterEffectInput):
            attributes["in"] = self._fe_input_value_mapping[filter_effect.in_]
        else:
            attributes["in"] = filter_effect.in_
        if isinstance(filter_effect.in2, momapy.drawing.FilterEffectInput):
            attributes["in2"] = self._fe_input_value_mapping[filter_effect.in2]
        else:
            attributes["in2"] = filter_effect.in2
        attributes["operator"] = self._fe_composite_comp_op_value_mapping[
            filter_effect.operator
        ]
        if filter_effect.result is not None:
            attributes["result"] = filter_effect.result
        element = SVGElement(
            name=name,
            attributes=attributes,
        )
        return element

    def _make_flood_effect_element(self, filter_effect):
        name = "feFlood"
        attributes = {}
        attributes["flood-opacity"] = filter_effect.flood_opacity
        attributes["flood-color"] = self._make_color_value(filter_effect.flood_color)
        if filter_effect.result is not None:
            attributes["result"] = filter_effect.result
        element = SVGElement(
            name=name,
            attributes=attributes,
        )
        return element

    def _make_gaussian_blur_effect_element(self, filter_effect):
        name = "feGaussianBlur"
        attributes = {}
        if isinstance(filter_effect.in_, momapy.drawing.FilterEffectInput):
            attributes["in"] = self._fe_input_value_mapping[filter_effect.in_]
        else:
            attributes["in"] = filter_effect.in_
        attributes["stdDeviation"] = filter_effect.std_deviation
        if filter_effect.edge_mode is not None:
            attributes["edgeMode"] = self._fe_gaussian_blur_edgemode_value_mapping[
                filter_effect.edge_mode
            ]
        if filter_effect.result is not None:
            attributes["result"] = filter_effect.result
        element = SVGElement(
            name=name,
            attributes=attributes,
        )
        return element

    def _make_offset_effect_element(self, filter_effect):
        name = "feOffset"
        attributes = {}
        attributes["dx"] = filter_effect.dx
        attributes["dy"] = filter_effect.dy
        if filter_effect.result is not None:
            attributes["result"] = filter_effect.result
        element = SVGElement(
            name=name,
            attributes=attributes,
        )
        return element

    def _make_drawing_element_element(self, drawing_element):
        class_ = type(drawing_element)
        if issubclass(class_, momapy.builder.Builder):
            class_ = class_._cls_to_build
        de_func = getattr(self, self._de_class_func_mapping[class_])
        element = de_func(drawing_element)
        return element

    def _make_group_element(self, group):
        name = "g"
        presentation_attributes = self._make_drawing_element_presentation_attributes(
            group
        )
        id_class_attributes = self._make_drawing_element_element_id_class_atrributes(
            group
        )
        attributes = presentation_attributes | id_class_attributes
        subelements = []
        for drawing_element in group.elements:
            subelement = self._make_drawing_element_element(drawing_element)
            subelements.append(subelement)
        element = SVGElement(name=name, attributes=attributes, elements=subelements)
        return element

    def _make_path_action_value(self, path_action):
        class_ = type(path_action)
        if issubclass(class_, momapy.builder.Builder):
            class_ = class_._cls_to_build
        pa_func = getattr(self, self._pa_class_func_mapping[class_])
        value = pa_func(path_action)
        return value

    def _make_move_to_value(self, move_to):
        return f"M {move_to.x} {move_to.y}"

    def _make_line_to_value(self, line_to):
        return f"L {line_to.x} {line_to.y}"

    def _make_close_value(self, close):
        return "Z"

    def _make_elliptical_arc_value(self, elliptical_arc):
        return (
            f"A {elliptical_arc.rx} "
            f"{elliptical_arc.ry} "
            f"{elliptical_arc.x_axis_rotation} "
            f"{elliptical_arc.arc_flag} "
            f"{elliptical_arc.sweep_flag} "
            f"{elliptical_arc.x} "
            f"{elliptical_arc.y}"
        )

    def _make_quadratic_curve_to_value(self, quadratic_curve_to):
        return (
            f"Q {quadratic_curve_to.control_point.x} "
            f"{quadratic_curve_to.control_point.y} "
            f"{quadratic_curve_to.x} "
            f"{quadratic_curve_to.y}"
        )

    def _make_curve_to_value(self, curve_to):
        return (
            f"C {curve_to.control_point1.x} "
            f"{curve_to.control_point1.y} "
            f"{curve_to.control_point2.x} "
            f"{curve_to.control_point2.y} "
            f"{curve_to.x} "
            f"{curve_to.y}"
        )

    def _make_path_element(self, path):
        name = "path"
        presentation_attributes = self._make_drawing_element_presentation_attributes(
            path
        )
        id_class_attributes = self._make_drawing_element_element_id_class_atrributes(
            path
        )
        attributes = presentation_attributes | id_class_attributes
        d_value = " ".join(
            [self._make_path_action_value(path_action) for path_action in path.actions]
        )
        attributes["d"] = d_value
        element = SVGElement(name=name, attributes=attributes)
        return element

    def _make_text_element(self, text):
        name = "text"
        presentation_attributes = self._make_drawing_element_presentation_attributes(
            text
        )
        id_class_attributes = self._make_drawing_element_element_id_class_atrributes(
            text
        )
        attributes = presentation_attributes | id_class_attributes
        attributes["x"] = text.x
        attributes["y"] = text.y
        value = xml.sax.saxutils.escape(text.text)
        element = SVGElement(name=name, attributes=attributes, value=value)
        return element

    def _make_ellipse_element(self, ellipse):
        name = "ellipse"
        presentation_attributes = self._make_drawing_element_presentation_attributes(
            ellipse
        )
        id_class_attributes = self._make_drawing_element_element_id_class_atrributes(
            ellipse
        )
        attributes = presentation_attributes | id_class_attributes
        attributes["cx"] = ellipse.x
        attributes["cy"] = ellipse.y
        attributes["rx"] = ellipse.rx
        attributes["ry"] = ellipse.ry
        element = SVGElement(name=name, attributes=attributes)
        return element

    def _make_rectangle_element(self, rectangle):
        name = "rect"
        presentation_attributes = self._make_drawing_element_presentation_attributes(
            rectangle
        )
        id_class_attributes = self._make_drawing_element_element_id_class_atrributes(
            rectangle
        )
        attributes = presentation_attributes | id_class_attributes
        attributes["x"] = rectangle.x
        attributes["y"] = rectangle.y
        attributes["width"] = rectangle.width
        attributes["height"] = rectangle.height
        attributes["rx"] = rectangle.rx
        attributes["ry"] = rectangle.ry
        element = SVGElement(name=name, attributes=attributes)
        return element

    def _make_translation_value(self, translation):
        return f"translate({translation.tx} {translation.ty})"

    def _make_rotation_value(self, rotation):
        angle = math.degrees(rotation.angle)
        s_point = (
            f" {rotation.point.x} {rotation.point.y}"
            if rotation.point is not None
            else ""
        )
        value = f"rotate({angle}{s_point})"
        return value

    def _make_scaling_value(self, scaling):
        return f"scale({scaling.sx} {scaling.sy})"

    def _make_matrix_transformation_value(self, matrix_transformation):
        matrix_values = [
            matrix_transformation.m[i][j]
            for j in range(len(matrix_transformation.m[0]))
            for i in range(len(matrix_transformation.m) - 1)
        ]
        value = f"matrix({' '.join(matrix_values)})"
        return value


@dataclasses.dataclass
class SVGNativeCompatRenderer(SVGNativeRenderer):
    """Renderer for SVG with compatibility mode filters.

    This renderer extends SVGNativeRenderer to provide compatibility with
    older SVG viewers by converting filters to a compatible format.

    Example:
        >>> renderer = SVGNativeCompatRenderer.from_file("output.svg", 800, 600, "svg")
        >>> renderer.begin_session()
        >>> renderer.render_layout_element(layout_element)
        >>> renderer.end_session()
    """

    def _make_filter_element(self, filter_):
        """Create an SVG filter element with compatibility conversion.

        Args:
            filter_: The filter to convert to an SVG element

        Returns:
            An SVGElement representing the filter

        This method converts the filter to a compatible format before
        creating the SVG element.
        """
        filter_ = filter_.to_compat()
        element = super()._make_filter_element(filter_)
        return element
