#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
#
#    This is stepconf, a graphical configuration editor for LinuxCNC
#    Copyright 2007 Jeff Epler <jepler@unpythonic.net>
#    stepconf 1.1 revamped by Chris Morley 2014
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    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 General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
#    This builds the INI file from the collected data.
#

import os
import time
import sys
import importlib
import shutil

class INI:
    def __init__(self,app):
        # access to:
        self.d = app.d  # collected data
        global SIG
        SIG = app._p    # private data (signals names)
        self.a = app    # The parent, stepconf

    def write_inifile(self, base):
        if self.d.axes == 2:
            maxvel = max(self.d.xmaxvel, self.d.zmaxvel)
        elif self.d.axes == 4:
            maxvel = max(self.d.xmaxvel, self.d.ymaxvel)
        else:
            maxvel = max(self.d.xmaxvel, self.d.ymaxvel, self.d.zmaxvel)
        hypotvel = (self.d.xmaxvel**2 + self.d.ymaxvel**2 + self.d.zmaxvel**2) **.5
        defvel = min(maxvel, max(.1, maxvel/10.))

        filename = os.path.join(base, self.d.machinename + ".ini")
        # make a backup copy if INI file exists
        if os.path.exists(filename):
            shutil.copy2(filename, filename.replace(".ini", "_" + str(int(time.time())) + ".ini"))
        file = open(filename, "w")
        print(_("# Generated by stepconf 1.1 at %s") % time.asctime(), file=file)
        print(_("# If you make changes to this file, they will be"), file=file)
        print(_("# overwritten when you run stepconf again"), file=file)

        print(file=file)
        print("[EMC]", file=file)
        print("MACHINE = %s" % self.d.machinename, file=file)
        print("DEBUG = 0", file=file)

        # the joints_axes conversion script named 'update_ini'
        # will try to update for joints_axes if no VERSION is set
        print("VERSION = 1.1", file=file)

        print(file=file)
        print("[DISPLAY]", file=file)
        if self.d.select_axis:
            print("DISPLAY = axis", file=file)
        elif self.d.select_gmoccapy:
            print("DISPLAY = gmoccapy", file=file)
        elif self.d.select_qtdragon:
            print("DISPLAY = qtvcp qtdragon", file=file)
            print("PREFERENCE_FILE_PATH = WORKINGFOLDER/qtdragon.pref", file=file)
        # qtplasmac has multiple screen sizes
        elif self.d.select_qtplasmac:
            if self.d.qtplasmacscreen == 2:
                screen = "qtplasmac_9x16"
            elif self.d.qtplasmacscreen == 1:
                screen = "qtplasmac_4x3"
            else:
                screen = "qtplasmac"
            print("DISPLAY = qtvcp {}".format(screen), file=file)
        # qtplasmac has an internal editor
        if not self.d.select_qtplasmac:
            print("EDITOR = gedit", file=file)
        print("POSITION_OFFSET = RELATIVE", file=file)
        print("POSITION_FEEDBACK = ACTUAL", file=file)
        print("ARCDIVISION = 64", file=file)
        print("GRIDS = 10mm 20mm 50mm 100mm 1in 2in 5in 10in", file=file)
        print("MAX_FEED_OVERRIDE = 1.2", file=file)
        # qtplasmac doesn't use spindle override
        if not self.d.select_qtplasmac:
            print("MIN_SPINDLE_OVERRIDE = 0.5", file=file)
            print("MAX_SPINDLE_OVERRIDE = 1.2", file=file)
        print("DEFAULT_LINEAR_VELOCITY = %.2f" % defvel, file=file)
        print("MIN_LINEAR_VELOCITY = 0", file=file)
        print("MAX_LINEAR_VELOCITY = %.2f" % maxvel, file=file)
        if self.d.axes == 1:
            defvel = min(60, self.d.amaxvel/10.)
            print("DEFAULT_ANGULAR_VELOCITY = %.2f" % defvel, file=file)
            print("MIN_ANGULAR_VELOCITY = 0", file=file)
            print("MAX_ANGULAR_VELOCITY = %.2f" % self.d.amaxvel, file=file)

        print("CYCLE_TIME = 0.100", file=file)
        print("INTRO_GRAPHIC = linuxcnc.gif", file=file)
        print("INTRO_TIME = 5", file=file)
        print("PROGRAM_PREFIX = %s" % os.path.expanduser("~/linuxcnc/nc_files"), file=file)
        # qtplasmac has different increments
        if self.d.select_qtplasmac:
            if self.d.units:
                print("INCREMENTS = 10mm 1mm .1mm .01mm .001mm", file=file)
            else:
                print("INCREMENTS = 1in .1in .01in .001in .0001in", file=file)
        else:
            if self.d.units:
                print("INCREMENTS = 5mm 1mm .5mm .1mm .05mm .01mm .005mm", file=file)
            else:
                print("INCREMENTS = .1in .05in .01in .005in .001in .0005in .0001in", file=file)
        if self.d.pyvcp:
            print("PYVCP = custompanel.xml", file=file)
        if self.d.axes == 2:
            print("LATHE = 1", file=file)
        if self.d.axes == 3:
            print("FOAM = 1", file=file)
            print("GEOMETRY = XY;UZ", file=file)
            print("OPEN_FILE = ./foam.ngc", file=file)
        print(file=file)

        # self.d.axes is coded 0: X Y Z
        #                      1: X Y Z A
        #                      2: X Z
        #                      3: X Y U V
        if   self.d.axes == 0: num_joints = 3 # X Y Z
        elif self.d.axes == 1: num_joints = 4 # X Y Z A
        elif self.d.axes == 2: num_joints = 2 # X Z
        elif self.d.axes == 3: num_joints = 4 # X Y U V
        elif self.d.axes == 4: num_joints = 2 # X Y
        else:
            print("___________________unknown self.d.axes",self.d.axes)
        num_joints += len(self.d.tandemjoints)

        if   self.d.axes == 1: coords = "X Y Z A"
        elif self.d.axes == 0: coords = "X Y Z"
        elif self.d.axes == 2: coords = "X Z"
        elif self.d.axes == 3: coords = "X Y U V"
        elif self.d.axes == 4: coords = "X Y"

        for j in self.d.tandemjoints:
            if j.upper() in coords:
                coords = coords.replace(j.upper(), "{0} {0}".format(j.upper()))
        self.d.axislist = []
        for c in coords.lower().replace(" ",""):
            if c not in self.d.axislist:
                self.d.axislist.append(c)
            else:
                self.d.axislist.append("{}2".format(c))
        print("[KINS]", file=file)
        # trivial kinematics: no. of joints == no.of axes)
        # with trivkins, axes do not have to be consecutive
        print("JOINTS = %d"%num_joints, file=file)
        if self.d.tandemjoints and not self.d.select_qtplasmac:
            print("KINEMATICS = trivkins coordinates=%s kinstype=BOTH"%coords.replace(" ",""), file=file)
        else:
            print("KINEMATICS = trivkins coordinates=%s"%coords.replace(" ",""), file=file)
        print(file=file)
        print("[FILTER]", file=file)
        # qtplasmac has a different filter section
        if self.d.select_qtplasmac:
            print("PROGRAM_EXTENSION = .ngc,.nc,.tap GCode File (*.ngc, *.nc, *.tap)", file=file)
            print("ngc = qtplasmac_gcode", file=file)
            print("nc  = qtplasmac_gcode", file=file)
            print("tap = qtplasmac_gcode", file=file)
        else:
            print("PROGRAM_EXTENSION = .png,.gif,.jpg Greyscale Depth Image", file=file)
            print("PROGRAM_EXTENSION = .py Python Script", file=file)
            print("PROGRAM_EXTENSION = .nc,.tap G-Code File", file=file)
            print("png = image-to-gcode", file=file)
            print("gif = image-to-gcode", file=file)
            print("jpg = image-to-gcode", file=file)
            print("py = python", file=file)        

        print(file=file)
        print("[TASK]", file=file)
        print("TASK = milltask", file=file)
        print("CYCLE_TIME = 0.010", file=file)

        print(file=file)
        print("[RS274NGC]", file=file)
        print("PARAMETER_FILE = linuxcnc.var", file=file)
        # qtplasmac has extra rs274ngc variables
        if self.d.select_qtplasmac:
            code = 21 if self.d.units else 20
            print("RS274NGC_STARTUP_CODE = G{} G40 G49 G80 G90 G92.1 G94 G97 M52P1".format(code), file=file)
            print("SUBROUTINE_PATH = ./:../../nc_files", file=file)
            print("USER_M_PATH = ./:../../nc_files", file=file)
            print("", file=file)

        base_period = self.d.ideal_period()

        print(file=file)
        print("[EMCMOT]", file=file)
        print("EMCMOT = motmod", file=file)
        print("COMM_TIMEOUT = 1.0", file=file)
        print("BASE_PERIOD = %d" % base_period, file=file)
        print("SERVO_PERIOD = 1000000", file=file)

        print(file=file)
        print("[HAL]", file=file)
        if self.d.halui or self.d.select_qtplasmac:
            print("HALUI = halui", file=file)          
        print("HALFILE = %s.hal" % self.d.machinename, file=file)
        # qtplasmac requires custom, custom_postgui and shutdown hal files
        if self.d.select_qtplasmac:
            print("HALFILE = qtplasmac_comp.hal", file=file)
            print("HALFILE = custom.hal", file=file)
            print("POSTGUI_HALFILE = custom_postgui.hal", file=file)
            if self.d.sim_hardware:
                print("POSTGUI_HALFILE = sim_postgui.hal", file=file)
            print("SHUTDOWN = shutdown.hal", file=file)
        elif self.d.customhal:
            print("HALFILE = custom.hal", file=file)
            print("POSTGUI_HALFILE = postgui_call_list.hal", file=file)

        if self.d.halui:
           print(file=file)
           print("[HALUI]", file=file)
           print(_("# add halui MDI commands here (max 64) "), file=file)
           for mdi_command in self.d.halui_list:
               print("MDI_COMMAND = %s" % mdi_command, file=file)

        print(file=file)
        print("[TRAJ]", file=file)
        # qtplasmac requires 3 spindles
        if self.d.select_qtplasmac:
            print("SPINDLES = 3", file=file)
        # [TRAJ]AXES notused for joints_axes
        print("COORDINATES = ",coords, file=file)
        if self.d.units:
            print("LINEAR_UNITS = mm", file=file)
        else:
            print("LINEAR_UNITS = inch", file=file)
        print("ANGULAR_UNITS = degree", file=file)
        print("DEFAULT_LINEAR_VELOCITY = %.2f" % defvel, file=file)
        print("MAX_LINEAR_VELOCITY = %.2f" % maxvel, file=file)
        print(file=file)
        print("[EMCIO]", file=file)
        print("EMCIO = io", file=file)
        print("CYCLE_TIME = 0.100", file=file)
        print("TOOL_TABLE = tool.tbl", file=file)

        all_homes = "None"
        for axis in self.d.axislist:
            all_homes = all_homes and self.a.home_sig(axis)

        for axis in self.d.axislist:
            if "2" in axis:
                tandem = True
            else:
                tandem = False
            self.write_one_axis(file, self.d.axislist.index(axis), axis[0], "LINEAR", all_homes, tandem)

        file.close()
        self.d.add_md5sum(filename)

#******************
# HELPER FUNCTIONS
#******************
    def write_one_axis(self, file, num, letter, type, all_homes, tandem):
        order = "1203"
        def get(s): return self.d[letter + s]
        scale = get("scale")
        vel = min(get("maxvel"), self.ideal_maxvel(scale))
        # linuxcnc doesn't like having home right on an end of travel,
        # so extend the travel limit by up to .001in or .01mm
        minlim = get("minlim")
        maxlim = get("maxlim")
        home = get("homepos")
        extend = 0.001 if self.d.units else 0.01
        if minlim == home:
            minlim = minlim - extend
        elif maxlim == home:
            maxlim = maxlim + extend
        axis_letter = letter.upper()
        if not tandem:
            print(file=file)
            print("#*** AXIS_%s *******************************" % axis_letter, file=file)
            print("[AXIS_%s]" % axis_letter, file=file)
            # qtplasmac requires double vel & acc to use eoffsets correctly
            if self.d.select_qtplasmac:
                print("# MAX_VEL & MAX_ACC need to be twice the corresponding joint value", file=file)
                print("MAX_VELOCITY = %s" % (vel * 2), file=file)
                print("MAX_ACCELERATION = %s" % (get("maxacc") * 2), file=file)
                print("OFFSET_AV_RATIO = 0.5", file=file)
            else:
                print("MAX_VELOCITY = %s" % vel, file=file)
                print("MAX_ACCELERATION = %s" % get("maxacc"), file=file)
            print("MIN_LIMIT = %s" % minlim, file=file)
            print("MAX_LIMIT = %s" % maxlim, file=file)

        print(file=file)
        print("[JOINT_%d]" % num, file=file)
        print("TYPE = %s" % type, file=file)
        print("HOME = %s" % get("homepos"), file=file)
        print("MIN_LIMIT = %s" % minlim, file=file)
        print("MAX_LIMIT = %s" % maxlim, file=file)
        print("MAX_VELOCITY = %s" % vel, file=file)
        print("MAX_ACCELERATION = %s" % get("maxacc"), file=file)
        print("STEPGEN_MAXACCEL = %s" % (1.25 * get("maxacc")), file=file)
        print("SCALE = %s" % scale, file=file)
        if num == 3:
            print("FERROR = 1", file=file)
            print("MIN_FERROR = .25", file=file)
        elif self.d.units:
            print("FERROR = 1", file=file)
            print("MIN_FERROR = .25", file=file)
        else:
            print("FERROR = 0.05", file=file)
            print("MIN_FERROR = 0.01", file=file)

        inputs = self.a.build_input_set()
        thisaxishome = set((SIG.ALL_HOME, SIG.ALL_LIMIT_HOME, "home-" + letter, "min-home-" + letter,
                            "max-home-" + letter, "both-home-" + letter))
        # no need to set HOME_IGNORE_LIMITS when ALL_LIMIT_HOME, HAL logic will do the trick
        ignore = set(("min-home-" + letter, "max-home-" + letter,
                            "both-home-" + letter))
        homes = bool(inputs & thisaxishome)
    
        if homes:
            print("HOME_OFFSET = %f" % get("homesw"), file=file)
            print("HOME_SEARCH_VEL = %f" % get("homevel"), file=file)
            latchvel = get("homevel") / abs(get("homevel"))
            if get("latchdir"): latchvel = -latchvel
            # set latch velocity to one step every two servo periods
            # to ensure that we can capture the position to within one step
            latchvel = latchvel * 500 / get("scale")
            # don't do the latch move faster than the search move
            if abs(latchvel) > abs(get("homevel")):
                latchvel = latchvel * (abs(get("homevel"))/abs(latchvel))
            print("HOME_LATCH_VEL = %f" % latchvel, file=file)
            if inputs & ignore:
                print("HOME_IGNORE_LIMITS = YES", file=file)
            if all_homes:
                if self.d.axes == 3: # XYUV
                    if letter in('y','v'): hs = 1
                    else: hs = 0
                    if letter in self.d.tandemjoints:
                        print("HOME_SEQUENCE = -%d"% hs, file=file)
                    else:
                        print("HOME_SEQUENCE = %d"% hs, file=file)
                else:
                    if letter in self.d.tandemjoints:
                        print("HOME_SEQUENCE = -%s" % order["xyza".index(letter)], file=file)
                    else:
                        print("HOME_SEQUENCE = %s" % order["xyza".index(letter)], file=file)
        else:
            print("HOME_OFFSET = %s" % get("homepos"), file=file)
        if letter not in self.d.tandemjoints or tandem:
            print("#******************************************", file=file)

    def hz(self, axname):
        steprev = self.d[axname+"steprev"]
        microstep = self.d[axname+"microstep"]
        pulleynum = self.d[axname+"pulleynum"]
        pulleyden = self.d[axname+"pulleyden"]
        leadscrew = self.d[axname+"leadscrew"]
        maxvel = self.d[axname+"maxvel"]
        if self.d.units or axname == 'a': leadscrew = 1./leadscrew
        pps = leadscrew * steprev * microstep * (pulleynum/pulleyden) * maxvel
        return abs(pps)

    def minperiod(self, steptime=None, stepspace=None, latency=None):
        if steptime is None: steptime = self.d.steptime
        if stepspace is None: stepspace = self.d.stepspace
        if latency is None: latency = self.d.latency
        if self.a.doublestep(steptime):
            return max(latency + steptime + stepspace + 5000, 4*steptime)
        else:
            return latency + max(steptime, stepspace)

    def maxhz(self):
        return 1e9 / self.minperiod()

    def ideal_maxvel(self, scale):
        if self.a.doublestep():
            return abs(.95 * 1e9 / self.d.ideal_period() / scale)
        else:
            return abs(.95 * .5 * 1e9 / self.d.ideal_period() / scale)

    # Boiler code
    def __getitem__(self, item):
        return getattr(self, item)
    def __setitem__(self, item, value):
        return setattr(self, item, value)
