"""
    polling.py
    ----------
    Implements a QThread for polling the Grid unit for fan rpm and voltage data,
    as well as CPU and GPU temperatures from OpenHardwareMonitor.
"""

import sys
import time

import pythoncom
import wmi
from PyQt5 import QtCore

import grid
import helper
import openhwmon

# Define status icons (available in the resource file built with "pyrcc5"
ICON_RED_LED = ":/icons/led-red-on.png"
ICON_GREEN_LED = ":/icons/green-led-on.png"

class PollingThread(QtCore.QThread):
    """QThread, performs the following:
        - Get fan rpm from Grid
        - Get fan voltage from Grid
        - Get CPU and GPU temperatures from OpenHardwareMonitor"""

    # Signals handling the fan rpm
    rpm_signal_fan1 = QtCore.pyqtSignal(str)
    rpm_signal_fan2 = QtCore.pyqtSignal(str)
    rpm_signal_fan3 = QtCore.pyqtSignal(str)
    rpm_signal_fan4 = QtCore.pyqtSignal(str)
    rpm_signal_fan5 = QtCore.pyqtSignal(str)
    rpm_signal_fan6 = QtCore.pyqtSignal(str)

    # Signals handling the fan voltage
    voltage_signal_fan1 = QtCore.pyqtSignal(str)
    voltage_signal_fan2 = QtCore.pyqtSignal(str)
    voltage_signal_fan3 = QtCore.pyqtSignal(str)
    voltage_signal_fan4 = QtCore.pyqtSignal(str)
    voltage_signal_fan5 = QtCore.pyqtSignal(str)
    voltage_signal_fan6 = QtCore.pyqtSignal(str)

    # Signals handling the pixmap icon (red or green led) indicating the fan status
    pixmap_signal_fan1 = QtCore.pyqtSignal(str)
    pixmap_signal_fan2 = QtCore.pyqtSignal(str)
    pixmap_signal_fan3 = QtCore.pyqtSignal(str)
    pixmap_signal_fan4 = QtCore.pyqtSignal(str)
    pixmap_signal_fan5 = QtCore.pyqtSignal(str)
    pixmap_signal_fan6 = QtCore.pyqtSignal(str)

    # Signals handling CPU and GPU temperatures
    cpu_temp_signal = QtCore.pyqtSignal(int)
    gpu_temp_signal = QtCore.pyqtSignal(int)

    hwmon_status_signal = QtCore.pyqtSignal(str)

    # Signal to indicate fan speed should be updated
    update_signal = QtCore.pyqtSignal()

    # Signal handling exceptions that may occur in the running thread
    exception_signal = QtCore.pyqtSignal(str)

    def __init__(self, polling_interval, ser, lock, cpu_sensor_ids, gpu_sensor_ids, cpu_calc, gpu_calc):
        """ Constructor for the polling thread."""

        super().__init__()

        # "keep_running" controls the while loop in the running thread
        # Initial value is False as the thread is not started yet
        self.keep_running = False

        # Polling interval (ms)
        self.polling_interval = polling_interval

        # Serial device
        self.ser = ser

        # Lock
        self.lock = lock

        # List of CPU and GPU temperature sensors to use
        self.cpu_sensor_ids = cpu_sensor_ids
        self.gpu_sensor_ids = gpu_sensor_ids

        # Defines if CPU and GPU temperatures should be "Maximum" or "Average" from selected sensors
        self.cpu_calc = cpu_calc
        self.gpu_calc = gpu_calc

    def __del__(self):
        self.wait()

    def stop(self):
        """Stop the running thread gracefully."""

        print("Stopping thread...")
        self.keep_running = False

        # Wait for the thread to stop
        self.wait()
        print("Thread stopped")

        # Uninitialize at thread stop (used for WMI in thread)
        pythoncom.CoUninitialize()

    def set_temp_calc(self, cpu_calc, gpu_calc):
        """Setter for cpu and gpu calc parameter."""

        self.cpu_calc = cpu_calc
        self.gpu_calc = gpu_calc

    def update_polling_interval(self, new_polling_interval):
        """Setter for polling interval value."""

        self.polling_interval = new_polling_interval

    def update_sensors(self, cpu_sensor_ids, gpu_sensor_ids):
        """Setter for CPU and GPU sensor id's."""

        self.cpu_sensor_ids = cpu_sensor_ids
        self.gpu_sensor_ids = gpu_sensor_ids

    def calculate_temp(self, temperature_sensors, type):
        """Calculate CPU/GPU temperatures (maximum or average value)"""

        if type == "cpu":
            cpu_temps = []

            # Check if any sensors are configured
            if self.cpu_sensor_ids:
                for id in self.cpu_sensor_ids:
                    for sensor in temperature_sensors:
                        if id == sensor.Identifier:
                            cpu_temps.append(sensor.Value)

                # Convert to float
                cpu_temps_float = [float(i) for i in cpu_temps]

            else:
                cpu_temps_float = [0]

            # Check if temperature values are available
            if cpu_temps_float:
                # Use maximum value
                if self.cpu_calc == "Max":
                    return max(cpu_temps_float)
                # Use average value
                elif self.cpu_calc == "Avg":
                    return (sum(cpu_temps_float) / len(cpu_temps_float))

            # If no temperature values are available, return 0
            else:
                return 0

        elif type == "gpu":
            gpu_temps = []

            # Check if any sensors are configured
            if self.gpu_sensor_ids:
                for id in self.gpu_sensor_ids:
                    for sensor in temperature_sensors:
                        if id == sensor.Identifier:
                            gpu_temps.append(sensor.Value)

                # Convert to float
                gpu_temps_float = [float(i) for i in gpu_temps]

            else:
                gpu_temps_float = [0]

            # Check if temperature values are available
            if gpu_temps_float:
                # Use maximum value
                if self.gpu_calc == "Max":
                    return max(gpu_temps_float)
                # Use average value
                elif self.gpu_calc == "Avg":
                    return (sum(gpu_temps_float) / len(gpu_temps_float))

            # If no temperature values are available, return 0
            else:
                return 0

    def run(self):
        """Main thread processing loop:
            - Poll the Grid for fan rpm and voltage.
            - Poll OpenHardwareMonitor for CPU and GPU temperatures
            - Emit signals:
                - Fan rpm's
                - Fan voltages
                - CPU temperature
                - GPU temperature
                - Update fans
        """

        try:
            print("Starting thread...")

            # CoInitialise() is needed when accessing WMI in a thread
            # CoUninitialize() is called in the stop method
            pythoncom.CoInitialize()

            # A new WMI object is needed in the thread
            hwmon_thread_wmi = wmi.WMI(namespace="root\OpenHardwareMonitor")

            # "keep_running" should be True before starting the while loop
            self.keep_running = True

            # Start the main polling loop
            while self.keep_running:
                # Get current temperature sensors from OpenHardwareMonitor
                temperature_sensors = openhwmon.get_temperature_sensors(hwmon_thread_wmi)

                # Calculate CPU and GPU temperatures
                current_cpu_temp = self.calculate_temp(temperature_sensors, "cpu")
                current_gpu_temp = self.calculate_temp(temperature_sensors, "gpu")

                # Emit temperature signals
                self.cpu_temp_signal.emit(current_cpu_temp)
                self.gpu_temp_signal.emit(current_gpu_temp)

                # If both CPU and GPU temp are 0, set OpenHardwareMonitor status to "Disconnected"
                if current_cpu_temp == current_gpu_temp == 0:
                    self.hwmon_status_signal.emit('<b><font color="red">---</font></b>')
                else:
                    self.hwmon_status_signal.emit('<b><font color="green">Connected</font></b>')

                # Read rpm for all fans
                fans_rpm = grid.read_fan_rpm(self.ser, self.lock)

                # Check if there is fan rpm data available
                if fans_rpm:
                    # Emit rpm signals with current rpm values
                    self.rpm_signal_fan1.emit(str(fans_rpm[0]))
                    self.rpm_signal_fan2.emit(str(fans_rpm[1]))
                    self.rpm_signal_fan3.emit(str(fans_rpm[2]))
                    self.rpm_signal_fan4.emit(str(fans_rpm[3]))
                    self.rpm_signal_fan5.emit(str(fans_rpm[4]))
                    self.rpm_signal_fan6.emit(str(fans_rpm[5]))

                # If no rpm data is available, emit "---" as value
                else:
                    self.rpm_signal_fan1.emit('<b><font color="red">---</font></b>')
                    self.rpm_signal_fan2.emit('<b><font color="red">---</font></b>')
                    self.rpm_signal_fan3.emit('<b><font color="red">---</font></b>')
                    self.rpm_signal_fan4.emit('<b><font color="red">---</font></b>')
                    self.rpm_signal_fan5.emit('<b><font color="red">---</font></b>')
                    self.rpm_signal_fan6.emit('<b><font color="red">---</font></b>')

                # Read voltage for all fans
                fans_voltage = grid.read_fan_voltage(self.ser, self.lock)

                # Check if there is fan voltages data available
                if fans_voltage:
                    # Emit voltage signals with current voltages
                    self.voltage_signal_fan1.emit(str(fans_voltage[0]))
                    self.voltage_signal_fan2.emit(str(fans_voltage[1]))
                    self.voltage_signal_fan3.emit(str(fans_voltage[2]))
                    self.voltage_signal_fan4.emit(str(fans_voltage[3]))
                    self.voltage_signal_fan5.emit(str(fans_voltage[4]))
                    self.voltage_signal_fan6.emit(str(fans_voltage[5]))

                # If no voltage data is available, emit "---" as value
                else:
                    self.voltage_signal_fan1.emit('<b><font color="red">---</font></b>')
                    self.voltage_signal_fan2.emit('<b><font color="red">---</font></b>')
                    self.voltage_signal_fan3.emit('<b><font color="red">---</font></b>')
                    self.voltage_signal_fan4.emit('<b><font color="red">---</font></b>')
                    self.voltage_signal_fan5.emit('<b><font color="red">---</font></b>')
                    self.voltage_signal_fan6.emit('<b><font color="red">---</font></b>')

                # Update status icons
                # Check if rpm and voltage data is available
                if fans_rpm and fans_voltage:
                    # Emit pixmap icon signal (red icon if fan rpm or voltage is 0, otherwise green icon)
                    self.pixmap_signal_fan1.emit(ICON_RED_LED if fans_rpm[0] == 0 or fans_voltage[0] == 0 else ICON_GREEN_LED)
                    self.pixmap_signal_fan2.emit(ICON_RED_LED if fans_rpm[1] == 0 or fans_voltage[1] == 0 else ICON_GREEN_LED)
                    self.pixmap_signal_fan3.emit(ICON_RED_LED if fans_rpm[2] == 0 or fans_voltage[2] == 0 else ICON_GREEN_LED)
                    self.pixmap_signal_fan4.emit(ICON_RED_LED if fans_rpm[3] == 0 or fans_voltage[3] == 0 else ICON_GREEN_LED)
                    self.pixmap_signal_fan5.emit(ICON_RED_LED if fans_rpm[4] == 0 or fans_voltage[4] == 0 else ICON_GREEN_LED)
                    self.pixmap_signal_fan6.emit(ICON_RED_LED if fans_rpm[5] == 0 or fans_voltage[5] == 0 else ICON_GREEN_LED)

                # If no fan rpm or voltage data is available, show the red status icon
                else:
                    self.pixmap_signal_fan1.emit(ICON_RED_LED)
                    self.pixmap_signal_fan2.emit(ICON_RED_LED)
                    self.pixmap_signal_fan3.emit(ICON_RED_LED)
                    self.pixmap_signal_fan4.emit(ICON_RED_LED)
                    self.pixmap_signal_fan5.emit(ICON_RED_LED)
                    self.pixmap_signal_fan6.emit(ICON_RED_LED)

                # Emit update signal
                self.update_signal.emit()

                #print("End of polling loop, sleeping for " + str(self.polling_interval) + " ms")

                # Sleep for the set polling interval (ms)
                time.sleep(self.polling_interval/1000)

        # Emits a signal if an exception occurs in the running thread
        # The main application will then show an error message about the problem
        # This is needed because a new message box widget cannot be created/displayed in the thread
        except Exception as e:
            # Stop the thread
            self.stop()
            print("Thread stopped at exception")

            # Get info about the exception
            (type, value, traceback) = sys.exc_info()

            # Generate a detailed error message
            msg = helper.exception_message_qthread(type, value, traceback)

            # Emit a signal with the error message to be displayed in a message box in the main UI
            self.exception_signal.emit(msg)