# ASubWindow.py -- Pamhyr
# Copyright (C) 2023-2026  INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

# -*- coding: utf-8 -*-

import os
import csv
import logging

from io import StringIO
from datetime import datetime

from tools import trace

from PyQt5.QtCore import Qt

from PyQt5.QtWidgets import (
    QMainWindow, QApplication, QDesktopWidget,
    QMdiArea, QMdiSubWindow, QDialog,
    QPushButton, QLineEdit, QCheckBox,
    QTimeEdit, QSpinBox, QTextEdit,
    QRadioButton, QComboBox, QFileDialog,
    QMessageBox, QTableView, QAction,
    QDateTimeEdit, QWidget, QPlainTextEdit,
    QLabel, QDoubleSpinBox, QButtonGroup,
)
from PyQt5.QtCore import (
    QTime, QDateTime,
)
from PyQt5.uic import loadUi

from Model.Except import ClipboardFormatError

logger = logging.getLogger()


class WindowToolKit(object):
    def __init__(self, parent=None):
        super(WindowToolKit, self).__init__()

    def copyTableIntoClipboard(self, table):
        stream = StringIO()
        csv.writer(stream, delimiter='\t').writerows(table)
        QApplication.clipboard().setText(stream.getvalue())

    def parseClipboardTable(self):
        clip = QApplication.clipboard()
        mime = clip.mimeData()
        if 'text/plain' not in mime.formats():
            raise ClipboardFormatError(mime='text/plain')

        data = mime.data('text/plain').data().decode()
        has_header = csv.Sniffer().has_header(data)

        header = []
        values = []

        delimiter = ' '
        if ';' in data:
            delimiter = ';'
        if '\t' in data:
            delimiter = '\t'

        stream = StringIO(data)
        rows = csv.reader(stream, delimiter=delimiter)
        for ind, row in enumerate(rows):
            if has_header and ind == 0:
                header = row.copy()
                continue

            values.append(row)

        return header, values

    def file_dialog(self, select_file="ExistingFile",
                    callback=lambda x: None,
                    directory=None,
                    default_suffix=None,
                    file_filter=None):
        """Open a new file dialog and send result to callback function

        Args:
            select_file: Select a file if True, else select a dir
            callback: The callback function with one arguments, files
                      selection list
            directory: Defaut directory
            default_suffix: Default file suffix
            file_filter: List of file filter
        Returns:
            The returns of callback
        """
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog

        dialog = QFileDialog(self, options=options)

        if select_file == "Existing_file":
            mode = QFileDialog.FileMode.ExistingFile
        elif select_file == "Directory":
            mode = QFileDialog.FileMode.Directory
        else:
            mode = QFileDialog.FileMode.AnyFile

        dialog.setFileMode(mode)
        if directory is not None:
            dialog.setDirectory(directory)

        if select_file != "Directory":
            if default_suffix is not None:
                dialog.setDefaultSuffix(default_suffix)

            if file_filter is not None:
                dialog.setNameFilters(file_filter)

        if dialog.exec_():
            file_names = dialog.selectedFiles()
            return callback(file_names)

    def message_box(self, text: str,
                    informative_text: str,
                    window_title: str = "Warning"):
        """Open a new message box

        Args:
            text: Short text string
            informative_text: Verbose text string
            window_title: Title of message box window

        Returns:
            Nothing
        """
        msg = QMessageBox(parent=self)

        msg.setIcon(QMessageBox.Warning)
        msg.setText(text)
        msg.setInformativeText(informative_text)
        msg.setWindowTitle(window_title)

        msg.findChild(QLabel, "qt_msgbox_label")\
           .setFixedWidth(384)

        msg.exec_()


class ASubWindowFeatures(object):
    def __init__(self, parent=None):
        super(ASubWindowFeatures, self).__init__()

    # Commun use features

    def _qtype_from_component_name(self, name):
        qtype = None

        if "action" in name:
            qtype = QAction
        elif "lineEdit" in name:
            qtype = QLineEdit
        elif "pushButton" in name:
            qtype = QPushButton
        elif "radioButton" in name:
            qtype = QRadioButton
        elif "tableView" in name:
            qtype = QTableView

        return qtype

    def get_label_text(self, name: str):
        """Get text of label component

        Args:
            label: The label component name

        Returns:
            Text
        """
        return self.find(QLabel, name).text()

    def set_label_text(self, name: str, text: str):
        """Set text of label component

        Args:
            text_edit: The label component name
            text: The text

        Returns:
            Nothing
        """
        self.find(QLabel, name).setText(text)

    def set_line_edit_text(self, name: str, text: str):
        """Set text of line edit component

        Args:
            line_edit: The line edit component name
            text: The text

        Returns:
            Nothing
        """
        try:
            self.find(QLineEdit, name).setText(text)
        except AttributeError as e:
            logger.error(e)

    def get_line_edit_text(self, name: str):
        """Get text of line edit component

        Args:
            line_edit: The line edit component name

        Returns:
            Text
        """
        return self.find(QLineEdit, name).text()

    def set_line_edit_enable(self, name: str, enable: bool):
        """Get enable status of line edit component

        Args:
            line_edit: The line edit component name
            enable: The boolean status

        Returns:
            Text
        """
        return self.find(QLineEdit, name).setEnabled(enable)

    def set_text_edit_text(self, name: str, text: str):
        """Set text of text edit component

        Args:
            text_edit: The text edit component name
            text: The text

        Returns:
            Nothing
        """
        self.find(QTextEdit, name).setMarkdown(text)

    def set_text_edit_enable(self, name: str, enable: bool):
        """Set enable status of text edit component

        Args:
            text_edit: The text edit component name
            enable: The boolean status

        Returns:
            Nothing
        """
        self.find(QTextEdit, name).setEnabled(enable)

    def get_text_edit_text(self, name: str):
        """Get text of text edit component

        Args:
            text_edit: The text edit component name

        Returns:
            Text
        """
        return self.find(QTextEdit, name).toMarkdown()

    def set_plaintext_edit_text(self, name: str, text: str):
        """Set text of text edit component

        Args:
            text_edit: The text edit component name
            text: The text

        Returns:
            Nothing
        """
        self.find(QPlainTextEdit, name).setPlainText(text)

    def set_plaintext_edit_enable(self, name: str, enable: bool):
        """Set enable status of text edit component

        Args:
            text_edit: The text edit component name
            enable: The boolean status

        Returns:
            Nothing
        """
        self.find(QPlainTextEdit, name).setEnabled(enable)

    def get_plaintext_edit_text(self, name: str):
        """Get text of text edit component

        Args:
            text_edit: The text edit component name

        Returns:
            Text
        """
        return self.find(QPlainTextEdit, name).toPlainText()

    def set_check_box(self, name: str, checked: bool):
        """Set status of checkbox component

        Args:
            name: The check box component name
            checked: Bool

        Returns:
            Nothing
        """
        self.find(QCheckBox, name).setChecked(checked)

    def set_check_box_enable(self, name: str, enable: bool):
        """Set status of checkbox component

        Args:
            name: The check box component name
            enable: Bool

        Returns:
            Nothing
        """
        self.find(QCheckBox, name).setEnabled(enable)

    def get_check_box(self, name: str):
        """Get status of checkbox component

        Args:
            name: The check box component name

        Returns:
            Status of checkbox (bool)
        """
        return self.find(QCheckBox, name).isChecked()

    def set_time_edit(self, name: str, time: str):
        """Set time of timeedit component

        Args:
            name: The timeedit component name
            time: The new time in format "HH:mm:ss"

        Returns:
            Nothing
        """
        qtime = QTime.fromString(time)
        self.find(QTimeEdit, name).setTime(qtime)

    def get_time_edit(self, name: str):
        """Get time of timeedit component

        Args:
            name: The timeedit component name

        Returns:
            The time of timeedit in format "HH:mm:ss"
        """
        return self.find(QTimeEdit, name).time().toString()

    def set_spin_box(self, name: str, value: int):
        """Set value of spinbox component

        Args:
            name: The spinbox component name
            value: The new value

        Returns:
            Nothing
        """
        self.find(QSpinBox, name).setValue(value)

    def get_spin_box(self, name: str):
        """Get time of spin box component

        Args:
            name: The spin box component

        Returns:
            The value of spin box
        """
        return self.find(QSpinBox, name).value()

    def set_double_spin_box(self, name: str, value: int):
        """Set value of spinbox component

        Args:
            name: The spinbox component name
            value: The new value

        Returns:
            Nothing
        """
        self.find(QDoubleSpinBox, name).setValue(value)

    def get_double_spin_box(self, name: str):
        """Get time of spin box component

        Args:
            name: The spin box component

        Returns:
            The value of spin box
        """
        return self.find(QDoubleSpinBox, name).value()

    def set_action_checkable(self, name: str, checked: bool):
        """Set value of action

        Args:
            name: The action component name
            value: The new value

        Returns:
            Nothing
        """
        self.find(QAction, name).setChecked(checked)

    def get_action_checkable(self, name: str):
        """Get status of action

        Args:
            name: The action component name

        Returns:
            The status of action
        """
        return self.find(QAction, name).isChecked()

    def set_push_button_checkable(self, name: str, checked: bool):
        """Set value of push button component

        Args:
            name: The push button component name
            value: The new value

        Returns:
            Nothing
        """
        self.find(QPushButton, name).setChecked(checked)

    def get_push_button_checkable(self, name: str):
        """Get status of push button

        Args:
            name: The push button component name

        Returns:
            The status of push button
        """
        return self.find(QPushButton, name).isChecked()

    def set_radio_button(self, name: str, checked: bool):
        """Set value of radio button component

        Args:
            name: The radio button component name
            checked: Checked

        Returns:
            Nothing
        """
        self.find(QRadioButton, name).setChecked(checked)

    def get_radio_button(self, name: str):
        """Get status of radio button

        Args:
            name: The radio button component name

        Returns:
            The status of radio button
        """
        return self.find(QRadioButton, name).isChecked()

    def combobox_add_item(self, name: str, item: str):
        """Add item in combo box

        Args:
            name: The combo box component name
            item: The item to add

        Returns:
            Nothing
        """
        self.find(QComboBox, name).addItem(item)

    def combobox_add_items(self, name: str, items: str):
        """Add item in combo box

        Args:
            name: The combo box component name
            item: The item to add

        Returns:
            Nothing
        """
        for item in items:
            self.find(QComboBox, name).addItem(item)

    def set_combobox_text(self, name: str, item: str):
        """Set current text of combo box

        Args:
            name: The combo box component name
            item: The item to add

        Returns:
            Nothing
        """
        self.find(QComboBox, name).setCurrentText(item)

    def get_combobox_text(self, name: str):
        """Get current text of combo box

        Args:
            name: The combo box component name

        Returns:
            Current text
        """
        return self.find(QComboBox, name).currentText()

    def get_datetime_edit(self, name: str):
        """Get datetime of datetime edit

        Args:
            name: The datetime edit component name

        Returns:
            The datetime
        """
        return self.find(QDateTimeEdit, name).dateTime().toPyDateTime()

    def set_datetime_edit(self, name: str, date: datetime):
        """Set datetime of a datetime edit

        Args:
            name: The datetime edit component name
            date: The new datetime

        Returns:
            Nothing
        """
        qdate = QDateTime.fromString(date.isoformat(), "yyyy-MM-ddThh:mm:ss")
        self.find(QDateTimeEdit, name).setDateTime(qdate)

    def get_checked_id_button_group(self, name: str):
        """Get current checked button id in a buttonGroup

        Args:
            name: The buttonGroup component name

        Returns:
            Current checked id
        """
        return self.find(QButtonGroup, name).checkedId()


# Top level interface

class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit):
    def __init__(self, name="", ui="dummy", parent=None, **kwargs):
        super(ASubMainWindow, self).__init__(parent=parent)
        if ui is not None:
            self.ui = loadUi(
                os.path.join(self._get_ui_directory(), f"{ui}.ui"),
                self
            )

        self.name = name
        self.parent = parent
        if self.parent is not None:
            self.parent.sub_win_add(name, self)

    def _get_ui_directory(self):
        return os.path.abspath(
            os.path.join(
                os.path.dirname(__file__),
                "..",  "ui"
            )
        )

    def closeEvent(self, event):
        if self.parent is not None:
            self.parent.sub_win_del(self.hash())

    def find(self, qtype, name):
        """Find an ui component

        Args:
            qtype: Type of QT component
            name: Name for component

        Returns:
            return the component
        """
        if qtype is None:
            qtype = self._qtype_from_component_name(name)

        return self.ui.findChild(qtype, name)


class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit):
    def __init__(self, name="", ui="dummy", parent=None, **kwargs):
        super(ASubWindow, self).__init__(parent=parent)
        self.ui = loadUi(
            os.path.abspath(
                os.path.join(
                    os.path.dirname(__file__),
                    "..",  "ui", f"{ui}.ui"
                )
            ),
            self
        )
        self.name = name
        self.parent = parent
        if self.parent is not None:
            self.parent.sub_win_add(name, self)

    def closeEvent(self, event):
        if self.parent is not None:
            self.parent.sub_win_del(self.hash())

    def find(self, qtype, name):
        """Find an ui component

        Args:
            qtype: Type of QT component
            name: Name for component

        Returns:
            return the component
        """
        if qtype is None:
            qtype = self._qtype_from_component_name(name)

        return self.ui.findChild(qtype, name)


class AWidget(QWidget, ASubWindowFeatures):
    def __init__(self, ui="", parent=None):
        super(AWidget, self).__init__(parent=parent)
        self.ui = loadUi(
            os.path.abspath(
                os.path.join(
                    os.path.dirname(__file__),
                    "..",  "ui", "Widgets", f"{ui}.ui"
                )
            ),
            self
        )
        self.parent = parent

    def find(self, qtype, name):
        """Find an ui component

        Args:
            qtype: Type of QT component
            name: Name for component

        Returns:
            return the component
        """
        if qtype is None:
            qtype = self._qtype_from_component_name(name)

        return self.ui.findChild(qtype, name)
