# -*- coding:utf-8 -*-

"""
This file is part of OpenSesame.

OpenSesame is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

OpenSesame is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with OpenSesame.  If not, see <http://www.gnu.org/licenses/>.
"""
from libopensesame.py3compat import *
from openexp import resources
from functools import partial
import inspect
try:
    from yamldoc._functiondoc import FunctionDoc
    from yamldoc import inherit as docinherit
except:
    FunctionDoc = None
    docinherit = type

_backend_info = None
_backend_types = [u'canvas', u'keyboard', u'mouse', u'sampler', u'color',
                  u'clock', u'log']


def backend_info():
    """
    arguments:
            experiment:
                    desc:	The experiment object.
                    type:	experiment

    returns:
            desc:	A dictionary with backend information.
            type:	dict
    """
    global _backend_info
    if _backend_info is None:
        with safe_open(resources['backend_info.yaml']) as fd:
            _backend_info = safe_yaml_load(fd.read())
    return _backend_info


def backend_guess(experiment, _type):
    r"""Guesses the backend of a specific type.

    Parameters
    ----------
    experiment : experiment
        The experiment object.
    _type : str
        A backend type, e.g. 'canvas'

    Returns
    -------
    str
        A backend, e.g. 'legacy'
    """
    if u'%s_backend' % _type in experiment.var:
        return experiment.var.get(u'%s_backend' % _type)
    d = backend_info()
    return d[experiment.var.canvas_backend][_type]


def backend_match(experiment):
    r"""Tries to determine which combination of backends is used by the
    experiment.

    Parameters
    ----------
    experiment : experiment
        The experiment object.

    Returns
    -------
    str
        The name of a backend combination, e.g. 'legacy'.
    """
    for name, info in backend_info().items():
        for _type in _backend_types:
            if backend_guess(experiment, _type) != info[_type]:
                break
        else:
            return name
    return u'custom'


def get_backend_mod(experiment, _type):
    r"""Gets a backend module.

    Parameters
    ----------
    experiment : experiment
        The experiment object.
    _type : str
        The backend type, e.g. u'canvas'.

    Returns
    -------
    module
        A module that contains the backend.
    """
    name = backend_guess(experiment, _type)
    return __import__('openexp._%s.%s' % (_type, name), fromlist=['dummy'])


def get_backend_class(experiment, _type):
    r"""Gets a backend class.

    Parameters
    ----------
    _type : str
        The backend type, e.g. u'canvas'.
    experiment : experiment
        The experiment object.

    Returns
    -------
    type
        A backend class.
    """
    name = backend_guess(experiment, _type)
    return getattr(get_backend_mod(experiment, _type), name)


def configurable(fnc):
    r"""A decorator that makes a function configurable.

    Parameters
    ----------
    fnc
        The function to make configurable.

    Returns
    -------
    A decorated configurable function.
    """
    def inner(self, *arglist, **kwdict):

        cfg = {}
        for key, val in list(kwdict.items()):
            if key in self.configurables:
                cfg[key] = val
                del kwdict[key]
        if len(cfg) > 0:
            old_cfg = self.get_config()
            self.set_config(**cfg)
        retval = fnc(self, *arglist, **kwdict)
        if len(cfg) > 0:
            self.set_config(**old_cfg)
        return retval
    # We need to copy the docstring and argument specification, otherwise using
    # this decorator will break the documentation functions.
    inner.__doc__ = fnc.__doc__
    inner.__argspec__ = inspect.getfullargspec(fnc)
    inner.__name__ = fnc.__name__
    if FunctionDoc is not None:
        inner._dict = FunctionDoc(fnc)._dict()
    return inner


def getter(key, self):
    r"""A getter function for configurable properties."""
    return self.__cfg__[key]


def setter(key, self, val):
    r"""A setter function for configurable properties."""
    self.set_config(**{key: val})


def deller():
    r"""A dummy delete function for configurable properties."""
    pass


def docstring(key):
    """
    returns:
            A docstring for configurable properties.
    """
    return """
	name:
		%(key)s
	desc:
		This is a read-and-write property. Changing the `%(key)s` property will
		affect subsequent operations.
	""" % {u'key': key}


class Backend:

    r"""A base backend that provides the configurable framework."""
    docinherit = type

    def __init__(self, configurables={}, **cfg):
        r"""Constructor.

        Parameters
        ----------
        configurables : dict, optional
            A dict of configurable properties with the name of the property as
            a key, and a validation function (which can be None) as value.
        **cfg : dict
            The initial values for all the configurable properties.
        """
        self.configurables = configurables
        for key in configurables:
            if hasattr(backend, key):
                continue
            fnc = property(
                partial(getter, key),
                partial(setter, key),
                deller,
                docstring(key))
            setattr(self.__class__, key, fnc)
        self.__cfg__ = {}
        _cfg = self.default_config().copy()
        _cfg.update(cfg)
        self.set_config(**_cfg)

    @classmethod
    def assert_list_or_None(cls, key, val):
        r"""Asserts that a value is a list or None.

        Parameters
        ----------
        key
            The name of the configurable.
        val
            The value of the configurable.
        """
        if not val is None and not isinstance(val, list):
            raise TypeError(
                u'%s should be a list or None, not %s' % (key, val))

    @classmethod
    def assert_bool(cls, key, val):
        r"""Asserts that a value is bool.

        Parameters
        ----------
        key
            The name of the configurable.
        val
            The value of the configurable.
        """
        if not isinstance(val, bool) and not isinstance(val, int):
            raise TypeError(
                u'%s should be True or False, not %s' % (key, val))

    @classmethod
    def assert_numeric_or_None(cls, key, val):
        r"""Asserts that a value is float, int, or None.

        Parameters
        ----------
        key
            The name of the configurable.
        val
            The value of the configurable.
        """
        if not val is None and not isinstance(val, int) and \
                not isinstance(val, float):
            raise TypeError(
                u'%s should be numeric (float or int) or None, not %s'
                % (key, val))

    @classmethod
    def assert_numeric(cls, key, val):
        r"""Asserts that a value is float or int.

        Parameters
        ----------
        key
            The name of the configurable.
        val
            The value of the configurable.
        """
        if not isinstance(val, int) and not isinstance(val, float):
            raise TypeError(
                u'%s should be numeric (float or int), not %s' % (key, val))

    @classmethod
    def assert_string(cls, key, val):
        r"""Asserts that a value is string or unicode.

        Parameters
        ----------
        key
            The name of the configurable.
        val
            The value of the configurable.
        """
        if not isinstance(val, str):
            raise TypeError(
                u'%s should be string (str or unicode), not %s' % (key, val))

    def get_config(self):
        """{nodoc}
        visible:	False

        returns:
                desc:	A dict that contains all configurables with names as keys
                                and values as values.
                type:	dict
        """
        return self.__cfg__.copy()

    def set_config(self, **cfg):
        r"""Updates the configurables.

        Parameters
        ----------
        **cfg : dict
            The to-be-updated configurables.
        """
        for key in cfg:
            if key not in self.configurables:
                raise RuntimeError(u'Unknown argument: %s' % key)
            assert_fnc = self.configurables[key]
            if assert_fnc is not None:
                assert_fnc(key, cfg[key])
        self.__cfg__.update(cfg)
        # Check whether the config is valid
        for key in self.configurables:
            if key not in self.__cfg__:
                raise RuntimeError(u'Invalid config: %s' % str(self.__cfg__))

    def default_config(self):
        """
        visible:	False

        returns:
                desc:	The default configurable values, with names as keys and
                                values as values.
                type:	dict
        """
        return {}


# Non PEP-8 alias for backwards compatibility
backend = Backend
