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

"""
API to create an entry in Spyder Preferences associated to a given plugin.
"""

# Standard library imports
import types
from typing import Tuple, Union, Set

# Third party imports
from qtpy.QtWidgets import QWidget

# Local imports
from spyder.config.manager import CONF
from spyder.config.types import ConfigurationKey
from spyder.api.utils import PrefixedTuple
from spyder.plugins.preferences.api import SpyderConfigPage, BaseConfigTab


OptionSet = Set[ConfigurationKey]


class SpyderPreferencesTab(BaseConfigTab):
    """
    Widget that represents a tab on a preference page.

    All calls to :class:`SpyderConfigPage` attributes are resolved
    via delegation.
    """

    # Name of the tab to display on the configuration page.
    TITLE = None

    def __init__(self, parent: SpyderConfigPage):
        super().__init__(parent)
        self.parent = parent

        if self.TITLE is None or not isinstance(self.TITLE, str):
            raise ValueError("TITLE must be a str")

    def apply_settings(self) -> OptionSet:
        """
        Hook called to manually apply settings that cannot be automatically
        applied.

        Reimplement this if the configuration tab has complex widgets that
        cannot be created with any of the `self.create_*` calls.
        """
        return set({})

    def is_valid(self) -> bool:
        """
        Return True if the tab contents are valid.

        This method can be overriden to perform complex checks.
        """
        return True

    def __getattr__(self, attr):
        this_class_dir = dir(self)
        if attr not in this_class_dir:
            return getattr(self.parent, attr)
        else:
            return super().__getattr__(attr)


class PluginConfigPage(SpyderConfigPage):
    """
    Widget to expose the options a plugin offers for configuration as
    an entry in Spyder's Preferences dialog.
    """

    # TODO: Temporal attribute to handle which appy_settings method to use
    # the one of the conf page or the one in the plugin, while the config
    # dialog system is updated.
    APPLY_CONF_PAGE_SETTINGS = False

    def __init__(self, plugin, parent):
        self.plugin = plugin
        self.CONF_SECTION = plugin.CONF_SECTION
        self.main = parent.main
        self.get_font = plugin.get_font

        if not self.APPLY_CONF_PAGE_SETTINGS:
            self._patch_apply_settings(plugin)

        SpyderConfigPage.__init__(self, parent)

    def _wrap_apply_settings(self, func):
        """
        Wrap apply_settings call to ensure that a user-defined custom call
        is called alongside the Spyder Plugin API configuration propagation
        call.
        """
        def wrapper(self, options):
            opts = self.previous_apply_settings() or set({})
            opts |= options
            self.aggregate_sections_partials(opts)
            func(opts)
        return types.MethodType(wrapper, self)

    def _patch_apply_settings(self, plugin):
        self.previous_apply_settings = self.apply_settings
        try:
            # New API
            self.apply_settings = self._wrap_apply_settings(plugin.apply_conf)
            self.get_option = plugin.get_conf
            self.set_option = plugin.set_conf
            self.remove_option = plugin.remove_conf
        except AttributeError:
            # Old API
            self.apply_settings = self._wrap_apply_settings(
                plugin.apply_plugin_settings)
            self.get_option = plugin.get_option
            self.set_option = plugin.set_option
            self.remove_option = plugin.remove_option

    def aggregate_sections_partials(self, opts):
        """Aggregate options by sections in order to notify observers."""
        to_update = {}
        for opt in opts:
            section = self.CONF_SECTION
            if opt in self.cross_section_options:
                section = self.cross_section_options[opt]
            section_options = to_update.get(section, [])
            section_options.append(opt)
            to_update[section] = section_options

        for section in to_update:
            section_prefix = PrefixedTuple()
            # Notify section observers
            CONF.notify_observers(section, '__section',
                                  recursive_notification=False)
            for opt in to_update[section]:
                if isinstance(opt, tuple):
                    opt = opt[:-1]
                    section_prefix.add_path(opt)
            # Notify prefixed observers
            for prefix in section_prefix:
                try:
                    CONF.notify_observers(section, prefix,
                                          recursive_notification=False)
                except Exception:
                    # Prevent unexpected failures on tests
                    pass

    def get_name(self):
        """
        Return plugin name to use in preferences page title, and
        message boxes.

        Normally you do not have to reimplement it, as soon as the
        plugin name in preferences page will be the same as the plugin
        title.
        """
        try:
            # New API
            name = self.plugin.get_name()
        except AttributeError:
            # Old API
            name = self.plugin.get_plugin_title()

        return name

    def get_icon(self):
        """
        Return plugin icon to use in preferences page.

        Normally you do not have to reimplement it, as soon as the
        plugin icon in preferences page will be the same as the plugin
        icon.
        """
        try:
            # New API
            icon = self.plugin.get_icon()
        except AttributeError:
            # Old API
            icon = self.plugin.get_plugin_icon()

        return icon

    def setup_page(self):
        """
        Setup configuration page widget

        You should implement this method and set the layout of the
        preferences page.

        layout = QVBoxLayout()
        layout.addWidget(...)
        ...
        self.setLayout(layout)
        """
        raise NotImplementedError

    def apply_settings(self) -> OptionSet:
        """
        Hook called to manually apply settings that cannot be automatically
        applied.

        Reimplement this if the configuration page has complex widgets that
        cannot be created with any of the `self.create_*` calls.

        This call should return a set containing the configuration options that
        changed.
        """
        return set({})
