# SPDX-License-Identifier: LGPL-2.1-or-later

# -*- coding: utf8 -*-
# ***************************************************************************
# *   Copyright (c) 2009 Yorik van Havre <yorik@uncreated.net>              *
# *   Copyright (c) 2018 George Shuklin (amarao)                            *
# *   Copyright (c) 2020 Eliud Cabrera Castillo <e.cabrera-castillo@tum.de> *
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU Lesser General Public License (LGPL)    *
# *   as published by the Free Software Foundation; either version 2 of     *
# *   the License, or (at your option) any later version.                   *
# *   for detail see the LICENCE text file.                                 *
# *                                                                         *
# *   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 Library General Public License for more details.                  *
# *                                                                         *
# *   You should have received a copy of the GNU Library 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                                                                   *
# *                                                                         *
# ***************************************************************************
"""Provides functions to return the SVG representation of some shapes."""
## @package svgshapes
# \ingroup draftfunctions
# \brief Provides functions to return the SVG representation of some shapes.

import math
import lazy_loader.lazy_loader as lz

import FreeCAD as App
import DraftVecUtils
import WorkingPlane
from draftutils import params
from draftutils import utils
from draftutils.messages import _msg, _wrn

# Delay import of module until first use because it is heavy
Part = lz.LazyLoader("Part", globals(), "Part")
DraftGeomUtils = lz.LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils")
TechDraw = lz.LazyLoader("TechDraw", globals(), "TechDraw")

## \addtogroup draftfunctions
# @{


def get_proj(vec, plane=None):
    """Get a projection of the vector in the plane's u and v directions.

    TODO: check if the same function for SVG and DXF projection can be used
    so that this function is not just duplicated code.
    This function may also be present elsewhere, like `WorkingPlane`
    or `DraftGeomUtils`, so we should avoid code duplication.

    Parameters
    ----------
    vec: Base::Vector3
        An arbitrary vector that will be projected on the U and V directions.

    plane: WorkingPlane.PlaneBase
        Working plane.
    """
    if not plane:
        return vec

    nx = DraftVecUtils.project(vec, plane.u)
    lx = nx.Length

    if abs(nx.getAngle(plane.u)) > 0.1:
        lx = -lx

    ny = DraftVecUtils.project(vec, plane.v)
    ly = ny.Length

    if abs(ny.getAngle(plane.v)) > 0.1:
        ly = -ly

    # if techdraw: buggy - we now simply do it at the end
    #    ly = -ly
    return App.Vector(lx, ly, 0)


def getProj(vec, plane=None):
    """Get a projection of a vector. DEPRECATED."""
    utils.use_instead("get_proj")
    return get_proj(vec, plane)


def get_discretized(edge, plane):
    """Get a discretized edge on a plane."""
    pieces = params.get_param("svgDiscretization")

    if pieces == 0:
        pieces = 10

    d = int(edge.Length / pieces)
    if d == 0:
        d = 1

    edata = ""
    for i in range(d + 1):
        _length = edge.LastParameter - edge.FirstParameter
        _point = edge.FirstParameter + float(i) / d * _length
        _vec = edge.valueAt(_point)
        v = get_proj(_vec, plane)

        if not edata:
            edata += "M " + str(v.x) + " " + str(v.y) + " "
        else:
            edata += "L " + str(v.x) + " " + str(v.y) + " "

    return edata


def getDiscretized(edge, plane):
    """Get a discretized edge on a plane. DEPRECATED."""
    utils.use_instead("get_discretized")
    return get_discretized(edge, plane)


def _get_path_circ_ellipse(
    plane, edge, verts, edata, iscircle, isellipse, fill, stroke, linewidth, lstyle
):
    """Get the edge data from a path that is a circle or ellipse."""
    if plane:
        drawing_plane_normal = plane.axis
    else:
        drawing_plane_normal = WorkingPlane.get_working_plane(update=False).axis

    center = edge.Curve
    ax = center.Axis

    # The angle between the curve axis and the plane is not 0 nor 180 degrees
    _angle = math.degrees(ax.getAngle(drawing_plane_normal))
    if round(_angle, 2) not in (0, 180):
        edata += get_discretized(edge, plane)
        return "edata", edata

    # The angle is 0 or 180, coplanar
    occversion = Part.OCC_VERSION.split(".")
    done = False
    if int(occversion[0]) >= 7 and int(occversion[1]) >= 1:
        # if using occ >= 7.1, use HLR algorithm
        snip = TechDraw.projectToSVG(edge, drawing_plane_normal)

        if snip:
            try:
                _a = snip.split('path d="')[1]
                _a = _a.split('"')[0]
                _a = _a.split("A")[1]
                A = "A " + _a
            except IndexError:
                pass
                # TODO: trap only specific exception.
                # Check the problem. Split didn't produce a two element list?
                # _wrn("Circle or ellipse: "
                #      "cannot split the projection snip "
                #      "obtained by 'projectToSVG', "
                #      "continue manually.")
            else:
                edata += A
                done = True

    if not done:
        if len(edge.Vertexes) == 1 and iscircle:
            # Complete circle not only arc
            svg = get_circle(plane, fill, stroke, linewidth, lstyle, edge)
            # If it's a circle we will return the final SVG string,
            # otherwise it will process the `edata` further
            return "svg", svg
        elif len(edge.Vertexes) == 1 and isellipse:
            # Complete ellipse not only arc
            # svg = get_ellipse(plane,
            #                   fill, stroke, linewidth,
            #                   lstyle, edge)
            # return svg

            # Difference in angles
            _diff = (center.LastParameter - center.FirstParameter) / 2.0
            endpoints = [get_proj(center.value(_diff), plane), get_proj(verts[-1].Point, plane)]
        else:
            endpoints = [get_proj(verts[-1].Point, plane)]

        # Arc with more than one vertex
        if iscircle:
            rx = ry = center.Radius
            rot = 0
        else:  # ellipse
            rx = center.MajorRadius
            ry = center.MinorRadius
            _rot = center.AngleXU * center.Axis * App.Vector(0, 0, 1)
            rot = math.degrees(_rot)
            if rot > 90:
                rot -= 180
            if rot < -90:
                rot += 180

        # Be careful with the sweep flag
        _diff = edge.ParameterRange[1] - edge.ParameterRange[0]
        _diff = _diff / math.pi
        flag_large_arc = (_diff % 2) > 1

        # flag_sweep = (center.Axis * drawing_plane_normal >= 0) \
        #     == (edge.LastParameter > edge.FirstParameter)
        #     == (edge.Orientation == "Forward")

        # Another method: check the direction of the angle
        # between tangents
        _diff = edge.LastParameter - edge.FirstParameter
        t1 = edge.tangentAt(edge.FirstParameter)
        t2 = edge.tangentAt(edge.FirstParameter + _diff / 10)
        flag_sweep = DraftVecUtils.angle(t1, t2, drawing_plane_normal) < 0

        for v in endpoints:
            edata += (
                "A {} {} {} "
                "{} {} "
                "{} {} ".format(rx, ry, rot, int(flag_large_arc), int(flag_sweep), v.x, v.y)
            )

    return "edata", edata


def _get_path_bspline(plane, edge, edata):
    """Convert the edge to a BSpline and discretize it."""
    bspline = edge.Curve.toBSpline(edge.FirstParameter, edge.LastParameter)
    if bspline.Degree > 3 or bspline.isRational():
        try:
            bspline = bspline.approximateBSpline(0.05, 50, 3, "C0")
        except RuntimeError:
            _wrn("Debug: unable to approximate bspline from edge")

    if bspline.Degree <= 3 and not bspline.isRational():
        for bezierseg in bspline.toBezier():
            if bezierseg.Degree > 3:  # should not happen
                _wrn("Bezier segment of degree > 3")
                raise AssertionError
            elif bezierseg.Degree == 1:
                edata += "L "
            elif bezierseg.Degree == 2:
                edata += "Q "
            elif bezierseg.Degree == 3:
                edata += "C "

            for pole in bezierseg.getPoles()[1:]:
                v = get_proj(pole, plane)
                edata += "{} {} ".format(v.x, v.y)
    else:
        _msg(
            "Debug: one edge (hash {}) "
            "has been discretized "
            "with parameter 0.1".format(edge.hashCode())
        )

        for linepoint in bspline.discretize(0.1)[1:]:
            v = get_proj(linepoint, plane)
            edata += "L {} {} ".format(v.x, v.y)

    return edata


def get_circle(plane, fill, stroke, linewidth, lstyle, edge):
    """Get the SVG representation from a circular edge."""
    cen = get_proj(edge.Curve.Center, plane)
    rad = edge.Curve.Radius

    if plane:
        drawing_plane_normal = plane.axis
    else:
        drawing_plane_normal = WorkingPlane.get_working_plane(update=False).axis

    if round(edge.Curve.Axis.getAngle(drawing_plane_normal), 2) in [0, 3.14]:
        # Perpendicular projection: circle
        svg = "<circle "
        svg += 'cx="{}" cy="{}" r="{}" '.format(cen.x, cen.y, rad)
    else:
        # Any other projection: ellipse
        svg = '<path d="{}" '.format(get_discretized(edge, plane))

    svg += 'stroke="{}" '.format(stroke)
    # Editor: why is stroke-width repeated? Is this really necessary
    # for the generated SVG?
    svg += 'stroke-width="{} px" '.format(linewidth)
    svg += 'style="'
    svg += "stroke-width:{};".format(linewidth)
    svg += "stroke-miterlimit:4;"
    svg += "stroke-dasharray:{};".format(lstyle)
    svg += "stroke-linecap:square;"
    svg += "fill:{}".format(fill) + '"'
    svg += "/>\n"
    return svg


def getCircle(plane, fill, stroke, linewidth, lstyle, edge):
    """Get the SVG representation from a circular edge."""
    utils.use_instead("get_circle")
    return get_circle(plane, fill, stroke, linewidth, lstyle, edge)


def get_ellipse(plane, fill, stroke, linewidth, lstyle, edge):
    """Get the SVG representation from an elliptical edge."""
    cen = get_proj(edge.Curve.Center, plane)
    mir = edge.Curve.MinorRadius
    mar = edge.Curve.MajorRadius
    svg = "<ellipse "
    svg += 'cx="{}" cy="{}" '.format(cen.x, cen.y)
    svg += 'rx="{}" ry="{}" '.format(mar, mir)
    svg += 'stroke="{}" '.format(stroke)
    svg += 'stroke-width="{} px" '.format(linewidth)
    svg += 'style="'
    svg += "stroke-width:{};".format(linewidth)
    svg += "stroke-miterlimit:4;"
    svg += "stroke-dasharray:{};".format(lstyle)
    svg += "stroke-linecap:square;"
    svg += "fill:{}".format(fill) + '"'
    svg += "/>\n"
    return svg


def getEllipse(plane, fill, stroke, linewidth, lstyle, edge):
    """Get the SVG representation from an elliptical edge. DEPRECATED."""
    utils.use_instead("get_ellipse")
    return get_ellipse(plane, fill, stroke, linewidth, lstyle, edge)


def get_path(
    obj,
    plane,
    fill,
    pathdata,
    stroke,
    linewidth,
    lstyle,
    fill_opacity=None,
    edges=[],
    wires=[],
    pathname=None,
):
    """Get the SVG representation from an object's edges or wires.

    TODO: the `edges` and `wires` must not default to empty list `[]`
    but to `None`. Verify that the code doesn't break with this change.

    `edges` and `wires` are mutually exclusive. If no `wires` are provided,
    sort the `edges`, and use them. If `wires` are provided, sort the edges
    in these `wires`, and use them.
    """
    svg = "<path "

    if pathname is None:
        svg += 'id="{}" '.format(obj.Name)
    elif pathname != "":
        svg += 'id="{}" '.format(pathname)

    svg += ' d="'

    if not wires:
        egroups = Part.sortEdges(edges)
    else:
        egroups = []
        first = True
        for w in wires:
            wire = w.copy()
            if first:
                first = False
            else:
                # invert further wires to create holes
                wire = DraftGeomUtils.invert(wire)

            wire.fixWire()
            egroups.append(Part.__sortEdges__(wire.Edges))

    for _edges in egroups:
        edata = ""

        for edgeindex, edge in enumerate(_edges):
            if edgeindex == 0:
                verts = edge.Vertexes
                if len(_edges) > 1:
                    last_pt = verts[-1].Point
                    nextverts = _edges[1].Vertexes
                    if (last_pt - nextverts[0].Point).Length > 1e-6 and (
                        last_pt - nextverts[-1].Point
                    ).Length > 1e-6:
                        verts.reverse()
                v = get_proj(verts[0].Point, plane)
                edata += "M {} {} ".format(v.x, v.y)
            else:
                previousverts = verts
                verts = edge.Vertexes
                if (verts[0].Point - previousverts[-1].Point).Length > 1e-6:
                    verts.reverse()
                    if (verts[0].Point - previousverts[-1].Point).Length > 1e-6:
                        raise ValueError("edges not ordered")

            iscircle = DraftGeomUtils.geomType(edge) == "Circle"
            isellipse = DraftGeomUtils.geomType(edge) == "Ellipse"

            if iscircle or isellipse:
                _type, data = _get_path_circ_ellipse(
                    plane, edge, verts, edata, iscircle, isellipse, fill, stroke, linewidth, lstyle
                )
                if _type == "svg":
                    # final svg string already calculated, so just return it
                    return data

                # else the `edata` was properly augmented, so re-assing it
                edata = data
            elif DraftGeomUtils.geomType(edge) == "Line":
                v = get_proj(verts[-1].Point, plane)
                edata += "L {} {} ".format(v.x, v.y)
            else:
                # If it's not a circle nor ellipse nor straight line
                # convert the curve to BSpline
                edata = _get_path_bspline(plane, edge, edata)

        if fill != "none":
            edata += "Z "

        if edata in pathdata:
            # do not draw a path on another identical path
            return ""
        else:
            svg += edata
            pathdata.append(edata)

    svg += '" '
    svg += 'stroke="{}" '.format(stroke)
    svg += 'stroke-width="{} px" '.format(linewidth)
    svg += 'style="'
    svg += "stroke-width:{};".format(linewidth)
    svg += "stroke-miterlimit:4;"
    svg += "stroke-dasharray:{};".format(lstyle)
    svg += "stroke-linecap:square;"
    svg += "fill:{};".format(fill)
    # fill_opacity must be a number, but if it's `None` it is omitted
    if fill_opacity is not None:
        svg += "fill-opacity:{};".format(fill_opacity)

    svg += 'fill-rule: evenodd"'
    svg += "/>\n"
    return svg


def getPath(
    obj,
    plane,
    fill,
    pathdata,
    stroke,
    linewidth,
    lstyle,
    fill_opacity,
    edges=[],
    wires=[],
    pathname=None,
):
    """Get the SVG representation from a path. DEPRECATED."""
    utils.use_instead("get_path")
    return get_path(
        obj,
        plane,
        fill,
        pathdata,
        stroke,
        linewidth,
        lstyle,
        fill_opacity,
        edges=edges,
        wires=wires,
        pathname=pathname,
    )


## @}
