#!/usr/bin/env python3

#------------------------------------------------------------------------------
# Copyright: 2013
# Author:    Dewey Garrett <dgarrett@panix.com>
#
# 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.
#------------------------------------------------------------------------------

"""gremlin_view
Provide class: GremlinView for gremlin with buttons for simpler embedding
Standalone functionality if linuxcnc running

A default ui file (gremlin_view.ui) is provided for a default
button arrangement but a user may provide their own by supplying
the glade_file argument.

The following objects are mandatory:

  'gremlin_view_window'      toplevel window
  'gremlin_view_hal_gremlin' hal_gremlin
  'gremlin_view_box'         HBox or VBox' containing hal_gremlin

Optional radiobutton group names:
  'select_p_view'
  'select_x_view'
  'select_y_view'
  'select_z_view'
  'select_z2_view'

Optional Checkbuttons names:
  'enable_dro'
  'show_machine_speed
  'show_distance_to_go'
  'show_limits'
  'show_extents'
  'show_tool'
  'show_metric'

Callbacks are provided for the following buttons actions
  on_clear_live_plotter_clicked
  on_enable_dro_clicked
  on_zoomin_pressed
  on_zoomout_pressed
  on_pan_x_minus_pressed
  on_pan_x_plus_pressed
  on_pan_y_minus_pressed
  on_pan_y_plus_pressed
  on_show_tool_clicked
  on_show_metric_clicked
  on_show_extents_clicked
  on_select_p_view_clicked
  on_select_x_view_clicked
  on_select_y_view_clicked
  on_select_z_view_clicked
  on_select_z2_view_clicked
  on_show_distance_to_go_clicked
  on_show_machine_speed_clicked
  on_show_limits_clicked
"""

import gi
gi.require_version("Gtk","3.0")
from gi.repository import Gtk
from gi.repository import GObject
from gi.repository import GLib
import os
import sys
import gladevcp.hal_actions  # reqd for Builder
import linuxcnc
import time
import subprocess
import gettext
import datetime
import glib # for glib.GError

g_ui_dir          = linuxcnc.SHARE + "/linuxcnc"
g_periodic_secs   = 1 # integer
g_delta_pixels    = 10
g_move_delay_secs = 0.2
g_progname        = os.path.basename(sys.argv[0])
g_verbose         = False

LOCALEDIR = linuxcnc.SHARE + "/locale"
gettext.install("linuxcnc", localedir=LOCALEDIR)

def ini_check ():
    """set environmental variable and change directory"""
    # Note:
    #   hal_gremlin gets INI file from os.environ (only)
    #   hal_gremlin expects cwd to be same as INI file
    ini_filename = get_linuxcnc_ini_file()
    if ini_filename is not None:
        os.putenv('INI_FILE_NAME',ini_filename)    # ineffective
        os.environ['INI_FILE_NAME'] = ini_filename # need for hal_gremlin
        os.chdir(os.path.dirname(ini_filename))
        if g_verbose:
            print(('ini_check: INI_FILENAME= %s' % ini_filename))
            print(('ini_check:       curdir= %s' % os.path.curdir))
        return True # success
    print((_('%s: LinuxCNC INI file not available') % g_progname))
    return False # exit here crashes glade-gtk2

def get_linuxcnc_ini_file():
    """find LinuxCNC INI file with pgrep"""
    ps   = subprocess.Popen('ps -C linuxcncsvr --no-header -o args'.split(),
                             stdout=subprocess.PIPE
                           )
    p,e = ps.communicate()
    if p is not None: p=p.decode()
    if e is not None: e=e.decode()

    if ps.returncode:
        print(_('get_linuxcnc_ini_file: stdout= %s') % p)
        print(_('get_linuxcnc_ini_file: stderr= %s') % e)
        return None

    ans = p.split()[p.split().index('-ini')+1]
    return ans

class GremlinView():
    """Implement a standalone gremlin with some buttons
       and provide means to embed using a glade ui file"""
    def __init__(self
                ,glade_file=None #None: use default ui
                ,parent=None
                ,width=None
                ,height=None
                ,alive=True
                ,gtk_theme_name="Follow System Theme"
                ):

        self.alive = alive
        linuxcnc_running = False
        if ini_check():
            linuxcnc_running = True

        if not linuxcnc_running:
            print("gremlin_view: LinuxCNC must be running")
            sys.exit(1)

        if (glade_file == None):
            glade_file = os.path.join(g_ui_dir,'gremlin_view.ui')

        print("gremlin_view glade_file=",glade_file)

        bldr = Gtk.Builder()
        try:
            bldr.add_from_file(glade_file)
        except glib.GError as detail:
            print('\nGremlinView:%s\n' % detail)
            raise glib.GError(detail) # re-raise

        # required objects:
        self.topwindow = bldr.get_object('gremlin_view_window')
        self.gbox      = bldr.get_object('gremlin_view_box')
        self.halg      = bldr.get_object('gremlin_view_hal_gremlin')

        #self.halg.show_lathe_radius = 1 # for test, hal_gremlin default is Dia

        if not linuxcnc_running:
            # blanks display area:
            self.halg.set_has_window(False)

        # radiobuttons for selecting view: (expect at least one)
        select_view_letters = ['p','x','y','z','z2']
        for vletter in select_view_letters:
            try:
                obj = bldr.get_object('select_' + vletter + '_view')
                self.my_view = vletter # first letter found is initial view
                break
            except:
                continue

        check_button_objects = ['enable_dro'
                               ,'show_machine_speed'
                               ,'show_distance_to_go'
                               ,'show_limits'
                               ,'show_extents'
                               ,'show_tool'
                               ,'show_metric'
                               ]
        for objname in check_button_objects:
            obj = bldr.get_object(objname)
            if obj is not None:
                setattr(self,'objname',obj)
                obj.set_active(True)
            else:
                if g_verbose:
                    print('%s: Optional object omitted <%s>'
                          % (__file__,objname))

        # show_metric: use INI file
#FIXME  show_metric,lunits s/b mandatory?
        try:
            objname = 'show_metric'
            self.show_metric = bldr.get_object('show_metric')
            lunits = self.halg.inifile.find('TRAJ','LINEAR_UNITS')
        except AttributeError:
            if g_verbose:
                print('%s: Problem for <%s>' % (__file__,objname))

        if linuxcnc_running:
            if   lunits == 'inch':
                self.halg.metric_units = False
            elif lunits == 'mm':
                self.halg.metric_units = True
            else:
                raise AttributeError('%s: unknown [TRAJ]LINEAR_UNITS] <%s>'
                                     % (__file__,lunits))

        if self.halg.get_show_metric():
            self.show_metric.set_active(True)
        else:
            self.show_metric.set_active(False)

        if alive:
            bldr.connect_signals(self)
            # todo: to remove other signals on halg:
            # bldr.disconnect(integer_handle_id)
            # bldr.disconnect_by_func('func_name')
            # bldr.handler_disconnect()

        minwidth  = 300 # smallest size
        minheight = 300 # smallest size

        if (width  is None):
            width = minwidth
        else:
            width = int(width)

        if (height is None):
            height = minheight
        else:
            height = int(height)

        if width  < minwidth:
            width  = minwidth
        if height < minheight:
            height = minheight

        # err from gremlin if omit this
        self.halg.width  = width
        self.halg.height = height
        self.halg.set_size_request(width,height)

        # self.x,self.y used in conjunction with pan buttons
        # but using mouse may change internal values in gremlin
        # resulting in unexpected movement if both mouse and
        # pan buttons are used
        self.x = 0
        self.y = 0

        #  prevent flashing topwindow
        self.topwindow.iconify()
        self.topwindow.show_all()
        self.topwindow.hide()

        self.preview_file(None)
        if linuxcnc_running:
            try:
                self.preview_file(None)
            except linuxcnc.error as detail:
                print('linuxcnc.error')
                print('        detail=',detail)

        try:
            self.last_file = self.halg._current_file
        except AttributeError:
            self.last_file = None
        self.last_file_mtime = None

        self.parent = parent
        if self.parent is None:
            # topwindow (standalone) application
            # print "TOP:",gtk_theme_name
            screen   = self.topwindow.get_screen()
        else:
            # print "REPARENT:",gtk_theme_name
            screen   = self.halg.get_screen()

        # not valid for Gtk3:
        # settings = Gtk.settings_get_for_screen(screen)
        # systname = settings.get_property("gtk-theme-name")
        # if (   (gtk_theme_name is None)
        #     or (gtk_theme_name == "")
        #     or (gtk_theme_name == "Follow System Theme")):
        #     gtk_theme_name = systname
        # settings.set_string_property('gtk-theme-name',gtk_theme_name,"")

        self.topwindow.connect('destroy',self._topwindowquit)
        self.topwindow.show_all()
        self.running = True

        if self.last_file is not None:
            self.topwindow.set_title(g_progname
                      + ': ' + os.path.basename(self.last_file))
            self.last_file_mtime = datetime.datetime.fromtimestamp(
                                   os.path.getmtime(self.last_file))

        self.ct = 0
        if self.parent is None: self.topwindow.deiconify()
        self._periodic('BEGIN')
        GLib.timeout_add_seconds(g_periodic_secs,self._periodic,'Continue')
        # or use GLib.timeout_add() interval units in mS

    def _periodic(self,arg):
        # print "_periodic:",self.ct,arg
        self.ct +=1
        self.halg.poll()

        if (self.parent is not None) and (self.ct) == 2:
            # not sure why delay is needed for reparenting
            # but without, the display of the (rgb) axes
            # and the cone to not appear in gremlin
            # print "REPARENT:",self.gbox, self.parent
            #-----------------------------------------------------------------------------
            # determine if glade interface designer is running
            # to avoid assertion error:
            # gtk_widget_reparent_fixup_child: assertion failed: (client_data != NULL)
            is_glade = False
            if 'glade' in sys.argv[0] and 'gladevcp' not in sys.argv[0]:
                for d in os.environ['PATH'].split(':'):
                    f = os.path.join(d,sys.argv[0])
                    if (    os.path.isfile(f)
                        and os.access(f, os.X_OK)):
                        is_glade = True
                        break
            #-----------------------------------------------------------------------------
            if (not is_glade):
                self.gbox.reparent(self.parent)
            self.gbox.show_all()
            self.gbox.connect('destroy',self._gboxquit)
            return True

        try:
            current_file = self.halg._current_file
        except AttributeError:
            current_file = None
        if current_file is None:
            return True # keep trying _periodic()
        current_file_mtime = datetime.datetime.fromtimestamp(
                               os.path.getmtime(current_file))
        if (   current_file       != self.last_file
            or current_file_mtime != self.last_file_mtime):
            # print('old,new',self.last_file_mtime,current_file_mtime)
            self.last_file       = current_file
            self.last_file_mtime = current_file_mtime
            self.halg.hide()
            self.halg.load()
            getattr(self.halg,'set_view_%s' % self.my_view)()
            self.halg.show()
            if self.topwindow is not None:
                self.topwindow.set_title(g_progname
                           + ': ' + os.path.basename(self.last_file))
        return True # repeat _periodic()

    def preview_file(self,filename):
        self.halg.hide()
        # handle exception in case glade is running
        try:
            self.halg.load(filename or None)
        except Exception as detail:
            if self.alive:
                print("file load fail:",Exception,detail)
            pass
        getattr(self.halg,'set_view_%s' % self.my_view)()
        self.halg.show()

    def _gboxquit(self,w):
        self.running = False # stop periodic checks

    def _topwindowquit(self,w):
        self.running = False # stop periodic checks
        Gtk.main_quit()

    def expose(self):
        self.halg.expose()

    def on_zoomin_pressed(self,w):
        while w.get_state() == Gtk.StateType.ACTIVE:
            self.halg.zoomin()
            time.sleep(g_move_delay_secs)
            Gtk.main_iteration_do(blocking=0)

    def on_zoomout_pressed(self,w):
        while w.get_state() == Gtk.StateType.ACTIVE:
            self.halg.zoomout()
            time.sleep(g_move_delay_secs)
            Gtk.main_iteration_do(blocking=0)

    def on_pan_x_minus_pressed(self,w):
        while w.get_state() == Gtk.StateType.ACTIVE:
            self.x -= g_delta_pixels
            self.halg.translate(self.x,self.y)
            time.sleep(g_move_delay_secs)
            Gtk.main_iteration_do(blocking=0)

    def on_pan_x_plus_pressed(self,w):
        while w.get_state() == Gtk.StateType.ACTIVE:
            self.x += g_delta_pixels
            self.halg.translate(self.x,self.y)
            time.sleep(g_move_delay_secs)
            Gtk.main_iteration_do(blocking=0)

    def on_pan_y_minus_pressed(self,w):
        while w.get_state() == Gtk.StateType.ACTIVE:
            self.y += g_delta_pixels
            self.halg.translate(self.x,self.y)
            time.sleep(g_move_delay_secs)
            Gtk.main_iteration_do(blocking=0)

    def on_pan_y_plus_pressed(self,w):
        while w.get_state() == Gtk.StateType.ACTIVE:
            self.y -= g_delta_pixels
            self.halg.translate(self.x,self.y)
            time.sleep(g_move_delay_secs)
            Gtk.main_iteration_do(blocking=0)

    def on_clear_live_plotter_clicked(self,w):
        self.halg.clear_live_plotter()

    def on_enable_dro_clicked(self,w):
        if w.get_active():
            self.halg.enable_dro = True
        else:
            self.halg.enable_dro = False
        self.expose()

    def on_show_machine_speed_clicked(self,w):
        if w.get_active():
            self.halg.show_velocity = True
        else:
            self.halg.show_velocity = False
        self.expose()

    def on_show_distance_to_go_clicked(self,w):
        if w.get_active():
            self.halg.show_dtg = True
        else:
            self.halg.show_dtg = False
        self.expose()

    def on_show_limits_clicked(self,w):
        if w.get_active():
            self.halg.show_limits = True
        else:
            self.halg.show_limits = False
        self.expose()

    def on_show_extents_clicked(self,w):
        if w.get_active():
            self.halg.show_extents_option = True
        else:
            self.halg.show_extents_option = False
        self.expose()

    def on_show_tool_clicked(self,w):
        if w.get_active():
            self.halg.show_tool = True
        else:
            self.halg.show_tool = False
        self.expose()

    def on_show_metric_clicked(self,w):
        if w.get_active():
            self.halg.metric_units = True
        else:
            self.halg.metric_units = False
        self.expose()

    def on_select_p_view_clicked(self,w):
        self.set_view_per_w(w,'p')

    def on_select_x_view_clicked(self,w):
        self.set_view_per_w(w,'x')

    def on_select_y_view_clicked(self,w):
        self.set_view_per_w(w,'y')

    def on_select_z_view_clicked(self,w):
        self.set_view_per_w(w,'z')

    def on_select_z2_view_clicked(self,w):
        self.set_view_per_w(w,'z2')

    def set_view_per_w(self,w,vletter):
        self.my_view = vletter
        getattr(self.halg,'set_view_%s' % vletter)()

#-----------------------------------------------------------------------------
# Standalone (and demo) usage:
def standalone_gremlin_view():

    import getopt
    #---------------------------------------
    def usage(msg=None):

        print(("""\n
Usage:   %s [options]\n
Options: [-h | --help]
         [-v | --verbose]
         [-W | --width]  width
         [-H | --height] height
         [-f | --file]   glade_file

Note: linuxcnc must be running on same machine
""") % g_progname)
        if msg:
            print('\n%s' % msg)
    #---------------------------------------

    glade_file  = None
    width       = None
    height      = None
    vbose       = False
    try:
        options,remainder = getopt.getopt(sys.argv[1:]
                                         , 'f:hH:vW:'
                                         , ['file='
                                           ,'help'
                                           ,'width='
                                           ,'height='
                                           ]
                                         )
    except getopt.GetoptError as msg:
        usage()
        print('GetoptError: %s' % msg)
        sys.exit(1)
    for opt,arg in options:
        if opt in ('-h','--help'):
            usage(),sys.exit(0)
        if opt in ('-v','--verbose'):
            g_verbose = True
            continue
        if opt in ('-W','--width' ): width=arg
        if opt in ('-H','--height'): height=arg
        if opt in ('-f','--file'):   glade_file=arg
    if remainder:
        usage('unknown argument:%s' % remainder)
        sys.exit(1)

    try:
        g = GremlinView(glade_file=glade_file
                       ,width=width
                       ,height=height
                       )
        Gtk.main()
    except linuxcnc.error as detail:
        Gtk.main()
        print('linuxcnc.error:',detail)
        usage()

# vim: sts=4 sw=4 et
