#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2026 jondale

import sys
from pathlib import Path

deps_dir = Path(__file__).parent / "deps"
if deps_dir.exists() and str(deps_dir) not in sys.path:
    sys.path.insert(0, str(deps_dir))

import math
import inkex
from inkex import PathElement, Rectangle, Transform


def calculate_path_length(path):
    if isinstance(path, str):
        path = inkex.Path(path)

    csp = path.to_superpath()
    total_length = 0.0

    for subpath in csp:
        for i, seg in enumerate(subpath[:-1]):
            next_seg = subpath[i + 1]
            bezier = (seg[1], seg[2], next_seg[0], next_seg[1])
            seg_length = inkex.bezier.bezierlength(bezier, tolerance=0.01)
            total_length += seg_length

    return total_length


def point_at_length(path, target_length):
    if isinstance(path, str):
        path = inkex.Path(path)

    csp = path.to_superpath()
    current_length = 0.0

    for subpath in csp:
        for i, seg in enumerate(subpath[:-1]):
            next_seg = subpath[i + 1]
            bezier = (seg[1], seg[2], next_seg[0], next_seg[1])
            seg_length = inkex.bezier.bezierlength(bezier, tolerance=0.01)

            if current_length + seg_length >= target_length:
                t = (target_length - current_length) / seg_length if seg_length > 0 else 0
                t = max(0.0, min(1.0, t))

                p0, p1, p2, p3 = bezier
                p0_p1_dist = ((p1[0] - p0[0])**2 + (p1[1] - p0[1])**2)**0.5
                p2_p3_dist = ((p3[0] - p2[0])**2 + (p3[1] - p2[1])**2)**0.5

                if p0_p1_dist < 0.001 and p2_p3_dist < 0.001:
                    point = (
                        p0[0] + t * (p3[0] - p0[0]),
                        p0[1] + t * (p3[1] - p0[1])
                    )
                else:
                    point = inkex.bezier.bezierpointatt(bezier, t)

                p0, p1, p2, p3 = bezier
                one_minus_t = 1.0 - t

                dx = (3 * one_minus_t * one_minus_t * (p1[0] - p0[0]) +
                      6 * one_minus_t * t * (p2[0] - p1[0]) +
                      3 * t * t * (p3[0] - p2[0]))
                dy = (3 * one_minus_t * one_minus_t * (p1[1] - p0[1]) +
                      6 * one_minus_t * t * (p2[1] - p1[1]) +
                      3 * t * t * (p3[1] - p2[1]))

                length = (dx * dx + dy * dy) ** 0.5
                if length > 0:
                    dx /= length
                    dy /= length

                return (point[0], point[1]), (dx, dy)

            current_length += seg_length

    last_subpath = csp[-1]
    last_point = last_subpath[-1][1]
    return (last_point[0], last_point[1]), (1.0, 0.0)


def _apply_placement(item, x, y, angle):
    # Bake placement into the path data; leaving it as a transform attr lets
    # Inkscape shift the shape on ungroup (near-identity matrices lose the X translate).
    transform = Transform()
    transform.add_translate(x, y)
    transform.add_rotate(angle)
    item.path = item.path.transform(transform)
    return item


def pattern_along_path(path, num_items, item_width, start_offset, spacing, create_shape_fn):

    if isinstance(path, str):
        path = inkex.Path(path)

    if num_items <= 0:
        return []

    total_length = calculate_path_length(path)
    items = []

    if spacing == "even":
        gap = (total_length - num_items * item_width) / num_items

        for i in range(num_items):
            item_start = (start_offset + i * (gap + item_width)) % total_length
            item_center = (item_start + item_width / 2) % total_length

            point, tangent = point_at_length(path, item_center)
            angle = math.degrees(math.atan2(tangent[1], tangent[0]))

            item = create_shape_fn(i)
            _apply_placement(item, point[0], point[1], angle)
            items.append(item)

    elif spacing == "endpoints":
        for i in range(num_items):
            base_distance = total_length * i / (num_items - 1) if num_items > 1 else 0
            distance = (base_distance + start_offset) % total_length
            point, tangent = point_at_length(path, distance)
            angle = math.degrees(math.atan2(tangent[1], tangent[0]))

            item = create_shape_fn(i)
            _apply_placement(item, point[0], point[1], angle)
            items.append(item)

    elif spacing == "simple":
        gap = (total_length - num_items * item_width) / num_items

        for i in range(num_items):
            item_start = (start_offset + i * (gap + item_width)) % total_length
            distance = (item_start + item_width / 2) % total_length
            point, tangent = point_at_length(path, distance)
            angle = math.degrees(math.atan2(tangent[1], tangent[0]))

            item = create_shape_fn(i)
            _apply_placement(item, point[0], point[1], angle)
            items.append(item)

    return items
