#!/usr/pkg/bin/python3.13
#
# Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
#               2021 Jonathan Neuhauser, jonathan.neuhauser@outlook.com
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
# barraud@math.univ-lille1.fr
#
"""
This script deforms an object (the pattern) along other paths (skeletons)...
The first selected object is the pattern
the last selected ones are the skeletons.

Imagine a straight horizontal line L in the middle of the bounding box of the pattern.
Consider the normal bundle of L: the collection of all the vertical lines meeting L.
Consider this as the initial state of the plane; in particular, think of the pattern
as painted on these lines.

Now move and bend L to make it fit a skeleton, and see what happens to the normals:
they move and rotate, deforming the pattern.
"""

import copy
import math

import inkex
from inkex.bezier import tpoint
from inkex.paths import CubicSuperPath
from inkex.localization import inkex_gettext as _

import pathmodifier


class PatternAlongPath(pathmodifier.PathModifier):
    """Deform a path along a second path"""

    def add_arguments(self, pars):
        pars.add_argument(
            "-n", "--noffset", type=float, default=0.0, help="normal offset"
        )
        pars.add_argument(
            "-t", "--toffset", type=float, default=0.0, help="tangential offset"
        )
        pars.add_argument("-k", "--kind", type=str, default="")
        pars.add_argument(
            "-c",
            "--copymode",
            default="Single",
            help="repeat the path to fit deformer's length",
        )
        pars.add_argument("-p", "--space", type=float, default=0.0)
        pars.add_argument(
            "-v",
            "--vertical",
            type=inkex.Boolean,
            default=False,
            help="reference path is vertical",
        )
        pars.add_argument(
            "-d",
            "--duplicate",
            type=inkex.Boolean,
            default=True,
            help="duplicate pattern before deformation",
        )
        pars.add_argument("--tab", help="The selected UI-tab when OK was pressed")

    def apply_diffeomorphism(self, bpt, skelcomp, lengths, isclosed, vects=()):
        """
        The kernel of this stuff:
        bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
        """
        s = bpt[0] - skelcomp[0][0]
        i, t = self.lengthtotime(s, lengths, isclosed)
        if i == len(skelcomp) - 1:
            x, y = tpoint(skelcomp[i - 1], skelcomp[i], 1 + t)
            dx = (skelcomp[i][0] - skelcomp[i - 1][0]) / lengths[-1]
            dy = (skelcomp[i][1] - skelcomp[i - 1][1]) / lengths[-1]
        else:
            x, y = tpoint(skelcomp[i], skelcomp[i + 1], t)
            dx = (skelcomp[i + 1][0] - skelcomp[i][0]) / lengths[i]
            dy = (skelcomp[i + 1][1] - skelcomp[i][1]) / lengths[i]

        vx = 0
        vy = bpt[1] - skelcomp[0][1]
        if self.options.wave:
            bpt[0] = x + vx * dx
            bpt[1] = y + vy + vx * dy
        else:
            bpt[0] = x + vx * dx - vy * dy
            bpt[1] = y + vx * dy + vy * dx

        for v in vects:
            vx = v[0] - skelcomp[0][0] - s
            vy = v[1] - skelcomp[0][1]
            if self.options.wave:
                v[0] = x + vx * dx
                v[1] = y + vy + vx * dy
            else:
                v[0] = x + vx * dx - vy * dy
                v[1] = y + vx * dy + vy * dx

    def effect(self):
        if len(self.options.ids) < 2:
            raise inkex.AbortExtension(_("This extension requires two selected paths."))

        self.options.wave = self.options.kind == "Ribbon"
        if self.options.copymode == "Single":
            self.options.repeat = False
            self.options.stretch = False
        elif self.options.copymode == "Repeated":
            self.options.repeat = True
            self.options.stretch = False
        elif self.options.copymode == "Single, stretched":
            self.options.repeat = False
            self.options.stretch = True
        elif self.options.copymode == "Repeated, stretched":
            self.options.repeat = True
            self.options.stretch = True

        patterns, skels = self.get_patterns_and_skeletons(True, self.options.duplicate)
        bboxes = [pattern.bounding_box() for pattern in patterns.values()]
        if None in bboxes:  # for texts, we can't compute the bounding box
            raise inkex.AbortExtension(_("Please convert texts to path first"))
        bbox = sum(bboxes, None)

        if self.options.vertical:
            # flipxy(bbox)...
            bbox = inkex.BoundingBox(-bbox.y, -bbox.x)

        width = bbox.width
        delta_x = width + self.options.space
        if delta_x < 0.01:
            raise inkex.AbortExtension(
                _(
                    "The total length of the pattern is too small\n"
                    "Please choose a larger object or set 'Space between copies' > 0"
                )
            )
        for pattern in patterns.values():
            if isinstance(pattern, inkex.PathElement):
                pattern.apply_transform()
                pattern.path = self._do_transform(
                    skels, pattern.path.to_superpath(), delta_x, bbox
                )

    def _do_transform(self, skeletons, p0, dx, bbox):
        if self.options.vertical:
            self.flipxy(p0)
        newp = []
        for skelnode in skeletons.values():
            skelnode.apply_transform()
            cur_skeleton = skelnode.path.to_superpath()
            if self.options.vertical:
                self.flipxy(cur_skeleton)
            for comp in cur_skeleton:
                path = copy.deepcopy(p0)
                skelcomp, lengths = self.linearize(comp)

                skel_closed = all(
                    [math.isclose(i, j) for i, j in zip(skelcomp[0], skelcomp[-1])]
                )

                length = sum(lengths)
                xoffset = skelcomp[0][0] - bbox.x.minimum + self.options.toffset
                yoffset = skelcomp[0][1] - bbox.y.center - self.options.noffset
                if self.options.repeat:
                    nb_copies = max(1, int(round((length + self.options.space) / dx)))
                    width = dx * nb_copies
                    if not skel_closed:
                        width -= self.options.space
                    bbox.x.maximum = bbox.x.minimum + width
                    new = []
                    for sub in path:
                        for _ in range(nb_copies):
                            new.append(copy.deepcopy(sub))
                            self.offset(sub, dx, 0)
                    path = new

                for sub in path:
                    self.offset(sub, xoffset, yoffset)

                if self.options.stretch:
                    if not bbox.width:
                        raise inkex.AbortExtension(
                            _(
                                "The 'stretch' option requires that the pattern must "
                                "have non-zero width :\nPlease edit the pattern width."
                            )
                        )
                    for sub in path:
                        self.stretch(sub, length / bbox.width, 1, skelcomp[0])

                for sub in path:
                    for ctlpt in sub:
                        self.apply_diffeomorphism(
                            ctlpt[1],
                            skelcomp,
                            lengths,
                            skel_closed,
                            (ctlpt[0], ctlpt[2]),
                        )

                if self.options.vertical:
                    self.flipxy(path)
                newp += path
        return CubicSuperPath(newp)


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