# ____________________________________________________________________________________
#
# 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.
# ____________________________________________________________________________________

"""This module provides standard :py:class:`enum.Enum` definitions used in
Pyomo, along with additional utilities for working with custom Enums

Utilities:

.. autosummary::

   ExtendedEnumType
   NamedIntEnum

Standard Enums:

.. autosummary::

   ObjectiveSense
   SolverAPIVersion

"""

import enum
import itertools
import re
import sys

# Aliases for enum members so that clients can use
# :py:mod:`pyomo.common.enums` like they would :py:mod:`enum`.
from enum import Enum, Flag

if sys.version_info[:2] < (3, 11):
    EnumType = enum.EnumMeta
else:
    EnumType = enum.EnumType
if sys.version_info[:2] < (3, 13):
    import inspect

    # prior to 3.13 the int.{to,from}_bytes docstrings had LaTeX-like
    # "`..'" quotations, which Sphinx can't parse correctly.
    def _fix_doc(ref):
        def _rewrite(func):
            func.__doc__ = re.sub(r"`(\S+)'", r"`\1`", ref.__doc__)
            return func

        return _rewrite

    class _int_bytes_doc_wrapper:
        """Class to wrap to_bytes/from_bytes and rewrite the docstring"""

        @_fix_doc(int.to_bytes)
        def to_bytes(self, /, length=1, byteorder='big', *, signed=False):
            return super().to_bytes(length=length, byteorder=byteorder, signed=signed)

        # Note: we need to use a decorator to set the __doc__ *before*
        # the @classmethod (which makes __doc__ read-only).
        @classmethod
        @_fix_doc(int.from_bytes)
        def from_bytes(cls, bytes, byteorder='big', *, signed=False):
            return super(_int_bytes_doc_wrapper, cls).from_bytes(
                bytes, byteorder=byteorder, signed=signed
            )

    def _doc_updater(base):
        return (
            inspect.cleandoc(
                f"""A compatibility wrapper around :class:`enum.{base.__name__}`

                This wrapper class updates the :meth:`to_bytes` and
                :meth:`from_bytes` docstrings in Python <= 3.12 to
                suppress warnings generated by Sphinx.

                .. rubric:: {base.__name__}

            """
            )
            + "\n\n"
            # There are environments where IntEnum.__doc__ is None (see #3710)
            + inspect.cleandoc(getattr(base, "__doc__", "") or "")
        )

    class IntEnum(_int_bytes_doc_wrapper, enum.IntEnum):
        __doc__ = _doc_updater(enum.IntEnum)

    class IntFlag(_int_bytes_doc_wrapper, enum.IntFlag):
        __doc__ = _doc_updater(enum.IntFlag)

else:
    IntEnum = enum.IntEnum
    IntFlag = enum.IntFlag


class ExtendedEnumType(EnumType):
    """Metaclass for creating an :py:class:`enum.Enum` that extends another Enum

    In general, :py:class:`enum.Enum` classes are not extensible: that is,
    they are frozen when defined and cannot be the base class of another
    Enum.  This Metaclass provides a workaround for creating a new Enum
    that extends an existing enum.  Members in the base Enum are all
    present as members on the extended enum.

    Example
    -------

    .. testcode::
       :hide:

       import enum
       from pyomo.common.enums import ExtendedEnumType

    .. testcode::

       class ObjectiveSense(enum.IntEnum):
           minimize = 1
           maximize = -1

       class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType):
           __base_enum__ = ObjectiveSense

           unknown = 0

    .. doctest::

       >>> list(ProblemSense)
       [<ProblemSense.unknown: 0>, <ObjectiveSense.minimize: 1>, <ObjectiveSense.maximize: -1>]
       >>> ProblemSense.unknown
       <ProblemSense.unknown: 0>
       >>> ProblemSense.maximize
       <ObjectiveSense.maximize: -1>
       >>> ProblemSense(0)
       <ProblemSense.unknown: 0>
       >>> ProblemSense(1)
       <ObjectiveSense.minimize: 1>
       >>> ProblemSense('unknown')
       <ProblemSense.unknown: 0>
       >>> ProblemSense('maximize')
       <ObjectiveSense.maximize: -1>
       >>> hasattr(ProblemSense, 'minimize')
       True
       >>> ProblemSense.minimize is ObjectiveSense.minimize
       True
       >>> ProblemSense.minimize in ProblemSense
       True

    """

    def __getattr__(cls, attr):
        try:
            return getattr(cls.__base_enum__, attr)
        except:
            return super().__getattr__(attr)

    def __iter__(cls):
        # The members of this Enum are the base enum members joined with
        # the local members
        return itertools.chain(super().__iter__(), cls.__base_enum__.__iter__())

    def __contains__(cls, member):
        # This enum "contains" both its local members and the members in
        # the __base_enum__ (necessary for good auto-enum[sphinx] docs)
        return super().__contains__(member) or member in cls.__base_enum__

    def __dir__(self):
        _dir = set(super().__dir__())
        _dir.update(e.name for e in self)
        return _dir

    def __instancecheck__(cls, instance):
        if cls.__subclasscheck__(type(instance)):
            return True
        # Also pretend that members of the extended enum are subclasses
        # of the __base_enum__.  This is needed to circumvent error
        # checking in enum.__new__ (e.g., for `ProblemSense('minimize')`)
        return cls.__base_enum__.__subclasscheck__(type(instance))

    def _missing_(cls, value):
        # Support attribute lookup by value or name
        for attr in ('value', 'name'):
            for member in cls:
                if getattr(member, attr) == value:
                    return member
        return None

    def __new__(metacls, cls, bases, classdict, **kwds):
        # Support lookup by name - but only if the new Enum doesn't
        # specify its own implementation of _missing_
        if '_missing_' not in classdict:
            classdict['_missing_'] = classmethod(ExtendedEnumType._missing_)
        return super().__new__(metacls, cls, bases, classdict, **kwds)


class NamedIntEnum(IntEnum):
    """An extended version of :py:class:`~pyomo.common.enums.IntEnum` that supports
    creating members by name as well as value.

    """

    @classmethod
    def _missing_(cls, value):
        for member in cls:
            if member.name == value:
                return member
        return None


class ObjectiveSense(NamedIntEnum):
    """Flag indicating if an objective is minimizing (1) or maximizing (-1).

    While the numeric values are arbitrary, there are parts of Pyomo
    that rely on this particular choice of value.  These values are also
    consistent with some solvers (notably Gurobi).

    """

    minimize = 1
    maximize = -1

    # Overloading __str__ is needed to match the behavior of the old
    # pyutilib.enum class (removed June 2020). There are spots in the
    # code base that expect the string representation for items in the
    # enum to not include the class name. New uses of enum shouldn't
    # need to do this.
    def __str__(self):
        return self.name


class SolverAPIVersion(NamedIntEnum):
    """
    Enum identifying Pyomo solver API version

    The numeric values are intentionally a bit odd because APPSI came
    between the official V1 and V2. We still want it to be chronologically
    in order without sacrificing the human-logic of v1 vs. v2.
    """

    #: Original Coopr/Pyomo solver interface
    V1 = 10
    #: Automatic Persistent Pyomo Solver Interface (experimental)
    APPSI = 15
    #: Redesigned solver interface (circa 2024)
    V2 = 20


minimize = ObjectiveSense.minimize
maximize = ObjectiveSense.maximize
