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

"""
This module provides user configuration file management features for Spyder.

It is based on the ConfigParser module present in the standard library.
"""

# Standard library imports
import ast
import configparser as cp
import copy
import io
import os
import os.path as osp
import re
import shutil
import time

# Local imports
from spyder.config.base import get_conf_path, get_module_source_path
from spyder.py3compat import is_text_string, to_text_string
from spyder.utils.programs import check_version


# ============================================================================
# Auxiliary classes
# ============================================================================
class NoDefault:
    pass


# ============================================================================
# Defaults class
# ============================================================================
class DefaultsConfig(cp.ConfigParser, object):
    """
    Class used to save defaults to a file and as UserConfig base class.
    """

    def __init__(self, name, path):
        """
        Class used to save defaults to a file and as UserConfig base class.
        """
        super(DefaultsConfig, self).__init__(interpolation=None)

        self._name = name
        self._path = path

        if not osp.isdir(osp.dirname(self._path)):
            os.makedirs(osp.dirname(self._path))

    def _write(self, fp):
        """
        Write method for Python 2.

        The one from configparser fails for non-ascii Windows accounts.
        """
        if self._defaults:
            fp.write('[{}]\n'.format(cp.DEFAULTSECT))
            for (key, value) in self._defaults.items():
                value_plus_end_of_line = str(value).replace('\n', '\n\t')
                fp.write('{} = {}\n'.format(key, value_plus_end_of_line))

            fp.write('\n')

        for section in self._sections:
            fp.write('[{}]\n'.format(section))
            for (key, value) in self._sections[section].items():
                if key == '__name__':
                    continue

                if (value is not None) or (self._optcre == self.OPTCRE):
                    value = to_text_string(value)
                    value_plus_end_of_line = value.replace('\n', '\n\t')
                    key = ' = '.join((key, value_plus_end_of_line))

                fp.write('{}\n'.format(key))

            fp.write('\n')

    def _set(self, section, option, value, verbose):
        """Set method."""
        if not self.has_section(section):
            self.add_section(section)

        if not is_text_string(value):
            value = repr(value)

        if verbose:
            text = '[{}][{}] = {}'.format(section, option, value)
            print(text)  # spyder: test-skip

        super(DefaultsConfig, self).set(section, option, value)

    def _save(self):
        """Save config into the associated .ini file."""
        fpath = self.get_config_fpath()

        def _write_file(fpath):
            with io.open(fpath, 'w', encoding='utf-8') as configfile:
                self.write(configfile)

        # See spyder-ide/spyder#1086 and spyder-ide/spyder#1242 for background
        # on why this method contains all the exception handling.
        try:
            # The "easy" way
            _write_file(fpath)
        except EnvironmentError:
            try:
                # The "delete and sleep" way
                if osp.isfile(fpath):
                    os.remove(fpath)

                time.sleep(0.05)
                _write_file(fpath)
            except Exception as e:
                print('Failed to write user configuration file to disk, with '
                      'the exception shown below')  # spyder: test-skip
                print(e)  # spyder: test-skip

    def get_config_fpath(self):
        """Return the ini file where this configuration is stored."""
        path = self._path
        config_file = osp.join(path, '{}.ini'.format(self._name))
        return config_file

    def set_defaults(self, defaults):
        """Set default values and save to defaults folder location."""
        for section, options in defaults:
            for option in options:
                new_value = options[option]
                self._set(section, option, new_value, verbose=False)


# ============================================================================
# User config class
# ============================================================================
class UserConfig(DefaultsConfig):
    """
    UserConfig class, based on ConfigParser.

    Parameters
    ----------
    name: str
        Name of the config
    path: str
        Configuration file will be saved in path/%name%.ini
    defaults: {} or [(str, {}),]
        Dictionary containing options *or* list of tuples (sec_name, options)
    load: bool
        If a previous configuration file is found, load will take the values
        from this existing file, instead of using default values.
    version: str
        version of the configuration file in 'major.minor.micro' format.
    backup: bool
        A backup will be created on version changes and on initial setup.
    raw_mode: bool
        If `True` do not apply any automatic conversion on values read from
        the configuration.
    remove_obsolete: bool
        If `True`, values that were removed from the configuration on version
        change, are removed from the saved configuration file.

    Notes
    -----
    The 'get' and 'set' arguments number and type differ from the overriden
    methods. 'defaults' is an attribute and not a method.
    """
    DEFAULT_SECTION_NAME = 'main'

    def __init__(self, name, path, defaults=None, load=True, version=None,
                 backup=False, raw_mode=False, remove_obsolete=False,
                 external_plugin=False):
        """UserConfig class, based on ConfigParser."""
        super(UserConfig, self).__init__(name=name, path=path)

        self._load = load
        self._version = self._check_version(version)
        self._backup = backup
        self._raw = 1 if raw_mode else 0
        self._remove_obsolete = remove_obsolete
        self._external_plugin = external_plugin

        self._module_source_path = get_module_source_path('spyder')
        self._defaults_folder = 'defaults'
        self._backup_folder = 'backups'
        self._backup_suffix = '.bak'
        self._defaults_name_prefix = 'defaults'

        # This attribute is overriding a method from cp.ConfigParser
        self.defaults = self._check_defaults(defaults)

        if backup:
            self._make_backup()

        if load:
            # If config file already exists, it overrides Default options
            previous_fpath = self.get_previous_config_fpath()
            self._load_from_ini(previous_fpath)
            old_version = self.get_version(version)
            self._old_version = old_version

            # Save new defaults
            self._save_new_defaults(self.defaults)

            # Updating defaults only if major/minor version is different
            if (self._get_minor_version(version)
                    != self._get_minor_version(old_version)):

                if backup:
                    self._make_backup(version=old_version)

                self.apply_configuration_patches(old_version=old_version)

                # Remove deprecated options if major version has changed
                if remove_obsolete:
                    self._remove_deprecated_options(old_version)

                # Set new version number
                self.set_version(version, save=False)

            if defaults is None:
                # If no defaults are defined set .ini file settings as default
                self.set_as_defaults()

    # --- Helpers and checkers
    # ------------------------------------------------------------------------
    @staticmethod
    def _get_minor_version(version):
        """Return the 'major.minor' components of the version."""
        return version[:version.rfind('.')]

    @staticmethod
    def _get_major_version(version):
        """Return the 'major' component of the version."""
        return version[:version.find('.')]

    @staticmethod
    def _check_version(version):
        """Check version is compliant with format."""
        regex_check = re.match(r'^(\d+).(\d+).(\d+)$', version)
        if version is not None and regex_check is None:
            raise ValueError('Version number {} is incorrect - must be in '
                             'major.minor.micro format'.format(version))

        return version

    def _check_defaults(self, defaults):
        """Check if defaults are valid and update defaults values."""
        if defaults is None:
            defaults = [(self.DEFAULT_SECTION_NAME, {})]
        elif isinstance(defaults, dict):
            defaults = [(self.DEFAULT_SECTION_NAME, defaults)]
        elif isinstance(defaults, list):
            # Check is a list of tuples with strings and dictionaries
            for sec, options in defaults:
                assert is_text_string(sec)
                assert isinstance(options, dict)
                for opt, _ in options.items():
                    assert is_text_string(opt)
        else:
            raise ValueError('`defaults` must be a dict or a list of tuples!')

        # This attribute is overriding a method from cp.ConfigParser
        self.defaults = defaults

        if defaults is not None:
            self.reset_to_defaults(save=False)

        return defaults

    @classmethod
    def _check_section_option(cls, section, option):
        """Check section and option types."""
        if section is None:
            section = cls.DEFAULT_SECTION_NAME
        elif not is_text_string(section):
            raise RuntimeError("Argument 'section' must be a string")

        if not is_text_string(option):
            raise RuntimeError("Argument 'option' must be a string")

        return section

    def _make_backup(self, version=None, old_version=None):
        """
        Make a backup of the configuration file.

        If `old_version` is `None` a normal backup is made. If `old_version`
        is provided, then the backup was requested for minor version changes
        and appends the version number to the backup file.
        """
        fpath = self.get_config_fpath()
        fpath_backup = self.get_backup_fpath_from_version(
            version=version, old_version=old_version)
        path = os.path.dirname(fpath_backup)

        if not osp.isdir(path):
            os.makedirs(path)

        try:
            shutil.copyfile(fpath, fpath_backup)
        except IOError:
            pass

    def _load_from_ini(self, fpath):
        """Load config from the associated .ini file found at `fpath`."""
        try:
            self.read(fpath, encoding='utf-8')
        except cp.MissingSectionHeaderError:
            error_text = 'Warning: File contains no section headers.'
            print(error_text)  # spyder: test-skip

    def _load_old_defaults(self, old_version):
        """Read old defaults."""
        old_defaults = cp.ConfigParser()
        path, name = self.get_defaults_path_name_from_version(old_version)
        old_defaults.read(osp.join(path, name + '.ini'))
        return old_defaults

    def _save_new_defaults(self, defaults):
        """Save new defaults."""
        path, name = self.get_defaults_path_name_from_version()
        new_defaults = DefaultsConfig(name=name, path=path)
        if not osp.isfile(new_defaults.get_config_fpath()):
            new_defaults.set_defaults(defaults)
            new_defaults._save()

    def _update_defaults(self, defaults, old_version, verbose=False):
        """Update defaults after a change in version."""
        old_defaults = self._load_old_defaults(old_version)
        for section, options in defaults:
            for option in options:
                new_value = options[option]
                try:
                    old_val = old_defaults.get(section, option)
                except (cp.NoSectionError, cp.NoOptionError):
                    old_val = None

                if old_val is None or to_text_string(new_value) != old_val:
                    self._set(section, option, new_value, verbose)

    def _remove_deprecated_options(self, old_version):
        """
        Remove options which are present in the .ini file but not in defaults.
        """
        old_defaults = self._load_old_defaults(old_version)
        for section in old_defaults.sections():
            for option, _ in old_defaults.items(section, raw=self._raw):
                if self.get_default(section, option) is NoDefault:
                    try:
                        self.remove_option(section, option)
                        if len(self.items(section, raw=self._raw)) == 0:
                            self.remove_section(section)
                    except cp.NoSectionError:
                        self.remove_section(section)

    # --- Compatibility API
    # ------------------------------------------------------------------------
    def get_previous_config_fpath(self):
        """Return the last configuration file used if found."""
        return self.get_config_fpath()

    def get_config_fpath_from_version(self, version=None):
        """
        Return the configuration path for given version.

        If no version is provided, it returns the current file path.
        """
        return self.get_config_fpath()

    def get_backup_fpath_from_version(self, version=None, old_version=None):
        """
        Get backup location based on version.

        `old_version` can be used for checking compatibility whereas `version`
        relates to adding the version to the file name.

        To be overridden if versions changed backup location.
        """
        fpath = self.get_config_fpath()
        path = osp.join(osp.dirname(fpath), self._backup_folder)
        new_fpath = osp.join(path, osp.basename(fpath))
        if version is None:
            backup_fpath = '{}{}'.format(new_fpath, self._backup_suffix)
        else:
            backup_fpath = "{}-{}{}".format(new_fpath, version,
                                            self._backup_suffix)
        return backup_fpath

    def get_defaults_path_name_from_version(self, old_version=None):
        """
        Get defaults location based on version.

        To be overridden if versions changed defaults location.
        """
        version = old_version if old_version else self._version
        defaults_path = osp.join(osp.dirname(self.get_config_fpath()),
                                 self._defaults_folder)
        name = '{}-{}-{}'.format(
            self._defaults_name_prefix,
            self._name,
            version,
        )
        if not osp.isdir(defaults_path):
            os.makedirs(defaults_path)

        return defaults_path, name

    def apply_configuration_patches(self, old_version=None):
        """
        Apply any patch to configuration values on version changes.

        To be overridden if patches to configuration values are needed.
        """
        pass

    # --- Public API
    # ------------------------------------------------------------------------
    def get_version(self, version='0.0.0'):
        """Return configuration (not application!) version."""
        return self.get(self.DEFAULT_SECTION_NAME, 'version', version)

    def set_version(self, version='0.0.0', save=True):
        """Set configuration (not application!) version."""
        version = self._check_version(version)
        self.set(self.DEFAULT_SECTION_NAME, 'version', version, save=save)

    def reset_to_defaults(self, save=True, verbose=False, section=None):
        """Reset config to Default values."""
        for sec, options in self.defaults:
            if section == None or section == sec:
                for option in options:
                    value = options[option]
                    self._set(sec, option, value, verbose)
        if save:
            self._save()

    def set_as_defaults(self):
        """Set defaults from the current config."""
        self.defaults = []
        for section in self.sections():
            secdict = {}
            for option, value in self.items(section, raw=self._raw):
                try:
                    value = ast.literal_eval(value)
                except (SyntaxError, ValueError):
                    pass
                secdict[option] = value
            self.defaults.append((section, secdict))

    def get_default(self, section, option):
        """
        Get default value for a given `section` and `option`.

        This is useful for type checking in `get` method.
        """
        section = self._check_section_option(section, option)
        for sec, options in self.defaults:
            if sec == section:
                if option in options:
                    value = options[option]
                    break
        else:
            value = NoDefault

        return value

    def get(self, section, option, default=NoDefault):
        """
        Get an option.

        Parameters
        ----------
        section: str
            Section name. If `None` is provide use the default section name.
        option: str
            Option name for `section`.
        default:
            Default value (if not specified, an exception will be raised if
            option doesn't exist).
        """
        section = self._check_section_option(section, option)

        if not self.has_section(section):
            if default is NoDefault:
                raise cp.NoSectionError(section)
            else:
                self.add_section(section)

        if not self.has_option(section, option):
            if default is NoDefault:
                raise cp.NoOptionError(option, section)
            else:
                self.set(section, option, default)
                return default

        value = super(UserConfig, self).get(section, option, raw=self._raw)

        default_value = self.get_default(section, option)
        if isinstance(default_value, bool):
            value = ast.literal_eval(value)
        elif isinstance(default_value, float):
            value = float(value)
        elif isinstance(default_value, int):
            value = int(value)
        elif is_text_string(default_value):
            pass
        else:
            try:
                # Lists, tuples, ...
                value = ast.literal_eval(value)
            except (SyntaxError, ValueError):
                pass

        return value

    def set_default(self, section, option, default_value):
        """
        Set Default value for a given `section`, `option`.

        If no defaults exist, no default is created. To be able to set
        defaults, a call to set_as_defaults is needed to create defaults
        based on current values.
        """
        section = self._check_section_option(section, option)
        for sec, options in self.defaults:
            if sec == section:
                options[option] = default_value

    def set(self, section, option, value, verbose=False, save=True):
        """
        Set an `option` on a given `section`.

        If section is None, the `option` is added to the default section.
        """
        section = self._check_section_option(section, option)
        default_value = self.get_default(section, option)

        if default_value is NoDefault:
            default_value = value
            self.set_default(section, option, default_value)

        if isinstance(default_value, bool):
            value = bool(value)
        elif isinstance(default_value, float):
            value = float(value)
        elif isinstance(default_value, int):
            value = int(value)
        elif not is_text_string(default_value):
            value = repr(value)

        self._set(section, option, value, verbose)
        if save:
            self._save()

    def remove_section(self, section):
        """Remove `section` and all options within it."""
        super(UserConfig, self).remove_section(section)
        self._save()

    def remove_option(self, section, option):
        """Remove `option` from `section`."""
        super(UserConfig, self).remove_option(section, option)
        self._save()

    def cleanup(self):
        """Remove .ini file associated to config."""
        os.remove(self.get_config_fpath())

    def to_list(self):
        """
        Return in list format.

        The format is [('section1', {'opt-1': value, ...}),
                       ('section2', {'opt-2': othervalue, ...}), ...]
        """
        new_defaults = []
        self._load_from_ini(self.get_config_fpath())
        for section in self._sections:
            sec_data = {}
            for (option, _) in self.items(section):
                sec_data[option] = self.get(section, option)
            new_defaults.append((section, sec_data))

        return new_defaults


class SpyderUserConfig(UserConfig):

    def get_previous_config_fpath(self):
        """
        Override method.

        Return the last configuration file used if found.
        """
        fpath = self.get_config_fpath()

        # We don't need to add the contents of the old spyder.ini to
        # the configuration of external plugins. This was the cause
        # of  part two (the shortcut conflicts) of issue
        # spyder-ide/spyder#11132
        if self._external_plugin:
            previous_paths = [fpath]
        else:
            previous_paths = [
                # >= 51.0.0
                fpath,
                # < 51.0.0
                os.path.join(get_conf_path(), 'spyder.ini'),
            ]

        for fpath in previous_paths:
            if osp.isfile(fpath):
                break

        return fpath

    def get_config_fpath_from_version(self, version=None):
        """
        Override method.

        Return the configuration path for given version.

        If no version is provided, it returns the current file path.
        """
        if version is None or self._external_plugin:
            fpath = self.get_config_fpath()
        elif check_version(version, '51.0.0', '<'):
            fpath = osp.join(get_conf_path(), 'spyder.ini')
        else:
            fpath = self.get_config_fpath()

        return fpath

    def get_backup_fpath_from_version(self, version=None, old_version=None):
        """
        Override method.

        Make a backup of the configuration file.
        """
        if old_version and check_version(old_version, '51.0.0', '<'):
            name = 'spyder.ini'
            fpath = os.path.join(get_conf_path(), name)
            if version is None:
                backup_fpath = "{}{}".format(fpath, self._backup_suffix)
            else:
                backup_fpath = "{}-{}{}".format(fpath, version,
                                                self._backup_suffix)
        else:
            super_class = super(SpyderUserConfig, self)
            backup_fpath = super_class.get_backup_fpath_from_version(
                version, old_version)

        return backup_fpath

    def get_defaults_path_name_from_version(self, old_version=None):
        """
        Override method.

        Get defaults location based on version.
        """
        if old_version:
            if check_version(old_version, '51.0.0', '<'):
                name = '{}-{}'.format(self._defaults_name_prefix, old_version)
                path = osp.join(get_conf_path(), 'defaults')
            else:
                super_class = super(SpyderUserConfig, self)
                path, name = super_class.get_defaults_path_name_from_version(
                    old_version)
        else:
            super_class = super(SpyderUserConfig, self)
            path, name = super_class.get_defaults_path_name_from_version()

        return path, name

    def apply_configuration_patches(self, old_version=None):
        """
        Override method.

        Apply any patch to configuration values on version changes.
        """
        self._update_defaults(self.defaults, old_version)

        if self._external_plugin:
            return
        if old_version and check_version(old_version, '44.1.0', '<'):
            run_lines = to_text_string(self.get('ipython_console',
                                                'startup/run_lines'))
            if run_lines is not NoDefault:
                run_lines = run_lines.replace(',', '; ')
                self.set('ipython_console', 'startup/run_lines', run_lines)


class MultiUserConfig(object):
    """
    Multiuser config class which emulates the basic UserConfig interface.

    This class provides the same basic interface as UserConfig but allows
    splitting the configuration sections and options among several files.

    The `name` is now a `name_map` where the sections and options per file name
    are defined.
    """
    DEFAULT_FILE_NAME = 'spyder'

    def __init__(self, name_map, path, defaults=None, load=True, version=None,
                 backup=False, raw_mode=False, remove_obsolete=False,
                 external_plugin=False):
        """Multi user config class based on UserConfig class."""
        self._name_map = self._check_name_map(name_map)
        self._path = path
        self._defaults = defaults
        self._load = load
        self._version = version
        self._backup = backup
        self._raw_mode = 1 if raw_mode else 0
        self._remove_obsolete = remove_obsolete
        self._external_plugin = external_plugin

        self._configs_map = {}
        self._config_defaults_map = self._get_defaults_for_name_map(defaults,
                                                                    name_map)
        self._config_kwargs = {
            'path': path,
            'defaults': defaults,
            'load': load,
            'version': version,
            'backup': backup,
            'raw_mode': raw_mode,
            'remove_obsolete': False,  # This will be handled later on if True
            'external_plugin': external_plugin
        }

        for name in name_map:
            defaults = self._config_defaults_map.get(name)
            mod_kwargs = {
                'name': name,
                'defaults': defaults,
            }
            new_kwargs = self._config_kwargs.copy()
            new_kwargs.update(mod_kwargs)
            config_class = self.get_config_class()
            self._configs_map[name] = config_class(**new_kwargs)

        # Remove deprecated options if major version has changed
        default_config = self._configs_map.get(self.DEFAULT_FILE_NAME)
        major_ver = default_config._get_major_version(version)
        major_old_ver = default_config._get_major_version(
            default_config._old_version)

        # Now we can apply remove_obsolete
        if remove_obsolete or major_ver != major_old_ver:
            for _, config in self._configs_map.items():
                config._remove_deprecated_options(config._old_version)

    def _get_config(self, section, option):
        """Get the correct configuration based on section and option."""
        # Check the filemap first
        name = self._get_name_from_map(section, option)
        config_value = self._configs_map.get(name, None)

        if config_value is None:
            config_value = self._configs_map[self.DEFAULT_FILE_NAME]
        return config_value

    def _check_name_map(self, name_map):
        """Check `name_map` follows the correct format."""
        # Check section option paris are not repeated
        sections_options = []
        for _, sec_opts in name_map.items():
            for section, options in sec_opts:
                for option in options:
                    sec_opt = (section, option)
                    if sec_opt not in sections_options:
                        sections_options.append(sec_opt)
                    else:
                        error_msg = (
                            'Different files are holding the same '
                            'section/option: "{}/{}"!'.format(section, option)
                        )
                        raise ValueError(error_msg)
        return name_map

    @staticmethod
    def _get_section_from_defaults(defaults, section):
        """Get the section contents from the defaults."""
        for sec, options in defaults:
            if section == sec:
                value = options
                break
        else:
            raise ValueError('section "{}" not found!'.format(section))

        return value

    @staticmethod
    def _get_option_from_defaults(defaults, section, option):
        """Get the section,option value from the defaults."""
        value = NoDefault
        for sec, options in defaults:
            if section == sec:
                value = options.get(option, NoDefault)
                break
        return value

    @staticmethod
    def _remove_section_from_defaults(defaults, section):
        """Remove section from defaults."""
        idx_remove = None
        for idx, (sec, _) in enumerate(defaults):
            if section == sec:
                idx_remove = idx
                break

        if idx_remove is not None:
            defaults.pop(idx)

    @staticmethod
    def _remove_option_from_defaults(defaults, section, option):
        """Remove section,option from defaults."""
        for sec, options in defaults:
            if section == sec:
                if option in options:
                    options.pop(option)
                    break

    def _get_name_from_map(self, section=None, option=None):
        """
        Search for section and option on the name_map and return the name.
        """
        for name, sec_opts in self._name_map.items():
            # Ignore the main section
            default_sec_name = self._configs_map.get(name).DEFAULT_SECTION_NAME
            if name == default_sec_name:
                continue
            for sec, options in sec_opts:
                if sec == section:
                    if len(options) == 0:
                        return name
                    else:
                        for opt in options:
                            if opt == option:
                                return name

    @classmethod
    def _get_defaults_for_name_map(cls, defaults, name_map):
        """Split the global defaults using the name_map."""
        name_map_config = {}
        defaults_copy = copy.deepcopy(defaults)

        for name, sec_opts in name_map.items():
            default_map_for_name = []
            if len(sec_opts) == 0:
                name_map_config[name] = defaults_copy
            else:
                for section, options in sec_opts:
                    if len(options) == 0:
                        # Use all on section
                        sec = cls._get_section_from_defaults(defaults_copy,
                                                             section)

                        # Remove section from defaults
                        cls._remove_section_from_defaults(defaults_copy,
                                                          section)

                    else:
                        # Iterate and pop!
                        sec = {}
                        for opt in options:
                            val = cls._get_option_from_defaults(defaults_copy,
                                                                section, opt)
                            if val is not NoDefault:
                                sec[opt] = val

                            # Remove option from defaults
                            cls._remove_option_from_defaults(defaults_copy,
                                                             section, opt)

                    # Add to config map
                    default_map_for_name.append((section, sec))

                name_map_config[name] = default_map_for_name

        return name_map_config

    def get_config_class(self):
        """Return the UserConfig class to use."""
        return SpyderUserConfig

    def sections(self):
        """Return all sections of the configuration file."""
        sections = set()
        for _, config in self._configs_map.items():
            for section in config.sections():
                sections.add(section)

        return list(sorted(sections))

    def items(self, section):
        """Return all the items option/values for the given section."""
        config = self._get_config(section, None)
        if config is None:
            config = self._configs_map[self.DEFAULT_FILE_NAME]

        if config.has_section(section):
            return config.items(section=section)
        else:
            return None

    def options(self, section):
        """Return all the options for the given section."""
        config = self._get_config(section, None)
        return config.options(section=section)

    def get_default(self, section, option):
        """
        Get Default value for a given `section` and `option`.

        This is useful for type checking in `get` method.
        """
        config = self._get_config(section, option)
        if config is None:
            config = self._configs_map[self.DEFAULT_FILE_NAME]
        return config.get_default(section, option)

    def get(self, section, option, default=NoDefault):
        """
        Get an `option` on a given `section`.

        If section is None, the `option` is requested from default section.
        """
        config = self._get_config(section, option)

        if config is None:
            config = self._configs_map[self.DEFAULT_FILE_NAME]

        return config.get(section=section, option=option, default=default)

    def set(self, section, option, value, verbose=False, save=True):
        """
        Set an `option` on a given `section`.

        If section is None, the `option` is added to the default section.
        """
        config = self._get_config(section, option)
        config.set(section=section, option=option, value=value,
                   verbose=verbose, save=save)

    def reset_to_defaults(self, section=None):
        """Reset configuration to Default values."""
        for _, config in self._configs_map.items():
            config.reset_to_defaults(section=section)

    def remove_section(self, section):
        """Remove `section` and all options within it."""
        config = self._get_config(section, None)
        config.remove_section(section)

    def remove_option(self, section, option):
        """Remove `option` from `section`."""
        config = self._get_config(section, option)
        config.remove_option(section, option)

    def cleanup(self):
        """Remove .ini files associated to configurations."""
        for _, config in self._configs_map.items():
            os.remove(config.get_config_fpath())


class PluginConfig(UserConfig):
    """Plugin configuration handler."""


class PluginMultiConfig(MultiUserConfig):
    """Plugin configuration handler with multifile support."""

    def get_config_class(self):
        return PluginConfig
