# 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>                   *
# *   Copyright (c) 2020 FreeCAD Developers                                 *
# *   Copyright (c) 2024 FreeCAD Project Association                        *
# *                                                                         *
# *   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 create Sketch objects from Draft objects."""
## @package make_sketch
# \ingroup draftmake
# \brief Provides functions to create Sketch objects from Draft objects.

## \addtogroup draftmake
# @{
import math

import FreeCAD as App
import DraftVecUtils
import DraftGeomUtils
from draftutils import gui_utils
from draftutils import utils
from draftutils.translate import translate


def make_sketch(
    objects_list,
    autoconstraints=False,
    addTo=None,
    delete=False,
    name="Sketch",
    radiusPrecision=-1,
    tol=1e-3,
):
    """make_sketch(objects_list, [autoconstraints], [addTo], [delete],
                   [name], [radiusPrecision], [tol])

    Makes a Sketch objects_list with the given Draft objects.

    Parameters
    ----------
    objects_list: can be single or list of objects of Draft type objects,
        Part::Feature, Part.Shape, or mix of them.

    autoconstraints(False): if True, coincident, horizontal and vertical
        constraints will be added automatically.

    addTo(None): if set to an existing sketch, geometry will be added to it
        instead of creating a new one.

    delete(False): if True, the original object will be deleted.
        If set to a string "all" the object and all its linked object will be
        deleted.

    name("Sketch"): the name for the new sketch object.

    radiusPrecision(-1): If <0, disable radius constraint. If =0, add individual
        radius constraint. If >0, the radius will be rounded according to this
        precision, and "Equal" constraint will be added to curve with equal
        radius within precision.

    tol(1e-3): Tolerance used to check if the shapes are planar and coplanar.
        Consider change to tol=-1 for a more accurate analysis.
    """

    if not App.ActiveDocument:
        App.Console.PrintError("No active document. Aborting\n")
        return

    import Part
    from Sketcher import Constraint

    # lists to accumulate shapes with defined normal and undefined normal
    shape_norm_yes = list()
    shape_norm_no = list()

    if not isinstance(objects_list, (list, tuple)):
        objects_list = [objects_list]

    for obj in objects_list:
        if isinstance(obj, Part.Shape):
            shape = obj
        elif not hasattr(obj, "Shape"):
            App.Console.PrintError(translate("draft", "No shape found") + "\n")
            return None
        else:
            shape = obj.Shape

        if not DraftGeomUtils.is_planar(shape, tol):
            App.Console.PrintError(translate("draft", "All shapes must be planar") + "\n")
            return None

        if DraftGeomUtils.get_normal(shape, tol):
            shape_norm_yes.append(shape)
        else:
            shape_norm_no.append(shape)

    shapes_list = shape_norm_yes + shape_norm_no

    # test if all shapes are coplanar
    if len(shape_norm_yes) >= 1:
        for shape in shapes_list[1:]:
            if not DraftGeomUtils.are_coplanar(shapes_list[0], shape, tol):
                App.Console.PrintError(translate("draft", "All shapes must be coplanar") + "\n")
                return None
        # define sketch normal
        normal = DraftGeomUtils.get_normal(shapes_list[0], tol)

    else:
        # suppose all geometries are straight lines or points
        points = [vertex.Point for shape in shapes_list for vertex in shape.Vertexes]
        if len(points) >= 2:
            try:
                poly = Part.makePolygon(points)
            except Part.OCCError:
                # all points coincide
                normal = App.Vector(0, 0, 1)
            else:
                if not DraftGeomUtils.is_planar(poly, tol):
                    App.Console.PrintError(translate("draft", "All shapes must be coplanar") + "\n")
                    return None
                normal = DraftGeomUtils.get_shape_normal(poly)
        else:
            # only one point
            normal = App.Vector(0, 0, 1)

    if addTo:
        nobj = addTo
    else:
        nobj = App.ActiveDocument.addObject("Sketcher::SketchObject", name)

    # Collect constraints and add in one go to improve performance
    constraints = []
    radiuses = {}

    def addRadiusConstraint(edge):
        try:
            if radiusPrecision < 0:
                return
            if radiusPrecision == 0:
                constraints.append(Constraint("Radius", nobj.GeometryCount - 1, edge.Curve.Radius))
                return
            r = round(edge.Curve.Radius, radiusPrecision)
            constraints.append(Constraint("Equal", radiuses[r], nobj.GeometryCount - 1))
        except KeyError:
            radiuses[r] = nobj.GeometryCount - 1
            constraints.append(Constraint("Radius", nobj.GeometryCount - 1, r))
        except AttributeError:
            pass

    def convertBezier(edge):
        if DraftGeomUtils.geomType(edge) == "BezierCurve":
            return edge.Curve.toBSpline(edge.FirstParameter, edge.LastParameter).toShape()
        else:
            return edge

    axis = App.Vector(0, 0, 1).cross(normal)
    if axis.Length > 1e-6:
        axis.normalize()
    elif normal.z >= 0:
        axis = App.Vector(0, 0, 1)
    else:
        axis = App.Vector(0, 0, -1)
    angle = math.degrees(DraftVecUtils.angle(normal, App.Vector(0, 0, 1)))
    rotation = App.Rotation(axis, angle)

    point = shapes_list[0].Vertexes[0].Point
    base = App.Vector(normal)
    base.Length = point.dot(
        base.normalize()
    )  # See https://forum.freecad.org/viewtopic.php?f=22&t=69304#p601149

    nobj.Placement = App.Placement(base, rotation)

    for obj in objects_list:
        ok = False
        tp = utils.get_type(obj)

        if tp == "Point":
            # obj.Shape.copy() does not work properly for a Draft_Point.
            # The coords of the point are multiplied by 2.
            # We therefore create a Part Vertex instead.
            shape = Part.Vertex(obj.Shape.Point)
            if angle:
                shape.rotate(App.Vector(0, 0, 0), axis, -angle)
            point = Part.Point(shape.Point)
            nobj.addGeometry(point)
            ok = True

        elif tp == "Shape" or hasattr(obj, "Shape"):
            shape = obj if tp == "Shape" else obj.Shape
            for e in shape.Edges:
                newedge = convertBezier(e)
                nobj.addGeometry(DraftGeomUtils.orientEdge(newedge, normal, make_arc=True))
                addRadiusConstraint(newedge)
            ok = True

        if ok and delete:

            def delObj(obj):
                if obj.InList:
                    App.Console.PrintWarning(
                        translate(
                            "draft", "Cannot delete object {} with dependency".format(obj.Label)
                        )
                        + "\n"
                    )
                else:
                    obj.Document.removeObject(obj.Name)

            try:
                if delete == "all":
                    objs = [obj]
                    while objs:
                        obj = objs[0]
                        objs = objs[1:] + obj.OutList
                        delObj(obj)
                else:
                    delObj(obj)
            except Exception as ex:
                App.Console.PrintWarning(
                    translate("draft", "Failed to delete object {}: {}".format(obj.Label, ex))
                    + "\n"
                )

    nobj.addConstraint(constraints)
    if autoconstraints:
        nobj.detectMissingPointOnPointConstraints(utils.tolerance())
        nobj.makeMissingPointOnPointCoincident(False)
        nobj.detectMissingVerticalHorizontalConstraints(utils.tolerance())
        nobj.makeMissingVerticalHorizontal(False)
        # The MissingVerticalHorizontal functions do not work properly.
        # If elements are added to an existing sketch redundant constraints are created.
        # This can happen if DXF files are imported with the legacy importer.
        # https://forum.freecad.org/viewtopic.php?t=97072
        # https://github.com/FreeCAD/FreeCAD/issues/21396
        # To address this we check for redundant constraints. This can be necessary even
        # the functions were to work properly. For example with this scenario:
        # https://github.com/FreeCAD/FreeCAD/issues/19978
        nobj.solve()
        for idx in nobj.RedundantConstraints[::-1]:
            # Output of RedundantConstraints is one-based.
            nobj.delConstraint(idx - 1)

    return nobj


makeSketch = make_sketch

## @}
