# Copyright (c) 2017-2019 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/>.

from six import add_metaclass
from spinn_utilities.abstract_base import AbstractBase, abstractmethod


@add_metaclass(AbstractBase)
class AbstractDict(object):
    """ Base class for the :py:class:`RangeDictionary` and *all* views.
    This allows the users to not have to worry if they have a view.
    """
    __slots__ = []

    @abstractmethod
    def get_value(self, key):
        """ Gets a single shared value for all IDs covered by this view.

        :param key: The key or keys to get the value of. Use None for all
        :type key: str or iterable(str) or None
        :return: If key is a str, this returns the single object.\
            If key is iterable (list, tuple, set, etc) of str (or None),\
            returns a dictionary object
        :raises spinn_utilities.ranged.MultipleValuesException:\
            If even one of the keys has multiple values set.\
            But not if other keys not asked for have multiple values
        """

    @abstractmethod
    def keys(self):
        """ Returns the keys in the dictionary

        :return: keys in the dict
        """

    @abstractmethod
    def set_value(self, key, value, use_list_as_value=False):
        """ Resets a already existing key to the new value. \
        All IDs in the whole range or view will have this key set.

        .. warning::
            This method does not allow adding keys.\
            Using `dict[str] =` will add a new key, but it is not supported\
            for views.

        .. warning::
            If a View is created over multiple ranges this method would\
            raise a `KeyError` if any the ranges does not have the key.\
            (Currently multiple ranges not yet supported.)

        :param key: key to value being set
        :type key: str
        :param value: any object
        :param use_list_as_value: True if the value *is* a list
        :raise KeyError: If a new key is being used.
        """

    @abstractmethod
    def ids(self):
        """ Returns the IDs in range or view.\
        If the view is setup with IDs out of numerical order the order used\
        to create the view is maintained.

        .. note::
            If indexing into a view, you are picking the X'th ID.\
            So if the IDs are `[2,3,4,5]` the `view[2]` will be the data for\
            ID `4` and not `2`

        :return: list of IDs
        :rtype: list(int)
        """

    @abstractmethod
    def iter_all_values(self, key, update_save=False):
        """ Iterates over the value(s) for all IDs covered by this view. \
        There will be one yield for each ID even if values are repeated.

        :param key: The key or keys to get the value of. Use None for all keys
        :type key: str or iterable(str) or None
        :param update_save: If set True the iteration will work even if values\
            are updated during iteration. If left False the iterator may be\
            faster but behaviour is *undefined* and *unchecked* if *any*\
            values are changed during iteration.
        :return: If key is a str, this yields single objects.\
            If key is iterable (list, tuple, set, etc) of str (or None),\
            yields dictionary objects
        """

    def get_ranges(self, key=None):
        """ Lists the ranges(s) for all IDs covered by this view. \
        There will be one yield for each range which may cover one or\
        more IDs.

        .. note::
            As the data is created in a single call this is not affected\
            by any updates.

        :param key: The key or keys to get the value of. Use None for all
        :type key: str or iterable(str) or None
        :return: List of tuples of (`start`, `stop`, `value`).\
            `start` is *inclusive* so is the first ID in the range.\
            `stop` is *exclusive* so is the last ID in the range + 1.\
            If `key` is a str, `value` is a single object.\
            If `key` is iterable (list, tuple, set, etc) of str (or None) \
            `value` is a dictionary object
        """
        return list(self.iter_ranges(key=key))

    @abstractmethod
    def iter_ranges(self, key=None):
        """ Iterates over the ranges(s) for all IDs covered by this view.\
        There will be one yield for each range which may cover one or\
        more IDs.

        .. warning::
            This iterator is *not* update safe!\
            Behaviour is *undefined* and *unchecked* if *any* values are\
            changed during iteration.

        :param key: The key or keys to get the value of. Use None for all
        :type key: str or iterable(str) or None
        :return: yields tuples of (`start`, `stop`, `value`).\
            `start` is *inclusive* so is the first ID in the range.\
            `stop` is *exclusive* so is the last ID in the range + 1.\
            If `key` is a str, `value` is a single object.\
            If `key` is iterable (list, tuple, set, etc) of str (or None),\
            `value` is a dictionary object
        """

    @abstractmethod
    def get_default(self, key):
        """ Gets the default value for a single key.\
        Unless changed, the default is the original value.

        .. note::
            Does not change any values but only changes what ``reset_value``\
            would do

        :param key: Existing dict key
        :type key: str
        :return: default for this key.
        """

    def items(self):
        """ Returns a list of (``key``, ``value``) tuples.
        Works only if the whole ranges/view has single values.

        If the key is a str, the values are single objects.\
        If the key is iterable (list, tuple, set, etc) of str (or None),\
        the values are dictionary objects

        :return: List of (``key``, ``value``) tuples
        :raises spinn_utilities.ranged.MultipleValuesException: \
            If even one of the keys has multiple values set.
        """
        results = []
        for key in self.keys():
            value = self.get_value(key)
            results.append((key, value))
        return results

    def iteritems(self):
        """ Iterates over the (``key``, ``value``) tuples. \
        Works only if the whole ranges/view has single values.

        If the key is a str, the values are single objects.\
        If the key is iterable (list, tuple, set, etc) of str (or None),\
        the values are dictionary objects

        This function is safe for value updates but may miss new keys\
        added during iteration.

        :return: yield (``key``, ``value``) tuples
        :raises spinn_utilities.ranged.MultipleValuesException: \
            If even one of the keys has multiple values set.
        """
        for key in self.keys():
            yield (key, self.get_value(key))

    def values(self):
        """ Returns a list of values.\
        Works only if the whole ranges/view has single values.

        If key is a str, the values are single objects.\
        If key is iterable (list, tuple, set, etc) of str (or None),\
        values are dictionary objects

        :return: List of values
        :raises spinn_utilities.ranged.MultipleValuesException: \
            If even one of the keys has multiple values set.
        """
        results = []
        for key in self.keys():
            value = self.get_value(key)
            results.append(value)
        return results

    def itervalues(self):
        """ Iterates over the values.\
        Works only if the whole ranges/view has single values.

        If key is a str, the values are single objects.\
        If key is iterable (list, tuple, set, etc) of str (or None),\
        values are dictionary objects

        This function is safe for value updates but may miss new keys\
        added during iteration.

        :return: yield values
        :raises spinn_utilities.ranged.MultipleValuesException: \
            If even one of the keys has multiple values set.
        """
        for key in self.keys():
            yield self.get_value(key)

    def __contains__(self, key):
        """ Checks if the key is a dictionary key or a range ID.

        :param key: Dictionary key or ID to check
        :type key: str or int
        :return: True if the str key is one of the dict keys or\
            if the int key is one of the range IDs. Otherwise False
        :rtype: bool
        """
        if isinstance(key, str):
            return key in self.keys()
        if isinstance(key, int):
            return key in self.ids()
        raise KeyError("Unexpected key type: {}".format(type(key)))

    def has_key(self, key):
        """ As the Deprecated dict ``has_keys`` function.

        .. note::
            Int keys to IDs are not supported.

        :param key: the key
        :type key: str
        :return: If the key is in dict
        :rtype: bool
        """
        return key in self.keys()

    def reset(self, key):
        """ Sets the value(s) for a single key back to the default value.

        :param key: Existing dict key
        :type key: str
        :param default: Value to be used by reset
        """
        self.set_value(key, self.get_default(key=key))
