#!/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?'''
