'''Registry of user interface widgets.

Format {"tool_key" : {"toolkit" : <function(obj, display, **kwargs)>}}

The ``tool_key` is defined by the "model function" to which the widget provides
and user interface. That function gets the widget function from this registry
and executes it passing the ``obj``, ``display`` and any extra keyword
arguments. When ``display`` is true, ``function`` displays the widget. If
``False`` it returns a dictionary with whatever is needed to display the
widgets externally (usually for testing or customisation purposes).

'''

import functools
import types
import importlib

from hyperspy.misc.utils import isiterable
from hyperspy.extensions import ALL_EXTENSIONS


UI_REGISTRY = {toolkey: {} for toolkey in ALL_EXTENSIONS["GUI"]["toolkeys"]}

TOOLKIT_REGISTRY = set()
KNOWN_TOOLKITS = set(("ipywidgets", "traitsui"))


if "widgets" in ALL_EXTENSIONS["GUI"] and ALL_EXTENSIONS["GUI"]["widgets"]:
    for toolkit, widgets in ALL_EXTENSIONS["GUI"]["widgets"].items():
        TOOLKIT_REGISTRY.add(toolkit)
        for toolkey, specs in widgets.items():
            if not toolkey in UI_REGISTRY:
                raise NameError("%s is not a registered toolkey" % toolkey)
            UI_REGISTRY[toolkey][toolkit] = specs


def _toolkits_to_string(toolkits):
    if isinstance(toolkits, str):
        return "{} toolkit".format(toolkits)
    else:
        toolkits = tuple(toolkits)
        if len(toolkits) == 1:
            return "{} toolkit".format(toolkits[0])

        elif len(toolkits) == 2:
            return " and ".join(toolkits) + " toolkits"
        else:  # > 2
            txt = ", ".join(toolkits[:-1])
            return txt + " and {}".format(toolkits[-1]) + " toolkits"


def get_gui(self, toolkey, display=True, toolkit=None, **kwargs):
    if not TOOLKIT_REGISTRY:
        raise ImportError(
            "No toolkit registered. Install hyperspy_gui_ipywidgets or "
            "hyperspy_gui_traitsui GUI elements. If hyperspy_gui_traits"
            "is installed, initialize a toolkit supported by traitsui "
            "before importing HyperSpy."
        )
    from hyperspy.defaults_parser import preferences
    if isinstance(toolkit, str):
        toolkit = (toolkit,)
    if isiterable(toolkit):
        toolkits = set()
        for tk in toolkit:
            if tk in TOOLKIT_REGISTRY:
                toolkits.add(tk)
            else:
                raise ValueError(
                    "{} is not a registered toolkit.".format(tk)
                )
    elif toolkit is None:
        toolkits = set()
        available_disabled_toolkits = set()
        if "ipywidgets" in TOOLKIT_REGISTRY:
            if preferences.GUIs.enable_ipywidgets_gui:
                toolkits.add("ipywidgets")
            else:
                available_disabled_toolkits.add("ipywidgets")
        if "traitsui" in TOOLKIT_REGISTRY:
            if preferences.GUIs.enable_traitsui_gui:
                toolkits.add("traitsui")
            else:
                available_disabled_toolkits.add("traitsui")
        if not toolkits and available_disabled_toolkits:
            is_or_are = "is" if len(
                available_disabled_toolkits) == 1 else "are"
            them_or_it = ("it" if len(available_disabled_toolkits) == 1
                          else "them")
            raise ValueError(
                "No toolkit available. The {} {} installed but "
                "disabled in `preferences`. Enable {} in `preferences` or "
                "manually select a toolkit with the `toolkit` argument.".format(
                    _toolkits_to_string(available_disabled_toolkits),
                    is_or_are, them_or_it)
            )

    else:
        raise ValueError(
            "`toolkit` must be a string, an iterable of strings or None.")
    if toolkey not in UI_REGISTRY or not UI_REGISTRY[toolkey]:
        propose = KNOWN_TOOLKITS - TOOLKIT_REGISTRY
        if propose:
            propose = ["hyperspy_gui_{}".format(tk) for tk in propose]
            if len(propose) > 1:
                propose_ = ", ".join(propose[:-1])
                propose = propose_ + " and/or {}".format(propose[-1])
            else:
                propose = propose.pop()
        raise NotImplementedError(
            "There is no user interface registered for this feature."
            "Try installing {}.".format(propose))
    if not display:
        widgets = {}
    available_toolkits = set()
    used_toolkits = set()
    for toolkit, specs in UI_REGISTRY[toolkey].items():
        f = getattr(
            importlib.import_module(
                specs["module"]),
            specs["function"])
        if toolkit in toolkits:
            used_toolkits.add(toolkit)
            thisw = f(obj=self, display=display, **kwargs)
            if not display:
                widgets[toolkit] = thisw
        else:
            available_toolkits.add(toolkit)
    if not used_toolkits and available_toolkits:
        is_or_are = "is" if len(toolkits) == 1 else "are"
        raise NotImplementedError(
            "The {} {} not available for this functionality,try with "
            "the {}.".format(
                _toolkits_to_string(toolkits),
                is_or_are,
                _toolkits_to_string(available_toolkits)))
    if not display:
        return widgets


def get_partial_gui(toolkey):
    def pg(self, display=True, toolkit=None, **kwargs):
        return get_gui(self, toolkey=toolkey, display=display,
                       toolkit=toolkit, **kwargs)
    return pg


DISPLAY_DT = """display : bool
            If True, display the user interface widgets. If False, return the
            widgets container in a dictionary, usually for customisation or
            testing."""

TOOLKIT_DT = """toolkit : str, iterable of strings or None
            If None (default), all available widgets are displayed or returned.
            If string, only the widgets of the selected toolkit are displayed
            if available. If an interable of toolkit strings, the widgets of
            all listed toolkits are displayed or returned."""
GUI_DT = """Display or return interactive GUI element if available.

Parameters
----------
%s
%s

""" % (DISPLAY_DT, TOOLKIT_DT)


def add_gui_method(toolkey):
    def decorator(cls):
        # Not using functools.partialmethod because it is not possible to set
        # the docstring that way.
        setattr(cls, "gui", get_partial_gui(toolkey))
        setattr(cls.gui, "__doc__", GUI_DT)
        return cls
    return decorator
