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

# ***************************************************************************
# *   Copyright (c) 2009, 2010 Yorik van Havre <yorik@uncreated.net>        *
# *   Copyright (c) 2009, 2010 Ken Cline <cline@frii.com>                   *
# *                                                                         *
# *   This file is part of the FreeCAD CAx development system.              *
# *                                                                         *
# *   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.                                 *
# *                                                                         *
# *   FreeCAD 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 FreeCAD; if not, write to the Free Software        *
# *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
# *   USA                                                                   *
# *                                                                         *
# ***************************************************************************
"""Provides various functions to work with wires."""
## @package wires
# \ingroup draftgeoutils
# \brief Provides various functions to work with wires.

import math
import lazy_loader.lazy_loader as lz

import FreeCAD as App
import DraftVecUtils
import WorkingPlane

from draftgeoutils.general import geomType, vec, precision
from draftgeoutils.geometry import get_normal
from draftgeoutils.geometry import project_point_on_plane
from draftgeoutils.edges import findMidpoint, isLine

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

## \addtogroup draftgeoutils
# @{


def findWires(edgeslist):
    """Find wires in a list of edges."""
    return [Part.Wire(e) for e in Part.sortEdges(edgeslist)]


def findWiresOld2(edgeslist):
    """Find connected wires in the given list of edges."""

    def touches(e1, e2):
        """Return True if two edges connect at the edges."""
        if len(e1.Vertexes) < 2:
            return False
        if len(e2.Vertexes) < 2:
            return False
        if DraftVecUtils.equals(e1.Vertexes[0].Point, e2.Vertexes[0].Point):
            return True
        if DraftVecUtils.equals(e1.Vertexes[0].Point, e2.Vertexes[-1].Point):
            return True
        if DraftVecUtils.equals(e1.Vertexes[-1].Point, e2.Vertexes[0].Point):
            return True
        if DraftVecUtils.equals(e1.Vertexes[-1].Point, e2.Vertexes[-1].Point):
            return True
        return False

    edges = edgeslist[:]
    wires = []
    lost = []
    while edges:
        e = edges[0]
        if not wires:
            # create first group
            edges.remove(e)
            wires.append([e])
        else:
            found = False
            for w in wires:
                if not found:
                    for we in w:
                        if touches(e, we):
                            edges.remove(e)
                            w.append(e)
                            found = True
                            break
            if not found:
                if e in lost:
                    # we already tried this edge, and still nothing
                    edges.remove(e)
                    wires.append([e])
                    lost = []
                else:
                    # put to the end of the list
                    edges.remove(e)
                    edges.append(e)
                    lost.append(e)
    nwires = []
    for w in wires:
        try:
            wi = Part.Wire(w)
        except Part.OCCError:
            print("couldn't join some edges")
        else:
            nwires.append(wi)
    return nwires


def findWiresOld(edges):
    """Return a list of lists containing edges that can be connected.

    Find connected edges in the list.
    """
    raise DeprecationWarning(
        "This function shouldn't be called anymore. " "Use findWires() instead"
    )

    def verts(shape):
        return [shape.Vertexes[0].Point, shape.Vertexes[-1].Point]

    def group(shapes):
        shapesIn = shapes[:]
        shapesOut = [shapesIn.pop()]
        changed = False
        for s in shapesIn:
            if len(s.Vertexes) < 2:
                continue
            else:
                clean = True
                for v in verts(s):
                    for i in range(len(shapesOut)):
                        if clean and (v in verts(shapesOut[i])):
                            shapesOut[i] = Part.Wire(shapesOut[i].Edges + s.Edges)
                            changed = True
                            clean = False
                if clean:
                    shapesOut.append(s)
        return changed, shapesOut

    working = True
    edgeSet = edges

    while working:
        result = group(edgeSet)
        working = result[0]
        edgeSet = result[1]

    return result[1]


def flattenWire(wire, origin=None, normal=None):
    """Force a wire to be flat on a plane defined by an origin and a normal.

    If origin or normal are None they are derived from the wire.
    """
    if normal is None:
        normal = get_normal(wire)
        # for backward compatibility with previous getNormal implementation
        if normal is None:
            normal = App.Vector(0, 0, 1)
    if origin is None:
        origin = wire.Vertexes[0].Point

    points = [project_point_on_plane(vert.Point, origin, normal) for vert in wire.Vertexes]
    if wire.isClosed():
        points.append(points[0])
    new_wire = Part.makePolygon(points)

    return new_wire


def superWire(edgeslist, closed=False):
    """Force a wire between edges that don't have coincident endpoints.

    Forces a wire between edges that don't necessarily
    have coincident endpoints. If closed=True, the wire will always be closed.
    """

    def median(v1, v2):
        vd = v2.sub(v1)
        vd.scale(0.5, 0.5, 0.5)
        return v1.add(vd)

    edges = Part.__sortEdges__(edgeslist)
    print(edges)
    newedges = []

    for i in range(len(edges)):
        curr = edges[i]
        if i == 0:
            if closed:
                prev = edges[-1]
            else:
                prev = None
        else:
            prev = edges[i - 1]

        if i == (len(edges) - 1):
            if closed:
                _next = edges[0]
            else:
                _next = None
        else:
            _next = edges[i + 1]

        print(i, prev, curr, _next)

        if prev:
            if curr.Vertexes[0].Point == prev.Vertexes[-1].Point:
                p1 = curr.Vertexes[0].Point
            else:
                p1 = median(curr.Vertexes[0].Point, prev.Vertexes[-1].Point)
        else:
            p1 = curr.Vertexes[0].Point

        if _next:
            if curr.Vertexes[-1].Point == _next.Vertexes[0].Point:
                p2 = _next.Vertexes[0].Point
            else:
                p2 = median(curr.Vertexes[-1].Point, _next.Vertexes[0].Point)
        else:
            p2 = curr.Vertexes[-1].Point

        if geomType(curr) == "Line":
            print("line", p1, p2)
            newedges.append(Part.LineSegment(p1, p2).toShape())
        elif geomType(curr) == "Circle":
            p3 = findMidpoint(curr)
            print("arc", p1, p3, p2)
            newedges.append(Part.Arc(p1, p3, p2).toShape())
        else:
            print("Cannot superWire edges that are not lines or arcs")
            return None

    print(newedges)
    return Part.Wire(newedges)


def isReallyClosed(wire):
    if isinstance(wire, (Part.Wire, Part.Edge)):
        return wire.isClosed()
    return isinstance(wire, Part.Face)


def curvetowire(obj, steps):
    """Discretize the object and return a list of edges."""
    points = obj.copy().discretize(steps)
    p0 = points[0]
    edgelist = []
    for p in points[1:]:
        edge = Part.makeLine((p0.x, p0.y, p0.z), (p.x, p.y, p.z))
        edgelist.append(edge)
        p0 = p
    return edgelist


def curvetosegment(curve, seglen):
    """Discretize the curve and return a list of edges."""
    points = curve.discretize(seglen)
    p0 = points[0]
    edgelist = []
    for p in points[1:]:
        edge = Part.makeLine((p0.x, p0.y, p0.z), (p.x, p.y, p.z))
        edgelist.append(edge)
        p0 = p
    return edgelist


def rebaseWire(wire, vidx=0):
    """Return a copy of the wire with the first vertex indicated by the index.

    Return a new wire which is a copy of the current wire,
    but where the first vertex is the vertex indicated by the given
    index vidx, starting from 1.
    0 will return an exact copy of the wire.
    """
    if vidx < 1:
        return wire

    if vidx > len(wire.Vertexes):
        # print("Vertex index above maximum")
        return wire

    # This can be done in one step
    return Part.Wire(wire.Edges[vidx - 1 :] + wire.Edges[: vidx - 1])


def removeInterVertices(wire):
    """Remove middle vertices from a straight wire and return a new wire.

    Remove unneeded vertices, those that are in the middle of a straight line,
    from a wire, return a new wire.
    """
    _pre = precision()
    edges = Part.__sortEdges__(wire.Edges)
    nverts = []

    def getvec(v1, v2):
        if not abs(round(v1.getAngle(v2), _pre) in [0, round(math.pi, _pre)]):
            nverts.append(edges[i].Vertexes[-1].Point)

    for i in range(len(edges) - 1):
        vA = vec(edges[i])
        vB = vec(edges[i + 1])
        getvec(vA, vB)

    vA = vec(edges[-1])
    vB = vec(edges[0])
    getvec(vA, vB)

    if nverts:
        if wire.isClosed():
            nverts.append(nverts[0])
        w = Part.makePolygon(nverts)
        return w
    else:
        return wire


def cleanProjection(shape, tessellate=True, seglength=0.05):
    """Return a compound of edges, optionally tessellate ellipses, splines
    and bezcurves.

    The function was formerly used to workaround bugs in the projection
    algorithm. These bugs have since been fixed. Now the function is only
    used when tessellation of ellipses, splines and bezcurves is required
    (DXF output and Draft_Shape2DView).
    """
    oldedges = shape.Edges
    newedges = []
    for e in oldedges:
        typ = geomType(e)
        try:
            if typ in ["Line", "Circle"]:
                newedges.append(e)
            elif typ == "Ellipse":
                if tessellate:
                    newedges.append(Part.Wire(curvetowire(e, seglength)))
                else:
                    newedges.append(e)
            elif typ in ["BSplineCurve", "BezierCurve"]:
                if isLine(e.Curve):
                    line = Part.LineSegment(e.Vertexes[0].Point, e.Vertexes[-1].Point)
                    newedges.append(line)
                elif tessellate:
                    newedges.append(Part.Wire(curvetowire(e, seglength)))
                else:
                    newedges.append(e)
            else:
                newedges.append(e)
        except Part.OCCError:
            print("Debug: error cleaning edge ", e)

    return Part.makeCompound(newedges)


def tessellateProjection(shape, seglen):
    """Return projection with BSplines and Ellipses broken into line segments.

    Useful for exporting projected views to DXF files.
    """
    oldedges = shape.Edges
    newedges = []
    for e in oldedges:
        try:
            if geomType(e) == "Line":
                newedges.append(e.Curve.toShape())
            elif geomType(e) == "Circle":
                newedges.append(e.Curve.toShape())
            elif geomType(e) == "Ellipse":
                newedges.append(Part.Wire(curvetosegment(e, seglen)))
            elif geomType(e) == "BSplineCurve":
                newedges.append(Part.Wire(curvetosegment(e, seglen)))
            else:
                newedges.append(e)
        except Part.OCCError:
            print("Debug: error cleaning edge ", e)

    return Part.makeCompound(newedges)


def get_placement_perpendicular_to_wire(wire):
    """Return the placement whose base is the wire's first vertex and it's z axis aligned to the wire's tangent."""
    pl = App.Placement()
    if wire.Length > 0.0:
        pl.Base = wire.OrderedVertexes[0].Point
        first_edge = wire.OrderedEdges[0]
        if first_edge.Orientation == "Forward":
            zaxis = -first_edge.tangentAt(first_edge.FirstParameter)
        else:
            zaxis = first_edge.tangentAt(first_edge.LastParameter)
        pl.Rotation = App.Rotation(App.Vector(1, 0, 0), App.Vector(0, 0, 1), zaxis, "ZYX")
    else:
        App.Console.PrintError(
            "debug: get_placement_perpendicular_to_wire called with a zero-length wire.\n"
        )
    return pl


def get_extended_wire(wire, offset_start, offset_end):
    """Return a wire trimmed (negative offset) or extended (positive offset) at its first vertex, last vertex or both ends.

    get_extended_wire(wire, -100.0, 0.0) -> returns a copy of the wire with its first 100 mm removed
    get_extended_wire(wire, 0.0, 100.0) -> returns a copy of the wire extended by 100 mm after it's last vertex
    """
    if min(offset_start, offset_end, offset_start + offset_end) <= -wire.Length:
        App.Console.PrintError(
            "debug: get_extended_wire error, wire's length insufficient for trimming.\n"
        )
        return wire
    if offset_start < 0:  # Trim the wire from the first vertex
        offset_start = -offset_start
        out_edges = []
        for edge in wire.OrderedEdges:
            if offset_start >= edge.Length:  # Remove entire edge
                offset_start -= edge.Length
            elif round(offset_start, precision()) > 0:  # Split edge, to remove the required length
                if edge.Orientation == "Forward":
                    new_edge = edge.split(edge.getParameterByLength(offset_start)).OrderedEdges[1]
                else:
                    new_edge = edge.split(
                        edge.getParameterByLength(edge.Length - offset_start)
                    ).OrderedEdges[0]
                new_edge.Placement = (
                    edge.Placement
                )  # Strangely, edge.split discards the placement and orientation
                new_edge.Orientation = edge.Orientation
                out_edges.append(new_edge)
                offset_start = 0
            else:  # Keep the remaining entire edges
                out_edges.append(edge)
        wire = Part.Wire(out_edges)
    elif offset_start > 0:  # Extend the first edge along its normal
        first_edge = wire.OrderedEdges[0]
        if first_edge.Orientation == "Forward":
            start, end = first_edge.FirstParameter, first_edge.LastParameter
            vec = first_edge.tangentAt(start).multiply(offset_start)
        else:
            start, end = first_edge.LastParameter, first_edge.FirstParameter
            vec = -first_edge.tangentAt(start).multiply(offset_start)
        if geomType(first_edge) == "Line":  # Replace first edge with the extended new edge
            new_edge = Part.LineSegment(
                first_edge.valueAt(start).sub(vec), first_edge.valueAt(end)
            ).toShape()
            wire = Part.Wire([new_edge] + wire.OrderedEdges[1:])
        else:  # Add a straight edge before the first vertex
            new_edge = Part.LineSegment(
                first_edge.valueAt(start).sub(vec), first_edge.valueAt(start)
            ).toShape()
            wire = Part.Wire([new_edge] + wire.OrderedEdges)
    if offset_end < 0:  # Trim the wire from the last vertex
        offset_end = -offset_end
        out_edges = []
        for edge in reversed(wire.OrderedEdges):
            if offset_end >= edge.Length:  # Remove entire edge
                offset_end -= edge.Length
            elif round(offset_end, precision()) > 0:  # Split edge, to remove the required length
                if edge.Orientation == "Forward":
                    new_edge = edge.split(
                        edge.getParameterByLength(edge.Length - offset_end)
                    ).OrderedEdges[0]
                else:
                    new_edge = edge.split(edge.getParameterByLength(offset_end)).OrderedEdges[1]
                new_edge.Placement = (
                    edge.Placement
                )  # Strangely, edge.split discards the placement and orientation
                new_edge.Orientation = edge.Orientation
                out_edges.insert(0, new_edge)
                offset_end = 0
            else:  # Keep the remaining entire edges
                out_edges.insert(0, edge)
        wire = Part.Wire(out_edges)
    elif offset_end > 0:  # Extend the last edge along its normal
        last_edge = wire.OrderedEdges[-1]
        if last_edge.Orientation == "Forward":
            start, end = last_edge.FirstParameter, last_edge.LastParameter
            vec = last_edge.tangentAt(end).multiply(offset_end)
        else:
            start, end = last_edge.LastParameter, last_edge.FirstParameter
            vec = -last_edge.tangentAt(end).multiply(offset_end)
        if geomType(last_edge) == "Line":  # Replace last edge with the extended new edge
            new_edge = Part.LineSegment(
                last_edge.valueAt(start), last_edge.valueAt(end).add(vec)
            ).toShape()
            wire = Part.Wire(wire.OrderedEdges[:-1] + [new_edge])
        else:  # Add a straight edge after the last vertex
            new_edge = Part.LineSegment(
                last_edge.valueAt(end), last_edge.valueAt(end).add(vec)
            ).toShape()
            wire = Part.Wire(wire.OrderedEdges + [new_edge])
    return wire


## @}
