#  ___________________________________________________________________________
#
#  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 copy
import weakref
from pyomo.common.autoslots import AutoSlots

def _not_implemented(*args, **kwds):
    raise NotImplementedError("This property is abstract")     #pragma:nocover

def _abstract_readwrite_property(**kwds):
    p = property(fget=_not_implemented,
                 fset=_not_implemented,
                 **kwds)
    return p

def _abstract_readonly_property(**kwds):
    p = property(fget=_not_implemented,
                 **kwds)
    return p

class _no_ctype(object):
    """The default argument for methods that accept a ctype."""
    pass

# This will be populated outside of core.kernel. It will map
# AML classes (which are the ctypes used by all of the
# solver interfaces) to Kernel classes
_convert_ctype = {}
_kernel_ctype_backmap = {}

def _convert_descend_into(value):
    """Converts the descend_into keyword to a function"""
    if hasattr(value, "__call__"):
        return value
    elif value:
        return _convert_descend_into._true
    else:
        return _convert_descend_into._false
_convert_descend_into._true = lambda x: True
_convert_descend_into._false = lambda x: False

class ICategorizedObject(AutoSlots.Mixin):
    """
    Interface for objects that maintain a weak reference to
    a parent storage object and have a category type.

    This class is abstract. It assumes any derived class
    declares the attributes below with or without slots:

    Attributes:
        _ctype: Stores the object's category type, which
            should be some class derived from
            ICategorizedObject. This attribute may be
            declared at the class level.
        _parent: Stores a weak reference to the object's
            parent container or :const:`None`.
        _storage_key: Stores key this object can be accessed
            with through its parent container.
        _active (bool): Stores the active status of this
            object.
    """
    __slots__ = ()
    __autoslot_mappers__ = {'_parent': AutoSlots.weakref_mapper}

    # These flags can be used by implementations
    # to avoid isinstance calls.
    _is_container = False
    """A flag used to indicate that the class is an instance
    of ICategorizedObjectContainer."""

    _is_heterogeneous_container = False
    """A flag used to indicate that the class is an instance
    of ICategorizedObjectContainer that stores objects with
    different category types than its own."""

    #
    # Interface
    #

    @property
    def ctype(self):
        """The object's category type."""
        return self._ctype

    @property
    def parent(self):
        """The object's parent (possibly None)."""
        if isinstance(self._parent, weakref.ReferenceType):
            return self._parent()
        else:
            return self._parent

    @property
    def storage_key(self):
        """The object's storage key within its parent"""
        return self._storage_key

    @property
    def active(self):
        """The active status of this object."""
        return self._active
    @active.setter
    def active(self, value):
        raise AttributeError(
            "Assignment not allowed. Use the "
            "(de)activate method")

    ### The following group of methods use object.__setattr__
    ### to update the _parent, _storage_key, and _active flags.
    ### This is done to allow the block implementation (block.py)
    ### to protect them from being overwritten with user added
    ### components in its __setattr__ method
    def _update_parent_and_storage_key(self, parent, key):
        object.__setattr__(self, "_parent", weakref.ref(parent))
        object.__setattr__(self, "_storage_key", key)

    def _clear_parent_and_storage_key(self):
        object.__setattr__(self, "_parent", None)
        object.__setattr__(self, "_storage_key", None)

    def activate(self):
        """Activate this object."""
        object.__setattr__(self, "_active", True)

    def deactivate(self):
        """Deactivate this object."""
        object.__setattr__(self, "_active", False)
    ###

    def getname(self,
                fully_qualified=False,
                name_buffer={}, # HACK: ignored (required to work with some solver interfaces, but that code should change soon)
                convert=str,
                relative_to=None):
        """
        Dynamically generates a name for this object.

        Args:
            fully_qualified (bool): Generate a full name by
                iterating through all anscestor containers.
                Default is :const:`False`.
            convert (function): A function that converts a
                storage key into a string
                representation. Default is the built-in
                function str.
            relative_to (object): When generating a fully
                qualified name, generate the name relative
                to this block.

        Returns:
            If a parent exists, this method returns a string
            representing the name of the object in the
            context of its parent; otherwise (if no parent
            exists), this method returns :const:`None`.
        """
        assert fully_qualified or \
            (relative_to is None)
        parent = self.parent
        if parent is None:
            return None

        key = self.storage_key
        name = parent._child_storage_entry_string % convert(key)
        if fully_qualified:
            parent_name = parent.getname(fully_qualified=True,
                                         relative_to=relative_to)
            if (parent_name is not None) and \
               ((relative_to is None) or \
                (parent is not relative_to)):
                return (parent_name +
                        parent._child_storage_delimiter_string +
                        name)
            else:
                return name
        else:
            return name

    @property
    def name(self):
        """The object's fully qualified name. Alias for
        `obj.getname(fully_qualified=True)`."""
        return self.getname(fully_qualified=True)

    @property
    def local_name(self):
        """The object's local name within the context of its
        parent. Alias for
        `obj.getname(fully_qualified=False)`."""
        return self.getname(fully_qualified=False)

    def __str__(self):
        """Convert this object to a string by first
        attempting to generate its fully qualified name. If
        the object does not have a name (because it does not
        have a parent, then a string containing the class
        name is returned."""
        name = self.name
        if name is None:
            return "<"+self.__class__.__name__+">"
        else:
            return name

    def clone(self):
        """
        Returns a copy of this object with the parent
        pointer set to :const:`None`.

        A clone is almost equivalent to deepcopy except that
        any categorized objects encountered that are not
        descendents of this object will reference the same
        object on the clone.
        """
        save_parent = self._parent
        object.__setattr__(self, "_parent", None)
        try:
            new_block = copy.deepcopy(self,
                                      {'__categorized_object_scope__':
                                       {id(self): True, id(None): False}})
        finally:
            object.__setattr__(self, "_parent", save_parent)
        return new_block

    #
    # The following method must be defined to allow
    # expressions and blocks to be correctly cloned.
    # (copied from JDS code in original component.py,
    # comments removed for now)
    #

    def __deepcopy__(self, memo):
        if '__categorized_object_scope__' in memo and \
                id(self) not in memo['__categorized_object_scope__']:
            _known = memo['__categorized_object_scope__']
            _new = []
            tmp = self.parent
            tmpId = id(tmp)
            while tmpId not in _known:
                _new.append(tmpId)
                tmp = tmp.parent
                tmpId = id(tmp)

            for _id in _new:
                _known[_id] = _known[tmpId]

            if not _known[tmpId]:
                # component is out-of-scope. shallow copy only
                memo[id(self)] = self
                return self

        if id(self) in memo:
            return memo[id(self)]

        ans = memo[id(self)] = self.__class__.__new__(self.__class__)
        ans.__setstate__(copy.deepcopy(self.__getstate__(), memo))
        return ans


class ICategorizedObjectContainer(ICategorizedObject):
    """
    Interface for categorized containers of categorized
    objects.
    """
    _is_container = True
    _child_storage_delimiter_string = None
    _child_storage_entry_string = None
    __slots__ = ()

    #
    # Interface
    #

    def activate(self, shallow=True):
        """Activate this container."""
        super(ICategorizedObjectContainer, self).activate()
        if not shallow:
            for child in self.children():
                if not child._is_container:
                    child.activate()
                else:
                    child.activate(shallow=False)

    def deactivate(self, shallow=True):
        """Deactivate this container."""
        super(ICategorizedObjectContainer, self).deactivate()
        if not shallow:
            for child in self.children():
                if not child._is_container:
                    child.deactivate()
                else:
                    child.deactivate(shallow=False)

    def child(self, *args, **kwds):
        """Returns a child of this container given a storage
        key."""
        raise NotImplementedError     #pragma:nocover

    def children(self, *args, **kwds):
        """A generator over the children of this container."""
        raise NotImplementedError     #pragma:nocover

    def components(self, *args, **kwds):
        """A generator over the set of components stored
        under this container."""
        raise NotImplementedError     #pragma:nocover
