# ***************************************************************************
# *   Copyright (c) 2023 Paul Gettings p.gettings@gmail.com                 *
# *                                                                         *
# *   Post-processor for Yasakawa (YASNAC) MX3 control for FreeCAD.         *
# *                                                                         *
# *   Set to inch for default, adapted from fanuc posts for MX3 dialect     *
# *                                                                         *
# *   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 code 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 Lesser General Public License for more details.                   *
# *                                                                         *
# *   You should have received a copy of the GNU Library General Public     *
# *   License along with FreeCAD; 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
from FreeCAD import Units
import Path
import argparse
import datetime
import shlex
import os.path
import Path.Post.Utils as PostUtils
import PathScripts.PathUtils as PathUtils
from builtins import open as pyopen

TOOLTIP = '''
This is a postprocessor file for the CAM workbench. It is used to
take a pseudo-g-code fragment from a Path object, and output
real g-code suitable for Yasakawa (YASNAC) MX3 controllers.
This postprocessor, once placed in the appropriate scripts or macros folder,
can be used directly from inside FreeCAD, via the GUI importer, or via
python scripts with:

import mx3_post
mx3_post.export(object,"/path/to/file.nc","")
'''

now = datetime.datetime.now()

parser = argparse.ArgumentParser(prog='mx3', add_help=False)
parser.add_argument('--no-header', action='store_true', help='suppress header output')
parser.add_argument('--no-comments', action='store_true', help='suppress comment output')
parser.add_argument('--line-numbers', action='store_true', help='prefix with line numbers')
parser.add_argument('--no-show-editor', action='store_true', help='do NOT pop up editor before writing output')
parser.add_argument('--precision', default='4', help='number of digits of precision, default=4')
parser.add_argument('--inches', action='store_true', help='convert output for US imperial mode (G20)')
parser.add_argument('--mm', action='store_true', help='convert output for metric millimeters (G21)')
parser.add_argument('--no-modal', action='store_true', help='repeat modal G codes (e.g. G0, G1) each move')
parser.add_argument('--no-axis-modal', action='store_true', help='repeat X, Y, Z, A locations each move')
parser.add_argument('--no-tlo', action='store_true', help='suppress tool length offset (G43) following tool changes')
parser.add_argument('--program-number', action='store_true', help='add program number to start of file')

TOOLTIP_ARGS = parser.format_help()

# These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
SHOW_EDITOR = True
MODAL = True  # if true commands are suppressed if the same as previous line.
USE_TLO = True # if true G43 will be output following tool changes
OUTPUT_DOUBLES = False  # if false duplicate axis values are suppressed if the same as previous line.
COMMAND_SPACE = " "
LINENR = 1  # line number starting value
LINEDELTA = 1 # line number increment
MAXLINENR = 9999 # maximum line number allowed by control
OUTPUT_PROGNR = False # add Oxxxx to head of program
TLC = False # flag for output of G43 Hxx on Z move
CURRENT_TOOL = 9999 # insane tool number in case

# These globals will be reflected in the Machine configuration of the project
UNITS = "G20"  # G21 for metric, G20 for us standard
UNIT_SPEED_FORMAT = 'in/min'
UNIT_FORMAT = 'in'
PRECISION = 4

MACHINE_NAME = "MX3"
# MX3 machines have machine home at max positive corner (X, Y), 0in
# below max Z positive travel.
# Z machine 0 at tool change location.
# These travels are set for a Matsuura MC-600V:
# 600mm X, 410mm Y, 560mm Z
CORNER_MIN = {'x': -23.619, 'y': -16.0, 'z': -18.}
CORNER_MAX = {'x': +0.003, 'y': +0.003, 'z': +0.003}

# Preamble text will appear at the beginning of the GCODE output file.
PREAMBLE = '''G17 G40 G49 G80 G90
'''

# Postamble text will appear following the last operation.
POSTAMBLE = '''M05
G17 G90 G80 G40
G0 G53 Z0.
M2
'''

# Pre operation text will be inserted before every operation
PRE_OPERATION = ''''''

# Post operation text will be inserted after every operation
POST_OPERATION = ''''''

# Tool Change commands will be inserted before a tool change
TOOL_CHANGE = '''M8
M5
G0 G53 Z0.
'''

def processArguments(argstring):
    # pylint: disable=global-statement
    global OUTPUT_HEADER
    global OUTPUT_COMMENTS
    global OUTPUT_LINE_NUMBERS
    global SHOW_EDITOR
    global PRECISION
    global PREAMBLE
    global POSTAMBLE
    global UNITS
    global UNIT_SPEED_FORMAT
    global UNIT_FORMAT
    global MODAL
    global USE_TLO
    global OUTPUT_PROGNR
    global OUTPUT_DOUBLES

    try:
        args = parser.parse_args(shlex.split(argstring))
        if args.no_header:
            OUTPUT_HEADER = False
        if args.no_comments:
            OUTPUT_COMMENTS = False
        if args.line_numbers:
            OUTPUT_LINE_NUMBERS = True
        if args.no_show_editor:
            SHOW_EDITOR = False
        print(">>>> process arguments- show editor = %d" % SHOW_EDITOR)
        PRECISION = args.precision
        if args.inches:
            UNITS = 'G20'
            UNIT_SPEED_FORMAT = 'in/min'
            UNIT_FORMAT = 'in'
            PRECISION = 4
        if args.mm:
            UNITS = 'G21'
            UNIT_SPEED_FORMAT = 'mm/min'
            UNIT_FORMAT = 'mm'
            PRECISION = 3
        if args.no_modal:
            MODAL = False
        if args.no_tlo:
            USE_TLO = False
        if args.no_axis_modal:
            OUTPUT_DOUBLES = True
        if args.program_number:
            OUTPUT_PROGNR = True

    except Exception as e: # pylint: disable=broad-except
        print(">>>> Exception in parameter handling!")
        print(e)
        return False

    return True


def export(objectslist, filename, argstring):
    # pylint: disable=global-statement
    print("> start export ")
    print(">> process arguments")
    if not processArguments(argstring):
        return None
    global UNITS
    global UNIT_FORMAT
    global UNIT_SPEED_FORMAT
    global USE_TLO
    global OUTPUT_PROGNR
    global TLC
    global OUTPUT_HEADER
    global MIST

    for obj in objectslist:
        if not hasattr(obj, "Path"):
            print("the object " + obj.Name + " is not a path. Please select only path and Compounds.")
            return None

    print(">> postprocessing")
    gcode = "%\n" # need % at beginning of file for upload

    #output a program number
    #format for nc-file *_xxxx.nc2 whereby xxxx=program number
    if OUTPUT_PROGNR == True:
        reply = QtGui.QInputDialog.getText(None,"MX3 Post","Enter programm number")
        print(">>> "+reply)
        if reply[1]:
            gcode += "%\nO" + reply[0] + "\n"

    # write header
    if OUTPUT_HEADER:
        gcode += linenumber() + "("+filename.upper()+" EXPORTED BY FREECAD)\n"
        gcode += linenumber() + "(POST PROCESSOR " + __name__.upper() + ")\n"
        gcode += linenumber() + "(PROCESSED AT " + str(now).upper() + ")\n"

    # Write the preamble
    if OUTPUT_COMMENTS:
        gcode += linenumber() + "(BEGIN PREAMBLE)\n"
    for line in PREAMBLE.splitlines(False):
        gcode += linenumber() + line + "\n"
    gcode += linenumber() + UNITS + "\n"

    for obj in objectslist:

        # Skip inactive operations
        if hasattr(obj, 'Active'):
            if not obj.Active:
                continue
        if hasattr(obj, 'Base') and hasattr(obj.Base, 'Active'):
            if not obj.Base.Active:
                continue

        # do the pre_op
        if OUTPUT_COMMENTS:
            gcode += linenumber() + "(BEGIN OPERATION %s)\n" % obj.Label.upper()
            gcode += linenumber() + "(MACHINE UNITS %s)\n" % (UNIT_SPEED_FORMAT.upper())
        for line in PRE_OPERATION.splitlines(True):
            gcode += linenumber() + line

        # get coolant mode
        coolantMode = 'None'
        if hasattr(obj, "CoolantMode") or hasattr(obj, 'Base') and  hasattr(obj.Base, "CoolantMode"):
            if hasattr(obj, "CoolantMode"):
                coolantMode = obj.CoolantMode
            else:
                coolantMode = obj.Base.CoolantMode

        # turn coolant on if required
        if OUTPUT_COMMENTS:
            if not coolantMode == 'None':
                gcode += linenumber() + '(COOLANT ON ' + coolantMode.upper() + ')\n'
        if coolantMode == 'Flood':
            gcode  += linenumber() + 'M8' + '\n'
        if coolantMode == 'Mist':# this mill uses the air brake for mist coolant!
            gcode += linenumber() + 'M60' + '\n'

        # process the operation gcode
        gcode += parse(obj)

        # do the post_op
        if OUTPUT_COMMENTS:
            gcode += linenumber() + "(FINISH OPERATION %s)\n" % obj.Label.upper()
        for line in POST_OPERATION.splitlines(True):
            gcode += linenumber() + line

        # turn coolant off if required
        if not coolantMode == 'None':
          if OUTPUT_COMMENTS:
            gcode += linenumber() + '(COOLANT OFF ' + coolantMode.upper() + ')\n'
        if coolantMode == 'Flood':
          gcode  += linenumber() + 'M9' + '\n'
        if coolantMode == 'Mist':  # this mill uses the air brake for mist coolant!
          gcode += linenumber() + 'M61' + '\n'

    # do the post_amble
    if OUTPUT_COMMENTS:
        gcode += "(BEGIN POSTAMBLE)\n"
    for line in POSTAMBLE.splitlines(True):
        gcode += linenumber() + line
    gcode += "%\n"

    if FreeCAD.GuiUp and SHOW_EDITOR:
        if len(gcode) > 100000:
          final = gcode
          print(">>>> Output >100kB ==> skip editor.")
        else:
          dia = PostUtils.GCodeEditorDialog()
          dia.editor.setText(gcode)
          result = dia.exec_()
          if result:
              final = dia.editor.toPlainText()
          else:
              final = gcode
    else:
        final = gcode

    print(">> done postprocessing.")

    if not filename == '-':
        gfile = pyopen(filename, "w")
        gfile.write(final)
        gfile.close()

    return final


def linenumber():
    # pylint: disable=global-statement
    global LINENR
    global LINEDELTA
    global MAXLINENR
    if OUTPUT_LINE_NUMBERS is True:
        LINENR += LINEDELTA
        if LINENR > MAXLINENR: LINENR = LINEDELTA
        return "N" + str(LINENR) + " "
    return ""


def parse(pathobj):
    # pylint: disable=global-statement
    global PRECISION
    global MODAL
    global OUTPUT_DOUBLES
    global UNIT_FORMAT
    global UNIT_SPEED_FORMAT
    global TLC
    global CURRENT_TOOL

    out = ""
    lastcommand = None
    precision_string = '.' + str(PRECISION) + 'f'
    currLocation = {}  # keep track for no doubles
    print("Startup!")

    # the order of parameters
    params = ['X', 'Y', 'Z', 'A', 'B', 'C', 'I', 'J', 'K', 'F', 'S', 'T', 'Q', 'R', 'L', 'H', 'D', 'P']
    firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0})
    currLocation.update(firstmove.Parameters)  # set First location Parameters

    if hasattr(pathobj, "Group"):  # We have a compound or project.
        # if OUTPUT_COMMENTS:
        #     out += linenumber() + "(compound: " + pathobj.Label + ")\n"
        for p in pathobj.Group:
            out += parse(p)
        return out
    else:  # parsing simple path
        # groups might contain non-path things like stock.
        if not hasattr(pathobj, "Path"):
            return out

        # if OUTPUT_COMMENTS:
        #     out += linenumber() + "(" + pathobj.Label + ")\n"

        for index,c in enumerate(pathobj.Path.Commands):
            outstring = []
            command = c.Name
            if index+1 == len(pathobj.Path.Commands):
              nextcommand = ""
            else:
              nextcommand = pathobj.Path.Commands[index+1].Name

            # suppress moves in fixture selection
            if pathobj.Label == "Fixture":
                if command == "G0":
                    continue

            # FIXME
            # convert drill cycles to tap cycles if tool is a tap
            if command == "G81" or command == "G83" and hasattr(pathobj, 'ToolController') and pathobj.ToolController.Tool.ToolType == "Tap":
              outstring.append("G84 G99")
              # append additional parameters for tapping
              if "R" in c.Parameters:
                outstring.append("R0"+ Units.Quantity(c.Parameters["R"], FreeCAD.Units.Length))
                del c.Parameters["R"]
              else:
                outstring.append("R0"+ Units.Quantity(currLocation["Z"], FreeCAD.Units.Length))
              if "S" in c.Parameters:
                outstring.append("F"+ str(int(c.Parameters["S"])))
                del c.Parameters["S"]
            else: # otherwise add command to output
              outstring.append(command)
            # end FIXME

            # if modal: suppress the command if it is the same as the last one
            if MODAL is True:
                if command == lastcommand:
                    outstring.pop(0)

            # suppress a G80 between two identical command
            if command == "G80" and lastcommand == nextcommand:
                continue

            if command[0] == '(' and not OUTPUT_COMMENTS: # command is a comment
                continue

            # Now add the remaining parameters in order
            for param in params:
                if param in c.Parameters:
                    if param == 'F' and (currLocation[param] != c.Parameters[param] or OUTPUT_DOUBLES):
                        if command not in ["G0", "G00"]:  # fadal doesn't use rapid speeds
                            speed = Units.Quantity(c.Parameters['F'], FreeCAD.Units.Velocity)
                            if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0:
                                outstring.append(param + format(float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string))
                            else:
                                continue
                    elif param == 'T':
                        outstring.append(param + str(int(c.Parameters['T'])))
                        CURRENT_TOOL = int(c.Parameters['T'])
                    elif param == 'H':
                        outstring.append(param + str(int(c.Parameters['H'])))
                    elif param == 'D':
                        outstring.append(param + str(int(c.Parameters['D'])))
                    elif param == 'S':
                        outstring.append(param + str(int(c.Parameters['S'])))
                    else: # coordinates & other parameters
                      if param == 'Z' and TLC: # add G43 Hxx to first Z move
                        pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
                        outstring.append(
                            param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string))
                        outstring.append("G43 H%d"%CURRENT_TOOL)
                        TLC = False
                      elif (not OUTPUT_DOUBLES) and (param in currLocation) and (currLocation[param] == c.Parameters[param]):
                        continue
                      else:
                        pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
                        outstring.append(param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string))

            # store the latest command
            lastcommand = command
            currLocation.update(c.Parameters)

            # Check for Tool Change:
            if command == 'M6':
              # add tool change preamble
              for line in TOOL_CHANGE.splitlines(True):
                out += linenumber() + line
              # flag for adding height offset to next Z move
              TLC = True

            if command == "message":
              if OUTPUT_COMMENTS is False:
                out = []
              else:
                outstring.pop(0)  # remove the command

            # prepend a line number and append a newline
            if len(outstring) >= 1:
              if OUTPUT_LINE_NUMBERS:
                outstring.insert(0, (linenumber()))

              # append the line to the final output
              for w in outstring:
                out += w.upper() + COMMAND_SPACE
              # drop strip() from previous version, since that apparently adds
              # quadratic complexity in memory allocations.
              out += "\n"

        return out

# print(__name__ + " gcode postprocessor loaded.")
