# -*- coding: utf-8 -*-

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2016 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 __future__ import print_function

import FreeCAD
import MeshPart
# import Part
import Path
import PathScripts.PathLog as PathLog
# import PathScripts.PathPocketBase as PathPocketBase
import PathScripts.PathUtils as PathUtils
import PathScripts.PathOp as PathOp

from PySide import QtCore

import sys
if sys.version_info.major >= 3:
    xrange = range

__title__ = "Path Surface Operation"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Class and implementation of Mill Facing operation."


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


# Qt tanslation handling
def translate(context, text, disambig=None):
    return QtCore.QCoreApplication.translate(context, text, disambig)


class ObjectSurface(PathOp.ObjectOp):
    '''Proxy object for Surfacing operation.'''
    def baseObject(self):
        '''baseObject() ... returns super of receiver
        Used to call base implementation in overwritten functions.'''
        return super(self.__class__, self)

    # def OpFeatures(self, obj):
    #     '''areaOpFeatures(obj) ... returns 0, Contour only requires the base profile features.'''
    #     return 0
    def opFeatures(self, obj):
        '''opFeatures(obj) ... return all standard features and edges based geomtries'''
        return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown

    def initOperation(self, obj):
        '''initPocketOp(obj) ... create facing specific properties'''
        obj.addProperty("App::PropertyEnumeration", "Algorithm", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The library to use to generate the path"))
        obj.addProperty("App::PropertyEnumeration", "DropCutterDir", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created"))
        obj.addProperty("App::PropertyEnumeration", "BoundBox", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Should the operation be limited by the stock object or by the bounding box of the base object"))
        obj.addProperty("App::PropertyVectorDistance", "DropCutterExtraOffset", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box"))
        obj.addProperty("App::PropertyPercent", "StepOver", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Step over percentage of the drop cutter path"))
        obj.addProperty("App::PropertyDistance", "DepthOffset", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Z-axis offset from the surface of the object"))
        obj.addProperty("App::PropertyFloatConstraint", "SampleInterval", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times"))
        obj.addProperty("App::PropertyBool", "Optimize", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization which removes unnecessary points from G-Code output"))
        obj.BoundBox = ['Stock', 'BaseBoundBox']
        obj.DropCutterDir = ['X', 'Y']
        obj.Algorithm = ['OCL Dropcutter', 'OCL Waterline']
        obj.SampleInterval = (0.04, 0.01, 1.0, 0.01)

        if not hasattr(obj, 'DoNotSetDefaultValues'):
            self.setEditorProperties(obj)

    def setEditorProperties(self, obj):
        if obj.Algorithm == 'OCL Dropcutter':
            obj.setEditorMode('DropCutterDir', 0)
            obj.setEditorMode('DropCutterExtraOffset', 0)
        else:
            obj.setEditorMode('DropCutterDir', 2)
            obj.setEditorMode('DropCutterExtraOffset', 2)

    def onChanged(self, obj, prop):
        if prop == "Algorithm":
            self.setEditorProperties(obj)

    def opOnDocumentRestored(self, obj):
        self.setEditorProperties(obj)

    def opExecute(self, obj):
        '''opExecute(obj) ... process surface operation'''
        PathLog.track()

        # OCL must be installed
        try:
            import ocl
        except:
            FreeCAD.Console.PrintError(
                translate("Path_Surface", "This operation requires OpenCamLib to be installed.") + "\n")
            return

        print("StepOver is  " + str(obj.StepOver))
        if obj.StepOver > 100:
            obj.StepOver = 100
        if obj.StepOver < 1:
            obj.StepOver = 1
        output = ""
        if obj.Comment != "":
            output += '(' + str(obj.Comment) + ')\n'

        output += "(" + obj.Label + ")"
        output += "(Compensated Tool Path. Diameter: " + str(obj.ToolController.Tool.Diameter) + ")"

        parentJob = PathUtils.findParentJob(obj)
        if parentJob is None:
            return

        for base in self.model:
            print("base object: " + base.Name)

            if base.TypeId.startswith('Mesh'):
                mesh = base.Mesh
            else:
                # try/except is for Path Jobs created before GeometryTolerance
                try:
                    deflection = parentJob.GeometryTolerance
                except AttributeError:
                    import PathScripts.PathPreferences as PathPreferences
                    deflection = PathPreferences.defaultGeometryTolerance()
                base.Shape.tessellate(0.5)
                mesh = MeshPart.meshFromShape(base.Shape, Deflection=deflection)
            if obj.BoundBox == "BaseBoundBox":
                bb = mesh.BoundBox
            else:
                bb = parentJob.Stock.Shape.BoundBox

            s = ocl.STLSurf()
            for f in mesh.Facets:
                p = f.Points[0]
                q = f.Points[1]
                r = f.Points[2]
                # offset the triangle in Z with DepthOffset
                t = ocl.Triangle(ocl.Point(p[0], p[1], p[2] + obj.DepthOffset.Value),
                                 ocl.Point(q[0], q[1], q[2] + obj.DepthOffset.Value),
                                 ocl.Point(r[0], r[1], r[2] + obj.DepthOffset.Value))
                s.addTriangle(t)

            if obj.Algorithm == 'OCL Dropcutter':
                output = self._dropcutter(obj, s, bb)
            elif obj.Algorithm == 'OCL Waterline':
                output = self._waterline(obj, s, bb)

            self.commandlist.extend(output)

    def _waterline(self, obj, s, bb):
        import time
        import ocl

        def drawLoops(loops):
            nloop = 0
            pp = []
            pp.append(Path.Command("(waterline begin)"))

            for loop in loops:
                p = loop[0]
                pp.append(Path.Command("(loop begin)"))
                pp.append(Path.Command('G0', {"Z": obj.SafeHeight.Value, 'F': self.vertRapid}))
                pp.append(Path.Command('G0', {'X': p.x, "Y": p.y, 'F': self.horizRapid}))
                pp.append(Path.Command('G1', {"Z": p.z, 'F': self.vertFeed}))
                prev = ocl.Point(float("inf"), float("inf"), float("inf"))
                next = ocl.Point(float("inf"), float("inf"), float("inf"))
                optimize = obj.Optimize
                for i in range(1, len(loop)):
                    p = loop[i]
                    if i < len(loop) - 1:
                        next.x = loop[i + 1].x
                        next.y = loop[i + 1].y
                        next.z = loop[i + 1].z
                    else:
                        optimize = False
                    if not optimize or not self.isPointOnLine(FreeCAD.Vector(prev.x, prev.y, prev.z), FreeCAD.Vector(next.x, next.y, next.z), FreeCAD.Vector(p.x, p.y, p.z)):
                        pp.append(Path.Command('G1', {'X': p.x, "Y": p.y, "Z": p.z, 'F': self.horizFeed}))
                    prev.x = p.x
                    prev.y = p.y
                    prev.z = p.z
                    # zheight = p.z
                p = loop[0]
                pp.append(Path.Command('G1', {'X': p.x, "Y": p.y, "Z": p.z, 'F': self.horizFeed}))
                pp.append(Path.Command("(loop end)"))

                print("    loop ", nloop, " with ", len(loop), " points")
                nloop = nloop + 1
            pp.append(Path.Command("(waterline end)"))

            return pp

        depthparams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value,
                                             obj.StartDepth.Value, obj.StepDown, 0.0, obj.FinalDepth.Value)

        t_before = time.time()
        zheights = [i for i in depthparams]

        wl = ocl.Waterline()
        wl.setSTL(s)

        if obj.ToolController.Tool.ToolType == 'BallEndMill':
            cutter = ocl.BallCutter(obj.ToolController.Tool.Diameter, 5)  # TODO: 5 represents cutting edge height. Should be replaced with the data from toolcontroller?
        else:
            cutter = ocl.CylCutter(obj.ToolController.Tool.Diameter, 5)

        wl.setCutter(cutter)
        # this should be smaller than the smallest details in the STL file
        wl.setSampling(obj.SampleInterval)
        # AdaptiveWaterline() also has settings for minimum sampling interval
        # (see c++ code)
        all_loops = []
        print ("zheights: {}".format(zheights))
        for zh in zheights:
            print("calculating Waterline at z= ", zh)
            wl.reset()
            wl.setZ(zh)  # height for this waterline
            wl.run()
            all_loops.append(wl.getLoops())
        t_after = time.time()
        calctime = t_after - t_before
        n = 0
        output = []
        for loops in all_loops:  # at each z-height, we may get many loops
            print("  %d/%d:" % (n, len(all_loops)))
            output.extend(drawLoops(loops))
            n = n + 1
        print("(" + str(calctime) + ")")
        return output

    def isPointOnLine(self, lineA, lineB, pointP):
        tolerance = 1e-6
        vectorAB = lineB - lineA
        vectorAC = pointP - lineA
        crossproduct = vectorAB.cross(vectorAC)
        dotproduct = vectorAB.dot(vectorAC)

        if crossproduct.Length > tolerance:
            return False

        if dotproduct < 0:
            return False

        if dotproduct > vectorAB.Length * vectorAB.Length:
            return False

        return True

    def _dropcutter(self, obj, s, bb):
        import ocl
        import time
        if obj.ToolController.Tool.ToolType == 'BallEndMill':
            cutter = ocl.BallCutter(obj.ToolController.Tool.Diameter, 5)  # TODO: 5 represents cutting edge height. Should be replaced with the data from toolcontroller?
        else:
            cutter = ocl.CylCutter(obj.ToolController.Tool.Diameter, 5)

        pdc = ocl.PathDropCutter()   # create a pdc
        pdc.setSTL(s)
        pdc.setCutter(cutter)
        pdc.setZ(obj.FinalDepth.Value + obj.DepthOffset.Value)  # set minimumZ
        pdc.setSampling(obj.SampleInterval)

        # the max and min XY area of the operation
        xmin = bb.XMin - obj.DropCutterExtraOffset.x
        xmax = bb.XMax + obj.DropCutterExtraOffset.x
        ymin = bb.YMin - obj.DropCutterExtraOffset.y
        ymax = bb.YMax + obj.DropCutterExtraOffset.y

        path = ocl.Path()                   # create an empty path object

        if obj.DropCutterDir == 'Y':
            Ny = int(bb.YLength / (cutter.getDiameter() * (obj.StepOver / 100.0)))
            dy = float(ymax - ymin) / Ny  # the y step-over

            # add Line objects to the path in this loop
            for n in xrange(0, Ny):
                y = ymin + n * dy
                p1 = ocl.Point(xmin, y, 0)   # start-point of line
                p2 = ocl.Point(xmax, y, 0)   # end-point of line
                if (n % 2 == 0):  # even
                    l = ocl.Line(p1, p2)     # line-object
                else:  # odd
                    l = ocl.Line(p2, p1)     # line-object

                path.append(l)        # add the line to the path
        else:
            Nx = int(bb.XLength / (cutter.getDiameter() * (obj.StepOver / 100.0)))
            dx = float(xmax - xmin) / Nx  # the y step-over

            # add Line objects to the path in this loop
            for n in xrange(0, Nx):
                x = xmin + n * dx
                p1 = ocl.Point(x, ymin, 0)   # start-point of line
                p2 = ocl.Point(x, ymax, 0)   # end-point of line
                if (n % 2 == 0):  # even
                    l = ocl.Line(p1, p2)     # line-object
                else:  # odd
                    l = ocl.Line(p2, p1)     # line-object

                path.append(l)        # add the line to the path

        pdc.setPath(path)

        # run drop-cutter on the path
        t_before = time.time()
        pdc.run()
        t_after = time.time()
        print("calculation took ", t_after - t_before, " s")

        # retrieve the points
        clp = pdc.getCLPoints()
        print("points received: " + str(len(clp)))

        # generate the path commands
        output = []
        output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
        output.append(Path.Command('G0', {'X': clp[0].x, "Y": clp[0].y, 'F': self.horizRapid}))
        output.append(Path.Command('G1', {'Z': clp[0].z, 'F': self.vertFeed}))
        prev = ocl.Point(float("inf"), float("inf"), float("inf"))
        next = ocl.Point(float("inf"), float("inf"), float("inf"))
        optimize = obj.Optimize
        for i in range(0, len(clp)):
            c = clp[i]
            if i < len(clp) - 1:
                next.x = clp[i + 1].x
                next.y = clp[i + 1].y
                next.z = clp[i + 1].z
            else:
                optimize = False

            if not optimize or not self.isPointOnLine(FreeCAD.Vector(prev.x, prev.y, prev.z), FreeCAD.Vector(next.x, next.y, next.z), FreeCAD.Vector(c.x, c.y, c.z)):
                output.append(Path.Command('G1', {'X': c.x, "Y": c.y, "Z": c.z, 'F': self.horizFeed}))
            prev.x = c.x
            prev.y = c.y
            prev.z = c.z
        print("points after optimization: " + str(len(output)))
        return output

    def pocketInvertExtraOffset(self):
        return True

    def opSetDefaultValues(self, obj, job):
        '''opSetDefaultValues(obj, job) ... initialize defaults'''

        # obj.ZigZagAngle = 45.0
        obj.StepOver = 50
        obj.Optimize = True
        # need to overwrite the default depth calculations for facing
        job = PathUtils.findParentJob(obj)
        if job and job.Stock:
            d = PathUtils.guessDepths(job.Stock.Shape, None)
            obj.OpStartDepth = d.start_depth
            obj.OpFinalDepth = d.final_depth


def SetupProperties():
    setup = []
    setup.append("Algorithm")
    setup.append("DropCutterDir")
    setup.append("BoundBox")
    setup.append("StepOver")
    setup.append("DepthOffset")
    return setup


def Create(name, obj=None):
    '''Create(name) ... Creates and returns a Surface operation.'''
    if obj is None:
        obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
    proxy = ObjectSurface(obj, name)
    return obj
