# Copyright (c) 2017-2018 The University of Manchester
#
# This program 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

import inspect

try:
    # pylint: disable=no-member
    _introspector = inspect.getfullargspec  # @UndefinedVariable
except AttributeError:
    _introspector = inspect.getargspec


class overrides(object):
    """ A decorator for indicating that a method overrides another method in\
        a superclass.  This checks that the method does actually exist,\
        copies the doc-string for the method, and enforces that the method\
        overridden is specified, making maintenance easier.
    """

    __slots__ = [
        # The method in the superclass that this method overrides
        "_superclass_method",
        # True if the doc string is to be extended, False to set if not set
        "_extend_doc",
        # Any additional arguments required by the subclass method
        "_additional_arguments",
        # True if the subclass method may have additional defaults
        "_extend_defaults",
        # True if the name check is relaxed
        "_relax_name_check",
        # The name of the thing being overridden for error messages
        "_override_name"
    ]

    def __init__(
            self, super_class_method, extend_doc=True,
            additional_arguments=None, extend_defaults=False):
        """
        :param super_class_method: The method to override in the superclass
        :param extend_doc:\
            True the method doc string should be appended to the super-method\
            doc string, False if the documentation should be set to the\
            super-method doc string only if there isn't a doc string already
        :param additional_arguments:\
            Additional arguments taken by the subclass method over the\
            superclass method, e.g., that are to be injected
        :param extend_defaults: \
            Whether the subclass may specify extra defaults for the parameters
        """
        self._superclass_method = super_class_method
        self._extend_doc = extend_doc
        self._additional_arguments = additional_arguments
        self._extend_defaults = extend_defaults
        self._relax_name_check = False
        self._override_name = "super class method"
        if additional_arguments is None:
            self._additional_arguments = {}
        if isinstance(super_class_method, property):
            self._superclass_method = super_class_method.fget

    @staticmethod
    def __match_defaults(default_args, super_defaults, extend_ok):
        if default_args is None:
            return super_defaults is None
        elif super_defaults is None:
            return extend_ok
        if extend_ok:
            return len(default_args) >= len(super_defaults)
        return len(default_args) == len(super_defaults)

    def __verify_method_arguments(self, method):
        """ Check that the arguments match. """
        method_args = _introspector(method)
        super_args = _introspector(self._superclass_method)
        all_args = [
            arg for arg in method_args.args
            if arg not in self._additional_arguments]
        default_args = None
        if method_args.defaults is not None:
            default_args = [
                arg for arg in method_args.defaults
                if arg not in self._additional_arguments]
        if len(all_args) != len(super_args.args):
            raise AttributeError(
                "Method has {} arguments but {} has {}"
                " arguments".format(
                    len(method_args.args), self._override_name,
                    len(super_args.args)))
        for arg, super_arg in zip(all_args, super_args.args):
            if arg != super_arg:
                raise AttributeError(
                    "Missing argument {}".format(super_arg))
        if not self.__match_defaults(
                default_args, super_args.defaults, self._extend_defaults):
            raise AttributeError(
                "Default arguments don't match {}".format(
                    self._override_name))

    def __call__(self, method):

        # Check and fail if this is a property
        if isinstance(method, property):
            raise AttributeError(
                "Please ensure that the {} decorator is the last"
                " decorator before the method declaration".format(
                    self.__class__.__name__))

        # Check that the name matches
        if (not self._relax_name_check and
                method.__name__ != self._superclass_method.__name__):
            raise AttributeError(
                "{} name {} does not match {}. "
                "Ensure {} is the last decorator before the method "
                "declaration".format(
                    self._override_name, self._superclass_method.__name__,
                    method.__name__, self.__class__.__name__))

        # Check that the arguments match (except for __init__ as this might
        # take extra arguments or pass arguments not specified)
        if method.__name__ != "__init__":
            self.__verify_method_arguments(method)

        if (self._superclass_method.__doc__ is not None and
                method.__doc__ is None):
            method.__doc__ = self._superclass_method.__doc__
        elif (self._extend_doc and
                self._superclass_method.__doc__ is not None):
            method.__doc__ = (
                self._superclass_method.__doc__ + method.__doc__)
        return method
