# User-friendly method for calling solvers

"""macro for importing odecall."""
macro import_odecall()
    return :(
        using ODEInterface: odecall
    )
end

"""
Solvers where OPT_OUTPUTATTIMES is needed for the get the solution
at predefined times.
"""
const solvers_outputattimes = (ddeabm, ddeabm_i32, ddebdf, ddebdf_i32)

"""
    function odecall(solver, rhs, t::Vector, x0::Vector,
                    opt::AbstractOptionsODE)
        -> (tVec,xVec,retcode,stats)

Calls `solver` with the given right-hand side `rhs`.
There are two cases:

1. `2==length(t)`
1. `2<length(t)`

If `2==length(t)`, then in the output `tVec` consists of the time points the
(adaptive) solver has automatically chosen. And the `xVec` has the
states at this times. So: `tVec` is a `Vector{Float64}(m)` and
`xVec` is a `Array{Float64}(m,length(x0))`.

If `2<length(t)`, then the values in `t` must be strictly ascending
or strictly descending. Then a special output function is used to get
the numerical solution at the given `t`-values. In this case
`tVec` is a `Vector{Float64}(length(t))` and
`xVec` is a `Array{Float64}(length(t),length(x0))`.

If in `opt` a output function is given, then this output function is
also called/used.
"""
function odecall(
        solver, rhs, t::Vector, x0::Vector,
        opt::AbstractOptionsODE
    )
    tLen = length(t)
    tLen < 2 && throw(
        ArgumentErrorODE(
            "t must have 2 or more elements; found $tLen", :t
        )
    )
    try
        t = getVectorCheckLength(t, Real, tLen)
    catch e
        throw(ArgumentErrorODE("Cannot convert to to Vector{Real}", :t, e))
    end

    t0 = t[1];  T = t[end]
    t0 == T && throw(ArgumentErrorODE("t[1]==t[end]", :t))
    d = length(x0)
    locopt = OptionsODE(opt)
    orig_outputfcn = getOption(opt, OPT_OUTPUTFCN, nothing)
    orig_outputmode = getOption(opt, OPT_OUTPUTMODE, OUTPUTFCN_NEVER)

    if 2 == tLen
        tVec = Vector{Float64}()    # we do not know how many steps/points
        xVec = Vector{Float64}()    # the solver will use => tVec and xVec grow

        #             orig    │       local
        #          ───────────┼────────────────
        #             NEVER   │       WODENSE
        #            WODENSE  │       WODENSE
        #             DENSE   │       DENSE
        orig_outputmode != OUTPUTFCN_DENSE &&
            setOption!(locopt, OPT_OUTPUTMODE, OUTPUTFCN_WODENSE)

        # save only grid points of solver in tVec, xVec
        function outputfcn_save(
                reason::OUTPUTFCN_CALL_REASON,
                told::Float64, tnew::Float64, x::Vector{Float64},
                eval_sol_fcn::Function, extra_data::Dict
            )

            if reason == OUTPUTFCN_CALL_STEP
                push!(tVec, tnew); append!(xVec, x)
            end
            if orig_outputmode == OUTPUTFCN_NEVER || orig_outputfcn === nothing
                return OUTPUTFCN_RET_CONTINUE
            else
                return orig_outputfcn(reason, told, tnew, x, eval_sol_fcn, extra_data)
            end
        end
        setOption!(locopt, OPT_OUTPUTFCN, outputfcn_save)
    else
        s = sign(T - t0)   # direction: forward or backward in time
        if any(x -> sign(x) != s, diff(t))
            throw(
                ArgumentErrorODE(
                    string(
                        "Because sign(T-t0)=sign($T-$t0)=$s ",
                        "the vector t has to be ", (T ≥ t0 ? "ascending" : "descending"),
                        ". But this is not the case."
                    ), :t
                )
            )
        end
        # We know the size of tVec and xVec (if the solver completes)
        # If not (e.g. stopped by output function) => init with NaN
        tVec = ones(tLen) * NaN; xVec = ones(tLen * d) * NaN
        tPos = 1  # next position to check in output fcn

        function outputfcn_attimes(
                reason::OUTPUTFCN_CALL_REASON,
                told::Float64, tnew::Float64, x::Vector{Float64},
                eval_sol_fcn::Function, extra_data::Dict
            )
            if (reason == OUTPUTFCN_CALL_STEP)
                tVec[tPos] = tnew
                xVec[(1 + (tPos - 1) * d):(tPos * d)] = x
                tPos += 1
            end
            if orig_outputmode == OUTPUTFCN_NEVER || orig_outputfcn === nothing
                return OUTPUTFCN_RET_CONTINUE
            else
                return orig_outputfcn(reason, told, tnew, x, eval_sol_fcn, extra_data)
            end
        end

        function outputfcn_givent(
                reason::OUTPUTFCN_CALL_REASON,
                told::Float64, tnew::Float64, x::Vector{Float64},
                eval_sol_fcn::Function, extra_data::Dict
            )

            if (reason == OUTPUTFCN_CALL_STEP)
                while (tPos ≤ tLen) &&
                        ((told ≤ t[tPos] ≤ tnew) || (told ≥ t[tPos] ≥ tnew))
                    tVec[tPos] = t[tPos]
                    xVec[(1 + (tPos - 1) * d):(tPos * d)] = eval_sol_fcn(t[tPos])
                    tPos += 1
                end
            end
            if orig_outputmode == OUTPUTFCN_NEVER || orig_outputfcn === nothing
                return OUTPUTFCN_RET_CONTINUE
            else
                return orig_outputfcn(reason, told, tnew, x, eval_sol_fcn, extra_data)
            end
        end
        if solver in solvers_outputattimes
            setOption!(locopt, OPT_OUTPUTMODE, OUTPUTFCN_WODENSE)
            setOption!(locopt, OPT_OUTPUTATTIMES, t[2:(end - 1)])
            setOption!(locopt, OPT_OUTPUTFCN, outputfcn_attimes)
        else
            setOption!(locopt, OPT_OUTPUTMODE, OUTPUTFCN_DENSE)
            setOption!(locopt, OPT_OUTPUTFCN, outputfcn_givent)
        end
    end

    (tout, xout, retcode, stats) = solver(rhs, t0, T, x0, locopt)

    return (tVec, transpose(reshape(xVec, d, length(xVec) ÷ d)), retcode, stats)

end

# vim:syn=julia:cc=79:fdm=indent:
