'''

Talks to the mopra legacy server

Created on Jan 5, 2012

@author: Chris Phillips, derived from module by ban115
Document at https://svn.atnf.csiro.au/trac/operations/browser/SCA/src/code/Packages/atoms-java/docs/legacyserver.html
'''

#import types
import logging
import socket
import math
#import BAT
#from datetime import datetime

log = logging.getLogger(__name__)

def create_connection(hostport, delim=':'):
    """Create's a socket of AF_INET and SOCK_STREAM
    
    hostport can be a 2-tuple of the (hostname, port) where
    the port can be a string or int-able.
    
    hostport can also be a string such as 'localhost:23'
    where the delimiter is specified by delim
    """

    if isinstance(hostport, str):
        hostport = hostport.split(delim)
        
    hostport = list(hostport)
    hostport[1] = int(hostport[1])
    skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    skt.connect(tuple(hostport))
    #setkeepalives(skt)
    return skt

class LegacyServerError(Exception):
    def __init__(self, *args):
        Exception.__init__(self, *args)

class MonitoringPacket(object):
    def __init__(self, line):
        '''
Example: 
MP01 OK STOWED       58610.24782822917 1.89458488 -0.52016088 1.30960443 1.48268238 -0.00003695 0.00007607 58608.89583025463

        '''
        self.line = line
        bits = line.strip().split()
        if len(bits) != 11:
            raise LegacyServerError('Line cannot be parsed as monitoring packet: {} nbits={}'.format(line, len(bits)))

        self.antname = bits[0]
        self.status2 = bits[1]
        self.servo_state = bits[2]
        self.timestamp = float(bits[3])
        self.ra, self.dec, self.az, self.el, self.delta_az, self.delta_el = map(float, bits[4:10])
        self.track_time = float(bits[10])
        self.azdeg = math.degrees(self.az)
        self.eldeg = math.degrees(self.el)
        self.delta_az_deg = math.degrees(self.delta_az)
        self.delta_el_deg = math.degrees(self.delta_el)

    def __str__(self):
        return self.line + ' ra={:6f} dec={:.5f} az={:.3f} el={:.3f} deg daz={:.1f} del={:.1f} "'.format(math.degrees(self.ra)/15, math.degrees(self.dec), self.azdeg, self.eldeg, self.delta_az_deg*60*60, self.delta_el_deg*60*60)

class LegacyServer(object):
    '''
    Communicates with a legacy server (e.g. mopra)
    '''

    def __init__(self, hostport, fake=False):
        '''
        standard port = (bigrock, 2335)
        '''
        self.hostport = hostport
        self.allocated = False
        self.sock = None
        self.fsock = None
        self.fake = fake
        
    def send_cmd(self, cmd, args=None, target="MP01", when="NOW", expect=None):
        """Connects to a legacy server, sends the command and returns the output
        If when is an int, it will be formatted as a BAT time
        
        """
        self.connect()
        strcmd = "%s %s" % (cmd, target)

        if when is None or when == '':
            pass
        elif isinstance(when, int):
            strcmd += " 0x%016X" % when # format BAT as a hexadecimal number
        else:
            strcmd += " " + when

        if args is not None:
            strcmd += ' ' + args

        if self.fake == True:
            fakestr = "FAKE "
        else:
            fakestr = ''
            
        log.debug("{}Sending command '{}'".format(fakestr, strcmd))

        if self.fake:
            return 'OK ' + target

        self.sock.sendall(str.encode(strcmd.strip() + '\n'))

        reply = ''
        # protocal always ends with a blank line
        while not reply.endswith('\n\n'):
            r =  self.sock.recv(1024)
            if len(r) == 0:
                log.debug('Socket returned nothing')
                break

            reply += r.decode()

        reply = reply.rstrip() # Remove trailing /n
            
        # It's a mess.
        # 'allocate' returns 'OK\nMP01 OK\n\n'
        # 'stow' returns 'CA00 OK\n\n'
        # stop returns 'CA00 OK\n\n'
        # deallocate returns 'CA00 OK\n\n'
        # getMon returns OK\nMP01 OK STOWED       58610.24782822917 1.89458488 -0.52016088 1.30960443 1.48268238 -0.00003695 0.00007607 58608.89583025463\n\n
        # sometimes you get an OK sometimes you dont, sometimes is MP01 and somtimes its CA01

        #lines = [l.rstrip() for l in reply.split('\n')]
        lines = reply.split('\n')
        
        log.debug('GOT REPLY %s %s', lines, 'OK' in lines[0])

        if not 'OK' in lines[0]:
            raise LegacyServerError(reply)

        return reply
        
    def connect(self):
        if self.sock is None and not self.fake:
            self.sock = create_connection(self.hostport)
            self.fsock = self.sock.makefile()

    def allocate(self):
        if not self.allocated: 
            self.send_cmd("allocate", when="", expect="OK\nMP01 OK")
            self.allocated = True

    def deallocate(self):
        self.send_cmd("deallocate", when="", expect="CA00 OK")
        self.allocated = False

    def enable(self):
        self.send_cmd("enable", when="", expect="CA00 OK")

    def drvOn(self, when='NOW'):
        self.send_cmd('drvOn', when=when)

    def drvOff(self, when='NOW'):
        self.send_cmd('drvOff', when=when)

    def stop(self, when='NOW'):
        self.send_cmd('stop', when=when)

    def stow(self, when='NOW'):
        self.send_cmd('stow', when=when)

    def park(self, when='NOW'):
        self.send_cmd('park', when=when)

    def get_mon(self):
        rep = self.send_cmd('getMon', when=None)
        s = rep.split('\n')[1].strip()
        return MonitoringPacket(s)
        
    def goto(self, wrap, mode, left, right, pmleft=None, pmright=None, pmepoch=None, when=None):
        # Mode: AzElApp, J2000Mean, RaDecApp, J2000Cat, EclipticMean, B1950Bat, J2000Moving, AzElMoving, B1950Mean, HaDecApp, Galactic, EclipticApp
        # left and right in Radians
        args = " {} {} {} {}".format(wrap, mode, left, right)
        if pmleft is not None:
            args += ' ' + ' '.join(map(str, [pmleft, pmright, pmepoch]))

        self.send_cmd('goto', args, when='NOW')

    def offs(self, left, right, when='NOW'):
        # left and right in Radians
        args = " {} {}".format(left, right)
        self.send_cmd('offs', args, when=when)
        
    def cycle(self, epoch, cycletime, duration, continuous=False):
        # Epoch: BAT when cal sycle starts
        # cycletime: Cycle length in seconds
        # duration: Length of cal pulse in useconds (period = 2xduration)

        prescale = duration//2
        code = 1
        while prescale>65536:
            code += 1
            prescale //= 2
            if code>8: break

        if code>8:
            log.error('duration {} too large'.format(duration))
            return

        if prescale*(2**code) != duration:
            log.error('Cannot set pulsecal to {} usec'.format(duration))
            return

        if continuous:
            continuous = 1
        else:
            continuous = 0

        if not isinstance(epoch, str):
            epoch = hex(epoch)

        prescale -= 1
        
        args = ' '.join(map(str, [epoch, cycletime, 0.0, 0.051, 0.045, 1, prescale, code, continuous]))

        self.send_cmd('cycle', args, when='NOW')

    def noiscl(self, lsnoise=3,cxnoise=3,knoise=3,qnoise=3,wnoise=3,paddle=False,ttone=False, expect="CA00 OK"):
        # lsnoise, cxnoise, knoise, qnoise, wnoise:  3 Switching, 0 off, 1 on
        # paddle: False=out
        # ttone: False=Off

        kqnoise = qnoise + knoise*4
        if not ttone:
            kqnoise += 32
        if paddle:
            wnoise+=4

        args = ' '.join(map(str, [lsnoise, cxnoise, kqnoise, wnoise]))
        
        self.send_cmd('noiscl', args, when='NOW')
        
        
    def close(self):
        if self.sock is not None:
            if self.allocated:
                try:
                    self.deallocate()
                except:
                    pass
                
            try:
                self.sock.close()
            except:
                pass
            
    def __del__(self):
        self.close()
        
