# Some help texts for the ODEInterface

"""macro for importing all the help-functions."""
macro import_help()
    return quote
        using ODEInterface: help_overview, help_callsolvers, help_options,
            help_outputfcn, help_solversupport, help_install,
            help_loadsolvers, help_specialstructure,
            help_internals
        @ODEInterface.import_dopri5_help
        @ODEInterface.import_odex_help
        @ODEInterface.import_radau5_help
        @ODEInterface.import_radau_help
        @ODEInterface.import_seulex_help
        @ODEInterface.import_rodas_help
        @ODEInterface.import_ddeabm_help
        @ODEInterface.import_ddebdf_help
        @ODEInterface.import_bvpsol_help
        @ODEInterface.import_colnew_help
        @ODEInterface.import_bvpm2_help
    end
end

buf2str(buf) = String(take!(buf))

try
    using Markdown
catch
end

"""
# Overview

## Importing all help topics

You can use

     using ODEInterface
     @ODEInterface.import_help

to have all `help_...` commands imported; so you can use

     ?help_overview
     help_overview()

## Help topics

     help_install          requirements, installation, compiling the solvers
     help_solversupport    supported ODE solvers
     help_loadsolvers      loading the ODE solvers
     help_callsolvers      how to call the ODE solvers
     help_options          how to use parameters/options for solvers
     help_outputfcn        how to use "output functions", dense output
     help_specialstructure support for problems with "special structure"

     help_internals        some internal/developer information

## Help for each solver

Each solver has its own help page. Just look at the documentation of
`dopri5`, `dop853`, `odex`, `radau5`, `radau`, `rodas`, `seulex`,
`bvpsol`, `ddeabm` and `Bvpm2`.
"""
function help_overview()
    return Docs.doc(help_overview)
end

"""
     function help_solversupport()

This function (when called) produces a (markdown) object with
information about the supported solvers.
"""
function help_solversupport()
    io = IOBuffer()
    write(io, "## Supported solvers\n\n")
    for solver in solverInfo
        write(io, "### ", solver.name, "\n\n", solver.description, "\n\n")
        for variant in solver.variants
            write(io, "* `", variant.name, "`: ", variant.description, "\n")
        end
        write(io, "\nHelp for compiling: `", string(solver.help_compile), "`")
        write(io, "\n\nLicense: `", string(solver.help_license), "`\n\n")
    end
    write(
        io, "## Load status\n\n",
        "     ╔═════════════╤═══════════════╤══════════",
        "╤═══════════════════════════════╗\n",
        "     ║    Name     │   libname     │  loaded  ",
        "│       method                  ║\n",
        "     ╠═════════════╪═══════════════╪══════════",
        "╪═══════════════════════════════╣\n"
    )
    first = Vector{Bool}(undef, 4)
    first_solver = true
    for solver in solverInfo
        if !first_solver
            write(
                io, "     ╟─────────────┼───────────────┼──────────",
                "┼───────────────────────────────╢\n"
            )
        end
        first_solver = false
        first[1] = true
        first_variant = true
        for variant in solver.variants
            if !first_variant
                write(
                    io, "     ║             ├───────────────┼──────────",
                    "┼───────────────────────────────╢\n"
                )
            end
            loaded = true
            try
                v = dlSolversInfo[variant.libname]
                @assert v.error === nothing
                @assert v.libhandle ≠ C_NULL
            catch
                loaded = false
            end

            first[2] = first[3] = true
            for method in variant.methods
                found = true
                try
                    index = findfirst(
                        x -> x.generic_name == method,
                        dlSolversInfo[variant.libname].methods
                    )
                    m = dlSolversInfo[variant.libname].methods[index]
                    @assert m.error === nothing
                    @assert m.method_ptr ≠ C_NULL
                catch e
                    found = false
                end
                method_name = method
                if length(method_name) > 22
                    method_name = string(method_name[1:19], "...")
                end
                write(
                    io, "     ║ ",
                    rpad(first[1] ? solver.name : "", 12), "│ ",
                    rpad(first[2] ? variant.libname : "", 14), "│ ",
                    rpad(first[3] ? (loaded ? "yes" : " no") : "", 9), "│ ",
                    rpad(method_name, 23), found ? "  OK  " : "FAILED", " ║\n"
                )
                first[1] = first[2] = first[3] = false
            end
            first[1] = false
            first_variant = false
        end
    end
    write(
        io, "     ╚═════════════╧═══════════════╧══════════",
        "╧═══════════════════════════════╝\n\n",
        "For more information: see result of `loadODESolvers`.\n"
    )
    return Markdown.parse(buf2str(io))
end

"""
## Installation

This module needs the *compiled* Fortran solvers as shared libraries 
(i.e. `.so`, `.dylib` or `.dll` files, respectively). There are three ways
to get them:

* The package [ODEInterface_jll.jl](https://github.com/JuliaBinaryWrappers/ODEInterface_jll.jl) has precompiled solvers for different platforms. Julia 1.3 or newer is needed for this. This is the default behaviour for julia versions 1.3 or newer.
* The build-script of this module: It tries to use `gfortran` and compile the solver libraries. This is the default behaviour for julia versions less than 1.3. A `gfortran` compiler is needed for this.
* You compile the solvers yourself (perhaps with different options and/or a different compiler). In this case just call `ODEInterface.help_solversupport` for further information (help topics) on how to compile the solvers and how to create shared libraries.
"""
function help_install()
    return Docs.doc(help_install)
end

"""
## Loading the solvers

All ODE solvers are dynamically loaded. See `help_install` for
information how to get or create such shared libraries for the solvers.

Before using a solver for the 1st time, it has to be loaded by
a call of `loadODESolvers`.
"""
function help_loadsolvers()
    return Docs.doc(help_loadsolvers)
end

"""
## Calling the Solvers

There are two ways of calling the solvers.

1. A calling convention close to the original Fortran-call,
   trying to provide/expose all the features the Fortran-codes have.
1. A simplified version, closer to odecalls like in MATLAB.

### The full-featured calling-method

All ODE-solvers have the same calling convention:

    (t,x,retcode,stats) =
        odesolver(rhs, t0::Real, T::Real,
                  x0::Vector, opt::AbstractOptionsODErhs)

    function rhs(t::Float64,x::Vector{Float64}) -> Vector{Float64}
        if OPT_RHS_CALLMODE == RHS_CALL_RETURNS_ARRAY

    function rhs(t::Float64,x::Vector{Float64},dx::Vector{Float64})
        if OPT_RHS_CALLMODE == RHS_CALL_INSITU

The input arguments are:

1. a julia function `rhs` for evaluating the right-hand side of the ODE,
   see below.
   It's OK to return something, that `convert` can transform
   to a `Vector{Float64}`.
1. the initial time `t0`. `(t0,x0)` is the initial value of the
   initial value problem.
1. the final time `T`.
1. the initial state `x0`. `(t0,x0)` is the initial value of the
   initial value problem.
1. further parameters/options in `opt` for the solver and for the interface.
   There is a separate section for the explanation of the options, see
   help_options.

The output arguments are:

1. `t` the *last* time for which the solution has been computed
   (if the whole computation was successful, then `t==T`)
1. `x` the numerical solation at time `t`
1. `retcode` the return code of the solver (interpretation is solver dependent)

There are two possible ways to provide the Julia right-hand side:

    function rhs(t::Float64,x::Vector{Float64}) -> Vector{Float64}

This is used, if `OPT_RHS_CALLMODE == RHS_CALL_RETURNS_ARRAY`. So you can
use anonymous functions like `(t,x) -> x` as right-hand sides. But this
form has a price: every time the right-hand side is called, a temporary
Array is created (the result). The form

    function rhs(t::Float64,x::Vector{Float64},dx::Vector{Float64})
                 -> nothing

used if `OPT_RHS_CALLMODE == RHS_CALL_INSITU` does not have this problem.
But the right-hand side must be a function filling in the values of `x'`
in `dx`.

### The simplified version

There is a simplified calling convention (using the methods above) to
provide a method like odecalls in MATLAB,
see `odecall`.
"""
function help_callsolvers()
    return Docs.doc(help_callsolvers)
end

"""
## The `opt` Argument: the Options

All options are handled by `OptionsODE`. See `OptionsODE` how to
query, set and change options.

There are the following classes of options.

1. Options for this ODEInterface (common for all solvers)
1. Options for the ODE solvers

### Options of this ODEInterface

* `OPT_RHS_CALLMODE`:
  There are two possible ways to call the Julia right-hand side:
  `RHS_CALL_RETURNS_ARRAY` and `RHS_CALL_INSITU`, see
  `help_callsolvers` for an explanation.
  difference.
* `OPT_LOGIO`:
  This option sets the `IO` that is used for logging messages
* `OPT_LOGLEVEL`:
  This is a bitmask for activating different logging messages.
  The following bitmasks are available.

         LOG_NOTHING     log nothing
         LOG_GENERAL     log some general information,
                         especially the main julia call of the solver
         LOG_RHS         log all calls of the right-hand side
         LOG_SOLVERARGS  log the arguments for the C-/Fortran-calls
                         before and after the call
         LOG_OUTPUTFCN   log calls of the julia output function
         LOG_SOLOUT      log calls of the solution output routine
         LOG_EVALSOL     log calls of the eval_sol_fcn
         LOG_MASS        log call(s) of the mass function
         LOG_JAC         log calls of the jacobian function of RHS
         LOG_BC          log calls of the boundary condition function
         LOG_BVPIVPSOL   log (during boundary value problems) calls to
                         initial value solvers
         LOG_RHSDT       log calls of the right-hand side time-derivative
         LOG_JACBC       log calls of the jacobian of the boundary condition
         LOG_GUESS       log calls to the guess function
         LOG_ALL         all of the above

### Options for the solvers

Different solvers support different options. All the options a solver
supports are listed in the help-command of the specific solver, e.g.
`help_dopri5`.

To get an overview, what options are supported by what solvers,
call `ODEInterface.help_options()`.
"""
function help_options()
    # sort solvers alphabetically and give each solver a number
    solvers = sort(solverInfo, by = x -> x.name)
    # get for every option all the solvers that use this option
    opt_usage = Dict{Symbol, Set{SolverInfo}}()
    for solver in solvers
        for opt in solver.supported_opts
            if !haskey(opt_usage, opt)
                opt_usage[opt] = Set{SolverInfo}()
            end
            push!(opt_usage[opt], solver)
        end
    end
    # produce output: alphab. order for options
    io = IOBuffer()
    write(io, "## Solvers\n\n")
    for solver in solvers
        write(io, "1. ", solver.name, "\n")
    end
    write(io, "\n## Options used by Solvers\n\n")

    opts = collect(keys(opt_usage))
    sort!(opts)

    write(io, "      ╔══════════════════════")
    for k in 1:length(solvers)
        write(io, "╤══")
    end
    write(io, "╗\n")

    write(io, "      ║        Option        ")
    for k in 1:length(solvers)
        write(io, "│", lpad(string(k), 2))
    end
    write(io, "║\n")

    write(io, "      ╠══════════════════════")
    for k in 1:length(solvers)
        write(io, "╪══")
    end
    write(io, "╣\n")

    for opt in opts
        write(io, "      ║ ", rpad(opt, 21))
        for solver in solvers
            write(io, "│ ", (solver ∈ opt_usage[opt]) ? "✔" : " ")
        end
        write(io, "║\n")
    end

    write(io, "      ╚══════════════════════")
    for k in 1:length(solvers)
        write(io, "╧══")
    end
    write(io, "╝\n")

    return Markdown.parse(buf2str(io))
end

"""
## `OPT_OUTPUTMODE`

This option determines if the `OPT_OUTPUTFCN` is
called, and if dense output (the `eval_sol_fcn`) is prepared/supported.

* `OUTPUTFCN_NEVER`: don't call the output function
* `OUTPUTFCN_WODENSE`: call the output function, but `eval_sol_fcn`
  is not used
* `OUTPUTFCN_DENSE`: call the output function and prepare `eval_sol_fcn`

## `OPT_OUTPUTFCN`

     function outputfcn(reason::OUTPUTFCN_CALL_REASON,
      told::Float64,t::Float64, x::Vector{Float64},eval_sol_fcn::Function,
      extra_data::Dict)  -> OUTPUTFCN_RETURN_VALUE

A (julia) function that is called

1. at beginning of the solution process with
   `reason == OUTPUTFCN_CALL_INIT`, `told=t0`, `t`=`T`, `x=x0`,
   `eval_sol_fcn` a dummy function throwing an error if called,
   `extra_data` a `Dict` persistent until the last call of the output
   function. The return value is *ignored*.
1. after every successful integration step with
   `reason == OUTPUTFCN_CALL_STEP`, `[told,t]` the time interval of the
   last step, `x` the numerical solution at time `t`,
   `eval_sol_fcn` a function to evaluate the solution in `t1 ∊ [told,t]`, if
   requested by `OPT_OUTPUTMODE`, otherwise a call to this function will
   result in an error.
1. at the end (after the last step) with
   `reason == OUTPUTFCN_CALL_DONE`.
   The return value is *ignored*.

With `eval_sol_fcn`

        function eval_sol_fcn(t1::Float64) -> Vector{Float64}

the numerical solution can be evaluated in the
time interval `[told,t]` (if `OPT_OUTPUTMODE == OUTPUTFCN_DENSE`).

If supported by the solver, the numerical solution may be changed
in the `outputfcn` (if `reason == OUTPUTFCN_CALL_STEP`) and the solver
continues the process with the changed solution.
The return value `OUTPUTFCN_RET_CONTINUE_XCHANGED` indicates
this. `OUTPUTFCN_RET_CONTINUE` tells the solver to continue (without
changes in `x`) and `OUTPUTFCN_RET_STOP` tells the solver to stop the
solver.
"""
function help_outputfcn()
    return Docs.doc(help_outputfcn)
end

"""
## Special Structure

Some solvers (e.g. radau5 and radau) supports ODEs with a
"special structure". In this context an ODE has special structure
if there exists M1>0 and M2>0 and M1 = M⋅M2 for some integer M
and

                 x'(k) = x(k+M2)   for all k = 1,…,M1            (*)

There are the options `OPT_M1` and `OPT_M2` to tell the solvers, if the
ODE has this special structure. In this case only the non-trivial parts
of the right-hand side, of the mass matrix and of the Jacobian matrix
had to be supplied.

If an ODE has the special structure, then the right-hand side
for `OPT_RHS_CALLMODE == RHS_CALL_RETURNS_ARRAY` has to return
a vector of length d-M1 because the right-hand side for the first M1 entries
are known from (*). For `OPT_RHS_CALLMODE == RHS_CALL_INSITU` the
right-hand side gets (a reference) to the array of length d, but only
the last d-M1 components need to be filled in.

The mass matrix has the form

         ⎛ 1    │    ⎞ ⎫
         ⎜  ⋱   │    ⎟ ⎪
         ⎜   ⋱  │    ⎟ ⎬ M1
         ⎜    ⋱ │    ⎟ ⎪
    M =  ⎜     1│    ⎟ ⎭
         ⎜──────┼────⎟
         ⎜      │    ⎟ ⎫
         ⎜      │ M̃  ⎟ ⎬ d-M1
         ⎝      │    ⎠ ⎭

Then there has to be only the (d-M1)×(d-M1) matrix M̃ in `OPT_MASSMATRIX`.
Of course, M̃ can be banded. Then `OPT_MASSMATRIX` is a `BandedMatrix` with
lower bandwidth < d-M1.

If an ODE has the special structure, then the Jacobian matrix has the form

         ⎛0   1      ⎞ ⎫
         ⎜ ⋱   ⋱     ⎟ ⎪
         ⎜  ⋱   ⋱    ⎟ ⎬ M1
         ⎜   ⋱   ⋱   ⎟ ⎪
    J =  ⎜    0   1  ⎟ ⎭
         ⎜───────────⎟
         ⎜           ⎟ ⎫
         ⎜      J̃    ⎟ ⎬ d-M1
         ⎝           ⎠ ⎭

Then there has to be only the (d-M1)×d matrix J̃ in `OPT_JACOBIMATRIX`.
In this case banded Jacobian matrices are only supported for the
case M1 + M2 == d. Then in this case J̃ is divided into d/M2 = 1+(M1/M2)
blocks of the size M2×M2. All this blocks can be banded with a (common)
lower bandwidth < M2.

The option `OPT_JACOBIBANDSTRUCT` is used to describe the banded structure
of the Jacobian. It is eigher `nothing` if the Jacobian is full or a
tuple `(l,u)` with the lower and upper bandwidth.

The function for providing the Jacobian for ∂f/∂x can have the following
forms:

     function (t,x,J)       -> nothing       (A)
     function (t,x,J1,…,JK) -> nothing       (B)

The following table shows when `OPT_JACOBIMATRIX` has the form (A)
and when it has the form (B):

     ╔══════╤═════════════════════════════╤════════════════════════════════╗
     ║      │ JACOBIBANDSTRUCT == nothing │ JACOBIBANDSTRUCT == (l,u)      ║
     ╟──────┼─────────────────────────────┼────────────────────────────────╢
     ║ M1==0│ (A), J full, size: d×d      │ (A) J (l,u)-banded, size d×d   ║
     ╟──────┼─────────────────────────────┼────────────────────────────────╢
     ║ M1>0 │ (A), J full, size: (d-M1)×d │ (B) J1,…,JK (l,u)-banded       ║
     ║      │                             │     each with size M2×M2 and   ║
     ║      │                             │     K = 1 + M1/M2              ║
     ║      │                             │     M1 + M2 == d               ║
     ╚══════╧═════════════════════════════╧════════════════════════════════╝
"""
function help_specialstructure()
    return Docs.doc(help_specialstructure)
end

"""
## Internals

1. What is the typical "call stack" for all this callbacks? see
   documentation of `DopriInternalCallInfos`,  `OdexInternalCallInfos` and
   `Radau5InternalCallInfos`.
1. What closures (and how many) are generated to support the `eval_sol_fcn`?
   see `create_radau_eval_sol_fcn_closure`
"""
function help_internals()
    return Docs.doc(help_internals)
end

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