# ____________________________________________________________________________________
#
# 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 pyomo.common.deprecation import deprecated
from pyomo.core.base import (
    Transformation,
    TransformationFactory,
    Var,
    Constraint,
    Objective,
    minimize,
    NonNegativeReals,
    NonPositiveReals,
    Reals,
    Block,
    Model,
    ConcreteModel,
)
from pyomo.duality.collect import collect_linear_terms


def load():
    pass


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


#
# This transformation creates a new block that
# is the dual of the specified block.  If no block is
# specified, then the entire model is dualized.
# This returns a new Block object.
#
@TransformationFactory.register(
    'duality.linear_dual', doc="[DEPRECATED] Dualize a linear model"
)
@deprecated(
    "Use of the pyomo.duality package is deprecated. There are known bugs "
    "in pyomo.duality, and we do not recommend the use of this code. "
    "Development of dualization capabilities has been shifted to "
    "the Pyomo Adversarial Optimization (PAO) library. Please contact "
    "William Hart for further details (wehart@sandia.gov).",
    version='5.6.2',
)
class LinearDual_PyomoTransformation(Transformation):
    def __init__(self):
        super(LinearDual_PyomoTransformation, self).__init__()

    def _create_using(self, instance, **kwds):
        options = kwds.pop('options', {})
        bname = options.get('block', None)
        #
        # Iterate over the model collecting variable data,
        # until the block is found.
        #
        block = None
        if block is None:
            block = instance
        else:
            for name, data in instance.component_map(Block, active=True).items():
                if name == bname:
                    block = instance
        if block is None:
            raise RuntimeError("Missing block: " + bname)
        #
        # Generate the dual
        #
        instance_ = self._dualize(block)

        return instance_

    def _dualize(self, block, unfixed=[]):
        """
        Generate the dual of a block
        """
        #
        # Collect linear terms from the block
        #
        A, b_coef, c_rhs, c_sense, d_sense, vnames, cnames, v_domain = (
            collect_linear_terms(block, unfixed)
        )
        ##print(A)
        ##print(vnames)
        ##print(cnames)
        ##print(list(A.keys()))
        ##print("---")
        ##print(A.keys())
        ##print(c_sense)
        ##print(c_rhs)
        #
        # Construct the block
        #
        if isinstance(block, Model):
            dual = ConcreteModel()
        else:
            dual = Block()
        dual.construct()
        _vars = {}

        def getvar(name, ndx=None):
            v = _vars.get((name, ndx), None)
            if v is None:
                v = Var()
                if ndx is None:
                    v_name = name
                elif type(ndx) is tuple:
                    v_name = "%s[%s]" % (name, ','.join(map(str, ndx)))
                else:
                    v_name = "%s[%s]" % (name, str(ndx))
                setattr(dual, v_name, v)
                _vars[name, ndx] = v
            return v

        #
        # Construct the objective
        #
        if d_sense == minimize:
            dual.o = Objective(
                expr=sum(
                    -b_coef[name, ndx] * getvar(name, ndx) for name, ndx in b_coef
                ),
                sense=d_sense,
            )
        else:
            dual.o = Objective(
                expr=sum(b_coef[name, ndx] * getvar(name, ndx) for name, ndx in b_coef),
                sense=d_sense,
            )
        #
        # Construct the constraints
        #
        for cname in A:
            for ndx, terms in A[cname].items():
                expr = 0
                for term in terms:
                    expr += term.coef * getvar(term.var, term.ndx)
                if not (cname, ndx) in c_rhs:
                    c_rhs[cname, ndx] = 0.0
                if c_sense[cname, ndx] == 'e':
                    e = expr - c_rhs[cname, ndx] == 0
                elif c_sense[cname, ndx] == 'l':
                    e = expr - c_rhs[cname, ndx] <= 0
                else:
                    e = expr - c_rhs[cname, ndx] >= 0
                c = Constraint(expr=e)
                if ndx is None:
                    c_name = cname
                elif type(ndx) is tuple:
                    c_name = "%s[%s]" % (cname, ','.join(map(str, ndx)))
                else:
                    c_name = "%s[%s]" % (cname, str(ndx))
                setattr(dual, c_name, c)
            #
            for (name, ndx), domain in v_domain.items():
                v = getvar(name, ndx)
                flag = type(ndx) is tuple and (ndx[-1] == 'lb' or ndx[-1] == 'ub')
                if domain == 1:
                    v.domain = NonNegativeReals
                elif domain == -1:
                    v.domain = NonPositiveReals
                else:
                    # TODO: verify that this case is possible
                    v.domain = Reals

        return dual
