#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Interface with an Astelco OpenTSI-based device
"""

# System modules
import telnetlib

# Custom modules
from utils import check
from utils.logger import log


class OpenTSI:
    '''Base wrapper class for astelco OpenTSI stuff'''

    def __init__(self, url):
        '''Constructor.'''

        self.url = url.split(":")[0] or "localhost"
        self.port = url.split(":")[1] or 22
        self.timeout = 3
        self.error = None

    @check.telnet_errors
    def connect(self):
        '''Setup a telnet connection to the cabinet'''

        tn = telnetlib.Telnet(self.url, self.port, timeout=3)
        return tn

    @check.telnet_errors
    def send(self, get_or_set, message=None):
        '''
        Connect to the cabinet and send messages to set or get properties
        '''

        # Login data for cabinet communication.
        # It is a byte-string.
        auth = b'auth plain "admin" "admin"\n'

        # Send the message. it can be a string (even a multi-line
        # string), or a list of strings

        if isinstance(message, str):
            # Maybe is multi-line
            msglist = message.split("\n")
            # Removing empty elements
            msglist = [x for x in msglist if x]
        elif isinstance(message,  list):
            msglist = message

        # GET: Transforming every string in "1 get string\n2 get string\n"
        # SET: Transforming every string in "1 set string=val\n2 set string=val"
        msg = [f"{i+1} {get_or_set} {j}\n" for i, j in enumerate(msglist)]
        # From list to string, and byte-coded
        msg = bytes("".join(msg), "utf+8")

        # Connect to the cabinet.
        tn = self.connect()
        if not tn:
            return {"Error": self.error}

        tn.read_until(b"[ruder]\n")
        tn.write(auth)
        tn.read_until(b"AUTH OK 0 0\n")

        # Send the message.
        tn.write(msg)

        # For each command, the *asyncrhronous* answer contains:
        #
        # COMMAND OK
        # DATA INLINE ...
        # COMMAND COMPLETE
        #
        # Append all the buffer line by line to a list.

        tn_answer = []
        for i in range(msg.count(b"\n")*3-1):
            tn_answer.append(tn.read_until(b"\n"))

        # Close the cabinet connection.
        tn.close()

        # Keep only the data.
        tn_values = [s for s in tn_answer if b"DATA INLINE" in s]

        # Create a python dictionary with name and value.
        telnet_dict = dict([m[14:-1].decode().split("=") for m in tn_values])

        # Try transforming values to float.
        for k, v in telnet_dict.items():
            try:
                telnet_dict[k] = int(v)
            except:
                try:
                    telnet_dict[k] = float(v)
                except:
                    try:
                        telnet_dict[k] = str(v.replace('"',''))
                    except ValueError as e:
                        log.error("Astelco class error:")
                        log.error(e)

        return telnet_dict

    def get(self, message):
        '''Send a telnet message to the cabinet to retrieve a value or a set
        of values.'''

        return self.send("get", message)

    def put(self, key, val):
        '''Send to telnet a key to set a a value.
        keys and values can be both lists with the same element number.'''
        if isinstance(key, list):
            message = [f"{k}={v}" for k, v in zip(key, val)]
        else:
            message = f"{key}={val}"

        return self.send("set", message)


class Telescope(OpenTSI):
    '''Implementation of the Telescope commands mocking my Alpaca Telescope wrapper.'''

    def __init__(self, url):
         '''Constructor.'''
         super().__init__(url)
         self._temps = self.temperature or [None,None,None]
         
    @property
    def tracking(self):
        '''Tracking status'''
        message = "CURRENT.TRACK"
        self._tracking = self.get(message)
        return list(self._tracking.values())[0]

    @tracking.setter
    def tracking(self, b):
        '''(Re)Start/Stop tracking of telescope in function of currently configured target'''
        ''' 1 slew to currently configured target and start tracking'''
        ''' 0 stop tracking'''
        message = "POINTING.TRACK"
        self.put(message,b)
        return self.tracking
    
    def track(self):
        '''More direct way to start tracking'''
        self.tracking(1)
        return self._tracking
                
    def abort(self):
        '''Direct way to stop tracking'''
        '''Check if need to add other abort'''
        self.tracking(0)
        return self._tracking

    @property
    def is_moving(self):
        '''Check the motion state of the telescope'''
        '''Bit 0 One or more axes are moving
           Bit 1 Trajectories are being executed
           Bit 2 Movement is blocked (e.g. by limits)
           Bit 3 Telescope is on target position (i.e. stopped or, if tracking, following the target)
           Bit 4 Movement is restricted by given jerk/acceleration/velocity limits
           Bit 5 Telescope is “unparked”, i.e. moved on startup position by READY=1 or PARK=0
           Bit 6 Telescope is “parked”, i.e. moved on park position by READY=0 or PARK=1'''
        message = ("TELESCOPE.MOTION_STATE")
        res = self.get(message)
        return res

    @property
    def temperature(self):
        '''Mirrors temperature'''
        message = ["AUXILIARY.SENSOR[2].VALUE",
                   "AUXILIARY.SENSOR[3].VALUE",
                   "AUXILIARY.SENSOR[4].VALUE"]
        res = self.get(message)
        temps = [round(float(res[m]), 1)
                 for m in message]  # ordered by message
        self._temps = temps
        return self._temps

    @property
    def status(self):
        '''need to check exit and error from get function'''
        message = "TELESCOPE.STATUS.LIST"
        res = self.get(message)
        if res:
            return list(res.values())[0]

    @property
    def state(self):

        message = "TELESCOPE.STATUS.GLOBAL"
        res = self.get(message)
        if res:
            return list(res.values())[0]

    def clear(self, n):

        res = self.put("TELESCOPE.STATUS.CLEAR_ERROR", n)
        if res:
            return list(res.values())[0]

    @property
    def clock(self):
        """
        in UNIX time
        """

        message = "POSITION.LOCAL.UTC"
        res = self.get(message)
        if res:
            return list(res.values())[0]

    @property
    def cover(self):

        message = "AUXILIARY.COVER.REALPOS"
        res = self.get(message)
        if res:
            self._cover = list(res.values())[0]
            return self._cover

    @property
    def open(self):

        res = self.cover
        if res == 1.0:
            self._open = True
        elif res == 0.0:
            self._open = False
        else:
            self._open = None
        return self._open

    @open.setter
    def open(self, b):
        pos = 1.0 if b else 0.0
        res = self.put("AUXILIARY.COVER.TARGETPOS", pos)

        self._open = self.open

    @property
    def park(self):
        '''Get if the telescope is in parked position'''
        message = "TELESCOPE.PARK"
        res = self.get(message)        
        self._park = True if 1 else False
        if res:
            self._park = list(res.values())[0]
            return self._park

    @park.setter
    def park(self, b, timeout=90):
        '''Init the telescope homing the axes,
        or put the telescope in park position'''
        message = "TELESCOPE.PARK"
        pos = 1 if b else 0
        res = self.put(message, pos)
        self._park = self.park

    @property
    def altaz(self):

        message = ["POSITION.HORIZONTAL.ALT",
                   "POSITION.HORIZONTAL.AZ"]
        res = self.get(message)
        altaz = [res[m] for m in message]  # ordered by message
        self._altaz = altaz  # [alt, az]
        return self._altaz

    @altaz.setter
    def altaz(self, a):
        self.tracking = False
        keys = ["POSITION.HORIZONTAL.ALT",
                "POSITION.HORIZONTAL.AZ"]
        values = [a[0], a[1]]
        res = self.put(keys, values)
        self._altaz = self.altaz

    @property
    def targetradec(self):
        '''TBD with the right telnet commands'''

        message = ["POSITION.EQUATORIAL.RA_J2000",
                   "POSITION.EQUATORIAL.DEC_J2000"]
        res = self.get(message)
        radec = [res[m] for m in message]  # ordered by message
        self._targetradec = targetradec
        return self._targetradec

    @targetradec.setter
    def targetradec(self, a):
        '''TBD with the right telnet commands'''

        keys = ["POSITION.EQUATORIAL.RA_J2000",
                "POSITION.EQUATORIAL.DEC_J2000"]
        values = [a[0], a[1]]
        res = self.put(keys, values)
        # self.tracking = True
        self._targetradec = self.targetradec

    @property
    def radec(self):

        message = ["POSITION.EQUATORIAL.RA_J2000",
                   "POSITION.EQUATORIAL.DEC_J2000"]
        res = self.get(message)
        radec = [res[m] for m in message]  # ordered by message
        self._radec = radec
        return self._radec

    @radec.setter
    def radec(self, a):

        keys = ["POSITION.EQUATORIAL.RA_J2000",
                "POSITION.EQUATORIAL.DEC_J2000"]
        values = [a[0], a[1]]
        res = self.put(keys, values)
        # self.tracking = True
        self._radec = self.radec

    @property
    def offset(self):

        message = ["POSITION.INSTRUMENTAL.ZD.OFFSET",
                   "POSITION.INSTRUMENTAL.AZ.OFFSET"]
        res = self.get(message)
        offset = [res[m] for m in message]  # ordered by message
        self._offset = offset
        return self._offset

    @offset.setter
    def offset(self, a):
        keys = ["POSITION.INSTRUMENTAL.ZD.OFFSET",
                "POSITION.INSTRUMENTAL.AZ.OFFSET"]
        values = [a[0], a[1]]
        if abs(values[0]) > 0.9:  # 54 arcmin
            log.error(f"zd {a[0]} too large. Maybe arcsec instead of deg?")
            return
        if abs(values[1]) > 0.9:  # 54 arcmin
            log.error(f"az {a[1]} too large. Maybe arcsec instead of deg?")
            return
        res = self.put(keys, values)
        self._offset = self.offset

    @property
    def coordinates(self):
        """
        Ask simultaneously
        RA2000, DEC2000, ALT, AZ, LST, UTC.
        Answer is given as a dict of decimal numbers.
        UTC is given as unix time.
        """

        message = ["POSITION.EQUATORIAL.RA_J2000",
                   "POSITION.EQUATORIAL.DEC_J2000",
                   "POSITION.HORIZONTAL.ALT",
                   "POSITION.HORIZONTAL.AZ",
                   "POSITION.LOCAL.SIDEREAL_TIME",
                   "POSITION.LOCAL.UTC"]

        telnet_dict = self.get(message)

        # print(telnet_dict)

        if all(key in telnet_dict for key in message):
            self.error = []
            # radec and altaz in lists as usual
            simple_dict = {
                "radec": [telnet_dict["POSITION.EQUATORIAL.RA_J2000"],
                          telnet_dict["POSITION.EQUATORIAL.DEC_J2000"]],
                "altaz": [telnet_dict["POSITION.HORIZONTAL.ALT"],
                          telnet_dict["POSITION.HORIZONTAL.AZ"]],
                "utc": telnet_dict["POSITION.LOCAL.UTC"],
                "lst": telnet_dict["POSITION.LOCAL.SIDEREAL_TIME"],
            }
        else:
            simple_dict = {
                "radec": [None, None],
                "altaz": [None, None],
                "utc": None,
                "lst": None,
            }
            self.error.append("ASTELCO: Failed getting coordinates")

        return simple_dict

    
class Focuser(OpenTSI):
    '''Implementation of the Focuser commands mocking my Alpaca Telescope wrapper.'''

    ### FOCUSER SETUP ###
    @property
    def focuser_mode(self):
        ''''''
    
    @focuser_mode.setter
    def focuser_mode(self, mode=0):
        '''
        Bit 0 Synchronize with focus position 
        Bit 1 Synchronize with M2 model 
        Bit 2 Synchronize with port specific offset
        Bit 3 Synchronize with filter specific offset
        Bit 4 Turn off focus motor during tracking
        Bit 5 Synchronize with instrument specific offset
        Bit 6 Synchronize with offset from active M1 model 
        '''
        message="POINTING.SETUP.FOCUS.SYNCMODE"
        res = self.put(message, mode)
        return res

    @property
    def position(self):
        message = "POINTING.SETUP.FOCUS.POSITION"
        res = self.get(message)
        self._position = res
        if res:
            self._position = list(res.values())[0]
            return self._position

    @position.setter
    def position(self, s):  # 0-34500=micron?
        message = "POINTING.SETUP.FOCUS.POSITION"
        pos = s/1000
        res = self.put(message, pos)
        self._position = self.position


class Rotator(OpenTSI):

    ### DEROTATOR SETUP ###
    '''Set the derotator tracking mode (before tracking)'''
    
    @property
    def derotator_mode(self):
        '''Don't undestand if there's a method for read the current mode'''
        # can we use the set command also as a getter?
    
    @derotator_mode.setter
    def derotator_mode(self,mode):
        ''' 
            0 Do not synchronize
            1 Absolute position, given as OFFSET (or DEROTATOR for trajectories)
            2 True orientation
            3 As 2, but use OFFSET (or DEROTATOR for trajectories) as an additional offset
            4 True orientation plus a 0, 90, 180 or 270 degree offset (to allow for a longer tracking time or shorter slewing time)
            5 As 4, but use OFFSET (or DEROTATOR for trajectories) as an additional offset
            6 True orientation plus arbitrary offset (for even better optimization)
        '''
        if (self.tracking()!=0):
            return "Stop current tracking or track with 'Do not sych mode' to change the rotator position/offset"
        
        message = "POINTING.SETUP.DEROTATOR.SYNCMODE"
        res = self.put(message, mode)
        return res
    
    ### DEROTATOR SETTINGS ###

    @property
    def is_moving(self):
        '''This does not exist (seems)'''
            
    @property
    def position(self):
        '''This is the derotator position in absolute mode and the derotator offset in relative mode'''
        
        message = "CURRENT.DEROTATOR_OFFSET"
        res = self.get(message)
        self._position = res
        if res:
            self._position = list(res.values())[0]
            return self._position

    @position.setter
    def position(self, s):  # 0-270 deg? # depend on the current derotating mode
        '''TBD with the right telnet commands'''
        
        message = "POINTING.SETUP.DEROTATOR.OFFSET"
        res = self.put(message, s)
        self._position = self.position


    




    #########
    # Tests
    #########

    def test1(self):
        message = "POSITION.EQUATORIAL.RA_J2000"
        return self.get(message)

    def test2(self):
        message = '''POSITION.EQUATORIAL.RA_J2000
        POSITION.EQUATORIAL.DEC_J2000
        POSITION.HORIZONTAL.alt
        POSITION.HORIZONTAL.az
        POSITION.LOCAL.SIDEREAL_TIME
        POSITION.LOCAL.UTC'''
        return self.get(message)

    def test3(self):
        message = ["POSITION.EQUATORIAL.RA_J2000",
                   "POSITION.EQUATORIAL.DEC_J2000",
                   "POSITION.HORIZONTAL.alt",
                   "POSITION.HORIZONTAL.az",
                   "POSITION.LOCAL.SIDEREAL_TIME",
                   "POSITION.LOCAL.UTC"]
        return self.get(message)
