import linuxcnc
import os,sys
# path to the configuration the user requested
# used to see if the is local handler files to use
try:
    CONFIGPATH = os.environ['CONFIG_DIR']
    CONFIGDIR = os.path.join(CONFIGPATH, 'panelui_handler.py')
except:
    print('**** PANEL COMMAND: no panelui_handlers.py file in config directory')
    CONFIGPATH = os.path.expanduser("~")
    CONFIGDIR = os.path.join(CONFIGPATH, 'panelui_handler.py')

# constants
JOGJOINT  = 1
JOGTELEOP = 0

inifile = linuxcnc.ini(os.environ['INI_FILE_NAME'])
trajcoordinates = inifile.find("TRAJ", "COORDINATES").lower().replace(" ","")
jointcount = int(inifile.find("KINS","JOINTS"))

DBG_state = 0
def DBG(str):
    if DBG_state > 0:
        print(str)

# Loads user commands from a file named 'panelui_handler.py' from the config
def load_handlers(usermod,halcomp,builder,commands,master):
    hdl_func = 'get_handlers'
    mod = object = None
    def add_handler(method, f):
        if method in handlers:
            handlers[method].append(f)
        else:
            handlers[method] = [f]
    handlers = {}
    for u in usermod:
        (directory,filename) = os.path.split(u)
        (basename,extension) = os.path.splitext(filename)
        if directory == '':
            directory = '.'
        if directory not in sys.path:
            sys.path.insert(0,directory)
            DBG( 'panelui: adding import dir %s' % directory)
        try:
            mod = __import__(basename)
        except ImportError as msg:
            print("panelui: module '%s' skipped - import error: %s" %(basename,msg))
            continue
        DBG( "panelui: module '%s' imported OK" % mod.__name__)
        try:
            # look for 'get_handlers' function
            h = getattr(mod,hdl_func,None)
            if h and callable(h):
                DBG("panelui: module '%s' : '%s' function found" % (mod.__name__,hdl_func))
                objlist = h(halcomp,builder,commands,master)
            else:
                # the module has no get_handlers() callable.
                # in this case we permit any callable except class Objects in the module to register as handler
                DBG("panelui: module '%s': no '%s' function - registering only functions as callbacks" % (mod.__name__,hdl_func))
                objlist =  [mod]
            # extract callback candidates
            for object in objlist:
                #DBG("Registering handlers in module %s object %s" % (mod.__name__, object))
                if isinstance(object, dict):
                    methods = list(dict.items())
                else:
                    methods = [(n, getattr(object, n, None)) for n in dir(object)]
                for method,f in methods:
                    if method.startswith('_'):
                        continue
                    if callable(f):
                        DBG("panelui: Register callback '%s' in %s" % (method, basename))
                        add_handler(method, f)
        except Exception as e:
            print("**** PANELUI ERROR: trouble looking for handlers in '%s': %s" %(basename, e))

    # Wrap lists in Trampoline, unwrap single functions
    for n,v in list(handlers.items()):
        if len(v) == 1:
            handlers[n] = v[0]
        else:
            handlers[n] = Trampoline(v)

    return handlers,mod,object

# trampoline and load_handlers are used for custom keyboard commands
class Trampoline(object):
    def __init__(self,methods):
        self.methods = methods

    def __call__(self, *a, **kw):
        for m in self.methods:
            m(*a, **kw)

# linuxcnc commands
class CNC_COMMANDS():
        def __init__(self, master):
            global DBG_state
            DBG_state = master._dbg
            self.emc = linuxcnc
            self.emcstat = linuxcnc.stat()
            self.emccommand = linuxcnc.command()
            self.return_to_mode = -1 # if not -1 return to the mode specified
            self.sb = 0;
            self.jog_velocity = 100.0/60.0
            self.angular_jog_velocity = 3600/60
            self._mdi = 0
            self.isjogging = [0,0,0,0,0,0,0,0,0]
            self.restart_line_number = self.restart_reset_line = 0
            try:
                handlers,self.handler_module,self.handler_instance = \
                load_handlers([CONFIGDIR], self.emcstat, self.emccommand,self, master)
            except Exception as e:
                print(e)

        def mdi_active(self, wname, m):
            self._mdi = m

        def mist_on(self, wname, b):
            self.emccommand.mist(1)

        def mist_off(self, wname, b):
            self.emccommand.mist(0)

        def flood_on(self, wname, b):
            self.emccommand.flood(1)

        def flood_off(self, wname, b):
            self.emccommand.flood(0)

        def estop(self, wname, b):
            self.emccommand.state(self.emc.STATE_ESTOP)

        def estop_reset(self, wname, b):
            self.emccommand.state(self.emc.STATE_ESTOP_RESET)

        def machine_off(self, wname, b):
            self.emccommand.state(self.emc.STATE_OFF)

        def machine_on(self, wname, b):
            self.emccommand.state(self.emc.STATE_ON)

        def home_all(self, wname, b):
            self.emccommand.mode(self.emc.MODE_MANUAL)
            self.emccommand.home(-1)

        def unhome_all(self, wname, b):
            self.emccommand.mode(self.emc.MODE_MANUAL)
            self.emccommand.unhome(-1)

        def home_selected(self, wname, joint):
            self.emccommand.mode(self.emc.MODE_MANUAL)
            self.emccommand.home(int(joint))

        def unhome_selected(self, wname, joint):
            self.emccommand.mode(self.emc.MODE_MANUAL)
            self.emccommand.unhome(int(joint))

        def jogging(self, wname, b):
            self.emccommand.mode(self.emc.MODE_MANUAL)

        def override_limits(self, wname, b):
            self.emccommand.mode(self.emc.MODE_MANUAL)
            self.emccommand.override_limits()

        def spindle_forward_adjust(self, wname, rpm=100):
            if self.get_mode() == self.emc.MODE_MDI:
                self.emccommand.mode(self.emc.MODE_MANUAL)
            speed = self.is_spindle_running()
            if  speed == 0:
                self.emccommand.spindle(1,float(rpm));
            elif speed > 0:
                self.emccommand.spindle(self.emc.SPINDLE_INCREASE)
            else:
                self.emccommand.spindle(self.emc.SPINDLE_DECREASE)

        def spindle_forward(self, wname, rpm=100):
            self.emccommand.mode(self.emc.MODE_MANUAL)
            speed = self.is_spindle_running()
            if  speed == 0:
                self.emccommand.spindle(1,float(rpm));

        def spindle_stop(self, wname, b):
            self.emccommand.mode(self.emc.MODE_MANUAL)
            self.emccommand.spindle(0);

        def spindle_reverse(self, wname, rpm=100):
            self.emccommand.mode(self.emc.MODE_MANUAL)
            speed = self.is_spindle_running()
            if  speed == 0:
                self.emccommand.spindle(-1,float(rpm));

        def spindle_reverse_adjust(self, wname, rpm=100):
            if self.get_mode() == self.emc.MODE_MDI:
                self.emccommand.mode(self.emc.MODE_MANUAL)
            speed = self.is_spindle_running()
            if  speed == 0:
                self.emccommand.spindle(-1,float(rpm));
            elif speed < 0:
                self.emccommand.spindle(self.emc.SPINDLE_INCREASE)
            else:
                self.emccommand.spindle(self.emc.SPINDLE_DECREASE)

        def spindle_faster(self, wname, b):
            self.emccommand.mode(self.emc.MODE_MANUAL)
            self.emccommand.spindle(0,self.emc.SPINDLE_INCREASE)

        def spindle_slower(self, wname, b):
            self.emccommand.mode(self.emc.MODE_MANUAL)
            self.emccommand.spindle(0,self.emc.SPINDLE_DECREASE)

        def set_linear_jog_velocity(self, wname, cmd):
            velocity = float(cmd)
            if velocity is not None:
                rate = self.jog_velocity = velocity / 60.0
                for axisnum in (0,1,2,6,7,8):
                    if self.isjogging[axisnum]:
                        jjogmode,j_or_a = self.get_jog_info(axisnum)
                        self.emccommand.jog(self.emc.JOG_CONTINUOUS, jjogmode, j_or_a, self.isjogging[axisnum] * rate)

        def set_angular_jog_velocity(self, wname, cmd):
            angular = float(cmd)
            if velocity is not None:
                rate = self.angular_jog_velocity = angular / 60.0
                for axisnum in (3,4,5):
                    if self.isjogging[axisnum]:
                        jjogmode,j_or_a = self.get_jog_info(axisnum)
                        self.emccommand.jog(self.emc.JOG_CONTINUOUS, jjogmode, j_or_a, self.isjogging[axisnum] * rate)

        def continuous_jog(self, wname, cmd):
            axisnum = int(cmd[0])
            jjogmode,j_or_a = self.get_jog_info(axisnum)
            if j_or_a == -1 or jjogmode == -1:
                print('missconfigured joint/axis or mode for {}'.format(axisnum))
                return
            direction = int(cmd[1])
            if direction == 0:
                self.isjogging[axisnum] = 0
                self.emccommand.jog(self.emc.JOG_STOP, jjogmode, j_or_a)
            else:
                if axisnum in (3,4,5):
                    rate = self.angular_jog_velocity
                else:
                    rate = self.jog_velocity
                self.isjogging[axisnum] = direction
                self.emccommand.jog(self.emc.JOG_CONTINUOUS, jjogmode, j_or_a, direction * rate)

        def incremental_jog(self, wname, cmd):
            axisnum = int(cmd[0])
            jjogmode,j_or_a = self.get_jog_info(axisnum)
            if j_or_a == -1 or jjogmode == -1:
                print('missconfigured joint/axis or mode for {}'.format(axisnum))
                return
            direction = int(cmd[1])
            distance = float(cmd[2])
            self.isjogging[axisnum] = direction
            if axisnum in (3,4,5):
                rate = self.angular_jog_velocity
            else:
                rate = self.jog_velocity
            self.emccommand.jog(self.emc.JOG_INCREMENT, jjogmode, j_or_a, direction * rate, distance)
            self.isjogging[axisnum] = 0

        def quill_up(self, wname, cmd):
            self.emccommand.mode(self.emc.MODE_MANUAL)
            self.emccommand.wait_complete()
            self.mdi('G53 G0 Z %f'% float(cmd))

        def feed_hold(self, wname, cmd):
            self.emccommand.set_feed_hold(int(cmd))

        def feed_override(self, wname, f):
            self.emccommand.feedrate(f)

        def rapid_override(self, wname, f):
            self.emccommand.rapidrate(f)

        def spindle_override(self, wname, s):
            self.emccommand.spindleoverride(0,s)

        def max_velocity(self, wname, m):
            self.emccommand.maxvel(m)

        def reload_tooltable(self, wname, b):
            self.emccommand.load_tool_table()

        def optional_stop(self, wname, cmd):
            self.emccommand.set_optional_stop(int(cmd))

        def block_delete(self, wname, cmd):
            self.emccommand.set_block_delete(int(cmd))

        def abort(self, wname, cmd=None):
            self.emccommand.abort()

        def pause(self, wname, cmd=None):
            self.emccommand.auto(self.emc.AUTO_PAUSE)

        def resume(self, wname, cmd=None):
            self.emccommand.auto(self.emc.AUTO_RESUME)

        def single_block(self, wname, s):
            self.sb = s
            self.emcstat.poll()
            if self.emcstat.queue > 0 or self.emcstat.paused:
                # program or mdi is running
                if s:
                    self.emccommand.auto(self.emc.AUTO_PAUSE)
                else:
                    self.emccommand.auto(self.emc.AUTO_RESUME)

        # make sure linuxcnc is in AUTO mode
        # if Linuxcnc is paused then pushing cycle start will step the program
        # else the program starts from restart_line_number
        # after restarting it resets the restart_line_number to 0.
        # You must explicitly set a different restart line each time
        def smart_cycle_start(self, wname, cmd=None):
            self.emcstat.poll()
            if self.emcstat.task_mode != self.emc.MODE_AUTO:
                self.emccommand.mode(self.emc.MODE_AUTO)
                self.emccommand.wait_complete()
            self.emcstat.poll()
            if self.emcstat.paused:
                self.emccommand.auto(self.emc.AUTO_STEP)
                return
            if self.emcstat.interp_state == self.emc.INTERP_IDLE:
                print(self.restart_line_number)
                self.emccommand.auto(self.emc.AUTO_RUN, self.restart_line_number)
            self.restart_line_number = self.restart_reset_line

        # This restarts the program at the line specified directly (without cyscle start push)
        def re_start(self, wname, line):
            self.emccommand.mode(self.emc.MODE_AUTO)
            self.emccommand.wait_complete()
            self.emccommand.auto(self.emc.AUTO_RUN, line)
            self.restart_line_number = self.restart_reset_line

        # checks if ready for commands
        # calls MDI commands and when idle, periodic() will return to the mode it was in
        def mdi_and_return(self, wname, cmd):
            if self.ok_for_mdi():
                self.return_to_mode = self.get_mode() # for periodic()
                self.set_mdi_mode()
                if isinstance(cmd,list):
                    for i in cmd:
                        #print(str(i))
                        self.emccommand.mdi(str(i))
                else:
                    self.emccommand.mdi(str(cmd))

        # call MDI commands, set mode if needed
        def mdi(self, wname, cmd):
            self.set_mdi_mode()
            if isinstance(cmd,list):
                for i in cmd:
                    #print(str(i))
                    self.emccommand.mdi(str(i))
            else:
                self.emccommand.mdi(str(cmd))

        # set the restart line, you can the either restart directly
        # or restart on the cycle start button push
        # see above.
        # reset option allows one to change the default restart after it next restarts
        # eg while a restart dialog is open, always restart at the line it says
        # when the dialog close change the  line and reset both to zero
        def set_restart_line (self,  wname, line,reset=0):
            self.restart_line_number = line
            self.restart_reset_line = reset

        def set_manual_mode(self):
            self.emcstat.poll()
            if self.emcstat.task_mode != self.emc.MODE_MANUAL:
                self.emccommand.mode(self.emc.MODE_MANUAL)
                self.emccommand.wait_complete()

        def set_mdi_mode(self):
            self.emcstat.poll()
            if self.emcstat.task_mode != self.emc.MODE_MDI:
                self.emccommand.mode(self.emc.MODE_MDI)
                self.emccommand.wait_complete()

        def set_auto_mode(self):
            self.emcstat.poll()
            if self.emcstat.task_mode != self.emc.MODE_AUTO:
                self.emccommand.mode(self.emc.MODE_AUTO)
                self.emccommand.wait_complete()

        def get_mode(self):
            self.emcstat.poll()
            return self.emcstat.task_mode

        def ok_for_mdi(self):
            self.emcstat.poll()
            s = self.emcstat
            return not s.estop and s.enabled and s.homed and \
                (s.interp_state == self.emc.INTERP_IDLE)

        def is_spindle_running(self):
            self.emcstat.poll()
            s = self.emcstat
            if s.spindle[0]['enabled']:
                return s.spindle[0]['speed']
            else:
                return 0

        def periodic(self):
            # return mode back to preset variable, when idle
            if self.return_to_mode > -1:
                self.emcstat.poll()
                if self.emcstat.interp_state == self.emc.INTERP_IDLE:
                    self.emccommand.mode(self.return_to_mode)
                    self.return_to_mode = -1

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

        def get_jjogmode(self):
            self.emcstat.poll()
            if self.emcstat.motion_mode == linuxcnc.TRAJ_MODE_FREE:
                return JOGJOINT
            if self.emcstat.motion_mode == linuxcnc.TRAJ_MODE_TELEOP:
                return JOGTELEOP
            print("commands.py: unexpected motion_mode",self.emcstat.motion_mode)
            return JOGTELEOP

        def jnum_for_axisnum(self,axisnum):
            if self.emcstat.kinematics_type != linuxcnc.KINEMATICS_IDENTITY:
                print("\n%s:\n  Joint jogging not supported for"
                       "non-identity kinematics"%__file__)
                return -1 # emcJogCont() et al reject neg joint/axis no.s
            jnum = trajcoordinates.index( "xyzabcuvw"[axisnum] )
            if jnum > jointcount:
                print("\n%s:\n  Computed joint number=%d for axisnum=%d "
                       "exceeds jointcount=%d with trajcoordinates=%s"
                       %(__file__,jnum,axisnum,jointcount,trajcoordinates))
                # Note: primary gui should protect for this misconfiguration
                # decline to jog
                return -1 # emcJogCont() et al reject neg joint/axis no.s
            return jnum


        def get_jog_info (self, num):
            jmode = self.get_jjogmode()
            if jmode == JOGJOINT:
                return jmode, self.jnum_for_axisnum(num)
            return jmode,num

