# -*- coding: utf-8 -*-
# ***************************************************************************
# *   Copyright (c) 2019 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                                                                   *
# *                                                                         *
# ***************************************************************************

from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import Path
import PathScripts.PathDressup as PathDressup
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathStock as PathStock
import PathScripts.PathUtil as PathUtil
import PathScripts.PathUtils as PathUtils

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


translate = FreeCAD.Qt.translate


def _vstr(v):
    if v:
        return "(%.2f, %.2f, %.2f)" % (v.x, v.y, v.z)
    return "-"


class DressupPathBoundary(object):
    def __init__(self, obj, base, job):
        obj.addProperty(
            "App::PropertyLink",
            "Base",
            "Base",
            QT_TRANSLATE_NOOP("App::Property", "The base path to modify"),
        )
        obj.Base = base
        obj.addProperty(
            "App::PropertyLink",
            "Stock",
            "Boundary",
            QT_TRANSLATE_NOOP(
                "App::Property",
                "Solid object to be used to limit the generated Path.",
            ),
        )
        obj.Stock = PathStock.CreateFromBase(job)
        obj.addProperty(
            "App::PropertyBool",
            "Inside",
            "Boundary",
            QT_TRANSLATE_NOOP(
                "App::Property",
                "Determines if Boundary describes an inclusion or exclusion mask.",
            ),
        )
        obj.Inside = True

        self.obj = obj
        self.safeHeight = None
        self.clearanceHeight = None

    def __getstate__(self):
        return None

    def __setstate__(self, state):
        return None

    def onDocumentRestored(self, obj):
        self.obj = obj

    def onDelete(self, obj, args):
        if obj.Base:
            job = PathUtils.findParentJob(obj)
            if job:
                job.Proxy.addOperation(obj.Base, obj)
            if obj.Base.ViewObject:
                obj.Base.ViewObject.Visibility = True
            obj.Base = None
        if obj.Stock:
            obj.Document.removeObject(obj.Stock.Name)
            obj.Stock = None
        return True

    def execute(self, obj):
        pb = PathBoundary(obj.Base, obj.Stock.Shape, obj.Inside)
        obj.Path = pb.execute()


# Eclass


class PathBoundary:
    """class PathBoundary...
    This class requires a base operation, boundary shape, and optional inside boolean (default is True).
    The `execute()` method returns a Path object with path commands limited to cut paths inside or outside
    the provided boundary shape.
    """

    def __init__(self, baseOp, boundaryShape, inside=True):
        self.baseOp = baseOp
        self.boundary = boundaryShape
        self.inside = inside
        self.safeHeight = None
        self.clearanceHeight = None
        self.strG0ZsafeHeight = None
        self.strG0ZclearanceHeight = None

    def boundaryCommands(self, begin, end, verticalFeed):
        PathLog.track(_vstr(begin), _vstr(end))
        if end and PathGeom.pointsCoincide(begin, end):
            return []
        cmds = []
        if begin.z < self.safeHeight:
            cmds.append(self.strG0ZsafeHeight)
        if begin.z < self.clearanceHeight:
            cmds.append(self.strG0ZclearanceHeight)
        if end:
            cmds.append(Path.Command("G0", {"X": end.x, "Y": end.y}))
            if end.z < self.clearanceHeight:
                cmds.append(Path.Command("G0", {"Z": max(self.safeHeight, end.z)}))
            if end.z < self.safeHeight:
                cmds.append(Path.Command("G1", {"Z": end.z, "F": verticalFeed}))
        return cmds

    def execute(self):
        if (
            not self.baseOp
            or not self.baseOp.isDerivedFrom("Path::Feature")
            or not self.baseOp.Path
        ):
            return None

        if len(self.baseOp.Path.Commands) == 0:
            PathLog.warning("No Path Commands for %s" % self.baseOp.Label)
            return []

        tc = PathDressup.toolController(self.baseOp)

        self.safeHeight = float(PathUtil.opProperty(self.baseOp, "SafeHeight"))
        self.clearanceHeight = float(
            PathUtil.opProperty(self.baseOp, "ClearanceHeight")
        )
        self.strG0ZsafeHeight = Path.Command(  # was a Feed rate with G1
            "G0", {"Z": self.safeHeight, "F": tc.VertRapid.Value}
        )
        self.strG0ZclearanceHeight = Path.Command("G0", {"Z": self.clearanceHeight})

        cmd = self.baseOp.Path.Commands[0]
        pos = cmd.Placement.Base  # bogus m/c position to create first edge
        bogusX = True
        bogusY = True
        commands = [cmd]
        lastExit = None
        for cmd in self.baseOp.Path.Commands[1:]:
            if cmd.Name in PathGeom.CmdMoveAll:
                if bogusX:
                    bogusX = "X" not in cmd.Parameters
                if bogusY:
                    bogusY = "Y" not in cmd.Parameters
                edge = PathGeom.edgeForCmd(cmd, pos)
                if edge:
                    inside = edge.common(self.boundary).Edges
                    outside = edge.cut(self.boundary).Edges
                    if not self.inside:  # UI "inside boundary" param
                        tmp = inside
                        inside = outside
                        outside = tmp
                    # it's really a shame that one cannot trust the sequence and/or
                    # orientation of edges
                    if 1 == len(inside) and 0 == len(outside):
                        PathLog.track(_vstr(pos), _vstr(lastExit), " + ", cmd)
                        # cmd fully included by boundary
                        if lastExit:
                            if not (
                                bogusX or bogusY
                            ):  # don't insert false paths based on bogus m/c position
                                commands.extend(
                                    self.boundaryCommands(
                                        lastExit, pos, tc.VertFeed.Value
                                    )
                                )
                            lastExit = None
                        commands.append(cmd)
                        pos = PathGeom.commandEndPoint(cmd, pos)
                    elif 0 == len(inside) and 1 == len(outside):
                        PathLog.track(_vstr(pos), _vstr(lastExit), " - ", cmd)
                        # cmd fully excluded by boundary
                        if not lastExit:
                            lastExit = pos
                        pos = PathGeom.commandEndPoint(cmd, pos)
                    else:
                        PathLog.track(
                            _vstr(pos), _vstr(lastExit), len(inside), len(outside), cmd
                        )
                        # cmd pierces boundary
                        while inside or outside:
                            ie = [e for e in inside if PathGeom.edgeConnectsTo(e, pos)]
                            PathLog.track(ie)
                            if ie:
                                e = ie[0]
                                LastPt = e.valueAt(e.LastParameter)
                                flip = PathGeom.pointsCoincide(pos, LastPt)
                                newPos = e.valueAt(e.FirstParameter) if flip else LastPt
                                # inside edges are taken at this point (see swap of inside/outside
                                # above - so we can just connect the dots ...
                                if lastExit:
                                    if not (bogusX or bogusY):
                                        commands.extend(
                                            self.boundaryCommands(
                                                lastExit, pos, tc.VertFeed.Value
                                            )
                                        )
                                    lastExit = None
                                PathLog.track(e, flip)
                                if not (
                                    bogusX or bogusY
                                ):  # don't insert false paths based on bogus m/c position
                                    commands.extend(
                                        PathGeom.cmdsForEdge(
                                            e,
                                            flip,
                                            False,
                                            50,
                                            tc.HorizFeed.Value,
                                            tc.VertFeed.Value,
                                        )
                                    )
                                inside.remove(e)
                                pos = newPos
                                lastExit = newPos
                            else:
                                oe = [
                                    e
                                    for e in outside
                                    if PathGeom.edgeConnectsTo(e, pos)
                                ]
                                PathLog.track(oe)
                                if oe:
                                    e = oe[0]
                                    ptL = e.valueAt(e.LastParameter)
                                    flip = PathGeom.pointsCoincide(pos, ptL)
                                    newPos = (
                                        e.valueAt(e.FirstParameter) if flip else ptL
                                    )
                                    # outside edges are never taken at this point (see swap of
                                    # inside/outside above) - so just move along ...
                                    outside.remove(e)
                                    pos = newPos
                                else:
                                    PathLog.error("huh?")
                                    import Part

                                    Part.show(Part.Vertex(pos), "pos")
                                    for e in inside:
                                        Part.show(e, "ei")
                                    for e in outside:
                                        Part.show(e, "eo")
                                    raise Exception("This is not supposed to happen")
                                # Eif
                            # Eif
                        # Ewhile
                    # Eif
                    # pos = PathGeom.commandEndPoint(cmd, pos)
                # Eif
            else:
                PathLog.track("no-move", cmd)
                commands.append(cmd)
        if lastExit:
            commands.extend(self.boundaryCommands(lastExit, None, tc.VertFeed.Value))
            lastExit = None

        PathLog.track(commands)
        return Path.Path(commands)


# Eclass


def Create(base, name="DressupPathBoundary"):
    """Create(base, name='DressupPathBoundary') ... creates a dressup limiting base's Path to a boundary."""

    if not base.isDerivedFrom("Path::Feature"):
        PathLog.error(
            translate("Path_DressupPathBoundary", "The selected object is not a path")
            + "\n"
        )
        return None

    obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
    job = PathUtils.findParentJob(base)
    obj.Proxy = DressupPathBoundary(obj, base, job)
    job.Proxy.addOperation(obj, base, True)
    return obj
