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

# ***************************************************************************
# *   Copyright (c) 2016 Lorenz Hüdepohl <dev@stellardeath.org>             *
# *                                                                         *
# *   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 Path.Base.Generator.helix as helix
from PathScripts.PathUtils import fmt
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import Part
import Path
import Path.Base.FeedRate as PathFeedRate
import Path.Op.Base as PathOp
import Path.Op.CircularHoleBase as PathCircularHoleBase


__title__ = "CAM Helix Operation"
__author__ = "Lorenz Hüdepohl"
__url__ = "https://www.freecad.org"
__doc__ = "Class and implementation of Helix Drill operation"
__contributors__ = "russ4262 (Russell Johnson)"
__created__ = "2016"
__scriptVersion__ = "1b testing"
__lastModified__ = "2019-07-12 09:50 CST"


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 _caclulatePathDirection(mode, side):
    """Calculates the path direction from cut mode and cut side"""
    # NB: at the time of writing, we need py3.8 compat, thus not using py3.10 pattern machting
    if mode == "Conventional" and side == "Inside":
        return "CW"
    elif mode == "Conventional" and side == "Outside":
        return "CCW"
    elif mode == "Climb" and side == "Inside":
        return "CCW"
    elif mode == "Climb" and side == "Outside":
        return "CW"
    else:
        raise ValueError(f"No mapping for '{mode}'/'{side}'")


def _caclulateCutMode(direction, side):
    """Calculates the cut mode from path direction and cut side"""
    # NB: at the time of writing, we need py3.8 compat, thus not using py3.10 pattern machting
    if direction == "CW" and side == "Inside":
        return "Conventional"
    elif direction == "CW" and side == "Outside":
        return "Climb"
    elif direction == "CCW" and side == "Inside":
        return "Climb"
    elif direction == "CCW" and side == "Outside":
        return "Conventional"
    else:
        raise ValueError(f"No mapping for '{direction}'/'{side}'")


class ObjectHelix(PathCircularHoleBase.ObjectOp):
    """Proxy class for Helix operations."""

    @classmethod
    def helixOpPropertyEnumerations(self, dataType="data"):
        """helixOpPropertyEnumerations(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("CAM_Helix", "CW"), "CW"),
                (translate("CAM_Helix", "CCW"), "CCW"),
            ],  # this is the direction that the profile runs
            "StartSide": [
                (translate("PathProfile", "Outside"), "Outside"),
                (translate("PathProfile", "Inside"), "Inside"),
            ],  # side of profile that cutter is on in relation to direction of profile
            "CutMode": [
                (translate("CAM_Helix", "Climb"), "Climb"),
                (translate("CAM_Helix", "Conventional"), "Conventional"),
            ],  # whether the tool "rolls" with or against the feed direction along the profile
        }

        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.append((v, [tup[idx] for tup in enums[v]]))
        Path.Log.debug(data)

        return data

    def circularHoleFeatures(self, obj):
        """circularHoleFeatures(obj) ... enable features supported by Helix."""
        return PathOp.FeatureStepDown | PathOp.FeatureBaseEdges | PathOp.FeatureBaseFaces

    def initCircularHoleOperation(self, obj):
        """initCircularHoleOperation(obj) ... create helix specific properties."""
        obj.addProperty(
            "App::PropertyEnumeration",
            "Direction",
            "Helix Drill",
            QT_TRANSLATE_NOOP(
                "App::Property",
                "The direction of the circular cuts, ClockWise (CW), or CounterClockWise (CCW)",
            ),
        )
        obj.setEditorMode("Direction", ["ReadOnly", "Hidden"])
        obj.setPropertyStatus("Direction", ["ReadOnly", "Output"])

        obj.addProperty(
            "App::PropertyEnumeration",
            "StartSide",
            "Helix Drill",
            QT_TRANSLATE_NOOP("App::Property", "Start cutting from the inside or outside"),
        )

        # TODO: revise property description once v1.0 release string freeze is lifted
        obj.addProperty(
            "App::PropertyEnumeration",
            "CutMode",
            "Helix Drill",
            QT_TRANSLATE_NOOP(
                "App::Property",
                "The direction of the circular cuts, ClockWise (Climb), or CounterClockWise (Conventional)",
            ),
        )

        obj.addProperty(
            "App::PropertyPercent",
            "StepOver",
            "Helix Drill",
            QT_TRANSLATE_NOOP(
                "App::Property", "Percent of cutter diameter to step over on each pass"
            ),
        )
        obj.addProperty(
            "App::PropertyLength",
            "StartRadius",
            "Helix Drill",
            QT_TRANSLATE_NOOP("App::Property", "Starting Radius"),
        )
        obj.addProperty(
            "App::PropertyDistance",
            "OffsetExtra",
            "Helix Drill",
            QT_TRANSLATE_NOOP(
                "App::Property",
                "Extra value to stay away from final profile- good for roughing toolpath",
            ),
        )

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

    def opOnDocumentRestored(self, obj):
        if not hasattr(obj, "StartRadius"):
            obj.addProperty(
                "App::PropertyLength",
                "StartRadius",
                "Helix Drill",
                QT_TRANSLATE_NOOP("App::Property", "Starting Radius"),
            )

        if not hasattr(obj, "OffsetExtra"):
            obj.addProperty(
                "App::PropertyDistance",
                "OffsetExtra",
                "Helix Drill",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "Extra value to stay away from final profile- good for roughing toolpath",
                ),
            )

        if not hasattr(obj, "CutMode"):
            # TODO: consolidate the duplicate definitions from opOnDocumentRestored and
            # initCircularHoleOperation once back on the main line
            obj.addProperty(
                "App::PropertyEnumeration",
                "CutMode",
                "Helix Drill",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "The direction of the circular cuts, ClockWise (Climb), or CounterClockWise (Conventional)",
                ),
            )
            obj.CutMode = ["Climb", "Conventional"]
            if obj.Direction in ["Climb", "Conventional"]:
                # For some month, late in the v1.0 release cycle, we had the cut mode assigned
                # to the direction (see PR#14364). Let's fix files created in this time as well.
                new_dir = "CW" if obj.Direction == "Climb" else "CCW"
                obj.Direction = ["CW", "CCW"]
                obj.Direction = new_dir
            obj.CutMode = _caclulateCutMode(obj.Direction, obj.StartSide)
            obj.setEditorMode("Direction", ["ReadOnly", "Hidden"])
            obj.setPropertyStatus("Direction", ["ReadOnly", "Output"])

    def circularHoleExecute(self, obj, holes):
        """circularHoleExecute(obj, holes) ... generate helix commands for each hole in holes"""
        Path.Log.track()
        obj.Direction = _caclulatePathDirection(obj.CutMode, obj.StartSide)

        self.commandlist.append(Path.Command("(helix cut operation)"))

        self.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value}))

        tool = obj.ToolController.Tool
        tooldiamter = tool.Diameter.Value if hasattr(tool.Diameter, "Value") else tool.Diameter

        args = {
            "edge": None,
            "hole_radius": None,
            "step_down": obj.StepDown.Value,
            "step_over": obj.StepOver / 100,
            "tool_diameter": tooldiamter,
            "inner_radius": obj.StartRadius.Value + obj.OffsetExtra.Value,
            "direction": obj.Direction,
            "startAt": obj.StartSide,
        }

        for hole in holes:
            args["hole_radius"] = (hole["r"] / 2) - (obj.OffsetExtra.Value)
            startPoint = FreeCAD.Vector(hole["x"], hole["y"], obj.StartDepth.Value)
            endPoint = FreeCAD.Vector(hole["x"], hole["y"], obj.FinalDepth.Value)
            args["edge"] = Part.makeLine(startPoint, endPoint)

            # move to starting position
            self.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value}))
            self.commandlist.append(
                Path.Command(
                    "G0",
                    {
                        "X": startPoint.x,
                        "Y": startPoint.y,
                        "Z": obj.ClearanceHeight.Value,
                    },
                )
            )
            self.commandlist.append(
                Path.Command("G0", {"X": startPoint.x, "Y": startPoint.y, "Z": startPoint.z})
            )

            results = helix.generate(**args)

            for command in results:
                self.commandlist.append(command)

        PathFeedRate.setFeedRate(self.commandlist, obj.ToolController)


def SetupProperties():
    """Returns property names for which the "Setup Sheet" should provide defaults."""
    setup = []
    setup.append("CutMode")
    setup.append("StartSide")
    setup.append("StepOver")
    setup.append("StartRadius")
    return setup


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