"""
not1mm Contest logger
Email: michael.bridak@gmail.com
GPL V3
Class: VfoWindow
Purpose: Provide onscreen widget that interacts with DIY VFO knob and remote rig.
"""

# 115200 pico default speed
# usb-Raspberry_Pi_Pico_E6612483CB1B242A-if00
# usb-Raspberry_Pi_Pico_W_E6614C311B331139-if00

import datetime
import logging
import os
import sys

import serial
from PyQt6 import QtWidgets, uic
from PyQt6.QtCore import QTimer, pyqtSignal
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import QDockWidget

from not1mm import fsutils
from not1mm.lib.cat_flrig import FlrigCAT
from not1mm.lib.cat_rigctld import RigctldCAT
from not1mm.lib.cat_tci import TciCAT
from not1mm.lib.preferences import Preferences

logger: logging.Logger = logging.getLogger(__name__)


class VfoWindow(QDockWidget):
    """The VFO window."""

    pref: dict = {}  # noqa: RUF012
    old_vfo: int = 0
    old_pico: str = ""
    message_shown: bool = False
    current_palette: QPalette | None = None
    device_reconnect: bool = False
    stale: datetime.datetime = datetime.datetime.now(datetime.UTC)
    vfowindow_closed = pyqtSignal()

    def __init__(self, action):
        super().__init__()
        self.action = action
        uic.loadUi(fsutils.APP_DATA_PATH / "vfo.ui", self)
        self.setWindowTitle("VFO Window")
        self.rig_control: FlrigCAT | RigctldCAT | TciCAT | None = None
        self.timer: QTimer = QTimer()
        self.timer.timeout.connect(self.getwaiting)
        self.load_pref()
        self.lcdNumber.display(0)
        self.pico: serial.Serial | None = None
        self.setup_serial()
        self.poll_rig_timer: QTimer = QTimer()
        self.poll_rig_timer.timeout.connect(self.poll_radio)
        self.poll_rig_timer.start(500)
        self.visibilityChanged.connect(self.window_state_changed)
        self.usb_devices = set()

    def load_pref(self) -> None:
        """
        Load preference file.
        Get CAT interface.
        """
        self.pref: dict = Preferences.data()

        if self.pref.get("useflrig", False):
            logger.debug(
                "Using flrig: %s",
                f"{self.pref.get('CAT_ip')} {self.pref.get('CAT_port')}",
            )
            self.rig_control: FlrigCAT | None = FlrigCAT(
                self.pref.get("CAT_ip", "127.0.0.1"),
                int(self.pref.get("CAT_port", 12345)),
            )
            self.timer.start(100)
        if self.pref.get("userigctld", False):
            logger.debug(
                "Using rigctld: %s",
                f"{self.pref.get('CAT_ip')} {self.pref.get('CAT_port')}",
            )
            self.rig_control: RigctldCAT | None = RigctldCAT(
                self.pref.get("CAT_ip", "127.0.0.1"),
                int(self.pref.get("CAT_port", 4532)),
            )
            self.timer.start(100)
        if self.pref.get("usetci", False):
            logger.debug(
                "Using TCI: %s",
                f"{self.pref.get('CAT_ip')} {self.pref.get('CAT_port')}",
            )
            self.rig_control: TciCAT | None = TciCAT(
                self.pref.get("CAT_ip", "127.0.0.1"),
                int(self.pref.get("CAT_port", 50001)),
            )
            self.timer.start(100)

    def discover_device(self) -> str | None:
        """
        Poll all serial devices looking for correct one.

        Rummage thru /dev/serial/by-id/ looking for Raspberry Picos
        Ask each if it's a vfoknob.
        Return the device ID if it is, or None if not found.
        """

        devices: list[str] | None = self.get_devices()
        data: bytes | None = None
        if devices is None:
            return None
        if sys.platform == "darwin":
            usb_devices = {device for device in devices if "usb" in device}
            new_usb_devices = usb_devices - self.usb_devices
            self.usb_devices = usb_devices
            if len(new_usb_devices) == 0:
                return None
            logger.debug(f"new_usb_devices: {new_usb_devices}")
            for device in new_usb_devices:
                try:
                    with serial.Serial("/dev/" + device, 115200) as ser:
                        logger.debug(f"trying usb device: {device}")
                        ser.timeout = 1.0
                        ser.write(b"whatareyou\r")
                        data = ser.readline()
                except serial.serialutil.SerialException as ex:
                    logger.debug(f"writing usb_device: {device} failed: {ex}")
                    return None
                if "vfoknob" in data.decode().strip():
                    return "/dev/" + device
            return None
        for device in devices:
            if "usb-Raspberry_Pi_Pico" in device:
                try:
                    with serial.Serial("/dev/serial/by-id/" + device, 115200) as ser:
                        ser.timeout = 1000
                        ser.write(b"whatareyou\r")
                        data = ser.readline()
                except serial.serialutil.SerialException:
                    return None
                if "vfoknob" in data.decode().strip():
                    return "/dev/serial/by-id/" + device

    def get_devices(self) -> list[str] | None:
        try:
            if sys.platform != "darwin":
                return os.listdir("/dev/serial/by-id")
            return os.listdir("/dev/")
        except FileNotFoundError:
            return None
        return None

    def window_state_changed(self) -> None:
        """Setup vfo knob if window is toggled on"""

        if self.isVisible():
            self.setup_serial()

    def setup_serial(self, supress_msg=False) -> None:
        """
        Setup the device returned by discover_device()
        Or display message saying we didn't find one.
        """

        if not self.isVisible():
            return

        device: str | None = self.discover_device()
        if device is not None:
            try:
                self.pico: serial.Serial = serial.Serial(device, 115200)
                self.pico.timeout = 100
                self.lcdNumber.setStyleSheet("QLCDNumber { color: green; }")
                self.device_reconnect: bool = True
            except OSError:
                if self.message_shown is False and supress_msg is False:
                    self.message_shown: bool = True
                self.lcdNumber.setStyleSheet("QLCDNumber { color: red; }")
        else:
            if self.message_shown is False and supress_msg is False:
                self.message_shown: bool = True
            self.lcdNumber.setStyleSheet("QLCDNumber { color: red; }")

    def msg_from_main(self, msg_dict) -> None:
        """
        Watch for a 'HALT' UPD packet from not1mm.
        Exit app if found.
        """
        if msg_dict.get("cmd", "") == "TUNE":
            # b'{"cmd": "TUNE", "freq": 7.0235, "spot": "MM0DGI"}'
            try:
                vfo: float = float(msg_dict.get("freq")) * 1000000
            except ValueError:
                return
            changefreq: str = f"F {int(vfo)}\r"
            try:
                if self.pico is not None:
                    self.pico.write(changefreq.encode())
            except OSError:
                logger.critical("Unable to write to serial device.")
            except AttributeError:
                logger.critical("Unable to write to serial device.")
            return

    def showNumber(self, the_number: int | str) -> None:
        """Display vfo value with dots"""
        dvfo: str = str(the_number)
        if len(dvfo) > 6:
            dnum: str = f"{dvfo[: len(dvfo) - 6]}.{dvfo[-6:-3]}.{dvfo[-3:]}"
            self.lcdNumber.display(dnum)

    def poll_radio(self) -> None:
        """
        Poll radio via CAT asking for VFO state.
        If it's with in the HAM bands set the vfo knob to match the radio.
        """
        if not self.isVisible():
            return
        if datetime.datetime.now(datetime.UTC) < self.stale:
            return
        if self.rig_control is not None:
            if self.rig_control.online is False:
                self.rig_control.reinit()
            if self.rig_control.online is True:
                try:
                    vfo: int = int(self.rig_control.get_vfo())
                except ValueError:
                    return
                if vfo != self.old_vfo or self.device_reconnect is True:
                    self.old_vfo: int = vfo
                    logger.debug(f"{vfo}")
                    self.showNumber(vfo)
                    cmd: str = f"F {vfo}\r"
                    self.device_reconnect = False
                    try:
                        if self.pico is not None:
                            self.pico.write(cmd.encode())
                    except OSError:
                        logger.critical("Unable to write to serial device.")
                    except AttributeError:
                        logger.critical("Unable to write to serial device.")

    def getwaiting(self) -> None:
        """
        Get the USB VFO knob state.
        Set the radio's VFO to match if it has changed.
        """
        try:
            if self.pico is not None:
                self.pico.write(b"f\r")
                while self.pico.in_waiting:
                    result: str = self.pico.read(self.pico.in_waiting).decode().strip()
                    if self.old_pico != result:
                        self.old_pico: str = result
                        self.stale: datetime.datetime = datetime.datetime.now(
                            datetime.UTC
                        ) + datetime.timedelta(seconds=1)
                        if self.rig_control is not None:
                            self.rig_control.set_vfo(result)
                            self.showNumber(result)
            else:
                self.setup_serial(supress_msg=True)
        except OSError:
            logger.critical("Unable to write to serial device.")
            self.pico: serial.Serial | None = None
        except AttributeError:
            logger.critical("Unable to write to serial device.")
            self.pico: serial.Serial | None = None
        except KeyboardInterrupt:
            ...

    def show_message_box(self, message: str) -> None:
        """
        Display an alert box with the supplied message.
        """
        message_box: QtWidgets.QMessageBox = QtWidgets.QMessageBox()
        if self.current_palette is not None:
            message_box.setPalette(self.current_palette)
        message_box.setIcon(QtWidgets.QMessageBox.Icon.Information)
        message_box.setText(message)
        message_box.setWindowTitle("Information")
        message_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
        _ = message_box.exec()

    def closeEvent(self, event) -> None:
        self.action.setChecked(False)
        self.vfowindow_closed.emit()
        event.accept()
