# -*- coding: utf-8 -*-
"""
This file contains the Qudi console app class.

Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this
distribution and on <https://github.com/Ulm-IQO/qudi-core/>

This file is part of qudi.

Qudi is free software: you can redistribute it and/or modify it under the terms of
the GNU Lesser General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.

Qudi 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with qudi.
If not, see <https://www.gnu.org/licenses/>.
"""

import os
import weakref
import platform
from PySide2 import QtCore, QtGui, QtWidgets
from qudi.core.gui.main_gui.main_gui import QudiMainGui
from qudi.core.modulemanager import ModuleManager
from qudi.util.paths import get_artwork_dir
from qudi.core.logger import get_logger

try:
    import pyqtgraph as pg
except ImportError:
    pg = None

logger = get_logger(__name__)


class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
    """Tray icon class subclassing QSystemTrayIcon for custom functionality.
    """

    def __init__(self):
        """Tray icon constructor.
        Adds all the appropriate menus and actions.
        """
        super().__init__()
        self._actions = dict()
        self.setIcon(QtWidgets.QApplication.instance().windowIcon())
        self.right_menu = QtWidgets.QMenu('Quit')
        self.left_menu = QtWidgets.QMenu('Manager')

        iconpath = os.path.join(get_artwork_dir(), 'icons')
        self.managericon = QtGui.QIcon()
        self.managericon.addFile(os.path.join(iconpath, 'go-home'), QtCore.QSize(16, 16))
        self.managerAction = QtWidgets.QAction(self.managericon, 'Manager', self.left_menu)

        self.exiticon = QtGui.QIcon()
        self.exiticon.addFile(os.path.join(iconpath, 'application-exit'), QtCore.QSize(16, 16))
        self.quitAction = QtWidgets.QAction(self.exiticon, 'Quit', self.right_menu)

        self.restarticon = QtGui.QIcon()
        self.restarticon.addFile(os.path.join(iconpath, 'view-refresh'), QtCore.QSize(16, 16))
        self.restartAction = QtWidgets.QAction(self.restarticon, 'Restart', self.right_menu)

        self.left_menu.addAction(self.managerAction)
        self.left_menu.addSeparator()

        self.right_menu.addAction(self.quitAction)
        self.right_menu.addAction(self.restartAction)
        self.setContextMenu(self.right_menu)

        self.activated.connect(self.handle_activation)

    @QtCore.Slot(QtWidgets.QSystemTrayIcon.ActivationReason)
    def handle_activation(self, reason):
        """Click handler.
        This method is called when the tray icon is left-clicked.
        It opens a menu at the position of the left click.

        @param reason: reason that caused the activation
        """
        if reason == self.Trigger:
            self.left_menu.exec_(QtGui.QCursor.pos())

    def add_action(self, label, callback, icon=None):
        if label in self._actions:
            raise ValueError(f'Action "{label}" already exists in system tray.')

        if not isinstance(icon, QtGui.QIcon):
            icon = QtGui.QIcon()
            iconpath = os.path.join(get_artwork_dir(), 'icons')
            icon.addFile(os.path.join(iconpath, 'go-next'))

        action = QtWidgets.QAction(label)
        action.setIcon(icon)
        action.triggered.connect(callback)
        self.left_menu.addAction(action)
        self._actions[label] = action

    def remove_action(self, label):
        action = self._actions.pop(label, None)
        if action is not None:
            action.triggered.disconnect()
            self.left_menu.removeAction(action)


class Gui(QtCore.QObject):
    """Set up all necessary GUI elements, like application icons, themes, etc.
    """

    _instance = None

    _sigPopUpMessage = QtCore.Signal(str, str)
    _sigBalloonMessage = QtCore.Signal(str, str, object, object)

    def __new__(cls, *args, **kwargs):
        if cls._instance is None or cls._instance() is None:
            obj = super().__new__(cls, *args, **kwargs)
            cls._instance = weakref.ref(obj)
            return obj
        raise RuntimeError(
            'Gui is a singleton. Please use Gui.instance() to get a reference to the already '
            'created instance.'
        )

    def __init__(self, qudi_instance, stylesheet_path=None, theme=None, use_opengl=False):
        if theme is None:
            theme = 'qudiTheme'

        super().__init__()

        app = QtWidgets.QApplication.instance()
        if app is None:
            raise RuntimeError('No Qt GUI app running (no QApplication instance).')

        app.setQuitOnLastWindowClosed(False)

        self._init_app_icon()
        self.set_theme(theme)
        if stylesheet_path is not None:
            self.set_style_sheet(stylesheet_path)
        self.system_tray_icon = SystemTrayIcon()

        self._sigPopUpMessage.connect(self.pop_up_message, QtCore.Qt.QueuedConnection)
        self._sigBalloonMessage.connect(self.balloon_message, QtCore.Qt.QueuedConnection)

        self._configure_pyqtgraph(use_opengl)
        self.main_gui_module = QudiMainGui(qudi_main_weakref=weakref.ref(qudi_instance),
                                           name='qudi_main_gui')
        self.system_tray_icon.managerAction.triggered.connect(self.activate_main_gui,
                                                              QtCore.Qt.QueuedConnection)
        self.system_tray_icon.quitAction.triggered.connect(qudi_instance.quit,
                                                           QtCore.Qt.QueuedConnection)
        self.system_tray_icon.restartAction.triggered.connect(qudi_instance.restart,
                                                              QtCore.Qt.QueuedConnection)
        qudi_instance.module_manager.sigModuleStateChanged.connect(self._tray_module_action_changed)
        self.show_system_tray_icon()

    @classmethod
    def instance(cls):
        if cls._instance is None:
            return None
        return cls._instance()

    @staticmethod
    def _init_app_icon():
        """Set up the Qudi application icon.
        """
        app_icon = QtGui.QIcon(os.path.join(get_artwork_dir(), 'logo', 'logo-qudi.svg'))
        QtWidgets.QApplication.instance().setWindowIcon(app_icon)

    @staticmethod
    def _configure_pyqtgraph(use_opengl=False):
        # Configure pyqtgraph (if present)
        if pg is not None:
            # test setting background of pyqtgraph
            testwidget = QtWidgets.QWidget()
            testwidget.ensurePolished()
            bgcolor = testwidget.palette().color(QtGui.QPalette.Normal, testwidget.backgroundRole())
            # set manually the background color in hex code according to our color scheme:
            pg.setConfigOption('background', bgcolor)
            # experimental opengl usage
            pg.setConfigOption('useOpenGL', use_opengl)

    @staticmethod
    def set_theme(theme):
        """
        Set icon theme for qudi app.

        @param str theme: qudi theme name
        """
        # Make icons work on non-X11 platforms, set custom theme
        # if not sys.platform.startswith('linux') and not sys.platform.startswith('freebsd'):
        #
        # To enable the use of custom action icons, for now the above if statement has been
        # removed and the QT theme is being set to our artwork/icons folder for
        # all OSs.
        themepaths = QtGui.QIcon.themeSearchPaths()
        themepaths.append(os.path.join(get_artwork_dir(), 'icons'))
        QtGui.QIcon.setThemeSearchPaths(themepaths)
        QtGui.QIcon.setThemeName(theme)

    @staticmethod
    def set_style_sheet(stylesheet_path):
        """
        Set qss style sheet for application.

        @param str stylesheet_path: path to style sheet file
        """
        try:
            if not os.path.exists(stylesheet_path):
                stylesheet_path = os.path.join(get_artwork_dir(), 'styles', stylesheet_path)

            with open(stylesheet_path, 'r') as stylesheetfile:
                stylesheet = stylesheetfile.read()

            if stylesheet_path.endswith('qdark.qss'):
                path = os.path.join(os.path.dirname(stylesheet_path), 'qdark').replace('\\', '/')
                stylesheet = stylesheet.replace('{qdark}', path)

            # see issue #12 on qdarkstyle github
            if platform.system().lower() == 'darwin' and stylesheet_path.endswith('qdark.qss'):
                mac_fix = '''
                QDockWidget::title
                {
                    background-color: #31363b;
                    text-align: center;
                    height: 12px;
                }
                '''
                stylesheet += mac_fix
            QtWidgets.QApplication.instance().setStyleSheet(stylesheet)
        except:
            logger.exception('Exception while setting qudi stylesheet:')

    @staticmethod
    def close_windows():
        """Close all application windows.
        """
        QtWidgets.QApplication.instance().closeAllWindows()

    def activate_main_gui(self):
        if QtCore.QThread.currentThread() is not self.thread():
            QtCore.QMetaObject.invokeMethod(self,
                                            'activate_main_gui',
                                            QtCore.Qt.BlockingQueuedConnection)
            return

        if self.main_gui_module.module_state() != 'deactivated':
            self.main_gui_module.show()
            return

        logger.info('Activating main GUI module...')
        print('> Activating main GUI module...')

        self.main_gui_module.module_state.activate()
        QtWidgets.QApplication.instance().processEvents()

    def deactivate_main_gui(self):
        if QtCore.QThread.currentThread() is not self.thread():
            QtCore.QMetaObject.invokeMethod(self,
                                            'deactivate_main_gui',
                                            QtCore.Qt.BlockingQueuedConnection)
            return

        if self.main_gui_module.module_state() == 'deactivated':
            return

        self.main_gui_module.module_state.deactivate()
        QtWidgets.QApplication.instance().processEvents()

    def show_system_tray_icon(self):
        """Show system tray icon.
        """
        self.system_tray_icon.show()

    def hide_system_tray_icon(self):
        """Hide system tray icon.
        """
        self.system_tray_icon.hide()

    def close_system_tray_icon(self):
        """
        Kill and delete system tray icon. Tray icon will be lost until Gui.__init__ is called again.
        """
        self.hide_system_tray_icon()
        self.system_tray_icon.quitAction.triggered.disconnect()
        self.system_tray_icon.restartAction.triggered.disconnect()
        self.system_tray_icon.managerAction.triggered.disconnect()
        self.system_tray_icon = None

    def system_tray_notification_bubble(self, title, message, time=None, icon=None):
        """
        Helper method to invoke balloon messages in the system tray by calling
        QSystemTrayIcon.showMessage.

        @param str title: The notification title of the balloon
        @param str message: The message to be shown in the balloon
        @param float time: optional, The lingering time of the balloon in seconds
        @param QIcon icon: optional, an icon to be used in the balloon. "None" will use OS default.
        """
        if icon is None:
            icon = QtGui.QIcon()
        if time is None:
            time = 15
        self.system_tray_icon.showMessage(title, message, icon, int(round(time * 1000)))

    def prompt_shutdown(self, modules_locked=True):
        """Display a dialog, asking the user to confirm shutdown.
        """
        if modules_locked:
            msg = 'Some qudi modules are locked right now.\n' \
                  'Do you really want to quit and force modules to deactivate?'
        else:
            msg = 'Do you really want to quit?'

        result = QtWidgets.QMessageBox.question(self.main_gui_module.mw,
                                                'Qudi: Quit?',
                                                msg,
                                                QtWidgets.QMessageBox.Yes,
                                                QtWidgets.QMessageBox.No)
        return result == QtWidgets.QMessageBox.Yes

    def prompt_restart(self, modules_locked=True):
        """Display a dialog, asking the user to confirm restart.
        """
        if modules_locked:
            msg = 'Some qudi modules are locked right now.\n' \
                  'Do you really want to restart and force modules to deactivate?'
        else:
            msg = 'Do you really want to restart?'

        result = QtWidgets.QMessageBox.question(self.main_gui_module.mw,
                                                'Qudi: Restart?',
                                                msg,
                                                QtWidgets.QMessageBox.Yes,
                                                QtWidgets.QMessageBox.No)
        return result == QtWidgets.QMessageBox.Yes

    @QtCore.Slot(str, str)
    def pop_up_message(self, title, message):
        """
        Slot prompting a dialog window with a message and an OK button to dismiss it.

        @param str title: The window title of the dialog
        @param str message: The message to be shown in the dialog window
        """
        if not isinstance(title, str):
            logger.error('pop-up message title must be str type')
            return
        if not isinstance(message, str):
            logger.error('pop-up message must be str type')
            return
        if self.thread() is not QtCore.QThread.currentThread():
            self._sigPopUpMessage.emit(title, message)
            return
        QtWidgets.QMessageBox.information(None, title, message, QtWidgets.QMessageBox.Ok)
        return

    @QtCore.Slot(str, str, object, object)
    def balloon_message(self, title, message, time=None, icon=None):
        """
        Slot prompting a balloon notification from the system tray icon.

        @param str title: The notification title of the balloon
        @param str message: The message to be shown in the balloon
        @param float time: optional, The lingering time of the balloon in seconds
        @param QIcon icon: optional, an icon to be used in the balloon. "None" will use OS default.
        """
        if not self.system_tray_icon.supportsMessages():
            logger.warning('{0}:\n{1}'.format(title, message))
            return
        if self.thread() is not QtCore.QThread.currentThread():
            self._sigBalloonMessage.emit(title, message, time, icon)
            return
        self.system_tray_notification_bubble(title, message, time=time, icon=icon)
        return

    @QtCore.Slot(str, str, str)
    def _tray_module_action_changed(self, base, module_name, state):
        if self.system_tray_icon and base == 'gui':
            if state == 'deactivated':
                self.system_tray_icon.remove_action(module_name)
            else:
                mod_manager = ModuleManager.instance()
                try:
                    module_inst = mod_manager[module_name].instance
                except KeyError:
                    return
                self.system_tray_icon.add_action(module_name, module_inst.show)
