# Source code for noctua.devices.astelco

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

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

    # System modules
    import telnetlib

    # Other templates
    from ..utils import check
    from ..utils.logger import log
    from .basedevice import BaseDevice

    class OpenTSI(BaseDevice):
        '''Base wrapper class for astelco OpenTSI stuff'''

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

            super().__init__(url)
            self.url = url.split(":")[0] or url
            self.port = url.split(":")[1] or 22
            self.timeout = 3
            self.connection = True

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

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

            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.upper()} {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:
                self.connection = False
                return None  # {"Error": self.error}

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

            # log.debug(msg)

            # 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):
                tn_answer.append(tn.read_until(b"\n"))

            # Close the cabinet connection.
            tn.close()

            # log.debug(tn_answer)

            # 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, as they are async.
            telnet_dict = dict([m[14:-1].decode().split("=") for m in tn_values])

            # # It can happen that values are missing, i.e. with cabinet off
            # if get_or_set == "get":
            #     if len(telnet_dict) != len(msglist):
            #         log.warning(f"Astelco asked {len(msglist)} values, {len(telnet_dict)} returned.")
            #         log.debug(f"Asked: {msglist}")
            #         log.debug(f"Returned: {telnet_dict}")

            # Try transforming values to int or float.
            for k, v in telnet_dict.items():
                if get_or_set == "set":
                    print(k, v)

                try:
                    telnet_dict[k] = int(v)
                except BaseException:
                    try:
                        telnet_dict[k] = float(v)
                    except BaseException:
                        try:
                            telnet_dict[k] = str(v.replace('"', ''))
                        except ValueError as e:
                            log.error("Astelco class error:")
                            log.error(e)

            # Order by message, add None if something is missing.
            res = [telnet_dict.get(m, None) for m in msglist]

            if len(res) > 1:
                return res
            else:
                return res[0]

        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.'''

        @property
        def tracking(self):
            '''Tracking status'''
            message = "POINTING.TRACK"
            res = self.get(message)
            try:
                self._tracking = True if int(res) else False
            except TypeError as e:
                self._tracking = None
            return self._tracking

        @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"
            track = 1 if b else 0
            self.put(message, track)
            return self.tracking

        def track(self):
            '''More direct way to start tracking'''
            self.tracking = True
            return self._tracking

        def abort(self):
            '''Direct way to stop tracking'''
            '''Check if need to add other abort'''
            self.tracking = False
            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 status(self):
            '''need to check exit and error from get function'''
            message = "TELESCOPE.STATUS.LIST"
            res = self.get(message)
            return res

        @property
        def state(self):

            message = "TELESCOPE.STATUS.GLOBAL"
            res = self.get(message)
            return res

        def clear(self, n):

            res = self.put("TELESCOPE.STATUS.CLEAR_ERROR", n)
            return res

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

            message = "POSITION.LOCAL.UTC"
            res = self.get(message)
            return res

        @property
        def cover(self):

            message = "AUXILIARY.COVER.REALPOS"
            res = self.get(message)
            self._cover = res
            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.READY_STATE"
            res = self.get(message)

            try:
                self._park = False if int(res) else True
            except (TypeError, ValueError) as e:
                # ValueError for "FAILED 2"
                self._park = None
            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.READY"
            pos = 0.0 if b else 1.0
            res = self.put(message, pos)
            self._park = self.park

        @property
        def altaz(self):

            # message = ["OBJECT.HORIZONTAL.ALT",
            #            "OBJECT.HORIZONTAL.AZ"]
            message = ["POSITION.HORIZONTAL.ALT",
                       "POSITION.HORIZONTAL.AZ"]
            res = self.get(message)
            altaz = res
            self._altaz = altaz  # [alt, az]
            return self._altaz

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

            # self.tracking = False
            keys = ["POINTING.SETUP.DEROTATOR.SYNCMODE",
                    "OBJECT.HORIZONTAL.ALT",
                    "OBJECT.HORIZONTAL.AZ",
                    "POINTING.TRACK"]
            derot = 2  # from astelos log file "true orientation"
            track = 2  # 2: go and stay there
            values = [derot, a[0], a[1], track]
            res = self.put(keys, values)
            self._altaz = self.altaz

        @property
        def targetradec(self):
            '''Get target Right Ascension / Declination'''

            message = ["POSITION.EQUATORIAL.RA_J2000",
                       "POSITION.EQUATORIAL.DEC_J2000"]
            res = self.get(message)
            targetradec = res
            self._targetradec = targetradec
            return self._targetradec

        @targetradec.setter
        def targetradec(self, a):
            '''Set target Right Ascension / Declination'''

            keys = ["POINTING.SETUP.DEROTATOR.SYNCMODE",
                    "OBJECT.EQUATORIAL.RA",
                    "OBJECT.EQUATORIAL.DEC",
                    "POINTING.TRACK"]
            derot = 2  # from astelos log file "true orientation"
            track = 1  # 1: go and track
            values = [derot, a[0], a[1], track]
            res = self.put(keys, values)
            self._targetradec = self.targetradec

        @property
        def radec(self):
            '''Get Right Ascension / Declination'''

            message = ["POSITION.EQUATORIAL.RA_J2000",
                       "POSITION.EQUATORIAL.DEC_J2000"]
            res = self.get(message)
            radec = res
            self._radec = radec
            return self._radec

        @radec.setter
        def radec(self, a):
            '''Set Right Ascension / Declination'''

            keys = ["POINTING.SETUP.DEROTATOR.SYNCMODE",
                    "OBJECT.EQUATORIAL.RA",
                    "OBJECT.EQUATORIAL.DEC",
                    "POINTING.TRACK"]
            derot = 2  # from astelos log file "true orientation"
            track = 1  # 1: go and track
            values = [derot, a[0], a[1], track]
            res = self.put(keys, values)
            self._radec = self.radec

        @property
        def offset(self):
            '''Get Telescope offset'''

            message = ["POSITION.INSTRUMENTAL.ZD.OFFSET",
                       "POSITION.INSTRUMENTAL.AZ.OFFSET"]
            res = self.get(message)
            offset = res
            self._offset = offset
            return self._offset

        @offset.setter
        def offset(self, a):
            '''Set Telescope offset'''

            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",
                       # "OBJECT.HORIZONTAL.ALT",
                       # "OBJECT.HORIZONTAL.AZ",
                       "POSITION.LOCAL.SIDEREAL_TIME",
                       "POSITION.LOCAL.UTC"]

            res = self.get(message)

            # print(telnet_dict)

            simple_dict = {
                "radec": [res[0],
                          res[1]],
                "altaz": [res[2],
                          res[3]],
                "lst": res[4],
                "utc": res[5],
            } if res else None

            return simple_dict

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

        @property
        def is_moving(self):
            '''TBD with the right telnet commands'''
            # log.warning("TBD with the right telnet commands")

        @property
        def position(self):
            '''Get Relative focuser position from telnet'''

            message = "POSITION.INSTRUMENTAL.FOCUS.OFFSET"
            pos = self.get(message)
            try:
                res = float(pos) * 1000
            except TypeError as e:
                res = None
            except ValueError as e:  # "FAILED 1"
                res = None
            self._position = res
            return self._position

        @position.setter
        def position(self, s):  # 0-34500=micron?
            '''Set Relative focuser position from telnet'''

            message = "POSITION.INSTRUMENTAL.FOCUS.OFFSET"
            pos = s / 1000
            res = self.put(message, pos)
            self._position = self.position

        @property
        def absolute(self):
            '''Get Absolute focuser position from telnet'''

            message = "POSITION.INSTRUMENTAL.FOCUS.REALPOS"
            res = self.get(message)
            self._absolute = res
            return self._absolute

        @absolute.setter
        def absolute(self, s):  # 0-34500=micron?
            '''Set Absolute focuser position from telnet'''

            message = "POSITION.INSTRUMENTAL.FOCUS.OFFSET"
            pos = s / 1000
            res = self.put(message, pos)
            self._absolute = self.absolute

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

        @property
        def is_moving(self):
            '''TBD with the right telnet commands'''
            # log.warning("TBD with the right telnet commands")

        @property
        def position(self):
            '''Get Relative rotator position from telnet'''
            message = "POSITION.INSTRUMENTAL.DEROTATOR[2].OFFSET"
            res = self.get(message)
            self._position = res
            return self._position

        @position.setter
        def position(self, s):  # 0-270 deg?
            '''Set Relative rotator position from telnet'''

            message = "POSITION.INSTRUMENTAL.DEROTATOR[2].OFFSET"
            res = self.put(message, s)
            self._position = self.position

        @property
        def absolute(self):
            '''Get Absolute rotator position from telnet'''

            message = "POSITION.INSTRUMENTAL.DEROTATOR[2].REALPOS"
            res = self.get(message)
            self._absolute = res
            return self._absolute

    class Sensor(OpenTSI):
        '''Implementation of a Sensor class.'''

        def __init__(self, url, temp_id, hum_id):
            '''Constructor.'''
            super().__init__(url)
            self.temp_id = temp_id  # recycle for last_update
            self.hum_id = hum_id

        @property
        def temperature(self):
            '''Mirrors temperature'''
            message = ["AUXILIARY.SENSOR[2].VALUE",
                       "AUXILIARY.SENSOR[3].VALUE",
                       "AUXILIARY.SENSOR[4].VALUE"]
            res = self.get(message)
            return res

        @property
        def humidity(self):
            '''Does it have humidity sensors?'''