#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright (c) 2008-2022
#  National Technology and Engineering Solutions of Sandia, LLC
#  Under the terms of Contract DE-NA0003525 with National Technology and
#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
#  rights in this software.
#  This software is distributed under the 3-clause BSD License.
#  ___________________________________________________________________________

import logging
import collections.abc

from pyomo.core.kernel.homogeneous_container import \
    IHomogeneousContainer

logger = logging.getLogger('pyomo.core')


class DictContainer(IHomogeneousContainer,
                    collections.abc.MutableMapping):
    """
    A partial implementation of the IHomogeneousContainer
    interface that provides dict-like storage functionality.

    Complete implementations need to set the _ctype property
    at the class level and initialize the remaining
    ICategorizedObject attributes during object creation. If
    using __slots__, a slot named "_data" must be included.

    Note that this implementation allows nested storage of
    other ICategorizedObjectContainer implementations that
    are defined with the same ctype.
    """
    __slots__ = ()
    _child_storage_delimiter_string = ""
    _child_storage_entry_string = "[%s]"

    def __init__(self, *args, **kwds):
        self._data = dict()
        if len(args) > 0:
            if len(args) > 1:
                raise TypeError(
                    "%s expected at most 1 arguments, "
                    "got %s" % (self.__class__.__name__,
                                len(args)))
            self.update(args[0])
        if len(kwds):
            self.update(**kwds)

    def _fast_insert(self, key, item):
        item._update_parent_and_storage_key(self, key)
        self._data[key] = item

    #
    # Define the ICategorizedObjectContainer abstract methods
    #

    def child(self, key):
        """Get the child object associated with a given
        storage key for this container.

        Raises:
            KeyError: if the argument is not a storage key
                for any children of this container
        """
        return self[key]

    def children(self):
        """A generator over the children of this container."""
        return self._data.values()

    #
    # Define the MutableMapping abstract methods
    #

    def __setitem__(self, key, item):
        if item.ctype is self.ctype:
            if item._parent is None:
                if key in self._data:
                    logger.warning(
                        "Implicitly replacing the entry %s (type=%s) "
                        "with a new object (type=%s). This is usually "
                        "indicative of a modeling error. To avoid this "
                        "warning, delete the original object from the "
                        "container before assigning a new object."
                        % (self[key].name,
                           self[key].__class__.__name__,
                           item.__class__.__name__))
                    self._data[key]._clear_parent_and_storage_key()
                self._fast_insert(key, item)
                return
            elif (key in self._data) and (self._data[key] is item):
                # a very special case that makes sense to handle
                # because the implied order should be: (1) delete
                # the object at the current index, (2) insert the
                # the new object. This performs both without any
                # actions, but it is an extremely rare case, so
                # it should go last.
                return
            # see note about allowing categorized objects to
            # live in more than one container
            raise ValueError(
                "Invalid assignment to %s type with name '%s' "
                "at key %s. A parent container has already been "
                "assigned to the object being inserted: %s"
                % (self.__class__.__name__,
                   self.name,
                   key,
                   item.parent.name))
        else:
            raise TypeError(
                "Invalid assignment to type %s with index %s. "
                "The object being inserted has the wrong "
                "category type: %s"
                % (self.__class__.__name__,
                   key,
                   item.ctype))

    def __delitem__(self, key):
        self._data[key]._clear_parent_and_storage_key()
        del self._data[key]

    def __getitem__(self, key):
        return self._data[key]

    def __iter__(self):
        return self._data.__iter__()

    def __len__(self):
        return self._data.__len__()

    #
    # Override a few default implementations on MutableMapping
    #

    def __contains__(self, key):
        return self._data.__contains__(key)

    # We want to avoid generating Pyomo expressions due to
    # comparison of values, so we convert both objects to a
    # plain dictionary mapping key->(type(val), id(val)) and
    # compare that instead.
    def __eq__(self, other):
        if not isinstance(other, collections.abc.Mapping):
            return False
        return {key:(type(val), id(val))
                    for key, val in self.items()} == \
               {key:(type(val), id(val))
                    for key, val in other.items()}

    def __ne__(self, other):
        return not (self == other)
