#! /usr/bin/env python
#
# ==================================================================
#
# LSST
#
# High level control of the XYZ used on the LSST testbench
# Tuned for the spot projector
# XML-RPC server to control it remotely
# default port: 8201
#
# Authors: Laurent Le Guillou 
# (recycled from DICE control system code)
#
# ==================================================================

import sys
import os, os.path
import time
import subprocess
import sys, os 
import signal
import shutil
import socket # to get the hostname
import datetime

# ==================================================================

import logging
#logging.basicConfig(level=logging.DEBUG,format='%(asctime)s: %(message)s')

# ==================================================================

import inspect
import xmlrpclib
from SimpleXMLRPCServer import SimpleXMLRPCServer, list_public_methods

# ==================================================================

# DEFAULT_HOSTNAME = "lpnlsst"
# DEFAULT_HOSTNAME = "lpnlsstbench.in2p3.fr"
DEFAULT_HOSTNAME = "134.158.155.98"
DEFAULT_PORT     = 8201
SERVER_HOSTNAME = os.getenv("XYZ_SERVER_HOSTNAME", DEFAULT_HOSTNAME)
SERVER_PORT = int(os.getenv("XYZ_SERVER_PORT", DEFAULT_PORT))

# ==================================================================

class XYZServer(SimpleXMLRPCServer):
    def serve_forever(self):
	self.quit = 0
	while not self.quit:
	    self.handle_request()

    # def __getattr__(self, name):
    #     if name == '__doc__':
    #         return 'sub special'
    #     else:  
    #         raise AttributeError, name

# ==================================================================

import lsst.instruments.pollux.xyz as xyz

# ==================================================================

class XYZremote(object):
    
    instance = None

    # --------------------------------------------------------------

    def __new__(cls, *args, **kargs):
        if cls.instance is None: 
            cls.instance = object.__new__(cls, *args, **kargs)
        return cls.instance
    
    def __init__(self,
                 ports = xyz.XYZ.default_ports, 
                 serials = xyz.XYZ.default_serials,
                 axis_ids = xyz.XYZ.default_axis_ids,
                 debug = True):

        self.state = 1
        self.XYZ = xyz.XYZ(ports = ports,
                           serials = serials,
                           axis_ids = axis_ids,
                           debug = debug)

    # --------------------------------------------------------------

    def status(self):
        """
        Return the status of the system.
        """
        return self.state 

    # --------------------------------------------------------------

    def open(self):
        """
        Open the connection with the XYZ motors.
        """
        logging.info("XYZ.open() called.")
        self.XYZ.open()
        logging.info("XYZ.open() done.")
        return True

    def close(self):
        """
        Close the connection with the XYZ motors.
        """ 
        logging.info("XYZ.close() called.")
        self.XYZ.close()
        logging.info("XYZ.close() done.")
        return True

    def checkConnection(self):
        """
        Check the connection.
        Returns a string with the serial numbers of the 3(4) motors.
        """
        logging.info("XYZ.checkConnection() called.")

        serials = []
        result = "Pollux XYZ(A): " # should contain "XYZ"

        for ax in ['x','y','z','a']:
            logging.info('ax = %s' % ax)
            if not(self.XYZ.axes.has_key(ax)): continue
            if self.XYZ.axes[ax] == None: continue
            #
            ax_serial = self.XYZ.axes[ax].get_serial()
            logging.info( "  XYZ : %s serial = %s" % 
                          ( ax.upper(), ax_serial ) )
            serials.append(ax_serial)
            result += ( (" %s: " % ax.upper()) + ax_serial )

        logging.info("  XYZ : serials = %s" % result)
        logging.info("XYZ.checkConnection() done.")
        return result

    # ----------------------- XYZ homing procedure -----------------

    def home(self, park=True):
        """
        Home the three motors, move up and down each of the 3 motors,
        to test the mechanical motor stops and reset the motor zeros.  
        Should be called once at the begining of a run.
        
        WARNING: blocking call. Takes about 3 min.
        
        see: XYZ.home()
        """
        logging.info("XYZ.home() called.")
        self.XYZ.home(park = park)
        logging.info("XYZ.home() done.")
        return True

    # ----------------------- XYZ parking procedure -----------------

    def park(self):
        """
        Park the XYZ out of the light beam.
        """ 
        logging.info("XYZ.park() called.")
        self.XYZ.park()
        logging.info("XYZ.park() done.")
        return True

    # ----------------------- XYZ movements ------------------------

    def move(self, moves, wait = True, check = True):
        """
        Move the XYZ to the given position (or offset).
        'moves' is a dictionary of the following format:
              {'x': 12.34, 'y': -23.1, 'dz' : -45.0 }
        This function can do relative (dx,dy,dz,da) 
        and absolute (x,y,z,a) movements.

        By default, check is and *MUST* be left to True.
        """

        logging.info("XYZ.move(%s) called." % str(moves))
        self.XYZ.move(wait=wait,check=check, **moves)
        logging.info("XYZ.move(%s) done." % str(moves))
        return True

    # ----------------------- XYZ movements ------------------------

    def get_position(self):
        """
        Return the current position on the XYZ(A) motors 
        (as a dictionary).
        It is then possible to do move(**get_position())
        """
        logging.info("XYZ.get_position() called.")
        pos = self.XYZ.get_position()
        logging.info("XYZ position is: %s" % str(pos))
        logging.info("XYZ.get_position() done.")
        return pos

    # position = property(get_position, doc = "XYZ current position")

    # ----------------------- Introspection ------------------------

    def _listMethods(self):
        logging.info("XYZ._listMethods() called.")
        methods = list_public_methods(self)
        logging.info("XYZ._listMethods() done.")
        return methods
        # return [ "status",
        #          "open",
        #          "close",
        #          "home",
        #          "get_position",
        #          "move",
        #          "park",
        #          "quit",
        #          "__dir__",
        #          "system.listMethods",
        #          "system.methodHelp" ]

    def _methodHelp(self, method):
        f = getattr(self, method)
        return inspect.getdoc(f)

# ==================================================================

# ------------- Missing functions and tests ------------------------

def server_quit():
    logging.info("Server going down.")
    server.quit = 1
    return 1

# ==================================================================

# ------------- Daemonization ---------------------------------------
# Default working directory for the daemon.
WORKDIR = "/"

def redirect_stream(system_stream, target_stream):
    """ 
    Redirect a system stream to a specified file.
    
    'system_stream' is a standard system stream such as
    ''sys.stdout''. 'target_stream' is an open file object that
    should replace the corresponding system stream object.

    If 'target_stream' is ''None'', defaults to opening the
    operating system's null device and using its file descriptor.
    
    """

    if target_stream is None:
        target_fd = os.open(os.devnull, os.O_RDWR)
    else:
        target_fd = target_stream.fileno()
    os.dup2(target_fd, system_stream.fileno())


def daemonize(options, args):
    try: 
        pid = os.fork() 
        if pid > 0:
            # exit first parent
            sys.exit(0) 
    except OSError, e: 
        print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror) 
        sys.exit(1)

    # Become leader of a new session to decouple from the controlling tty
    os.setsid() 
    # We stay with the same umask
    # os.umask(0) 

    # do second fork and kill session leader to ensure one will never get attach to a TTY.
    try: 
        pid = os.fork() 
        if pid > 0:
            # exit from second parent, print eventual PID before
            print "Starting server as daemon with PID %d ..." % pid 
            sys.exit(0) 
    except OSError, e: 
        print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror) 
        sys.exit(1) 
    

    # start the daemon main
    main(options, args) 


def main(options, args):

    logging.basicConfig(filename=options.log_file, 
                        level=logging.DEBUG, format='%(asctime)s: %(message)s')

    # Now that logging is set up decouple from parent environnement
    redirect_stream(sys.stdin, None)
    redirect_stream(sys.stdout, None)
    redirect_stream(sys.stderr, None)    

    # General Control Functions 
    server.register_function(XYZ.status,       "status")
    server.register_function(XYZ.open,         "open")
    server.register_function(XYZ.close,        "close")
    server.register_function(XYZ.checkConnection, "checkConnection")

    # XYZ motion
    server.register_function(XYZ.home,         "home")
    server.register_function(XYZ.get_position, "get_position")
    # server.register_function(XYZ.position,     "position")
    server.register_function(XYZ.move,         "move")
    server.register_function(XYZ.park,         "park")

    # misc 
    server.register_function(server_quit,      "quit")

    # for remote introspection (tab completion with ipython)
    server.register_function(XYZ._listMethods,  "__dir__")
    server.register_function(XYZ._listMethods,  "system.listMethods")
    server.register_function(XYZ._listMethods,  "trait_names")
    server.register_function(XYZ._listMethods,  "_getAttributeNames")
    # TODO: implement: system.methodSignature
    server.register_function(XYZ._methodHelp,   "system.methodHelp")

    logging.info("Server going up.")
    server.serve_forever()

# ------------------------------------------------------------------

# ==================================================================

if __name__ == '__main__':
    now = datetime.datetime.now()
    logdir = os.path.join(os.getenv("HOME"), "logs")
    logname = os.path.join(logdir,
                           "xyz-server-%s.log" % now.date().isoformat())

    # recreate the symlink xyz-server.log 
    logsymlink = os.path.join(logdir, "xyz-server.log")
    if os.path.islink(logsymlink):
        try:
            os.unlink(logsymlink)
            os.symlink(logname, logsymlink)
        except OSError:
            pass

    import optparse
    parser = optparse.OptionParser(usage="""
%prog [-l log-file] [-d] 

Start the XYZ control server. 
""")
    parser.add_option('-d', '--daemon', default=False, action='store_true',
                      help='Run as a background daemon')
    parser.add_option('-p', '--port', default=SERVER_PORT, action='store', type='int',
                      help='Listen on port')
    parser.add_option('-H', '--hostname', default=SERVER_HOSTNAME, action='store',
                      help='Listen adress')
    parser.add_option('-l', '--log-file', default=logname, action='store',
                      help='Specify a file for daemon logs.')

    (options, args) = parser.parse_args()

    # ------------- Initialize Head ---------------

    XYZ = XYZremote()

    # ------------- LEDHEAD Server ----------------
    # HOSTNAME = DEFAULT_HOSTNAME
    # PORT = DEFAULT_PORT

    SERVER_HOSTNAME = options.hostname
    SERVER_PORT = options.port
    
    server = XYZServer((SERVER_HOSTNAME, SERVER_PORT))
    server.register_introspection_functions()

    print "XYZ: Listening on port %s:%d. Waiting for commands." % (SERVER_HOSTNAME, SERVER_PORT)
    
    if options.daemon:
        daemonize(options, args)
    else:
        main(options, args)


# ==================================================================
