# Source code for noctua.devices.alpaca

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

    # System modules
    import time
    from urllib.parse import urlencode

    # Third-party modules
    import requests

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

    class AlpacaDevice(BaseDevice):
        '''Base wrapper class for ASCOM Alpaca devices'''

        def __init__(self, url, dev=None):
            '''Constructor.'''

            super().__init__(url)
            self._dev = dev  # switch, dome, telescope etc.
            self.sim = 0
            self.timeout = 10

        @property
        def addr(self):
            '''Get or set the device URL address.'''

            return f"{self.url}/{self._dev}/{self.sim}"

        @addr.setter
        def addr(self, a):
            '''Setter for the device URL address.'''

            self._addr = a

        @check.request_errors
        def get(self, method, params={}, timeout=None):
            '''Send a HTTP GET request to the device address.'''

            if not timeout:
                timeout = self.timeout

            res = requests.get(f"{self.addr}/{method}",
                               params=urlencode(params),
                               timeout=timeout)
            res.raise_for_status()
            resj = res.json()
            if resj["ErrorNumber"] != 0:
                msg = f'ASCOM {resj["ErrorNumber"]}: {resj["ErrorMessage"]}'
                log.error(msg)
                self.error.append(msg)
            else:
                self.error = []
                value = resj["Value"]
                return value

        @check.request_errors
        def put(self, method, data={}, timeout=None):
            '''Send a HTTP PUT request to the device address.'''

            if not timeout:
                timeout = self.timeout

            res = requests.put(f"{self.addr}/{method}",
                               data=data,
                               timeout=timeout)

            res.raise_for_status()
            resj = res.json()
            if resj["ErrorNumber"] != 0:
                msg = f'ASCOM {resj["ErrorNumber"]}: {resj["ErrorMessage"]}'
                log.error(msg)
                self.error.append(msg)
            else:
                if "Value" in resj:
                    try:
                        value = resj["Value"]
                        self.error = []
                        return value
                    except AttributeError as e:
                        log.error(e)
                        self.error.append(e)
                        return res
                    except KeyError as e:
                        log.error(e)
                        self.error.append(e)
                        return res

        @property
        def actions(self):
            '''List the actions supported by the device.'''
            res = self.get("supportedactions")
            return res

        @property
        def name(self):
            '''Get the name of the device.'''
            res = self.get("name")
            return res

        @property
        def connection(self):
            '''Get or set the connection status of the device.'''
            res = self.get("connected")
            self._connection = res
            return self._connection

        @connection.setter
        def connection(self, b):
            '''Setter for the connection status of the device.'''
            data = {"Connected": b}
            res = self.put("connected", data=data)
            self._connection = self.connection

        def command(self, val, cmd="commandstring"):

            data = {"Command": val, "Raw": False}
            res = self.put(cmd, data=data)
            value = res
            if type(value) in [bool, int]:
                return value
            else:
                try:
                    float(value)
                    return float(value)
                except BaseException:
                    return value

        def commandblind(self, val):

            res = self.command(val, cmd="commandblind")
            return res

    class Switch(AlpacaDevice):
        '''Wrapper for ASCOM Alpaca devices of "switch" type.'''

        def __init__(self, url, id=3):
            '''Constructor.'''
            super().__init__(url, dev="switch")
            self.id = id

        @property
        def description(self):
            '''Describe the switch.'''
            params = {"Id": self.id}
            res = self.get("getswitchname", params=params)
            return res

        @property
        def state(self):
            '''Get or set the switch state.'''
            params = {"Id": self.id}
            res = self.get("getswitch", params=params)
            self._state = True if res else False
            return self._state

        @state.setter
        def state(self, s: bool):
            '''Setter method for the switch state.'''
            data = {"Id": self.id, "State": s}
            res = self.put("setswitch", data=data)
            self._state = self.state

    class Dome(AlpacaDevice):
        '''Wrapper for ASCOM Alpaca devices of "dome" type.'''

        def __init__(self, url):
            '''Constructor.'''
            super().__init__(url, dev="dome")
            self.PARK = dome_park_position
            self.TOLERANCE = 1.0  # deg tolerance for park

        def park(self):
            '''Put the dome in its park position.'''
            self.slave = False
            self.azimuth = self.PARK

        def sync(self, az):
            '''Force the value of the dome azimuth to assume
            a specific value.'''
            self.slave = False
            data = {"Azimuth": az}
            res = self.put("synctoazimuth", data=data)

        def abort(self):
            '''Abort the motion of the dome.'''
            self.slave = False
            res = self.put("abortslew")

        @property
        def is_parked(self):
            '''Check if the dome is at its park position.'''

            try:
                res = abs(self.PARK - self.azimuth) < self.TOLERANCE
            except TypeError as e:
                res = None
            self._is_parked = res
            return self._is_parked

        @property
        def is_moving(self):
            '''Check if the dome is slewing.'''
            res = self.get("slewing")
            self._is_moving = res
            return self._is_moving

        @property
        def azimuth(self):
            '''Get or set the dome azimuth.'''
            res = self.get("azimuth")
            self._azimuth = res
            return self._azimuth

        @azimuth.setter
        def azimuth(self, a):
            '''Setter method for the dome azimuth.'''
            data = {"Azimuth": a}
            res = self.put("slewtoazimuth", data=data)
            self._azimuth = res

        @property
        def shutter(self):
            '''Get the status of the dome shutter.'''
            res = self.get("shutterstatus")
            return res

        @property
        def open(self):
            '''Get the dome shutter position, or open/close it.'''
            res = self.shutter
            if res == 0:
                self._open = True
            elif res == 1:
                self._open = False
            else:
                self._open = None
            return self._open

        @open.setter
        def open(self, b):
            '''Setter method to open/close the dome shutter.'''
            if b:
                res = self.put("openshutter")
            else:
                res = self.put("closeshutter")
                self._open = self.open

        @property
        def slave(self):
            '''Check if the dome position is slaved to the telescope,
            or slave/unslave it.
            '''
            res = self.get("slaved")
            self._slave = res
            return self._slave

        @slave.setter
        def slave(self, b):
            '''Setter method to slave/unslave the dome
            to the telescope.'''
            data = {"Slaved": b}
            res = self.put("slaved", data=data)
            self._slave = self.slave

    class Telescope(AlpacaDevice):
        def __init__(self, url):
            super().__init__(url, dev="telescope")

        def track(self):

            self.tracking = True
            res = self.put("slewtotargetasync")

        def abort(self):

            res = self.put("abortslew")

        @property
        def is_moving(self):

            res = self.get("slewing")
            self._is_moving = res
            return self._is_moving

        @property
        def temperature(self):

            res1 = self.command("AUXILIARY.SENSOR[2].VALUE")
            res2 = self.command("AUXILIARY.SENSOR[3].VALUE")
            res3 = self.command("AUXILIARY.SENSOR[4].VALUE")
            return [round(float(res1), 1),
                    round(float(res2), 1),
                    round(float(res3), 1)]

        @property
        def status(self):
            res = self.command("TELESCOPE.STATUS.LIST")
            return res

        @property
        def state(self):
            res = self.command("TELESCOPE.STATUS.GLOBAL")
            return res

        def clear(self, n):
            res = self.command(f"TELESCOPE.STATUS.CLEAR_ERROR={n}")
            return res

        @property
        def clock(self):
            res = self.command("POSITION.LOCAL.UTC")
            # try:
            #     time = Time(res, format="unix")
            #     return time.isot
            # except ValueError:
            #     return res
            return res

        @property
        def cover(self):
            res = self.command("AUXILIARY.COVER.REALPOS")
            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.commandblind(f"AUXILIARY.COVER.TARGETPOS={pos}")
            time.sleep(0.2)
            status = self.command("TELESCOPE.STATUS.GLOBAL")
            log.error(status)

            self._open = self.open

        @property
        def park(self):
            res = self.get("atpark")
            self._park = True if res else False
            return self._park

        @park.setter
        def park(self, b, timeout=90):
            if b:
                res = self.put("park", timeout=timeout)
            else:
                res = self.put("unpark", timeout=timeout)
            # res = self._park # why I put this?
            self._park = self.park

        @property
        def altaz(self):
            alt = self.get("altitude")
            az = self.get("azimuth")
            self._altaz = [alt, az]
            return self._altaz

        @altaz.setter
        def altaz(self, a):
            self.tracking = False
            data = {"Altitude": a[0], "Azimuth": a[1]}
            res = self.put("slewtoaltazasync", data=data)
            self._altaz = self.altaz

        @property
        def targetradec(self):
            ra = self.get("targetrightascension")
            dec = self.get("targetdeclination")
            self._targetradec = [ra, dec]
            return self._targetradec

        @targetradec.setter
        def targetradec(self, a):
            data = {"TargetRightAscension": a[0]}
            ra = self.put("targetrightascension", data=data)
            data = {"TargetDeclination": a[1]}
            dec = self.put("targetdeclination", data=data)
            self._targetradec = self.targetradec

        @property
        def radec(self):
            ra = self.get("rightascension")
            dec = self.get("declination")
            # ra = self.command("POSITION.EQUATORIAL.RA_J2000")
            # dec = self.command("POSITION.EQUATORIAL.DEC_J2000")
            self._radec = [ra, dec]
            return self._radec

        @radec.setter
        def radec(self, a):
            self.tracking = True
            data = {"RightAscension": a[0], "Declination": a[1]}
            res = self.put("slewtocoordinatesasync", data=data)
            self._radec = self.radec

        @property
        def offset(self):
            zd = self.command("POSITION.INSTRUMENTAL.ZD.OFFSET")
            az = self.command("POSITION.INSTRUMENTAL.AZ.OFFSET")
            self._offset = [zd, az]
            return self._offset

        @offset.setter
        def offset(self, a):
            zd = a[0]  # in degrees!
            az = a[1]  # in degrees!
            if abs(zd) > 0.3:  # 18 arcmin
                log.error(f"zd {zd} too large. Maybe arcsec instead of deg?")
                return
            if abs(az) > 0.3:  # 18 arcmin
                log.error(f"az {az} too large. Maybe arcsec instead of deg?")
                return
            zd = self.commandblind(f"POSITION.INSTRUMENTAL.ZD.OFFSET={a[0]}")
            az = self.commandblind(f"POSITION.INSTRUMENTAL.AZ.OFFSET={a[1]}")
            self._offset = self.offset

        @property
        def tracking(self):
            res = self.get("tracking")
            self._tracking = res
            return self._tracking

        @tracking.setter
        def tracking(self, b):
            data = {"Tracking": b}
            res = self.put("tracking", data=data)
            self._tracking = self.tracking

    class Focuser(AlpacaDevice):
        def __init__(self, url):
            super().__init__(url, dev="focuser")

        @property
        def is_moving(self):
            res = self.get("ismoving")
            self._moving = res
            return self._moving

        @property
        def position(self):
            res = self.get("position")
            self._position = res
            return self._position

        @position.setter
        def position(self, s):  # 0-34500=micron?
            data = {"Position": s}
            res = self.put("move", data=data)
            self._position = self.position

    class Rotator(AlpacaDevice):
        def __init__(self, url):
            super().__init__(url, dev="rotator")

        @property
        def is_moving(self):
            res = self.get("ismoving")
            self._moving = res
            return self._moving

        @property
        def position(self):
            res = self.get("position")
            self._position = res
            return self._position

        @position.setter
        def position(self, s):  # 0-270 deg?
            data = {"Position": s}
            res = self.put("moveabsolute", data=data)
            self._position = self.position