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


"""Spyder global registries for actions, toolbuttons, toolbars and menus."""

# Standard library imports
import inspect
import logging
from typing import Any, Optional, Dict
import warnings
import weakref

logger = logging.getLogger(__name__)


def get_caller(func):
    """
    Get file and line where the methods that create actions, toolbuttons,
    toolbars and menus are called.
    """
    frames = []
    for frame in inspect.stack():
        if frame.code_context:
            code_context = frame.code_context[0]
        else:
            code_context = ''
        if func in code_context:
            frames.append(f'{frame.filename}:{frame.lineno}')
    frames = ', '.join(frames)
    return frames


class SpyderRegistry:
    """General registry for global references (per plugin) in Spyder."""

    def __init__(self, creation_func: str, obj_type: str = ''):
        self.registry_map = {}
        self.obj_type = obj_type
        self.creation_func = creation_func

    def register_reference(self, obj: Any, id_: str,
                           plugin: Optional[str] = None,
                           context: Optional[str] = None,
                           overwrite: Optional[bool] = False):
        """
        Register a reference `obj` for a given plugin name on a given context.

        Parameters
        ----------
        obj: Any
            Object to register as a reference.
        id_: str
            String identifier used to store the object reference.
        plugin: Optional[str]
            Plugin name used to store the reference. Should belong to
            :class:`spyder.api.plugins.Plugins`. If None, then the object will
            be stored under the global `main` key.
        context: Optional[str]
            Additional key used to store and identify the object reference.
            In any Spyder plugin implementation, this context may refer to an
            identifier of a widget. This context enables plugins to define
            multiple actions with the same key that live on different widgets.
            If None, this context will default to the special `__global`
            identifier.
        """
        plugin = plugin if plugin is not None else 'main'
        context = context if context is not None else '__global'

        plugin_contexts = self.registry_map.get(plugin, {})
        context_references = plugin_contexts.get(
            context, weakref.WeakValueDictionary())

        if id_ in context_references:
            try:
                frames = get_caller(self.creation_func)
                if not overwrite:
                    warnings.warn(
                        f'There already exists a '
                        f'reference {context_references[id_]} '
                        f'with id {id_} under the context {context} '
                        f'of plugin {plugin}. The new reference {obj} will '
                        f'overwrite the previous reference. Hint: {obj} '
                        f'should have a different id_. See {frames}')
            except (RuntimeError, KeyError):
                # Do not raise exception if a wrapped Qt Object was deleted.
                # Or if the object reference dissapeared concurrently.
                pass

        logger.debug(f'Registering {obj} ({id_}) under context {context} for '
                     f'plugin {plugin}')
        context_references[id_] = obj
        plugin_contexts[context] = context_references
        self.registry_map[plugin] = plugin_contexts

    def get_reference(self, id_: str,
                      plugin: Optional[str] = None,
                      context: Optional[str] = None) -> Any:
        """
        Retrieve a stored object reference under a given id of a specific
        context of a given plugin name.

        Parameters
        ----------
        id_: str
            String identifier used to retrieve the object.
        plugin: Optional[str]
            Plugin name used to store the reference. Should belong to
            :class:`spyder.api.plugins.Plugins`. If None, then the object will
            be retrieved from the global `main` key.
        context: Optional[str]
            Additional key that was used to store the object reference.
            In any Spyder plugin implementation, this context may refer to an
            identifier of a widget. This context enables plugins to define
            multiple actions with the same key that live on different widgets.
            If None, this context will default to the special `__global`
            identifier.

        Returns
        -------
        obj: Any
            The object that was stored under the given identifier.

        Raises
        ------
        KeyError
            If neither of `id_`, `plugin` or `context` were found in the
            registry.
        """
        plugin = plugin if plugin is not None else 'main'
        context = context if context is not None else '__global'

        plugin_contexts = self.registry_map[plugin]
        context_references = plugin_contexts[context]
        return context_references[id_]

    def get_references(self, plugin: Optional[str] = None,
                       context: Optional[str] = None) -> Dict[str, Any]:
        """
        Retrieve all stored object references under the context of a
        given plugin name.

        Parameters
        ----------
        plugin: Optional[str]
            Plugin name used to store the reference. Should belong to
            :class:`spyder.api.plugins.Plugins`. If None, then the object will
            be retrieved from the global `main` key.
        context: Optional[str]
            Additional key that was used to store the object reference.
            In any Spyder plugin implementation, this context may refer to an
            identifier of a widget. This context enables plugins to define
            multiple actions with the same key that live on different widgets.
            If None, this context will default to the special `__global`
            identifier.

        Returns
        -------
        objs: Dict[str, Any]
            A dict that contains the actions mapped by their corresponding
            identifiers.
        """
        plugin = plugin if plugin is not None else 'main'
        context = context if context is not None else '__global'

        plugin_contexts = self.registry_map.get(plugin, {})
        context_references = plugin_contexts.get(
            context, weakref.WeakValueDictionary())
        return context_references

    def reset_registry(self):
        self.registry_map = {}

    def __str__(self) -> str:
        return f'SpyderRegistry[{self.obj_type}, {self.registry_map}]'


ACTION_REGISTRY = SpyderRegistry('create_action', 'SpyderAction')
TOOLBUTTON_REGISTRY = SpyderRegistry('create_toolbutton', 'QToolButton')
TOOLBAR_REGISTRY = SpyderRegistry('create_toolbar', 'QToolBar')
MENU_REGISTRY = SpyderRegistry('create_menu', 'SpyderMenu')
