#! /usr/bin/env python
#
# ==================================================================
#
# DICE/StarDICE 
#
# XML-RPC server to control the Takahashi EM-200 mount
# default port: 8810
#
# 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 numpy as np
from xmlrpclib import ServerProxy, Error

# ==================================================================
from temma import Temma, MountError

import ephem

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

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 = "134.158.155.98"
DEFAULT_HOSTNAME = "192.168.132.100"
DEFAULT_PORT     = 8810
SERVER_HOSTNAME = os.getenv("MOUNT_SERVER_HOSTNAME", DEFAULT_HOSTNAME)
SERVER_PORT = int(os.getenv("MOUNT_SERVER_PORT", DEFAULT_PORT))

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

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

def server_quit():
    logging.info("Exiting. Bye.")
    server.quit = 1
    return 1

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

# ==================================================================
class TemmaDummy(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, port = "/dev/temma",
                 debug = True):
        self.state = 1
        self.mount = None
        self.observer = ephem.Observer()
        self.fi = ServerProxy('http://localhost:8900/')
        # # name: "OHP"
        # # longitude: "+05d42m44s" # E
        # # latitude: "+43d55m54s"  # N
        # self.observer.long = "05:42:44"  # E
        # self.observer.lat  = "43:55:54"  # N
        # self.observer.elevation = 650.0 # m

    def set_observer(self, lon, lat, elev, name):
        logging.info("Temma.set_observer() called.")
        self.observer.lon = lon
        self.observer.lat = lat
        self.observer.elev = elev
        self.observer.name = name
        return True
    
    def status(self):
        return True
    
    def open(self):
        logging.info("Temma.open() called.")
        logging.info("Temma.open() done.")
        return True
    
    def get_version(self):
        logging.info("Temma.open() called.")
        logging.info("Temma.open() done.")
        return True

    def close(self):
        logging.info("Temma.close() called.")
        logging.info("Temma.close() done.")
        return True
    def send(self, cmd):
        logging.info("Temma.send(%s) called." % cmd)
        logging.info("Temma.send() done.")
        return "sent dummy"

    # ----------------------- setup: latitude, LST, zenith ----------

    def set_longitude(self, d, m, s):
        print "Temma.set_longitude(%f,%f,%f) called." % (d,m,s)
        logging.info("Temma.set_longitude(%f,%f,%f) called." % (d,m,s))
        logging.info("Temma.set_longiitude() done.")
        return True

    def set_latitude(self, d, m, s):
        logging.info("Temma.set_latitude(%f,%f,%f) called." % (d,m,s))
        logging.info("Temma.set_latitude() done.")
        return True

    def get_latitude(self):
        logging.info("Temma.get_latitude() called.")
        logging.info("Temma.get_latitude() done.")
        return 600

    def set_elevation(self, elevation): 
        logging.info("Temma.set_elevation() called.")
        logging.info("Temma.set_elevation() done.")
        return True

    def set_LST(self, hour, minute, sec):
        logging.info("Temma.set_LST(%f,%f,%f) called." % (hour, minute, sec))
        logging.info("Temma.set_LST() done.")
        return True

    def get_LST(self):
        logging.info("Temma.get_LST() called.")
        lst = self.observer.sidereal_time()
        logging.info("Temma.get_LST() done.")
        return str(lst).split(':')

    def zenith(self):
        logging.info("Temma.zenith() called.")
        logging.info("Temma.zenith() done.")
        return True

    def define_radec(self, hour, minute, sec,
                     deg, arcmin, arcsec):
        logging.info("Temma.define_radec() called.")
        logging.info("Temma.define_radec() done.")

        return True

    def set_UTC(self, date_str):
        #now = datetime.datetime.utcnow()
        logging.info("Setting UTC : ", date_str)
        return True

        # ----------------------- Temma movements ------------------------

    def get_radec(self):
        logging.info("Temma.get_radec() called.")
        logging.info("Temma.get_radec() done.")
        return 10, 10, 10, 10

    def goto_radec(self, hour, minute, sec,
                   deg, arcmin, arcsec):
        logging.info("Temma.goto_radec() called.")
        logging.info("Temma.goto_radec() done.")

        return True

    def goto(self, ra, dec):
        logging.info("High level Temma.goto() called.")
        self.fi.set_ra_dec(ra, dec)
        ra = str(ephem.hours(np.radians(ra)))
        hour, minute, sec = map(float, ra.split(':'))
        dec = str(ephem.degrees(np.radians(dec)))
        deg, arcmin, arcsec = map(float, dec.split(':'))
        logging.info("High level Temma.goto() called.")
        logging.info("ra=%s dec=%s"%(str(ra), str(dec)))
        logging.info("ra=%s:%s:%s dec=%s:%s:%s"%(hour, minute, sec,deg, arcmin, arcsec))
        logging.info("High level Temma.goto() done.")

        return True


    # ---------- Stop goto ----------------------------------- 

    def stop(self):
        logging.info("Temma.stop() called.")
        logging.info("Temma.stop() done.")
        return True

    def get_side(self):
        """
        Tell us if the telescope is on the East(E) or West(W) side.
        """
        logging.info("Temma.get_side() called.")
        logging.info("Temma.get_side() done.")
        return True
    
    def flip_side(self):
        logging.info("Temma.flip_side() called.")
        logging.info("Temma.flip_side() done.")
        return True

   # ---------- Is the telescope doing a goto ? ------------- 

    def is_pointing(self):
        logging.info("Temma.is_pointing() called.")
        logging.info("Temma.is_pointing() done.")
        return False

    def move(self, direction, fast = False):
        logging.info("Temma.move() called.")
        logging.info("Temma.move() done.")
        return True


    # ---------- Standby mode (on = RA motors off) ----------- 

    def standby(self, on=True):
        logging.info("Temma.standby(%d) called." % on)
        logging.info("Temma.standby(%d) done." % on)
        return True

    def is_standby(self):
        logging.info("Temma.is_standby() called.")
        logging.info("Temma.is_standby() done.")
        return True
    # ----------------------- Introspection ------------------------

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

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

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

class TemmaRemote(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,
                 port = "/dev/temma",
                 debug = True):

        self.state = 1
        self.observer = ephem.Observer()

        self.mount = Temma(port = port,
                                 debug = debug)

        # # name: "OHP"
        # # longitude: "+05d42m44s" # E
        # # latitude: "+43d55m54s"  # N
        # self.observer.long = "05:42:44"  # E
        # self.observer.lat  = "43:55:54"  # N
        # self.observer.elevation = 650.0 # m

    def set_observer(self, lon, lat, elev, name):
        """
        lon and lat are in radians
        """
        self.observer.long = lon
        self.observer.lat = lat
        d,m,s=str(self.observer.lat).split(':')
        self.set_latitude(float(d), float(m), float(s))
        self.observer.elev = elev
        self.observer.name = name
        self.update_LST()
        return True
    
    # --------------------------------------------------------------

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

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

    def open(self):
        """
        Open the connection with the Takahashi mount.
        """
        logging.info("Temma.open() called.")
        self.mount.open()
        logging.info("Temma.open() done.")
        return True

    def get_version(self):
        """
        Open the connection with the Takahashi mount.
        """
        logging.info("Temma.get_version() called.")
        self.mount.get_version()
        logging.info("Temma.get_version() done.")
        return True

    def close(self):
        """
        Close the connection with the Takahashi mount.
        """ 
        logging.info("Temma.close() called.")
        self.mount.close()
        logging.info("Temma.close() done.")
        return True

    # ----------------------- Temma basic send----------------------

    def send(self, cmd):
        """
        Send a low level command to the Takahashi mount.
        """ 
        logging.info("Temma.send(%s) called." % cmd)
        answer = self.mount.send(cmd)
        logging.info("Temma.send() done.")
        return answer

    # ----------------------- setup: latitude, LST, zenith ----------

    def set_longitude(self, d, m, s):
        """
        Set the longitude for the observing site (ephem).
        """
        print "Temma.set_longitude(%f,%f,%f) called." % (d,m,s)
        logging.info("Temma.set_longitude(%f,%f,%f) called." % (d,m,s))
        self.observer.long = "%02d:%02d:%02d" % (d,m,s)
        logging.info("Temma.set_longitude() done.")
        return True
    
    def set_latitude(self, d, m, s):
        """
        Tell the latitude of the observing site to the Takahashi mount.
        Set the latitude for the observing site (ephem).
        """ 
        logging.info("Temma.set_latitude(%f,%f,%f) called." % (d,m,s))
        self.observer.lat = "%02d:%02d:%02d" % (d,m,s)
        answer = self.mount.set_latitude(d,m,s)
        logging.info("Temma.set_latitude() done.")
        return answer
    
    def get_latitude(self):
        """
        Get the latitude of the observing site (see set_latitude).
        """ 
        logging.info("Temma.get_latitude() called.")
        answer = self.mount.get_latitude()
        logging.info("Temma.get_latitude() done.")
        return answer

    def set_elevation(self, elevation):
        """
        Set the elevation of the observing site (in meter).
        """ 
        logging.info("Temma.set_elevation() called.")
        self.observer.elevation = elevation
        logging.info("Temma.set_elevation() done.")
        return True

    def set_LST(self, hour, minute, sec):
        """
        Set the sidereal time for the Takahashi mount.
        """ 
        logging.info("Temma.set_LST(%f,%f,%f) called." % (hour, minute, sec))
        answer = self.mount.set_LST(hour, minute, sec)
        logging.info("Temma.set_LST() done.")
        return answer
        
    def get_LST(self):
        """
        Get the sidereal time loaded in the Takahashi mount.
        """ 
        logging.info("Temma.get_LST() called.")
        answer = self.mount.get_LST()
        logging.info("Temma.get_LST() done.")
        return answer

    def zenith(self):
        """
        Initialisation: put the telescope to zenith
        and call this method.
        """
        logging.info("Temma.zenith() called.")
        self.update_LST() # Important!!!
        answer = self.mount.zenith()
        self.update_LST() # Important!!!
        logging.info("Temma.zenith() done.")
        return answer

    def define_radec(self,
                     hour, minute, sec,
                     deg, arcmin, arcsec):
        """
        Set current RA, DEC (initialisation).
        """
        logging.info("Temma.define_radec() called.")


        self.update_LST() # Important!!!

        try:
            answer = self.mount.define_radec(hour, minute, sec,
                                             deg, arcmin, arcsec)
        except MountError, msg:
            logging.info("Temma.define_radec() failed: returns [%s]." % msg)
            return False

        logging.info("Temma.define_radec() done.")

        return answer

    def set_UTC(self, date_str):
        """
        Resync the UTC time of the mount-server.
        (Important for the Raspberry Pi which has no real-time clock).
        """
        # invoke the Unix command:
        # sudo date -u [MMDDhhmm[[CC]YY][.ss]]
        #now = datetime.datetime.utcnow()
        #date_str = "%02d%02d%02d%02d%04d.%02d" % (now.month, now.day, now.hour, now.minute, now.year, now.second)
        
        ret = os.system("sudo date -u %s" % date_str)
        if ret != 0:
            return False

        return True

    
    def update_LST(self):
        """
        Compute the local sidereal time
        and update it in the Takahashi mount.
        (and for ephem observer).
        """

        logging.info("Temma.update_LST() called.")

        self.observer.date = datetime.datetime.utcnow()
        lst = self.observer.sidereal_time()

        # un peu pourri...
        lst_h, lst_m, lst_s = [int(float(i)) for i in str(lst).split(":")[0:3]]
        
        self.mount.set_LST(lst_h, lst_m, lst_s)

        logging.info("Temma.update_LST() done.")

        return True

    
    # ----------------------- Temma movements ------------------------

    def get_radec(self):
        """
        Return current RA, DEC, E/W, H???
        """
        logging.info("Temma.get_radec() called.")
        answer = self.mount.get_radec()
        logging.info("Temma.get_radec() done.")
        return answer

    def goto_radec(self,
                   hour, minute, sec,
                   deg, arcmin, arcsec):
    
        logging.info("Temma.goto_radec() called.")

        try:
            answer = self.mount.goto_radec(hour, minute, sec,
                                           deg, arcmin, arcsec)
        except MountError, msg:
            logging.info("Temma.goto_radec() failed: returns [%s]." % msg)
            return False

        logging.info("Temma.goto_radec() done.")

        return answer
        

    def goto(self, ra, dec):
        """
        ra, dec in degrees.
        High level goto() method.
        Recomputes the LST, and then repoint the telescope.
        """
        ra = str(ephem.hours(np.radians(ra)))
        hour, minute, sec = map(float, ra.split(':'))
        dec = str(ephem.degrees(np.radians(dec)))
        deg, arcmin, arcsec = map(float, dec.split(':'))
        logging.info("High level Temma.goto() called.")
        
        # A faire avant chaque observation
        self.update_LST()
        result = self.goto_radec(hour, minute, sec,
                                 deg, arcmin, arcsec)

        logging.info("High level Temma.goto() done.")

        return result
    
    # ---------- Stop goto ----------------------------------- 
    
    def stop(self):
        """
        Stop an on-going command.
        """
        logging.info("Temma.stop() called.")
        answer = self.mount.stop()
        logging.info("Temma.stop() done.")
        return answer
    
    # ---------- Is it on E/W side ? ------------------------- 

    def get_side(self):
        """
        Tell us if the telescope is on the East(E) or West(W) side.
        """
        logging.info("Temma.get_side() called.")
        answer = self.mount.get_side()
        logging.info("Temma.get_side() done.")
        return answer

    # ---------- Tell the mount to change side (E/W) --------- 

    def flip_side(self):
        """
        Switch the mount side (E/W). Does not move the mount,
        but tell it on which side it actually is.
        IMPORTANT to avoid collision of the telescope tube.
        See also the zenith() method.
        When turned on, the mount believes it is on the West side.
        Please check with get_side() after!
        """
        logging.info("Temma.flip_side() called.")
        answer = self.mount.flip_side()
        logging.info("Temma.flip_side() done.")
        return answer

    # ---------- Is the telescope doing a goto ? ------------- 

    def is_pointing(self):
        """
        A goto command is ongoing ?
        """
        logging.info("Temma.is_pointing() called.")
        answer = self.mount.is_pointing()
        logging.info("Temma.is_pointing() done.")
        return answer

    # ---------- Is the telescope doing a goto ? ------------- 

    def move(self, direction, fast = False):
        """
        Move N/S/W/E as when using the handset.
        speed is defined by the correction speed.
        Depending on the E/W position of the scope,
        the S/N direction is reversed.
        """
        logging.info("Temma.move() called.")
        answer = self.mount.move(direction, fast)
        logging.info("Temma.move() done.")
        return answer

    # ---------- Standby mode (on = RA motors off) ----------- 

    def standby(self, on=True):
        """
        Initialisation: put the telescope RA motors on standby
        standby ON  = RA OFF
        standby OFF = RA ON
        """
        logging.info("Temma.standby(%d) called." % on)
        answer = self.mount.standby(on)
        logging.info("Temma.standby(%d) done." % on)
        return answer

    def is_standby(self):
        """
        Is the telescope RA motors in stand-by ?
        """
        logging.info("Temma.is_standby() called.")
        answer = self.mount.is_standby()
        logging.info("Temma.is_standby() done.")
        return answer

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

    def _listMethods(self):
        logging.info("Temma._listMethods() called.")
        methods = list_public_methods(self)
        logging.info("Temma._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.")
    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(Temma.status,       "status")
    server.register_function(Temma.open,         "open")
    server.register_function(Temma.get_version,  "get_version")
    server.register_function(Temma.close,        "close")
    server.register_function(Temma.send,         "send")

    # Temma init
    server.register_function(Temma.set_UTC,       "set_UTC")
    server.register_function(Temma.set_observer,  "set_observer")
    server.register_function(Temma.set_longitude, "set_longitude")
    server.register_function(Temma.set_latitude,  "set_latitude")
    server.register_function(Temma.get_latitude,  "get_latitude")
    server.register_function(Temma.set_elevation, "set_elevation")
    server.register_function(Temma.set_LST,       "set_LST")
    server.register_function(Temma.get_LST,       "get_LST")
    #server.register_function(Temma.update_LST,       "update_LST")
    server.register_function(Temma.zenith,        "zenith")
    server.register_function(Temma.define_radec,  "define_radec")

    # Temma motion
    server.register_function(Temma.get_radec,    "get_radec")
    server.register_function(Temma.goto_radec,   "goto_radec")
    server.register_function(Temma.goto,         "goto")
    server.register_function(Temma.stop,         "stop")
    server.register_function(Temma.get_side,     "get_side")
    server.register_function(Temma.flip_side,    "flip_side")
    server.register_function(Temma.is_pointing,  "is_pointing")
    server.register_function(Temma.move,         "move")
    server.register_function(Temma.standby,      "standby")
    server.register_function(Temma.is_standby,   "is_standby")

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

    # for remote introspection (tab completion with ipython)
    server.register_function(Temma._listMethods,  "__dir__")
    server.register_function(Temma._listMethods,  "system.listMethods")
    server.register_function(Temma._listMethods,  "trait_names")
    server.register_function(Temma._listMethods,  "_getAttributeNames")
    # TODO: implement: system.methodSignature
    server.register_function(Temma._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,
                           "mount-server-%s.log" % now.date().isoformat())

    # recreate the symlink mount-server.log 
    logsymlink = os.path.join(logdir, "mount-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 Temma control server for the Takahashi EM-200 mount. 
""")
    parser.add_option('-d', '--daemon', default=False, action='store_true',
                      help='Run as a background daemon')
    parser.add_option('--dummy', default=False, 
                      action='store_true', 
                      help='Run a fake instance instead')
    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.')
    parser.add_option('-t', '--tty', default='/dev/temma', 
                      dest='tty', action='store', 
                      help='specify the serial port')
    (options, args) = parser.parse_args()


    SERVER_HOSTNAME = options.hostname
    SERVER_PORT = options.port
    
    if options.dummy:
        Temma = TemmaDummy(port=options.tty)
    else:
        Temma = TemmaRemote(port=options.tty)

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

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


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