# -*- 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 __future__ import print_function

import FreeCAD
import Path
import PathScripts.PathCircularHoleBase as PathCircularHoleBase
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathOp as PathOp
import PathScripts.PathUtils as PathUtils
import math

from PySide import QtCore

__title__ = "Path Thread Milling Operation"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Path thread milling operation."

PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
#PathLog.trackModule(PathLog.thisModule())

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


def radiiInternal(majorDia, minorDia, toolDia, toolCrest = None):
    '''internlThreadRadius(majorDia, minorDia, toolDia, toolCrest) ... returns the maximum radius for thread.'''
    PathLog.track(majorDia, minorDia, toolDia, toolCrest)
    if toolCrest is None:
        toolCrest = 0.0
    # As it turns out metric and imperial standard threads follow the same rules.
    # The determining factor is the height of the full 60 degree triangle H.
    # - The minor diameter is 1/4 * H smaller than the pitch diameter.
    # - The major diameter is 3/8 * H bigger than the pitch diameter
    # Since we already have the outer diameter it's simpler to just add 1/8 * H
    # to get the outer tip of the thread.
    H = ((majorDia - minorDia) / 2.0 ) * 1.6             # (D - d)/2 = 5/8 * H
    outerTip = majorDia / 2.0 + H / 8.0
    # Compensate for the crest of the tool
    toolTip = outerTip - toolCrest * 0.8660254037844386  # math.sqrt(3)/2 ... 60deg triangle height
    return ((minorDia - toolDia) / 2.0, toolTip - toolDia / 2.0)

def threadPasses(count, radii, majorDia, minorDia, toolDia, toolCrest = None):
    PathLog.track(count, radii, majorDia, minorDia, toolDia, toolCrest)
    minor, major = radii(majorDia, minorDia, toolDia, toolCrest)
    dr  = float(major - minor) / count
    return [major - dr * (count - (i + 1)) for i in range(count)]

class _InternalThread(object):
    '''Helper class for dealing with different thread types'''
    def __init__(self, cmd, zStart, zFinal, pitch):
        self.cmd = cmd
        if zStart < zFinal:
            self.pitch = pitch
        else:
            self.pitch = -pitch
        self.hPitch = self.pitch / 2
        self.zStart = zStart
        self.zFinal = zFinal

    def overshoots(self, z):
        '''overshoots(z) ... returns true if adding another half helix goes beyond the thread bounds'''
        if self.pitch < 0:
            return z + self.hPitch < self.zFinal
        return z + self.hPitch > self.zFinal

    def adjustX(self, x, dx):
        '''adjustX(x, dx) ... move x by dx, the direction depends on the thread settings'''
        if self.isG3() == (self.pitch > 0):
            return x + dx
        return x - dx

    def adjustY(self, y, dy):
        '''adjustY(y, dy) ... move y by dy, the direction depends on the thread settings'''
        if self.isG3():
            return y - dy
        return y - dy

    def isG3(self):
        '''isG3() ... returns True if this is a G3 command'''
        return self.cmd in ['G3', 'G03', 'g3', 'g03']

    def isUp(self):
        '''isUp() ... returns True if the thread goes from the bottom up'''
        return self.pitch > 0

def internalThreadCommands(loc, cmd, zStart, zFinal, pitch, radius, leadInOut):
    '''internalThreadCommands(loc, cmd, zStart, zFinal, pitch, radius) ... returns the g-code to mill the given internal thread'''
    thread = _InternalThread(cmd, zStart, zFinal, pitch)

    yMin = loc.y - radius
    yMax = loc.y + radius

    path = []
    # at this point the tool is at a safe height (depending on the previous thread), so we can move
    # into position first, and then drop to the start height. If there is any material in the way this
    # op hasn't been setup properly.
    path.append(Path.Command('G0', {'X': loc.x, 'Y': loc.y}))
    path.append(Path.Command('G0', {'Z': thread.zStart}))
    if leadInOut:
        path.append(Path.Command(thread.cmd, {'Y': yMax, 'J': (yMax - loc.y) / 2}))
    else:
        path.append(Path.Command('G1', {'Y': yMax}))

    z = thread.zStart
    r = -radius
    i = 0
    while True:
        z = thread.zStart + i*thread.hPitch
        if thread.overshoots(z):
            break
        if 0 == (i & 0x01):
            y = yMin
        else:
            y = yMax
        path.append(Path.Command(thread.cmd, {'Y': y, 'Z': z + thread.hPitch, 'J': r}))
        r = -r
        i = i + 1


    z = thread.zStart + i*thread.hPitch
    if PathGeom.isRoughly(z, thread.zFinal):
        x = loc.x
    else:
        n = math.fabs(thread.zFinal - thread.zStart) / thread.hPitch
        k = n - int(n)
        dy = math.cos(k * math.pi)
        dx = math.sin(k * math.pi)
        y = thread.adjustY(loc.y, r * dy)
        x = thread.adjustX(loc.x, r * dx)
        path.append(Path.Command(thread.cmd, {'X': x, 'Y': y, 'Z': thread.zFinal, 'J': r}))

    if leadInOut:
        path.append(Path.Command(thread.cmd, {'X': loc.x, 'Y': loc.y, 'I': (loc.x - x) / 2, 'J': (loc.y - y) / 2})) 
    else:
        path.append(Path.Command('G1', {'X': loc.x, 'Y': loc.y}))
    return path

class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
    '''Proxy object for thread milling operation.'''

    LeftHand                    = 'LeftHand'
    RightHand                   = 'RightHand'
    ThreadTypeCustom            = 'Custom'
    ThreadTypeMetricInternal    = 'Metric - internal'
    ThreadTypeImperialInternal  = 'Imperial - internal'
    DirectionClimb              = 'Climb'
    DirectionConventional       = 'Conventional'

    ThreadOrientations = [LeftHand, RightHand]
    ThreadTypes = [ThreadTypeCustom, ThreadTypeMetricInternal, ThreadTypeImperialInternal]
    Directions =  [DirectionClimb, DirectionConventional]

    def circularHoleFeatures(self, obj):
        return PathOp.FeatureBaseGeometry

    def initCircularHoleOperation(self, obj):
        obj.addProperty("App::PropertyEnumeration", "ThreadOrientation", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread orientation"))
        obj.ThreadOrientation = self.ThreadOrientations
        obj.addProperty("App::PropertyEnumeration", "ThreadType", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Currently only internal"))
        obj.ThreadType = self.ThreadTypes
        obj.addProperty("App::PropertyString", "ThreadName", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Defines which standard thread was chosen"))
        obj.addProperty("App::PropertyLength", "MajorDiameter", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's major diameter"))
        obj.addProperty("App::PropertyLength", "MinorDiameter", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's minor diameter"))
        obj.addProperty("App::PropertyLength", "Pitch", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's pitch - used for metric threads"))
        obj.addProperty("App::PropertyInteger", "TPI", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's TPI (turns per inch) - used for imperial threads"))
        obj.addProperty("App::PropertyInteger", "ThreadFit", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set how many passes are used to cut the thread"))
        obj.addProperty("App::PropertyInteger", "Passes", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set how many passes are used to cut the thread"))
        obj.addProperty("App::PropertyEnumeration", "Direction", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Direction of thread cutting operation"))
        obj.addProperty("App::PropertyBool", "LeadInOut", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set to True to get lead in and lead out arcs at the start and end of the thread cut"))
        obj.addProperty("App::PropertyLink", "ClearanceOp", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Operation to clear the inside of the thread"))
        obj.Direction = self.Directions

        # Rotation related properties
        if not hasattr(obj, 'EnableRotation'):
            obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis."))
            obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B']


    def threadStartDepth(self, obj):
        if obj.ThreadOrientation == self.RightHand:
            if obj.Direction == self.DirectionClimb:
                PathLog.track(obj.Label, obj.FinalDepth)
                return obj.FinalDepth
            PathLog.track(obj.Label, obj.StartDepth)
            return obj.StartDepth
        if obj.Direction == self.DirectionClimb:
            PathLog.track(obj.Label, obj.StartDepth)
            return obj.StartDepth
        PathLog.track(obj.Label, obj.FinalDepth)
        return obj.FinalDepth

    def threadFinalDepth(self, obj):
        PathLog.track(obj.Label)
        if obj.ThreadOrientation == self.RightHand:
            if obj.Direction == self.DirectionClimb:
                PathLog.track(obj.Label, obj.StartDepth)
                return obj.StartDepth
            PathLog.track(obj.Label, obj.FinalDepth)
            return obj.FinalDepth
        if obj.Direction == self.DirectionClimb:
            PathLog.track(obj.Label, obj.FinalDepth)
            return obj.FinalDepth
        PathLog.track(obj.Label, obj.StartDepth)
        return obj.StartDepth

    def threadDirectionCmd(self, obj):
        PathLog.track(obj.Label)
        if obj.ThreadOrientation == self.RightHand:
            if obj.Direction == self.DirectionClimb:
                PathLog.track(obj.Label, 'G2')
                return 'G2'
            PathLog.track(obj.Label, 'G3')
            return 'G3'
        if obj.Direction == self.DirectionClimb:
            PathLog.track(obj.Label, 'G3')
            return 'G3'
        PathLog.track(obj.Label, 'G2')
        return 'G2'

    def threadSetup(self, obj):
        # the thing to remember is that Climb, for an internal thread must always be G3
        if obj.Direction == self.DirectionClimb:
            if obj.ThreadOrientation == self.RightHand:
                return ('G3', obj.FinalDepth.Value, obj.StartDepth.Value)
            return ('G3', obj.StartDepth.Value, obj.FinalDepth.Value)
        if obj.ThreadOrientation == self.RightHand:
            return ('G2', obj.StartDepth.Value, obj.FinalDepth.Value)
        return ('G2', obj.FinalDepth.Value, obj.StartDepth.Value)

    def threadPassRadii(self, obj):
        PathLog.track(obj.Label)
        rMajor = (obj.MajorDiameter.Value - self.tool.Diameter) / 2.0
        rMinor = (obj.MinorDiameter.Value - self.tool.Diameter) / 2.0
        if obj.Passes < 1:
            obj.Passes = 1
        rPass  = (rMajor - rMinor) / obj.Passes
        passes = [rMajor]
        for i in range(1, obj.Passes):
            passes.append(rMajor - rPass * i)
        return list(reversed(passes))

    def executeThreadMill(self, obj, loc, gcode, zStart, zFinal, pitch):
        PathLog.track(obj.Label, loc, gcode, zStart, zFinal, pitch)

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

        for radius in threadPasses(obj.Passes, radiiInternal, obj.MajorDiameter.Value, obj.MinorDiameter.Value, float(self.tool.Diameter), float(self.tool.Crest)):
            commands = internalThreadCommands(loc, gcode, zStart, zFinal, pitch, radius, obj.LeadInOut)
            for cmd in commands:
                p = cmd.Parameters
                if cmd.Name in ['G0']:
                    p.update({'F': self.vertRapid})
                if cmd.Name in ['G1', 'G2', 'G3']:
                    p.update({'F': self.horizFeed})
                cmd.Parameters = p
            self.commandlist.extend(commands)

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

    def circularHoleExecute(self, obj, holes):
        PathLog.track()
        if self.isToolSupported(obj, self.tool):
            self.commandlist.append(Path.Command("(Begin Thread Milling)"))

            (cmd, zStart, zFinal) = self.threadSetup(obj)
            pitch = obj.Pitch.Value
            if obj.TPI > 0:
                pitch = 25.4 / obj.TPI
            if pitch <= 0:
                PathLog.error("Cannot create thread with pitch {}".format(pitch))
                return

            # rapid to clearance height
            for loc in holes:
                self.executeThreadMill(obj, FreeCAD.Vector(loc['x'], loc['y'], 0), cmd, zStart, zFinal, pitch)
        else:
            PathLog.error("No suitable Tool found for thread milling operation")


    def opSetDefaultValues(self, obj, job):
        obj.ThreadOrientation = self.RightHand
        obj.ThreadType = self.ThreadTypeMetricInternal
        obj.ThreadFit = 50
        obj.Pitch = 1
        obj.TPI = 0
        obj.Passes = 1
        obj.Direction = self.DirectionClimb
        obj.LeadInOut = True

    def isToolSupported(self, obj, tool):
        '''Thread milling only supports thread milling cutters.'''
        return hasattr(tool, 'Diameter') and hasattr(tool, 'Crest')


def SetupProperties():
    setup = []
    setup.append("ThreadOrientation")
    setup.append("ThreadType")
    setup.append("ThreadName")
    setup.append("ThreadFit")
    setup.append("MajorDiameter")
    setup.append("MinorDiameter")
    setup.append("Pitch")
    setup.append("TPI")
    setup.append("Passes")
    setup.append("Direction")
    setup.append("LeadInOut")
    return setup


def Create(name, obj=None):
    '''Create(name) ... Creates and returns a thread milling operation.'''
    if obj is None:
        obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
    obj.Proxy = ObjectThreadMilling(obj, name)
    if obj.Proxy:
        obj.Proxy.findAllHoles(obj)
    return obj

