#!/usr/pkg/bin/python3.13
# coding=utf-8
#
# Copyright (C) 2005 Aaron Spike, aaron@ekips.org
#
# 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.
"""
Perspective approach & math by Dmitry Platonov, shadowjack@mail.ru, 2006
"""

import inkex
from inkex.localization import inkex_gettext as _

X, Y = range(2)

try:
    import numpy as np
    import numpy.linalg as lin

    FLOAT = np.float64
except ImportError:
    np = None


class Perspective(inkex.EffectExtension):
    """Apply a perspective to a path/group of paths"""

    def effect(self):
        if np is None:
            raise inkex.AbortExtension(
                _(
                    "Failed to import the numpy or numpy.linalg modules. "
                    "These modules are required by this extension. Please install them."
                    "  On a Debian-like system this can be done with the command, "
                    "sudo apt-get install python-numpy."
                )
            )
        if len(self.svg.selection) != 2:
            raise inkex.AbortExtension(
                _("This extension requires two selected objects.")
            )

        obj, envelope = self.svg.selection

        if isinstance(obj, (inkex.PathElement, inkex.Group)):
            if isinstance(envelope, inkex.PathElement):
                path = envelope.path.transform(
                    envelope.composed_transform()
                ).to_superpath()

                if len(path) < 1 or len(path[0]) < 4:
                    raise inkex.AbortExtension(
                        _(
                            "This extension requires that the second path be four "
                            "nodes long."
                        )
                    )

                dip = np.zeros((4, 2), dtype=FLOAT)
                for i in range(4):
                    dip[i][0] = path[0][i][1][0]
                    dip[i][1] = path[0][i][1][1]

                # Get bounding box plus any extra composed transform of parents.
                bbox = obj.bounding_box(obj.getparent().composed_transform())

                sip = np.array(
                    [
                        [bbox.left, bbox.bottom],
                        [bbox.left, bbox.top],
                        [bbox.right, bbox.top],
                        [bbox.right, bbox.bottom],
                    ],
                    dtype=FLOAT,
                )
            else:
                if isinstance(envelope, inkex.Group):
                    raise inkex.AbortExtension(
                        _(
                            "The second selected object is a group, not a"
                            " path.\nTry using Object->Ungroup."
                        )
                    )
                raise inkex.AbortExtension(
                    _(
                        "The second selected object is not a path.\nTry using"
                        " the procedure Path->Object to Path."
                    )
                )
        else:
            raise inkex.AbortExtension(
                _(
                    "The first selected object is neither a path nor a group.\nTry "
                    "using the procedure Path->Object to Path."
                )
            )

        solmatrix = np.zeros((8, 8), dtype=FLOAT)
        free_term = np.zeros(8, dtype=FLOAT)
        for i in (0, 1, 2, 3):
            solmatrix[i][0] = sip[i][0]
            solmatrix[i][1] = sip[i][1]
            solmatrix[i][2] = 1
            solmatrix[i][6] = -dip[i][0] * sip[i][0]
            solmatrix[i][7] = -dip[i][0] * sip[i][1]
            solmatrix[i + 4][3] = sip[i][0]
            solmatrix[i + 4][4] = sip[i][1]
            solmatrix[i + 4][5] = 1
            solmatrix[i + 4][6] = -dip[i][1] * sip[i][0]
            solmatrix[i + 4][7] = -dip[i][1] * sip[i][1]
            free_term[i] = dip[i][0]
            free_term[i + 4] = dip[i][1]

        res = lin.solve(solmatrix, free_term)
        projmatrix = np.array(
            [[res[0], res[1], res[2]], [res[3], res[4], res[5]], [res[6], res[7], 1.0]],
            dtype=FLOAT,
        )

        self.process_object(obj, projmatrix)

    def process_object(self, obj, matrix):
        if isinstance(obj, inkex.PathElement):
            self.process_path(obj, matrix)
        elif isinstance(obj, inkex.Group):
            self.process_group(obj, matrix)

    def process_group(self, group, matrix):
        """Go through all groups to process all paths inside them"""
        for node in group:
            self.process_object(node, matrix)

    def process_path(self, element, matrix):
        """Apply the transformation to the selected path"""
        point = (
            element.path.to_absolute()
            .transform(element.composed_transform())
            .to_superpath()
        )
        for subs in point:
            for csp in subs:
                csp[0] = self.project_point(csp[0], matrix)
                csp[1] = self.project_point(csp[1], matrix)
                csp[2] = self.project_point(csp[2], matrix)
        element.path = inkex.Path(point).transform(-element.composed_transform())

    @staticmethod
    def project_point(point, matrix):
        """Apply the matrix to the given point"""
        return [
            (point[X] * matrix[0][0] + point[Y] * matrix[0][1] + matrix[0][2])
            / (point[X] * matrix[2][0] + point[Y] * matrix[2][1] + matrix[2][2]),
            (point[X] * matrix[1][0] + point[Y] * matrix[1][1] + matrix[1][2])
            / (point[X] * matrix[2][0] + point[Y] * matrix[2][1] + matrix[2][2]),
        ]


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