## Common Interface Solve Functions

function DiffEqBase.__solve(
        prob::Union{
            SciMLBase.AbstractODEProblem,
            SciMLBase.AbstractDAEProblem,
        },
        alg::algType,
        timeseries = [],
        ts = [],
        ks = [],
        recompile::Type{Val{recompile_flag}} = Val{true};
        calculate_error = true,
        kwargs...
    ) where {
        algType <: Union{
            SundialsODEAlgorithm,
            SundialsDAEAlgorithm,
        },
        recompile_flag,
    }
    integrator = DiffEqBase.__init(prob, alg, timeseries, ts, ks; kwargs...)
    if integrator.sol.retcode == ReturnCode.Default
        solve!(integrator; early_free = true, calculate_error = calculate_error)
    end
    return integrator.sol
end

function DiffEqBase.__solve(
        prob::Union{
            SciMLBase.AbstractSteadyStateProblem{
                uType,
                isinplace,
            },
            SciMLBase.AbstractNonlinearProblem{
                uType,
                isinplace,
            },
        },
        alg::algType,
        timeseries = [],
        ts = [],
        ks = [],
        recompile::Type{Val{recompile_flag}} = Val{true};
        abstol = eps(Float64)^(4 // 5),
        maxiters = 1000,
        kwargs...
    ) where {
        algType <: SundialsNonlinearSolveAlgorithm,
        recompile_flag, uType, isinplace,
    }
    if prob.u0 isa Number
        u0 = [prob.u0]
    else
        u0 = deepcopy(prob.u0)
    end

    p = prob.p
    userdata = alg.userdata
    linsolve = linear_solver(alg)
    jac_upper = alg.jac_upper
    jac_lower = alg.jac_lower

    ### Fix the more general function to Sundials allowed style
    if prob.f isa ODEFunction
        t = Inf
        if !isinplace && prob.u0 isa Number
            f! = (du, u) -> (du .= prob.f(first(u), p, t); Cint(0))
        elseif !isinplace
            f! = (du, u) -> (du .= prob.f(u, p, t); Cint(0))
        else # Then it's an in-place function on an abstract array
            f! = (du, u) -> prob.f(du, u, p, t)
        end
    elseif prob.f isa NonlinearFunction
        if !isinplace && prob.u0 isa Number
            f! = (du, u) -> (du .= prob.f(first(u), p); Cint(0))
        elseif !isinplace
            f! = (du, u) -> (du .= prob.f(u, p); Cint(0))
        else # Then it's an in-place function on an abstract array
            f! = (du, u) -> prob.f(du, u, p)
        end
    else
        error("Problem function must be ODEFunction or NonlinearFunction")
    end
    u = zero(u0)
    resid = similar(u)
    u,
        flag = ___kinsol(
        f!, u0;
        userdata = userdata,
        linear_solver = linsolve,
        jac_upper = jac_upper,
        jac_lower = jac_lower,
        abstol,
        prob.f.jac_prototype,
        alg.prec_side,
        alg.krylov_dim,
        maxiters,
        strategy = alg.globalization_strategy,
        alg.maxsetupcalls
    )

    f!(resid, u)
    retcode = interpret_sundials_retcode(flag)
    return if prob.u0 isa Number
        SciMLBase.build_solution(prob, alg, u[1], resid[1]; retcode = retcode)
    else
        SciMLBase.build_solution(prob, alg, u, resid; retcode = retcode)
    end
end

function DiffEqBase.__init(
        prob::SciMLBase.AbstractODEProblem{uType, tupType, isinplace},
        alg::SundialsODEAlgorithm{Method, LinearSolver},
        timeseries = [],
        ts = [],
        ks = [];
        verbose = Standard(),
        callback = nothing,
        abstol = 1 / 10^6,
        reltol = 1 / 10^3,
        saveat = Float64[],
        d_discontinuities = Float64[],
        tstops = Float64[],
        maxiters = Int(1.0e5),
        dt = nothing,
        dtmin = 0.0,
        dtmax = 0.0,
        timeseries_errors = true,
        dense_errors = false,
        save_everystep = isempty(saveat), save_idxs = nothing,
        save_on = true,
        save_start = save_everystep || isempty(saveat) ||
            saveat isa Number ? true :
            prob.tspan[1] in saveat,
        save_end = save_everystep || isempty(saveat) ||
            saveat isa Number ? true :
            prob.tspan[2] in saveat,
        dense = save_everystep && isempty(saveat),
        progress = false,
        progress_steps = 1000,
        progress_name = "ODE",
        progress_message = DiffEqBase.ODE_DEFAULT_PROG_MESSAGE,
        progress_id = :Sundials,
        save_timeseries = nothing,
        advance_to_tstop = false,
        stop_at_next_tstop = false,
        userdata = nothing,
        alias_u0 = false,
        initializealg = DefaultInit(),
        kwargs...
    ) where {
        uType, tupType, isinplace, Method, LinearSolver,
    }
    tType = eltype(tupType)
    verbose = _process_verbose_param(verbose)

    if !isempty(kwargs)
        warned = DiffEqBase.check_keywords(alg, kwargs, warnlist)
        if warned
            @SciMLMessage(
                "Unrecognized keyword arguments passed to Sundials solver.",
                verbose, :inconsistent_input
            )
        end
    end

    if prob.f.mass_matrix != LinearAlgebra.I
        error("This solver is not able to use mass matrices.")
    end

    if reltol isa AbstractArray
        error("Sundials only allows scalar reltol.")
    end

    if length(prob.u0) <= 0
        error("Sundials requires at least one state variable.")
    end

    progress &&
        Logging.@logmsg(Logging.LogLevel(-1), progress_name, _id = progress_id, progress = 0)

    tstops = vcat(tstops, d_discontinuities)
    callbacks_internal = DiffEqBase.CallbackSet(callback)

    max_len_cb = DiffEqBase.max_vector_callback_length(callbacks_internal)
    if max_len_cb isa VectorContinuousCallback
        callback_cache = DiffEqBase.CallbackCache(max_len_cb.len, Float64, Float64)
    else
        callback_cache = nothing
    end

    tspan = Float64.(prob.tspan)
    t0 = tspan[1]

    tdir = sign(tspan[2] - tspan[1])

    tstops_internal,
        saveat_internal = tstop_saveat_disc_handling(
        tstops, saveat, tdir,
        tspan, tType
    )

    if prob.u0 isa Number
        u0 = [prob.u0]
    else
        if alias_u0
            u0 = prob.u0
        else
            u0 = copy(prob.u0)
        end
    end

    ### Fix the more general function to Sundials allowed style
    if !isinplace && prob.u0 isa Number
        f! = (du, u, p, t) -> (du .= prob.f(first(u), p, t); Cint(0))
    elseif !isinplace
        f! = (du, u, p, t) -> (du .= prob.f(u, p, t); Cint(0))
    else # Then it's an in-place function on an abstract array
        f! = prob.f
    end

    if alg isa CVODE_BDF
        alg_code = CV_BDF
    elseif alg isa CVODE_Adams
        alg_code = CV_ADAMS
    else
        error("Algorithm must be CVODE_BDF or CVODE_Adams")
    end

    #if Method == :Newton
    #    method_code = CV_NEWTON
    #elseif Method ==  :Functional
    #    method_code = CV_FUNCTIONAL
    #end

    ctx_handle = ContextHandle()
    ctx = ctx_handle.ctx
    mem_ptr = CVodeCreate(alg_code, ctx)
    (mem_ptr == C_NULL) && error("Failed to allocate CVODE solver object")
    mem = Handle(mem_ptr)

    save_start ? ts = [t0] : ts = Float64[]

    out = copy(u0)
    uvec = vec(u0) # aliases u0
    utmp = NVector(uvec, ctx) # aliases u0

    use_jac_prototype = (
        isa(prob.f.jac_prototype, SparseArrays.SparseMatrixCSC) &&
            LinearSolver ∈ SPARSE_SOLVERS
    ) ||
        prob.f.jac_prototype isa AbstractSciMLOperator
    userfun = FunJac(
        f!,
        prob.f.jac,
        prob.p,
        nothing,
        use_jac_prototype ? prob.f.jac_prototype : nothing,
        alg.prec,
        alg.psetup,
        u0,
        out
    )

    function getcfunf(::T) where {T}
        return @cfunction(cvodefunjac, Cint, (realtype, N_Vector, N_Vector, Ref{T}))
    end

    flag = CVodeInit(mem, getcfunf(userfun), t0, utmp)

    dt !== nothing && (flag = CVodeSetInitStep(mem, Float64(dt)))
    flag = CVodeSetMinStep(mem, Float64(dtmin))
    flag = CVodeSetMaxStep(mem, Float64(dtmax))
    flag = CVodeSetUserData(mem, userfun)
    if abstol isa Array
        abstol_nvec = NVector(abstol, ctx)
        flag = CVodeSVtolerances(mem, reltol, abstol_nvec)
    else
        flag = CVodeSStolerances(mem, reltol, abstol)
    end
    flag = CVodeSetMaxNumSteps(mem, maxiters)
    flag = CVodeSetMaxOrd(mem, alg.max_order)
    flag = CVodeSetMaxHnilWarns(mem, alg.max_hnil_warns)
    flag = CVodeSetStabLimDet(mem, alg.stability_limit_detect)
    flag = CVodeSetMaxErrTestFails(mem, alg.max_error_test_failures)
    flag = CVodeSetMaxNonlinIters(mem, alg.max_nonlinear_iters)
    flag = CVodeSetMaxConvFails(mem, alg.max_convergence_failures)

    nojacobian = true

    if Method == :Newton # Only use a linear solver if it's a Newton-based method
        if LinearSolver in (:Dense, :LapackDense)
            nojacobian = false
            A = SUNDenseMatrix(length(uvec), length(uvec), ctx)
            _A = MatrixHandle(A, DenseMatrix())
            if LinearSolver === :Dense
                LS = SUNLinSol_Dense(utmp, A, ctx)
                _LS = LinSolHandle(LS, Dense())
            else
                LS = SUNLinSol_LapackDense(utmp, A, ctx)
                _LS = LinSolHandle(LS, LapackDense())
            end
        elseif LinearSolver in (:Band, :LapackBand)
            nojacobian = false
            A = SUNBandMatrix(length(uvec), alg.jac_upper, alg.jac_lower, ctx)
            _A = MatrixHandle(A, BandMatrix())
            if LinearSolver === :Band
                LS = SUNLinSol_Band(utmp, A, ctx)
                _LS = LinSolHandle(LS, Band())
            else
                LS = SUNLinSol_LapackBand(utmp, A, ctx)
                _LS = LinSolHandle(LS, LapackBand())
            end
        elseif LinearSolver == :Diagonal
            nojacobian = false
            flag = CVDiag(mem)
            _A = nothing
            _LS = nothing
        elseif LinearSolver == :GMRES
            LS = SUNLinSol_SPGMR(utmp, Cint(alg.prec_side), Cint(alg.krylov_dim), ctx)
            _A = nothing
            _LS = Sundials.LinSolHandle(LS, Sundials.SPGMR())
        elseif LinearSolver == :FGMRES
            LS = SUNLinSol_SPFGMR(utmp, Cint(alg.prec_side), Cint(alg.krylov_dim), ctx)
            _A = nothing
            _LS = LinSolHandle(LS, SPFGMR())
        elseif LinearSolver == :BCG
            LS = SUNLinSol_SPBCGS(utmp, Cint(alg.prec_side), Cint(alg.krylov_dim), ctx)
            _A = nothing
            _LS = LinSolHandle(LS, SPBCGS())
        elseif LinearSolver == :PCG
            LS = SUNLinSol_PCG(utmp, Cint(alg.prec_side), Cint(alg.krylov_dim), ctx)
            _A = nothing
            _LS = LinSolHandle(LS, PCG())
        elseif LinearSolver == :TFQMR
            LS = SUNLinSol_SPTFQMR(utmp, Cint(alg.prec_side), Cint(alg.krylov_dim), ctx)
            _A = nothing
            _LS = LinSolHandle(LS, PTFQMR())
        elseif LinearSolver == :KLU
            nojacobian = false
            nnz = length(SparseArrays.nonzeros(prob.f.jac_prototype))
            A = SUNSparseMatrix(length(uvec), length(uvec), nnz, CSC_MAT, ctx)
            LS = SUNLinSol_KLU(utmp, A, ctx)
            _A = MatrixHandle(A, SparseMatrix())
            _LS = LinSolHandle(LS, KLU())
        end
        if LinearSolver !== :Diagonal
            flag = CVodeSetLinearSolver(mem, LS, _A === nothing ? C_NULL : A)
        end
        NLS = SUNNonlinSol_Newton(utmp, ctx)
        CVodeSetNonlinearSolver(mem, NLS)
    else
        _A = nothing
        _LS = nothing
        # TODO: Anderson Acceleration
        anderson_m = Cint(0)
        NLS = SUNNonlinSol_FixedPoint(utmp, anderson_m, ctx)
        CVodeSetNonlinearSolver(mem, NLS)
    end

    if SciMLBase.has_jac(prob.f) && Method == :Newton
        function getcfunjac(::T) where {T}
            return @cfunction(
                cvodejac,
                Cint,
                (
                    realtype,
                    N_Vector,
                    N_Vector,
                    SUNMatrix,
                    Ref{T},
                    N_Vector,
                    N_Vector,
                    N_Vector,
                )
            )
        end
        jac = getcfunjac(userfun)
        flag = CVodeSetUserData(mem, userfun)
        nojacobian || (flag = CVodeSetJacFn(mem, jac))
    else
        jac = nothing
    end

    if prob.f.jac_prototype isa AbstractSciMLOperator
        "here!!!!"
        function getcfunjtimes(::T) where {T}
            return @cfunction(
                jactimes,
                Cint,
                (N_Vector, N_Vector, realtype, N_Vector, N_Vector, Ref{T}, N_Vector)
            )
        end
        jtimes = getcfunjtimes(userfun)
        CVodeSetJacTimes(mem, C_NULL, jtimes)
    end

    if alg.prec !== nothing
        function getpercfun(::T) where {T}
            return @cfunction(
                precsolve,
                Cint,
                (
                    Float64,
                    N_Vector,
                    N_Vector,
                    N_Vector,
                    N_Vector,
                    Float64,
                    Float64,
                    Int,
                    Ref{T},
                )
            )
        end
        precfun = getpercfun(userfun)

        function getpsetupfun(::T) where {T}
            return @cfunction(
                precsetup,
                Cint,
                (Float64, N_Vector, N_Vector, Int, Ptr{Int}, Float64, Ref{T})
            )
        end
        psetupfun = alg.psetup === nothing ? C_NULL : getpsetupfun(userfun)

        CVodeSetPreconditioner(mem, psetupfun, precfun)
    end

    tmp = isnothing(callbacks_internal) ? u0 : similar(u0)
    uprev = isnothing(callbacks_internal) ? u0 : similar(u0)
    tout = [tspan[1]]

    if save_start
        if save_idxs === nothing
            ures = Vector{uType}()
            dures = Vector{uType}()
            save_value!(ures, u0, uType, save_idxs)
            if dense
                f!(out, u0, prob.p, tspan[1])
                save_value!(dures, out, uType, save_idxs)
            end
        else
            ures = [u0[save_idxs]]
            if dense
                f!(out, u0, prob.p, tspan[1])
                dures = [out[save_idxs]]
            end
        end
    else
        ures = Vector{uType}()
        dures = Vector{uType}()
    end

    sol = SciMLBase.build_solution(
        prob,
        alg,
        ts,
        ures;
        dense = dense,
        interp = dense ?
            SciMLBase.HermiteInterpolation(
                ts, ures,
                dures
            ) :
            SciMLBase.LinearInterpolation(ts, ures),
        timeseries_errors = timeseries_errors,
        stats = SciMLBase.DEStats(0),
        calculate_error = false
    )
    opts = DEOptions(
        saveat_internal,
        tstops_internal,
        saveat, tstops, save_start,
        save_everystep, save_idxs,
        dense,
        timeseries_errors,
        dense_errors,
        save_on,
        save_end,
        callbacks_internal,
        abstol,
        reltol,
        verbose,
        advance_to_tstop,
        stop_at_next_tstop,
        progress,
        progress_steps,
        progress_name,
        progress_message,
        progress_id,
        maxiters
    )
    integrator = CVODEIntegrator(
        u0,
        utmp,
        prob.p,
        t0,
        t0,
        mem,
        _LS,
        _A,
        sol,
        alg,
        f!,
        userfun,
        jac,
        opts,
        tout,
        tdir,
        false,
        tmp,
        uprev,
        Cint(flag),
        false,
        0,
        1,
        callback_cache,
        0.0,
        initializealg,
        ctx_handle
    )
    SciMLBase.initialize_dae!(integrator, initializealg)
    integrator.u_modified && CVodeReInit(integrator.mem, integrator.t, integrator.u_nvec)
    initialize_callbacks!(integrator)
    return integrator
end # function solve

function DiffEqBase.__init(
        prob::SciMLBase.AbstractODEProblem{uType, tupType, isinplace},
        alg::ARKODE{Method, LinearSolver, MassLinearSolver},
        timeseries = [],
        ts = [],
        ks = [];
        verbose = Standard(),
        callback = nothing,
        abstol = 1 / 10^6,
        reltol = 1 / 10^3,
        saveat = Float64[],
        tstops = Float64[],
        d_discontinuities = Float64[],
        maxiters = Int(1.0e5),
        dt = nothing,
        dtmin = 0.0,
        dtmax = 0.0,
        timeseries_errors = true,
        dense_errors = false,
        save_everystep = isempty(saveat), save_idxs = nothing,
        dense = save_everystep,
        save_on = true,
        save_start = save_everystep || isempty(saveat) ||
            saveat isa Number ? true :
            prob.tspan[1] in saveat,
        save_end = save_everystep || isempty(saveat) ||
            saveat isa Number ? true :
            prob.tspan[2] in saveat,
        save_timeseries = nothing,
        progress = false,
        progress_steps = 1000,
        progress_name = "ODE",
        progress_message = DiffEqBase.ODE_DEFAULT_PROG_MESSAGE,
        progress_id = :Sundials,
        advance_to_tstop = false,
        stop_at_next_tstop = false,
        userdata = nothing,
        alias_u0 = false,
        initializealg = DefaultInit(),
        kwargs...
    ) where {
        uType, tupType, isinplace, Method,
        LinearSolver,
        MassLinearSolver,
    }
    tType = eltype(tupType)
    verbose = _process_verbose_param(verbose)

    if !isempty(kwargs)
        warned = DiffEqBase.check_keywords(alg, kwargs, warnlist)
        if warned
            @SciMLMessage(
                "Unrecognized keyword arguments passed to Sundials solver.",
                verbose, :inconsistent_input
            )
        end
    end

    if reltol isa AbstractArray
        error("Sundials only allows scalar reltol.")
    end

    if length(prob.u0) <= 0
        error("Sundials requires at least one state variable.")
    end

    progress &&
        Logging.@logmsg(Logging.LogLevel(-1), progress_name, _id = progress_id, progress = 0)

    tstops = vcat(tstops, d_discontinuities)
    callbacks_internal = DiffEqBase.CallbackSet(callback)

    max_len_cb = DiffEqBase.max_vector_callback_length(callbacks_internal)
    if max_len_cb isa VectorContinuousCallback
        callback_cache = DiffEqBase.CallbackCache(max_len_cb.len, Float64, Float64)
    else
        callback_cache = nothing
    end

    tspan = prob.tspan
    t0 = tspan[1]

    tdir = sign(tspan[2] - tspan[1])

    tstops_internal,
        saveat_internal = tstop_saveat_disc_handling(
        tstops, saveat, tdir,
        tspan, tType
    )

    if prob.u0 isa Number
        u0 = [prob.u0]
    else
        if alias_u0
            u0 = prob.u0
        else
            u0 = copy(prob.u0)
        end
    end

    save_start ? ts = [t0] : ts = Float64[]
    out = copy(u0)
    uvec = vec(u0)
    ctx_handle = ContextHandle()
    ctx = ctx_handle.ctx
    utmp = NVector(uvec, ctx)

    function arkodemem(; fe = C_NULL, fi = C_NULL, t0 = t0, u0 = utmp)
        mem_ptr = ARKStepCreate(fe, fi, t0, u0, ctx)
        (mem_ptr == C_NULL) && error("Failed to allocate ARKODE solver object")
        mem = Handle(mem_ptr)

        return mem
    end

    function erkodemem(; f = C_NULL, t0 = t0, u0 = utmp)
        mem_ptr = ERKStepCreate(f, t0, u0, ctx)
        (mem_ptr == C_NULL) && error("Failed to allocate ERKODE solver object")
        mem = Handle(mem_ptr)

        return mem
    end

    ### Fix the more general function to Sundials allowed style
    if !isinplace && prob.u0 isa Number
        f! = (du, u, p, t) -> (du .= prob.f(first(u), p, t); Cint(0))
    elseif !isinplace
        f! = (du, u, p, t) -> (du .= prob.f(u, p, t); Cint(0))
    else # Then it's an in-place function on an abstract array
        f! = prob.f
    end

    cfj1, cfj2, mem = if prob.problem_type isa SplitODEProblem

        ### Fix the more general function to Sundials allowed style
        if !isinplace && prob.u0 isa Number
            f1! = (du, u, p, t) -> (du .= prob.f.f1(first(u), p, t); Cint(0))
            f2! = (du, u, p, t) -> (du .= prob.f.f2(first(u), p, t); Cint(0))
        elseif !isinplace
            f1! = (du, u, p, t) -> (du .= prob.f.f1(u, p, t); Cint(0))
            f2! = (du, u, p, t) -> (du .= prob.f.f2(u, p, t); Cint(0))
        else # Then it's an in-place function on an abstract array
            f1! = prob.f.f1
            f2! = prob.f.f2
        end

        use_jac_prototype = (
            isa(prob.f.f1.jac_prototype, SparseArrays.SparseMatrixCSC) &&
                LinearSolver ∈ SPARSE_SOLVERS
        )
        userfun = FunJac(
            f1!,
            f2!,
            prob.f.f1.jac,
            prob.p,
            prob.f.mass_matrix,
            use_jac_prototype ? prob.f.f1.jac_prototype : nothing,
            alg.prec,
            alg.psetup,
            u0,
            out,
            nothing
        )

        function getcfunjac(::T) where {T}
            return @cfunction(cvodefunjac, Cint, (realtype, N_Vector, N_Vector, Ref{T}))
        end
        function getcfunjac2(::T) where {T}
            return @cfunction(cvodefunjac2, Cint, (realtype, N_Vector, N_Vector, Ref{T}))
        end
        cfj1 = getcfunjac(userfun)
        cfj2 = getcfunjac2(userfun)

        mem = arkodemem(; fi = cfj1, fe = cfj2)
        cfj1, cfj2, mem
    else
        use_jac_prototype = (
            isa(prob.f.jac_prototype, SparseArrays.SparseMatrixCSC) &&
                LinearSolver ∈ SPARSE_SOLVERS
        )
        userfun = FunJac(
            f!,
            prob.f.jac,
            prob.p,
            prob.f.mass_matrix,
            use_jac_prototype ? prob.f.jac_prototype : nothing,
            alg.prec,
            alg.psetup,
            u0,
            out
        )
        if alg.stiffness == Explicit()
            function getcfun1(::T) where {T}
                return @cfunction(cvodefunjac, Cint, (realtype, N_Vector, N_Vector, Ref{T}))
            end
            cfj1 = getcfun1(userfun)
            cfj2 = C_NULL
            mem = erkodemem(; f = cfj1)
            cfj1, cfj2, mem
        elseif alg.stiffness == Implicit()
            function getcfun2(::T) where {T}
                return @cfunction(cvodefunjac, Cint, (realtype, N_Vector, N_Vector, Ref{T}))
            end
            cfj1 = C_NULL
            cfj2 = getcfun2(userfun)
            mem = arkodemem(; fi = cfj2)
            cfj1, cfj2, mem
        end
    end

    # Use ERKStep functions for explicit methods, ARKStep for others
    is_explicit = (alg.stiffness == Explicit() && !isa(prob.problem_type, SplitODEProblem))

    if is_explicit
        # ERKStep functions
        dt !== nothing && (flag = ERKStepSetInitStep(mem, Float64(dt)))
        flag = ERKStepSetMinStep(mem, Float64(dtmin))
        flag = ERKStepSetMaxStep(mem, Float64(dtmax))
        flag = ERKStepSetUserData(mem, userfun)
        if abstol isa Array
            abstol_nvec = NVector(abstol, ctx)
            flag = ERKStepSVtolerances(mem, reltol, abstol_nvec)
        else
            flag = ERKStepSStolerances(mem, reltol, abstol)
        end
        flag = ERKStepSetMaxNumSteps(mem, maxiters)
        flag = ERKStepSetMaxHnilWarns(mem, alg.max_hnil_warns)
        flag = ERKStepSetMaxErrTestFails(mem, alg.max_error_test_failures)
        # ERKStep doesn't have max convergence fails (no nonlinear solver)
        # ERKStep doesn't have predictor or nonlinear convergence settings
        flag = ERKStepSetDenseOrder(mem, alg.dense_order)
    else
        # ARKStep functions
        dt !== nothing && (flag = ARKStepSetInitStep(mem, Float64(dt)))
        flag = ARKStepSetMinStep(mem, Float64(dtmin))
        flag = ARKStepSetMaxStep(mem, Float64(dtmax))
        flag = ARKStepSetUserData(mem, userfun)
        if abstol isa Array
            abstol_nvec = NVector(abstol, ctx)
            flag = ARKStepSVtolerances(mem, reltol, abstol_nvec)
        else
            flag = ARKStepSStolerances(mem, reltol, abstol)
        end
        flag = ARKStepSetMaxNumSteps(mem, maxiters)
        flag = ARKStepSetMaxHnilWarns(mem, alg.max_hnil_warns)
        flag = ARKStepSetMaxErrTestFails(mem, alg.max_error_test_failures)
        flag = ARKStepSetMaxConvFails(mem, alg.max_convergence_failures)
        flag = ARKStepSetPredictorMethod(mem, alg.predictor_method)
        flag = ARKStepSetNonlinConvCoef(mem, alg.nonlinear_convergence_coefficient)
        flag = ARKStepSetDenseOrder(mem, alg.dense_order)
    end

    #=
    Reference from Manual on ARKODE
    To choose an explicit table, set itable to a negative value. This automatically calls ARKStepSetExplicit(). However, if the problem is posed in explicit form, i.e. 𝑦 ̇ = 𝑓 (𝑡, 𝑦), then we recommend that the ERKStep time- stepper module be used instead of ARKStep.
    To select an implicit table, set etable to a negative value. This automatically calls ARKStepSetImplicit(). If both itable and etable are non-negative, then these should match an existing implicit/explicit pair, listed in the section Additive Butcher tables. This automatically calls ARKStepSetImEx().
    =#
    if is_explicit
        # ERKStep table settings
        if alg.etable !== nothing
            flag = ERKStepSetTableNum(mem, alg.etable)
        else
            flag = ERKStepSetOrder(mem, alg.order)
        end
    else
        # ARKStep table settings
        if alg.itable === nothing && alg.etable === nothing
            flag = ARKStepSetOrder(mem, alg.order)
        elseif alg.itable === nothing && alg.etable !== nothing
            flag = ARKStepSetTableNum(mem, -1, alg.etable)
        elseif alg.itable !== nothing && alg.etable === nothing
            flag = ARKStepSetTableNum(mem, alg.itable, -1)
        else
            flag = ARKStepSetTableNum(mem, alg.itable, alg.etable)
        end

        flag = ARKStepSetNonlinCRDown(mem, alg.crdown)
        flag = ARKStepSetNonlinRDiv(mem, alg.rdiv)
        flag = ARKStepSetDeltaGammaMax(mem, alg.dgmax)
        flag = ARKStepSetLSetupFrequency(mem, alg.msbp)
        #flag = ARKStepSetAdaptivityMethod(mem,alg.adaptivity_method,1,0)

        #flag = ARKStepSetFixedStep(mem,)
        alg.set_optimal_params && (flag = ARKStepSetOptimalParams(mem))
    end

    if Method == :Newton && alg.stiffness !== Explicit() # Only use a linear solver if it's a Newton-based method
        if LinearSolver in (:Dense, :LapackDense)
            nojacobian = false
            A = SUNDenseMatrix(length(uvec), length(uvec), ctx)
            _A = MatrixHandle(A, DenseMatrix())
            if LinearSolver === :Dense
                LS = SUNLinSol_Dense(utmp, A, ctx)
                _LS = LinSolHandle(LS, Dense())
            else
                LS = SUNLinSol_LapackDense(utmp, A, ctx)
                _LS = LinSolHandle(LS, LapackDense())
            end
        elseif LinearSolver in (:Band, :LapackBand)
            nojacobian = false
            A = SUNBandMatrix(length(uvec), alg.jac_upper, alg.jac_lower, ctx)
            _A = MatrixHandle(A, BandMatrix())
            if LinearSolver === :Band
                LS = SUNLinSol_Band(utmp, A, ctx)
                _LS = LinSolHandle(LS, Band())
            else
                LS = SUNLinSol_LapackBand(utmp, A, ctx)
                _LS = LinSolHandle(LS, LapackBand())
            end
        elseif LinearSolver == :GMRES
            LS = SUNLinSol_SPGMR(utmp, Cint(alg.prec_side), Cint(alg.krylov_dim), ctx)
            _A = nothing
            _LS = Sundials.LinSolHandle(LS, Sundials.SPGMR())
        elseif LinearSolver == :FGMRES
            LS = SUNLinSol_SPFGMR(utmp, Cint(alg.prec_side), Cint(alg.krylov_dim), ctx)
            _A = nothing
            _LS = LinSolHandle(LS, SPFGMR())
        elseif LinearSolver == :BCG
            LS = SUNLinSol_SPBCGS(utmp, Cint(alg.prec_side), Cint(alg.krylov_dim), ctx)
            _A = nothing
            _LS = LinSolHandle(LS, SPBCGS())
        elseif LinearSolver == :PCG
            LS = SUNLinSol_PCG(utmp, Cint(alg.prec_side), Cint(alg.krylov_dim), ctx)
            _A = nothing
            _LS = LinSolHandle(LS, PCG())
        elseif LinearSolver == :TFQMR
            LS = SUNLinSol_SPTFQMR(utmp, Cint(alg.prec_side), Cint(alg.krylov_dim), ctx)
            _A = nothing
            _LS = LinSolHandle(LS, PTFQMR())
        elseif LinearSolver == :KLU
            nnz = length(SparseArrays.nonzeros(prob.f.jac_prototype))
            A = SUNSparseMatrix(length(uvec), length(uvec), nnz, CSC_MAT, ctx)
            LS = SUNLinSol_KLU(utmp, A, ctx)
            _A = MatrixHandle(A, SparseMatrix())
            _LS = LinSolHandle(LS, KLU())
        end
        flag = ARKStepSetLinearSolver(mem, LS, _A === nothing ? C_NULL : A)
        flag = ARKStepSetMaxNonlinIters(mem, alg.max_nonlinear_iters)
    elseif Method == :Functional && alg.stiffness !== Explicit()
        ARKStepSetFixedPoint(mem, Clong(alg.krylov_dim))
    else
        _A = nothing
        _LS = nothing
    end

    if (
            prob.problem_type isa SplitODEProblem &&
                prob.f.f1.jac_prototype isa AbstractSciMLOperator
        ) ||
            (
            !(prob.problem_type isa SplitODEProblem) &&
                prob.f.jac_prototype isa AbstractSciMLOperator
        ) &&
            alg.stiffness !== Explicit()
        function getcfunjtimes(::T) where {T}
            return @cfunction(
                jactimes,
                Cint,
                (N_Vector, N_Vector, realtype, N_Vector, N_Vector, Ref{T}, N_Vector)
            )
        end
        jtimes = getcfunjtimes(userfun)
        ARKStepSetJacTimes(mem, C_NULL, jtimes)
    end

    if prob.f.mass_matrix != LinearAlgebra.I && alg.stiffness !== Explicit()
        if MassLinearSolver in (:Dense, :LapackDense)
            nojacobian = false
            M = SUNDenseMatrix(length(uvec), length(uvec), ctx)
            _M = MatrixHandle(M, DenseMatrix())
            if MassLinearSolver === :Dense
                MLS = SUNLinSol_Dense(utmp, M, ctx)
                _MLS = LinSolHandle(MLS, Dense())
            else
                MLS = SUNLinSol_LapackDense(utmp, M, ctx)
                _MLS = LinSolHandle(MLS, LapackDense())
            end
        elseif MassLinearSolver in (:Band, :LapackBand)
            nojacobian = false
            M = SUNBandMatrix(length(uvec), alg.jac_upper, alg.jac_lower, ctx)
            _M = MatrixHandle(M, BandMatrix())
            if MassLinearSolver === :Band
                MLS = SUNLinSol_Band(utmp, M, ctx)
                _MLS = LinSolHandle(MLS, Band())
            else
                MLS = SUNLinSol_LapackBand(utmp, M, ctx)
                _MLS = LinSolHandle(MLS, LapackBand())
            end
        elseif MassLinearSolver == :GMRES
            MLS = SUNLinSol_SPGMR(utmp, Cint(alg.prec_side), Cint(alg.mass_krylov_dim), ctx)
            _M = nothing
            _MLS = LinSolHandle(MLS, SPGMR())
        elseif MassLinearSolver == :FGMRES
            MLS = SUNLinSol_SPGMR(utmp, Cint(alg.prec_side), Cint(alg.mass_krylov_dim), ctx)
            _M = nothing
            _MLS = LinSolHandle(MLS, SPFGMR())
        elseif MassLinearSolver == :BCG
            MLS = SUNLinSol_SPGMR(utmp, Cint(alg.prec_side), Cint(alg.mass_krylov_dim), ctx)
            _M = nothing
            _MLS = LinSolHandle(MLS, SPBCGS())
        elseif MassLinearSolver == :PCG
            MLS = SUNLinSol_SPGMR(utmp, Cint(alg.prec_side), Cint(alg.mass_krylov_dim), ctx)
            _M = nothing
            _MLS = LinSolHandle(MLS, PCG())
        elseif MassLinearSolver == :TFQMR
            MLS = SUNLinSol_SPGMR(utmp, Cint(alg.prec_side), Cint(alg.mass_krylov_dim), ctx)
            _M = nothing
            _MLS = LinSolHandle(MLS, PTFQMR())
        elseif MassLinearSolver == :KLU
            nnz = length(SparseArrays.nonzeros(prob.f.mass_matrix))
            M = SUNSparseMatrix(length(uvec), length(uvec), nnz, CSC_MAT, ctx)
            MLS = SUNLinSol_KLU(utmp, M, ctx)
            _M = MatrixHandle(M, SparseMatrix())
            _MLS = LinSolHandle(MLS, KLU())
        end
        flag = ARKStepSetMassLinearSolver(mem, MLS, _M === nothing ? C_NULL : M, false)
        function getmatfun(::T) where {T}
            return @cfunction(
                massmat,
                Cint,
                (realtype, SUNMatrix, Ref{T}, N_Vector, N_Vector, N_Vector)
            )
        end
        matfun = getmatfun(userfun)
        ARKStepSetMassFn(mem, matfun)
    else
        _M = nothing
        _MLS = nothing
    end

    if SciMLBase.has_jac(prob.f) && alg.stiffness !== Explicit()
        function getfunjac(::T) where {T}
            return @cfunction(
                cvodejac,
                Cint,
                (
                    realtype,
                    N_Vector,
                    N_Vector,
                    SUNMatrix,
                    Ref{T},
                    N_Vector,
                    N_Vector,
                    N_Vector,
                )
            )
        end
        jac = getfunjac(userfun)
        flag = ARKStepSetUserData(mem, userfun)
        flag = ARKStepSetJacFn(mem, jac)
    else
        jac = nothing
    end

    if alg.prec !== nothing && alg.stiffness !== Explicit()
        function getpercfun(::T) where {T}
            return @cfunction(
                precsolve,
                Cint,
                (
                    Float64,
                    N_Vector,
                    N_Vector,
                    N_Vector,
                    N_Vector,
                    Float64,
                    Float64,
                    Int,
                    Ref{T},
                )
            )
        end
        precfun = getpercfun(userfun)

        function getpsetupfun(::T) where {T}
            return @cfunction(
                precsetup,
                Cint,
                (Float64, N_Vector, N_Vector, Int, Ptr{Int}, Float64, Ref{T})
            )
        end
        psetupfun = alg.psetup === nothing ? C_NULL : getpsetupfun(userfun)

        ARKStepSetPreconditioner(mem, psetupfun, precfun)
    end

    tmp = isnothing(callbacks_internal) ? u0 : similar(u0)
    uprev = isnothing(callbacks_internal) ? u0 : similar(u0)
    tout = [tspan[1]]

    if save_start
        if save_idxs === nothing
            ures = Vector{uType}()
            dures = Vector{uType}()
            save_value!(ures, u0, uType, save_idxs)
            if dense
                f!(out, u0, prob.p, tspan[1])
                save_value!(dures, out, uType, save_idxs)
            end
        else
            ures = [u0[save_idxs]]
            if dense
                f!(out, u0, prob.p, tspan[1])
                dures = [out[save_idxs]]
            end
        end
    else
        ures = Vector{uType}()
        dures = Vector{uType}()
    end

    sol = SciMLBase.build_solution(
        prob,
        alg,
        ts,
        ures;
        dense = dense,
        interp = dense ?
            SciMLBase.HermiteInterpolation(
                ts, ures,
                dures
            ) :
            SciMLBase.LinearInterpolation(ts, ures),
        timeseries_errors = timeseries_errors,
        stats = SciMLBase.DEStats(0),
        calculate_error = false
    )
    opts = DEOptions(
        saveat_internal,
        tstops_internal,
        saveat, tstops, save_start,
        save_everystep, save_idxs,
        dense,
        timeseries_errors,
        dense_errors,
        save_on,
        save_end,
        callbacks_internal,
        abstol,
        reltol,
        verbose,
        advance_to_tstop,
        stop_at_next_tstop,
        progress,
        progress_steps,
        progress_name,
        progress_message,
        progress_id,
        maxiters
    )
    integrator = ARKODEIntegrator(
        u0,
        utmp,
        prob.p,
        t0,
        t0,
        mem,
        _LS,
        _A,
        _MLS,
        _M,
        sol,
        alg,
        f!,
        userfun,
        jac,
        opts,
        tout,
        tdir,
        false,
        tmp,
        uprev,
        Cint(flag),
        false,
        0,
        1,
        callback_cache,
        0.0,
        initializealg,
        cfj1, cfj2,
        ctx_handle
    )
    SciMLBase.initialize_dae!(integrator, initializealg)
    integrator.u_modified &&  ARKStepReInit(
        integrator.mem, integrator.cfj2, integrator.cfj1,
        integrator.t, integrator.u_nvec
    )

    initialize_callbacks!(integrator)
    return integrator
end # function solve

function tstop_saveat_disc_handling(tstops, saveat, tdir, tspan, tType)
    tstops_internal = DataStructures.BinaryHeap{tType}(DataStructures.FasterForward())
    saveat_internal = DataStructures.BinaryHeap{tType}(DataStructures.FasterForward())

    t0, tf = tspan
    tdir_t0 = tdir * t0
    tdir_tf = tdir * tf

    for t in tstops
        tdir_t = tdir * t
        tdir_t0 < tdir_t ≤ tdir_tf && push!(tstops_internal, tdir_t)
    end
    push!(tstops_internal, tdir_tf)

    if saveat isa Number
        saveat = (t0:(tdir * abs(saveat)):tf)[2:end]
    end
    for t in saveat
        tdir_t = tdir * t
        tdir_t0 < tdir_t ≤ tdir_tf && push!(saveat_internal, tdir_t)
    end

    return tstops_internal, saveat_internal
end

## Solve for DAEs uses IDA

function DiffEqBase.__init(
        prob::SciMLBase.AbstractDAEProblem{
            uType, duType, tupType,
            isinplace,
        },
        alg::SundialsDAEAlgorithm{LinearSolver},
        timeseries = [],
        ts = [],
        ks = [];
        verbose = Standard(),
        dt = nothing,
        dtmax = 0.0,
        save_on = true,
        save_start = true,
        callback = nothing,
        abstol = 1 / 10^6,
        reltol = 1 / 10^3,
        saveat = Float64[],
        tstops = Float64[],
        d_discontinuities = Float64[],
        maxiters = Int(1.0e5),
        timeseries_errors = true,
        dense_errors = false,
        save_everystep = isempty(saveat), save_idxs = nothing,
        dense = save_everystep,
        save_timeseries = nothing,
        save_end = true,
        progress = false,
        progress_steps = 1000,
        progress_name = "DAE IDA",
        progress_message = DiffEqBase.ODE_DEFAULT_PROG_MESSAGE,
        progress_id = :Sundials,
        advance_to_tstop = false,
        stop_at_next_tstop = false,
        userdata = nothing,
        initializealg = DefaultInit(),
        kwargs...
    ) where {
        uType, duType, tupType, isinplace, LinearSolver,
    }
    tType = eltype(tupType)
    verbose = _process_verbose_param(verbose)

    if !isempty(kwargs)
        warned = DiffEqBase.check_keywords(alg, kwargs, warnida)
        if warned
            @SciMLMessage(
                "Unrecognized keyword arguments passed to Sundials solver.",
                verbose, :inconsistent_input
            )
        end
    end

    if reltol isa AbstractArray
        error("Sundials only allows scalar reltol.")
    end

    if length(prob.u0) == 0
        error("Sundials requires at least one state variable.")
    end

    progress &&
        Logging.@logmsg(Logging.LogLevel(-1), progress_name, _id = progress_id, progress = 0)

    tstops = vcat(tstops, d_discontinuities)
    callbacks_internal = DiffEqBase.CallbackSet(callback)

    max_len_cb = DiffEqBase.max_vector_callback_length(callbacks_internal)
    if max_len_cb isa VectorContinuousCallback
        callback_cache = DiffEqBase.CallbackCache(max_len_cb.len, Float64, Float64)
    else
        callback_cache = nothing
    end

    tspan = prob.tspan
    t0 = tspan[1]

    tdir = sign(tspan[2] - tspan[1])

    tstops_internal,
        saveat_internal = tstop_saveat_disc_handling(
        tstops, saveat, tdir,
        tspan, tType
    )
    @assert size(prob.u0) == size(prob.du0)
    if prob.u0 isa Number
        u0 = [prob.u0]
        du0 = [prob.du0]
    else
        u0 = copy(prob.u0)
        du0 = copy(prob.du0)
    end

    ### Fix the more general function to Sundials allowed style
    if !isinplace && prob.u0 isa Number
        f! = (out, du, u, p, t) -> (out .= prob.f(first(du), first(u), p, t); Cint(0))
    elseif !isinplace
        f! = (out, du, u, p, t) -> (out .= prob.f(du, u, p, t); Cint(0))
    else # Then it's an in-place function on an abstract array
        f! = prob.f
    end

    ctx_handle = ContextHandle()
    ctx = ctx_handle.ctx
    mem_ptr = IDACreate(ctx)
    (mem_ptr == C_NULL) && error("Failed to allocate IDA solver object")
    mem = Handle(mem_ptr)

    ts = [t0]

    # vec shares memory
    utmp = NVector(vec(u0), ctx)
    dutmp = NVector(vec(du0), ctx)
    rtest = zeros(size(u0))

    use_jac_prototype = (
        isa(prob.f.jac_prototype, SparseArrays.SparseMatrixCSC) &&
            LinearSolver ∈ SPARSE_SOLVERS
    )
    userfun = FunJac(
        f!,
        prob.f.jac,
        prob.p,
        nothing,
        use_jac_prototype ? prob.f.jac_prototype : nothing,
        alg.prec,
        alg.psetup,
        u0,
        du0,
        rtest
    )

    function getcfun(::T) where {T}
        return @cfunction(idasolfun, Cint, (realtype, N_Vector, N_Vector, N_Vector, Ref{T}))
    end
    cfun = getcfun(userfun)
    flag = IDAInit(mem, cfun, t0, utmp, dutmp)
    dt !== nothing && (flag = IDASetInitStep(mem, dt))
    flag = IDASetUserData(mem, userfun)
    flag = IDASetMaxStep(mem, dtmax)
    if abstol isa Array
        abstol_nvec = NVector(abstol, ctx)
        flag = IDASVtolerances(mem, reltol, abstol_nvec)
    else
        flag = IDASStolerances(mem, reltol, abstol)
    end
    flag = IDASetMaxNumSteps(mem, maxiters)
    flag = IDASetMaxOrd(mem, alg.max_order)
    flag = IDASetMaxErrTestFails(mem, alg.max_error_test_failures)
    flag = IDASetNonlinConvCoef(mem, alg.nonlinear_convergence_coefficient)
    flag = IDASetMaxNonlinIters(mem, alg.max_nonlinear_iters)
    flag = IDASetMaxConvFails(mem, alg.max_convergence_failures)
    flag = IDASetNonlinConvCoefIC(mem, alg.nonlinear_convergence_coefficient_ic)
    flag = IDASetMaxNumStepsIC(mem, alg.max_num_steps_ic)
    flag = IDASetMaxNumJacsIC(mem, alg.max_num_jacs_ic)
    flag = IDASetMaxNumItersIC(mem, alg.max_num_iters_ic)
    #flag = IDASetMaxBacksIC(mem,alg.max_num_backs_ic) # Needs newer version?
    flag = IDASetLineSearchOffIC(mem, alg.use_linesearch_ic)

    prec_side = isnothing(alg.prec) ? 0 : 1  # IDA only supports left preconditioning (prec_side = 1)
    if LinearSolver in (:Dense, :LapackDense)
        nojacobian = false
        A = SUNDenseMatrix(length(u0), length(u0), ctx)
        _A = MatrixHandle(A, DenseMatrix())
        if LinearSolver === :Dense
            LS = SUNLinSol_Dense(utmp, A, ctx)
            _LS = LinSolHandle(LS, Dense())
        else
            LS = SUNLinSol_LapackDense(utmp, A, ctx)
            _LS = LinSolHandle(LS, LapackDense())
        end
    elseif LinearSolver in (:Band, :LapackBand)
        nojacobian = false
        A = SUNBandMatrix(length(u0), alg.jac_upper, alg.jac_lower, ctx)
        _A = MatrixHandle(A, BandMatrix())
        if LinearSolver === :Band
            LS = SUNLinSol_Band(utmp, A, ctx)
            _LS = LinSolHandle(LS, Band())
        else
            LS = SUNLinSol_LapackBand(utmp, A, ctx)
            _LS = LinSolHandle(LS, LapackBand())
        end
    elseif LinearSolver == :GMRES
        LS = SUNLinSol_SPGMR(utmp, Cint(prec_side), Cint(alg.krylov_dim), ctx)
        _A = nothing
        _LS = LinSolHandle(LS, SPGMR())
    elseif LinearSolver == :FGMRES
        LS = SUNLinSol_SPFGMR(utmp, Cint(prec_side), Cint(alg.krylov_dim), ctx)
        _A = nothing
        _LS = LinSolHandle(LS, SPFGMR())
    elseif LinearSolver == :BCG
        LS = SUNLinSol_SPBCGS(utmp, Cint(prec_side), Cint(alg.krylov_dim), ctx)
        _A = nothing
        _LS = LinSolHandle(LS, SPBCGS())
    elseif LinearSolver == :PCG
        LS = SUNLinSol_PCG(utmp, Cint(prec_side), Cint(alg.krylov_dim), ctx)
        _A = nothing
        _LS = LinSolHandle(LS, PCG())
    elseif LinearSolver == :TFQMR
        LS = SUNLinSol_SPTFQMR(utmp, Cint(prec_side), Cint(alg.krylov_dim), ctx)
        _A = nothing
        _LS = LinSolHandle(LS, PTFQMR())
    elseif LinearSolver == :KLU
        nnz = length(SparseArrays.nonzeros(prob.f.jac_prototype))
        A = SUNSparseMatrix(length(u0), length(u0), nnz, Sundials.CSC_MAT, ctx)
        LS = SUNLinSol_KLU(utmp, A, ctx)
        _A = MatrixHandle(A, SparseMatrix())
        _LS = LinSolHandle(LS, KLU())
    end
    flag = IDASetLinearSolver(mem, LS, _A === nothing ? C_NULL : A)

    if prob.f.jac_prototype isa AbstractSciMLOperator
        function getcfunjtimes(::T) where {T}
            return @cfunction(
                idajactimes,
                Cint,
                (
                    realtype,
                    N_Vector,
                    N_Vector,
                    N_Vector,
                    N_Vector,
                    N_Vector,
                    realtype,
                    Ref{T},
                    N_Vector,
                    N_Vector,
                )
            )
        end
        jtimes = getcfunjtimes(userfun)
        IDASetJacTimes(mem, C_NULL, jtimes)
    end

    if alg.prec !== nothing
        function getprecfun(::T) where {T}
            return @cfunction(
                idaprecsolve,
                Cint,
                (
                    Float64,
                    N_Vector,
                    N_Vector,
                    N_Vector,
                    N_Vector,
                    N_Vector,
                    Float64,
                    Float64,
                    Ref{T},
                )
            )
        end
        precfun = getprecfun(userfun)

        function getpsetupfun(::T) where {T}
            return @cfunction(
                idaprecsetup,
                Cint,
                (Float64, N_Vector, N_Vector, N_Vector, Float64, Ref{T})
            )
        end
        psetupfun = alg.psetup === nothing ? C_NULL : getpsetupfun(userfun)

        IDASetPreconditioner(mem, psetupfun, precfun)
    end

    if SciMLBase.has_jac(prob.f)
        function getcfunjacc(::T) where {T}
            return @cfunction(
                idajac,
                Cint,
                (
                    realtype,
                    realtype,
                    N_Vector,
                    N_Vector,
                    N_Vector,
                    SUNMatrix,
                    Ref{T},
                    N_Vector,
                    N_Vector,
                    N_Vector,
                )
            )
        end
        jac = getcfunjacc(userfun)
        flag = IDASetUserData(mem, userfun)
        flag = IDASetJacFn(mem, jac)
    else
        jac = nothing
    end

    tout = Float64[first(tspan)]
    if save_idxs isa Integer
        ures = Vector{eltype(uType)}()
        dures = Vector{eltype(uType)}()
    else
        ures = Vector{uType}()
        dures = Vector{uType}()
    end
    tmp = isnothing(callbacks_internal) ? u0 : similar(u0)
    uprev = isnothing(callbacks_internal) ? u0 : similar(u0)
    retcode = flag >= 0 ? ReturnCode.Default : ReturnCode.InitialFailure
    sol = SciMLBase.build_solution(
        prob,
        alg,
        ts,
        ures, dense ? dures : nothing;
        dense = dense,
        calculate_error = false,
        timeseries_errors = timeseries_errors,
        retcode = retcode,
        stats = SciMLBase.DEStats(0),
        dense_errors = dense_errors
    )

    opts = DEOptions(
        saveat_internal,
        tstops_internal,
        saveat, tstops, save_start,
        save_everystep, save_idxs,
        dense,
        timeseries_errors,
        dense_errors,
        save_on,
        save_end,
        callbacks_internal,
        abstol,
        reltol,
        verbose,
        advance_to_tstop,
        stop_at_next_tstop,
        progress,
        progress_steps,
        progress_name,
        progress_message,
        progress_id,
        maxiters
    )

    # Preallocate NVector for differential_vars if provided
    diff_vars_nvec = if prob.differential_vars !== nothing
        NVector(vec(Float64.(prob.differential_vars)), ctx)
    else
        nothing
    end

    integrator = IDAIntegrator(
        u0,
        du0,
        prob.p,
        t0,
        t0,
        mem,
        _LS,
        _A,
        sol,
        alg,
        f!,
        userfun,
        jac,
        opts,
        tout,
        tdir,
        false,
        tmp,
        uprev,
        Cint(flag),
        0,
        false,
        0,
        1,
        callback_cache,
        0.0,
        utmp,
        dutmp,
        diff_vars_nvec,
        initializealg,
        ctx_handle
    )

    # Context will be freed when integrator is garbage collected
    # through the Handle mechanism

    SciMLBase.initialize_dae!(integrator, initializealg)
    integrator.u_modified && IDAReinit!(integrator)

    if save_start
        save_value!(ures, integrator.u, uType, save_idxs)
        save_value!(dures, integrator.du, duType, save_idxs)
    end

    initialize_callbacks!(integrator)
    return integrator
end # function solve

## Common calls

function interpret_sundials_retcode(flag)
    flag >= 0 && return ReturnCode.Success
    flag == -1 && return ReturnCode.MaxIters
    (flag == -2 || flag == -3) && return ReturnCode.Unstable
    flag == -4 && return ReturnCode.ConvergenceFailure
    return ReturnCode.Failure
end

function solver_step(integrator::CVODEIntegrator, tstop)
    integrator.flag = CVode(
        integrator.mem, tstop, integrator.u_nvec, integrator.tout,
        CV_ONE_STEP
    )
    return if integrator.opts.progress
        Logging.@logmsg(
            Logging.LogLevel(-1),
            integrator.opts.progress_name,
            _id = integrator.opts.progress_id,
            message = integrator.opts.progress_message(
                integrator.dt,
                integrator.u,
                integrator.p,
                integrator.t
            ),
            progress = integrator.t / integrator.sol.prob.tspan[2]
        )
    end
end
# Dispatch for ARKStep (implicit methods)
function solver_step(
        integrator::ARKODEIntegrator{
            N, pType, solType, algType, fType, UFType, JType, oType,
            LStype, Atype, MLStype, Mtype, CallbackCacheType, ARKStepMem,
        },
        tstop
    ) where {
        N, pType, solType, algType, fType, UFType, JType, oType,
        LStype, Atype, MLStype, Mtype, CallbackCacheType,
    }
    integrator.flag = ARKStepEvolve(
        integrator.mem, tstop, integrator.u_nvec,
        integrator.tout, ARK_ONE_STEP
    )
    return if integrator.opts.progress
        Logging.@logmsg(
            Logging.LogLevel(-1),
            integrator.opts.progress_name,
            _id = integrator.opts.progress_id,
            message = integrator.opts.progress_message(
                integrator.dt,
                integrator.u_nvec,
                integrator.p,
                integrator.t
            ),
            progress = integrator.t / integrator.sol.prob.tspan[2]
        )
    end
end

# Dispatch for ERKStep (explicit methods)
function solver_step(
        integrator::ARKODEIntegrator{
            N, pType, solType, algType, fType, UFType, JType, oType,
            LStype, Atype, MLStype, Mtype, CallbackCacheType, ERKStepMem,
        },
        tstop
    ) where {
        N, pType, solType, algType, fType, UFType, JType, oType,
        LStype, Atype, MLStype, Mtype, CallbackCacheType,
    }
    integrator.flag = ERKStepEvolve(
        integrator.mem, tstop, integrator.u_nvec,
        integrator.tout, ARK_ONE_STEP
    )
    return if integrator.opts.progress
        Logging.@logmsg(
            Logging.LogLevel(-1),
            integrator.opts.progress_name,
            _id = integrator.opts.progress_id,
            message = integrator.opts.progress_message(
                integrator.dt,
                integrator.u_nvec,
                integrator.p,
                integrator.t
            ),
            progress = integrator.t / integrator.sol.prob.tspan[2]
        )
    end
end
function solver_step(integrator::IDAIntegrator, tstop)
    integrator.flag = IDASolve(
        integrator.mem,
        tstop,
        integrator.tout,
        integrator.u_nvec,
        integrator.du_nvec,
        IDA_ONE_STEP
    )
    integrator.iter += 1
    return if integrator.opts.progress && integrator.iter % integrator.opts.progress_steps == 0
        Logging.@logmsg(
            Logging.LogLevel(-1),
            integrator.opts.progress_name,
            _id = integrator.opts.progress_id,
            message = integrator.opts.progress_message(
                integrator.dt,
                integrator.u,
                integrator.p,
                integrator.t
            ),
            progress = integrator.t / integrator.sol.prob.tspan[2]
        )
    end
end

function set_stop_time(integrator::CVODEIntegrator, tstop)
    return CVodeSetStopTime(integrator.mem, tstop)
end
# Dispatch for ARKStep (implicit methods)
function set_stop_time(
        integrator::ARKODEIntegrator{
            N, pType, solType, algType, fType, UFType, JType, oType,
            LStype, Atype, MLStype, Mtype, CallbackCacheType, ARKStepMem,
        },
        tstop
    ) where {
        N, pType, solType, algType, fType, UFType, JType, oType,
        LStype, Atype, MLStype, Mtype, CallbackCacheType,
    }
    return ARKStepSetStopTime(integrator.mem, tstop)
end

# Dispatch for ERKStep (explicit methods)
function set_stop_time(
        integrator::ARKODEIntegrator{
            N, pType, solType, algType, fType, UFType, JType, oType,
            LStype, Atype, MLStype, Mtype, CallbackCacheType, ERKStepMem,
        },
        tstop
    ) where {
        N, pType, solType, algType, fType, UFType, JType, oType,
        LStype, Atype, MLStype, Mtype, CallbackCacheType,
    }
    return ERKStepSetStopTime(integrator.mem, tstop)
end
function set_stop_time(integrator::IDAIntegrator, tstop)
    return IDASetStopTime(integrator.mem, tstop)
end

function get_iters!(integrator::CVODEIntegrator, iters)
    return CVodeGetNumSteps(integrator.mem, iters)
end
# Dispatch for ARKStep (implicit methods)
function get_iters!(
        integrator::ARKODEIntegrator{
            N, pType, solType, algType, fType, UFType, JType, oType,
            LStype, Atype, MLStype, Mtype, CallbackCacheType, ARKStepMem,
        },
        iters
    ) where {
        N, pType, solType, algType, fType, UFType, JType, oType,
        LStype, Atype, MLStype, Mtype, CallbackCacheType,
    }
    return ARKStepGetNumSteps(integrator.mem, iters)
end

# Dispatch for ERKStep (explicit methods)
function get_iters!(
        integrator::ARKODEIntegrator{
            N, pType, solType, algType, fType, UFType, JType, oType,
            LStype, Atype, MLStype, Mtype, CallbackCacheType, ERKStepMem,
        },
        iters
    ) where {
        N, pType, solType, algType, fType, UFType, JType, oType,
        LStype, Atype, MLStype, Mtype, CallbackCacheType,
    }
    return ERKStepGetNumSteps(integrator.mem, iters)
end
function get_iters!(integrator::IDAIntegrator, iters)
    return IDAGetNumSteps(integrator.mem, iters)
end

function DiffEqBase.solve!(
        integrator::AbstractSundialsIntegrator; early_free = false,
        calculate_error = false
    )
    uType = eltype(integrator.sol.u)
    iters = Ref(Clong(-1))
    while !isempty(integrator.opts.tstops)
        # The call to first is an overload of Base.first implemented in DataStructures
        while integrator.tdir * integrator.t < first(integrator.opts.tstops)
            tstop = integrator.tdir * first(integrator.opts.tstops)
            set_stop_time(integrator, tstop)
            integrator.tprev = integrator.t
            if !(integrator.opts.callback.continuous_callbacks isa Tuple{})
                integrator.uprev .= integrator.u
            end
            integrator.userfun.p = integrator.p
            solver_step(integrator, tstop)
            integrator.t = first(integrator.tout)
            # NB: CVode, ARKode may warn and then recover if integrator.t == integrator.tprev so don't flag this as an error
            integrator.flag < 0 && break
            handle_callbacks!(integrator) # this also updates the interpolation
            integrator.flag < 0 && break
            if isempty(integrator.opts.tstops)
                break
            end
            get_iters!(integrator, iters)
            if iters[] + 1 > integrator.opts.maxiters
                integrator.flag = -1 # MaxIters: -1
                break
            end
        end
        integrator.flag < 0 && break
        handle_tstop!(integrator)
    end

    DiffEqBase.finalize!(integrator.opts.callback, integrator.u, integrator.t, integrator)
    tend = integrator.t
    if integrator.opts.save_end &&
            (isempty(integrator.sol.t) || integrator.sol.t[end] != tend)
        save_value!(
            integrator.sol.u, integrator.u, uType,
            integrator.opts.save_idxs
        )
        push!(integrator.sol.t, tend)
        if integrator.opts.dense
            save_value!(
                integrator.sol.interp.du, get_du(integrator), uType,
                integrator.opts.save_idxs
            )
        end
    end

    if integrator.opts.progress
        Logging.@logmsg(
            Logging.LogLevel(-1),
            integrator.opts.progress_name,
            _id = integrator.opts.progress_id,
            message = integrator.opts.progress_message(
                integrator.dt,
                integrator.u,
                integrator.p,
                integrator.t
            ),
            progress = "done"
        )
    end

    fill_stats!(integrator)

    if early_free
        empty!(integrator.mem)
        integrator.A !== nothing && empty!(integrator.A)
        integrator.LS !== nothing && empty!(integrator.LS)
    end

    if SciMLBase.has_analytic(integrator.sol.prob.f) && calculate_error
        SciMLBase.calculate_solution_errors!(
            integrator.sol;
            timeseries_errors = integrator.opts.timeseries_errors,
            dense_errors = integrator.opts.dense_errors
        )
    end

    if integrator.sol.retcode == ReturnCode.Default
        integrator.sol = DiffEqBase.solution_new_retcode(
            integrator.sol,
            interpret_sundials_retcode(integrator.flag)
        )
    end

    return integrator.sol
end

function handle_tstop!(integrator::AbstractSundialsIntegrator)
    tstops = integrator.opts.tstops
    return if !isempty(tstops) && integrator.tdir * integrator.t >= first(tstops)
        pop!(tstops)
        # If we passed multiple tstops at once (possible if Sundials ignores us or we had redundant tstops)
        while !isempty(tstops) && integrator.tdir * integrator.t >= first(tstops)
            pop!(tstops)
        end
        integrator.just_hit_tstop = true
    end
end

function fill_stats!(integrator::AbstractSundialsIntegrator) end

function fill_stats!(integrator::CVODEIntegrator)
    stats = integrator.sol.stats
    mem = integrator.mem
    tmp = Ref(Clong(-1))
    CVodeGetNumRhsEvals(mem, tmp)
    stats.nf = tmp[]
    CVodeGetNumLinSolvSetups(mem, tmp)
    stats.nw = tmp[]
    CVodeGetNumErrTestFails(mem, tmp)
    stats.nreject = tmp[]
    CVodeGetNumSteps(mem, tmp)
    stats.naccept = tmp[] - stats.nreject
    CVodeGetNumNonlinSolvIters(mem, tmp)
    stats.nnonliniter = tmp[]
    CVodeGetNumNonlinSolvConvFails(mem, tmp)
    stats.nnonlinconvfail = tmp[]
    return if method_choice(integrator.alg) == :Newton
        CVodeGetNumJacEvals(mem, tmp)
        stats.njacs = tmp[]
    end
end

# Dispatch for ARKStep (implicit methods)
function fill_stats!(
        integrator::ARKODEIntegrator{
            N,
            pType,
            solType,
            algType,
            fType,
            UFType,
            JType,
            oType,
            LStype,
            Atype,
            MLStype,
            Mtype,
            CallbackCacheType,
            ARKStepMem,
        }
    ) where {
        N, pType, solType, algType, fType, UFType, JType, oType,
        LStype, Atype, MLStype, Mtype, CallbackCacheType,
    }
    stats = integrator.sol.stats
    mem = integrator.mem
    tmp = Ref(Clong(-1))
    tmp2 = Ref(Clong(-1))
    ARKStepGetNumRhsEvals(mem, tmp, tmp2)
    stats.nf = tmp[]
    stats.nf2 = tmp2[]
    integrator.alg.stiffness !== Explicit() && ARKStepGetNumLinSolvSetups(mem, tmp)
    stats.nw = tmp[]
    ARKStepGetNumErrTestFails(mem, tmp)
    stats.nreject = tmp[]
    ARKStepGetNumSteps(mem, tmp)
    stats.naccept = tmp[] - stats.nreject
    integrator.alg.stiffness !== Explicit() && ARKStepGetNumNonlinSolvIters(mem, tmp)
    stats.nnonliniter = tmp[]
    integrator.alg.stiffness !== Explicit() && ARKStepGetNumNonlinSolvConvFails(mem, tmp)
    stats.nnonlinconvfail = tmp[]
    return if integrator.alg.stiffness !== Explicit() && method_choice(integrator.alg) == :Newton
        ARKStepGetNumJacEvals(mem, tmp)
        stats.njacs = tmp[]
    end
end

# Dispatch for ERKStep (explicit methods)
function fill_stats!(
        integrator::ARKODEIntegrator{
            N,
            pType,
            solType,
            algType,
            fType,
            UFType,
            JType,
            oType,
            LStype,
            Atype,
            MLStype,
            Mtype,
            CallbackCacheType,
            ERKStepMem,
        }
    ) where {
        N, pType, solType, algType, fType, UFType, JType, oType,
        LStype, Atype, MLStype, Mtype, CallbackCacheType,
    }
    stats = integrator.sol.stats
    mem = integrator.mem
    tmp = Ref(Clong(-1))
    # ERKStepGetNumRhsEvals only takes one argument for explicit RHS evaluations
    ERKStepGetNumRhsEvals(mem, tmp)
    stats.nf = tmp[]
    stats.nf2 = 0  # No implicit RHS for explicit methods
    # No linear solver setups for explicit methods
    stats.nw = 0
    ERKStepGetNumErrTestFails(mem, tmp)
    stats.nreject = tmp[]
    ERKStepGetNumSteps(mem, tmp)
    stats.naccept = tmp[] - stats.nreject
    # No nonlinear solver iterations or convergence failures for explicit methods
    stats.nnonliniter = 0
    stats.nnonlinconvfail = 0
    # No Jacobian evaluations for explicit methods
    return stats.njacs = 0
end

function fill_stats!(integrator::IDAIntegrator)
    stats = integrator.sol.stats
    mem = integrator.mem
    tmp = Ref(Clong(-1))
    IDAGetNumResEvals(mem, tmp)
    stats.nf = tmp[]
    IDAGetNumLinSolvSetups(mem, tmp)
    stats.nw = tmp[]
    IDAGetNumErrTestFails(mem, tmp)
    stats.nreject = tmp[]
    IDAGetNumSteps(mem, tmp)
    stats.naccept = tmp[] - stats.nreject
    IDAGetNumNonlinSolvIters(mem, tmp)
    stats.nnonliniter = tmp[]
    IDAGetNumNonlinSolvConvFails(mem, tmp)
    stats.nnonlinconvfail = tmp[]
    return if method_choice(integrator.alg) == :Newton
        IDAGetNumJacEvals(mem, tmp)
        stats.njacs = tmp[]
    end
end

function initialize_callbacks!(integrator, initialize_save = true)
    t = integrator.t
    u = integrator.u
    callbacks = integrator.opts.callback
    integrator.u_modified = true

    u_modified = initialize!(callbacks, u, t, integrator)

    # if the user modifies u, we need to fix current values
    if u_modified
        handle_callback_modifiers!(integrator)

        if initialize_save &&
                (
                any((c) -> c.save_positions[2], callbacks.discrete_callbacks) ||
                    any((c) -> c.save_positions[2], callbacks.continuous_callbacks)
            )
            savevalues!(integrator, true)
        end
    end

    # reset this as it is now handled so the integrators should proceed as normal
    return integrator.u_modified = false
end
