# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# *   Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com>               *
# *                                                                         *
# *   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 math

__title__ = "PathLanguage - classes for an internal language/representation for Path"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecad.org"
__doc__ = "Functions to extract and convert between Path.Command and Part.Edge and utility functions to reason about them."

CmdMoveStraight = Path.Geom.CmdMoveStraight + Path.Geom.CmdMoveRapid


class Instruction(object):
    """An Instruction is a pure python replacement of Path.Command which also tracks its begin position."""

    def __init__(self, begin, cmd, param=None):
        self.begin = begin
        if isinstance(cmd, Path.Command):
            self.cmd = Path.Name
            self.param = Path.Parameters
        else:
            self.cmd = cmd
            if param is None:
                self.param = {}
            else:
                self.param = param

    def anglesOfTangents(self):
        return (0, 0)

    def setPositionBegin(self, begin):
        self.begin = begin

    def positionBegin(self):
        """positionBegin() ... returns a Vector of the begin position"""
        return self.begin

    def positionEnd(self):
        """positionEnd() ... returns a Vector of the end position"""
        return FreeCAD.Vector(self.x(self.begin.x), self.y(self.begin.y), self.z(self.begin.z))

    def pathLength(self):
        """pathLength() ... returns the length in mm"""
        return 0

    def isMove(self):
        return False

    def isRapid(self):
        return False

    def isPlunge(self):
        """isPlunge() ... return true if this moves is vertical"""
        if self.isMove():
            if (
                Path.Geom.isRoughly(self.begin.x, self.x(self.begin.x))
                and Path.Geom.isRoughly(self.begin.y, self.y(self.begin.y))
                and not Path.Geom.isRoughly(self.begin.z, self.z(self.begin.z))
            ):
                return True
        return False

    def leadsInto(self, instr):
        """leadsInto(instr) ... return true if instr is a continuation of self"""
        return Path.Geom.pointsCoincide(self.positionEnd(), instr.positionBegin())

    def x(self, default=0):
        return self.param.get("X", default)

    def y(self, default=0):
        return self.param.get("Y", default)

    def z(self, default=0):
        return self.param.get("Z", default)

    def i(self, default=0):
        return self.param.get("I", default)

    def j(self, default=0):
        return self.param.get("J", default)

    def k(self, default=0):
        return self.param.get("K", default)

    def xyBegin(self):
        """xyBegin() ... internal convenience function"""
        return FreeCAD.Vector(self.begin.x, self.begin.y, 0)

    def xyEnd(self):
        """xyEnd() ... internal convenience function"""
        return FreeCAD.Vector(self.x(self.begin.x), self.y(self.begin.y), 0)

    def __repr__(self):
        return f"{self.cmd}{self.param}"

    def str(self, digits=2):
        if digits == 0:
            s = [f"{k}: {int(v)}" for k, v in self.param.items()]
        else:
            fmt = f"{{}}: {{:.{digits}}}"
            s = [fmt.format(k, v) for k, v in self.param.items()]
        return f"{self.cmd}{{{', '.join(s)}}}"


class MoveStraight(Instruction):
    def anglesOfTangents(self):
        """anglesOfTangents() ... return a tuple with the tangent angles at begin and end position"""
        begin = self.xyBegin()
        end = self.xyEnd()
        if end == begin:
            return (0, 0)
        a = Path.Geom.getAngle(end - begin)
        return (a, a)

    def isMove(self):
        return True

    def isStraight(self):
        return True

    def isArc(self):
        return False

    def isRapid(self):
        return self.cmd in Path.Geom.CmdMoveRapid

    def pathLength(self):
        return (self.positionEnd() - self.positionBegin()).Length


class MoveArc(Instruction):
    def anglesOfTangents(self):
        """anglesOfTangents() ... return a tuple with the tangent angles at begin and end position"""
        begin = self.xyBegin()
        end = self.xyEnd()
        center = self.xyCenter()
        # calculate angle of the hypotenuse at begin and end
        s0 = Path.Geom.getAngle(begin - center)
        s1 = Path.Geom.getAngle(end - center)
        # the tangents are perpendicular to the hypotenuse with the sign determined by the
        # direction of the arc
        return (
            Path.Geom.normalizeAngle(s0 + self.arcDirection()),
            Path.Geom.normalizeAngle(s1 + self.arcDirection()),
        )

    def isMove(self):
        return True

    def isArc(self):
        return True

    def isStraight(self):
        return False

    def isCW(self):
        return self.arcDirection() < 0

    def isCCW(self):
        return self.arcDirection() > 0

    def arcAngle(self):
        """arcAngle() ... return the angle of the arc opening"""
        begin = self.xyBegin()
        end = self.xyEnd()
        center = self.xyCenter()
        s0 = Path.Geom.getAngle(begin - center)
        s1 = Path.Geom.getAngle(end - center)

        if self.isCW():
            while s0 < s1:
                s0 = s0 + 2 * math.pi
            return s0 - s1

        # CCW
        while s1 < s0:
            s1 = s1 + 2 * math.pi
        return s1 - s0

    def arcRadius(self):
        """arcRadius() ... return the radius"""
        return (self.xyBegin() - self.xyCenter()).Length

    def pathLength(self):
        return self.arcAngle() * self.arcRadius()

    def xyCenter(self):
        return FreeCAD.Vector(self.begin.x + self.i(), self.begin.y + self.j(), 0)


class MoveArcCW(MoveArc):
    def arcDirection(self):
        return -math.pi / 2


class MoveArcCCW(MoveArc):
    def arcDirection(self):
        return math.pi / 2


class Maneuver(object):
    """A series of instructions and moves"""

    def __init__(self, begin=None, instr=None):
        self.instr = instr if instr else []
        self.setPositionBegin(begin if begin else FreeCAD.Vector(0, 0, 0))

    def setPositionBegin(self, begin):
        self.begin = begin
        for i in self.instr:
            i.setPositionBegin(begin)
            begin = i.positionEnd()

    def positionBegin(self):
        return self.begin

    def getMoves(self):
        return [instr for instr in self.instr if instr.isMove()]

    def addInstruction(self, instr):
        self.instr.append(instr)

    def addInstructions(self, coll):
        self.instr.extend(coll)

    def toPath(self):
        return Path.Path([instruction_to_command(instr) for instr in self.instr])

    def __repr__(self):
        if self.instr:
            return "\n".join([str(i) for i in self.instr])
        return ""

    @classmethod
    def InstructionFromCommand(cls, cmd, begin=None):
        if not begin:
            begin = FreeCAD.Vector(0, 0, 0)

        if cmd.Name in CmdMoveStraight:
            return MoveStraight(begin, cmd.Name, cmd.Parameters)
        if cmd.Name in Path.Geom.CmdMoveCW:
            return MoveArcCW(begin, cmd.Name, cmd.Parameters)
        if cmd.Name in Path.Geom.CmdMoveCCW:
            return MoveArcCCW(begin, cmd.Name, cmd.Parameters)
        return Instruction(begin, cmd.Name, cmd.Parameters)

    @classmethod
    def FromPath(cls, path, begin=None):
        maneuver = Maneuver(begin)
        instr = []
        begin = maneuver.positionBegin()
        for cmd in path.Commands:
            i = cls.InstructionFromCommand(cmd, begin)
            instr.append(i)
            begin = i.positionEnd()
        maneuver.instr = instr
        return maneuver

    @classmethod
    def FromGCode(cls, gcode, begin=None):
        return cls.FromPath(Path.Path(gcode), begin)


def instruction_to_command(instr):
    return Path.Command(instr.cmd, instr.param)
