isa_dataiterator(data) = false

struct AnalysisResults{O, C}
    objective::O
    constraints::C
end

"""
    OptimizationCache(prob::OptimizationProblem, opt; kwargs...)

Prepared optimization problem state used by cache-based solvers.

`OptimizationCache` stores the selected optimizer, instantiated objective and
constraint functions, bounds, constraint limits, callbacks, verbosity settings,
and solver keyword arguments. Use [`init`](@ref) to construct caches through the
public solver interface.
"""
struct OptimizationCache{
        O, IIP, F <: SciMLBase.AbstractOptimizationFunction{IIP},
        RC, LB, UB, LC, UC, S, P, C, M, V,
    } <:
    SciMLBase.AbstractOptimizationCache
    opt::O
    f::F
    reinit_cache::RC
    lb::LB
    ub::UB
    lcons::LC
    ucons::UC
    sense::S
    progress::P
    callback::C
    manifold::M
    analysis_results::AnalysisResults
    solver_args::NamedTuple
    verbose::V
end

function OptimizationCache(
        prob::SciMLBase.OptimizationProblem, opt;
        callback = DEFAULT_CALLBACK,
        maxiters::Union{Number, Nothing} = nothing,
        maxtime::Union{Number, Nothing} = nothing,
        abstol::Union{Number, Nothing} = nothing,
        reltol::Union{Number, Nothing} = nothing,
        progress = false,
        structural_analysis = false,
        manifold = nothing,
        verbose = OptimizationVerbosity(),
        kwargs...
    )
    if isa_dataiterator(prob.p)
        reinit_cache = OptimizationBase.ReInitCache(prob.u0, iterate(prob.p)[1])
        reinit_cache_passedon = OptimizationBase.ReInitCache(prob.u0, prob.p)
    else
        reinit_cache = OptimizationBase.ReInitCache(prob.u0, prob.p)
        reinit_cache_passedon = reinit_cache
    end

    num_cons = prob.ucons === nothing ? 0 : length(prob.ucons)

    # Check if verbose is a solver-specific type (not OptimizationVerbosity-related)
    # If so, pass it through to solver_args and use default OptimizationVerbosity
    if !(
            verbose isa Bool || verbose isa OptimizationVerbosity ||
                verbose isa SciMLLogging.AbstractVerbosityPreset
        )
        # Solver-specific verbosity type (e.g., MadNLP.LogLevels)
        kwargs = merge(NamedTuple(kwargs), (; verbose = verbose))
        processed_verbose = OptimizationVerbosity()
    else
        processed_verbose = _process_verbose_param(verbose)
    end

    if !(
            prob.f.adtype isa DifferentiationInterface.SecondOrder ||
                prob.f.adtype isa AutoSparse{<:DifferentiationInterface.SecondOrder} ||
                prob.f.adtype isa AutoZygote
        ) &&
            (
            SciMLBase.requireshessian(opt) || SciMLBase.requiresconshess(opt) ||
                SciMLBase.requireslagh(opt)
        )
        @SciMLMessage(
            lazy"The selected optimization algorithm requires second order derivatives, but `SecondOrder` ADtype was not provided. So a `SecondOrder` with $(prob.f.adtype) for both inner and outer will be created, this can be suboptimal and not work in some cases so an explicit `SecondOrder` ADtype is recommended.",
            processed_verbose, :missing_second_order_ad
        )
    elseif prob.f.adtype isa AutoZygote &&
            (
            SciMLBase.requiresconshess(opt) || SciMLBase.requireslagh(opt) ||
                SciMLBase.requireshessian(opt)
        )
        @SciMLMessage(
            lazy"The selected optimization algorithm requires second order derivatives, but `AutoZygote` ADtype was provided. So a `SecondOrder` with `AutoZygote` for inner and `AutoForwardDiff` for outer will be created, for choosing another pair an explicit `SecondOrder` ADtype is recommended.",
            processed_verbose, :incompatible_ad_backend
        )
    end

    f_base = if prob.sense === SciMLBase.MaxSense && !supports_sense(opt)
        apply_sense(prob.f, prob.sense)
    else
        prob.f
    end

    f = OptimizationBase.instantiate_function(
        f_base, reinit_cache, f_base.adtype, num_cons;
        g = SciMLBase.requiresgradient(opt), h = SciMLBase.requireshessian(opt),
        hv = SciMLBase.requireshessian(opt), fg = SciMLBase.allowsfg(opt),
        fgh = SciMLBase.allowsfgh(opt), cons_j = SciMLBase.requiresconsjac(opt), cons_h = SciMLBase.requiresconshess(opt),
        cons_vjp = SciMLBase.allowsconsvjp(opt), cons_jvp = SciMLBase.allowsconsjvp(opt), lag_h = SciMLBase.requireslagh(opt)
    )

    if structural_analysis
        obj_res, cons_res = symify_cache(f, prob, num_cons, manifold)
    else
        obj_res = nothing
        cons_res = nothing
    end

    return OptimizationCache(
        opt, f, reinit_cache_passedon, prob.lb, prob.ub, prob.lcons,
        prob.ucons, prob.sense,
        progress, callback, manifold, AnalysisResults(obj_res, cons_res),
        merge((; maxiters, maxtime, abstol, reltol), NamedTuple(kwargs)),
        processed_verbose
    )
end

function SciMLBase.__init(
        prob::SciMLBase.OptimizationProblem, opt;
        callback = DEFAULT_CALLBACK,
        maxiters::Union{Number, Nothing} = nothing,
        maxtime::Union{Number, Nothing} = nothing,
        abstol::Union{Number, Nothing} = nothing,
        reltol::Union{Number, Nothing} = nothing,
        progress = false,
        verbose = OptimizationVerbosity(),
        kwargs...
    )
    return OptimizationCache(
        prob, opt; maxiters, maxtime, abstol, callback,
        reltol, progress, verbose,
        kwargs...
    )
end

SciMLBase.isinplace(::OptimizationCache{o, iip}) where {o, iip} = iip

# Wrapper for fields that may change in `reinit!(cache)` of a cache.
mutable struct ReInitCache{uType, P}
    u0::uType
    p::P
end
