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

# Routines to collect data in a structured format

from pyomo.common.collections import Bunch
from pyomo.core.base import Var, Constraint, Objective, maximize, minimize
from pyomo.repn.standard_repn import generate_standard_repn


def collect_linear_terms(block, unfixed):
    #
    # Variables are constraints of block
    # Constraints are unfixed variables of block and the parent model.
    #
    vnames = set()
    for obj in block.component_objects(Constraint, active=True):
        vnames.add(
            (obj.getname(fully_qualified=True, relative_to=block), obj.is_indexed())
        )
    cnames = set(unfixed)
    for obj in block.component_objects(Var, active=True):
        cnames.add(
            (obj.getname(fully_qualified=True, relative_to=block), obj.is_indexed())
        )
    #
    A = {}
    b_coef = {}
    c_rhs = {}
    c_sense = {}
    d_sense = None
    v_domain = {}
    #
    # Collect objective
    #
    for odata in block.component_objects(Objective, active=True):
        for ndx in odata:
            if odata[ndx].sense == maximize:
                o_terms = generate_standard_repn(
                    -1 * odata[ndx].expr, compute_values=False
                )
                d_sense = minimize
            else:
                o_terms = generate_standard_repn(odata[ndx].expr, compute_values=False)
                d_sense = maximize
            for var, coef in zip(o_terms.linear_vars, o_terms.linear_coefs):
                c_rhs[var.parent_component().local_name, var.index()] = coef
        # Stop after the first objective
        break
    #
    # Collect constraints
    #
    for data in block.component_objects(Constraint, active=True):
        name = data.getname(relative_to=block)
        for ndx in data:
            con = data[ndx]
            body_terms = generate_standard_repn(con.body, compute_values=False)
            if body_terms.is_fixed():
                #
                # If a constraint has a fixed body, then don't collect it.
                #
                continue
            lower_terms = (
                generate_standard_repn(con.lower, compute_values=False)
                if not con.lower is None
                else None
            )
            upper_terms = (
                generate_standard_repn(con.upper, compute_values=False)
                if not con.upper is None
                else None
            )
            #
            if not lower_terms is None and not lower_terms.is_constant():
                raise (
                    RuntimeError,
                    "Error during dualization:  Constraint '%s' has a lower bound that is non-constant",
                )
            if not upper_terms is None and not upper_terms.is_constant():
                raise (
                    RuntimeError,
                    "Error during dualization:  Constraint '%s' has an upper bound that is non-constant",
                )
            #
            for var, coef in zip(body_terms.linear_vars, body_terms.linear_coefs):
                try:
                    # The variable is in the subproblem
                    varname = var.parent_component().getname(
                        fully_qualified=True, relative_to=block
                    )
                except:
                    # The variable is somewhere else in the model
                    varname = var.parent_component().getname(
                        fully_qualified=True, relative_to=block.model()
                    )
                varndx = var.index()
                A.setdefault(varname, {}).setdefault(varndx, []).append(
                    Bunch(coef=coef, var=name, ndx=ndx)
                )
            #
            if not con.equality:
                #
                # Inequality constraint
                #
                if lower_terms is None:
                    #
                    # body <= upper
                    #
                    v_domain[name, ndx] = -1
                    b_coef[name, ndx] = upper_terms.constant - body_terms.constant
                elif upper_terms is None:
                    #
                    # lower <= body
                    #
                    v_domain[name, ndx] = 1
                    b_coef[name, ndx] = lower_terms.constant - body_terms.constant
                else:
                    #
                    # lower <= body <= upper
                    #
                    # Dual for lower bound
                    #
                    ndx_ = tuple(list(ndx).append('lb'))
                    v_domain[name, ndx_] = 1
                    b_coef[name, ndx] = lower_terms.constant - body_terms.constant
                    #
                    # Dual for upper bound
                    #
                    ndx_ = tuple(list(ndx).append('ub'))
                    v_domain[name, ndx_] = -1
                    b_coef[name, ndx] = upper_terms.constant - body_terms.constant
            else:
                #
                # Equality constraint
                #
                v_domain[name, ndx] = 0
                b_coef[name, ndx] = lower_terms.constant - body_terms.constant

    #
    # Collect bound constraints
    #
    def all_vars(b):
        """
        This conditionally chains together the active variables in the current block with
        the active variables in all of the parent blocks (if any exist).
        """
        for obj in b.component_objects(Var, active=True, descend_into=True):
            name = obj.parent_component().getname(fully_qualified=True, relative_to=b)
            yield (name, obj)
        #
        # Look through parent blocks
        #
        b = b.parent_block()
        while not b is None:
            for obj in b.component_objects(Var, active=True, descend_into=False):
                name = obj.parent_component().name
                yield (name, obj)
            b = b.parent_block()

    for name, data in all_vars(block):
        #
        # Skip fixed variables (in the parent)
        #
        if not (name, data.is_indexed()) in cnames:
            continue
        #
        # Iterate over all variable indices
        #
        for ndx in data:
            var = data[ndx]
            bounds = var.bounds
            if bounds[0] is None and bounds[1] is None:
                c_sense[name, ndx] = 'e'
            elif bounds[0] is None:
                if bounds[1] == 0.0:
                    c_sense[name, ndx] = 'g'
                else:
                    c_sense[name, ndx] = 'e'
                    #
                    # Add constraint that defines the upper bound
                    #
                    name_ = name + "_upper_"
                    varname = data.parent_component().getname(
                        fully_qualified=True, relative_to=block
                    )
                    varndx = data[ndx].index()
                    A.setdefault(varname, {}).setdefault(varndx, []).append(
                        Bunch(coef=1.0, var=name_, ndx=ndx)
                    )
                    #
                    v_domain[name_, ndx] = -1
                    b_coef[name_, ndx] = bounds[1]
            elif bounds[1] is None:
                if bounds[0] == 0.0:
                    c_sense[name, ndx] = 'l'
                else:
                    c_sense[name, ndx] = 'e'
                    #
                    # Add constraint that defines the lower bound
                    #
                    name_ = name + "_lower_"
                    varname = data.parent_component().getname(
                        fully_qualified=True, relative_to=block
                    )
                    varndx = data[ndx].index()
                    A.setdefault(varname, {}).setdefault(varndx, []).append(
                        Bunch(coef=1.0, var=name_, ndx=ndx)
                    )
                    #
                    v_domain[name_, ndx] = 1
                    b_coef[name_, ndx] = bounds[0]
            else:
                # Bounded above and below
                c_sense[name, ndx] = 'e'
                #
                # Add constraint that defines the upper bound
                #
                name_ = name + "_upper_"
                varname = data.parent_component().getname(
                    fully_qualified=True, relative_to=block
                )
                varndx = data[ndx].index()
                A.setdefault(varname, {}).setdefault(varndx, []).append(
                    Bunch(coef=1.0, var=name_, ndx=ndx)
                )
                #
                v_domain[name_, ndx] = -1
                b_coef[name_, ndx] = bounds[1]
                #
                # Add constraint that defines the lower bound
                #
                name_ = name + "_lower_"
                varname = data.parent_component().getname(
                    fully_qualified=True, relative_to=block
                )
                varndx = data[ndx].index()
                A.setdefault(varname, {}).setdefault(varndx, []).append(
                    Bunch(coef=1.0, var=name_, ndx=ndx)
                )
                #
                v_domain[name_, ndx] = 1
                b_coef[name_, ndx] = bounds[0]
    #
    return (A, b_coef, c_rhs, c_sense, d_sense, vnames, cnames, v_domain)
