# Copyright (c) 2016 Electric Power Research Institute, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# 3. Neither the name of the EPRI nor the names of its contributors may be used
#    to endorse or promote products derived from this software without specific
#    prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import importlib
import logging
import os
import subprocess
import sys
import tempfile
import threading
from typing import Callable, Concatenate, Optional, ParamSpec

from PySide6 import QtCore, QtGui, QtWidgets

import asphodel
from hyperborea.dark_mode import set_style
import hyperborea.download
from hyperborea.preferences import read_bool_setting

from .ui.ui_main import Ui_MondoMainWindow
from .about import AboutDialog
from .analysis import csv, file, psd, spectrogram, time
from .preferences import PreferencesDialog

logger = logging.getLogger(__name__)


P = ParamSpec('P')


class MondoMainWindow(Ui_MondoMainWindow, QtWidgets.QMainWindow):
    find_update_finished = QtCore.Signal(object)
    update_download_finished = QtCore.Signal(object)

    def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
        super().__init__(parent)

        self.update_progress_lock = threading.Lock()

        self.settings = QtCore.QSettings()
        self.update_style()

        self.setupUi(self)

        self.extra_ui_setup()
        self.setup_callbacks()
        self.setup_update_actions()

    def extra_ui_setup(self) -> None:
        app_name = QtWidgets.QApplication.applicationName()
        is_frozen = getattr(sys, 'frozen', False)
        if is_frozen:
            version = QtWidgets.QApplication.applicationVersion()
            title = self.tr("{} ({})").format(app_name, version)
        else:
            title = self.tr("{} (dev)").format(app_name)
        self.setWindowTitle(title)

        self.update_progress = QtWidgets.QProgressDialog("", "", 0, 100)
        self.update_progress.setLabelText(self.tr(""))
        self.update_progress.setWindowTitle(self.tr("Check for Update"))
        self.update_progress.setCancelButton(None)  # type: ignore
        self.update_progress.setWindowModality(
            QtCore.Qt.WindowModality.WindowModal)
        self.update_progress.setMinimumDuration(0)
        self.update_progress.setAutoReset(False)
        self.update_progress.reset()

    def setup_callbacks(self) -> None:
        self.timeButton.clicked.connect(self.time_analysis)
        self.synchronousTimeButton.clicked.connect(
            self.synchronous_time_analysis)
        self.overlaidTimeButton.clicked.connect(self.overlaid_time_analysis)
        self.spectrogramButton.clicked.connect(self.spectrogram_analysis)
        self.psdButton.clicked.connect(self.psd_analysis)
        self.singleChannelPsdButton.clicked.connect(
            self.single_channel_psd_analysis)
        self.singleSlicePsdButton.clicked.connect(
            self.single_slice_psd_analysis)
        self.overlaidPsdButton.clicked.connect(self.overlaid_psd_analysis)
        self.csvButton.clicked.connect(self.csv_export)
        self.csvDownsampledButton.clicked.connect(self.csv_downsampled_export)
        self.fileInformationButton.clicked.connect(self.file_information)
        self.viewSettingsButton.clicked.connect(self.view_settings)
        self.rawExportButton.clicked.connect(self.raw_export)
        self.splitFileButton.clicked.connect(self.split_file)

        self.actionPreferences.triggered.connect(self.show_preferences)

        self.actionAbout.triggered.connect(self.show_about)
        self.actionAboutLibraries.triggered.connect(self.show_about_libraries)
        self.actionShowLogs.triggered.connect(self.show_log_dir)

        self.actionUpdateLatestStable.triggered.connect(
            self.update_latest_stable)
        self.actionUpdateCurrentBranch.triggered.connect(
            self.update_current_branch)
        self.actionUpdateSpecificBranch.triggered.connect(
            self.update_specific_branch)
        self.actionUpdateSpecificCommit.triggered.connect(
            self.update_specific_commit)

        self.software_finder = hyperborea.download.SoftwareFinder(logger)
        self.software_finder.completed.connect(self.update_finder_completed)
        self.software_finder.error.connect(self.update_finder_error)
        self.ref_finder = hyperborea.download.RefFinder(logger)
        self.ref_finder.completed.connect(self.ref_finder_completed)
        self.ref_finder.error.connect(self.ref_finder_error)
        self.software_downloader = hyperborea.download.Downloader(logger)
        self.software_downloader.update.connect(self.update_progress_cb)
        self.software_downloader.completed.connect(
            self.software_download_completed)
        self.software_downloader.error.connect(self.software_download_error)

    def setup_update_actions(self) -> None:
        is_frozen = getattr(sys, 'frozen', False)

        valid_info = False
        if is_frozen:
            # load the build_info.txt
            main_dir = os.path.dirname(sys.executable)
            build_info_filename = os.path.join(main_dir, "build_info.txt")
            try:
                with open(build_info_filename, "r", encoding="utf-8") as f:
                    lines = f.readlines()
                    self.branch_name = lines[0].strip()
                    self.commit_hash = lines[1].strip()
                    self.build_key = lines[2].strip()
                    valid_info = True
            except Exception:
                logger.exception('Could not read build_info.txt')

        if not valid_info:
            self.menuCheckForUpdates.setEnabled(False)
            self.menuCheckForUpdates.setTitle(self.tr("Not Updatable"))
            self.actionUpdateLatestStable.setEnabled(False)
            self.actionUpdateCurrentBranch.setEnabled(False)
            self.actionUpdateSpecificBranch.setEnabled(False)
            self.actionUpdateSpecificCommit.setEnabled(False)
        else:
            if self.branch_name == "master":
                # master is latest stable
                self.actionUpdateCurrentBranch.setEnabled(False)
                self.actionUpdateCurrentBranch.setVisible(False)
            else:
                action_str = self.tr("Latest {}").format(self.branch_name)
                self.actionUpdateCurrentBranch.setText(action_str)

    def find_update(self, branch=None, commit=None, fallback_branch=None):
        self.update_fallback_branch = fallback_branch

        self.update_progress.setMinimum(0)
        self.update_progress.setMaximum(0)
        self.update_progress.setValue(0)
        self.update_progress.setLabelText(self.tr("Checking for update..."))
        self.update_progress.forceShow()

        self.software_finder.find_software("mondo", self.build_key, branch,
                                           commit)

    @QtCore.Slot(str)
    def update_finder_error(self, error_str: str):
        if self.update_fallback_branch:
            self.find_update(branch=self.update_fallback_branch)
        else:
            self.update_progress.reset()
            QtWidgets.QMessageBox.critical(self, self.tr("Error"), error_str)

    @QtCore.Slot(object)
    def update_finder_completed(self, params) -> None:
        self.update_progress.reset()
        url, commit, ready = params

        if commit is not None and commit == self.commit_hash:
            logger.info("Up to date with commit %s", commit)
            QtWidgets.QMessageBox.information(
                self, self.tr("Up to date"),
                self.tr("Already running this version"))
            return

        if not ready:
            logger.info("Update is not ready")
            QtWidgets.QMessageBox.information(self,
                                              self.tr("Update not ready"),
                                              self.tr("Update is not ready"))
            return

        # ask if the user wants to proceed
        ret = QtWidgets.QMessageBox.question(
            self, self.tr("Update?"), self.tr("Update available. Update now?"),
            QtWidgets.QMessageBox.StandardButton.Yes |
            QtWidgets.QMessageBox.StandardButton.No)
        if ret != QtWidgets.QMessageBox.StandardButton.Yes:
            return

        logger.info("Downloading update from %s", url)

        self.update_progress.setMinimum(0)
        self.update_progress.setMaximum(0)
        self.update_progress.setValue(0)
        self.update_progress.setLabelText(self.tr("Downloading update..."))
        self.update_progress.forceShow()

        fd, filename = tempfile.mkstemp(".exe", "setup-", text=False)
        file = os.fdopen(fd, "wb")
        file.filename = filename  # type: ignore
        self.software_downloader.start_download(url, file)

    @QtCore.Slot(int, int)
    def update_progress_cb(self, written_bytes: int, total_length: int):
        if total_length != 0:
            self.update_progress.setMinimum(0)
            self.update_progress.setMaximum(total_length)
            self.update_progress.setValue(written_bytes)

    @QtCore.Slot(object, str)
    def software_download_error(self, file, error_str: str):
        self.update_progress.reset()
        file.close()
        QtWidgets.QMessageBox.critical(self, self.tr("Error"), error_str)
        os.unlink(file.filename)

    @QtCore.Slot(str, object)
    def software_download_completed(self, _url: str, file) -> None:
        self.update_progress.reset()
        file.close()

        # run the intstaller
        subprocess.Popen([
            file.filename, '/silent', "/DeleteInstaller=Yes", "/SP-",
            "/SUPPRESSMSGBOXES", "/NORESTART", "/NOCANCEL"
        ])

        # close the application (though installer will force kill regardless)
        self.close()

    @QtCore.Slot()
    def update_latest_stable(self):
        self.find_update(branch="master")

    @QtCore.Slot()
    def update_current_branch(self):
        if self.branch_name in ("master", "develop"):
            fallback = None
        else:
            fallback = "develop"

        self.find_update(branch=self.branch_name, fallback_branch=fallback)

    @QtCore.Slot()
    def update_specific_branch(self):
        self.update_progress.setMinimum(0)
        self.update_progress.setMaximum(0)
        self.update_progress.setValue(0)
        self.update_progress.setLabelText(self.tr("Collecting branches..."))
        self.update_progress.forceShow()

        self.ref_finder.get_software_refs("mondo")

    @QtCore.Slot()
    def ref_finder_error(self):
        self.ref_finder_completed([])

    @QtCore.Slot(object)
    def ref_finder_completed(self, refs):
        self.update_progress.reset()

        default_branch = self.branch_name
        branch_choices = [default_branch]
        if default_branch != "master":
            branch_choices.append("master")
        if default_branch != "develop":
            branch_choices.append("develop")

        ref_names = []
        for ref in refs:
            ref_name = ref.get("name", None)
            if ref_name and ref_name not in branch_choices:
                ref_names.append(ref_name)

        if ref_names:
            label = self.tr("Branch:")
        else:
            label = self.tr("No list of branches!\nBranch:")

        branch_choices.extend(sorted(ref_names,
                                     key=hyperborea.download.ref_sort_key))

        branch, ok = QtWidgets.QInputDialog.getItem(self, self.tr("Branch"),
                                                    label, branch_choices, 0,
                                                    True)
        if not ok:
            return

        branch = branch.strip()

        self.find_update(branch=branch)

    @QtCore.Slot()
    def update_specific_commit(self):
        commit, ok = QtWidgets.QInputDialog.getText(
            self, self.tr("Commit"), self.tr("Commit:"),
            QtWidgets.QLineEdit.EchoMode.Normal, "")
        if not ok:
            return

        commit = commit.strip()

        self.find_update(commit=commit)

    @QtCore.Slot()
    def show_about(self) -> None:
        dialog = AboutDialog(self)
        dialog.exec()

    def _get_version(self, library: str) -> str:
        try:
            lib = importlib.import_module(library)
            return lib.__version__
        except (AttributeError, ImportError):
            return "ERROR"

    @QtCore.Slot()
    def show_about_libraries(self) -> None:
        libraries = [
            "hyperborea",
            "matplotlib",
            "numpy",
            "PySide6",
            "requests",
            "scipy",
        ]
        vers: dict[str, str] = {}
        for lib in libraries:
            vers[lib] = self._get_version(lib)

        # special case for asphodel and asphodel_py
        vers["asphodel_py"] = self._get_version("asphodel")
        vers["asphodel"] = asphodel.build_info

        # python version (sys.version is too long)
        is_64bit = sys.maxsize > (2 ** 32)
        bit_str = "64 bit" if is_64bit else "32 bit"
        python_ver = ".".join(map(str, sys.version_info[:3]))
        python_str = "{} ({} {})".format(python_ver, sys.platform, bit_str)
        vers['python'] = python_str

        s = "\n".join(k + ": " + vers[k] for k in sorted(vers, key=str.lower))
        QtWidgets.QMessageBox.information(self, "About Libraries", s)

    @QtCore.Slot()
    def show_log_dir(self) -> None:
        logdir = os.path.abspath(
            QtCore.QStandardPaths.writableLocation(
                QtCore.QStandardPaths.StandardLocation.AppLocalDataLocation))
        logfile = os.path.join(logdir, "main.log")

        if sys.platform == "win32":
            subprocess.Popen(['explorer', '/select,', logfile])
        elif sys.platform == "darwin":
            args = [
                "osascript",
                "-e", 'tell application "Finder"',
                "-e", "activate",
                "-e", f'select POSIX file "{logfile}"',
                "-e", "end tell",
            ]
            subprocess.Popen(args)
        else:
            url = QtCore.QUrl.fromLocalFile(logdir)
            QtGui.QDesktopServices.openUrl(url)

    def do_analysis(
            self, analysis: Callable[Concatenate[QtWidgets.QWidget, P], None],
            *args: P.args, **kwargs: P.kwargs) -> None:
        try:
            analysis(self, *args, **kwargs)
        except Exception:
            logger.exception("Uncaught exception")
            QtWidgets.QMessageBox.critical(
                self, "Error", "Uncaught exception. See log for details.")

    @QtCore.Slot()
    def time_analysis(self) -> None:
        self.do_analysis(time.time_analysis)

    @QtCore.Slot()
    def synchronous_time_analysis(self) -> None:
        self.do_analysis(time.synchronous_time_analysis)

    @QtCore.Slot()
    def overlaid_time_analysis(self) -> None:
        self.do_analysis(time.overlaid_time_analysis)

    @QtCore.Slot()
    def spectrogram_analysis(self) -> None:
        self.do_analysis(spectrogram.spectrogram_analysis)

    @QtCore.Slot()
    def psd_analysis(self) -> None:
        self.do_analysis(psd.psd_analysis)

    @QtCore.Slot()
    def single_channel_psd_analysis(self) -> None:
        self.do_analysis(psd.single_channel_psd_analysis)

    @QtCore.Slot()
    def single_slice_psd_analysis(self) -> None:
        self.do_analysis(psd.single_slice_psd_analysis)

    @QtCore.Slot()
    def overlaid_psd_analysis(self) -> None:
        self.do_analysis(psd.overlaid_psd_analysis)

    @QtCore.Slot()
    def csv_export(self) -> None:
        self.do_analysis(csv.csv_export)

    @QtCore.Slot()
    def csv_downsampled_export(self) -> None:
        self.do_analysis(csv.csv_export, downsample=True)

    @QtCore.Slot()
    def file_information(self) -> None:
        self.do_analysis(file.file_information)

    @QtCore.Slot()
    def view_settings(self) -> None:
        self.do_analysis(file.view_settings)

    @QtCore.Slot()
    def raw_export(self) -> None:
        self.do_analysis(file.raw_export)

    @QtCore.Slot()
    def split_file(self) -> None:
        self.do_analysis(file.split_file)

    @QtCore.Slot()
    def show_preferences(self) -> None:
        dialog = PreferencesDialog(self)
        dialog.exec()
        self.update_style()

    def update_style(self) -> None:
        dark_mode = read_bool_setting(self.settings, "DarkMode", True)
        set_style(QtWidgets.QApplication.instance(), dark_mode)  # type: ignore
