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

# System modules
import time
from urllib.parse import urlencode

# Third-party modules
import requests

# Custom modules
from devices.basedevice import BaseDevice
from config.constants import dome_park_position
from utils import check
from utils.logger import log


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:
                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
