# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""Utility functions for the Spyder application."""

# Standard library imports
import glob
import logging
import os
import os.path as osp
import re
import sys

# Third-party imports
import psutil
from qtpy.QtCore import QCoreApplication, Qt
from qtpy.QtGui import QColor, QIcon, QPalette, QPixmap, QPainter, QImage
from qtpy.QtWidgets import QApplication, QSplashScreen
from qtpy.QtSvg import QSvgRenderer

# Local imports
from spyder.config.base import (
    DEV, get_conf_path, get_debug_level, running_in_mac_app,
    running_under_pytest)
from spyder.config.manager import CONF
from spyder.utils.external.dafsa.dafsa import DAFSA
from spyder.utils.image_path_manager import get_image_path
from spyder.utils.installers import running_installer_test
from spyder.utils.palette import QStylePalette
from spyder.utils.qthelpers import file_uri, qapplication

# For spyder-ide/spyder#7447.
try:
    from qtpy.QtQuick import QQuickWindow, QSGRendererInterface
except Exception:
    QQuickWindow = QSGRendererInterface = None


root_logger = logging.getLogger()
FILTER_NAMES = os.environ.get('SPYDER_FILTER_LOG', "").split(',')
FILTER_NAMES = [f.strip() for f in FILTER_NAMES]

# Keeping a reference to the original sys.exit before patching it
ORIGINAL_SYS_EXIT = sys.exit


class Spy:
    """
    This is used to inject a 'spy' object in the internal console
    namespace to inspect Spyder internals.

    Attributes:
        app       Reference to main QApplication object
        window    Reference to spyder.MainWindow widget
    """
    def __init__(self, app, window):
        self.app = app
        self.window = window

    def __dir__(self):
        return (list(self.__dict__.keys()) +
                [x for x in dir(self.__class__) if x[0] != '_'])


def get_python_doc_path():
    """
    Return Python documentation path
    (Windows: return the PythonXX.chm path if available)
    """
    if os.name == 'nt':
        doc_path = osp.join(sys.prefix, "Doc")
        if not osp.isdir(doc_path):
            return
        python_chm = [path for path in os.listdir(doc_path)
                      if re.match(r"(?i)Python[0-9]{3,6}.chm", path)]
        if python_chm:
            return file_uri(osp.join(doc_path, python_chm[0]))
    else:
        vinf = sys.version_info
        doc_path = '/usr/share/doc/python%d.%d/html' % (vinf[0], vinf[1])
    python_doc = osp.join(doc_path, "index.html")
    if osp.isfile(python_doc):
        return file_uri(python_doc)


def set_opengl_implementation(option):
    """
    Set the OpenGL implementation used by Spyder.

    See spyder-ide/spyder#7447 for the details.
    """
    if option == 'software':
        QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL)
        if QQuickWindow is not None:
            QQuickWindow.setSceneGraphBackend(QSGRendererInterface.Software)
    elif option == 'desktop':
        QCoreApplication.setAttribute(Qt.AA_UseDesktopOpenGL)
        if QQuickWindow is not None:
            QQuickWindow.setSceneGraphBackend(QSGRendererInterface.OpenGL)
    elif option == 'gles':
        QCoreApplication.setAttribute(Qt.AA_UseOpenGLES)
        if QQuickWindow is not None:
            QQuickWindow.setSceneGraphBackend(QSGRendererInterface.OpenGL)


def setup_logging(cli_options):
    """Setup logging with cli options defined by the user."""
    if cli_options.debug_info or get_debug_level() > 0:
        levels = {2: logging.INFO, 3: logging.DEBUG}
        log_level = levels[get_debug_level()]
        log_format = '%(asctime)s [%(levelname)s] [%(name)s] -> %(message)s'

        console_filters = cli_options.filter_log.split(',')
        console_filters = [x.strip() for x in console_filters]
        console_filters = console_filters + FILTER_NAMES
        console_filters = [x for x in console_filters if x != '']

        handlers = [logging.StreamHandler()]
        filepath = os.environ['SPYDER_DEBUG_FILE']
        handlers.append(
            logging.FileHandler(filename=filepath, mode='w+')
        )

        match_func = lambda x: True
        if console_filters != [''] and len(console_filters) > 0:
            dafsa = DAFSA(console_filters)
            match_func = lambda x: (dafsa.lookup(x, stop_on_prefix=True)
                                    is not None)

        formatter = logging.Formatter(log_format)

        class ModuleFilter(logging.Filter):
            """Filter messages based on module name prefix."""

            def filter(self, record):
                return match_func(record.name)

        filter = ModuleFilter()
        root_logger.setLevel(log_level)
        for handler in handlers:
            handler.addFilter(filter)
            handler.setFormatter(formatter)
            handler.setLevel(log_level)
            root_logger.addHandler(handler)


def delete_debug_log_files():
    """Delete previous debug log files."""
    regex = re.compile(r'.*_.*_(\d+)[.]log')
    files = glob.glob(osp.join(get_conf_path('lsp_logs'), '*.log'))
    for f in files:
        match = regex.match(f)
        if match is not None:
            pid = int(match.group(1))
            if not psutil.pid_exists(pid):
                os.remove(f)

    debug_file = os.environ['SPYDER_DEBUG_FILE']
    if osp.exists(debug_file):
        os.remove(debug_file)


def qt_message_handler(msg_type, msg_log_context, msg_string):
    """
    Qt warning messages are intercepted by this handler.

    On some operating systems, warning messages might be displayed
    even if the actual message does not apply. This filter adds a
    blacklist for messages that are being printed for no apparent
    reason. Anything else will get printed in the internal console.

    In DEV mode, all messages are printed.
    """
    BLACKLIST = [
        'QMainWidget::resizeDocks: all sizes need to be larger than 0',
    ]
    if DEV or msg_string not in BLACKLIST:
        print(msg_string)  # spyder: test-skip


def create_splash_screen():
    """Create splash screen."""
    if not running_under_pytest():
        image = QImage(500, 400, QImage.Format_ARGB32_Premultiplied)
        image.fill(0)
        painter = QPainter(image)
        renderer = QSvgRenderer(get_image_path('splash'))
        renderer.render(painter)
        painter.end()

        pm = QPixmap.fromImage(image)
        pm = pm.copy(0, 0, 500, 400)

        splash = QSplashScreen(pm)
        splash_font = splash.font()
        splash_font.setPixelSize(14)
        splash.setFont(splash_font)
    else:
        splash = None

    return splash


def set_links_color(app):
    """
    Fix color for links.

    This was taken from QDarkstyle, which is MIT licensed.
    """
    color = QStylePalette.COLOR_ACCENT_4
    qcolor = QColor(color)

    app_palette = app.palette()
    app_palette.setColor(QPalette.Normal, QPalette.Link, qcolor)
    app.setPalette(app_palette)


def create_application():
    """Create application and patch sys.exit."""
    # Our QApplication
    app = qapplication()

    # --- Set application icon
    app_icon = QIcon(get_image_path("spyder"))
    app.setWindowIcon(app_icon)

    # Required for correct icon on GNOME/Wayland:
    if hasattr(app, 'setDesktopFileName'):
        app.setDesktopFileName('spyder')

    # ---- Monkey patching QApplication
    class FakeQApplication(QApplication):
        """Spyder's fake QApplication"""
        def __init__(self, args):
            self = app  # analysis:ignore

        @staticmethod
        def exec_():
            """Do nothing because the Qt mainloop is already running"""
            pass

    from qtpy import QtWidgets
    QtWidgets.QApplication = FakeQApplication

    # ---- Monkey patching sys.exit
    def fake_sys_exit(arg=[]):
        pass
    sys.exit = fake_sys_exit

    # ---- Monkey patching sys.excepthook to avoid crashes in PyQt 5.5+
    def spy_excepthook(type_, value, tback):
        sys.__excepthook__(type_, value, tback)
        if running_installer_test():
            # This will exit Spyder with exit code 1 without invoking
            # macOS system dialogue window.
            raise SystemExit(1)
    sys.excepthook = spy_excepthook

    # Removing arguments from sys.argv as in standard Python interpreter
    sys.argv = ['']

    return app


def create_window(WindowClass, app, splash, options, args):
    """
    Create and show Spyder's main window and start QApplication event loop.

    Parameters
    ----------
    WindowClass: QMainWindow
        Subclass to instantiate the Window.
    app: QApplication
        Instance to start the application.
    splash: QSplashScreen
        Splash screen instamce.
    options: argparse.Namespace
        Command line options passed to Spyder
    args: list
        List of file names passed to the Spyder executable in the
        command line.
    """
    # Main window
    main = WindowClass(splash, options)
    try:
        main.setup()
    except BaseException:
        if main.console is not None:
            try:
                main.console.exit_interpreter()
            except BaseException:
                pass
        raise

    main.pre_visible_setup()
    main.show()
    main.post_visible_setup()

    if main.console:
        namespace = CONF.get('internal_console', 'namespace', {})
        main.console.start_interpreter(namespace)
        main.console.set_namespace_item('spy', Spy(app=app, window=main))

    # Propagate current configurations to all configuration observers
    CONF.notify_all_observers()

    # Don't show icons in menus for Mac
    if sys.platform == 'darwin':
        QCoreApplication.setAttribute(Qt.AA_DontShowIconsInMenus, True)

    # Open external files with our Mac app
    if running_in_mac_app():
        app.sig_open_external_file.connect(main.open_external_file)
        app._has_started = True
        if hasattr(app, '_pending_file_open'):
            if args:
                args = app._pending_file_open + args
            else:
                args = app._pending_file_open

    # Open external files passed as args
    if args:
        for a in args:
            main.open_external_file(a)

    # To give focus again to the last focused widget after restoring
    # the window
    app.focusChanged.connect(main.change_last_focused_widget)

    if not running_under_pytest():
        app.exec_()
    return main
