#!/usr/pkg/bin/python3.13
#
# Copyright (C) 2016 su_v, <suv-sf@users.sf.net>
#
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
"""
Convert mesh gradient to path
"""

import inkex
from inkex.elements import MeshGradient

# globals
EPSILON = 1e-3
MG_PROPS = ["fill", "stroke"]


def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
    """Test approximate equality.

    ref:
        PEP 485 -- A Function for testing approximate equality
        https://www.python.org/dev/peps/pep-0485/#proposed-implementation
    """
    # pylint: disable=invalid-name
    return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)


def reverse_path(csp):
    """Reverse path in CSP notation."""
    rcsp = []
    for subpath in reversed(csp):
        rsub = [list(reversed(cp)) for cp in reversed(subpath)]
        rcsp.append(rsub)
    return rcsp


def join_path(csp1, sp1, csp2, sp2):
    """Join sub-paths *sp1* and *sp2*."""
    pt1 = csp1[sp1][-1][1]
    pt2 = csp2[sp2][0][1]
    if isclose(pt1[0], pt2[0], EPSILON) and isclose(pt1[1], pt2[1], EPSILON):
        csp1[sp1][-1][2] = csp2[sp2][0][2]
        csp1[sp1].extend(csp2[sp2][1:])
    else:
        # inkex.debug('not close')
        csp1.append(csp2[sp2])
    return csp1


def is_url(val):
    """Check whether attribute value is linked resource."""
    return val.startswith("url(#")


def mesh_corners(meshgradient):
    """Return list of mesh patch corners, patch paths."""
    rows = len(meshgradient)
    cols = len(meshgradient[0])
    # first corner of mesh gradient
    corner_x = float(meshgradient.get("x", "0.0"))
    corner_y = float(meshgradient.get("y", "0.0"))
    # init corner and meshpatch lists
    corners = [[None for _ in range(cols + 1)] for _ in range(rows + 1)]
    corners[0][0] = [corner_x, corner_y]
    meshpatch_csps = []
    for meshrow in range(rows):
        for meshpatch in range(cols):
            # get start point for current meshpatch edges
            if meshrow == 0:
                first_corner = corners[meshrow][meshpatch]
            if meshrow > 0:
                first_corner = corners[meshrow][meshpatch + 1]
            # parse path of meshpatch edges
            path = "M {},{}".format(*first_corner)
            for edge in meshgradient[meshrow][meshpatch]:
                path = " ".join([path, edge.get("path")])
            csp = inkex.Path(path).to_superpath()
            # update corner list with current meshpatch
            if meshrow == 0:
                corners[meshrow][meshpatch + 1] = csp[0][1][1]
                corners[meshrow + 1][meshpatch + 1] = csp[0][2][1]
                if meshpatch == 0:
                    corners[meshrow + 1][meshpatch] = csp[0][3][1]
            if meshrow > 0:
                corners[meshrow][meshpatch + 1] = csp[0][0][1]
                corners[meshrow + 1][meshpatch + 1] = csp[0][1][1]
                if meshpatch == 0:
                    corners[meshrow + 1][meshpatch] = csp[0][2][1]
            # append to list of meshpatch csp
            meshpatch_csps.append(csp)
    return corners, meshpatch_csps


def mesh_hvlines(meshgradient):
    """Return lists of vertical and horizontal patch edges."""
    rows = len(meshgradient)
    cols = len(meshgradient[0])
    # init lists for horizontal, vertical lines
    hlines = [[None for _ in range(cols)] for _ in range(rows + 1)]
    vlines = [[None for _ in range(rows)] for _ in range(cols + 1)]
    for meshrow in range(rows):
        for meshpatch in range(cols):
            # horizontal edges
            if meshrow == 0:
                edge = meshgradient[meshrow][meshpatch][0]
                hlines[meshrow][meshpatch] = edge.get("path")
                edge = meshgradient[meshrow][meshpatch][2]
                hlines[meshrow + 1][meshpatch] = edge.get("path")
            if meshrow > 0:
                edge = meshgradient[meshrow][meshpatch][1]
                hlines[meshrow + 1][meshpatch] = edge.get("path")
            # vertical edges
            if meshrow == 0:
                edge = meshgradient[meshrow][meshpatch][1]
                vlines[meshpatch + 1][meshrow] = edge.get("path")
                if meshpatch == 0:
                    edge = meshgradient[meshrow][meshpatch][3]
                    vlines[meshpatch][meshrow] = edge.get("path")
            if meshrow > 0:
                edge = meshgradient[meshrow][meshpatch][0]
                vlines[meshpatch + 1][meshrow] = edge.get("path")
                if meshpatch == 0:
                    edge = meshgradient[meshrow][meshpatch][2]
                    vlines[meshpatch][meshrow] = edge.get("path")
    return hlines, vlines


def mesh_to_outline(corners, hlines, vlines):
    """Construct mesh outline as CSP path."""
    outline_csps = []
    path = "M {},{}".format(*corners[0][0])
    for edge_path in hlines[0]:
        path = " ".join([path, edge_path])
    for edge_path in vlines[-1]:
        path = " ".join([path, edge_path])
    for edge_path in reversed(hlines[-1]):
        path = " ".join([path, edge_path])
    for edge_path in reversed(vlines[0]):
        path = " ".join([path, edge_path])
    outline_csps.append(inkex.Path(path).to_superpath())
    return outline_csps


def mesh_to_grid(corners, hlines, vlines):
    """Construct mesh grid with CSP paths."""
    rows = len(corners) - 1
    cols = len(corners[0]) - 1
    gridline_csps = []
    # horizontal
    path = "M {},{}".format(*corners[0][0])
    for edge_path in hlines[0]:
        path = " ".join([path, edge_path])
    gridline_csps.append(inkex.Path(path).to_superpath())
    for i in range(1, rows + 1):
        path = "M {},{}".format(*corners[i][-1])
        for edge_path in reversed(hlines[i]):
            path = " ".join([path, edge_path])
        gridline_csps.append(inkex.Path(path).to_superpath())
    # vertical
    path = "M {},{}".format(*corners[-1][0])
    for edge_path in reversed(vlines[0]):
        path = " ".join([path, edge_path])
    gridline_csps.append(inkex.Path(path).to_superpath())
    for j in range(1, cols + 1):
        path = "M {},{}".format(*corners[0][j])
        for edge_path in vlines[j]:
            path = " ".join([path, edge_path])
        gridline_csps.append(inkex.Path(path).to_superpath())
    return gridline_csps


def mesh_to_faces(corners, hlines, vlines):
    """Construct mesh faces with CSP paths."""
    rows = len(corners) - 1
    cols = len(corners[0]) - 1
    face_csps = []
    for row in range(rows):
        for col in range(cols):
            # init new face
            face = []
            # init edge paths
            edge_t = hlines[row][col]
            edge_b = hlines[row + 1][col]
            edge_l = vlines[col][row]
            edge_r = vlines[col + 1][row]
            # top edge, first
            if row == 0:
                path = "M {},{}".format(*corners[row][col])
                path = " ".join([path, edge_t])
                face.append(inkex.Path(path).to_superpath()[0])
            else:
                path = "M {},{}".format(*corners[row][col + 1])
                path = " ".join([path, edge_t])
                face.append(reverse_path(inkex.Path(path).to_superpath())[0])
            # right edge
            path = "M {},{}".format(*corners[row][col + 1])
            path = " ".join([path, edge_r])
            join_path(face, -1, inkex.Path(path).to_superpath(), 0)
            # bottom edge
            path = "M {},{}".format(*corners[row + 1][col + 1])
            path = " ".join([path, edge_b])
            join_path(face, -1, inkex.Path(path).to_superpath(), 0)
            # left edge
            if col == 0:
                path = "M {},{}".format(*corners[row + 1][col])
                path = " ".join([path, edge_l])
                join_path(face, -1, inkex.Path(path).to_superpath(), 0)
            else:
                path = "M {},{}".format(*corners[row][col])
                path = " ".join([path, edge_l])
                join_path(face, -1, reverse_path(inkex.Path(path).to_superpath()), 0)
            # append face to output list
            face_csps.append(face)
    return face_csps


class MeshToPath(inkex.EffectExtension):
    """Effect extension to convert mesh geometry to path data."""

    def add_arguments(self, pars):
        pars.add_argument("--tab", help="The selected UI-tab")
        pars.add_argument("--mode", default="outline", help="Edge mode")

    def process_props(self, mdict, res_type="meshgradient"):
        """Process style properties of style dict *mdict*."""
        result = []
        for key, val in mdict.items():
            if key in MG_PROPS:
                if is_url(val):
                    paint_server = self.svg.getElementById(val)
                    if res_type == "meshgradient" and isinstance(
                        paint_server, MeshGradient
                    ):
                        result.append(paint_server)
        return result

    def process_style(self, node, res_type="meshgradient"):
        """Process style of *node*."""
        result = node.specified_style()
        result = self.process_props(result, res_type)
        # TODO: check for child paint servers
        return result

    def find_meshgradients(self, node):
        """Parse node style, return list with linked meshgradients."""
        return self.process_style(node, res_type="meshgradient")

    # ----- Process meshgradient definitions

    def mesh_to_csp(self, meshgradient):
        """Parse mesh geometry and build csp-based path data."""

        # init variables
        transform = None
        mode = self.options.mode

        # gradient units
        mesh_units = meshgradient.get("gradientUnits", "objectBoundingBox")
        if mesh_units == "objectBoundingBox":
            # TODO: position and scale based on "objectBoundingBox" units
            return

        # Inkscape SVG 0.92 and SVG 2.0 draft mesh transformations
        transform = meshgradient.gradientTransform @ meshgradient.transform

        # parse meshpatches, calculate absolute corner coords
        corners, meshpatch_csps = mesh_corners(meshgradient)

        if mode == "meshpatches":
            return meshpatch_csps, transform
        else:
            hlines, vlines = mesh_hvlines(meshgradient)
            if mode == "outline":
                return mesh_to_outline(corners, hlines, vlines), transform
            elif mode == "gridlines":
                return mesh_to_grid(corners, hlines, vlines), transform
            elif mode == "faces":
                return mesh_to_faces(corners, hlines, vlines), transform

    # ----- Convert meshgradient definitions

    def csp_to_path(self, node, csp_list, transform=None):
        """Create new paths based on csp data, return group with paths."""
        # set up stroke width, group
        stroke_width = self.svg.viewport_to_unit("1px")
        stroke_color = "#000000"
        style = {
            "fill": "none",
            "stroke": stroke_color,
            "stroke-width": str(stroke_width),
        }

        group = inkex.Group()
        # apply gradientTransform and node's preserved transform to group
        group.transform = transform @ node.transform

        # convert each csp to path, append to group
        for csp in csp_list:
            elem = group.add(inkex.PathElement())
            elem.style = style
            elem.path = inkex.CubicSuperPath(csp)
            if self.options.mode == "outline":
                elem.path.close()
            elif self.options.mode == "faces":
                if len(csp) == 1 and len(csp[0]) == 5:
                    elem.path.close()
        return group

    def effect(self):
        """Main routine to convert mesh geometry to path data."""
        # loop through selection
        for node in self.svg.selection.values():
            meshgradients = self.find_meshgradients(node)
            # if style references meshgradient
            if meshgradients:
                for meshgradient in meshgradients:
                    csp_list = None
                    result = None
                    # parse mesh geometry
                    if meshgradient is not None:
                        csp_list, mat = self.mesh_to_csp(meshgradient)
                    # generate new paths with path data based on mesh geometry
                    if csp_list is not None:
                        result = self.csp_to_path(node, csp_list, mat)
                    # add result (group) to document
                    if result is not None:
                        index = node.getparent().index(node)
                        node.getparent().insert(index + 1, result)


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