#!/usr/bin/env python3
"""
@file           tmtc_frontend.py
@date           01.11.2019
@brief          This is part of the TMTC client developed by the SOURCE project by KSat
@description    GUI is still work-in-progress
@manual
@author         R. Mueller, P. Scheurenbrand, D. Nguyen
"""
import enum
import os
import sys
import time
import webbrowser
from multiprocessing import Process
from typing import Union

from PyQt5.QtWidgets import (
    QMainWindow,
    QGridLayout,
    QTableWidget,
    QWidget,
    QLabel,
    QCheckBox,
    QDoubleSpinBox,
    QFrame,
    QComboBox,
    QPushButton,
    QTableWidgetItem,
    QMenu,
    QAction,
    QMenuBar,
)
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtCore import Qt, pyqtSignal, QObject, QThread, QRunnable

from tmtccmd.core.frontend_base import FrontendBase
from tmtccmd.core.backend import TmTcHandler
from tmtccmd.config.hook import TmTcHookBase
from tmtccmd.config.definitions import CoreGlobalIds, CoreModeList, CoreComInterfaces
from tmtccmd.config.hook import get_global_hook_obj
from tmtccmd.utility.logger import get_console_logger
from tmtccmd.core.globals_manager import get_global, update_global
from tmtccmd.com_if.tcpip_utilities import TcpIpConfigIds
import tmtccmd.config as config_module


LOGGER = get_console_logger()


CONNECT_BTTN_STYLE = (
    "background-color: #1fc600;"
    "border-style: inset;"
    "font: bold;"
    "padding: 6px;"
    "border-width: 2px;"
    "border-radius: 6px;"
)


DISCONNECT_BTTN_STYLE = (
    "background-color: orange;"
    "border-style: inset;"
    "font: bold;"
    "padding: 6px;"
    "border-width: 2px;"
    "border-radius: 6px;"
)


COMMAND_BUTTON_STYLE = (
    "background-color: #cdeefd;"
    "border-style: inset;"
    "font: bold;"
    "padding: 6px;"
    "border-width: 2px;"
    "border-radius: 6px;"
)


class WorkerOperationsCodes(enum.IntEnum):
    DISCONNECT = 0
    SEQUENTIAL_COMMANDING = 1


class WorkerThread(QObject):
    finished = pyqtSignal()

    def __init__(self, op_code: WorkerOperationsCodes, tmtc_handler: TmTcHandler):
        super(QObject, self).__init__()
        self.op_code = op_code
        self.tmtc_handler = tmtc_handler

    def run_worker(self):
        if self.op_code == WorkerOperationsCodes.DISCONNECT:
            self.tmtc_handler.close_listener()
            while True:
                if not self.tmtc_handler.is_com_if_active():
                    break
                else:
                    time.sleep(0.4)
            self.finished.emit()
        elif self.op_code == WorkerOperationsCodes.SEQUENTIAL_COMMANDING:
            self.tmtc_handler.set_mode(CoreModeList.SEQUENTIAL_CMD_MODE)
            # It is expected that the TMTC handler is in the according state to perform the operation
            self.tmtc_handler.perform_operation()
            self.finished.emit()
        else:
            LOGGER.warning("Unknown worker operation code!")
            self.finished.emit()


class RunnableThread(QRunnable):
    """
    Runnable thread which can be used with QThreadPool. Not used for now, might be needed in the future.
    """

    def run(self):
        pass


class TmTcFrontend(QMainWindow, FrontendBase):
    def __init__(
        self, hook_obj: TmTcHookBase, tmtc_backend: TmTcHandler, app_name: str
    ):
        super(TmTcFrontend, self).__init__()
        super(QMainWindow, self).__init__()
        self._tmtc_handler = tmtc_backend
        self._app_name = app_name
        self._hook_obj = hook_obj

        self._tmtc_handler.initialize()
        self.service_op_code_dict = dict()
        self._service_list = []
        self._op_code_list = []
        self._com_if_list = []
        self._last_com_if = CoreComInterfaces.UNSPECIFIED.value
        self._current_com_if = CoreComInterfaces.UNSPECIFIED.value
        self._current_service = ""
        self._current_op_code = ""
        self._current_com_if_key = "unspec"
        self.__connected = False

        self.__worker = None
        self.__thread = None
        self.__debug_mode = False

        self.__combo_box_op_codes: Union[None, QComboBox] = None
        module_path = os.path.abspath(config_module.__file__).replace("__init__.py", "")
        self.logo_path = f"{module_path}/logo.png"

    def prepare_start(self, args: any) -> Process:
        return Process(target=self.start)

    def start(self, qt_app: any):
        self.__start_ui()
        sys.exit(qt_app.exec())

    def set_gui_logo(self, logo_total_path: str):
        if os.path.isfile(logo_total_path):
            self.logo_path = logo_total_path
        else:
            LOGGER.warning("Could not set logo, path invalid!")

    def __start_ui(self):
        self.__create_menu_bar()
        win = QWidget(self)
        self.setCentralWidget(win)

        grid = QGridLayout()
        win.setLayout(grid)
        row = 0
        self.setWindowTitle(self._app_name)
        self.setWindowIcon(QIcon(self.logo_path))

        add_pixmap = False

        if add_pixmap:
            row = self.__set_up_pixmap(grid=grid, row=row)

        row = self.__set_up_config_section(grid=grid, row=row)
        row = self.__add_vertical_separator(grid=grid, row=row)

        # com if configuration
        row = self.__set_up_com_if_section(grid=grid, row=row)
        row = self.__add_vertical_separator(grid=grid, row=row)

        row = self.__set_up_service_op_code_section(grid=grid, row=row)

        self.__command_button = QPushButton()
        self.__command_button.setText("Send Command")
        self.__command_button.setStyleSheet(COMMAND_BUTTON_STYLE)
        self.__command_button.clicked.connect(self.__start_seq_cmd_op)
        self.__command_button.setEnabled(False)
        grid.addWidget(self.__command_button, row, 0, 1, 2)
        row += 1
        self.show()

    def __start_seq_cmd_op(self):
        if self.__debug_mode:
            LOGGER.info("Start Service Test Button pressed.")
        if not self.__get_send_button():
            return
        self.__set_send_button(False)
        self._tmtc_handler.set_service(self._current_service)
        self._tmtc_handler.set_opcode(self._current_op_code)
        self.__start_qthread_task(
            op_code=WorkerOperationsCodes.SEQUENTIAL_COMMANDING,
            finish_callback=self.__finish_seq_cmd_op,
        )

    def __finish_seq_cmd_op(self):
        self.__set_send_button(True)

    def __connect_button_action(self):
        if not self.__connected:
            LOGGER.info("Starting TM listener..")
            # Build and assign new communication interface
            if self._current_com_if != self._last_com_if:
                hook_obj = get_global_hook_obj()
                new_com_if = hook_obj.assign_communication_interface(
                    com_if_key=self._current_com_if,
                    tmtc_printer=self._tmtc_handler.get_printer(),
                )
                self._tmtc_handler.set_com_if(new_com_if)
            self._tmtc_handler.start_listener(False)
            self.__connect_button.setStyleSheet(DISCONNECT_BTTN_STYLE)
            self.__command_button.setEnabled(True)
            self.__connect_button.setText("Disconnect")
            self.__connected = True
        else:
            LOGGER.info("Closing TM listener..")
            self.__command_button.setEnabled(False)
            self.__connect_button.setEnabled(False)
            self.__start_qthread_task(
                op_code=WorkerOperationsCodes.DISCONNECT,
                finish_callback=self.__finish_disconnect_button_op,
            )

    def __finish_disconnect_button_op(self):
        self.__connect_button.setEnabled(True)
        # self.__disconnect_button.setEnabled(False)
        self.__connect_button.setStyleSheet(CONNECT_BTTN_STYLE)
        self.__connect_button.setText("Connect")
        LOGGER.info("Disconnect successfull")
        self.__connected = False

    def __create_menu_bar(self):
        menu_bar = self.menuBar()
        # Creating menus using a QMenu object
        file_menu = QMenu("&File", self)
        menu_bar.addMenu(file_menu)
        # Creating menus using a title
        help_menu = menu_bar.addMenu("&Help")

        help_action = QAction("Help", self)
        help_action.triggered.connect(self.__help_url)
        help_menu.addAction(help_action)

    @staticmethod
    def __help_url():
        webbrowser.open("https://tmtccmd.readthedocs.io/en/latest/")

    def __set_up_config_section(self, grid: QGridLayout, row: int) -> int:
        grid.addWidget(QLabel("Configuration:"), row, 0, 1, 2)
        row += 1
        checkbox_console = QCheckBox("Print output to console")
        checkbox_console.stateChanged.connect(self.__checkbox_console_update)

        checkbox_log = QCheckBox("Print output to log file")
        checkbox_log.stateChanged.connect(self.__checkbox_log_update)

        checkbox_raw_tm = QCheckBox("Print all raw TM data directly")
        checkbox_raw_tm.stateChanged.connect(self.__checkbox_print_raw_data_update)

        checkbox_hk = QCheckBox("Print Housekeeping Data")
        # checkbox_hk.setChecked(tmtcc_config.G_PRINT_HK_DATA)
        checkbox_hk.stateChanged.connect(checkbox_print_hk_data)

        checkbox_short = QCheckBox("Short Display Mode")
        # checkbox_short.setChecked(tmtcc_config.G_DISPLAY_MODE == "short")
        checkbox_short.stateChanged.connect(checkbox_short_display_mode)

        grid.addWidget(checkbox_log, row, 0, 1, 1)
        grid.addWidget(checkbox_console, row, 1, 1, 1)
        row += 1
        grid.addWidget(checkbox_raw_tm, row, 0, 1, 1)
        grid.addWidget(checkbox_hk, row, 1, 1, 1)
        row += 1
        grid.addWidget(checkbox_short, row, 0, 1, 1)
        row += 1

        grid.addWidget(QLabel("TM Timeout:"), row, 0, 1, 1)
        grid.addWidget(QLabel("TM Timeout Factor:"), row, 1, 1, 1)
        row += 1

        spin_timeout = QDoubleSpinBox()
        spin_timeout.setValue(4)
        # TODO: set sensible min/max values
        spin_timeout.setSingleStep(0.1)
        spin_timeout.setMinimum(0.25)
        spin_timeout.setMaximum(60)
        # https://youtrack.jetbrains.com/issue/PY-22908
        # Ignore those warnings for now.
        spin_timeout.valueChanged.connect(number_timeout)
        grid.addWidget(spin_timeout, row, 0, 1, 1)

        spin_timeout_factor = QDoubleSpinBox()
        # spin_timeout_factor.setValue(tmtcc_config.G_TC_SEND_TIMEOUT_FACTOR)
        # TODO: set sensible min/max values
        spin_timeout_factor.setSingleStep(0.1)
        spin_timeout_factor.setMinimum(0.25)
        spin_timeout_factor.setMaximum(10)
        spin_timeout_factor.valueChanged.connect(number_timeout_factor)
        grid.addWidget(spin_timeout_factor, row, 1, 1, 1)
        row += 1
        return row

    def __set_up_com_if_section(self, grid: QGridLayout, row: int) -> int:
        grid.addWidget(QLabel("Communication Interface:"), row, 0, 1, 1)
        com_if_combo_box = QComboBox()
        all_com_ifs = get_global(CoreGlobalIds.COM_IF_DICT)
        index = 0
        # add all possible ComIFs to the comboBox
        for com_if_key, com_if_value in all_com_ifs.items():
            com_if_combo_box.addItem(com_if_value)
            self._com_if_list.append((com_if_key, com_if_value))
            if self._tmtc_handler.get_com_if_id() == com_if_key:
                com_if_combo_box.setCurrentIndex(index)
            index += 1
        com_if_combo_box.currentIndexChanged.connect(self.__com_if_index_changed)
        grid.addWidget(com_if_combo_box, row, 1, 1, 1)
        row += 1

        self.com_if_cfg_button = QPushButton()
        self.com_if_cfg_button.setText("Configure")
        grid.addWidget(self.com_if_cfg_button, row, 0, 1, 2)
        row += 1

        self.__connect_button = QPushButton()
        self.__connect_button.setText("Connect")
        self.__connect_button.setStyleSheet(CONNECT_BTTN_STYLE)
        self.__connect_button.clicked.connect(self.__connect_button_action)

        grid.addWidget(self.__connect_button, row, 0, 1, 2)
        row += 1
        return row

    def __set_up_service_op_code_section(self, grid: QGridLayout, row: int):
        grid.addWidget(QLabel("Service: "), row, 0, 1, 2)
        grid.addWidget(QLabel("Operation Code: "), row, 1, 1, 2)
        row += 1

        combo_box_services = QComboBox()
        default_service = get_global(CoreGlobalIds.CURRENT_SERVICE)
        self.service_op_code_dict = self._hook_obj.get_service_op_code_dictionary()
        if self.service_op_code_dict is None:
            LOGGER.warning("Invalid service to operation code dictionary")
            LOGGER.warning("Setting default dictionary")
            from tmtccmd.config.globals import get_default_service_op_code_dict

            self.service_op_code_dict = get_default_service_op_code_dict()
        index = 0
        default_index = 0
        for service_key, service_value in self.service_op_code_dict.items():
            combo_box_services.addItem(service_value[0])
            if service_key == default_service:
                default_index = index
            self._service_list.append(service_key)
            index += 1
        combo_box_services.setCurrentIndex(default_index)
        self._current_service = self._service_list[default_index]

        combo_box_services.currentIndexChanged.connect(self.__service_index_changed)
        grid.addWidget(combo_box_services, row, 0, 1, 1)

        self.__combo_box_op_codes = QComboBox()
        self._current_service = self._service_list[default_index]
        self.__update_op_code_combo_box()
        self.__combo_box_op_codes.currentIndexChanged.connect(
            self.__op_code_index_changed
        )
        # TODO: Combo box also needs to be updated if another service is selected
        grid.addWidget(self.__combo_box_op_codes, row, 1, 1, 1)
        row += 1
        return row

    def __set_up_pixmap(self, grid: QGridLayout, row: int) -> int:
        label = QLabel(self)
        label.setGeometry(720, 10, 100, 100)
        label.adjustSize()

        pixmap = QPixmap(self.logo_path)
        pixmap_width = pixmap.width()
        pixmap_height = pixmap.height()
        row += 1

        pixmap_scaled = pixmap.scaled(
            pixmap_width * 0.3, pixmap_height * 0.3, Qt.KeepAspectRatio
        )
        label.setPixmap(pixmap_scaled)
        label.setScaledContents(True)

        grid.addWidget(label, row, 0, 1, 2)
        row += 1
        return row

    def __start_qthread_task(self, op_code: WorkerOperationsCodes, finish_callback):
        self.__thread = QThread()
        self.__worker = WorkerThread(op_code=op_code, tmtc_handler=self._tmtc_handler)
        self.__worker.moveToThread(self.__thread)

        self.__thread.started.connect(self.__worker.run_worker)
        self.__worker.finished.connect(self.__thread.quit)
        self.__worker.finished.connect(self.__worker.deleteLater)
        self.__thread.finished.connect(self.__thread.deleteLater)

        self.__thread.finished.connect(finish_callback)
        self.__thread.start()

    @staticmethod
    def __add_vertical_separator(grid: QGridLayout, row: int):
        separator = QFrame()
        separator.setFrameShape(QFrame.HLine)
        grid.addWidget(separator, row, 0, 1, 2)
        row += 1
        return row

    def __service_index_changed(self, index: int):
        self._current_service = self._service_list[index]
        self.__update_op_code_combo_box()
        if self.__debug_mode:
            LOGGER.info("Service changed")

    def __op_code_index_changed(self, index: int):
        self._current_op_code = self._op_code_list[index]
        if self.__debug_mode:
            LOGGER.info("Op Code changed")

    def __update_op_code_combo_box(self):
        self.__combo_box_op_codes.clear()
        self._op_code_list = []
        op_code_dict = self.service_op_code_dict[self._current_service][1]
        if op_code_dict is not None:
            for op_code_key, op_code_value in op_code_dict.items():
                try:
                    self._op_code_list.append(op_code_key)
                    self.__combo_box_op_codes.addItem(op_code_value[0])
                except TypeError:
                    LOGGER.warning(f"Invalid op code entry {op_code_value}, skipping..")
            self._current_op_code = self._op_code_list[0]

    def __checkbox_log_update(self, state: int):
        update_global(CoreGlobalIds.PRINT_TO_FILE, state)
        if self.__debug_mode:
            LOGGER.info(["Enabled", "Disabled"][state == 0] + " print to log.")

    def __checkbox_console_update(self, state: bool):
        update_global(CoreGlobalIds.PRINT_TM, state)
        if self.__debug_mode:
            LOGGER.info(["enabled", "disabled"][state == 0] + " console print")

    def __checkbox_print_raw_data_update(self, state: int):
        update_global(CoreGlobalIds.PRINT_RAW_TM, state)
        if self.__debug_mode:
            LOGGER.info(["enabled", "disabled"][state == 0] + " printing of raw data")

    def __set_send_button(self, state: bool):
        self.__command_button.setEnabled(state)

    def __get_send_button(self):
        return self.__command_button.isEnabled()

    def __com_if_index_changed(self, index: int):
        self._current_com_if = self._com_if_list[index][0]
        if self.__debug_mode:
            LOGGER.info(f"Communication IF updated: {self._com_if_list[index][1]}")


class SingleCommandTable(QTableWidget):
    def __init__(self):
        super().__init__()
        self.setRowCount(1)
        self.setColumnCount(5)
        self.setHorizontalHeaderItem(0, QTableWidgetItem("Service"))
        self.setHorizontalHeaderItem(1, QTableWidgetItem("Subservice"))
        self.setHorizontalHeaderItem(2, QTableWidgetItem("SSC"))
        self.setHorizontalHeaderItem(3, QTableWidgetItem("Data"))
        self.setHorizontalHeaderItem(4, QTableWidgetItem("CRC"))
        self.setItem(0, 0, QTableWidgetItem("17"))
        self.setItem(0, 1, QTableWidgetItem("1"))
        self.setItem(0, 2, QTableWidgetItem("20"))


def checkbox_print_hk_data(state: int):
    update_global(CoreGlobalIds.PRINT_HK, state)
    LOGGER.info(["enabled", "disabled"][state == 0] + " printing of hk data")


def checkbox_short_display_mode(state: int):
    update_global(CoreGlobalIds.DISPLAY_MODE, state)
    LOGGER.info(["enabled", "disabled"][state == 0] + " short display mode")


def number_timeout(value: float):
    update_global(CoreGlobalIds.TM_TIMEOUT, value)
    LOGGER.info("PUS TM timeout changed to: " + str(value))


def number_timeout_factor(value: float):
    update_global(CoreGlobalIds.TC_SEND_TIMEOUT_FACTOR, value)
    LOGGER.info("PUS TM timeout factor changed to: " + str(value))


def ip_change_client(value):
    ethernet_config = get_global(CoreGlobalIds.ETHERNET_CONFIG)
    ethernet_config[TcpIpConfigIds.RECV_ADDRESS] = value
    update_global(CoreGlobalIds.ETHERNET_CONFIG, ethernet_config)
    LOGGER.info("Client IP changed: " + value)


def ip_change_board(value):
    ethernet_config = get_global(CoreGlobalIds.ETHERNET_CONFIG)
    ethernet_config[TcpIpConfigIds.SEND_ADDRESS] = value
    update_global(CoreGlobalIds.ETHERNET_CONFIG, ethernet_config)
    LOGGER.info("Board IP changed: " + value)
