#! /usr/bin/env python
#
# ==================================================================
#
# LSST
#
# Remote control (XPL-RPC) of the SBIG camera for the DICE testbench
# XML-RPC server to control it remotely
# default device: USB on lpnsbig
# default port: 8888
#
# Authors: Laurent Le Guillou 
# (recycled from DICE and LSST control system code)
#
# ==================================================================
import datetime
from exceptions import Exception
import inspect
import os, os.path
import logging
import numpy as np
import sbig
import signal
import shutil
import socket # to get the hostname
import struct
import subprocess
import sys
import time
import xmlrpclib


xmlrpclib.Marshaller.dispatch[type(0L)] = lambda _, v, w: w("<value><i8>%d</i8></value>" % v)

from SimpleXMLRPCServer import SimpleXMLRPCServer, list_public_methods

# DEFAULT_HOSTNAME = "192.168.132.100" ### To change
DEFAULT_HOSTNAME = "192.168.130.178"
DEFAULT_PORT     = 8788

SERVER_HOSTNAME = os.getenv("SBIG_SERVER_HOSTNAME", DEFAULT_HOSTNAME)
SERVER_PORT     = int(os.getenv("SBIG_SERVER_PORT", DEFAULT_PORT))


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

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


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

def logfunc(f):
    def wrapper(*args, **kwargs):
        logging.info("SBIG.%s called." % f.__name__)
        res = f(*args, **kwargs)
        logging.info("SBIG.%s done." % f.__name__)
        return res
    return wrapper
    
class SBIGRemote(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, debug = False):
        self.debug = debug
        self.ccdinfo = {}
        self.cfwc_model = 3
        self.filter_position = -1 # unknown

    # --------------------------------------------------------------
    @logfunc
    def status(self):
        """
        Return the status of the sbig camera.
        """
        status = sbig.query_command_status()
        return status

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

    @logfunc
    def open(self):
        """
        Open the SBIG connection and set up the SBIG.
        """
        logging.info("SBIG.open() called.")

        # Open the USB SBIG camera

        # === Opening the driver

        logging.info("Open the driver...")

        try:
            try:
                sbig.close_driver()
            except:
                pass
            sbig.open_driver()
            time.sleep(1)
            devices = sbig.query_usb() # return list of USB SBIG cameras found
            logging.info(str(devices))
            # There is a bug with this last function
        except sbig.SBIGDriverError, msg:
            logging.error(msg)
            sys.exit(2)

        # === Opening the device

        logging.info("Open the device...")

        try:
            try:
                sbig.close_device()
            except:
                pass
            sbig.open_device(sbig.DEV_USB)
        except sbig.SBIGDriverError, msg:
            logging.error("Unable to open SBIG camera device")
            logging.error("Error: %s", msg)
            sys.exit(3)


        # === Establish link with the device
        #
        # it is often needed to repeat establish_link until it succeed.
        # (Ref: Jan Soldan)

        logging.info("Establish the link with the camera...")

        tries=5
        camera_type = sbig.NO_CAMERA
        for i in xrange(tries):
            try:
                camera_type = sbig.establish_link()
                break
            except sbig.SBIGDriverError, msg:
                logging.error(msg + "Trying again...")
                pass
        if (camera_type == sbig.NO_CAMERA):
            logging.error("Cannot establish link with the camera.")
            sys.exit(4)

        # Open the filter wheel

        sbig.cfwc_open_device()
        logging.info(str(sbig.cfwc_query(model=self.cfwc_model)))

        # === Get infos about the CCD and the available readout modes 
        #
            
        logging.info("Get CCD infos...")
        self.ccdinfo = sbig.get_ccd_info()
        logging.info("  IMAGING CCD : %s", str(self.ccdinfo))
        logging.info("SBIG.open() done.")
        return True

    @logfunc
    def close(self):
        """
        Close the connection with the SBIG.
        """ 
        logging.info("SBIG.close() called.")
        sbig.cfwc_close_device()
        sbig.close_device()
        sbig.close_driver()
        logging.info("SBIG.close() done.")
        return True

    # def reset(self):
    #     """
    #     Reset the SBIG (hard reset).
    #     (with the exception of all remote interface settings).
    #     """
    #     logging.info("SBIG.reset() called.")

    #     logging.info("SBIG.reset() done.")
    #     return True

    # def clear(self):
    #     """
    #     Clear the instrument status.
    #     """ 
    #     logging.info("SBIG.clear() called.")

    #     logging.info("SBIG.clear() done.")
    #     return True
    
    # ----------------------- SBIG commands -----------------------------
    @logfunc
    def exposure(self, exposure, open_shutter, auto_freeze):
        """
        Take a frame of exposure time 'exposure' in seconds.
        Open the shutter if shutter = True;
        Leave the shutter closed if shutter = False.
        If autofreeze is True, AUTOFREEZE the Peltier regulation
        during readout (a good idea!).
        Returns the pixel array.
        """

        if auto_freeze:
            logging.info("Switching to AUTOFREEZE mode during exposure.")
            sbig.set_temperature_regulation(sbig.REGULATION_ENABLE_AUTOFREEZE)


        # === get temperature information

        temp_status = sbig.query_temperature_status()


        logging.info("Starting a %2.1fs exposure...", exposure)

        utcnow = datetime.datetime.utcnow() # instant of exposure

        if open_shutter:
            shutter = sbig.SC_OPEN_SHUTTER
        else:
            shutter = sbig.SC_CLOSE_SHUTTER

        # Start exposure 
        sbig.start_exposure(ccd=sbig.CCD_IMAGING, 
                            exposure=exposure, shutter=shutter)

        # Waiting until the end of the exposure (by checking status)
        # sbig.query_command_status() gives status of the last command
        done = 0.0             # just for display
        wait_increment = 0.1   # just for display
        time.sleep(exposure)
        while (sbig.query_command_status() == sbig.STATUS_INTEGRATING):
            time.sleep(wait_increment)
            done += wait_increment
            print "%2.1f done..." % done

        logging.info("Ending exposure...")
        # ending exposure (can also be used to end it prematurely)
        sbig.end_exposure(ccd=sbig.CCD_IMAGING)

        # === Readout

        mode = 0  # readout mode (binning 1x1 i.e. not binned)
        fullheight = self.ccdinfo['modes'][mode]['height']
        fullwidth = self.ccdinfo['modes'][mode]['width']
        height=fullheight
        width=fullwidth
        top=0
        left=0

        logging.info("Starting readout in mode %d ...", mode)

        time_start = time.time()

        sbig.start_readout(ccd=sbig.CCD_IMAGING, mode=mode,
                           top=top, left=left,
                           height=height, width=width)

        # You may read line by line with readout_line(...),
        # or a block with readout_lines(...)
        # image is a numarray 2D-array.

        image = sbig.readout_lines(ccd=sbig.CCD_IMAGING, mode=mode,
                                   start=left, length=width, lines=height)


        logging.info("Readout complete.")

        sbig.end_readout(ccd=sbig.CCD_IMAGING)

        time_end = time.time()

        logging.info("Readout in %f s", (time_end - time_start))

        logging.info("Received frame : \n%s", str(image))

        logging.info("Image stats:")
        logging.info("    min = %f   max = %f", image.min(), image.max())
        # logging.info("    mean = %f   rms = %f", image.mean(), image.std())
        # logging.info("    median = %f", np.median(image))

        data = {
            # FITS keys
            'DATE-OBS': utcnow.isoformat(),
            'EXPTIME': exposure,
            'FILTER': self.filter_position,
            'INSTRUM': self.ccdinfo['name'],
            # temperature information (Peltier module)
            'TAMBIENT': temp_status['ambient_celsius'], 
            'REG-TCCD': temp_status['ccd_celsius'], 
            'REG-ENAB': temp_status['enabled'], 
            'REG-FROZ': temp_status['frozen'],
            'REG-POWR': temp_status['power'], 
            'REG-SETP': temp_status['setpoint_celsius'],
            # Image
            'shape': image.shape,
            'binarydata': xmlrpclib.Binary(image)}

        del image

        return data

    @logfunc
    def take_patch(self, top, bottom, left, right, exposure, mode):
        #top, left = pic[0]- int(height * 0.5), pic[1] - int(width * 0.5)
        #bottom, right = top + height, left + width
        #shutter = 2 if dark else 1
        shutter = 1
        utcnow = datetime.datetime.utcnow() # instant of exposure
        sbig.start_exposure(ccd=sbig.CCD_IMAGING, exposure=exposure, shutter=shutter)
        # Waiting until the end of the exposure (by checking status)                                                       
        # sbig.query_command_status() gives status of the last command                                                     
        time.sleep(exposure)
        wait_increment = .05
        while (sbig.query_command_status() == sbig.STATUS_INTEGRATING):
            time.sleep(wait_increment)
        sbig.end_exposure(ccd=sbig.CCD_IMAGING)

        top = max(0, top)
        left = max(0, left)
        right = min(self.ccdinfo['modes'][mode]['width'], right) if right else self.ccdinfo['modes'][mode]['width']
        bottom = min(self.ccdinfo['modes'][mode]['height'], bottom) if bottom else self.ccdinfo['modes'][mode]['height'] 
        width = right - left
        height = bottom - top
        #print top, left, width, height
        sbig.start_readout(ccd=sbig.CCD_IMAGING, mode=mode,
                           top=top, left=left,
                           height=height, width=width)
        image = np.zeros(shape=(height, width), dtype=np.uint16)
        for line in xrange(height):
            # you should repeat the next line to read every row
            tries = 5
            while (tries > 0):
                try:
                    l = sbig.readout_line(ccd=sbig.CCD_IMAGING, mode=mode,
                                          start=left, length=width)
                    break
                except sbig.SBIGDriverError, msg:
                    logging.error("line [%d]: %s" % (line, str(msg)))
                    tries -= 1
                    pass
    
            if (tries == 0):
                logging.error("error: timeout (5 times)")
    
            image[line] = l
        data = {
            # FITS keys
            'DATE-OBS': utcnow.isoformat(),
            'EXPTIME': exposure,
            'FILTER': self.filter_position,
            'INSTRUM': self.ccdinfo['name'],
            # Image
            'shape': image.shape,
            'binarydata': xmlrpclib.Binary(image)}
        del image
        return data


    @logfunc
    def image(self, exposure, auto_freeze=True):
         return self.exposure(exposure, open_shutter=True, auto_freeze=auto_freeze)

    @logfunc
    def dark(self, exposure, auto_freeze=True):
        return self.exposure(exposure, open_shutter=False, auto_freeze=auto_freeze)

    @logfunc
    def set_filter(self, position):
        """
        Set the filter on position 'position'.
        """
        sbig.cfwc_goto(model=self.cfwc_model, position=position)
        self.filter_position = position
        return True

    @logfunc
    def get_filter(self):
        """
        Return the filter position.
        """
        return self.filter_position

    @logfunc
    def get_temperature(self):
        """
        Return cooling status and temperature.
        """
        return sbig.query_temperature_status()
    
    @logfunc
    def set_temperature(self, mode, setpoint):
        """
        Set regulation mode and temperature (in celsius).
        mode values: 

               0 = sbig.REGULATION_OFF
               1 = sbig.REGULATION_ON
               2 = sbig.REGULATION_OVERRIDE
               3 = sbig.REGULATION_FREEZE
               4 = sbig.REGULATION_UNFREEZE
               5 = sbig.REGULATION_ENABLE_AUTOFREEZE
               6 = sbig.REGULATION_DISABLE_AUTOFREEZE

        """
        logging.info('Setting temperature : mode %d, setpoint %f' % (mode, setpoint))
        sbig.set_temperature_regulation(mode, setpoint_celsius=setpoint)
        return True

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

    def _listMethods(self):
        logging.info("SBIG._listMethods() called.")
        methods = list_public_methods(self)
        logging.info("SBIG._listMethods() done.")
        return methods

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

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

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

def server_quit():
    logging.info("Server going down.")
    sbig_instance.close()
    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
    if options.daemon:
        redirect_stream(sys.stdin,  None)
        redirect_stream(sys.stdout, None)
        redirect_stream(sys.stderr, None)    

    # General Control Functions 
    server.register_function(sbig_instance.status,          "status")
    server.register_function(sbig_instance.open,            "open")
    server.register_function(sbig_instance.close,           "close")
    # server.register_function(sbig_instancereset,          "reset")
    # server.register_function(sbig_instanceclear,          "clear")

    # Sbig commands
    server.register_function(sbig_instance.image,           "image")
    server.register_function(sbig_instance.dark,            "dark")
    server.register_function(sbig_instance.exposure,        "exposure")
    server.register_function(sbig_instance.take_patch,        "take_patch")
    server.register_function(sbig_instance.set_filter,      "set_filter")
    server.register_function(sbig_instance.get_filter,      "get_filter")
    server.register_function(sbig_instance.get_temperature, "get_temperature")
    server.register_function(sbig_instance.set_temperature, "set_temperature")
    server.register_function(server_quit,                   "quit")

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

    logging.info("Server going up.")
    # NRL & MB -- 2016-07-07 : 
    # we want the camera to initialize itself as soon as possible.
    sbig_instance.open()
    try:
        sbig_instance.set_temperature(sbig.REGULATION_ON, -10)
        logging.info('Camera initialized - ready to serve')
        server.serve_forever()
    except:
        logging.error('Error during server life')
        sbig_instance.close()


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

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

if __name__ == '__main__':

    # ------------- log file ----------------------

    now = datetime.datetime.utcnow()
    # logdir = os.path.join(os.getenv("HOME"), "logs")
    logdir = os.path.join("/data/sbig", "logs")
    logname = os.path.join(logdir,
                           ( "sbig-server-%d-%s.log" % 
                             ( SERVER_PORT,
                               now.isoformat().split('T')[0] ) ))

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

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

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

    (options, args) = parser.parse_args()

    # ------------- XML-RPC Server ----------------

    SERVER_HOSTNAME = options.hostname
    SERVER_PORT = int(options.port)

    if options.dummy:
        import fakesbig as sbig
    
    # ------------- Initialize Instrument ---------

    sbig_instance = SBIGRemote()

    server = SBIGServer((SERVER_HOSTNAME, SERVER_PORT))
    server.register_introspection_functions()

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

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




