import asyncio
import qasync

from enum import Enum

from PySide6 import QtWidgets, QtCore
from PySide6.QtCore import QTimer, Signal
from PySide6.QtWidgets import QMessageBox, QApplication
from astropy.coordinates import Angle, SkyCoord, EarthLocation, ICRS, AltAz
import astropy.units as u
from astropy.time import Time

from pybrotlib import BROT, Transport, Telemetry
from pybrotlib.components import *
from .qmessagelogger import QMessageLogger
from .qt.telescopecontrol_ui import Ui_TelescopeControl

from .qmessagelogger import QMessageLogger


class COORDS(Enum):
    EQUATORIAL = "Equatorial"
    HORIZONTAL = "Horizontal"


class QTelescopeControl(QtWidgets.QWidget, Ui_TelescopeControl):
    new_target_coordinates = Signal(SkyCoord)

    def __init__(
        self,
        transport: Transport,
        brot: BROT,
        logger: QMessageLogger,
        location: EarthLocation,
        parent=None,
    ):
        QtWidgets.QWidget.__init__(self, parent)
        self.setupUi(self)
        self.transport = transport
        self.brot = brot
        self.logger = logger
        self.location = location

        self._focus = brot.focus
        self._mirror_covers = brot.mirrorcovers
        self._dome = brot.dome
        self._telescope = brot.telescope

        # init variables:
        self._set_coord_equi = SkyCoord(
            ra=0 * u.deg,
            dec=0 * u.deg,
            frame="icrs",
            location=location,
            obstime=Time.now(),
        )
        self._frame_alt_az = AltAz(
            obstime=Time.now(),
            location=self.location,
            pressure=998.07853205838114 * u.hPa,
            temperature=(282.35 - 273.15) * u.deg_C,
        )
        self._set_coord_hor = self._set_coord_equi.transform_to(self._frame_alt_az)
        self._jog_distance = 30.0

        self.messageLogger = QMessageLogger(listIncomingMessages=self.listIncomingMessages)
        self.transport.logMessageReceived.connect(self.messageLogger.log)
        self.messageLogger.log("CRITICAL", "GUI has been started, now listening...")
        # button connections
        self.buttonCoordSystem.clicked.connect(self._change_coord_system)
        self.buttonSetSimbadObject.clicked.connect(self._query_simbad)
        self.buttonSetJplObject.clicked.connect(self._query_jpl_horizons)
        self.buttonSetSolarObject.clicked.connect(self._query_solar_system)
        self.buttonSetCoord.clicked.connect(self._set_coordinates)
        self.buttonInitialize.clicked.connect(self._power_on)
        self.buttonPark.clicked.connect(self._park)
        self.buttonStop.clicked.connect(self._stop)
        self.buttonSetFocus.clicked.connect(self._set_focus)
        self.buttonMove.clicked.connect(self._move_telescope)
        self.buttonErrAckTel.clicked.connect(self._error_ack_tel)
        self.buttonResetTel.clicked.connect(self._reset_tel)
        self.buttonSetJog.clicked.connect(self._set_jog_distance)
        self.buttonJogUp.clicked.connect(lambda: self._set_jog_offset("up"))
        self.buttonJogDown.clicked.connect(lambda: self._set_jog_offset("down"))
        self.buttonJogLeft.clicked.connect(lambda: self._set_jog_offset("left"))
        self.buttonJogRight.clicked.connect(lambda: self._set_jog_offset("right"))
        self.buttonClearJogOffsets.clicked.connect(lambda: self._set_jog_offset("clear"))
        self.buttonOpenDome.clicked.connect(self._set_dome_open)
        self.buttonCloseDome.clicked.connect(self._set_dome_close)
        self.buttonParkDome.clicked.connect(self._set_dome_park)
        self.buttonDomeTrack.clicked.connect(self._set_dome_track)
        # self.buttonErrAckDome.clicked.connect(self._error_ack_dome)
        self.buttonResetDome.clicked.connect(self._reset_dome)
        self._coord_system = COORDS(self.buttonCoordSystem.text())

        QTimer.singleShot(100, self._update_values_loop)

    @qasync.asyncSlot()
    async def _update_values_loop(self) -> None:
        while True:
            await self._update_values()
            await asyncio.sleep(0.5)

    @qasync.asyncSlot()
    async def _update_values(self) -> None:
        """Updates the values received from the Telescope."""

        # covers
        self.labelMirrorCoverStatus.setText(self._mirror_covers.status.value)

        # dome
        if self._dome.error_state:
            self.buttonResetDome.setEnabled(True)
            self.buttonResetDome.setVisible(True)
        else:
            # disable reset
            self.buttonResetDome.setVisible(False)
            self.buttonResetDome.setEnabled(False)

        # dome shutter
        self.labelDomeShutter.setText(self._dome.shutter.value)

        match self.brot.dome.status:
            case DomeStatus.PARKED:
                self.labelDomeParked.setText("parked")
                self.labelDomeTracking.setText("not tracking")
                self.buttonDomeTrack.setText("track")
            case DomeStatus.TRACKING:
                self.labelDomeParked.setText("not parked")
                self.labelDomeTracking.setText("tracking")
                self.buttonDomeTrack.setText("stop")
            case DomeStatus.UNKNOWN:
                self.labelDomeParked.setText("not parked")
                self.labelDomeTracking.setText("not tracking")
                self.buttonDomeTrack.setText("track")

        # Focus position
        self.labelCurrentFocus.setText(f"{self._focus.position:.2f}" + " mm")

        match self.brot.telescope.global_status:
            case GlobalTelescopeStatus.OPERATIONAL:
                # diable err_ack and reset
                self.buttonErrAckTel.setVisible(False)
                self.buttonResetTel.setVisible(False)
                self.buttonResetTel.setEnabled(False)
            case GlobalTelescopeStatus.PANIC | GlobalTelescopeStatus.ERROR | _:
                # enable err_ack and reset
                self.buttonErrAckTel.setVisible(True)
                self.buttonResetTel.setVisible(True)

                match self.brot.telescope.global_status:
                    case GlobalTelescopeStatus.PANIC:
                        self.labelErrorDisplayTel.setText("E-Stop triggered")
                    case GlobalTelescopeStatus.ERROR:
                        self.labelErrorDisplayTel.setText(
                            "DEC: "
                            + str(self.brot.telescope.dec.error_state)
                            + " HA: "
                            + str(self.brot.telescope.ha.error_state)
                            + " FOC: "
                            + str(self.brot.telescope.focus.error_state)
                        )

        # account for the different coordinate systems
        if self._coord_system == COORDS.EQUATORIAL:
            self.labelSetCoord1.setText(self._set_coord_equi.ra.to_string(unit=u.hour, precision=1))
            self.labelSetCoord2.setText(self._set_coord_equi.dec.to_string(precision=1))

        else:
            self.labelSetCoord1.setText(f"{self._set_coord_hor.alt.degree:.2f}°")
            self.labelSetCoord2.setText(f"{self._set_coord_hor.az.degree:.2f}°")

        # offsets
        if self._telescope.mount == "HA_DEC":
            self.labelOffset1.setText("HA:")
            self.labelValueOffset1.setText(f'{self._telescope.offset_ha:.2f}"')
            self.labelOffset2.setText("Dec:")
            self.labelValueOffset2.setText(f'{self._telescope.offset_dec:.2f}"')
        elif self._telescope.mount == "ALT_AZ":
            self.labelOffset1.setText("Az:")
            self.labelValueOffset1.setText(f'{self._telescope.offset_az:.2f}"')
            self.labelOffset2.setText("Alt:")
            self.labelValueOffset2.setText(f'{self._telescope.offset_alt:.2f}"')

        self.labelJogValue.setText(str(self._jog_distance) + '"')

    def _log_mess(self, level: str, message: str) -> None:
        """Update logger with message"""
        self.logger.log(level, message)
        self.messageLogger.log(level, message)

    def _power_on(self) -> None:
        """Power on the Telescope."""
        dlg = QMessageBox(self)
        dlg.setWindowTitle("Telescope Power On")
        dlg.setText("Power On and Home the Telescope?")
        dlg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel)
        dlg.setIcon(QMessageBox.Icon.Question)
        button = dlg.exec()

        if button == QMessageBox.StandardButton.Yes:
            self._log_mess("INFO", "Initializing telescope...")
            self._log_mess("SEND", "command power=TRUE")
            asyncio.create_task(self.brot.telescope.power_on())

    def _stop(self) -> None:
        """Stop the Telescope."""
        self._log_mess("INFO", "Stopping telescope...")
        self._log_mess("SEND", "command stop=TRUE")
        asyncio.create_task(self.brot.telescope.stop())

    def _park(self) -> None:
        """Park the Telescope."""
        if self.brot.telescope.status == TelescopeStatus.PARKED:
            QMessageBox.critical(self, "Park", "Telescope is allready parked.")
            self._log_mess("INFO", "Telescope is allready parked.")
        else:
            self._log_mess("INFO", "Parking telescope...")
            self._log_mess("SEND", "command park=TRUE")

            asyncio.create_task(self.brot.telescope.park())

    def _set_focus(self) -> None:
        """Set the Focus of the Telescope."""
        self._log_mess("INFO", "Setting focus to: " + str(self.spinFocus.value()))
        if not self.brot.focus.powered or not self.brot.focus.referenced:
            QMessageBox.critical(
                self,
                "Set Focus",
                "Focus Axis is not calibrated and ready.\n Cannot set Focus. Please init the telescope first.",
            )
            self._log_mess(
                "ERROR", " Cannot set Focus, Focus Axis is not calibrated and ready. Please init the telescope first."
            )
        else:
            self._log_mess("SEND", "command focus=" + str(self.spinFocus.value()))
            asyncio.create_task(self.brot.focus.set(self.spinFocus.value()))

    def _set_dome_open(self) -> None:
        """Set the Dome to open shutter"""
        dlg = QMessageBox(self)
        dlg.setWindowTitle("Dome Control")
        dlg.setText("Open Dome?")
        dlg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel)
        dlg.setIcon(QMessageBox.Icon.Question)
        button = dlg.exec()

        if button == QMessageBox.StandardButton.Yes:
            self._log_mess("INFO", "Opening Dome.")
            asyncio.create_task(self.brot.dome.open())

    def _set_dome_close(self) -> None:
        """Set the Dome to close shutter"""
        self._log_mess("INFO", "Closing Dome.")
        asyncio.create_task(self.brot.dome.close())

    def _set_dome_track(self) -> None:
        """Link/ Un-link the Dome to azimuth"""
        if self.brot.dome.status != DomeStatus.TRACKING:
            self._log_mess("INFO", "Linking the Dome to Telescope Azimuth.")
            asyncio.create_task(self.brot.dome.start_tracking())
        else:
            self._log_mess("INFO", "Un-linking the Dome to Telescope Azimuth.")
            asyncio.create_task(self.brot.dome.stop_tracking())

    def _set_dome_park(self) -> None:
        """Park the Dome"""
        self._log_mess("INFO", "Park Dome.")
        asyncio.create_task(self.brot.dome.park())

    def _set_jog_distance(self) -> None:
        """Set the Jog distance of the control"""
        self._jog_distance = self.spinJog.value()

    def _set_jog_offset(self, command) -> None:
        """Jog the telescope via offsets, or clear said offsets"""
        if self.brot.telescope.status == TelescopeStatus.ONLINE:
            # get offset paramaters
            if self._telescope.mount == "ALT_AZ":
                offset1 = self._telescope.offset_az
                offset2 = self._telescope.offset_alt
                set_offset1 = self._telescope.set_offset_az
                set_offset2 = self._telescope.set_offset_alt
            elif self._telescope.mount == "HA_DEC":
                offset1 = self._telescope.offset_ha
                offset2 = self._telescope.offset_dec
                set_offset1 = self._telescope.set_offset_ha
                set_offset2 = self._telescope.set_offset_dec
            else:
                return

            match command:
                case "up":
                    print("up", offset2 + self._jog_distance)
                    asyncio.create_task(set_offset2(offset2 + self._jog_distance))
                case "down":
                    asyncio.create_task(set_offset2(offset2 - self._jog_distance))
                case "left":
                    asyncio.create_task(set_offset1(offset1 - self._jog_distance))
                case "right":
                    asyncio.create_task(set_offset1(offset1 + self._jog_distance))
                case "clear":
                    asyncio.create_task(set_offset1(0.0))
                    asyncio.create_task(set_offset2(0.0))
        else:
            QMessageBox.critical(self, "Jog Telescope", "Telescope is not ready. Please init the telescope first.")
            self._log_mess("ERROR", "Telescope is not ready. Please init the telescope first.")

    def _move_telescope(self) -> None:
        """Move the Telescope."""
        self._log_mess("INFO", "Moving telescope")
        if self.brot.telescope.status == TelescopeStatus.ONLINE:
            if self._coord_system == COORDS.EQUATORIAL:
                # Goto: Move AND track
                self._log_mess("SEND", f"command rightascension={self._set_coord_equi.ra.value}")
                self._log_mess("SEND", f"command declination={self._set_coord_equi.dec.value}")
                self._log_mess("SEND", f"command track=1")
                asyncio.create_task(
                    self.brot.telescope.track(self._set_coord_equi.ra.value, self._set_coord_equi.dec.value)
                )

            else:
                # Slew: Move to and stop
                self._log_mess("SEND", f"command elevation={self._set_coord_hor.alt.value}")
                self._log_mess("SEND", f"command azimuth={self._set_coord_hor.az.value}")
                self._log_mess("SEND", f"command slew=1")
                asyncio.create_task(
                    self.brot.telescope.move(self._set_coord_hor.alt.value, self._set_coord_hor.az.value)
                )

        else:
            QMessageBox.critical(self, "Move Telescope", "Telescope is not ready. Please init the telescope first.")
            self._log_mess("ERROR", "Telescope is not ready. Please init the telescope first.")

    def _error_ack_tel(self) -> None:
        """Acknowledgement of the Error and if so, enabling the reset button."""

        dlg = QMessageBox(self)
        dlg.setWindowTitle("Error Acknowledgement")
        dlg.setText("I have acknowledged the Error and have made sure the telescope is save to reset.")
        dlg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel)
        dlg.setIcon(QMessageBox.Icon.Warning)
        button = dlg.exec()

        if button == QMessageBox.StandardButton.Yes:
            self.buttonResetTel.setEnabled(True)

    def _reset_tel(self) -> None:
        """Reset the telescope."""
        dlg = QMessageBox(self)
        dlg.setWindowTitle("Reset Telescope")
        dlg.setText("Reset the Telescope?")
        dlg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel)
        dlg.setIcon(QMessageBox.Icon.Question)
        button = dlg.exec()

        if button == QMessageBox.StandardButton.Yes:
            self._log_mess("INFO", "Resetting telescope...")
            self._log_mess("SEND", "command reset=1")
            asyncio.create_task(self.brot.telescope.reset())

    def _reset_dome(self) -> None:
        """Reset the telescope."""
        dlg = QMessageBox(self)
        dlg.setWindowTitle("Reset Dome")
        dlg.setText("Reset the Dome?")
        dlg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel)
        dlg.setIcon(QMessageBox.Icon.Question)
        button = dlg.exec()

        if button == QMessageBox.StandardButton.Yes:
            self._log_mess("INFO", "Resetting telescope...")
            self._log_mess("SEND", "command reset=1")
            asyncio.create_task(self.brot.dome.reset())

    def _query_simbad(self) -> None:
        # https://github.com/pyobs/pyobs-gui/blob/478b5a00ec4aac4637df06ff526366fa8a984657/pyobs_gui/telescopewidget.py#L422C5-L451C48
        # namespace was modified to current GUI
        """
        Takes the object name from the text box, queries simbad, and fills the RA/Dec inputs with the result.
        """
        from astroquery.simbad import Simbad

        # clear solar system and jpl and coords
        self.comboSolar.setCurrentText("")
        self.editJplQuery.clear()
        self.editCoord1.clear()
        self.editCoord2.clear()
        # wait cursor
        QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)

        # query
        try:
            query_input = self.editSimbadQuery.text()
            result = Simbad.query_object(query_input)
        except Exception as error:
            print(error)
            QMessageBox.critical(self, "Simbad", "No result found for (see terminal for details):\n" + query_input)
            return
        finally:
            # restore cursor
            QApplication.restoreOverrideCursor()

        # always use first result
        for r in result:
            # set it
            self._set_coord_equi = SkyCoord(
                ra=r["ra"] * u.deg,
                dec=r["dec"] * u.deg,
                frame="icrs",
                location=self.location,
                obstime=Time.now(),
            )
            self.label_set_coord.setText("Set: " + query_input)
            self.new_target_coordinates.emit(self._set_coord_equi)
        # self._update_values()

    def _query_jpl_horizons(self) -> None:
        """
        Takes the object name from the text box, queries JPL Horizons, and fills the RA/Dec inputs with the result.
        """
        from astroquery.jplhorizons import Horizons

        # clear solar system and simbad and coords
        self.comboSolar.setCurrentText("")
        self.editSimbadQuery.clear()
        self.editCoord1.clear()
        self.editCoord2.clear()
        # wait cursor
        QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
        query_input = self.editJplQuery.text()
        # query
        try:
            # query
            obj = Horizons(id=query_input, location=None, epochs=Time.now().jd)

            # get ephemerides
            eph = obj.ephemerides()

        except ValueError as error:
            print(error)
            QMessageBox.critical(self, "JPL Horizons", "Error in result, see terminal for details.")
            return
        except:
            QMessageBox.critical(self, "JPL Horizons", "No result found for:\n" + query_input)
            return
        finally:
            # restore cursor
            QApplication.restoreOverrideCursor()

        # always use first result
        self._set_coord_equi = SkyCoord(
            ra=eph["RA"][0] * u.deg,
            dec=eph["DEC"][0] * u.deg,
            frame="icrs",
            location=self.location,
            obstime=Time.now(),
        )
        self.label_set_coord.setText("Set: " + query_input)
        self.new_target_coordinates.emit(self._set_coord_equi)
        return

    def _query_solar_system(self) -> None:
        """Set RA/Dec for selected solar system body."""
        from astropy.coordinates import solar_system_ephemeris, get_body

        query_input = self.comboSolar.currentText()
        # nothing?
        if query_input == "":
            return

        # clear simbad and JPL and coords
        self.editJplQuery.clear()
        self.editSimbadQuery.clear()
        self.editCoord1.clear()
        self.editCoord2.clear()

        QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
        try:
            with solar_system_ephemeris.set("builtin"):
                # get coordinates
                self._set_coord_equi = get_body(query_input, Time.now(), self.location)
                self.new_target_coordinates.emit(self._set_coord_equi)
        except Exception as error:
            print(error)
            QMessageBox.critical(self, "Solar Object", "Error in result, see terminal for details.")
            return
        finally:
            QApplication.restoreOverrideCursor()

        self.label_set_coord.setText("Set: " + query_input)
        return

    def _set_coordinates(self):
        """Set Coordinates via the lineEdits"""
        self.editJplQuery.clear()
        self.editSimbadQuery.clear()
        self.comboSolar.setCurrentText("")
        if self._coord_system == COORDS.EQUATORIAL:
            ra = self.editCoord1.text()
            dec = self.editCoord2.text()
            try:
                self._set_coord_equi = SkyCoord(
                    ra + " " + dec,
                    frame=ICRS,
                    unit=(u.hour, u.deg),
                    obstime=Time.now(),
                    location=self.location,
                )
                self.new_target_coordinates.emit(self._set_coord_equi)
            except ValueError as error:
                print(error)
                # could not create coordinates
                QMessageBox.critical(
                    self,
                    "Equatorial Coords",
                    "Invalid coordinates. Please enter coordinates as:\n Ra: hour minutes seconds e.g. 18h37m45.6s for 18h37m45.6s\n Dec: degree arcmin arcsec e.g. 38d48m12.6s for 38°48\"12.6'",
                )
                return

        else:
            alt = self.editCoord1.text()
            az = self.editCoord2.text()
            try:
                self._set_coord_hor = SkyCoord(
                    az + " " + alt,
                    frame=AltAz,
                    unit=(u.deg, u.deg),
                    obstime=Time.now(),
                    location=self.location,
                    pressure=998.07853205838114 * u.hPa,
                    temperature=(282.35 - 273.15) * u.deg_C,
                )
                self.new_target_coordinates.emit(self._set_coord_hor)
            except ValueError as error:
                # could not create coordinates
                print(error)
                QMessageBox.critical(
                    self,
                    "Equatorial Coords",
                    "Invalid coordinates. Please enter coordinates as:\n Alt: degree arcmin arcsec e.g. 38d48m12.6s for 38°48\"12.6' Az: degree arcmin arcsec e.g. 38d48m12.6s for 38°48\"12.6'",
                )
                return
        self.label_set_coord.setText("Set: Coords")

    def _change_coord_system(self) -> None:
        self.editCoord1.clear()
        self.editCoord2.clear()
        self.editJplQuery.clear()
        self.editSimbadQuery.clear()
        self.comboSolar.setCurrentText("")
        if self._coord_system == COORDS.EQUATORIAL:
            self._coord_system = COORDS.HORIZONTAL
            self._set_coord_hor = self._ra_dec_to_alt_az(Time.now())
            self.label_set_coord.setText("Set: Coords")
            self.labelCoordName1.setText("ALT")
            self.labelCoordName2.setText("AZ")
            self.label_coord_name_1_.setText("ALT")
            self.label_coord_name_2_.setText("AZ")
            self.groupBox_query.setEnabled(False)
            self.buttonMove.setText("Move to, no track")
        else:
            self._coord_system = COORDS.EQUATORIAL
            self.labelCoordName1.setText("RA")
            self.labelCoordName2.setText("DEC")
            self.label_coord_name_1_.setText("RA")
            self.label_coord_name_2_.setText("DEC")
            self.groupBox_query.setEnabled(True)
            self.buttonMove.setText("Move to and track")
        self.buttonCoordSystem.setText(self._coord_system.value)
        # self._update_values()

    def _ra_dec_to_alt_az(self, times) -> SkyCoord:

        # update Alt Az frame to current time
        self._frame_alt_az = AltAz(
            obstime=times,
            location=self.location,
            pressure=998.07853205838114 * u.hPa,
            temperature=(282.35 - 273.15) * u.deg_C,
        )
        # create SkyCoord obj
        coord_ra_dec = SkyCoord(
            ra=self._set_coord_equi.ra,
            dec=self._set_coord_equi.dec,
            frame="icrs",
            location=self.location,
            obstime=times,
            pressure=998.07853205838114 * u.hPa,
            temperature=(282.35 - 273.15) * u.deg_C,
        )
        coord_alt_az = coord_ra_dec.transform_to(self._frame_alt_az)

        return coord_alt_az
