#!/usr/bin/env python3

#################################################
#
#  KM Living Hinge 
#  A highly opinionated living hinge generator
#
#  With love, by Jondale
#
#################################################

import math
import inkex
from inkex import ShapeElement, bezier

from shapes import get_shape, get_config
from livinghinge import generate_hinge, segments_to_svg_paths


class KMLivingHinge(inkex.EffectExtension):
    def add_arguments(self, pars) -> None:
        pars.add_argument("--units", default="mm")
        pars.add_argument("--angle", type=float, default=0.0)
        pars.add_argument("--type", default="line")
        pars.add_argument("--line_height_pct", type=int, default=80)
        pars.add_argument("--line_x_spacing", type=float, default=2.0)
        pars.add_argument("--line_y_spacing", type=float, default=2.0)
        pars.add_argument("--fishbone_height", type=float, default=10.0)
        pars.add_argument("--fishbone_width", type=float, default=5.0)
        pars.add_argument("--fishbone_x_spacing", type=float, default=2.0)
        pars.add_argument("--fishbone_y_spacing", type=float, default=2.0)
        pars.add_argument("--cross_height", type=float, default=10.0)
        pars.add_argument("--cross_width", type=float, default=5.0)
        pars.add_argument("--bezier_height", type=float, default=10.0)
        pars.add_argument("--bezier_width", type=float, default=5.0)
        pars.add_argument("--bezier_x_spacing", type=float, default=2.0)
        pars.add_argument("--bezier_y_spacing", type=float, default=2.0)
        pars.add_argument("--wave_height", type=float, default=10.0)
        pars.add_argument("--wave_width", type=float, default=5.0)
        pars.add_argument("--fabric_height", type=float, default=10.0)
        pars.add_argument("--fabric_width", type=float, default=5.0)
        pars.add_argument("--circle_height", type=float, default=5.0)
        pars.add_argument("--circle_width", type=float, default=5.0)
        pars.add_argument("--circle_x_spacing", type=float, default=2.0)
        pars.add_argument("--circle_y_spacing", type=float, default=2.0)

    def effect(self) -> None:
        opts = self.options
        pattern_type = opts.type or "line"
        if pattern_type not in ("line", "fishbone", "cross", "bezier", "wave", "fabric", "circle"):
            pattern_type = "line"

        selection = getattr(self.svg, "selection", None)
        if not selection or len(selection) == 0:
            raise inkex.AbortExtension(
                "Select a shape (path, rectangle, circle, etc.); the hinge fills and is trimmed to your selection."
            )
        selected_elems = list(selection)

        def _uu(value, unit):
            return float(self.svg.unittouu(f"{value}{unit}"))

        def _superpath_to_polygons(superpath):
            polys = []
            for sub in superpath:
                if not sub:
                    continue
                poly = []
                for node in sub:
                    if len(node) >= 2 and len(node[1]) == 2:
                        poly.append((node[1][0], node[1][1]))
                if len(poly) >= 3:
                    if poly[0] != poly[-1]:
                        poly.append(poly[0])
                    polys.append(poly)
            return polys

        seen_shapes = set()
        shapes_to_process = []

        def _iter_shapes(elem):
            if isinstance(elem, ShapeElement) and not isinstance(elem, inkex.Group):
                yield elem
            if isinstance(elem, inkex.Group):
                for child in elem.iterdescendants():
                    if isinstance(child, ShapeElement) and not isinstance(child, inkex.Group):
                        yield child
                return
            for child in elem.iterdescendants():
                if isinstance(child, ShapeElement) and not isinstance(child, inkex.Group):
                    yield child

        for elem in selected_elems:
            for shape in _iter_shapes(elem):
                key = getattr(shape, "get_id", lambda: None)() or shape.get("id") or id(shape)
                if key in seen_shapes:
                    continue
                seen_shapes.add(key)
                shapes_to_process.append(shape)

        if not shapes_to_process:
            raise inkex.AbortExtension("Unable to read the selected shape geometry.")

        min_x, min_y, max_x, max_y = float('inf'), float('inf'), float('-inf'), float('-inf')
        for shape in shapes_to_process:
            bbox = shape.bounding_box(shape.composed_transform())
            if bbox:
                min_x = min(min_x, bbox.left)
                min_y = min(min_y, bbox.top)
                max_x = max(max_x, bbox.right)
                max_y = max(max_y, bbox.bottom)
        bbox_width = max_x - min_x
        bbox_height = max_y - min_y
        bbox = (min_x, min_y, bbox_width, bbox_height)

        unit = opts.units
        angle_rad = math.radians(float(opts.angle))

        if pattern_type == "line":
            height = opts.line_height_pct / 100.0
            width = 0
            x_spacing = _uu(opts.line_x_spacing, unit)
            y_spacing = _uu(opts.line_y_spacing, unit)
        elif pattern_type == "fishbone":
            height = _uu(opts.fishbone_height, unit)
            width = _uu(opts.fishbone_width, unit)
            x_spacing = _uu(opts.fishbone_x_spacing, unit)
            y_spacing = _uu(opts.fishbone_y_spacing, unit)
        elif pattern_type == "cross":
            height = _uu(opts.cross_height, unit)
            width = _uu(opts.cross_width, unit)
            x_spacing = 0
            y_spacing = 0
        elif pattern_type == "bezier":
            height = _uu(opts.bezier_height, unit)
            width = _uu(opts.bezier_width, unit)
            x_spacing = _uu(opts.bezier_x_spacing, unit)
            y_spacing = _uu(opts.bezier_y_spacing, unit)
        elif pattern_type == "wave":
            height = _uu(opts.wave_height, unit)
            width = _uu(opts.wave_width, unit)
            x_spacing = 0
            y_spacing = 0
        elif pattern_type == "fabric":
            height = _uu(opts.fabric_height, unit)
            width = _uu(opts.fabric_width, unit)
            x_spacing = 0
            y_spacing = 0
        elif pattern_type == "circle":
            height = _uu(opts.circle_height, unit)
            width = _uu(opts.circle_width, unit)
            x_spacing = _uu(opts.circle_x_spacing, unit)
            y_spacing = _uu(opts.circle_y_spacing, unit)
        else:
            height = opts.line_height_pct / 100.0
            width = 0
            x_spacing = _uu(opts.line_x_spacing, unit)
            y_spacing = _uu(opts.line_y_spacing, unit)

        config_fn = get_config(pattern_type)
        height, width, x_spacing, y_spacing, y_offset = config_fn(
            height, width, x_spacing, y_spacing, bbox
        )

        if height <= 0:
            raise inkex.AbortExtension("Height must be greater than zero.")
        if width < 0:
            raise inkex.AbortExtension("Width must not be negative.")

        stroke_width = float(self.svg.unittouu("0.25mm"))
        stroke_style = {
            "stroke": "#000000",
            "stroke-width": str(stroke_width),
            "fill": "none",
            "stroke-linecap": "round",
        }

        parent = self.svg.get_current_layer()
        hinge_group = inkex.Group(id=self.svg.get_unique_id("km-living-hinge"))
        hinge_group.set("{http://www.inkscape.org/namespaces/inkscape}label", "KM Living Hinge")
        parent.add(hinge_group)

        shape_fn = get_shape(pattern_type)

        for shape in shapes_to_process:
            shape_path = shape.to_path_element().path.transform(shape.composed_transform())
            csp = shape_path.to_superpath()
            bezier.cspsubdiv(csp, 0.25)
            polygons = _superpath_to_polygons(csp)
            if not polygons:
                continue

            segments = generate_hinge(
                polygons=polygons,
                shape_fn=shape_fn,
                height=height,
                width=width,
                x_spacing=x_spacing,
                y_spacing=y_spacing,
                y_offset=y_offset,
                angle_rad=angle_rad,
            )

            segments_to_svg_paths(segments, stroke_style, hinge_group)


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