"""
Core functions and attributes for the matplotlib style library:

``use``
    Select style sheet to override the current matplotlib settings.
``context``
    Context manager to use a style sheet temporarily.
``available``
    List available style sheets.
``library``
    A dictionary of style names and matplotlib settings.
"""

import contextlib
import logging
import os
from pathlib import Path
import sys
import warnings

if sys.version_info >= (3, 10):
    import importlib.resources as importlib_resources
else:
    # Even though Py3.9 has importlib.resources, it doesn't properly handle
    # modules added in sys.path.
    import importlib_resources

import matplotlib as mpl
from matplotlib import _api, _docstring, _rc_params_in_file, rcParamsDefault

_log = logging.getLogger(__name__)

__all__ = ['use', 'context', 'available', 'library', 'reload_library']


BASE_LIBRARY_PATH = os.path.join(mpl.get_data_path(), 'stylelib')
# Users may want multiple library paths, so store a list of paths.
USER_LIBRARY_PATHS = [os.path.join(mpl.get_configdir(), 'stylelib')]
STYLE_EXTENSION = 'mplstyle'
# A list of rcParams that should not be applied from styles
STYLE_BLACKLIST = {
    'interactive', 'backend', 'webagg.port', 'webagg.address',
    'webagg.port_retries', 'webagg.open_in_browser', 'backend_fallback',
    'toolbar', 'timezone', 'figure.max_open_warning',
    'figure.raise_window', 'savefig.directory', 'tk.window_focus',
    'docstring.hardcopy', 'date.epoch'}
_DEPRECATED_SEABORN_STYLES = {
    s: s.replace("seaborn", "seaborn-v0_8")
    for s in [
        "seaborn",
        "seaborn-bright",
        "seaborn-colorblind",
        "seaborn-dark",
        "seaborn-darkgrid",
        "seaborn-dark-palette",
        "seaborn-deep",
        "seaborn-muted",
        "seaborn-notebook",
        "seaborn-paper",
        "seaborn-pastel",
        "seaborn-poster",
        "seaborn-talk",
        "seaborn-ticks",
        "seaborn-white",
        "seaborn-whitegrid",
    ]
}
_DEPRECATED_SEABORN_MSG = (
    "The seaborn styles shipped by Matplotlib are deprecated since %(since)s, "
    "as they no longer correspond to the styles shipped by seaborn. However, "
    "they will remain available as 'seaborn-v0_8-<style>'. Alternatively, "
    "directly use the seaborn API instead.")


@_docstring.Substitution(
    "\n".join(map("- {}".format, sorted(STYLE_BLACKLIST, key=str.lower)))
)
def use(style):
    """
    Use Matplotlib style settings from a style specification.

    The style name of 'default' is reserved for reverting back to
    the default style settings.

    .. note::

       This updates the `.rcParams` with the settings from the style.
       `.rcParams` not defined in the style are kept.

    Parameters
    ----------
    style : str, dict, Path or list

        A style specification. Valid options are:

        str
            - One of the style names in `.style.available` (a builtin style or
              a style installed in the user library path).

            - A dotted name of the form "package.style_name"; in that case,
              "package" should be an importable Python package name, e.g. at
              ``/path/to/package/__init__.py``; the loaded style file is
              ``/path/to/package/style_name.mplstyle``.  (Style files in
              subpackages are likewise supported.)

            - The path or URL to a style file, which gets loaded by
              `.rc_params_from_file`.

        dict
            A mapping of key/value pairs for `matplotlib.rcParams`.

        Path
            The path to a style file, which gets loaded by
            `.rc_params_from_file`.

        list
            A list of style specifiers (str, Path or dict), which are applied
            from first to last in the list.

    Notes
    -----
    The following `.rcParams` are not related to style and will be ignored if
    found in a style specification:

    %s
    """
    if isinstance(style, (str, Path)) or hasattr(style, 'keys'):
        # If name is a single str, Path or dict, make it a single element list.
        styles = [style]
    else:
        styles = style

    style_alias = {'mpl20': 'default', 'mpl15': 'classic'}

    for style in styles:
        if isinstance(style, str):
            style = style_alias.get(style, style)
            if style in _DEPRECATED_SEABORN_STYLES:
                _api.warn_deprecated("3.6", message=_DEPRECATED_SEABORN_MSG)
                style = _DEPRECATED_SEABORN_STYLES[style]
            if style == "default":
                # Deprecation warnings were already handled when creating
                # rcParamsDefault, no need to reemit them here.
                with _api.suppress_matplotlib_deprecation_warning():
                    # don't trigger RcParams.__getitem__('backend')
                    style = {k: rcParamsDefault[k] for k in rcParamsDefault
                             if k not in STYLE_BLACKLIST}
            elif style in library:
                style = library[style]
            elif "." in style:
                pkg, _, name = style.rpartition(".")
                try:
                    path = (importlib_resources.files(pkg)
                            / f"{name}.{STYLE_EXTENSION}")
                    style = _rc_params_in_file(path)
                except (ModuleNotFoundError, OSError, TypeError) as exc:
                    # There is an ambiguity whether a dotted name refers to a
                    # package.style_name or to a dotted file path.  Currently,
                    # we silently try the first form and then the second one;
                    # in the future, we may consider forcing file paths to
                    # either use Path objects or be prepended with "./" and use
                    # the slash as marker for file paths.
                    pass
        if isinstance(style, (str, Path)):
            try:
                style = _rc_params_in_file(style)
            except IOError as err:
                raise IOError(
                    f"{style!r} is not a valid package style, path of style "
                    f"file, URL of style file, or library style name (library "
                    f"styles are listed in `style.available`)") from err
        filtered = {}
        for k in style:  # don't trigger RcParams.__getitem__('backend')
            if k in STYLE_BLACKLIST:
                _api.warn_external(
                    f"Style includes a parameter, {k!r}, that is not "
                    f"related to style.  Ignoring this parameter.")
            else:
                filtered[k] = style[k]
        mpl.rcParams.update(filtered)


@contextlib.contextmanager
def context(style, after_reset=False):
    """
    Context manager for using style settings temporarily.

    Parameters
    ----------
    style : str, dict, Path or list
        A style specification. Valid options are:

        str
            - One of the style names in `.style.available` (a builtin style or
              a style installed in the user library path).

            - A dotted name of the form "package.style_name"; in that case,
              "package" should be an importable Python package name, e.g. at
              ``/path/to/package/__init__.py``; the loaded style file is
              ``/path/to/package/style_name.mplstyle``.  (Style files in
              subpackages are likewise supported.)

            - The path or URL to a style file, which gets loaded by
              `.rc_params_from_file`.
        dict
            A mapping of key/value pairs for `matplotlib.rcParams`.

        Path
            The path to a style file, which gets loaded by
            `.rc_params_from_file`.

        list
            A list of style specifiers (str, Path or dict), which are applied
            from first to last in the list.

    after_reset : bool
        If True, apply style after resetting settings to their defaults;
        otherwise, apply style on top of the current settings.
    """
    with mpl.rc_context():
        if after_reset:
            mpl.rcdefaults()
        use(style)
        yield


def update_user_library(library):
    """Update style library with user-defined rc files."""
    for stylelib_path in map(os.path.expanduser, USER_LIBRARY_PATHS):
        styles = read_style_directory(stylelib_path)
        update_nested_dict(library, styles)
    return library


def read_style_directory(style_dir):
    """Return dictionary of styles defined in *style_dir*."""
    styles = dict()
    for path in Path(style_dir).glob(f"*.{STYLE_EXTENSION}"):
        with warnings.catch_warnings(record=True) as warns:
            styles[path.stem] = _rc_params_in_file(path)
        for w in warns:
            _log.warning('In %s: %s', path, w.message)
    return styles


def update_nested_dict(main_dict, new_dict):
    """
    Update nested dict (only level of nesting) with new values.

    Unlike `dict.update`, this assumes that the values of the parent dict are
    dicts (or dict-like), so you shouldn't replace the nested dict if it
    already exists. Instead you should update the sub-dict.
    """
    # update named styles specified by user
    for name, rc_dict in new_dict.items():
        main_dict.setdefault(name, {}).update(rc_dict)
    return main_dict


class _StyleLibrary(dict):
    def __getitem__(self, key):
        if key in _DEPRECATED_SEABORN_STYLES:
            _api.warn_deprecated("3.6", message=_DEPRECATED_SEABORN_MSG)
            key = _DEPRECATED_SEABORN_STYLES[key]

        return dict.__getitem__(self, key)


# Load style library
# ==================
_base_library = read_style_directory(BASE_LIBRARY_PATH)
library = _StyleLibrary()
available = []


def reload_library():
    """Reload the style library."""
    library.clear()
    library.update(update_user_library(_base_library))
    available[:] = sorted(library.keys())


reload_library()
