#!/usr/pkg/bin/python3.13
# coding=utf-8
"""
see #inkscape on Freenode and
https://github.com/nikitakit/svg2sif/blob/master/synfig_prepare.py#L370
for an example how to do the transform of parent to children.
"""

import inkex
from inkex import (
    Group,
    Layer,
    Anchor,
    Switch,
    NamedView,
    Defs,
    Metadata,
    ForeignObject,
    ClipPath,
    Use,
    SvgDocumentElement,
)


class UngroupDeep(inkex.EffectExtension):
    def add_arguments(self, pars):
        pars.add_argument(
            "--preserve_layers",
            type=inkex.Boolean,
            default=False,
            help="Do not ungroup layers",
        )
        pars.add_argument(
            "--startdepth", type=int, default=0, help="starting depth for ungrouping"
        )
        pars.add_argument(
            "--maxdepth", type=int, default=65535, help="maximum ungrouping depth"
        )
        pars.add_argument(
            "--keepdepth",
            type=int,
            default=0,
            help="levels of ungrouping to leave untouched",
        )

    @staticmethod
    def _merge_style(node, style):
        """Propagate style and transform to remove inheritance
        Originally from
        https://github.com/nikitakit/svg2sif/blob/master/synfig_prepare.py#L370
        """

        # Compose the style attribs
        this_style = node.style
        remaining_style = {}  # Style attributes that are not propagated

        # Filters should remain on the top ancestor
        non_propagated = ["filter"]
        for key in non_propagated:
            if key in this_style.keys():
                remaining_style[key] = this_style[key]
                del this_style[key]

        # Create a copy of the parent style, and merge this style into it
        parent_style_copy = style.copy()
        parent_style_copy.update(this_style)
        this_style = parent_style_copy

        # Merge in any attributes outside of the style
        style_attribs = ["fill", "stroke"]
        for attrib in style_attribs:
            if node.get(attrib):
                this_style[attrib] = node.get(attrib)
                del node.attrib[attrib]

        if isinstance(node, (SvgDocumentElement, Anchor, Group, Switch)):
            # Leave only non-propagating style attributes
            if not remaining_style:
                if "style" in node.keys():
                    del node.attrib["style"]
            else:
                node.style = remaining_style

        else:
            # This element is not a container

            # Merge remaining_style into this_style
            this_style.update(remaining_style)

            # Set the element's style attribs
            node.style = this_style

    def _merge_clippath(self, node, clippathurl):
        if clippathurl and clippathurl != "none":
            node_transform = node.transform
            if node_transform:
                # Clip-paths on nodes with a transform have the transform
                # applied to the clipPath as well, which we don't want.  So, we
                # create new clipPath element with references to all existing
                # clippath subelements, but with the inverse transform applied
                new_clippath = self.svg.defs.add(
                    ClipPath(clipPathUnits="userSpaceOnUse")
                )
                new_clippath.set_random_id("clipPath")
                clippath = self.svg.getElementById(clippathurl[5:-1])
                for child in clippath.iterchildren():
                    new_clippath.add(Use.new(child, 0, 0))

                # Set the clippathurl to be the one with the inverse transform
                clippathurl = "url(#" + new_clippath.get("id") + ")"

            # Reference the parent clip-path to keep clipping intersection
            # Find end of clip-path chain and add reference there
            node_clippathurl = node.get("clip-path")
            while node_clippathurl:
                node = self.svg.getElementById(node_clippathurl[5:-1])
                node_clippathurl = node.get("clip-path")
            node.set("clip-path", clippathurl)

    # Flatten a group into same z-order as parent, propagating attribs
    def _ungroup(self, node):
        node_parent = node.getparent()
        node_index = list(node_parent).index(node)
        node_style = node.style

        node_transform = node.transform
        node_clippathurl = node.get("clip-path")
        node_parent.remove(node)
        for child in reversed(list(node)):
            if not isinstance(child, inkex.BaseElement):
                continue
            child.transform = node_transform @ child.transform

            if node.get("style") is not None:
                self._merge_style(child, node_style)
            self._merge_clippath(child, node_clippathurl)
            node_parent.insert(node_index, child)

    # Put all ungrouping restrictions here
    def _want_ungroup(self, node, depth, height):
        if (
            isinstance(node, Group)
            and not (self.options.preserve_layers and isinstance(node, Layer))
            and node.getparent() is not None
            and height > self.options.keepdepth
            and self.options.startdepth <= depth <= self.options.maxdepth
        ):
            return True
        return False

    def _deep_ungroup(self, node):
        # using iteration instead of recursion to avoid hitting Python
        # max recursion depth limits, which is a problem in converted PDFs

        # Seed the queue (stack) with initial node
        q = [{"node": node, "depth": 0, "prev": {"height": None}, "height": None}]

        while q:
            current = q[-1]
            node = current["node"]
            depth = current["depth"]
            height = current["height"]

            # Recursion path
            if height is None:
                # Don't enter non-graphical portions of the document
                if isinstance(node, (NamedView, Defs, Metadata, ForeignObject)):
                    q.pop()

                # Base case: Leaf node
                if not isinstance(node, Group) or not list(node):
                    current["height"] = 0

                # Recursive case: Group element with children
                else:
                    depth += 1
                    for child in node.iterchildren():
                        q.append(
                            {
                                "node": child,
                                "prev": current,
                                "depth": depth,
                                "height": None,
                            }
                        )

            # Return path
            else:
                # Ungroup if desired
                if self._want_ungroup(node, depth, height):
                    self._ungroup(node)

                # Propagate (max) height up the call chain
                height += 1
                previous = current["prev"]
                prev_height = previous["height"]
                if prev_height is None or prev_height < height:
                    previous["height"] = height

                # Only process each node once
                q.pop()

    def effect(self):
        if self.svg.selection:
            for node in self.svg.selection.values():
                self._deep_ungroup(node)
        else:
            for node in self.document.getroot():
                self._deep_ungroup(node)


if __name__ == "__main__":
    UngroupDeep().run()
