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

# ***************************************************************************
# *   Copyright (c) 2018 sliptonic <shopinthewoods@gmail.com>               *
# *   Copyright (c) 2020-2021 Schildkroet                                   *
# *                                                                         *
# *   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                                                                   *
# *                                                                         *
# ***************************************************************************

import FreeCAD
import Path
import Path.Op.Base as PathOp
import Path.Op.EngraveBase as PathEngraveBase
import Path.Op.Util as PathOpUtil
import math

from PySide.QtCore import QT_TRANSLATE_NOOP

# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader

Part = LazyLoader("Part", globals(), "Part")

__title__ = "CAM Deburr Operation"
__author__ = "sliptonic (Brad Collette), Schildkroet"
__url__ = "https://www.freecad.org"
__doc__ = "Deburr operation."


if False:
    Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
    Path.Log.trackModule(Path.Log.thisModule())
else:
    Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())

translate = FreeCAD.Qt.translate


def toolDepthAndOffset(width, extraDepth, tool, printInfo):
    """toolDepthAndOffset(width, extraDepth, tool) ... return tuple for given\n
    parameters."""

    if not hasattr(tool, "Diameter"):
        raise ValueError("Deburr requires tool with diameter\n")

    suppressInfo = False
    if hasattr(tool, "CuttingEdgeAngle"):
        angle = float(tool.CuttingEdgeAngle)
        if Path.Geom.isRoughly(angle, 180) or Path.Geom.isRoughly(angle, 0):
            angle = 180
            toolOffset = float(tool.Diameter) / 2
        else:
            if hasattr(tool, "TipDiameter"):
                toolOffset = float(tool.TipDiameter) / 2
            elif hasattr(tool, "FlatRadius"):
                toolOffset = float(tool.FlatRadius)
            else:
                toolOffset = 0.0
                if printInfo and not suppressInfo:
                    FreeCAD.Console.PrintMessage(
                        translate(
                            "PathDeburr",
                            "The selected tool has no FlatRadius and no TipDiameter property. Assuming {}\n".format(
                                "Endmill" if angle == 180 else "V-Bit"
                            ),
                        )
                    )
                suppressInfo = True
    else:
        angle = 180
        toolOffset = float(tool.Diameter) / 2
        if printInfo:
            FreeCAD.Console.PrintMessage(
                translate(
                    "PathDeburr",
                    "The selected tool has no CuttingEdgeAngle property. Assuming Endmill\n",
                )
            )
        suppressInfo = True

    tan = math.tan(math.radians(angle / 2))

    toolDepth = 0 if Path.Geom.isRoughly(tan, 0) else width / tan
    depth = toolDepth + extraDepth
    extraOffset = -width if angle == 180 else (extraDepth * tan)
    offset = toolOffset + extraOffset

    return (depth, offset, extraOffset, suppressInfo)


class ObjectDeburr(PathEngraveBase.ObjectOp):
    """Proxy class for Deburr operation."""

    def opFeatures(self, obj):
        return (
            PathOp.FeatureTool
            | PathOp.FeatureHeights
            | PathOp.FeatureStepDown
            | PathOp.FeatureBaseEdges
            | PathOp.FeatureBaseFaces
            | PathOp.FeatureCoolant
            | PathOp.FeatureBaseGeometry
        )

    def initOperation(self, obj):
        Path.Log.track(obj.Label)
        obj.addProperty(
            "App::PropertyDistance",
            "Width",
            "Deburr",
            QT_TRANSLATE_NOOP("App::Property", "The desired width of the chamfer"),
        )
        obj.addProperty(
            "App::PropertyDistance",
            "ExtraDepth",
            "Deburr",
            QT_TRANSLATE_NOOP("App::Property", "The additional depth of the toolpath"),
        )
        obj.addProperty(
            "App::PropertyEnumeration",
            "Join",
            "Deburr",
            QT_TRANSLATE_NOOP("App::Property", "How to join chamfer segments"),
        )
        # obj.Join = ["Round", "Miter"]
        obj.setEditorMode("Join", 2)  # hide for now
        obj.addProperty(
            "App::PropertyEnumeration",
            "Direction",
            "Deburr",
            QT_TRANSLATE_NOOP("App::Property", "Direction of toolpath"),
        )
        # obj.Direction = ["CW", "CCW"]
        obj.addProperty(
            "App::PropertyEnumeration",
            "Side",
            "Deburr",
            QT_TRANSLATE_NOOP("App::Property", "Side of base object"),
        )
        obj.Side = ["Outside", "Inside"]
        obj.setEditorMode("Side", 2)  # Hide property, it's calculated by op
        obj.addProperty(
            "App::PropertyInteger",
            "EntryPoint",
            "Deburr",
            QT_TRANSLATE_NOOP("App::Property", "The segment where the toolpath starts"),
        )

        ENUMS = self.propertyEnumerations()
        for n in ENUMS:
            setattr(obj, n[0], n[1])

    @classmethod
    def propertyEnumerations(self, dataType="data"):
        """opPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
        Args:
            dataType = 'data', 'raw', 'translated'
        Notes:
        'data' is list of internal string literals used in code
        'raw' is list of (translated_text, data_string) tuples
        'translated' is list of translated string literals
        """

        # Enumeration lists for App::PropertyEnumeration properties
        enums = {
            "Direction": [
                (translate("Path", "CW"), "CW"),
                (translate("Path", "CCW"), "CCW"),
            ],  # this is the direction that the profile runs
            "Join": [
                (translate("PathDeburr", "Round"), "Round"),
                (translate("PathDeburr", "Miter"), "Miter"),
            ],  # this is the direction that the profile runs
        }

        if dataType == "raw":
            return enums

        data = list()
        idx = 0 if dataType == "translated" else 1

        Path.Log.debug(enums)

        for k, v in enumerate(enums):
            # data[k] = [tup[idx] for tup in v]
            data.append((v, [tup[idx] for tup in enums[v]]))
        Path.Log.debug(data)

        return data

    def opOnDocumentRestored(self, obj):
        obj.setEditorMode("Join", 2)  # hide for now

    def opExecute(self, obj):
        Path.Log.track(obj.Label)

        if not obj.Base:
            return

        if not hasattr(self, "printInfo"):
            self.printInfo = True
        try:
            (depth, offset, extraOffset, suppressInfo) = toolDepthAndOffset(
                obj.Width.Value, obj.ExtraDepth.Value, self.tool, self.printInfo
            )
            self.printInfo = not suppressInfo
        except ValueError as e:
            msg = "{} \n No path will be generated".format(e)
            raise ValueError(msg)
            # QtGui.QMessageBox.information(None, "Tool Error", msg)
            # return

        Path.Log.track(obj.Label, depth, offset)

        self.basewires = []
        self.adjusted_basewires = []
        wires = []

        for base, subs in obj.Base:
            edges = []
            basewires = []
            max_h = -99999
            radius_top = 0
            radius_bottom = 0

            for f in subs:
                sub = base.Shape.getElement(f)

                if type(sub) == Part.Edge:  # Edge
                    edges.append(sub)

                elif type(sub) == Part.Face and sub.normalAt(0, 0) != FreeCAD.Vector(
                    0, 0, 1
                ):  # Angled face
                    # If an angled face is selected, the lower edge is projected to the height of the upper edge,
                    # to simulate an edge

                    # Find z value of upper edge
                    for edge in sub.Edges:
                        for p0 in edge.Vertexes:
                            if p0.Point.z > max_h:
                                max_h = p0.Point.z

                    # Find biggest radius for top/bottom
                    for edge in sub.Edges:
                        if Part.Circle == type(edge.Curve):
                            if edge.Vertexes[0].Point.z == max_h:
                                if edge.Curve.Radius > radius_top:
                                    radius_top = edge.Curve.Radius
                            else:
                                if edge.Curve.Radius > radius_bottom:
                                    radius_bottom = edge.Curve.Radius

                    # Search for lower edge and raise it to height of upper edge
                    for edge in sub.Edges:
                        if Part.Circle == type(edge.Curve):  # Edge is a circle
                            if edge.Vertexes[0].Point.z < max_h:

                                if edge.Closed:  # Circle
                                    # New center
                                    center = FreeCAD.Vector(
                                        edge.Curve.Center.x, edge.Curve.Center.y, max_h
                                    )
                                    new_edge = Part.makeCircle(
                                        edge.Curve.Radius,
                                        center,
                                        FreeCAD.Vector(0, 0, 1),
                                    )
                                    edges.append(new_edge)

                                    # Modify offset for inner angled faces
                                    if radius_bottom < radius_top:
                                        offset -= 2 * extraOffset

                                    break

                                else:  # Arc
                                    if edge.Vertexes[0].Point.z == edge.Vertexes[1].Point.z:
                                        # Arc vertexes are on same layer
                                        l1 = math.sqrt(
                                            (edge.Vertexes[0].Point.x - edge.Curve.Center.x) ** 2
                                            + (edge.Vertexes[0].Point.y - edge.Curve.Center.y) ** 2
                                        )
                                        l2 = math.sqrt(
                                            (edge.Vertexes[1].Point.x - edge.Curve.Center.x) ** 2
                                            + (edge.Vertexes[1].Point.y - edge.Curve.Center.y) ** 2
                                        )

                                        # New center
                                        center = FreeCAD.Vector(
                                            edge.Curve.Center.x,
                                            edge.Curve.Center.y,
                                            max_h,
                                        )

                                        # Calculate angles based on x-axis (0 - PI/2)
                                        start_angle = math.acos(
                                            (edge.Vertexes[0].Point.x - edge.Curve.Center.x) / l1
                                        )
                                        end_angle = math.acos(
                                            (edge.Vertexes[1].Point.x - edge.Curve.Center.x) / l2
                                        )

                                        # Angles are based on x-axis (Mirrored on x-axis) -> negative y value means negative angle
                                        if edge.Vertexes[0].Point.y < edge.Curve.Center.y:
                                            start_angle *= -1
                                        if edge.Vertexes[1].Point.y < edge.Curve.Center.y:
                                            end_angle *= -1

                                        # Create new arc
                                        new_edge = Part.ArcOfCircle(
                                            Part.Circle(
                                                center,
                                                FreeCAD.Vector(0, 0, 1),
                                                edge.Curve.Radius,
                                            ),
                                            start_angle,
                                            end_angle,
                                        ).toShape()
                                        edges.append(new_edge)

                                        # Modify offset for inner angled faces
                                        if radius_bottom < radius_top:
                                            offset -= 2 * extraOffset

                                        break

                        else:  # Line
                            if (
                                edge.Vertexes[0].Point.z == edge.Vertexes[1].Point.z
                                and edge.Vertexes[0].Point.z < max_h
                            ):
                                new_edge = Part.Edge(
                                    Part.LineSegment(
                                        FreeCAD.Vector(
                                            edge.Vertexes[0].Point.x,
                                            edge.Vertexes[0].Point.y,
                                            max_h,
                                        ),
                                        FreeCAD.Vector(
                                            edge.Vertexes[1].Point.x,
                                            edge.Vertexes[1].Point.y,
                                            max_h,
                                        ),
                                    )
                                )
                                edges.append(new_edge)

                elif sub.Wires:
                    basewires.extend(sub.Wires)

                else:  # Flat face
                    basewires.append(Part.Wire(sub.Edges))

            self.edges = edges
            for edgelist in Part.sortEdges(edges):
                basewires.append(Part.Wire(edgelist))

            self.basewires.extend(basewires)

            # Set default side
            side = ["Outside"]

            for w in basewires:
                self.adjusted_basewires.append(w)
                wire = PathOpUtil.offsetWire(w, base.Shape, offset, True, side)
                if wire:
                    wires.append(wire)

        # Set direction of op
        forward = obj.Direction == "CW"

        # Set value of side
        obj.Side = side[0]
        # Check side extra for angled faces
        if radius_top > radius_bottom:
            obj.Side = "Inside"

        zValues = []
        z = 0
        if obj.StepDown.Value != 0:
            while z + obj.StepDown.Value < depth:
                z = z + obj.StepDown.Value
                zValues.append(z)

        zValues.append(depth)
        Path.Log.track(obj.Label, depth, zValues)

        if obj.EntryPoint < 0:
            obj.EntryPoint = 0

        self.wires = wires
        self.buildpathocc(obj, wires, zValues, True, forward, obj.EntryPoint)

    def opRejectAddBase(self, obj, base, sub):
        """The chamfer op can only deal with features of the base model, all others are rejected."""
        return base not in self.model

    def opSetDefaultValues(self, obj, job):
        Path.Log.track(obj.Label, job.Label)
        obj.Width = "1 mm"
        obj.ExtraDepth = "0.5 mm"
        obj.Join = "Round"
        obj.setExpression("StepDown", "0 mm")
        obj.StepDown = "0 mm"
        obj.Direction = "CW"
        obj.Side = "Outside"
        obj.EntryPoint = 0


def SetupProperties():
    setup = []
    setup.append("Width")
    setup.append("ExtraDepth")
    return setup


def Create(name, obj=None, parentJob=None):
    """Create(name) ... Creates and returns a Deburr operation."""
    if obj is None:
        obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
    obj.Proxy = ObjectDeburr(obj, name, parentJob)
    return obj
