# Source code for noctua.devices.stx

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

    """
    Interface with a SBIG STX camera device
    """

    # System modules
    import time
    from datetime import datetime
    from urllib.parse import urlencode

    # Third-party modules
    import requests

    # Other templates
    from ..config.constants import temp_fits
    from ..utils import check
    from .basedevice import BaseDevice

    class STX(BaseDevice):
        '''Base wrapper class for SBIG STX cameras.'''

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

            super().__init__(url)
            self.url = url
            self.addr = self.url
            self.timeout = 3
            self._last_command_time = 0
            self._command_interval = 0.05  # 50 milliseconds

        def _wait_if_needed(self):
            """
            Ensures the 50ms command interval is respected between commands.
            """
            elapsed = time.time() - self._last_command_time
            if elapsed < self._command_interval:
                time.sleep(self._command_interval - elapsed)
            self._last_command_time = time.time()

        @property
        def connection(self):
            '''Setup a telnet connection to the cabinet.
            No decorator to silence errors. If error, not connected.
            '''

            try:
                method = "VersionNumbers"
                res = requests.get(f"{self.addr}/{method}.cgi",
                                   timeout=self.timeout)
                return True
            except Exception as e:
                self.error = ["No ping"]
                return False

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

            self._wait_if_needed()  # Wait before sending command
            res = requests.get(f"{self.addr}/{method}.cgi",
                               params="&".join(params),
                               timeout=self.timeout, verify=False)

            value = res.text.split("\r\n")
            if not value[-1]:
                value = value[:-1]
            if len(value) == 1:
                value = value[0]

            try:
                int(value)
                return int(value)
            except BaseException:
                try:
                    float(value)
                    return float(value)
                except BaseException:
                    return value

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

            self._wait_if_needed()  # Wait before sending command
            res = requests.get(f"{self.addr}/{method}.cgi",
                               params=urlencode(params),
                               timeout=self.timeout)

            res.raise_for_status()
            text = res.text.split("\r\n")
            if not text[-1]:
                text = text[:-1]
            if len(text) == 1:
                text = text[0]
            return text

    class Camera(STX):
        '''Camera device based on STX cameras.'''

        def abort(self):
            self.put("ImagerAbortExposure")

        @check.request_errors
        def abort2(self):
            # https://github.com/dkirkby/STXLDriver/blob/master/stxldriver/camera.py
            # Maybe /exposure.html?Abort
            res = requests.get(f"exposure.html",
                               params=urlencode("Abort"),
                               timeout=self.timeout)

            res.raise_for_status()
            text = res.text.split("\r\n")
            if not text[-1]:
                text = text[:-1]
            if len(text) == 1:
                text = text[0]
            return text

        def start(
                self,
                duration,
                frametype,
                datetime=datetime.utcnow().isoformat()):
            params = {"Duration": duration,
                      "FrameType": frametype,
                      "DateTime": datetime}
            self.put("ImagerStartExposure", params=params)

        def download(self):
            res = requests.get(f"{self.addr}/Imager.FIT")
            with open(temp_fits, 'wb') as f:
                f.write(res.content)

            # return fits.ImageHDU.fromstring(res.content)

        def full_frame(self):
            maxx, maxy = self.max_range
            self.xrange = [0, maxx]
            self.yrange = [0, maxy]
            self.xystart = [0, 0]
            self.xyend = [maxx, maxy]
            return {"xrange": self.xrange, "yrange": self.yrange}

        def half_frame(self):
            maxx, maxy = self.max_range
            self.xrange = [maxx // 4 * 1, maxx // 4 * 3]
            self.yrange = [maxy // 4 * 1, maxy // 4 * 3]
            self.xystart = [maxx // 4, maxy // 4]
            self.xyend = [maxx // 4 * 3, maxy // 4 * 3]
            return {"xrange": self.xrange, "yrange": self.yrange}

        def small_frame(self):
            maxx, maxy = self.max_range
            self.xrange = [maxx // 20 * 9, maxx // 20 * 11]
            self.yrange = [maxy // 20 * 9, maxy // 20 * 11]
            self.xystart = [maxx // 20 * 9, maxy // 20 * 9]
            self.xyend = [maxx // 20 * 11, maxy // 20 * 11]
            return {"xrange": self.xrange, "yrange": self.yrange}

        @property
        def version(self):
            res = self.get("VersionNumbers")
            return res

        @property
        def description(self):
            res = self.get("Description")
            return res

        @property
        def state(self):
            res = self.get("ImagerState")
            return res

        @property
        def ready(self):
            res = self.get("ImagerImageReady")
            return res

        @property
        def all(self):
            params = ["AmbientTemperature",  # 0
                      "CCDTemperatureSetpoint",  # 1
                      "CCDTemperature",  # 2
                      "CoolerState",  # 3
                      "CoolerPower",  # 4
                      "BinX", "BinY",  # 5 #6
                      "CameraXSize", "CameraYSize",  # 7 #8
                      "StartX", "StartY",  # 9 #10
                      "NumX", "NumY"]  # 11 #12
            res = self.get("ImagerGetSettings", params=params)

            try:
                ambient, setpoint, temperature, cool, fan, binx, biny, camx, camy, startx, starty, numx, numy = list(
                    map(float, res[:13]))
            except (TypeError, ValueError) as e:
                ambient, setpoint, temperature, cool, fan, binx, biny, camx, camy, startx, starty, numx, numy = [
                    999, 999, 999, 0, 0, 999, 999, 0, 0, 0, 0, 0, 0]

            cooler = True if cool else False

            x_start = startx // binx
            y_start = starty // biny
            x_end = (startx + numx) // binx
            y_end = (starty + numy) // biny
            centx = camx // binx // 2
            centy = camy // biny // 2
            all_dict = {"ambient": round(ambient, 1),
                        "setpoint": round(setpoint, 1),
                        "temperature": round(temperature, 1),
                        "cooler": cooler,
                        "fan": round(fan, 0),
                        "binning": [binx, biny],
                        "max_range": [camx // binx, camy // biny],
                        "xystart": [x_start, y_start],
                        "xyend": [x_end, y_end],
                        "xrange": [x_start, x_end],
                        "yrange": [y_start, y_end],
                        "center": [centx, centy]}
            return all_dict

        @property
        def ambient(self):
            params = ["AmbientTemperature"]
            res = self.get("ImagerGetSettings", params=params)
            return round(res, 1)

        @property
        def max_range(self):
            params = ["CameraXSize", "CameraYSize"]
            res = self.get("ImagerGetSettings", params=params)
            if self.error:
                return None, None
            res = list(map(int, res))
            binning = self.binning
            return [r // b for r, b in zip(res, binning)]

        @property
        def is_moving(self):
            res = self.get("FilterState")
            self._is_moving = res
            return self._is_moving

        @property
        def filter(self):
            params = ["CurrentFilter"]  # , "CurrentFilterName"
            res = self.get("GetFilterSetting", params=params)
            self._filter = res
            return self._filter

        @filter.setter
        def filter(self, n):
            params = {"NewPosition": n}
            res = self.put("ChangeFilter", params=params)
            self._filter = self.filter

        @property
        def binning(self):
            params = ["BinX", "BinY"]
            res = self.get("ImagerGetSettings", params=params)
            if self.error:
                return None
            self.binning = list(map(int, res))
            return self._binning

        @binning.setter
        def binning(self, b):
            params = {"BinX": b[0], "BinY": b[1]}
            self.put("ImagerSetSettings", params=params)
            if self.error:
                return None
            self._binning = b

        @property
        def setpoint(self):
            params = ["CCDTemperatureSetpoint"]
            res = self.get("ImagerGetSettings", params=params)
            return res

        @property
        def temperature(self):
            params = ["CCDTemperature"]
            res = self.get("ImagerGetSettings", params=params)
            return round(res, 1)

        @temperature.setter
        def temperature(self, t):
            params = {"CCDTemperatureSetpoint": t}
            res = self.put("ImagerSetSettings", params=params)
            return self.setpoint

        @property
        def cooler(self):
            params = ["CoolerState"]
            res = self.get("ImagerGetSettings", params=params)
            self.cooler = True if res else False
            return self._cooler

        @cooler.setter
        def cooler(self, b):
            s = "1" if b else "0"
            params = {"CoolerState": s}
            res = self.put("ImagerSetSettings", params=params)
            self._cooler = b

        @property
        def fan(self):
            params = ["CoolerPower"]
            res = self.get("ImagerGetSettings", params=params)
            return round(res)

        @property
        def xrange(self):
            params = ["StartX", "NumX", "BinX"]  # start=20, width=100 (binning=1)
            res = self.get("ImagerGetSettings", params=params)
            if self.error:
                return None
            startx, numx, binx = list(map(int, res))
            # binx = self.binning[0] # binning is 2
            x_start = startx // binx
            x_end = numx // binx + x_start
            self.xrange = [x_start, x_end]  # start=10, end=60
            return self._xrange

        @xrange.setter
        def xrange(self, b):  # start=10, end=60
            binx = self.binning[0]  # binning is 2
            startx = b[0] * binx  # start: 10*2=20
            numx = b[1] * binx - startx  # width: 60*2-startx=100
            params = {"StartX": startx, "NumX": numx}
            res = self.put("ImagerSetSettings", params=params)
            self._xrange = b

        @property
        def yrange(self):
            params = ["StartY", "NumY", "BinY"]  # start=20, height=100 (binning=1)
            res = self.get("ImagerGetSettings", params=params)
            if self.error:
                return None
            starty, numy, biny = list(map(int, res))
            # biny = self.binning[1] # binning is 2
            y_start = starty // biny
            y_end = numy // biny + y_start
            self.yrange = [y_start, y_end]  # start=10, end=60
            return self._yrange

        @yrange.setter
        def yrange(self, b):  # start=10, end=60
            biny = self.binning[1]  # binning is 2
            starty = b[0] * biny  # start: 10*2=20
            numy = b[1] * biny - starty  # height: 60*2-starty=100
            params = {"StartY": starty, "NumY": numy}
            res = self.put("ImagerSetSettings", params=params)
            self._yrange = b

        @property
        def xystart(self):
            params = ["StartX", "StartY", "BinX", "BinY"]
            res = self.get("ImagerGetSettings", params=params)
            if self.error:
                return None
            startx, starty, binx, biny = list(map(int, res))
            x_start = startx // binx
            y_start = starty // biny
            self.xystart = [x_start, y_start]
            return self._xystart

        @xystart.setter
        def xystart(self, b):
            params = ["StartX", "StartY", "NumX", "NumY", "BinX", "BinY"]
            res = self.get("ImagerGetSettings", params=params)
            if self.error:
                return None
            startx, starty, numx, numy, binx, biny = list(map(int, res))
            numx = numx + startx - b[0] * binx
            numy = numy + starty - b[1] * biny
            startx = int(b[0]) * binx
            starty = int(b[1]) * biny
            params = {"StartX": startx, "StartY": starty,
                      "NumX": numx, "NumY": numy}
            res = self.put("ImagerSetSettings", params=params)
            self._xystart = b

        @property
        def xyend(self):
            params = ["StartX", "StartY", "NumX", "NumY", "BinX", "BinY"]
            res = self.get("ImagerGetSettings", params=params)
            if self.error:
                return None
            startx, starty, numx, numy, binx, biny = list(map(int, res))
            x_end = (startx + numx) // binx
            y_end = (starty + numy) // biny
            return [x_end, y_end]

        @xyend.setter
        def xyend(self, b):
            params = ["StartX", "StartY", "BinX", "BinY"]
            res = self.get("ImagerGetSettings", params=params)
            if self.error:
                return None
            startx, starty, binx, biny = list(map(int, res))
            numx = int(b[0]) * binx - startx
            numy = int(b[1]) * biny - starty
            params = {"NumX": numx, "NumY": numy}
            res = self.put("ImagerSetSettings", params=params)
            self._xyend = b

        @property
        def center(self):
            params = ["CameraXSize", "CameraYSize", "BinX", "BinY"]
            res = self.get("ImagerGetSettings", params=params)
            if self.error:
                return None
            camx, camy, binx, biny = list(map(int, res))
            centx = camx // binx // 2
            centy = camy // biny // 2
            return [centx, centy]