# 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 general functions to work with topological shapes."""
## @package general
# \ingroup draftgeoutils
# \brief Provides general functions to work with topological shapes.

import math
import lazy_loader.lazy_loader as lz

import FreeCAD as App
import DraftVecUtils
from draftutils import params

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

## \addtogroup draftgeoutils
# @{

# Default normal direction for all geometry operations
NORM = App.Vector(0, 0, 1)


def precision():
    """Return the Draft precision setting."""
    # Set precision level with a cap to avoid overspecification that:
    #  1 - whilst it is precise enough (e.g. that OCC would consider
    #      2 points are coincident)
    #      (not sure what it should be 10 or otherwise);
    #  2 - but FreeCAD/OCC can handle 'internally'
    #      (e.g. otherwise user may set something like
    #      15 that the code would never consider 2 points are coincident
    #      as internal float is not that precise)
    precisionMax = 10
    precisionInt = params.get_param("precision")
    precisionInt = precisionInt if precisionInt <= 10 else precisionMax
    return precisionInt


def vec(edge, use_orientation=False):
    """Return a vector from an edge or a Part.LineSegment.

    If use_orientation is True, it takes into account the edges orientation.
    If edge is not straight, you'll get strange results!
    """
    if isinstance(edge, Part.Shape):
        if use_orientation and isinstance(edge, Part.Edge) and edge.Orientation == "Reversed":
            return edge.Vertexes[0].Point.sub(edge.Vertexes[-1].Point)
        else:
            return edge.Vertexes[-1].Point.sub(edge.Vertexes[0].Point)
    elif isinstance(edge, Part.LineSegment):
        return edge.EndPoint.sub(edge.StartPoint)
    else:
        return None


def edg(p1, p2):
    """Return an edge from 2 vectors."""
    if isinstance(p1, App.Vector) and isinstance(p2, App.Vector):
        if DraftVecUtils.equals(p1, p2):
            return None

        return Part.LineSegment(p1, p2).toShape()


def getVerts(shape):
    """Return a list containing vectors of each vertex of the shape."""
    if not hasattr(shape, "Vertexes"):
        return []

    p = []
    for v in shape.Vertexes:
        p.append(v.Point)
    return p


def v1(edge):
    """Return the first point of an edge."""
    return edge.Vertexes[0].Point


def isNull(something):
    """Return True if the given shape, vector, or placement is Null.

    If the vector is (0, 0, 0), it will return True.
    """
    if isinstance(something, Part.Shape):
        return something.isNull()
    elif isinstance(something, App.Vector):
        if something == App.Vector(0, 0, 0):
            return True
        else:
            return False
    elif isinstance(something, App.Placement):
        if something.Base == App.Vector(0, 0, 0) and something.Rotation.Q == (0, 0, 0, 1):
            return True
        else:
            return False


def isPtOnEdge(pt, edge):
    """Test if a point lies on an edge."""
    v = Part.Vertex(pt)
    try:
        d = v.distToShape(edge)
    except Part.OCCError:
        return False
    else:
        if d:
            if round(d[0], precision()) == 0:
                return True
    return False


def hasCurves(shape):
    """Check if the given shape has curves."""
    for e in shape.Edges:
        if not isinstance(e.Curve, (Part.LineSegment, Part.Line)):
            return True
    return False


def isAligned(edge, axis="x"):
    """Check if the given edge or line is aligned to the given axis.

    The axis can be 'x', 'y' or 'z'.
    """

    def is_same(a, b):
        return round(a, precision()) == round(b, precision())

    if axis == "x":
        if isinstance(edge, Part.Edge):
            if len(edge.Vertexes) == 2:
                return is_same(edge.Vertexes[0].X, edge.Vertexes[-1].X)
        elif isinstance(edge, Part.LineSegment):
            return is_same(edge.StartPoint.x, edge.EndPoint.x)

    elif axis == "y":
        if isinstance(edge, Part.Edge):
            if len(edge.Vertexes) == 2:
                return is_same(edge.Vertexes[0].Y, edge.Vertexes[-1].Y)
        elif isinstance(edge, Part.LineSegment):
            return is_same(edge.StartPoint.y, edge.EndPoint.y)

    elif axis == "z":
        if isinstance(edge, Part.Edge):
            if len(edge.Vertexes) == 2:
                return is_same(edge.Vertexes[0].Z, edge.Vertexes[-1].Z)
        elif isinstance(edge, Part.LineSegment):
            return is_same(edge.StartPoint.z, edge.EndPoint.z)

    return False


def getQuad(face):
    """Return a list of 3 vectors if the face is a quad, otherwise None.

    Returns
    -------
    basepoint, Xdir, Ydir
        If the face is a quad.

    None
        If the face is not a quad.
    """
    if len(face.Edges) != 4:
        return None

    v1 = vec(face.Edges[0])  # Warning redefinition of function v1
    v2 = vec(face.Edges[1])
    v3 = vec(face.Edges[2])
    v4 = vec(face.Edges[3])
    angles90 = [round(math.pi * 0.5, precision()), round(math.pi * 1.5, precision())]

    angles180 = [0, round(math.pi, precision()), round(math.pi * 2, precision())]
    for ov in [v2, v3, v4]:
        if not (round(v1.getAngle(ov), precision()) in angles90 + angles180):
            return None

    for ov in [v2, v3, v4]:
        if round(v1.getAngle(ov), precision()) in angles90:
            v1.normalize()
            ov.normalize()
            return [face.Edges[0].Vertexes[0].Point, v1, ov]


def areColinear(e1, e2):
    """Return True if both edges are colinear."""
    if not isinstance(e1.Curve, (Part.LineSegment, Part.Line)):
        return False
    if not isinstance(e2.Curve, (Part.LineSegment, Part.Line)):
        return False

    v1 = vec(e1)
    v2 = vec(e2)
    a = round(v1.getAngle(v2), precision())
    if (a == 0) or (a == round(math.pi, precision())):
        v3 = e2.Vertexes[0].Point.sub(e1.Vertexes[0].Point)
        if DraftVecUtils.isNull(v3):
            return True
        else:
            a2 = round(v1.getAngle(v3), precision())
            if (a2 == 0) or (a2 == round(math.pi, precision())):
                return True
    return False


def hasOnlyWires(shape):
    """Return True if all edges are inside a wire."""
    ne = 0
    for w in shape.Wires:
        ne += len(w.Edges)
    if ne == len(shape.Edges):
        return True
    return False


def geomType(edge):
    """Return the type of geometry this edge is based on.

    Parameters
    ----------
    edge: the edge whose `Curve` attribute is to be checked.

    Returns
    -------
    str
        Return the type of the edge's Curve attribute or "Unknown",
        if the parameter is missing.
    """
    try:
        if isinstance(edge.Curve, (Part.LineSegment, Part.Line)):
            return "Line"
        elif isinstance(edge.Curve, Part.Circle):
            return "Circle"
        elif isinstance(edge.Curve, Part.BSplineCurve):
            return "BSplineCurve"
        elif isinstance(edge.Curve, Part.BezierCurve):
            return "BezierCurve"
        elif isinstance(edge.Curve, Part.Ellipse):
            return "Ellipse"
        else:
            return "Unknown"
    except Exception:  # catch all errors, no only TypeError
        return "Unknown"


def isValidPath(shape):
    """Return True if the shape can be used as an extrusion path."""
    if shape.isNull():
        return False
    if shape.Faces:
        return False
    if len(shape.Wires) > 1:
        return False
    if shape.Wires:
        if shape.Wires[0].isClosed():
            return False
    if shape.isClosed():
        return False
    return True


def findClosest(base_point, point_list):
    """Find closest point in a list of points to the base point.

    Returns
    -------
    int
        An index from the list of points is returned.

    None
        If point_list is empty.
    """
    npoint = None
    if not point_list:
        return None

    smallest = 1000000
    for n in range(len(point_list)):
        new = base_point.sub(point_list[n]).Length
        if new < smallest:
            smallest = new
            npoint = n

    return npoint


def getBoundaryAngles(angle, alist):
    """Return the 2 closest angles that encompass the given angle."""
    negs = True
    while negs:
        negs = False
        for i in range(len(alist)):
            if alist[i] < 0:
                alist[i] = 2 * math.pi + alist[i]
                negs = True
        if angle < 0:
            angle = 2 * math.pi + angle
            negs = True

    lower = None
    for a in alist:
        if a < angle:
            if lower is None:
                lower = a
            else:
                if a > lower:
                    lower = a

    if lower is None:
        lower = 0
        for a in alist:
            if a > lower:
                lower = a

    higher = None
    for a in alist:
        if a > angle:
            if higher is None:
                higher = a
            else:
                if a < higher:
                    higher = a

    if higher is None:
        higher = 2 * math.pi
        for a in alist:
            if a < higher:
                higher = a

    return lower, higher


## @}
