#  -*- coding: utf-8 -*-
# ***************************************************************************
# *   Copyright (c) 2017 LTS <SammelLothar@gmx.de> under LGPL               *
# *   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 as App
import FreeCADGui
import Path
import Path.Base.Language as PathLanguage
import Path.Dressup.Utils as PathDressup
import PathScripts.PathUtils as PathUtils
from Path.Geom import wireForPath
import math

__doc__ = """LeadInOut Dressup USE ROLL-ON ROLL-OFF to profile"""

from PySide.QtCore import QT_TRANSLATE_NOOP

from PathPythonGui.simple_edit_panel import SimpleEditPanel

translate = App.Qt.translate

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())


class ObjectDressup:
    def __init__(self, obj):
        lead_styles = [
            QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Arc"),
            QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Tangent"),
            QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Perpendicular"),
        ]
        self.obj = obj
        obj.addProperty(
            "App::PropertyLink",
            "Base",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "The base toolpath to modify"),
        )
        obj.addProperty(
            "App::PropertyBool",
            "LeadIn",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "Calculate roll-on to toolpath"),
        )
        obj.addProperty(
            "App::PropertyBool",
            "LeadOut",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "Calculate roll-off from toolpath"),
        )
        obj.addProperty(
            "App::PropertyBool",
            "KeepToolDown",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "Keep the Tool Down in toolpath"),
        )
        obj.addProperty(
            "App::PropertyDistance",
            "Length",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "Length or Radius of the approach"),
        )
        obj.addProperty(
            "App::PropertyDistance",
            "LengthOut",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "Length or Radius of the exit"),
        )
        obj.addProperty(
            "App::PropertyEnumeration",
            "StyleOn",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "The Style of motion into the toolpath"),
        )
        obj.StyleOn = lead_styles
        obj.addProperty(
            "App::PropertyEnumeration",
            "StyleOff",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "The Style of motion out of the toolpath"),
        )
        obj.StyleOff = lead_styles
        obj.addProperty(
            "App::PropertyDistance",
            "ExtendLeadIn",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "Extends LeadIn distance"),
        )
        obj.addProperty(
            "App::PropertyDistance",
            "ExtendLeadOut",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "Extends LeadOut distance"),
        )
        obj.addProperty(
            "App::PropertyBool",
            "RapidPlunge",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "Perform plunges with G0"),
        )
        obj.addProperty(
            "App::PropertyBool",
            "IncludeLayers",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "Apply LeadInOut to layers within an operation"),
        )
        obj.Proxy = self

        self.wire = None
        self.rapids = None

    def dumps(self):
        return None

    def loads(self, state):
        return None

    def setup(self, obj):
        obj.Length = PathDressup.toolController(obj.Base).Tool.Diameter * 0.75
        obj.LengthOut = PathDressup.toolController(obj.Base).Tool.Diameter * 0.75
        obj.LeadIn = True
        obj.LeadOut = True
        obj.KeepToolDown = False
        obj.StyleOn = "Arc"
        obj.StyleOff = "Arc"
        obj.ExtendLeadIn = 0
        obj.ExtendLeadOut = 0
        obj.RapidPlunge = False
        obj.IncludeLayers = True

    def execute(self, obj):
        if not obj.Base:
            return
        if not obj.Base.isDerivedFrom("Path::Feature"):
            return
        if not obj.Base.Path:
            return

        if obj.Length <= 0:
            Path.Log.error(
                translate("CAM_DressupLeadInOut", "Length/Radius positive not Null") + "\n"
            )
            obj.Length = 0.1

        if obj.LengthOut <= 0:
            Path.Log.error(
                translate("CAM_DressupLeadInOut", "Length/Radius positive not Null") + "\n"
            )
            obj.LengthOut = 0.1

        self.wire, self.rapids = wireForPath(PathUtils.getPathWithPlacement(obj.Base))
        obj.Path = self.generateLeadInOutCurve(obj)

    def getDirectionOfPath(self, obj):
        op = PathDressup.baseOp(obj.Base)
        side = op.Side if hasattr(op, "Side") else "Inside"
        direction = op.Direction if hasattr(op, "Direction") else "CCW"

        if side == "Outside":
            return "left" if direction == "CW" else "right"
        else:
            return "right" if direction == "CW" else "left"

    def getArcDirection(self, obj):
        direction = self.getDirectionOfPath(obj)
        return math.pi / 2 if direction == "left" else -math.pi / 2

    def getTravelStart(self, obj, pos, first):
        op = PathDressup.baseOp(obj.Base)
        vertfeed = PathDressup.toolController(obj.Base).VertFeed.Value
        travel = []

        # begin positions for travel and plunge moves are not used anywhere,
        # skipping them makes our life a lot easier

        # move to clearance height
        if first:
            travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": op.ClearanceHeight.Value}))

        # move to correct xy-position
        travel.append(PathLanguage.MoveStraight(None, "G0", {"X": pos.x, "Y": pos.y}))

        # move to correct z-position (either rapidly or in two steps)
        if obj.RapidPlunge:
            travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": pos.z}))
        else:
            if first or not obj.KeepToolDown:
                travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": op.SafeHeight.Value}))
            travel.append(PathLanguage.MoveStraight(None, "G1", {"Z": pos.z, "F": vertfeed}))

        return travel

    def getTravelEnd(self, obj, pos, last):
        op = PathDressup.baseOp(obj.Base)
        travel = []

        # move to clearance height
        if last or not obj.KeepToolDown:
            travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": op.ClearanceHeight.Value}))

        return travel

    def angleToVector(self, angle):
        return App.Vector(math.cos(angle), math.sin(angle), 0)

    def createArcMove(self, obj, begin, end, c):
        horizfeed = PathDressup.toolController(obj.Base).HorizFeed.Value

        param = {"X": end.x, "Y": end.y, "I": c.x, "J": c.y, "F": horizfeed}
        if self.getArcDirection(obj) > 0:
            return PathLanguage.MoveArcCCW(begin, "G3", param)
        else:
            return PathLanguage.MoveArcCW(begin, "G2", param)

    def createStraightMove(self, obj, begin, end):
        horizfeed = PathDressup.toolController(obj.Base).HorizFeed.Value

        param = {"X": end.x, "Y": end.y, "F": horizfeed}
        return PathLanguage.MoveStraight(begin, "G1", param)

    def getLeadStart(self, obj, move, first):
        lead = []
        begin = move.positionBegin()

        def prepend(instr):
            nonlocal lead
            nonlocal begin
            lead.insert(0, instr)
            begin = lead[0].positionBegin()

        #    tangent  begin      move
        #    <----_-----x-------------------x
        #       /       |
        #     /         | normal
        #    |          |
        #    x          v

        if obj.LeadIn:
            length = obj.Length.Value
            angle = move.anglesOfTangents()[0]
            tangent = -self.angleToVector(angle) * length
            normal = self.angleToVector(angle + self.getArcDirection(obj)) * length

            # prepend the selected lead-in
            if obj.StyleOn == "Arc":
                arcbegin = begin + tangent + normal
                prepend(self.createArcMove(obj, arcbegin, begin, -tangent))
            elif obj.StyleOn == "Tangent":
                prepend(self.createStraightMove(obj, begin + tangent, begin))
            else:  # obj.StyleOn == "Perpendicular"
                prepend(self.createStraightMove(obj, begin + normal, begin))

            extend = obj.ExtendLeadIn.Value
            if extend != 0:
                # prepend extension
                extendbegin = begin + normal / length * extend
                prepend(self.createStraightMove(obj, extendbegin, begin))

        # prepend travel moves
        lead = self.getTravelStart(obj, begin, first) + lead

        return lead

    def getLeadEnd(self, obj, move, last):
        lead = []
        end = move.positionEnd()

        def append(instr):
            nonlocal lead
            nonlocal end
            lead.append(instr)
            end = lead[-1].positionEnd()

        #            move       end   tangent
        #    x-------------------x-----_---->
        #                        |       \
        #                 normal |         \
        #                        |          |
        #                        v          x

        if obj.LeadOut:
            length = obj.LengthOut.Value
            angle = move.anglesOfTangents()[1]
            tangent = self.angleToVector(angle) * length
            normal = self.angleToVector(angle + self.getArcDirection(obj)) * length

            # append the selected lead-out
            if obj.StyleOff == "Arc":
                arcend = end + tangent + normal
                append(self.createArcMove(obj, end, arcend, normal))
            elif obj.StyleOff == "Tangent":
                append(self.createStraightMove(obj, end, end + tangent))
            else:  # obj.StyleOff == "Perpendicular"
                append(self.createStraightMove(obj, end, end + normal))

            extend = obj.ExtendLeadOut.Value
            if extend != 0:
                # append extension
                extendend = end + normal / length * extend
                append(self.createStraightMove(obj, end, extendend))

        # append travel moves
        lead += self.getTravelEnd(obj, end, last)

        return lead

    def isCuttingMove(self, obj, instr):
        return (
            instr.isMove()
            and not instr.isRapid()
            and (not obj.IncludeLayers or not instr.isPlunge())
        )

    def findLastCuttingMoveIndex(self, obj, source):
        for i in range(len(source) - 1, -1, -1):
            if self.isCuttingMove(obj, source[i]):
                return i
        return None

    def generateLeadInOutCurve(self, obj):
        source = PathLanguage.Maneuver.FromPath(PathUtils.getPathWithPlacement(obj.Base)).instr
        maneuver = PathLanguage.Maneuver()

        # Knowing weather a given instruction is the first cutting move is easy,
        # we just use a flag and set it to false afterwards. To find the last
        # cutting move we need to search the list in reverse order.
        first = True
        lastCuttingMoveIndex = self.findLastCuttingMoveIndex(obj, source)

        for i, instr in enumerate(source):
            if not self.isCuttingMove(obj, instr):
                # non-move instructions get added verbatim
                if not instr.isMove():
                    maneuver.addInstruction(instr)

                # skip travel and plunge moves, travel moves will be added in
                # getLeadStart and getLeadEnd
                continue

            if first or not self.isCuttingMove(obj, source[i - 1]):
                # add lead start and travel moves
                maneuver.addInstructions(self.getLeadStart(obj, instr, first))
                first = False

            # add current move
            maneuver.addInstruction(instr)

            last = i == lastCuttingMoveIndex
            if last or not self.isCuttingMove(obj, source[i + 1]):
                # add lead end and travel moves
                maneuver.addInstructions(self.getLeadEnd(obj, instr, last))

        return maneuver.toPath()


class TaskDressupLeadInOut(SimpleEditPanel):
    _transaction_name = "Edit LeadInOut Dress-up"
    _ui_file = ":/panels/DressUpLeadInOutEdit.ui"

    def setupUi(self):
        self.connectWidget("LeadIn", self.form.chkLeadIn)
        self.connectWidget("LeadOut", self.form.chkLeadOut)
        self.connectWidget("Length", self.form.dspLenIn)
        self.connectWidget("LengthOut", self.form.dspLenOut)
        self.connectWidget("ExtendLeadIn", self.form.dspExtendIn)
        self.connectWidget("ExtendLeadOut", self.form.dspExtendOut)
        self.connectWidget("StyleOn", self.form.cboStyleIn)
        self.connectWidget("StyleOff", self.form.cboStyleOut)
        self.connectWidget("RapidPlunge", self.form.chkRapidPlunge)
        self.connectWidget("IncludeLayers", self.form.chkLayers)
        self.connectWidget("KeepToolDown", self.form.chkKeepToolDown)
        self.setFields()


class ViewProviderDressup:
    def __init__(self, vobj):
        self.obj = vobj.Object
        self.setEdit(vobj)

    def attach(self, vobj):
        self.obj = vobj.Object
        self.panel = None

    def claimChildren(self):
        if hasattr(self.obj.Base, "InList"):
            for i in self.obj.Base.InList:
                if hasattr(i, "Group"):
                    group = i.Group
                    for g in group:
                        if g.Name == self.obj.Base.Name:
                            group.remove(g)
                    i.Group = group
        return [self.obj.Base]

    def setEdit(self, vobj, mode=0):
        FreeCADGui.Control.closeDialog()
        panel = TaskDressupLeadInOut(vobj.Object, self)
        FreeCADGui.Control.showDialog(panel)
        return True

    def unsetEdit(self, vobj, mode=0):
        if self.panel:
            self.panel.abort()

    def onDelete(self, arg1=None, arg2=None):
        """this makes sure that the base operation is added back to the project and visible"""
        Path.Log.debug("Deleting Dressup")
        if arg1.Object and arg1.Object.Base:
            FreeCADGui.ActiveDocument.getObject(arg1.Object.Base.Name).Visibility = True
            job = PathUtils.findParentJob(self.obj)
            if job:
                job.Proxy.addOperation(arg1.Object.Base, arg1.Object)
            arg1.Object.Base = None
        return True

    def dumps(self):
        return None

    def loads(self, state):
        return None

    def clearTaskPanel(self):
        self.panel = None


class CommandPathDressupLeadInOut:
    def GetResources(self):
        return {
            "Pixmap": "CAM_Dressup",
            "MenuText": QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "LeadInOut"),
            "ToolTip": QT_TRANSLATE_NOOP(
                "CAM_DressupLeadInOut",
                "Creates a Cutter Radius Compensation G41/G42 Entry Dressup object from a selected path",
            ),
        }

    def IsActive(self):
        op = PathDressup.selection()
        if op:
            return not PathDressup.hasEntryMethod(op)
        return False

    def Activated(self):
        # check that the selection contains exactly what we want
        selection = FreeCADGui.Selection.getSelection()
        if len(selection) != 1:
            Path.Log.error(
                translate("CAM_DressupLeadInOut", "Please select one toolpath object") + "\n"
            )
            return
        baseObject = selection[0]
        if not baseObject.isDerivedFrom("Path::Feature"):
            Path.Log.error(
                translate("CAM_DressupLeadInOut", "The selected object is not a toolpath") + "\n"
            )
            return
        if baseObject.isDerivedFrom("Path::FeatureCompoundPython"):
            Path.Log.error(translate("CAM_DressupLeadInOut", "Please select a Profile object"))
            return

        # everything ok!
        App.ActiveDocument.openTransaction("Create LeadInOut Dressup")
        FreeCADGui.addModule("Path.Dressup.Gui.LeadInOut")
        FreeCADGui.addModule("PathScripts.PathUtils")
        FreeCADGui.doCommand(
            'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "LeadInOutDressup")'
        )
        FreeCADGui.doCommand("dbo = Path.Dressup.Gui.LeadInOut.ObjectDressup(obj)")
        FreeCADGui.doCommand("base = FreeCAD.ActiveDocument." + selection[0].Name)
        FreeCADGui.doCommand("job = PathScripts.PathUtils.findParentJob(base)")
        FreeCADGui.doCommand("obj.Base = base")
        FreeCADGui.doCommand("job.Proxy.addOperation(obj, base)")
        FreeCADGui.doCommand("dbo.setup(obj)")
        FreeCADGui.doCommand(
            "obj.ViewObject.Proxy = Path.Dressup.Gui.LeadInOut.ViewProviderDressup(obj.ViewObject)"
        )
        FreeCADGui.doCommand("Gui.ActiveDocument.getObject(base.Name).Visibility = False")
        App.ActiveDocument.commitTransaction()
        App.ActiveDocument.recompute()


if App.GuiUp:
    # register the FreeCAD command
    FreeCADGui.addCommand("CAM_DressupLeadInOut", CommandPathDressupLeadInOut())

Path.Log.notice("Loading CAM_DressupLeadInOut... done\n")
