"""
    IntegrandValuesSum{integrandType}

A struct used to save the accumulated integrand value in `integrand::integrandType`.

# Fields

  - `integrand::integrandType`: the running quadrature total. The callback mutates this value
    in place for mutable values such as arrays; scalar totals are replaced with the updated
    scalar value.

Do not mutate `integrand` while a solve using this storage is active.

# Constructors

  - `IntegrandValuesSum(initial_value)`: Create with an initial value (recommended).
    The type is inferred from the value. Use `zeros(n)` for arrays or `zero(T)` for scalars.

# Returns

An `IntegrandValuesSum` container for [`IntegratingSumCallback`](@ref) or
[`IntegratingGKSumCallback`](@ref).

# Examples

```julia
# For array-valued integrands
integrated = IntegrandValuesSum(zeros(3))

# For scalar-valued integrands
integrated = IntegrandValuesSum(0.0)
```
"""
mutable struct IntegrandValuesSum{integrandType}
    integrand::integrandType
end

# The default constructor IntegrandValuesSum(value) is automatically generated by Julia
# and is the recommended API. It correctly infers the type from the value.

"""
    IntegrandValuesSum(integrandType::Type)

!!! warning "Deprecated"
    Passing a type is deprecated. Pass a value instead:
    - For scalars: `IntegrandValuesSum(zero(T))` instead of `IntegrandValuesSum(T)`
    - For arrays: `IntegrandValuesSum(zeros(n))` instead of `IntegrandValuesSum(Vector{T})`

This method is kept for backwards compatibility but will be removed in a future version.

# Returns

  - `IntegrandValuesSum`: scalar storage initialized with `zero(integrandType)`.

# Throws

  - `ArgumentError`: if `integrandType` is not a subtype of `Number`, because its shape
    cannot be inferred from the type alone.
"""
function IntegrandValuesSum(::Type{integrandType}) where {integrandType}
    Base.depwarn(
        "IntegrandValuesSum(Type) is deprecated. " *
            "Pass a value instead: IntegrandValuesSum(zero(T)) for scalars " *
            "or IntegrandValuesSum(zeros(n)) for arrays.",
        :IntegrandValuesSum
    )
    # For scalar types, we can create a proper zero value
    if integrandType <: Number
        return IntegrandValuesSum{integrandType}(zero(integrandType))
    else
        # For array types and others, we cannot create a proper zero without knowing the size
        throw(
            ArgumentError(
                "Cannot create IntegrandValuesSum from type $integrandType. " *
                    "Pass a value instead, e.g., IntegrandValuesSum(zeros(n)) for arrays."
            )
        )
    end
end

function Base.show(io::IO, integrand_values::IntegrandValuesSum)
    integrandType = eltype(integrand_values.integrand)
    return print(
        io, "IntegrandValuesSum{integrandType=", integrandType, "}",
        "\nintegrand:\n", integrand_values.integrand
    )
end

mutable struct SavingIntegrandSumAffect{IntegrandFunc, integrandType, IntegrandCacheType}
    integrand_func::IntegrandFunc
    integrand_values::IntegrandValuesSum{integrandType}
    integrand_cache::IntegrandCacheType
    accumulation_cache::IntegrandCacheType
    integrand_inplace::Union{Nothing, Bool}
end

function SavingIntegrandSumAffect(
        integrand_func, integrand_values, integrand_cache, accumulation_cache
    )
    return SavingIntegrandSumAffect(
        integrand_func, integrand_values, integrand_cache, accumulation_cache, nothing
    )
end

function (affect!::SavingIntegrandSumAffect)(integrator)
    n = 0
    if integrator.sol.prob isa Union{SDEProblem, RODEProblem}
        n = 10
    else
        n = div(SciMLBase.alg_order(integrator.alg) + 1, 2)
    end
    accumulation_cache = recursive_zero!(affect!.accumulation_cache)
    isinplace_prob = SciMLBase.isinplace(integrator.sol.prob)
    inplace_integrand = affect!.integrand_inplace === nothing ?
        (isinplace_prob && affect!.integrand_cache !== nothing) :
        affect!.integrand_inplace
    for i in 1:n
        t_temp = ((integrator.t - integrator.tprev) / 2) * gauss_points[n][i] + (integrator.t + integrator.tprev) / 2
        if isinplace_prob
            curu = first(get_tmp_cache(integrator))
            integrator(curu, t_temp)
        else
            curu = integrator(t_temp)
        end
        if inplace_integrand
            affect!.integrand_func(affect!.integrand_cache, curu, t_temp, integrator)
            accumulation_cache = recursive_axpy!(
                gauss_weights[n][i], affect!.integrand_cache, accumulation_cache
            )
        else
            accumulation_cache = recursive_axpy!(
                gauss_weights[n][i],
                affect!.integrand_func(curu, t_temp, integrator), accumulation_cache
            )
        end
    end
    accumulation_cache = recursive_scalar_mul!(
        accumulation_cache, (integrator.t - integrator.tprev) / 2
    )
    affect!.integrand_values.integrand = recursive_add!(
        affect!.integrand_values.integrand, accumulation_cache
    )
    return derivative_discontinuity!(integrator, false)
end

"""
    IntegratingSumCallback(integrand_func, integrand_values::IntegrandValuesSum,
        integrand_prototype; integrand_inplace = nothing) -> DiscreteCallback

Construct a callback that uses fixed-order Gaussian quadrature to accumulate the integral of
`integrand_func` over accepted solver steps in `integrand_values.integrand`.

# Arguments

  - `integrand_func`: define either `integrand_func(u, t, integrator)` to return the
    integrand or `integrand_func(out, u, t, integrator)` to write it into `out`. Returned or
    written values must be compatible with `integrand_values.integrand`.
  - `integrand_values::IntegrandValuesSum`: storage for the running integral. Construct it
    as
    `IntegrandValuesSum(initial_value)` with an initial value compatible with the integrand.
  - `integrand_prototype`: representative integrand output used as an in-place output
    buffer.

# Keywords

  - `integrand_inplace::Union{Nothing, Bool} = nothing`: select the integrand calling form.
    With `nothing`, use the in-place form for an in-place problem when a cache can be
    allocated, and otherwise use the allocating form. Set this to `true` to force the
    in-place form or `false` to force the allocating form. `true` requires a non-`nothing`
    `integrand_prototype`.

# Returns

  - `DiscreteCallback`: a callback that adds each Gaussian quadrature estimate to
    `integrand_values.integrand`.

# Throws

  - `ArgumentError`: if `integrand_inplace = true` and `integrand_prototype === nothing`.

# Examples

```julia
using DiffEqCallbacks, OrdinaryDiffEq

prob = ODEProblem((u, p, t) -> -u, 1.0, (0.0, 1.0))
values = IntegrandValuesSum(0.0)
cb = IntegratingSumCallback((u, t, integrator) -> u^2, values, 0.0)

sol = solve(prob, Tsit5(); callback = cb)
total = values.integrand
```

!!! note

    This method is currently limited to ODE solvers of order 10 or lower. Open an issue if other
    solvers are required.
"""
function IntegratingSumCallback(
        integrand_func, integrand_values::IntegrandValuesSum, integrand_prototype;
        integrand_inplace::Union{Nothing, Bool} = nothing
    )
    if integrand_inplace === true && integrand_prototype === nothing
        throw(
            ArgumentError(
                "integrand_inplace = true requires an integrand_prototype to use as the output buffer."
            )
        )
    end
    affect! = SavingIntegrandSumAffect(
        integrand_func, integrand_values, integrand_prototype,
        allocate_zeros(integrand_prototype), integrand_inplace
    )
    condition = true_condition
    return DiscreteCallback(condition, affect!, save_positions = (false, false))
end

export IntegratingSumCallback, IntegrandValuesSum
