import numpy as np


def transform_constraint_function(nlc):
    """
    The Python interfaces receives the constraints as lb <= constraint(x) <= ub,
    but the Fortran backend expects the nonlinear constraints to be constraint(x) <= 0.
    Thus a conversion is needed.

    In addition to the conversion, we add a check to ensure that the provided lower/upper bounds
    have a shape consistent with the output of the constraint function.
    """

    def newconstraint(x):
        values = np.atleast_1d(np.array(nlc.fun(x), dtype=np.float64))

        # Upgrade the lower/upper bounds to vectors if necessary
        lb = nlc.lb
        try:
            _ = len(lb)
        except TypeError:
            lb = np.array([nlc.lb] * len(values), dtype=np.float64)

        ub = nlc.ub
        try:
            _ = len(ub)
        except TypeError:
            ub = np.array([nlc.ub] * len(values), dtype=np.float64)

        # Check the shapes and raise an exception if they do not match
        if len(values) != len(lb):
            raise ValueError(
                "The number of elements in the constraint function's output does not match the number of elements in the lower bound."
            )
        if len(values) != len(ub):
            raise ValueError(
                "The number of elements in the constraint function's output does not match the number of elements in the upper bound."
            )

        # Combine the upper and lower bounds to transform the function into the form
        # expected by the Fortran backend.
        return np.concatenate(
            (
                [lb_ii - vi for lb_ii, vi in zip(lb, values) if lb_ii > -np.inf],
                [vi - ub_ii for ub_ii, vi in zip(ub, values) if ub_ii < np.inf],
            )
        )

    return newconstraint


def process_nl_constraints(nlcs):
    functions = []
    for nlc in nlcs:
        fun_i = transform_constraint_function(nlc)
        functions.append(fun_i)

    def constraint_function(x):
        values = np.empty(0, dtype=np.float64)
        for fun in functions:
            values = np.concatenate((values, fun(x)))
        return values

    return constraint_function
