# -*- coding: utf-8 -*-
"""Revolution Pi PLC program configuration."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv2"

import gzip
import os
import tarfile
import zipfile
from logging import getLogger
from shutil import rmtree
from tempfile import mkdtemp
from xmlrpc.client import Binary

from PyQt5 import QtCore, QtGui, QtWidgets

from . import helper
from .ui.revpiprogram_ui import Ui_diag_program

log = getLogger(__name__)


class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
    """Program options of RevPiPyLoad."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.setFixedSize(self.size())

        self.dc = {}
        self.lst_files = []

        self.cbx_pictory.setEnabled(False)
        self.recover_gui()

    def _apply_acl(self):
        """Set availability of controls depending on ACL level."""

        # Setting properties require level 4
        self.cbb_plcprogram.setEnabled(helper.cm.xml_mode >= 4)
        self.txt_plcarguments.setEnabled(helper.cm.xml_mode >= 4)
        self.cbx_plcworkdir_set_uid.setEnabled(helper.cm.xml_mode >= 4)

        # Downloads require level 2
        self.btn_program_download.setEnabled(helper.cm.xml_mode >= 2)
        self.btn_pictory_download.setEnabled(helper.cm.xml_mode >= 2)
        self.btn_procimg_download.setEnabled(helper.cm.xml_mode >= 2)

        # Uploads require level 3
        self.btn_program_upload.setEnabled(helper.cm.xml_mode >= 3)
        self.btn_pictory_upload.setEnabled(helper.cm.xml_mode >= 3)

    def _changesdone(self):
        """
        Check for unsaved changes in dialog.

        :return: True, if unsaved changes was found
        """
        return self.cbb_plcprogram.currentText() != self.dc.get("plcprogram", "") or \
            self.txt_plcarguments.text() != self.dc.get("plcarguments", "") or \
            int(self.cbx_plcworkdir_set_uid.isChecked()) != self.dc.get("plcworkdir_set_uid", 0) or \
            self.sbx_plcprogram_watchdog.value() != self.dc.get("plcprogram_watchdog", 0)

    def _load_settings(self, files_only=False):
        """Load values to GUI widgets."""
        log.debug("RevPiProgram._load_settings")

        if files_only:
            mrk_program = self.cbb_plcprogram.currentText()
        else:
            mrk_program = ""

        self.cbb_plcprogram.clear()
        self.cbb_plcprogram.addItem("")

        self.lst_files.sort()
        is_in_list = False
        for file in self.lst_files:
            if file == ".placeholder":
                continue
            self.cbb_plcprogram.addItem(file)
            check = mrk_program or self.dc.get("plcprogram", "")
            if file == check:
                is_in_list = True

        if not is_in_list:
            log.warning("File {0} is not in list".format(mrk_program or self.dc.get("plcprogram", "")))

        if files_only:
            self.cbb_plcprogram.setCurrentText(mrk_program)
        else:
            self.cbb_plcprogram.setCurrentText(self.dc.get("plcprogram", ""))
            self.txt_plcarguments.setText(self.dc.get("plcarguments", ""))
            self.cbx_plcworkdir_set_uid.setChecked(bool(self.dc.get("plcworkdir_set_uid", 0)))
            self.sbx_plcprogram_watchdog.setValue(self.dc.get("plcprogram_watchdog", 0))

    def accept(self) -> None:
        # todo: After upload ask for restart pcl program?
        if not self._changesdone():
            super().accept()
            return

        if self.cbb_plcprogram.currentText() == "":
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "You have to select a start program, before uploading the "
                    "settings."
                )
            )
            return

        ask = QtWidgets.QMessageBox.question(
            self, self.tr("Question"), self.tr(
                "The settings will be set on the Revolution Pi now.\n\n"
                "If you made changes on the 'PCL Program' section, your plc "
                "program will restart now!"
            )
        ) == QtWidgets.QMessageBox.Yes

        if not ask:
            return

        self.dc["plcprogram"] = self.cbb_plcprogram.currentText()
        self.dc["plcarguments"] = self.txt_plcarguments.text()
        self.dc["plcworkdir_set_uid"] = int(self.cbx_plcworkdir_set_uid.isChecked())
        self.dc["plcprogram_watchdog"] = self.sbx_plcprogram_watchdog.value()

        saved = helper.cm.call_remote_function(
            "set_config", self.dc, ask,
            default_value=False
        )

        if saved:
            super().accept()
        else:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "The settings could not be saved on the Revolution Pi!\n"
                    "Try to save the values one mor time and check the log "
                    "files of RevPiPyLoad if the error rises again."
                )
            )

    def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
        if self._changesdone():
            ask = QtWidgets.QMessageBox.question(
                self, self.tr("Question"), self.tr(
                    "Do you really want to quit? \nUnsaved changes will be lost."
                )
            ) == QtWidgets.QMessageBox.Yes

            if ask:
                self.reject()
            else:
                a0.ignore()

    def exec(self) -> int:
        # Reset class values
        if not helper.cm.connected:
            return QtWidgets.QDialog.Rejected

        self.dc = helper.cm.call_remote_function("get_config", default_value={})
        self.lst_files = helper.cm.call_remote_function("get_filelist", default_value=None)
        if len(self.dc) == 0 or self.lst_files is None:
            return QtWidgets.QDialog.Rejected

        self._load_settings()
        self._apply_acl()

        return super().exec()

    def reject(self) -> None:
        """Reject all sub windows and reload settings."""
        self._load_settings()
        super().reject()

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    # region #      REGION: PLC program

    def _upload_pictory(self, filename: str):
        """Upload piCtory configuration."""
        fh = open(filename, "rb")
        file_binary = Binary(fh.read())
        fh.close()

        ask = QtWidgets.QMessageBox.question(
            self, self.tr("Reset driver..."), self.tr(
                "Reset piControl driver after successful uploading new piCtory "
                "configuration?\nThe process image will be interrupted for a "
                "short time!"
            ), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel
        )
        if ask == QtWidgets.QMessageBox.Cancel:
            return

        ec = helper.cm.call_remote_function(
            "set_pictoryrsc", file_binary, ask == QtWidgets.QMessageBox.Yes
        )

        if ec is None:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "Got an network error while send data to Revolution Pi.\n"
                    "Please try again."
                )
            )
        elif ec == 0:
            helper.cm.settings.last_pictory_file = filename
            if ask == QtWidgets.QMessageBox.Yes:
                QtWidgets.QMessageBox.information(
                    self, self.tr("Success"), self.tr(
                        "The transfer of the piCtory configuration "
                        "and the reset of piControl have been "
                        "successfully executed."
                    ),
                )
            else:
                QtWidgets.QMessageBox.information(
                    self, self.tr("Success"), self.tr(
                        "The piCtory configuration was successfully transferred."
                    )
                )

        elif ec == -1:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "Can not process the transferred file."
                )
            )
        elif ec == -2:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "Can not find main elements in piCtory file."
                )
            )
        elif ec == -4:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "Contained devices could not be found on Revolution "
                    "Pi. The configuration may be from a newer piCtory version!"
                )
            )
        elif ec == -5:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "Could not load RAP catalog on Revolution Pi."
                )
            )
        elif ec < 0:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "The piCtory configuration could not be "
                    "written on the Revolution Pi."
                )
            )
        elif ec > 0:
            QtWidgets.QMessageBox.warning(
                self, self.tr("Warning"), self.tr(
                    "The piCtroy configuration has been saved successfully.\n"
                    "An error occurred on piControl reset!"
                )
            )

    def check_replacedir(self, rootdir: str):
        """
        Return root dir of a extracted archive an piCtory configuration.

        This function checks the root dir of a extracted directory. It
        will check the existence of a piCtory configuration will will
        return that information in the return tuple.

        :param rootdir: Dir to check
        :return: Tuple with corrected root dir and piCtory configuration
        """
        lst_dir = os.listdir(rootdir)
        if len(lst_dir) == 1 and \
                os.path.isdir(os.path.join(rootdir, lst_dir[0])):
            return os.path.join(rootdir, lst_dir[0]), None

        if len(lst_dir) == 2:
            rscfile = None
            for fname in lst_dir:
                if fname.find(".rsc"):
                    rscfile = os.path.join(rootdir, fname)
            return os.path.join(rootdir, lst_dir[0]), rscfile

        else:
            return rootdir, None

    def create_filelist(self, rootdir):
        """Create a file list of a directory.

        :param rootdir: Root dir to crate file list
        :return: File list with <class 'str'>
        """
        filelist = []
        for tup_dir in os.walk(rootdir):
            for fname in tup_dir[2]:
                filelist.append(os.path.join(tup_dir[0], fname))
        return filelist

    def recover_gui(self):
        """Load saved GUI states."""
        self.cbb_format.setCurrentIndex(helper.settings.value("program/cbb_format_index", 0, int))
        self.cbx_pictory.setChecked(helper.settings.value("program/cbx_pictory_checked", False, bool))
        self.cbx_clear.setChecked(helper.settings.value("program/cbx_clear_checked", False, bool))

    @QtCore.pyqtSlot(int)
    def on_cbb_format_currentIndexChanged(self, index: int):
        helper.settings.setValue("program/cbb_format_index", index)
        self.cbx_pictory.setEnabled(index >= 1)

    @QtCore.pyqtSlot()
    def on_btn_program_download_clicked(self):
        """Download plc program from Revolution Pi."""
        if not helper.cm.connected:
            return

        if self.cbb_format.currentIndex() == 0:
            # Save files as zip archive
            diag_save = QtWidgets.QFileDialog(
                self, self.tr("Save ZIP archive..."),
                helper.cm.settings.last_zip_file or "{0}.zip".format(helper.cm.settings.name),
                self.tr("ZIP archive (*.zip);;All files (*.*)")
            )
            diag_save.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
            diag_save.setDefaultSuffix("zip")
            self.rejected.connect(diag_save.reject)

            if diag_save.exec() != QtWidgets.QFileDialog.AcceptSave or len(diag_save.selectedFiles()) != 1:
                return
            filename = diag_save.selectedFiles()[0]
            fh = open(filename, "wb")

            helper.cm.settings.last_zip_file = filename

        elif self.cbb_format.currentIndex() == 1:
            # Save files as TarGz archive
            diag_save = QtWidgets.QFileDialog(
                self, self.tr("Save TGZ archive..."),
                helper.cm.settings.last_tar_file or "{0}.tgz".format(helper.cm.settings.name),
                self.tr("TGZ archive (*.tgz);;All files (*.*)")
            )
            diag_save.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
            diag_save.setDefaultSuffix("tgz")
            self.rejected.connect(diag_save.reject)

            if diag_save.exec() != QtWidgets.QFileDialog.AcceptSave or len(diag_save.selectedFiles()) != 1:
                return
            filename = diag_save.selectedFiles()[0]
            fh = open(filename, "wb")

            helper.cm.settings.last_tar_file = filename

        else:
            # Other indexes are not allowed for download
            return

        plcfile = helper.cm.call_remote_function(
            "plcdownload",
            "zip" if self.cbb_format.currentIndex() == 0 else "tar",
            self.cbx_pictory.isChecked()
        )

        if plcfile is None:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "Could not load PLC program from Revolution Pi."
                )
            )

        else:
            try:
                fh.write(plcfile.data)
                fh.close()

            except Exception as e:
                log.error(e)
                QtWidgets.QMessageBox.critical(
                    self, self.tr("Error"), self.tr(
                        "Coud not save the archive or extract the files!\n"
                        "Please retry.")
                )
            else:
                QtWidgets.QMessageBox.information(
                    self, self.tr("Success"), self.tr(
                        "Transfer successfully completed."
                    )
                )

    @QtCore.pyqtSlot()
    def on_btn_program_upload_clicked(self):
        """Upload plc program to Revolution Pi."""
        if not helper.cm.connected:
            return

        dirtmp = None

        def remove_temp():
            # Remove temp dir
            if dirtmp is not None:
                rmtree(dirtmp)

        dirselect = ""
        lst_files = []
        folder_name = ""
        rscfile = None

        if self.cbb_format.currentIndex() == 0:
            # Upload zip archive content
            diag_open = QtWidgets.QFileDialog(
                self, self.tr("Upload content of ZIP archive..."),
                helper.cm.settings.last_file_upload,
                self.tr("ZIP archive (*.zip);;All files (*.*)")
            )
            diag_open.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen)
            diag_open.setFileMode(QtWidgets.QFileDialog.ExistingFile)
            diag_open.setDefaultSuffix("zip")
            self.rejected.connect(diag_open.reject)

            if diag_open.exec() != QtWidgets.QFileDialog.AcceptSave or len(diag_open.selectedFiles()) != 1:
                return

            filename = diag_open.selectedFiles()[0]
            helper.cm.settings.last_file_upload = filename
            if zipfile.is_zipfile(filename):
                dirtmp = mkdtemp()
                fhz = zipfile.ZipFile(filename)
                fhz.extractall(dirtmp)
                fhz.close()

                lst_files = self.create_filelist(dirtmp)
                dirselect, rscfile = self.check_replacedir(dirtmp)

            else:
                QtWidgets.QMessageBox.critical(
                    self, self.tr("Error"), self.tr(
                        "The selected file ist not a ZIP archive."
                    )
                )
                return

        elif self.cbb_format.currentIndex() == 1:
            # Upload TarGz content
            diag_open = QtWidgets.QFileDialog(
                self, self.tr("Upload content of TAR archive..."),
                helper.cm.settings.last_file_upload,
                self.tr("TAR archive (*.tgz);;All files (*.*)")
            )
            diag_open.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen)
            diag_open.setFileMode(QtWidgets.QFileDialog.ExistingFile)
            diag_open.setDefaultSuffix("tgz")
            self.rejected.connect(diag_open.reject)

            if diag_open.exec() != QtWidgets.QFileDialog.AcceptSave or len(diag_open.selectedFiles()) != 1:
                return

            filename = diag_open.selectedFiles()[0]
            helper.cm.settings.last_file_upload = filename
            if tarfile.is_tarfile(filename):
                dirtmp = mkdtemp()
                fht = tarfile.open(filename)
                fht.extractall(dirtmp)
                fht.close()

                lst_files = self.create_filelist(dirtmp)
                dirselect, rscfile = self.check_replacedir(dirtmp)

            else:
                QtWidgets.QMessageBox.critical(
                    self, self.tr("Error"), self.tr(
                        "The selected file ist not a TAR archive."
                    )
                )
                return

        # No files selected
        if len(lst_files) == 0:
            QtWidgets.QMessageBox.warning(
                self, self.tr("No files to upload..."), self.tr(
                    "Found no files to upload in given location or archive."
                )
            )
            remove_temp()
            return

        # Clean up directory before upload
        clean_revpi = helper.cm.call_remote_function("plcuploadclean", default_value=False)
        if self.cbx_clear.isChecked() and not clean_revpi:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "There was an error deleting the files on the Revolution Pi.\n"
                    "Upload aborted! Please try again."
                )
            )
            remove_temp()
            return

        plc_program_not_in_upload = True
        ec = 0

        for file_name in lst_files:

            if file_name == rscfile:
                # Do not send piCtory configuration
                continue

            # fixme: Fehlerabfang bei Dateilesen
            with open(file_name, "rb") as fh:

                # Generate relative file name for transfer
                if dirselect == "":
                    sendname = os.path.basename(file_name)
                else:
                    # Append folder name on complete folder transfer to crate it on Revolution Pi
                    sendname = os.path.join(
                        folder_name,
                        file_name.replace(dirselect, "")[1:]
                    )

                # Try to find given plc start program in upload files
                if sendname == self.dc.get("plcprogram", ""):
                    plc_program_not_in_upload = False

                ustatus = helper.cm.call_remote_function(
                    "plcupload", Binary(gzip.compress(fh.read())), sendname
                )
                if ustatus is None:
                    ec = -2
                    break
                elif not ustatus:
                    ec = -1
                    break

        if ec == 0:
            QtWidgets.QMessageBox.information(
                self, self.tr("Success"), self.tr(
                    "The PLC program was transferred successfully."
                )
            )
            self.lst_files = helper.cm.call_remote_function("get_filelist", default_value=[])
            self._load_settings(files_only=True)
            if plc_program_not_in_upload:
                QtWidgets.QMessageBox.warning(
                    self, self.tr("Information"), self.tr(
                        "Could not find the selected PLC start program in "
                        "uploaded files.\nThis is not an error, if the file "
                        "was already on the Revolution Pi. Check PLC start "
                        "program field"
                    )
                )

            if self.cbx_pictory.isChecked():
                if rscfile is not None:
                    self._upload_pictory(rscfile)
                else:
                    QtWidgets.QMessageBox.critical(
                        self, self.tr("Error"), self.tr(
                            "There is no piCtory configuration in this archive."
                        )
                    )

        elif ec == -1:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "The Revolution Pi could not process some parts of the transmission."
                )
            )

        elif ec == -2:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "Errors occurred during transmission."
                )
            )

        remove_temp()

    # endregion # # # # #

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    # region #      REGION: Control files

    @QtCore.pyqtSlot()
    def on_btn_pictory_download_clicked(self):
        """Download piCtory configuration."""
        if not helper.cm.connected:
            return

        diag_save = QtWidgets.QFileDialog(
            self, self.tr("Save piCtory file..."),
            os.path.join(
                helper.cm.settings.last_dir_pictory,
                "{0}.rsc".format(helper.cm.settings.name)
            ),
            self.tr("piCtory file (*.rsc);;All files (*.*)")
        )
        diag_save.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
        diag_save.setDefaultSuffix("rsc")
        self.rejected.connect(diag_save.reject)

        if diag_save.exec() != QtWidgets.QFileDialog.AcceptSave or len(diag_save.selectedFiles()) != 1:
            return

        filename = diag_save.selectedFiles()[0]
        helper.cm.settings.last_dir_pictory = os.path.dirname(filename)
        bin_buffer = helper.cm.call_remote_function("get_pictoryrsc")  # type: Binary
        if bin_buffer is None:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "Could not load piCtory file from Revolution Pi."
                )
            )
        else:
            fh = open(filename, "wb")
            fh.write(bin_buffer.data)
            fh.close()

            QtWidgets.QMessageBox.information(
                self, self.tr("Success"), self.tr(
                    "piCtory configuration successfully loaded and saved to:\n{0}."
                ).format(filename)
            )

    @QtCore.pyqtSlot()
    def on_btn_pictory_upload_clicked(self):
        if not helper.cm.connected:
            return

        diag_open = QtWidgets.QFileDialog(
            self, self.tr("Upload piCtory file..."),
            helper.cm.settings.last_pictory_file or "{0}.rsc".format(helper.cm.settings.name),
            self.tr("piCtory file (*.rsc);;All files (*.*)")
        )
        diag_open.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen)
        diag_open.setFileMode(QtWidgets.QFileDialog.ExistingFile)
        diag_open.setDefaultSuffix("rsc")
        self.rejected.connect(diag_open.reject)

        if diag_open.exec() != QtWidgets.QFileDialog.AcceptSave or len(diag_open.selectedFiles()) != 1:
            return

        self._upload_pictory(diag_open.selectedFiles()[0])

    @QtCore.pyqtSlot()
    def on_btn_procimg_download_clicked(self):
        """Download process image."""
        if not helper.cm.connected:
            return

        diag_save = QtWidgets.QFileDialog(
            self,
            self.tr("Save piControl file..."),
            os.path.join(
                helper.cm.settings.last_dir_picontrol,
                "{0}.img".format(helper.cm.settings.name)
            ),
            self.tr("Process image file (*.img);;All files (*.*)")
        )
        diag_save.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
        diag_save.setDefaultSuffix("img")
        self.rejected.connect(diag_save.reject)

        if diag_save.exec() != QtWidgets.QFileDialog.AcceptSave or len(diag_save.selectedFiles()) != 1:
            return

        filename = diag_save.selectedFiles()[0]
        helper.cm.settings.last_dir_picontrol = os.path.dirname(filename)
        bin_buffer = helper.cm.call_remote_function("get_procimg")  # type: Binary

        if bin_buffer is None:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Error"), self.tr(
                    "Could not load process image from Revolution Pi."
                )
            )
        else:
            fh = open(filename, "wb")
            fh.write(bin_buffer.data)
            fh.close()

            QtWidgets.QMessageBox.information(
                self, self.tr("Success"), self.tr(
                    "Process image successfully loaded and saved to:\n{0}."
                ).format(filename)
            )

    # endregion # # # # #
