# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 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
from weakref import ref as weakref_ref

from pyomo.common.log import is_debug_set
from pyomo.core.base.set_types import Any
from pyomo.core.base.var import IndexedVar, VarData
from pyomo.core.base.constraint import IndexedConstraint, ConstraintData
from pyomo.core.base.objective import IndexedObjective, ObjectiveData
from pyomo.core.base.expression import IndexedExpression, ExpressionData

from collections.abc import MutableSequence

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

#
# In the future I think ComponentDict and ComponentList should inherit
# directly from (Active)IndexedComponent, and that class should be
# stripped down to a minimal interface. The abstract interface should
# be implemented on top of these classes.
#


class ComponentList(MutableSequence):
    def __init__(self, interface_datatype, *args):
        self._interface_datatype = interface_datatype
        self._data = []
        if len(args) > 0:
            if len(args) > 1:
                raise TypeError(
                    "ComponentList expected at most 1 arguments, "
                    "got %s" % (len(args))
                )
            for item in args[0]:
                self.append(item)

    def construct(self, data=None):
        if is_debug_set(logger):
            logger.debug(  # pragma:nocover
                "Constructing ComponentList object, name=%s, from data=%s"
                % (self.name, str(data))
            )
        if self._constructed:  # pragma:nocover
            return
        self._constructed = True

    #
    # Note: The following methods add a flavor of the dict-interface
    #       to this component. This is simply a hack that allows us to
    #       create working Pyomo examples using this prototype. The
    #       correct way to go would be to remove these methods and to
    #       update pyomo code everywhere to first check whether it is
    #       dealing with a dict or a list interface before
    #       iterating. I don't think that would be difficult to do.
    #

    def keys(self):
        return range(len(self))

    iterkeys = keys

    def values(self):
        return list(iter(self))

    itervalues = values

    def items(self):
        return zip(self.keys(), self.values())

    iteritems = items

    #
    # Define the MutableSequence abstract methods
    #

    # Currently __setitem__ only supports assignment of an explicit
    # instantiation of the interface datatype. Updates to an already
    # existing object inside this container need to be done via that
    # objects interface methods or by directly accessing object
    # attributes.
    #
    # * See notes above the __setitem__ method for ComponentDict
    #   about potentially supporting implicit assignment / update
    #
    def __setitem__(self, i, item):
        if isinstance(item, self._interface_datatype):
            # release the current component (assuming we don't get
            # an index error)
            # * see __delitem__ for explanation
            if item._component is None:
                item._component = weakref_ref(self)
                if hasattr(self, "_active"):
                    self._active |= getattr(item, '_active', True)
                self._data[i]._component = None
                self._data[i] = item
                item._index = i
                return
            elif self._data[i] 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 components to live in more than
            # one container
            raise ValueError(
                "Invalid component object assignment to ComponentList "
                "%s at index %s. A parent component has already been "
                "assigned the object: %s" % (self.name, i, item.parent_component().name)
            )
        # see note about implicit assignment and update
        raise TypeError(
            "ComponentList must be assigned objects "
            "of type %s. Invalid type for key %s: %s"
            % (self._interface_datatype.__name__, i, type(item))
        )

    # * Only supports explicit objects. See notes above __setitem__
    #   for more information
    def insert(self, i, item):
        if isinstance(item, self._interface_datatype):
            if item._component is None:
                item._component = weakref_ref(self)
                if hasattr(self, "_active"):
                    self._active |= getattr(item, '_active', True)
                self._data.insert(i, item)
                item._index = i
                return
            # see note about allowing components to live in more than
            # one container
            raise ValueError(
                "Invalid component object assignment to ComponentList "
                "%s at index %s. A parent component has already been "
                "assigned the object: %s" % (self.name, i, item.parent_component().name)
            )
        # see note about implicit assignment and update
        raise TypeError(
            "ComponentList must be assigned objects "
            "of type %s. Invalid type for key %s: %s"
            % (self._interface_datatype.__name__, i, type(item))
        )

    # Since we don't currently allow objects to be assigned when their
    # parent component is already set, it would make sense to reset
    # the parent component back to None for an object being removed
    # from this container; thus, allowing objects to be naturally
    # transferred between containers if one so chooses. This would be
    # more complicated if allowing components to live in more than one
    # container (see note above __setitem__), and maybe this is the
    # reason not to support that.
    def __delitem__(self, i):
        obj = self._data[i]
        obj._component = None
        del self._data[i]

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

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

    #
    # Override a few default implementations on MutableSequence
    #

    # We want to avoid generating Pyomo expressions by comparing values
    def __contains__(self, item):
        item_id = id(item)
        return any(item_id == id(_v) for _v in self._data)

    # We want to avoid generating Pyomo expressions by comparing values
    def index(self, item, start=0, stop=None):
        '''S.index(value, [start, [stop]]) -> integer -- return first index of value.

        Raises ValueError if the value is not present.
        '''
        if start is not None and start < 0:
            start = max(len(self) + start, 0)
        if stop is not None and stop < 0:
            stop += len(self)

        item_id = id(item)
        i = start
        while stop is None or i < stop:
            try:
                if id(self[i]) == item_id:
                    return i
            except IndexError:
                break
            i += 1
        raise ValueError

    # We want to avoid generating Pyomo expressions by comparing values
    def count(self, item):
        'S.count(value) -> integer -- return number of occurrences of value'
        item_id = id(item)
        cnt = sum(1 for _v in self._data if id(_v) == item_id)
        assert cnt == 1
        return cnt

    # Avoid errors related to calling __setitem__
    # with a component that is already owned
    def reverse(self):
        'S.reverse() -- reverse *IN PLACE*'
        n = len(self)
        data = self._data
        for i in range(n // 2):
            data[i], data[n - i - 1] = data[n - i - 1], data[i]


#
# ComponentList needs to come before IndexedComponent
# (or subclasses of) so we can override certain methods
#


class XVarList(ComponentList, IndexedVar):
    def __init__(self, *args, **kwds):
        IndexedVar.__init__(self, Any, **kwds)
        # Constructor for ComponentList needs to
        # go last in order to handle any initialization
        # iterable as an argument
        ComponentList.__init__(self, VarData, *args, **kwds)


class XConstraintList(ComponentList, IndexedConstraint):
    def __init__(self, *args, **kwds):
        IndexedConstraint.__init__(self, Any, **kwds)
        # Constructor for ComponentList needs to
        # go last in order to handle any initialization
        # iterable as an argument
        ComponentList.__init__(self, ConstraintData, *args, **kwds)


class XObjectiveList(ComponentList, IndexedObjective):
    def __init__(self, *args, **kwds):
        IndexedObjective.__init__(self, Any, **kwds)
        # Constructor for ComponentList needs to
        # go last in order to handle any initialization
        # iterable as an argument
        ComponentList.__init__(self, ObjectiveData, *args, **kwds)


class XExpressionList(ComponentList, IndexedExpression):
    def __init__(self, *args, **kwds):
        IndexedExpression.__init__(self, Any, **kwds)
        # Constructor for ComponentList needs to
        # go last in order to handle any initialization
        # iterable as an argument
        ComponentList.__init__(self, ExpressionData, *args, **kwds)
