using Base.Threads
using SymbolicUtils.Code: LazyState, apply_optimization_rules

abstract type BuildTargets end
struct JuliaTarget <: BuildTargets end
struct StanTarget <: BuildTargets end
struct CTarget <: BuildTargets end

"""
    MATLABTarget()

A `build_function` target that generates an anonymous function expression for MATLAB and
GNU Octave.

# Arguments

None.

# Keywords

None. Pass this target as `target` to [`build_function`](@ref).

# Examples

```julia
julia> @variables x
1-element Vector{Num}:
 x

julia> build_function(x^2, x; target = MATLABTarget()) isa String
true
```
"""
struct MATLABTarget <: BuildTargets end
@public MATLABTarget

abstract type ParallelForm end
struct SerialForm <: ParallelForm end

"""
    ShardedForm{multithread}(cutoff, ncalls)

Split a long array construction into nested functions where each function calls
`ncalls` other functions, and the leaf functions populate at most `cutoff` number
of items in the array. If `multithread` is true, uses threading.
"""
struct ShardedForm{multithreaded} <: ParallelForm
    cutoff::Union{Nothing,Int}
    ncalls::Int
end

ShardedForm(cutoff, ncalls) = ShardedForm{false}(cutoff, ncalls)
ShardedForm() = ShardedForm(80, 4)

const MultithreadedForm = ShardedForm{true}

MultithreadedForm() = MultithreadedForm(nothing, 2*nthreads())

function throw_missing_specialization(n)
    throw(ArgumentError("Missing specialization for $n arguments. Check `iip_config`."))
end

"""
    build_function(ex, args...;
                   expression = Val{true},
                   target = JuliaTarget(),
                   parallel=nothing,
                   kwargs...)

Generates a numerically-usable function from a Symbolics `Num`.

Arguments:

- `ex`: The `Num` to compile
- `args`: The arguments of the function
- `expression`: Whether to generate code or whether to generate the compiled form.
  By default, `expression = Val{true}`, which means that the code for the
  function is returned. If `Val{false}`, then the returned value is compiled.

Keyword Arguments:

- `target`: The output target of the compilation process. Possible options are:
    - `JuliaTarget`: Generates a Julia function
    - `CTarget`: Generates a C function
    - `StanTarget`: Generates a function for compiling with the Stan probabilistic
      programming language
    - `MATLABTarget`: Generates an anonymous function for use in MATLAB and Octave
      environments
- `parallel`: The kind of parallelism to use in the generated function. Defaults
  to `SerialForm()`, i.e. no parallelism, if `ex` is a single expression or an
  array containing <= 1500 non-zero expressions. If `ex` is an array of > 1500
  non-zero expressions, then `ShardedForm(80, 4)` is used. See below for more on
  `ShardedForm`.
  Note that the parallel forms are not exported and thus need to be chosen like
  `Symbolics.SerialForm()`.
  The choices are:
  - `SerialForm()`: Serial execution.
  - `ShardedForm(cutoff, ncalls)`: splits the output function into sub-functions
     which contain at most `cutoff` number of output `rhss`. These sub-functions
     are called by the top-level function that _build_function returns.
     This helps in reducing the compile time of the generated function.
  - `MultithreadedForm()`: Multithreaded execution with a static split, evenly
    splitting the number of expressions per thread.
- `fname`: Used by some targets for the name of the function in the target space.

Note that not all build targets support the full compilation interface. Check the
individual target documentation for details.
"""
function build_function(args...;target = JuliaTarget(),kwargs...)
  _build_function(target,args...;kwargs...)
end

# Scalar output

unwrap_nometa(x) = unwrap(x)
function destructure_arg(arg::Union{AbstractArray, Tuple,NamedTuple}, inbounds, name)
    if !(arg isa Arr)
        DestructuredArgs(map(unwrap_nometa, arg), name, inbounds=inbounds, create_bindings=false)
    else
        unwrap_nometa(arg)
    end
end
destructure_arg(arg, _, _) = unwrap_nometa(arg)

function default_arg_name(i)
    Symbol("ˍ₋arg$(i)")
end

const DEFAULT_OUTSYM = Symbol("ˍ₋out")

# don't CSE inside operators
SymbolicUtils.Code.cse_inside_expr(sym, ::Symbolics.Operator) = false
# don't CSE inside `getindex` of things created via `@variables`
# EXCEPT called variables
function SymbolicUtils.Code.cse_inside_expr(sym, ::typeof(getindex))
    x = arguments(sym)[1]
    return !hasmetadata(x, VariableSource) || SymbolicUtils.is_called_function_symbolic(x)
end

function _build_function(target::JuliaTarget, op, args...;
                         conv = toexpr,
                         expression = Val{true},
                         expression_module = @__MODULE__(),
                         checkbounds = false,
                         states = LazyState(),
                         linenumbers = true,
                         wrap_code = nothing,
                         cse = false,
                         nanmath = true,
                         kwargs...)
    op = _recursive_unwrap(op)
    if symtype(op) <: AbstractArray
        if wrap_code === nothing
            wrap_code = (identity, identity)
        end
        return _build_function(target, wrap(op), args...; conv, expression, expression_module, checkbounds, states, linenumbers, cse, nanmath, wrap_code, kwargs...)
    end
    states.rewrites[:nanmath] = nanmath
    dargs = map((x) -> destructure_arg(x[2], !checkbounds, default_arg_name(x[1])), enumerate(collect(args)))
    fun = Func(dargs, [], op)
    if wrap_code !== nothing
        fun = wrap_code(fun)
    end
    if cse
        fun = Code.cse(fun)
    end
    expr = conv(fun, states)
    if !checkbounds
        @assert Meta.isexpr(expr, :function)
        expr.args[2] = :(@inbounds begin; $(expr.args[2]); end)
    end
    if expression == Val{true}
        expr
    else
        _build_and_inject_function(expression_module, expr)
    end
end

function get_unimplemented_expr(dargs)
    Func(dargs, [], term(throw_missing_specialization, length(dargs)))
end

SymbolicUtils.Code.get_rewrites(x::Arr) = SymbolicUtils.Code.get_rewrites(unwrap(x))

function _build_function(target::JuliaTarget, op::Arr, args...;
                         conv = toexpr,
                         expression = Val{true},
                         expression_module = @__MODULE__(),
                         checkbounds = false,
                         states = LazyState(),
                         linenumbers = true,
                         cse = false,
                         nanmath = true,
                         wrap_code = (identity, identity),
                         iip_config = (true, true),
                         kwargs...)
    op = _recursive_unwrap(op)
    dargs = map((x) -> destructure_arg(x[2], !checkbounds, default_arg_name(x[1])), enumerate(collect(args)))
    states.rewrites[:nanmath] = nanmath
    if iip_config[1]
        oop_expr = wrap_code[1](Func(dargs, [], op))
    else
        oop_expr = get_unimplemented_expr(dargs)
    end

    outsym = DEFAULT_OUTSYM
    if iip_config[2]
        body = inplace_expr(op, outsym)
        iip_expr = wrap_code[2](Func(vcat(outsym, dargs), [], body))
    else
        iip_expr = get_unimplemented_expr([outsym; dargs])
    end

    if cse
        oop_expr = Code.cse(oop_expr)
        iip_expr = Code.cse(iip_expr)
    end
    oop_expr = conv(oop_expr, states)
    if SymbolicUtils.isarrayop(op) && !haskey(states.rewrites, :arrayop_output)
        states.rewrites[:arrayop_output] = outsym
    end
    iip_expr = conv(iip_expr, states)

    if !checkbounds
        @assert Meta.isexpr(oop_expr, :function)
        oop_expr.args[2] = :(@inbounds begin; $(oop_expr.args[2]); end)
        @assert Meta.isexpr(iip_expr, :function)
        iip_expr.args[2] = :(@inbounds begin; $(iip_expr.args[2]); end)
    end
    if expression == Val{true}
        oop_expr, iip_expr
    else
        _build_and_inject_function(expression_module, oop_expr),
        _build_and_inject_function(expression_module, iip_expr)
    end
end

function _build_and_inject_function(mod::Module, ex)
    if ex.head == :function && ex.args[1].head == :tuple
        ex.args[1] = Expr(:call, :($mod.$(gensym())), ex.args[1].args...)
    elseif ex.head == :(->)
        return _build_and_inject_function(mod, Expr(:function, ex.args...))
    end
    RuntimeGeneratedFunction(mod, mod, ex)
end

toexpr(n::Num, st) = toexpr(value(n), st)

function fill_array_with_zero!(x::AbstractArray)
    if eltype(x) <: AbstractArray
        foreach(fill_array_with_zero!, x)
    else
        fill!(x, false)
    end
    return x
end

"""
    _build_function(target::JuliaTarget, rhss::AbstractArray, args...;
                       conv=toexpr,
                       expression = Val{true},
                       expression_module = @__MODULE__(),
                       checkbounds = false,
                       postprocess_fbody=ex -> ex,
                       linenumbers = false,
                       outputidxs=nothing,
                       skipzeros = false,
                       force_SA = false,
                       wrap_code = (nothing, nothing),
                       fillzeros = skipzeros && !(rhss isa SparseMatrixCSC),
                       states = LazyState(),
                       iip_config = (true, true),
                       parallel=nothing, cse = false, kwargs...)

Build function target: `JuliaTarget`

```julia
_build_function(target::JuliaTarget, rhss, args...;
                conv = toexpr,
                expression = Val{true},
                checkbounds = false,
                linenumbers = false,
                headerfun = addheader, outputidxs=nothing,
                convert_oop = true, force_SA = false,
                skipzeros = outputidxs===nothing,
                fillzeros = skipzeros && !(typeof(rhss)<:SparseMatrixCSC),
                parallel=SerialForm(), kwargs...)
```

Generates a Julia function which can then be utilized for further evaluations.
If expression=Val{false}, the return is a Julia function which utilizes
RuntimeGeneratedFunctions.jl to be free of world-age issues.

If the `rhss` is a scalar, the generated function is a function
with a scalar output. Otherwise, if it's an `AbstractArray`, the output
is two functions, one for out-of-place AbstractArray output and a second which
is a mutating function. The outputted functions match the given argument order,
i.e., f(u,p,args...) for the out-of-place and scalar functions and
`f!(du,u,p,args..)` for the in-place version.

Special Keyword Arguments:

- `parallel`: The kind of parallelism to use in the generated function. Defaults
  to `SerialForm()`, i.e. no parallelism. Note that the parallel forms are not
  exported and thus need to be chosen like `Symbolics.SerialForm()`.
  The choices are:
  - `SerialForm()`: Serial execution.
  - `ShardedForm(cutoff, ncalls)`: splits the output function into sub-functions
     which contain at most `cutoff` number of output `rhss`. These sub-functions
     are called by the top-level function that _build_function returns.
  - `MultithreadedForm()`: Multithreaded execution with a static split, evenly
    splitting the number of expressions per thread.
- `conv`: The conversion function of symbolic types to Expr. By default, this uses
  the `toexpr` function.
- `checkbounds`: For whether to enable bounds checking inside the generated
  function. Defaults to false, meaning that `@inbounds` is applied.
- `linenumbers`: Determines whether the generated function expression retains
  the line numbers. Defaults to true.
- `convert_oop`: Determines whether the OOP version should try to convert
  the output to match the type of the first input. This is useful for
  cases like LabelledArrays or other array types that carry extra
  information. Defaults to true.
- `force_SA`: Forces the output of the OOP version to be a StaticArray.
  Defaults to `false`, and outputs a static array when the first argument
  is a static array.
- `similarto`: An `AbstractArray` subtype which controls the type of the
  returned array for the OOP version. If provided, it ignores the value of `force_SA`.
- `skipzeros`: Whether to skip filling zeros in the in-place version if the
  filling function is 0.
- `fillzeros`: Whether to perform `fill(out,0)` before the calculations to ensure
  safety with `skipzeros`.
"""
function _build_function(target::JuliaTarget, rhss::AbstractArray, args...;
                       conv=toexpr,
                       expression = Val{true},
                       expression_module = @__MODULE__(),
                       checkbounds = false,
                       postprocess_fbody=ex -> ex,
                       linenumbers = false,
                       outputidxs=nothing,
                       skipzeros = false,
                       force_SA = false,
                       similarto = nothing,
                       wrap_code = (nothing, nothing),
                       fillzeros = skipzeros && !(rhss isa SparseMatrixCSC),
                       states = LazyState(),
                       iip_config = (true, true),
                       nanmath = true,
                       parallel=nothing, cse = false,
                       optimize = nothing,
                       kwargs...)
    if rhss isa SubArray
        rhss = copy(rhss)
    end
    rhss = _recursive_unwrap(rhss)
    states.rewrites[:nanmath] = nanmath
    # We cannot switch to ShardedForm because it deadlocks with
    # RuntimeGeneratedFunctions
    dargs = map((x) -> destructure_arg(x[2], !checkbounds,
                                  Symbol("ˍ₋arg$(x[1])")), enumerate([args...]))
    i = findfirst(x->x isa DestructuredArgs, dargs)
    if similarto === nothing
        similarto = force_SA ? SArray : i === nothing ? Array : dargs[i].name
    end

    oop, iip = iip_config
    if oop
        oop_expr = Func(dargs, [], postprocess_fbody(make_array(parallel, dargs, rhss, similarto)))
        if wrap_code[1] !== nothing
            oop_expr = wrap_code[1](oop_expr)
        end
    else
        oop_expr = get_unimplemented_expr(dargs)
    end


    if iip
        out = Sym{VartypeT}(DEFAULT_OUTSYM; type = Any, shape = SymbolicUtils.Unknown(-1))
        iip_expr = Func(vcat(out, dargs), [], postprocess_fbody(set_array(parallel,
                                    dargs,
                                    out,
                                    outputidxs,
                                    rhss,
                                    checkbounds,
                                    skipzeros)))
        if wrap_code[2] !== nothing
            iip_expr = wrap_code[2](iip_expr)
        end
    else
        iip_expr = get_unimplemented_expr([DEFAULT_OUTSYM; dargs])
    end

    if cse
        oop_expr = Code.cse(oop_expr)
        iip_expr = Code.cse(iip_expr)
    end

    if !isnothing(optimize) 
        iip_expr = apply_optimization_rules(iip_expr, states, optimize)
        oop_expr = apply_optimization_rules(oop_expr, states, optimize)
    end

    oop_expr = conv(oop_expr, states)
    iip_expr = conv(iip_expr, states)

    if !checkbounds
        @assert Meta.isexpr(oop_expr, :function)
        oop_expr.args[2] = :(@inbounds begin; $(oop_expr.args[2]); end)
        @assert Meta.isexpr(iip_expr, :function)
        iip_expr.args[2] = :(@inbounds begin; $(iip_expr.args[2]); end)
    end
    if expression == Val{true}
        return oop_expr, iip_expr
    else
        return _build_and_inject_function(expression_module, oop_expr),
        _build_and_inject_function(expression_module, iip_expr)
    end
end

_nnz(x::AbstractArray) = length(x)
_nnz(x::AbstractSparseArray) = nnz(x)
_nnz(x::Union{Base.ReshapedArray, LinearAlgebra.Transpose}) = _nnz(parent(x))

function make_array(s, dargs, arr, similarto)
    s !== nothing && Base.@warn("Parallel form of $(typeof(s)) not implemented")
    _make_array(arr, similarto)
end

function make_array(s::SerialForm, dargs, arr, similarto)
    _make_array(arr, similarto)
end

function make_array(s::ShardedForm, closed_args, arr, similarto)
    if arr isa AbstractSparseArray
        return term(SparseMatrixCSC, arr.m, arr.n, copy(arr.colptr), copy(arr.rowval), make_array(s, closed_args, arr.nzval, Vector))
    end
    per_task = ceil(Int, length(arr) / s.ncalls)
    slices = collect(Iterators.partition(arr, per_task))
    arrays = map(slices) do slice
        Func(closed_args, [], _make_array(slice, similarto)), closed_args
    end
    SpawnFetch{typeof(s)}(first.(arrays), last.(arrays), vcat)
end

struct Funcall{F, T}
    f::F
    args::T
end

(f::Funcall)() = f.f(f.args...)

function toexpr(p::SpawnFetch{MultithreadedForm}, st)
    args = isnothing(p.args) ?
              Iterators.repeated((), length(p.exprs)) : p.args
    spawns = map(p.exprs, args) do thunk, a
        ex = :($Funcall($(drop_expr(@RuntimeGeneratedFunction(@__MODULE__, toexpr(thunk, st), false))),
                       ($(toexpr.(a, (st,))...),)))
        quote
            let
                task = Base.Task($ex)
                task.sticky = false
                Base.schedule(task)
                task
            end
        end
    end
    quote
        $(toexpr(p.combine, st))(map(fetch, ($(spawns...),))...)
    end
end

function toexpr(p::SpawnFetch{ShardedForm{false}}, st)
    args = isnothing(p.args) ?
              Iterators.repeated((), length(p.exprs)) : p.args
    spawns = map(p.exprs, args) do thunk, a
        :($(drop_expr(@RuntimeGeneratedFunction(@__MODULE__, toexpr(thunk, st), false)))($(toexpr.(a, (st,))...),))
    end
    quote
        $(toexpr(p.combine, st))($(spawns...))
    end
end

function nzmap(f, x::Union{Base.ReshapedArray, LinearAlgebra.Transpose})
    Setfield.@set x.parent = nzmap(f, x.parent)
end

function nzmap(f, x::SubArray)
    unview = copy(x)
    if unview isa Union{SparseMatrixCSC, SparseVector}
        n = nnz(unview)
        if n != length(unview.nzval)
            resize!(unview.nzval, n)
            resize!(unview.rowval, n)
        end
    end
    nzmap(f, unview)
end

function nzmap(f, x::AbstractSparseArray)
    Setfield.@set x.nzval = nzmap(f, x.nzval)
end

function nzmap(f, x::UpperTriangular)
    UpperTriangular(nzmap(f, parent(x)))
end

function nzmap(f, x::LowerTriangular)
    UpperTriangular(nzmap(f, parent(x)))
end

nzmap(f, x) = map(f, x)

_issparse(x::AbstractArray) = issparse(x)
_issparse(x::Union{SubArray, Base.ReshapedArray, LinearAlgebra.Transpose}) = _issparse(parent(x))

function setparent(arr, val)
    Setfield.@set arr.parent = val
end

function set_nzval(arr, val)
    Setfield.@set arr.nzval = val
end

function _make_sparse_array(arr, similarto)
    if arr isa Union{SubArray, Base.ReshapedArray, LinearAlgebra.Transpose}
        newarr = _make_array(parent(arr), typeof(parent(arr)))
        return term(setparent, nzmap(Returns(true), arr), newarr)
    else
        newarr = _make_array(arr.nzval, Vector{symtype(eltype(arr))})
        return Let([Assignment(:__reference, term(copy, nzmap(Returns(true), arr)))], term(set_nzval, :__reference, newarr), false)
    end
end

function _make_array(rhs::UpperTriangular, similarto)
    return term(UpperTriangular, _make_array(parent(rhs), similarto))
end
function _make_array(rhs::LowerTriangular, similarto)
    return term(LowerTriangular, _make_array(parent(rhs), similarto))
end

function _make_array(rhss::AbstractArray, similarto)
    arr = nzmap(x->_make_array(x, similarto), rhss)
    if _issparse(arr)
        _make_sparse_array(arr, similarto)
    else
        MakeArray(arr, similarto)
    end
end

_make_array(x, similarto) = x

## In-place version

function set_array(p, closed_vars, args...)
    p !== nothing && Base.@warn("Parallel form of $(typeof(p)) not implemented")
    _set_array(args...)
end

function set_array(s::SerialForm, closed_vars, args...)
    _set_array(args...)
end

function recursive_split(leaf_f, s, out, args, outputidxs, xs)
    cutoff = isnothing(s.cutoff) ? ceil(Int, length(xs) / (2*s.ncalls)) : s.cutoff
    if length(xs) <= cutoff
        return leaf_f(outputidxs, xs)
    else
        per_part = ceil(Int, length(xs) / s.ncalls)
        slices = collect(Iterators.partition(zip(outputidxs, xs), per_part))
        fs = map(slices) do slice
            recursive_split(leaf_f, s, out, args, first.(slice), last.(slice))
        end
        return Func(args, [],
                    SpawnFetch{typeof(s)}(fs, [args for f in fs],
                                          (@inline noop(x...) = nothing)),
                    [])
    end
end

function set_array(s::ShardedForm, closed_args, out, outputidxs, rhss, checkbounds, skipzeros)
    if rhss isa AbstractSparseArray
        return set_array(s,
                         closed_args,
                         LiteralExpr(:($out.nzval)),
                         nothing,
                         rhss.nzval,
                         checkbounds,
                         skipzeros)
    end

    outvar = !(out isa Sym) ? gensym("out") : out

    if outputidxs === nothing
        outputidxs = collect(eachindex(rhss))
    end
    all_args = [outvar, closed_args...]
    ex = recursive_split(s, outvar, all_args, outputidxs, rhss) do idxs, xs
        Func(all_args, [],
             _set_array(outvar, idxs, xs, checkbounds, skipzeros),
             [])
    end.body

    return out isa Sym ? ex : LiteralExpr(quote
        $outvar = $out
        $ex
    end)
end

function _set_array(out, outputidxs, rhss::AbstractSparseArray, checkbounds, skipzeros)
    Let([Assignment(Symbol("%$out"), _set_array(LiteralExpr(:($out.nzval)), nothing, rhss.nzval, checkbounds, skipzeros))], out, false)
end

function _set_array(out, outputidxs, rhss::AbstractArray, checkbounds, skipzeros)
    if parent(rhss) !== rhss
        return _set_array(out, outputidxs, parent(rhss), checkbounds, skipzeros)
    end
    if outputidxs === nothing
        outputidxs = collect(eachindex(rhss))
    end
    indexes = AtIndex[]
    for (i, outi) in enumerate(outputidxs)
        if !(rhss[i] isa AbstractArray) && !(skipzeros && _iszero(rhss[i]))
            push!(indexes, AtIndex(outi, rhss[i]))
        elseif rhss[i] isa AbstractArray
            push!(indexes, AtIndex(outi, _set_array(LiteralExpr(:($out[$outi])), nothing, rhss[outi], checkbounds, skipzeros)))
        end
    end
    return SetArray(!checkbounds, out, indexes, true)
end

_set_array(out, outputidxs, rhs, checkbounds, skipzeros) = rhs


function vars_to_pairs(name,vs::Union{Tuple, AbstractArray}, symsdict=Dict())
    vs_names = tosymbol.(vs)
    for (v,k) in zip(vs_names, vs)
        symsdict[k] = Sym{symtype(k)}(v)
    end
    exs = [:($name[$i]) for (i, u) ∈ enumerate(vs)]
    vs_names,exs
end
function vars_to_pairs(name,vs, symsdict)
    symsdict[vs] = Sym{symtype(vs)}(tosymbol(vs))
    [tosymbol(vs)], [name]
end

get_varnumber(varop, vars::Vector) =  findfirst(x->isequal(x,varop),vars)
get_varnumber(varop, var) =  isequal(var,varop) ? 0 : nothing

function buildvarnumbercache(args...)
    varnumsdict = Pair[]
    for (argi,arg) in enumerate(args)
        if isa(arg,AbstractArray)
            for (eli,el) in enumerate(arg)
                push!(varnumsdict, el=>(argi,eli))
            end
        else
            push!(varnumsdict ,arg=>(argi,0))
        end
    end
    return Dict(varnumsdict)
end

function numbered_expr(O::BasicSymbolic,varnumbercache,args...;varordering = args[1],offset = 0,
                       states = LazyState(),
                       lhsname=:du,rhsnames=[Symbol("MTK$i") for i in 1:length(args)])
    O = value(O)
    O isa BasicSymbolic || return O
    if (issym(O) || issym(operation(O))) || (iscall(O) && operation(O) == getindex)
        (j,i) = get(varnumbercache, O, (nothing, nothing))
        if !isnothing(j)
            return i==0 ? :($(rhsnames[j])) : :($(rhsnames[j])[$(i+offset)])
        end
    end
    if iscall(O)
        if operation(O) === getindex
            args = arguments(O)
            Expr(:ref, toexpr(args[1], states), toexpr.(args[2:end] .+ offset, (states,))...)
        else
            Expr(:call, Symbol(operation(O)), (numbered_expr(x,varnumbercache,args...;offset=offset,lhsname=lhsname,
                                                             rhsnames=rhsnames,varordering=varordering) for x in sorted_arguments(O))...)
        end
    elseif issym(O)
        tosymbol(O, escape=false)
    else
        O
    end
end

function numbered_expr(de::Equation,varnumbercache,args...;varordering = args[1],
                       lhsname=:du,rhsnames=[Symbol("MTK$i") for i in 1:length(args)],offset=0)

    varordering = value.(args[1])
    var = var_from_nested_derivative(de.lhs)[1]
    i = findfirst(x->isequal(tosymbol(issym(x) ? x : operation(x), escape=false), tosymbol(var, escape=false)),varordering)
    :($lhsname[$(i+offset)] = $(numbered_expr(de.rhs,varnumbercache,args...;offset=offset,
                                              varordering = varordering,
                                              lhsname = lhsname,
                                              rhsnames = rhsnames)))
end
numbered_expr(c,args...;kwargs...) = c
numbered_expr(c::Num,args...;kwargs...) = error("Num found")


# The solver introduces "safe" variants of some math functions that handle
# negative/complex inputs in Julia. They have no C equivalent, so map them back
# to the standard math.h names when generating C code.
const c_safe_function_renames = Dict(:ssqrt => :sqrt, :scbrt => :cbrt, :slog => :log)

# Replace certain multiplication and power expressions so they form valid C code
# Extra factors of 1 are hopefully eliminated by the C compiler
function coperators(expr)
    expr isa Expr || return expr
    for e in expr.args
        if e isa Expr
            coperators(e)
        end
    end
    if expr.head == :call && expr.args[1] isa Symbol
        expr.args[1] = get(c_safe_function_renames, expr.args[1], expr.args[1])
    end
    for i in eachindex(expr.args)
        if expr.args[i] isa Rational
            expr.args[i] = float(expr.args[i]) # Evaluate rational numbers to floating-point
        end
    end
    # Introduce another factor 1 to prevent contraction of terms like "5 * t" to "5t" (not valid C code)
    if expr.head==:call && expr.args[1]==:* && length(expr.args)==3 && isa(expr.args[2], Real) && isa(expr.args[3], Symbol)
        push!(expr.args, 1)
    # Power operator does not exist in C, replace by multiplication or "pow"
    elseif expr.head==:call && expr.args[1]==:^
        @assert length(expr.args)==3 "Don't know how to handle ^ operation with <> 2 arguments"
        x = expr.args[2]
        n = expr.args[3]
        empty!(expr.args)
        # Replace by multiplication/division if
        #   x is a symbol and  n is a small integer
        #   x is a more complex expression and n is ±1
        #   n is exactly 0
        if (isa(n,Integer) && ((isa(x, Symbol) && abs(n) <= 3) || abs(n) <= 1)) || n==0
            if n >= 0
                append!(expr.args, [:*, fill(x, n)...])
                # fill up with factor 1 so this expr can still be a multiplication
                while length(expr.args) < 3
                    push!(expr.args, 1)
                end
            else # inverse of the above
                if n==-1
                    term = x
                else
                    term = :( ($(x)) ^ ($(-n)))
                    coperators(term)
                end
                append!(expr.args, [:/, 1., term])
            end
        #... otherwise use "pow" function
        else
            append!(expr.args, [:pow, x, n])
        end
    end
    expr
end


"""
Build function target: `CTarget`

```julia
_build_function(target::CTarget, eqs::Array{<:Equation}, args...;
                conv = toexpr, expression = Val{true},
                fname = :diffeqf,
                lhsname=:du,rhsnames=[Symbol("RHS\$i") for i in 1:length(args)],
                libpath=tempname(), compiler=:gcc)
```

This builds an in-place C function. Only works on arrays of equations. If
`expression == Val{false}`, then this builds a function in C, compiles it,
and returns a lambda to that compiled function. These special keyword arguments
control the compilation:

- libpath: the path to store the binary. Defaults to a temporary path.
- compiler: which C compiler to use. Defaults to `:gcc`, which is currently the
  only available option.
"""
function _build_function(target::CTarget, eqs::Array{<:Equation}, args...;
                         conv = toexpr, expression = Val{true},
                         fname = :diffeqf,
                         lhsname=:du,rhsnames=[Symbol("RHS$i") for i in 1:length(args)],
                         libpath=tempname(),compiler=:gcc)

    @warn "build_function(::Array{<:Equation}...) is deprecated. Use build_function(::AbstractArray...) instead."

    varnumbercache = buildvarnumbercache(args...)
    differential_equation = string(join([numbered_expr(eq,varnumbercache,args...,lhsname=lhsname,
                                  rhsnames=rhsnames,offset=-1) for
                                  (i, eq) ∈ enumerate(eqs)],";\n  "),";")

    argstrs = join(vcat("double* $(lhsname)",[typeof(args[i])<:AbstractArray ? "double* $(rhsnames[i])" : "double $(rhsnames[i])" for i in 1:length(args)]),", ")
    ex = """
    void $fname($(argstrs...)) {
      $differential_equation
    }
    """

    if expression == Val{true}
        return ex
    else
        @assert compiler == :gcc
        ex = build_function(eqs,args...;target=Symbolics.CTarget())
        open(`gcc -fPIC -O3 -msse3 -xc -shared -o $(libpath * "." * Libdl.dlext) -`, "w") do f
            print(f, ex)
        end
        drop_expr(@RuntimeGeneratedFunction(@__MODULE__,
                                            :((du::Array{Float64},u::Array{Float64},p::Array{Float64},t::Float64)
                                              -> ccall(("diffeqf", $libpath),
                                                       Cvoid, (Ptr{Float64},
                                                               Ptr{Float64},
                                                               Ptr{Float64},
                                                               Float64), du, u,
                                                               p, t)), false))
    end
end


"""
Build function target: `CTarget`

```julia
_build_function(target::CTarget, ex::AbstractArray, args...;
                columnmajor = true,
                conv        = toexpr,
                expression  = Val{true},
                fname       = :diffeqf,
                lhsname     = :du,
                rhsnames    = [Symbol("RHS\$i") for i in 1:length(args)],
                libpath     = tempname(),
                compiler    = :gcc)
```

This builds an in-place C function. Only works on expressions. If
`expression == Val{false}`, then this builds a function in C, compiles it,
and returns a lambda to that compiled function. These special keyword arguments
control the compilation:

- libpath: the path to store the binary. Defaults to a temporary path.
- compiler: which C compiler to use. Defaults to :gcc, which is currently the
  only available option.
"""
function _build_function(target::CTarget, ex::AbstractArray, args...;
                         columnmajor = true,
                         conv        = toexpr,
                         expression  = Val{true},
                         fname       = :diffeqf,
                         lhsname     = :du,
                         rhsnames    = [Symbol("RHS$i") for i in 1:length(args)],
                         libpath     = tempname(),
                         compiler    = :gcc)

    if !columnmajor
        return _build_function(target, hcat([row for row ∈ eachrow(ex)]...), args...;
                               columnmajor = true,
                               conv        = conv,
                               fname       = fname,
                               lhsname     = lhsname,
                               rhsnames    = rhsnames,
                               libpath     = libpath,
                               compiler    = compiler)
    end


    varnumbercache = buildvarnumbercache(args...)
    equations = Vector{String}()
    for col ∈ 1:size(ex,2)
        for row ∈ 1:size(ex,1)
            lhs = string(lhsname, "[", (col-1) * size(ex,1) + row-1, "]")
            rhs = numbered_expr(value(ex[row, col]), varnumbercache, args...;
                                lhsname  = lhsname,
                                rhsnames = rhsnames,
                                offset   = -1) |> coperators |> string  # Filter through coperators to produce valid C code in more cases
            push!(equations, string(lhs, " = ", rhs, ";"))
        end
    end

    argstrs = join(vcat("double* $(lhsname)",[typeof(args[i])<:AbstractArray ? "const double* $(rhsnames[i])" : "const double $(rhsnames[i])" for i in 1:length(args)]),", ")

    ccode = """
    #include <math.h>
    void $fname($(argstrs...)) {$([string("\n  ", eqn) for eqn ∈ equations]...)\n}
    """

    if expression == Val{true}
        return ccode
    else
        @assert compiler == :gcc
        open(`gcc -fPIC -O3 -msse3 -xc -shared -o $(libpath * "." * Libdl.dlext) -`, "w") do f
            print(f, ccode)
        end
        drop_expr(@RuntimeGeneratedFunction(@__MODULE__,
                                            :((du::Array{Float64},u::Array{Float64},p::Array{Float64},t::Float64)
                                              -> ccall(("diffeqf", $libpath),
                                                       Cvoid, (Ptr{Float64},
                                                               Ptr{Float64},
                                                               Ptr{Float64},
                                                               Float64), du, u,
                                                       p, t)), false))
    end

end
_build_function(target::CTarget, ex::Num, args...; kwargs...) = _build_function(target, [ex], args...; kwargs...)


"""
Build function target: `StanTarget`

```julia
_build_function(target::StanTarget, eqs::Array{<:Equation}, vs, ps, iv;
                conv = toexpr, expression = Val{true},
                fname = :diffeqf, lhsname=:internal_var___du,
                rhsnames=[:internal_var___u,:internal_var___p,:internal_var___t])
```

This builds an in-place Stan function compatible with the Stan differential equation solvers.
Unlike other build targets, this one requires `(vs, ps, iv)` as the function arguments.
Only allowed on arrays of equations.
"""
function _build_function(target::StanTarget, eqs::Array{<:Equation}, vs, ps, iv;
                         conv = toexpr, expression = Val{true},
                         fname = :diffeqf, lhsname=:internal_var___du,
                         rhsnames=[:internal_var___u,:internal_var___p,:internal_var___t])

    @warn "build_function(::Array{<:Equation}...) is deprecated. Use build_function(::AbstractArray...) instead."
    @assert expression == Val{true}

    varnumbercache = buildvarnumbercache(vs,ps...)
    par_str = join(["real $(rhsnames[2])_$i" for i in 1:length(ps)], ", ")
    rhsnames_mod = [:internal_var___u, [Symbol("$(rhsnames[2])_$i") for i in 1:length(ps)]..., :internal_var___t]
    differential_equation = string(join([numbered_expr(eq,varnumbercache,vs,ps,lhsname=lhsname,
                                   rhsnames=rhsnames_mod) for
                                   (i, eq) ∈ enumerate(eqs)],";\n  "),";")


    """
    vector $fname(real $(conv(iv)),vector $(rhsnames[1]), $par_str) {
      vector[$(length(eqs))] $lhsname;
      $differential_equation
      return $lhsname;
    }
    """
end

"""
Build function target: `StanTarget`

```julia
_build_function(target::StanTarget, ex::AbstractArray, vs, ps, iv;
                columnmajor = true,
                conv        = toexpr,
                expression  = Val{true},
                fname       = :diffeqf, lhsname=:internal_var___du,
                rhsnames    =  [:internal_var___u,:internal_var___p,:internal_var___t])
```

This builds an in-place Stan function compatible with the Stan differential equation solvers.
Unlike other build targets, this one requires `(vs, ps, iv)` as the function arguments.
Only allowed on expressions, and arrays of expressions.
"""
function _build_function(target::StanTarget, ex::AbstractArray, vs, ps, iv;
                         columnmajor = true,
                         conv        = toexpr,
                         expression  = Val{true},
                         fname       = :diffeqf, lhsname=:internal_var___du,
                         rhsnames    =  [:internal_var___u,:internal_var___p,:internal_var___t])

    @assert expression == Val{true}

    if !columnmajor
        return _build_function(target, hcat([row for row ∈ eachrow(ex)]...), vs, ps, iv;
                            columnmajor = true,
                            conv        = conv,
                            expression  = expression,
                            fname       = fname,
                            lhsname     = lhsname,
                            rhsnames    = rhsnames)
    end

    varnumbercache = buildvarnumbercache(vs,ps,iv)
    equations = Vector{String}()
    for col ∈ 1:size(ex,2)
        for row ∈ 1:size(ex,1)
            lhs = string(lhsname, "[", (col-1) * size(ex,1) + row, "]")
            rhs = numbered_expr(value(ex[row, col]), varnumbercache, vs, ps, iv;
                                lhsname  = lhsname,
                                rhsnames = rhsnames,
                                offset   = 0) |> string
            push!(equations, string(lhs, " = ", rhs, ";"))
        end
    end

    """
    vector $fname(real $(conv(iv)),vector $(rhsnames[1]),vector $(rhsnames[2])) {
      vector[$(length(equations))] $lhsname;
    $([eqn == equations[end] ? string("  ", eqn) : string("  ", eqn, "\n") for eqn ∈ equations]...)
      return $lhsname;
    }
    """
end
_build_function(target::StanTarget, ex::Num, vs, ps, iv; kwargs...) = _build_function(target, [ex], vs, ps, iv; kwargs...)

"""
Build function target: `MATLABTarget`

```julia
_build_function(target::MATLABTarget, eqs::Array{<:Equation}, args...;
                conv = toexpr, expression = Val{true},
                lhsname=:internal_var___du,
                rhsnames=[:internal_var___u,:internal_var___p,:internal_var___t])
```

This builds an out of place anonymous function `@(t,rhsnames[1])` to be used in MATLAB.
Compatible with the MATLAB differential equation solvers. Only allowed on expressions,
and arrays of expressions.
"""
function _build_function(target::MATLABTarget, eqs::Array{<:Equation}, args...;
                         conv = toexpr, expression = Val{true},
                         fname = :diffeqf, lhsname=:internal_var___du,
                         rhsnames=[:internal_var___u,:internal_var___p,:internal_var___t])

    @warn "build_function(::Array{<:Equation}...) is deprecated. Use build_function(::AbstractArray...) instead."
    @assert expression == Val{true}

    varnumbercache = buildvarnumbercache(args...)
    matstr = join([numbered_expr(eq.rhs,varnumbercache,args...,lhsname=lhsname,
                                  rhsnames=rhsnames) for
                                  (i, eq) ∈ enumerate(eqs)],"; ")

    matstr = replace(matstr,"["=>"(")
    matstr = replace(matstr,"]"=>")")
    matstr = "$fname = @($(rhsnames[3]),$(rhsnames[1])) ["*matstr*"];"
    matstr
end

"""
Build function target: `MATLABTarget`

```julia
_build_function(target::MATLABTarget, ex::AbstractArray, args...;
                columnmajor = true,
                conv        = toexpr,
                expression  = Val{true},
                fname       = :diffeqf,
                lhsname     = :internal_var___du,
                rhsnames    = [:internal_var___u,:internal_var___p,:internal_var___t])
```

This builds an out of place anonymous function @(t,rhsnames[1]) to be used in MATLAB.
Compatible with the MATLAB differential equation solvers. Only allowed on expressions,
and arrays of expressions.
"""
function _build_function(target::MATLABTarget, ex::AbstractArray, args...;
                         columnmajor = true,
                         conv        = toexpr,
                         expression  = Val{true},
                         fname       = :diffeqf,
                         lhsname     = :internal_var___du,
                         rhsnames    = [:internal_var___u,:internal_var___p,:internal_var___t])

    @assert expression == Val{true}

    if !columnmajor
        return _build_function(target, hcat([row for row ∈ eachrow(ex)]...), args...;
                               columnmajor = true,
                               conv        = conv,
                               expression  = expression,
                               fname       = fname,
                               lhsname     = lhsname,
                               rhsnames    = rhsnames)
    end

    varnumbercache = buildvarnumbercache(args...)
    matstr = ""
    for row ∈ 1:size(ex,1)
        row_strings = Vector{String}()
        for col ∈ 1:size(ex,2)
            lhs = string(lhsname, "[", (col-1) * size(ex,1) + row-1, "]")
            rhs = numbered_expr(value(ex[row, col]), varnumbercache, args...;
                                lhsname  = lhsname,
                                rhsnames = rhsnames,
                                offset   = 0) |> string
            push!(row_strings, rhs)
        end
        matstr = matstr * "  " * join(row_strings, ", ") * ";\n"
    end

    matstr = replace(matstr,"["=>"(")
    matstr = replace(matstr,"]"=>")")
    matstr = "$fname = @($(rhsnames[3]),$(rhsnames[1])) [\n"*matstr*"];\n"

    return matstr

end
_build_function(target::MATLABTarget, ex::Num, args...; kwargs...) = _build_function(target, [ex], args...; kwargs...)
