cell_to_vtkcell(::Type{Line}) = VTKCellTypes.VTK_LINE
cell_to_vtkcell(::Type{Line2D}) = VTKCellTypes.VTK_LINE
cell_to_vtkcell(::Type{Line3D}) = VTKCellTypes.VTK_LINE
cell_to_vtkcell(::Type{QuadraticLine}) = VTKCellTypes.VTK_QUADRATIC_EDGE

cell_to_vtkcell(::Type{Quadrilateral}) = VTKCellTypes.VTK_QUAD
cell_to_vtkcell(::Type{Quadrilateral3D}) = VTKCellTypes.VTK_QUAD
cell_to_vtkcell(::Type{QuadraticQuadrilateral}) = VTKCellTypes.VTK_BIQUADRATIC_QUAD
cell_to_vtkcell(::Type{Triangle}) = VTKCellTypes.VTK_TRIANGLE
cell_to_vtkcell(::Type{QuadraticTriangle}) = VTKCellTypes.VTK_QUADRATIC_TRIANGLE
cell_to_vtkcell(::Type{Cell{2,8,4}}) = VTKCellTypes.VTK_QUADRATIC_QUAD

cell_to_vtkcell(::Type{Hexahedron}) = VTKCellTypes.VTK_HEXAHEDRON
cell_to_vtkcell(::Type{Cell{3,20,6}}) = VTKCellTypes.VTK_QUADRATIC_HEXAHEDRON
cell_to_vtkcell(::Type{Tetrahedron}) = VTKCellTypes.VTK_TETRA
cell_to_vtkcell(::Type{QuadraticTetrahedron}) = VTKCellTypes.VTK_QUADRATIC_TETRA

"""
    vtk_grid(filename::AbstractString, grid::Grid)

Create a unstructured VTK grid from a `Grid`. Return a `DatasetFile`
which data can be appended to, see `vtk_point_data` and `vtk_cell_data`.
"""
function WriteVTK.vtk_grid(filename::AbstractString, grid::Grid{dim,C,T}; compress::Bool=true) where {dim,C,T}
    cls = MeshCell[]
    for cell in grid.cells
        celltype = Ferrite.cell_to_vtkcell(typeof(cell))
        push!(cls, MeshCell(celltype, collect(cell.nodes)))
    end
    coords = reshape(reinterpret(T, getnodes(grid)), (dim, getnnodes(grid)))
    return vtk_grid(filename, coords, cls; compress=compress)
end

"""
    vtk_point_data(vtk, data::Vector{<:SymmetricTensor{2}}, name)

Write the second order tensor field `data` to the vtk file. Twodimensional tensors are padded with zeros.
The tensor field is written such that Paraview recognizes the tensor components, where `XX` corresponds to the `[1,1]` component and so on.
"""
function WriteVTK.vtk_point_data(
    vtk::WriteVTK.DatasetFile,
    data::Union{
        Vector{SymmetricTensor{2,dim,T,M}}
        },
    name::AbstractString
    ) where {dim,T,M}

    if dim == 1
        noutputs = 1
        sort_paraview = [1]
    elseif dim == 2
        noutputs = 6
        sort_paraview = [1,4,2]
    elseif dim == 3
        noutputs = 6
        sort_paraview = [1,4,6,2,5,3]
    end

    npoints = length(data)
    out = zeros(T, noutputs, npoints)
    out[sort_paraview, :] .= reshape(reinterpret(T, data), (M, npoints))

    return vtk_point_data(vtk, out, name)
end

"""
    vtk_point_data(vtk, data::Vector{<:Tensor}, name)

Write the tensor field data to the vtk file. Only writes the tensor values available in `data`.
In the vtu-file, ordering of the tensor components is column-wise (just like Julia).
[1 2
 3 4] => 1, 3, 2, 4
"""
function WriteVTK.vtk_point_data(
    vtk::WriteVTK.DatasetFile,
    data::Union{
        Vector{Tensor{order,dim,T,M}},
        Vector{SymmetricTensor{order,dim,T,M}}
        },
    name::AbstractString
    ) where {order,dim,T,M}

    npoints = length(data)
    out = zeros(T, M, npoints)
    out[1:M, :] .= reshape(reinterpret(T, data), (M, npoints))
    return vtk_point_data(vtk, out, name)
end

"""
    vtk_point_data(vtk, data::Vector{<:Vec}, name)

Write the vector field data to the vtk file.
"""
function WriteVTK.vtk_point_data(vtk::WriteVTK.DatasetFile, data::Vector{Vec{dim,T}}, name::AbstractString) where {dim,T}
    npoints = length(data)
    out = zeros(T, (dim == 2 ? 3 : dim), npoints)
    out[1:dim, :] .= reshape(reinterpret(T, data), (dim, npoints))
    return vtk_point_data(vtk, out, name)
end

function vtk_nodeset(vtk::WriteVTK.DatasetFile, grid::Grid{dim}, nodeset::String) where {dim}
    z = zeros(getnnodes(grid))
    z[collect(getnodeset(grid, nodeset))] .= 1.0
    vtk_point_data(vtk, z, nodeset)
end

"""
    vtk_cellset(vtk, grid::Grid)

Export all cell sets in the grid. Each cell set is exported with
`vtk_cell_data` with value 1 if the cell is in the set, and 0 otherwise.
"""
function vtk_cellset(vtk::WriteVTK.DatasetFile, grid::AbstractGrid, cellsets=keys(grid.cellsets))
    z = zeros(getncells(grid))
    for cellset in cellsets
        z .= 0.0
        z[collect(getcellset(grid, cellset))] .= 1.0
        vtk_cell_data(vtk, z, cellset)
    end
    return vtk
end

"""
    vtk_cellset(vtk, grid::Grid, cellset::String)

Export the cell set specified by `cellset` as cell data with value 1 if
the cell is in the set and 0 otherwise.
"""
vtk_cellset(vtk::WriteVTK.DatasetFile, grid::AbstractGrid, cellset::String) =
    vtk_cellset(vtk, grid, [cellset])

function WriteVTK.vtk_point_data(vtkfile, dh::AbstractDofHandler, u::Vector, suffix="")

    fieldnames = Ferrite.getfieldnames(dh)  # all primary fields

    for name in fieldnames
        data = reshape_to_nodes(dh, u, name)
        vtk_point_data(vtkfile, data, string(name, suffix))
    end

    return vtkfile
end


using Cascadia
using Gumbo
using Requests
using PlotlyJS
using DataStructures: OrderedDict

include("scraper/heise.jl")
include("scraper/golem.jl")
include("scraper/spiegel.jl")

ignore_year = 2019

data = Dict()
#               scrape_...(maxpages, ignore_year)
data["heise"] = scrape_heise(43, ignore_year)
data["golem"] = scrape_golem(6, ignore_year)
data["spiegel"] = scrape_spiegel(20, ignore_year)

# plotting the results
trace_heise = scatter(;x=collect(keys(data["heise"])), y=collect(values(data["heise"])), name= "heise.de")
trace_golem = scatter(;x=collect(keys(data["golem"])), y=collect(values(data["golem"])), name= "golem.de")
trace_spiegel = scatter(;x=collect(keys(data["spiegel"])), y=collect(values(data["spiegel"])), name="spiegel-online.de")

records = [trace_heise, trace_golem, trace_spiegel]

layout = Layout(;
    xaxis = Dict(
        :title => "Jahr der Veröffentlichung",
        :range => [2000, ignore_year-1],
        :dtick => 2
    ),
    margin = Dict(
        :l => 50,
        :r => 10,
        :b => 60,
        :t => 50
    ),
    yaxis = Dict(
        :title => "Anzahl der Artikel"
    ),
    title = "Artikel über künstliche Intelligenz pro Jahr (Stand: $(Dates.today()))"
)

println("plotting...")
plot(records, layout)


"""
Pilot response object. Limits pilot compliance with CAS instructions.
"""
Base.@kwdef mutable struct PilotResponse
    compliant::Bool = true       # whether pilot complies with CAS advisories
	turn_rate::R    = 3.0        # complying turn rate
	vert_rate::R    = 10.0       # complying vertical rate
	cmd::Command    = Command()  # resultant effective command
end

function initialize(pr::PilotResponse)
	pr.cmd = Command()
end

function update(pr::PilotResponse, cmd::Command, cas::CAS)
	pr.cmd = deepcopy(cmd)
    pr.compliant || return

	# horizontal
	if cas.hra == :cw
		pr.cmd.psi_d = pr.turn_rate
	elseif cas.hra == :ccw
		pr.cmd.psi_d = -pr.turn_rate
	end

	# vertical
	if cas.vra == :asc
		pr.cmd.h_d = pr.vert_rate
	elseif cas.vra == :desc
		pr.cmd.h_d = -pr.vert_rate
	end
end


# Quotes
# module Quotes # not a module anymore
type Quote
  value::Float64
  is_valid::Bool

  Quote(value::Float64, is_valid::Bool = true) = new(value, is_valid)
end

# end


const v_e = 2550  # rocket exhaust velocity [m/s]
const M = 250000  # rocket dry mass [kg]

function rocket_fuel(v)
    m_fuel = 0

    # Your code goes here!

    return m_fuel
end


using ALifeBenchmark
using Printf
using Serialization
using Dates
import Random
using Profile
using PProf

const NUM_ITERATIONS = 1e6
const NUM_TRIALS = 1

create_model(logger) = GebModel(logger)
function print_function(trial, step, model)
    if step % 500 == 0
        @info "$step \t $(step / NUM_ITERATIONS) \t $(length(model.organisms))"
    end
end

collect_distribution(create_model, execute!, print_function, NUM_ITERATIONS, NUM_TRIALS, 1)


using PyCall, PyPlot

@testset "plots" begin
    data = load_arviz_data("centered_eight")
    data2 = load_arviz_data("non_centered_eight")

    rng = MersenneTwister(42)
    arr1 = randn(rng, 4, 100)
    arr2 = randn(rng, 4, 100)
    arr3 = randn(rng, 100)

    @testset "$(f)" for f in (plot_trace, plot_pair, plot_joint)
        f(data; var_names = ["tau", "mu"])
        close(gcf())
        f((x = arr1, y = arr2); var_names = ["x", "y"])
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test f((x = arr1, y = arr2); var_names = ["x", "y"], backend = :bokeh) isa
                  ArviZ.BokehPlot
        end
    end

    @testset "$(f)" for f in
                        (plot_autocorr, plot_ess, plot_mcse, plot_posterior, plot_violin)
        f(data; var_names = ["tau", "mu"])
        close(gcf())
        f(arr1)
        close(gcf())
        f((x = arr1, y = arr2); var_names = ["x", "y"])
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test f((x = arr1, y = arr2); var_names = ["x", "y"], backend = :bokeh) isa
                  ArviZ.BokehPlot
        end
    end

    @testset "$(f)" for f in (plot_energy, plot_parallel)
        f(data)
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test f(data; backend = :bokeh) isa ArviZ.BokehPlot
        end
    end

    @testset "$(f)" for f in (plot_density, plot_forest)
        f(data; var_names = ["tau", "mu"])
        close(gcf())
        f([(x = arr1,), (x = arr2,)], var_names = ["x"])
        close(gcf())
        f(arr3)
        close(gcf())
        f((x = arr1, y = arr2); var_names = ["x", "y"])
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test f([(x = arr1,), (x = arr2,)]; var_names = ["x"], backend = :bokeh) isa
                  ArviZ.BokehPlot
        end
    end

    @testset "plot_rank" begin
        plot_rank(data; var_names = ["tau", "mu"])
        close(gcf())
        plot_rank(arr1)
        close(gcf())
        plot_rank((x = arr1, y = arr2); var_names = ["x", "y"])
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test isa(
                plot_rank((x = arr1, y = arr2); var_names = ["x", "y"], backend = :bokeh),
                ArviZ.BokehPlot,
            )
        end
    end

    @testset "plot_compare" begin
        df = compare(Dict("a" => data, "b" => data2))
        plot_compare(df)
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test plot_compare(df; backend = :bokeh) isa ArviZ.BokehPlot
        end
    end

    @testset "$(f)" for f in (plot_dist, plot_kde)
        f(arr1)
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test f(arr1; backend = :bokeh) isa ArviZ.BokehPlot
        end
    end

    @testset "$f" for f in (plot_hpd, plot_hdi)
        x_data = randn(rng, 100)
        y_data = 2 .+ x_data .* 0.5
        y_data_rep = 0.5 .* randn(rng, 200, 100) .+ transpose(y_data)
        f(x_data, y_data_rep)
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test f(x_data, y_data_rep; backend = :bokeh) isa ArviZ.BokehPlot
        end
    end

    @testset "plot_elpd" begin
        plot_elpd(Dict("a" => data, "b" => data2))
        close(gcf())
        plot_elpd(Dict(
            "a" => loo(data; pointwise = true),
            "b" => loo(data2; pointwise = true),
        ))
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test plot_elpd(Dict("a" => data, "b" => data2); backend = :bokeh) isa
                  ArviZ.BokehPlot
        end
    end

    @testset "plot_khat" begin
        l = loo(data; pointwise = true)
        plot_khat(l)
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test plot_khat(l; backend = :bokeh) isa ArviZ.BokehPlot
        end
    end

    @testset "plot_loo_pit" begin
        plot_loo_pit(data; y = "obs")
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test plot_loo_pit(data; y = "obs", backend = :bokeh) isa ArviZ.BokehPlot
        end
    end

    @testset "plot_loo_pit" begin
        plot_ppc(data)
        close(gcf())
        ispynull(ArviZ.bokeh) || @testset "bokeh" begin
            @test plot_ppc(data; backend = :bokeh) isa ArviZ.BokehPlot
        end
    end
end


using Phylo

function _newlabel(ids::Vector{Label}) where Label <: Integer
    return isempty(ids) ? 1 : maximum(ids) + 1
end

function _newlabel(names::Vector{String}, prefix)
    names = collect(Iterators.filter(n -> length(n) > length(prefix) &&
                                     n[1:length(prefix)]==prefix, names))
    start = length(names) + 1
    name = prefix * "$start"
    while (name ∈ names)
        start += 1
        name = prefix * "$start"
    end
    return name
end

function _ntrees(::AbstractTree)
    return 1
end

function _extractnode(::T, node::N) where {T <: AbstractTree, N <: AbstractNode}
    return node
end

function _extractnode(tree::T, nodename::NL) where {NL, BL, T <: AbstractTree{NL, BL}}
    return getnode(tree, nodename)
end

function _extractnode(::T, pair::Pair{NL, N}) where {NL, BL, T <: AbstractTree{NL, BL},
                                                     N <: AbstractNode}
    return pair[2]
end

function _extractnodename(::T, pair::Pair{NL, N}) where {NL, BL, T <: AbstractTree{NL, BL},
                                                         N <: AbstractNode}
    return pair[1]
end

function _extractbranch(::T, branch::B) where {T <: AbstractTree, B <: AbstractBranch}
    return branch
end

function _extractbranch(tree::T, branchname::BL) where {NL, BL, T <: AbstractTree{NL, BL}}
    return getbranch(tree, branchname)
end

function _extractbranch(::T, pair::Pair{BL, B}) where {NL, BL, T <: AbstractTree{NL, BL},
                                                       B <: AbstractBranch}
    return pair[2]
end

function _extractbranchname(::T, pair::Pair{BL, B}) where {NL, BL, T <: AbstractTree{NL, BL},
                                                           B <: AbstractBranch}
    return pair[1]
end

# AbstractTree methods
"""
    _nodetype(::AbstractTree)

Returns type of nodes in a tree.
"""
function _nodetype end

"""
    _branchtype(::AbstractTree)

Returns type of branches in a tree.
"""
function _branchtype end

"""
    _newbranchlabel(tree::AbstractTree)

"""
function _newbranchlabel end

function _newbranchlabel(tree::AbstractTree{NL, String}) where NL
    return _newlabel(_getbranchnames(tree), "Branch ")
end

function _newbranchlabel(tree::AbstractTree{NL, I}) where {NL, I <: Integer}
    return _newlabel(_getbranchnames(tree))
end

"""
    _addbranch!(tree::AbstractTree, source, destination;
                length::Float64 = NaN,
                branchname = _newbranchlabel(tree))

Must be implemented for any AbstractTree subtype.
"""
function _addbranch! end

"""
    _deletebranch!(tree::AbstractTree, branchname)

Must be implemented for any AbstractTree subtype.
"""
function _deletebranch! end

"""
    _branch!(tree::AbstractTree, source, length::Float64, destination, branchname)

"""
function _branch!(tree::AbstractTree, source, length::Float64, destination, branchname)
    _addbranch!(tree, source, _addnode!(tree, destination), length, branchname)
    return destination
end

"""
    _newnodelabel(tree::AbstractTree)

"""
function _newnodelabel end

function _newnodelabel(tree::T) where {BL, T <: AbstractTree{String, BL}}
    return _newlabel(_getnodenames(tree), "Node ")
end

function _newnodelabel(tree::T) where {I <: Integer, BL, T <: AbstractTree{I, BL}}
    return _newlabel(_getnodenames(tree))
end

"""
    _getnodenames(tree::AbstractTree)

Must be implemented for any AbstractTree subtype.
"""
function _getnodenames end

"""
    _getnodes(tree::AbstractTree)

Must be implemented for any AbstractTree subtype.
"""
function _getnodes end

"""
    _addnode!(tree::AbstractTree, nodename)

Must be implemented for any AbstractTree subtype.
"""
function _addnode! end

"""
    _addnodes!(tree::AbstractTree, nodenames::AbstractVector)
    _addnodes!(tree:AbstractTree, count::Integer)

"""
function _addnodes! end

function _addnodes!(tree::AbstractTree, nodenames::AbstractVector)
    return map(name -> _addnode!(tree, name), nodenames)
end

function _addnodes!(tree::AbstractTree, count::Integer)
    return map(name -> addnode!(tree), 1:count)
end

"""
    _deletenode!(tree::AbstractTree, nodename)

Must be implemented for any AbstractTree subtype.
"""
function _deletenode! end

"""
    _hasnode(tree::AbstractTree, nodename)

Must be implemented for any AbstractTree subtype.
"""
function _hasnode end

"""
    _getnode(tree::AbstractTree, nodename)

Must be implemented for any AbstractTree subtype.
"""
function _getnode end

"""
    _getbranchnames(tree::AbstractTree)

Must be implemented for any AbstractTree subtype.
"""
function _getbranchnames end

"""
    _getbranches(tree::AbstractTree)

Must be implemented for any AbstractTree subtype.
"""
function _getbranches end

"""
    _hasbranch(tree::AbstractTree, branchname)

Must be implemented for any AbstractTree subtype.
"""
function _hasbranch end

"""
    _getbranch(tree::AbstractTree, branchname)

Must be implemented for any AbstractTree subtype.
"""
function _getbranch end

"""
    _hasrootheight(::AbstractTree)

"""
function _hasrootheight(::AbstractTree)
    return false
end

"""
    _getrootheight(::AbstractTree)

"""
function _getrootheight(::AbstractTree)
    throw(NullException())
    return NaN
end

"""
    _setrootheight!(::AbstractTree, value)

"""
function _setrootheight!(::AbstractTree, value)
    throw(NullException())
    return value
end
"""
    _validate(::AbstractTree)

"""
function _validate(::AbstractTree)
    return true
end

# AbstractNode methods
"""
    _isleaf(node::AbstractNode)

"""
_isleaf(node::AbstractNode) = _outdegree(node) == 0

"""
    _isroot(node::AbstractNode)

"""
_isroot(node::AbstractNode) = !_hasinbound(node)

"""
    _isinternal(node::AbstractNode)

"""
_isinternal(node::AbstractNode) = _outdegree(node) > 0 && _hasinbound(node)

"""
    _isunattached(node::AbstractNode)

"""
_isunattached(node::AbstractNode) = _outdegree(node) == 0 && !_hasinbound(node)

"""
    _indegree(node::AbstractNode)

"""
_indegree(node::AbstractNode) = _hasinbound(node) ? 1 : 0

"""
    _hasinboundspace(node::AbstractNode)

"""
_hasinboundspace(node::AbstractNode) = !_hasinbound(node)

"""
    _outdegree(node::AbstractNode)

Must be implemented for any AbstractNode subtype.
"""
function _outdegree end

"""
    _hasoutboundspace(node::AbstractNode)

"""
function _hasoutboundspace end

"""
    _hasinbound(node::AbstractNode)

Must be implemented for any AbstractNode subtype.
"""
function _hasinbound end

"""
    _getinbound(node::AbstractNode)

Must be implemented for any AbstractNode subtype.
"""
function _getinbound end

"""
    _setinbound!(node::AbstractNode, inbound)

Must be implemented for any AbstractNode subtype.
"""
function _setinbound! end

"""
    _deleteinbound!(node::AbstractNode, inbound)

Must be implemented for any AbstractNode subtype.
"""
function _deleteinbound! end

"""
    _getoutbounds(node::AbstractNode)

Must be implemented for any AbstractNode subtype.
"""
function _getoutbounds end

"""
    _addinbound!(node::AbstractNode, outbound)

Must be implemented for any AbstractNode subtype.
"""
function _addoutbound! end

"""
    _deleteoutbound!(node::AbstractNode, outbound)

Must be implemented for any AbstractNode subtype.
"""
function _deleteoutbound! end

"""
    _hasheight(tree::AbstractTree, nodename)

"""
function _hasheight(::AbstractTree, _)
    return false
end

"""
    _getheight(tree::AbstractTree, nodename)

"""
function _getheight(::AbstractTree, _)
    throw(NullException())
    return NaN
end

"""
    _setheight!(::AbstractTree, nodename, value)

"""
function _setheight!(::AbstractTree, _, value)
    throw(NullException())
    return value
end

# AbstractBranch methods
"""
    _src

Return source node for a branch. Must be implemented for any
AbstractBranch subtype.
"""
function _src end

"""
    _dst

Return destination node for a branch. Must be implemented for any
AbstractBranch subtype.
"""
function _dst end

"""
    _getlength

Return length of a branch. May be implemented for any AbstractBranch
subtype.
"""
function _getlength end
function _getlength(::AbstractBranch)
    return NaN
end

"""
    _setsrc!

Set source node for a branch. Must be implemented for any
AbstractBranch subtype.
"""
function _setsrc! end

"""
    _setdst!

Set destination node for a graph. Must be implemented for any
AbstractBranch subtype.
"""
function _setdst! end

#  - _getleafnames()
function _getleafnames(tree::AbstractTree)
    return collect(nodenamefilter(_isleaf, tree))
end

function _getleafinfo end
function _setleafinfo! end
function _getnoderecord end
function _setnoderecord! end
function _resetleaves! end
function _clearrootheight! end
function _setnode! end
function _setbranch! end
function _leafinfotype end
function _nleaves end


using ArgParse
using DrWatson
@quickactivate
using BSON
using Random
using FileIO
using Statistics
using DataFrames

# pkgs which come from deserialized BSONs
# have to be present in the Main module
using ValueHistories
using LinearAlgebra

s = ArgParseSettings()
@add_arg_table! s begin
    "modelname"
		arg_type = String
		default = "sptn"
		help = "Name of the model from which to compute ensemble."
	"dataset"
		arg_type = String
		default = "iris"
		help = "Dataset name."
    "dataset_type"
		arg_type = String
		default = "tabular"
        help = "images | tabular"
    "max_seed"
        arg_type = Int
        default = 5
        help = "Number of seeds to go through with each dataset."
    "anomaly_classes"
        arg_type = Int
        default = 10
        help = "Number of anomaly_classes to go through with each dataset."
end

function ensemble_experiment(eval_directory, exp_directory, out_directory)
    eval_files = isdir(eval_directory) ? readdir(eval_directory, join=true) : String[]
    if length(eval_files) == 0
        @warn "There are no valid files in $eval_directory"
        return
    end

    # load the dataframe
    df = reduce(vcat, map(f -> load(f)[:df], eval_files))

    # select best based on criterion
    for criterion in [:val_auc, :val_tpr_5, :val_pat_10]
        best = sortperm(df, order(criterion, rev=true))
        for select_top in [5, 10] # 0 ... automatic ensemble size
            if (select_top > 0) && (length(best) > select_top)
                top = best[1:select_top]
            elseif (length(best) <= select_top)
                top = best
            else
                @warn "Automatic ensemble size is not yet implemented."
                continue
            end

            # get rid of the "eval_" prefix
            exp_files = replace.(basename.(eval_files[top]), "eval_" => "")

            # this assumes that evaluation and experiments files are in sync
            # more precisely for every evaluation file there exist it's exp.
            files_to_load = joinpath.(exp_directory, exp_files)
            results = if all(isfile.(files_to_load))
                load.(files_to_load)
            else
                @warn "Discrepancy between $exp_directory and $eval_directory"
                return
            end

            scores, ensemble = _init_ensemble(results)
            ensemble[:ensemble_files] = exp_files
            for method in [:max, :mean]
                for ignore in [true, false]
                    eagg, fit_t, _, _, _ = @timed aggregate_score!(deepcopy(ensemble), scores;
                                        method=method, ignore_nan=ignore)
                    parameters = (
                        modelname=ensemble[:modelname],
                        criterion=criterion,
                        size=select_top,
                        method=method,
                        ignore_nan=ignore)
                    eagg[:parameters] = parameters
                    eagg[:fit_t] = fit_t
                    eagg[:tr_eval_t] = 0.0
                    eagg[:tst_eval_t] = 0.0
                    eagg[:val_eval_t] = 0.0

                    savef = joinpath(out_directory, savename("ensemble", parameters, "bson"))
                    @info "Saving ensemble experiment to $savef"
                    tagsave(savef, eagg, safe = true)
                end
            end
        end
    end
end

const SPLITS = ["tr", "tst", "val"]

_prefix_symbol(prefix, s) = Symbol("$(prefix)_$(s)")

function _init_ensemble(results)
    ensemble = Dict{Symbol, Any}()      # new ensemble experiment dictionary
    r = first(results)                  # reference dictionary

    for key in vcat([:dataset, :modelname, :seed, :model], _prefix_symbol.(SPLITS, :labels))
        ensemble[key] = r[key]
    end
    # add anomaly class if present
    if Symbol("anomaly_class") in keys(r)
        ensemble[:anomaly_class] = r[:anomaly_class]
	elseif Symbol("ac") in keys(r)
        ensemble[:anomaly_class] = r[:ac]
	end

    ensemble[:ensemble_phash] = [hash(r[:parameters]) for r in results]

    # sum training times
    ensemble[:ensemble_fit_t] = sum([r[:fit_t] for r in results])
    ensemble[:ensemble_eval_t] = sum([r[:tr_eval_t] + r[:tst_eval_t] + r[:val_eval_t] for r in results])

    scores = Dict()
    for key in _prefix_symbol.(SPLITS, :scores)
        kscores = similar(r[key], (length(r[key]), length(results)))
        for (i, rr) in enumerate(values(results))
            kscores[:,i] .= rr[key][:]
        end
        scores[key] = kscores
    end
    scores, ensemble
end

function aggregate_score!(ensemble, scores, weights=nothing;
                            method=:max, ignore_nan=true)
    for (k, s) in scores
        if method == :max
            mask_nan_max = (x) -> (isnan(x) ? -Inf : x)
            ensemble[k] = ignore_nan ?
                        maximum(mask_nan_max, s, dims=2)[:] :
                        maximum(s, dims=2)[:]
        elseif method == :mean
            ensemble[k] = ignore_nan ?
                        [mean(filter(x -> !isnan(x), s[i,:])) for i in 1:size(s, 1)] :
                        mean(s, dims=2)[:]
        elseif method == :wsum
            ensemble[k] = sum(weights' .* s, dims=2)[:]
        else
            error("Unsupported ensemble aggregation.")
        end
    end
    ensemble
end

function main(args)
    @unpack modelname, dataset, dataset_type, max_seed, anomaly_classes = args
    if dataset_type == "tabular"
        for s in 1:max_seed
            eval_directory = datadir("evaluation/$(dataset_type)/$(modelname)/$(dataset)/seed=$(s)/")
            exp_directory = datadir("experiments/$(dataset_type)/$(modelname)/$(dataset)/seed=$(s)/")
            # for now the ensemble outputs are kept in separate directory
            out_directory = datadir("experiments_ensembles/$(dataset_type)/$(modelname)/$(dataset)/seed=$(s)/")

            # run experiment
            @info "Generating ensemble scores of $(modelname) on $(dataset):$(s)"
            ensemble_experiment(eval_directory, exp_directory, out_directory)
        end
    elseif dataset_type == "images"
        for s in 1:max_seed
            for ac in 1:anomaly_classes
                eval_directory = datadir("evaluation/$(dataset_type)/$(modelname)/$(dataset)/ac=$(ac)/seed=$(s)/")
                exp_directory = datadir("experiments/$(dataset_type)/$(modelname)/$(dataset)/ac=$(ac)/seed=$(s)/")
                # for now the ensemble outputs are kept in separate directory
                out_directory = datadir("experiments_ensembles/$(dataset_type)/$(modelname)/$(dataset)/ac=$(ac)/seed=$(s)/")

                # run experiment
                @info "Generating ensemble scores of $(modelname) on $(dataset):$(ac):$(s)"
                ensemble_experiment(eval_directory, exp_directory, out_directory)
            end
        end
    else
        @error "Unsupported dataset type."
    end
end

main(parse_args(ARGS, s))


# ------------------------------------------------------------------
# Licensed under the ISC License. See LICENSE in the project root.
# ------------------------------------------------------------------

"""
    AbstractPath

A path on a spatial domain of type `D`.
"""
abstract type AbstractPath{D<:AbstractDomain} end

"""
    Base.iterate(path, state=1)

Iterate `path` from a given `state`.
"""
Base.iterate(::AbstractPath, state=1) = @error "not implemented"

"""
    Base.length(path)

Return the length of a `path`.
"""
Base.length(p::AbstractPath) = npoints(p.domain)

"""
    Base.eltype(::Type{P})

Return element type of path type `P`.
"""
Base.eltype(::Type{P}) where {P<:AbstractPath} = Int

#------------------
# IMPLEMENTATIONS
#------------------
include("paths/linear.jl")
include("paths/random.jl")
include("paths/source.jl")
include("paths/shifted.jl")


using DataDeps

register(DataDep(
  "thoracic-surgery",
  "http://archive.ics.uci.edu/ml/datasets/Thoracic+Surgery+Data",
  "http://archive.ics.uci.edu/ml/machine-learning-databases/00277/ThoraricSurgery.arff",
  "a92b446b681138619ac3e210d5641744fe77b039c2b7219456a21533e36f3015",
  post_fetch_method=(path -> begin
    UCIData.process_dataset(path,
        target_index=17,
        feature_indices=1:16,
        categoric_indices=[1; 4:15],
        skipto=22,
        delim=",",
    )
  end),
))


import CUDArt
using Base.Test
using Compat

#########################
# Test CUDA array types #
#########################
result = CUDArt.devices(dev->CUDArt.capability(dev)[1] >= 2, nmax=1) do devlist
    # Copying memory to and from device
    @test length(devlist) == 1
    CUDArt.device(devlist[1])
    # Test CudaArrays and CudaPitchedArrays
    for AT in (CUDArt.CudaArray, CUDArt.CudaPitchedArray)
        g = AT(Float64, (5,3))
        if AT == CUDArt.CudaPitchedArray
            p = pointer(g)
            @test p.xsize == 5
            @test p.ysize == 3
        end
        h = CUDArt.to_host(g)
        @test typeof(h) == Array{Float64,2}
        @test size(h) == (5,3)
        g2 = AT(1:5)
        g3 = similar(g2)
        @test eltype(g3) == eltype(g2)
        @test size(g3) == size(g2)
        g3 = similar(g2, Float32)
        @test eltype(g3) == Float32
        g3 = similar(g2, (7,))
        @test eltype(g3) == Int
        @test size(g3) == (7,)
        @test typeof(g3) == AT{Int, 1}
        h2 = CUDArt.to_host(g2)
        @test typeof(h2) == Array{Int,1}
        @test h2 == [1:5;]
        if AT == CUDArt.CudaPitchedArray
            p = pointer(g2)
            @test p.xsize == 5
            @test p.ysize == 1
        end
        # fill!
        fill!(g2, uint8(0))
        h2 = CUDArt.to_host(g2)
        @test h2 == zeros(Int, 5)
        fill!(g2, 0)
        h2 = CUDArt.to_host(g2)
        @test h2 == zeros(Int, 5)
        fill!(g, 0.0)
        h = CUDArt.to_host(g)
        @test all(h .== 0)
        fill!(g, 0)
        h = CUDArt.to_host(g)
        @test all(h .== 0)
        fill!(g, 8.4)
        h = CUDArt.to_host(g)
        @test all(h .== 8.4)
        fill!(g, NaN)
        h = CUDArt.to_host(g)
        @test isequal(h, fill(NaN, size(g)))
        gbig = AT(Int32, (2000,1000))
        fill!(gbig, -17)
        hbig = CUDArt.to_host(gbig)
        @test all(hbig .== -17) && size(hbig) == (2000,1000)
    end
    # Getting portions of an array
    h_src = reshape(1.0:15.0, 3, 5)
    d_src = CUDArt.CudaPitchedArray(h_src)
    d_dest = CUDArt.CudaPitchedArray(Float64, (1, 2))
    get!(d_dest, d_src, (2,2:3), NaN)
    h_dest = CUDArt.to_host(d_dest)
    @test h_dest == [5 8]
    get!(d_dest, d_src, (2,0:1), NaN)
    h_dest = CUDArt.to_host(d_dest)
    @test isequal(h_dest, [NaN 2])
    get!(d_dest, d_src, (3,5:6), 7.3)
    h_dest = CUDArt.to_host(d_dest)
    @test isequal(h_dest, [15 7.3])
    # Copies between CudaPitchedArrays and CudaArrays
    A = rand(uint16(5:11), 7, 2)
    GA = CUDArt.CudaArray(A)
    GP = CUDArt.CudaPitchedArray(eltype(A), size(A))
    copy!(GP, GA)
    H = CUDArt.to_host(GP)
    @test H == A
    A[3,2] = 0
    copy!(GP, A)
    copy!(GA, GP)
    H = CUDArt.to_host(GA)
    @test H == A
    # HostArray (pinned memory)
    A = CUDArt.HostArray(Int32, (6,4))
    A[2,2] = 7
    @test A[2,2] == 7
    G = CUDArt.CudaArray(A)
    CUDArt.device_synchronize()
    A[2,2] = 3
    @test A[2,2] == 3
    copy!(A, G)
#     @test A[2,2] == 3  # asynchronous
    CUDArt.device_synchronize()
    @test A[2,2] == 7
    S = pointer_to_array(pointer(A)+48, (6,2), false)  # a "SubArray"
    B = rand(int32(1:15), 6, 2)
    GB = CUDArt.CudaArray(B)
    copy!(S, GB)
    CUDArt.device_synchronize()
    @test A[:,3:4] == B
    GB = CUDArt.CudaPitchedArray(2B)
    copy!(S, GB)
    CUDArt.device_synchronize()
    @test A[:,3:4] == 2B
end

#####################
# Executing kernels #
#####################
result = CUDArt.devices(dev->CUDArt.capability(dev)[1] >= 2, nmax=1) do devlist
    CUDArt.device(devlist[1])
    a = rand(Float32, 3, 4)
    b = rand(Float32, 3, 4)
    c = CUDArt.CuModule("vadd.ptx") do md
        vadd = CUDArt.CuFunction(md, "vadd")
        ga = CUDArt.CudaArray(a)
        gb = CUDArt.CudaArray(b)
        gc = CUDArt.CudaArray(Float32, size(a))
        CUDArt.launch(vadd, length(a), 1, (ga, gb, gc))
        CUDArt.to_host(gc)
    end
    @test_approx_eq c (a+b)
end

gc()  # check for finalizer errors

#########################################
# Multiple devices, streams, and wait() #
#########################################
if CUDArt.devcount() > 1
    CUDArt.devices(dev->true, nmax=2) do devlist
        sleeptime = 0.5
        results = Array(Any, ceil(Int, 2.5*length(devlist)))
        streams = [(CUDArt.device(dev); CUDArt.Stream()) for dev in devlist]
        # Force one run to precompile
        CUDArt.cudasleep(sleeptime; dev=devlist[1], stream=streams[1])
        wait(streams[1])
        i = 1
        nextidx() = (idx=i; i+=1; idx)
        @sync begin
            for idev = 1:length(devlist)
                sleep(0.01)
                @async begin
                    while true
                        idx = nextidx()
                        if idx > length(results)
                            break
                        end
                        tstart = time()
                        dev = devlist[idev]
                        stream = streams[idev]
                        CUDArt.cudasleep(sleeptime; dev=dev, stream=stream)
                        wait(stream)
                        tstop = time()
                        results[idx] = (tstart, tstop, dev)
                    end
                end
            end
        end
        for i = 1:length(results)
            r = results[i]
            @test sleeptime <= (r[2]-r[1]) <= 1.1*sleeptime
            @test r[3] == devlist[mod1(i, 2)]
        end
        @test results[end][2]-results[1][1] < 1.1*sleeptime*ceil(Int, length(results)/length(devlist))
        nothing
    end
end


## methods to output the GASS results, mainly in CASA format
##

### save an array in CASA format using cfg mapping
###
function save_CASAarr(fileprefix::String, cfg::cfg, bestsub)
    headerCasa= "# observatory=ALMA\n# coordsys=LOC (local tangent plane)\n# x y z diam pad#\n"

    for i in 1:length(bestsub)
        arrStr= string(headerCasa)
        subname= cfg.sub.Subarray_Name[i]

        for j in 1:length(bestsub[i])
            arrStr *= @sprintf("%f %f %f %2.1f %s\n",cfg.arr.X[bestsub[i][j]], cfg.arr.Y[bestsub[i][j]] ,
                cfg.arr.Z[bestsub[i][j]] , cfg.arr.diam[bestsub[i][j]] , cfg.arr.name[bestsub[i][j]])
        end

        filename= fileprefix*"_"*subname*".cfg"
        f= open(filename, "w")
        print(f,arrStr)
        close(f)
        println("## $filename written...")
    end
end

## sort population wrt. score
##
function sort_population(p::population)
    s= sortperm(p.score, rev=true)
    psorted= population(p.age, p.subarr[s,:], p.fitness[s,:], p.score[s], p.param[s,:])
    return(psorted)
end

## full run with JLD output ..
##
function full_run(fileprefix::String, cfg::cfg, jld= false)
    res= gass_optimization(cfg)

    lastPopulation= cfg.ga.Number_Iterations
    psort= sort_population(res[lastPopulation])
    save_CASAarr(fileprefix, cfg, psort.subarr[1,:])

    if jld
       JLD.save(fileprefix*".jld", "gass", res)
       println("## $fileprefix.jld written..")
    end

    return(res)
end


nextfloat(p::Posit8) = reinterpret(Posit8,reinterpret(UInt8,p)+one(UInt8))
nextfloat(p::Posit16) = reinterpret(Posit16,reinterpret(UInt16,p)+one(UInt16))
nextfloat(p::Posit32) = reinterpret(Posit32,reinterpret(UInt32,p)+one(UInt32))

prevfloat(p::Posit8) = reinterpret(Posit8,reinterpret(UInt8,p)-one(UInt8))
prevfloat(p::Posit16) = reinterpret(Posit16,reinterpret(UInt16,p)-one(UInt16))
prevfloat(p::Posit32) = reinterpret(Posit32,reinterpret(UInt32,p)-one(UInt32))

nextfloat(p::T) where {T<:Union{PositX1,PositX2}} = reinterpret(T,reinterpret(UInt32,p)+reinterpret(UInt32,floatmin(T)))
prevfloat(p::T) where {T<:Union{PositX1,PositX2}} = reinterpret(T,reinterpret(UInt32,p)-reinterpret(UInt32,floatmin(T)))


using ACE, BenchmarkTools

pool = ACE.ArrayCache{Float64}()
pool32 = ACE.ArrayCache{Float32}()
genpool = ACE.GenArrayCache()

##

function dosomething!(_A, x)
   A = parent(_A)
   @inbounds begin
      A[1] = 1; A[2] = x
      for n = 3:length(A)
         A[n] = 2 * x * A[n-1] - A[n-2]
      end
   end
   return _A
end

function acquire_N(pool, N, len)
   x = rand()
   for n = 1:N
      x -= 1e-12
      A = ACE.acquire!(pool, Float64, len)
      dosomething!(A, x)
      ACE.release!(A)
   end
   return nothing
end

##

N = 1000
len = 100
acquire_N(pool, N, len)
acquire_N(genpool, N, len)
acquire_N(pool32, N, len)

@btime acquire_N($pool, $N, $len)
@btime acquire_N($genpool, $N, $len)
@btime acquire_N($pool32, $N, $len)

##

A = ACE.acquire!(pool, Float64, len)
gA = ACE.acquire!(genpool, Float64, len)
aA = ACE.acquire!(pool32, Float64, len)
x = rand()

@btime dosomething!($A, $x)
@btime dosomething!($gA, $x)
@btime dosomething!($aA, $x)

##

# @profview acquire_N(genpool, 100_000_000, len)

##
nothing
##

# ##

# function runn(x, N, len)
#    @assert length(x) >= len
#    fill!(x, 0)
#    for n = 1:N, i = 1:len
#       x[i] += i/n
#    end
#    return nothing
# end

# x = rand(1000)
# @btime runn($x, 1000, 1000)

# ##

# function runn2(cache, N, len)
#    x = ACE.acquire!(cache, len)
#    fill!(x.A, 0)
#    for n = 1:N, i = 1:len
#       x[i] += i/n
#    end
#    ACE.release!(x)
#    return nothing
# end

# @btime runn2($pool, 1000, 1000)

# ##

# N = 1000

# for len in [1000, 300, 100, 30, 10]
#    N = 1 # (1000 ÷ len) * 2
#    println("len = $len")
#    x = zeros(len)
#    print(" preallocate: ")
#    @btime runn($x, $N, $len)
#    print("      cached: ")
#    @btime runn2($pool, $N, $len)
#    print("garbage coll: ")
#    @btime runn(zeros($len), $N, $len)
# end


using CSV, DataFrames

export load_data, load_GDP_per_capita, load_expenses_percent_GDP
export load_maddison_GDP_data
export datadir

datadir = normpath(joinpath(@__DIR__, "../data"))

name_mapping = Dict(
    "Norway" => :norway,
    "Sweden" => :sweden,
    "Denmark" => :denmark,
    "Finland" => :finland,
    "United Kingdom" => :uk,
    "United States" => :usa,
    "Canada" => :canada,
    "Japan" => :japan)

function load_data(filename)
    path = joinpath(datadir, filename)

    # Description of each column begins at row 5
    df = CSV.File(path, header=5) |> DataFrame

    # First column is country name
    # 5th to end is where the actual numerical data is
    df = select(df, 1=>:country, 5:ncol(df))
    countries = ["Norway", "Sweden", "Denmark", "Finland",
                 "United Kingdom", "United States", "Canada", "Japan"]

     df = filter(df) do row
        row.country in countries
     end

     table = DataFrame(year = 1960:2020)
     for country in countries
        matches = filter(row -> row.country == country, df)
        if nrow(matches) < 1
            error("Could not find $country in table")
        end
        name = name_mapping[country]

        # extract numberical data for each country.
        # make a column in new table with this data.
        # one column per country
        datacol = collect(matches[1, 2:ncol(matches)-1])
        table[!, name] = datacol
     end

     return table
end

"""
    load_GDP_per_capita() -> DataFrame

Returns a data frame where the first column is the year GDP was recorded.
The next rows are data for one of the Nordic countries, UK, USA, Canada and Japan.

They don't show actual GDP values as they are hard to compare. Instead we have divided
the GDP numbers for every country by its GDP in 1960, and basically said the GDP for
every country is 100 in 1960. Thus the table becomes a way of comparing how GDP
has grown over the years for each country rather than an absolute measure of how
rich each country is.
"""
function load_GDP_per_capita()
    filename = "GDP-per-capita-local-currency.csv"

    gdp = load_data(filename)

    # normalize data, so that each country begins with a GDP index of 100
    # thus we can see relative development of GDP for each country
    for country in values(name_mapping)
        basevalue = gdp[1, country]
        gdp[!, country] .= 100 * (gdp[!, country] / basevalue)
    end

    return gdp
end

"""
    load_expenses_percent_GDP() -> DataFrame
Government expenses such a social benefits, wages to public sector employees.
"""
function load_expenses_percent_GDP()
    filename = "expenses-percent-GDP.csv"
    load_data(filename)
end

function load_maddison_GDP_data()
    path = joinpath(datadir, "maddison-data-gdp-per-capita-in-2011us-single-benchmark.csv")
    gdp = CSV.File(path) |> DataFrame
    gdp = select(gdp, :Entity => :Country, :Year, Symbol("GDP per capita") => :GDP)
end


function eosshow(io::IO, ::MIME"text/plain", Base.@nospecialize(model::EoSModel))
    print(io, typeof(model))
    length(model) == 1 && println(io, " with 1 component:")
    length(model) > 1 && println(io, " with ", length(model), " components:")
    for i in 1:length(model)
        print(io, " \"", model.components[i], "\"")
        println(io)
    end
    if hasfield(typeof(model),:params)
        paramnames = fieldnames(typeof(model.params))
        len_params = length(paramnames)
        !iszero(len_params) && print(io,"Contains parameters: ")
        firstloop = true
        for fieldname in paramnames
            firstloop == false && print(io, ", ")
            print(io, fieldname)
            firstloop = false
        end
    end
end

function eosshow(io::IO, Base.@nospecialize(model::EoSModel))
    print(io, typeof(model))
    firstloop = true
    print(io, "(")
    for i in 1:length(model.components)
        firstloop == false && print(io, ", ")
        print(io, "\"", model.components[i], "\"")
        firstloop = false
    end
    print(io, ")")
end

function gc_eosshow(io::IO, ::MIME"text/plain", Base.@nospecialize(model::EoSModel))
    print(io, typeof(model))
    length(model) == 1 && println(io, " with 1 component:")
    length(model) > 1 && println(io, " with ", length(model), " components:")
    param = model.groups
    for i in 1:length(param.components)

        print(io, " \"", param.components[i], "\": ")
        firstloop = true
        for j in 1:length(param.n_groups[i])
            firstloop == false && print(io, ", ")
            print(io, "\"", param.groups[i][j], "\" => ", param.n_groups[i][j])
            firstloop = false
        end
        i != length(param.components) && println(io)
    end
    println(io)
    print(io, "Contains parameters: ")
    firstloop = true
    for fieldname in fieldnames(typeof(model.params))
        firstloop == false && print(io, ", ")
        print(io, fieldname)
        firstloop = false
    end
end

export eosshow


using SimpleTraits.BaseTraits
using Compat: view

@test istrait(IsAnything{Any})
@test istrait(IsAnything{Union{}})
@test istrait(IsAnything{Int})

@test !istrait(IsNothing{Any})
@test !istrait(IsNothing{Union{}})
@test !istrait(IsNothing{Int})

@test istrait(IsBits{Int})
@test !istrait(IsBits{Vector{Int}})

@test istrait(IsImmutable{Float64})
@test !istrait(IsImmutable{Vector{Int}})

@test istrait(IsCallable{Function})

if VERSION>v"0.4-" # use @generated functions
    a = collect(1:5)
    b = view(a, 2:3)
    c = view(a, 1:2:5)
    @test istrait(IsContiguous{typeof(b)})
    @test !istrait(IsContiguous{typeof(c)})

    @test istrait(IsFastLinearIndex{Vector})
    @test !istrait(IsFastLinearIndex{AbstractArray})

    if VERSION < v"0.5.0-dev"
        @test istrait(IsCallable{Base.AddFun})
    end
end


using ModelingToolkit, OrdinaryDiffEq, Test
using ModelingToolkit: SymbolicContinuousCallback, SymbolicContinuousCallbacks, NULL_AFFECT, get_callback

@parameters t
@variables x(t)=0
D = Differential(t)

eqs = [D(x) ~ 1]
affect = [x ~ 0]

## Test SymbolicContinuousCallback
@testset "SymbolicContinuousCallback constructors" begin
    e = SymbolicContinuousCallback(eqs[])
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == NULL_AFFECT

    e = SymbolicContinuousCallback(eqs)
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == NULL_AFFECT

    e = SymbolicContinuousCallback(eqs, NULL_AFFECT)
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == NULL_AFFECT

    e = SymbolicContinuousCallback(eqs[], NULL_AFFECT)
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == NULL_AFFECT

    e = SymbolicContinuousCallback(eqs => NULL_AFFECT)
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == NULL_AFFECT

    e = SymbolicContinuousCallback(eqs[] => NULL_AFFECT)
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == NULL_AFFECT

    ## With affect

    e = SymbolicContinuousCallback(eqs[], affect)
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == affect

    e = SymbolicContinuousCallback(eqs, affect)
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == affect

    e = SymbolicContinuousCallback(eqs, affect)
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == affect

    e = SymbolicContinuousCallback(eqs[], affect)
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == affect

    e = SymbolicContinuousCallback(eqs => affect)
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == affect

    e = SymbolicContinuousCallback(eqs[] => affect)
    @test e isa SymbolicContinuousCallback
    @test isequal(e.eqs, eqs)
    @test e.affect == affect

    # test plural constructor

    e = SymbolicContinuousCallbacks(eqs[])
    @test e isa Vector{SymbolicContinuousCallback}
    @test isequal(e[].eqs, eqs)
    @test e[].affect == NULL_AFFECT

    e = SymbolicContinuousCallbacks(eqs)
    @test e isa Vector{SymbolicContinuousCallback}
    @test isequal(e[].eqs, eqs)
    @test e[].affect == NULL_AFFECT

    e = SymbolicContinuousCallbacks(eqs[] => affect)
    @test e isa Vector{SymbolicContinuousCallback}
    @test isequal(e[].eqs, eqs)
    @test e[].affect == affect

    e = SymbolicContinuousCallbacks(eqs => affect)
    @test e isa Vector{SymbolicContinuousCallback}
    @test isequal(e[].eqs, eqs)
    @test e[].affect == affect

    e = SymbolicContinuousCallbacks([eqs[] => affect])
    @test e isa Vector{SymbolicContinuousCallback}
    @test isequal(e[].eqs, eqs)
    @test e[].affect == affect

    e = SymbolicContinuousCallbacks([eqs => affect])
    @test e isa Vector{SymbolicContinuousCallback}
    @test isequal(e[].eqs, eqs)
    @test e[].affect == affect

    e = SymbolicContinuousCallbacks(SymbolicContinuousCallbacks([eqs => affect]))
    @test e isa Vector{SymbolicContinuousCallback}
    @test isequal(e[].eqs, eqs)
    @test e[].affect == affect

end

##

@named sys = ODESystem(eqs, continuous_events = [x ~ 1])
@test getfield(sys, :continuous_events)[] == SymbolicContinuousCallback(Equation[x ~ 1], NULL_AFFECT)
@test isequal(equations(getfield(sys, :continuous_events))[], x ~ 1)
fsys = flatten(sys)
@test isequal(equations(getfield(fsys, :continuous_events))[], x ~ 1)

@named sys2 = ODESystem([D(x) ~ 1], continuous_events = [x ~ 2], systems=[sys])
@test getfield(sys2, :continuous_events)[] == SymbolicContinuousCallback(Equation[x ~ 2], NULL_AFFECT)
@test all(ModelingToolkit.continuous_events(sys2) .== [SymbolicContinuousCallback(Equation[x ~ 2], NULL_AFFECT), SymbolicContinuousCallback(Equation[sys.x ~ 1], NULL_AFFECT)])

@test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2)
@test length(ModelingToolkit.continuous_events(sys2)) == 2
@test isequal(ModelingToolkit.continuous_events(sys2)[1].eqs[], x ~ 2)
@test isequal(ModelingToolkit.continuous_events(sys2)[2].eqs[], sys.x ~ 1)

# Functions should be generated for root-finding equations
prob = ODEProblem(sys, Pair[], (0.0, 2.0))
p0 = 0
t0 = 0
@test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback
cb = ModelingToolkit.generate_rootfinding_callback(sys)
cond = cb.condition
out = [0.0]
cond.rf_ip(out, [0], p0, t0)
@test out[] ≈ -1 # signature is u,p,t
cond.rf_ip(out, [1], p0, t0)
@test out[] ≈ 0  # signature is u,p,t
cond.rf_ip(out, [2], p0, t0)
@test out[] ≈ 1  # signature is u,p,t

prob = ODEProblem(sys, Pair[], (0.0, 2.0))
sol = solve(prob, Tsit5())
@test minimum(t->abs(t-1), sol.t) < 1e-10 # test that the solver stepped at the root

# Test that a user provided callback is respected
test_callback = DiscreteCallback(x->x, x->x)
prob = ODEProblem(sys, Pair[], (0.0, 2.0), callback = test_callback)
cbs = get_callback(prob)
@test cbs isa CallbackSet
@test cbs.discrete_callbacks[1] == test_callback

prob = ODEProblem(sys2, Pair[], (0.0, 3.0))
cb = get_callback(prob)
@test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback

cond = cb.condition
out = [0.0, 0.0]
# the root to find is 2
cond.rf_ip(out, [0,0], p0, t0)
@test out[1] ≈ -2 # signature is u,p,t
cond.rf_ip(out, [1,0], p0, t0)
@test out[1] ≈ -1  # signature is u,p,t
cond.rf_ip(out, [2,0], p0, t0) # this should return 0
@test out[1] ≈ 0  # signature is u,p,t

# the root to find is 1
out = [0.0, 0.0]
cond.rf_ip(out, [0,0], p0, t0)
@test out[2] ≈ -1 # signature is u,p,t
cond.rf_ip(out, [0,1], p0, t0) # this should return 0
@test out[2] ≈ 0  # signature is u,p,t
cond.rf_ip(out, [0,2], p0, t0)
@test out[2] ≈ 1  # signature is u,p,t

sol = solve(prob, Tsit5())
@test minimum(t->abs(t-1), sol.t) < 1e-10 # test that the solver stepped at the first root
@test minimum(t->abs(t-2), sol.t) < 1e-10 # test that the solver stepped at the second root

@named sys = ODESystem(eqs, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same state
prob = ODEProblem(sys, Pair[], (0.0, 3.0))
@test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback
sol = solve(prob, Tsit5())
@test minimum(t->abs(t-1), sol.t) < 1e-10 # test that the solver stepped at the first root
@test minimum(t->abs(t-2), sol.t) < 1e-10 # test that the solver stepped at the second root

## Test bouncing ball with equation affect
@variables t x(t)=1 v(t)=0
D = Differential(t)

root_eqs = [x ~ 0]
affect   = [v ~ -v]

@named ball = ODESystem([
    D(x) ~ v
    D(v) ~ -9.8
], t, continuous_events = root_eqs => affect)

@test getfield(ball, :continuous_events)[] == SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -v])
ball = structural_simplify(ball)

tspan = (0.0,5.0)
prob = ODEProblem(ball, Pair[], tspan)
sol = solve(prob,Tsit5())
@test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close
# plot(sol)

## Test bouncing ball in 2D with walls
@variables t x(t)=1 y(t)=0 vx(t)=0 vy(t)=1
D = Differential(t)

continuous_events = [
    [x ~ 0] => [vx ~ -vx]
    [y ~ -1.5, y ~ 1.5] => [vy ~ -vy]
]

@named ball = ODESystem([
    D(x) ~ vx
    D(y) ~ vy
    D(vx) ~ -9.8
    D(vy) ~ -0.01vy # there is some small air resistance
], t, continuous_events = continuous_events)

ball = structural_simplify(ball)

tspan = (0.0,5.0)
prob = ODEProblem(ball, Pair[], tspan)

cb = get_callback(prob)
@test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback
@test getfield(ball, :continuous_events)[1] == SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -vx])
@test getfield(ball, :continuous_events)[2] == SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -vy])
cond = cb.condition
out = [0.0, 0.0, 0.0]
cond.rf_ip(out, [0,0,0,0], p0, t0)
@test out ≈ [0, 1.5, -1.5]

sol = solve(prob,Tsit5())
@test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close
@test minimum(sol[y]) ≈ -1.5 # check wall conditions
@test maximum(sol[y]) ≈ 1.5  # check wall conditions

# tv = sort([LinRange(0, 5, 200); sol.t])
# plot(sol(tv)[y], sol(tv)[x], line_z=tv)
# vline!([-1.5, 1.5], l=(:black, 5), primary=false)
# hline!([0], l=(:black, 5), primary=false)

## Test multi-variable affect
# in this test, there are two variables affected by a single event.
continuous_events = [
    [x ~ 0] => [vx ~ -vx, vy ~ -vy]
]

@named ball = ODESystem([
    D(x) ~ vx
    D(y) ~ vy
    D(vx) ~ -1
    D(vy) ~ 0
], t, continuous_events = continuous_events)

ball = structural_simplify(ball)

tspan = (0.0,5.0)
prob = ODEProblem(ball, Pair[], tspan)
sol = solve(prob,Tsit5())
@test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close
@test -minimum(sol[y]) ≈ maximum(sol[y]) ≈ sqrt(2)  # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number)

# tv = sort([LinRange(0, 5, 200); sol.t])
# plot(sol(tv)[y], sol(tv)[x], line_z=tv)
# vline!([-1.5, 1.5], l=(:black, 5), primary=false)
# hline!([0], l=(:black, 5), primary=false)


"""
    print_parameters(all_obs::AllObservations)

Print information about the variable parameters about which the
`all_obs.param_depend` object stores some interdependency information.
"""
function print_parameters(all_obs::AllObservations)
    num_rec = length(all_obs.recordings)
    params = collect(keys(all_obs.param_depend))
    obs = collect(keys(all_obs.param_depend))
    p_and_o = collect(unique(hcat(obs, params)))
    println()
    println("There are $num_rec independent recordings.")
    println("There are also $(length(p_and_o)) variable parameters.")
    println("* * *")
    println("You may define the var-parameters using the following template:")
    println("# start of template")
    println("using OrderedCollections")
    println()
    println("θ_init = OrderedDict(")
    for (i,p) in enumerate(p_and_o)
        println("\t:$p => ... , # param $i")
    end
    println(")")
    println("# end of template")
    println("and in an MCMC setting you may let your parameter update step ")
    println("refer to a subset of indices you wish to update using the order ")
    println("given above.")
    println("* * *")
end

const _SYMS = Tuple{Vararg{Pair{Int64,Symbol},N} where N} # localθidx, pname
const _OBSIDX = Tuple{Vararg{Pair{Int64,Int64},N} where N} # localθidx, obsidx
"""
    local_symbols(
        all_obs::AllObservations,
        objects::AbstractArray{<:AbstractArray},
        f::Function,
        θsym::Vector{Symbol}
    ) where T

`objects` is usually an array of arrays that must in total have the same number
of elements as there are observations in the recording. It may however have a
different jagged structure. The output is of the same structure as `objects`
and gives for each `o` in `object` a corresponding lists of parameters and
observations it depends on.
"""
function local_symbols(
        all_obs::AllObservations,
        objects::AbstractArray{<:AbstractArray},
        f::Function,
        θsym::Vector{Symbol}
    ) where T
    all_p_symbols = Vector{Vector{_SYMS}}(undef, length(objects))
    all_o_indices = Vector{Vector{_OBSIDX}}(undef, length(objects))
    _i = _j = 1

    for i in eachindex(objects)
        all_p_symbols[i] = Vector{_SYMS}(undef, length(objects[i]))
        all_o_indices[i] = Vector{_OBSIDX}(undef, length(objects[i]))

        for j in eachindex(objects[i])
            plocal, olocal = find_local_names(all_obs, θsym, (_i, _j))
            all_p_symbols[i][j] = Tuple(
                filter(s->hasproperty(f(objects[i][j]), s[2]), plocal)
            )
            all_o_indices[i][j] = Tuple(olocal)
            _j += 1
            if _j > length(all_obs.recordings[_i].obs)
                _j = 1
                _i += 1
            end
        end
    end
    all_p_symbols, all_o_indices
end

function find_local_names(all_obs::AllObservations, θsym, (i, j))
    plocal = Pair{Int64,Symbol}[]
    p_dep = all_obs.param_depend_rev[i]

    for k in 1:length(p_dep)
        global_symbol, local_symbol = p_dep[k]
        idx = findfirst(x->x==global_symbol, θsym)
        if idx != nothing
            push!(plocal, (idx => local_symbol))
        end
    end

    olocal = Pair{Int64,Int64}[]
    o_dep = all_obs.obs_depend_rev[i][j]

    for k in 1:length(o_dep)
        global_symbol, local_idx = o_dep[k]
        idx = findfirst(x->x==global_symbol, θsym)
        if idx != nothing
            push!(olcal, (idx => local_idx))
        end
    end
    plocal, olocal
end


function get_inside_point(obj::InsideFinder)
    return jcall(obj, "getInsidePoint", S2Point, ())
end

function visit_internal_node(obj::InsideFinder, arg0::BSPTree)
    return jcall(obj, "visitInternalNode", void, (BSPTree,), arg0)
end

function visit_leaf_node(obj::InsideFinder, arg0::BSPTree)
    return jcall(obj, "visitLeafNode", void, (BSPTree,), arg0)
end

function visit_order(obj::InsideFinder, arg0::BSPTree)
    return jcall(obj, "visitOrder", BSPTreeVisitor_Order, (BSPTree,), arg0)
end


# Test basic methods
@testset "Basics" begin
    # initialize model and semi_infinite variable
    m = InfiniteModel()
    @infinite_parameter(m, a in [0, 1])
    @infinite_parameter(m, b[1:2] in [0, 1], independent = true)
    @infinite_parameter(m, c[1:2] in [0, 1])
    @variable(m, ivref, Infinite(a, b, c))
    eval_supps = Dict{Int, Float64}(1 => 0.5, 3 => 1, 4 => 0, 5 => 0)
    var = SemiInfiniteVariable(ivref, eval_supps, [2], [2])
    object = VariableData(var, "var")
    idx = SemiInfiniteVariableIndex(1)
    vref = SemiInfiniteVariableRef(m, idx)
    gvref = GeneralVariableRef(m, 1, SemiInfiniteVariableIndex)
    bad_vref = SemiInfiniteVariableRef(m, SemiInfiniteVariableIndex(-1))
    # JuMP.owner_model
    @testset "JuMP.owner_model" begin
        @test owner_model(vref) === m
        @test owner_model(gvref) === m
    end
    # JuMP.index
    @testset "JuMP.index" begin
        @test index(vref) == idx
        @test index(gvref) == idx
    end
    # dispatch_variable_ref
    @testset "dispatch_variable_ref" begin
        @test isequal(dispatch_variable_ref(m, idx), vref)
        @test isequal(dispatch_variable_ref(gvref), vref)
    end
    # _add_data_object
    @testset "_add_data_object" begin
        @test InfiniteOpt._add_data_object(m, object) == idx
    end
    # _data_dictionary
    @testset "_data_dictionary" begin
        @test InfiniteOpt._data_dictionary(m, SemiInfiniteVariable) === m.semi_infinite_vars
        @test InfiniteOpt._data_dictionary(vref) === m.semi_infinite_vars
        @test InfiniteOpt._data_dictionary(gvref) === m.semi_infinite_vars
    end
    # JuMP.is_valid
    @testset "JuMP.is_valid" begin
        @test is_valid(m, vref)
        @test is_valid(m, gvref)
    end
    # _data_object
    @testset "_data_object" begin
        @test InfiniteOpt._data_object(vref) === object
        @test InfiniteOpt._data_object(gvref) === object
        @test_throws ErrorException InfiniteOpt._data_object(bad_vref)
    end
    # _core_variable_object
    @testset "_core_variable_object" begin
        @test InfiniteOpt._core_variable_object(vref) === var
        @test InfiniteOpt._core_variable_object(gvref) === var
    end
    # _set_core_variable_object
    @testset "_set_core_variable_object" begin
        @test InfiniteOpt._set_core_variable_object(vref, var) isa Nothing
    end
    # _measure_dependencies
    @testset "_measure_dependencies" begin
        @test InfiniteOpt._measure_dependencies(vref) == MeasureIndex[]
        @test InfiniteOpt._measure_dependencies(gvref) == MeasureIndex[]
    end
    # _constraint_dependencies
    @testset "_constraint_dependencies" begin
        @test InfiniteOpt._constraint_dependencies(vref) == InfOptConstraintIndex[]
        @test InfiniteOpt._constraint_dependencies(gvref) == InfOptConstraintIndex[]
    end
    # _derivative_dependencies
    @testset "_derivative_dependencies" begin
        @test InfiniteOpt._derivative_dependencies(vref) == DerivativeIndex[]
        @test InfiniteOpt._derivative_dependencies(gvref) == DerivativeIndex[]
    end
    # _object_numbers
    @testset "_object_numbers" begin
        @test InfiniteOpt._object_numbers(vref) == [2]
    end
    # _parameter_numbers
    @testset "_parameter_numbers" begin
        @test InfiniteOpt._parameter_numbers(vref) == [2]
    end
    # infinite_variable_ref
    @testset "infinite_variable_ref" begin
        @test isequal(infinite_variable_ref(vref), ivref)
        @test isequal(infinite_variable_ref(gvref), ivref)
    end
    # eval_supports
    @testset "eval_supports" begin
        @test eval_supports(vref) === eval_supps
        @test eval_supports(gvref) === eval_supps
    end
    # raw_parameter_refs
    @testset "raw_parameter_refs" begin
        @test isequal(raw_parameter_refs(vref), IC.VectorTuple(b[1]))
        @test isequal(raw_parameter_refs(gvref), IC.VectorTuple(b[1]))
    end
    # parameter_refs
    @testset "parameter_refs" begin
        @test isequal(parameter_refs(vref), (b[1],))
        @test isequal(parameter_refs(gvref), (b[1],))
    end
    # parameter_list
    @testset "parameter_list" begin
        @test isequal(parameter_list(vref), [b[1]])
        @test isequal(parameter_list(gvref), [b[1]])
    end
    # JuMP.set_name
    @testset "JuMP.set_name" begin
        # test normal
        @test isa(set_name(vref, "new"), Nothing)
        @test name(vref) == "new"
        @test_throws ErrorException set_name(bad_vref, "")
        # test default
        @test isa(set_name(gvref, "a"), Nothing)
        @test name(vref) == "a"
    end
    # JuMP.name
    @testset "JuMP.name" begin
        @test name(vref) == "a"
        @test name(bad_vref) == ""
    end
    # _make_variable_ref
    @testset "_make_variable_ref" begin
        @test isequal(InfiniteOpt._make_variable_ref(m, idx), gvref)
    end
    # _var_name_dict
    @testset "_var_name_dict" begin
        @test InfiniteOpt._var_name_dict(m) isa Nothing
    end
    # _update_var_name_dict
    @testset "_update_var_name_dict" begin
        m.name_to_var = Dict{String, ObjectIndex}()
        dict = InfiniteOpt._data_dictionary(vref)
        @test InfiniteOpt._update_var_name_dict(m, dict) isa Nothing
        @test InfiniteOpt._var_name_dict(m) == Dict(name(vref) => idx)
        m.name_to_var = nothing
    end
    # parameter_by_name
    @testset "JuMP.variable_by_name" begin
        # test normal
        @test isequal(variable_by_name(m, "a"), gvref)
        @test variable_by_name(m, "test") isa Nothing
        # prepare variable with same name
        idx2 = SemiInfiniteVariableIndex(2)
        @test InfiniteOpt._add_data_object(m, object) == idx2
        vref2 = SemiInfiniteVariableRef(m, idx2)
        @test set_name(vref2, "a") isa Nothing
        # test multiple name error
        @test_throws ErrorException variable_by_name(m, "a")
    end
    # _delete_data_object
    @testset "_delete_data_object" begin
        @test InfiniteOpt._delete_data_object(vref) isa Nothing
        @test length(InfiniteOpt._data_dictionary(vref)) == 1
        @test !is_valid(m, vref)
    end
end

# Test definition methods
@testset "Definition" begin
    # initialize model and semi_infinite variable
    m = InfiniteModel()
    @infinite_parameter(m, a in [0, 1])
    @infinite_parameter(m, b[1:2] in [0, 1], independent = true)
    @infinite_parameter(m, c[1:2] in [0, 1])
    @variable(m, ivref, Infinite(a, b, c))
    eval_supps = Dict{Int, Float64}(1 => 0.5, 3 => 1, 4 => 0, 5 => 0)
    info = VariableInfo(false, NaN, false, NaN, false, NaN, false, NaN, false, false)
    info2 = VariableInfo(false, NaN, false, NaN, false, NaN, false, NaN, false, true)
    # test _process_value
    @testset "_process_value" begin
        @test isequal(InfiniteOpt._process_value(0), 0)
        @test isequal(InfiniteOpt._process_value(0a + 1), 1)
        @test isequal(InfiniteOpt._process_value(1a), a)
        @test isequal(InfiniteOpt._process_value(a + 2), a + 2)
    end
    # test SemiInfinite{V, T}
    @testset "SemiInfinite{T, V}" begin
        @test isequal(SemiInfinite(ivref, 0, b, c).infinite_variable_ref, ivref)
        @test SemiInfinite(ivref, 0, [b[1], 0], c).parameter_values isa IC.VectorTuple
    end
    # test JuMP.build_variable for macros
    @testset "JuMP.build_variable macro wrapper" begin
        # test errors
        @test_throws ErrorException build_variable(error, info,
                                                   SemiInfinite(ivref, 0, b, a),
                                                   bad = 42)
        @test_throws ErrorException build_variable(error, info2,
                                                   SemiInfinite(ivref, 0, b, a))
        @test_throws ErrorException build_variable(error, info, SemiInfinite(0))
        @test_throws ErrorException build_variable(error, info,
                                                   SemiInfinite(ivref, 0, b,
                                                   dispatch_variable_ref.(c)))
        @test_throws ErrorException build_variable(error, info,
                                                   SemiInfinite(ivref, 0, b))
        # test normal
        @test isequal(build_variable(error, info,
                             SemiInfinite(ivref, 0, b, c)).infinite_variable_ref, ivref)
        @test build_variable(error, info,
                             SemiInfinite(ivref, 0, b, c)).eval_supports == Dict(1 => 0.0)
    end
    # test JuMP.build_variable
    @testset "JuMP.build_variable" begin
        # test errors
        dvref = InfiniteOpt._make_variable_ref(m, FiniteVariableIndex(1))
        @test_throws ErrorException build_variable(error, dvref, eval_supps)
        eval_supps[6] = 1
        @test_throws ErrorException build_variable(error, ivref, eval_supps)
        delete!(eval_supps, 6)
        eval_supps[4] = 2
        @test_throws ErrorException build_variable(error, ivref, eval_supps)
        eval_supps[4] = 0
        # test normal
        @test isequal(build_variable(error, ivref, eval_supps).infinite_variable_ref, ivref)
        @test build_variable(error, ivref, eval_supps).eval_supports === eval_supps
        @test build_variable(error, ivref, eval_supps).object_nums == [2]
        @test build_variable(error, ivref, eval_supps, check = false).object_nums == [2]
        @test build_variable(error, ivref, eval_supps, check = false).parameter_nums == [2]
    end
    # test JuMP.add_variable
    @testset "JuMP.add_variable" begin
        # test errors
        m2 = InfiniteModel()
        var = build_variable(error, ivref, eval_supps, check = false)
        @test_throws VariableNotOwned{InfiniteVariableRef} add_variable(m2, var)
        # test normal
        idx = SemiInfiniteVariableIndex(1)
        vref = SemiInfiniteVariableRef(m, idx)
        gvref = GeneralVariableRef(m, 1, SemiInfiniteVariableIndex)
        @test isequal(add_variable(m, var), gvref)
        @test name(vref) == ""
        @test eval_supports(vref) === eval_supps
        @test supports(a) == [0.5]
        @test supports(b[2]) == [1]
        @test supports(c) == zeros(2, 1)
        @test supports(b[1]) == []
        @test InfiniteOpt._object_numbers(vref) == [2]
        @test InfiniteOpt._semi_infinite_variable_dependencies(ivref) == [idx]
        # test with set name (redundant add)
        @test isequal(add_variable(m, var, "cat"), gvref)
        @test name(vref) == "cat"
        # test with partial supports
        eval_supps2 = Dict{Int, Float64}(1 => 0.5, 3 => 0, 5 => 1)
        var = build_variable(error, ivref, eval_supps2, check = false)
        idx = SemiInfiniteVariableIndex(2)
        vref = SemiInfiniteVariableRef(m, idx)
        gvref = GeneralVariableRef(m, 2, SemiInfiniteVariableIndex)
        @test isequal(add_variable(m, var), gvref)
        @test InfiniteOpt._data_object(vref).name == ""
        @test supports(a) == [0.5]
        @test supports(b[2]) == [0, 1]
        @test supports(b[1]) == []
        @test supports(c) == zeros(2, 1)
    end
    # test macro definition
    @testset "Macro Definition" begin
        # anonymous definition
        vref = GeneralVariableRef(m, 3, SemiInfiniteVariableIndex)
        @test isequal(@variable(m, variable_type = SemiInfinite(ivref, 0, [b[1], 0], c)), vref)
        @test isequal(parameter_refs(vref), (b[1], c))
        @test eval_supports(vref) == Dict(1 => 0.0, 3 => 0.0)
        @test supports(a) == [0, 0.5]
        @test supports(b[2]) == [0, 1]
        @test supports(b[1]) == []
        # explicit definition (redundant)
        vref = GeneralVariableRef(m, 3, SemiInfiniteVariableIndex)
        @test isequal(@variable(m, test, SemiInfinite(ivref, 0, [b[1], 0], c)), vref)
        @test isequal(parameter_refs(vref), (b[1], c))
        @test eval_supports(vref) == Dict(1 => 0.0, 3 => 0.0)
        @test name(vref) == "test"
        # array definition
        vrefs = [GeneralVariableRef(m, i, SemiInfiniteVariableIndex) for i in 4:5]
        @test isequal(@variable(m, [i = 1:2], SemiInfinite(ivref, i - 1, b, c)), vrefs)
        @test isequal(parameter_refs(vrefs[1]), (b, c))
        @test eval_supports.(vrefs) == [Dict(1 => 0.0), Dict(1 => 1.0)]
    end
    # test restriciton definition
    @testset "Restriction" begin
        # test errors
        @test_throws ErrorException restrict(ivref, 0, b)
        @test_throws ErrorException ivref(0, b)
        # test normal wth restrict
        vref = GeneralVariableRef(m, 6, SemiInfiniteVariableIndex)
        @test isequal(restrict(ivref, 0.5, [b[1], 0], c), vref)
        @test isequal(parameter_refs(vref), (b[1], c))
        @test eval_supports(vref) == Dict(1 => 0.5, 3 => 0.0)
        # test normal functionally
        vref = GeneralVariableRef(m, 6, SemiInfiniteVariableIndex)
        @test isequal(ivref(0.5, [b[1], 0], c), vref)
        @test isequal(parameter_refs(vref), (b[1], c))
        @test eval_supports(vref) == Dict(1 => 0.5, 3 => 0.0)
    end
end

# Test the info methods
@testset "Info" begin
    # initialize model and semi_infinite variable
    m = InfiniteModel()
    @infinite_parameter(m, a in [0, 1])
    @infinite_parameter(m, b[1:2] in [0, 1], independent = true)
    @infinite_parameter(m, c[1:2] in [0, 1])
    @variable(m, 0 <= ivref1 <= 1, Infinite(a, b, c), Int)
    @variable(m, ivref2 == 1, Infinite(a, b, c), Bin, start = 0)
    eval_supps = Dict{Int, Float64}(1 => 0.5, 3 => 1, 4 => 0, 5 => 0)
    var1 = build_variable(error, ivref1, eval_supps, check = false)
    gvref1 = add_variable(m, var1)
    rvref1 = dispatch_variable_ref(gvref1)
    var2 = build_variable(error, ivref2, eval_supps, check = false)
    gvref2 = add_variable(m, var2)
    rvref2 = dispatch_variable_ref(gvref2)
    dvref1 = dispatch_variable_ref(ivref1)
    dvref2 = dispatch_variable_ref(ivref2)
    # has_lower_bound
    @testset "JuMP.has_lower_bound" begin
        @test has_lower_bound(rvref1)
        @test has_lower_bound(gvref1)
        @test !has_lower_bound(rvref2)
    end
    # lower_bound
    @testset "JuMP.lower_bound" begin
        @test lower_bound(rvref1) == 0
        @test lower_bound(gvref1) == 0
        @test_throws ErrorException lower_bound(rvref2)
    end
    # _lower_bound_index
    @testset "InfiniteOpt._lower_bound_index" begin
        @test InfiniteOpt._lower_bound_index(rvref1) == InfiniteOpt._lower_bound_index(dvref1)
        @test_throws ErrorException InfiniteOpt._lower_bound_index(rvref2)
    end
    # LowerBoundRef
    @testset "JuMP.LowerBoundRef" begin
        @test LowerBoundRef(rvref1) == LowerBoundRef(dvref1)
        @test_throws ErrorException LowerBoundRef(rvref2)
        @test_throws ErrorException LowerBoundRef(gvref2)
    end
    # has_upper_bound
    @testset "JuMP.has_upper_bound" begin
        @test has_upper_bound(rvref1)
        @test has_upper_bound(gvref1)
        @test !has_upper_bound(rvref2)
    end
    # upper_bound
    @testset "JuMP.upper_bound" begin
        @test upper_bound(rvref1) == 1
        @test upper_bound(gvref1) == 1
        @test_throws ErrorException upper_bound(rvref2)
    end
    # _upper_bound_index
    @testset "InfiniteOpt._upper_bound_index" begin
        @test InfiniteOpt._upper_bound_index(rvref1) == InfiniteOpt._upper_bound_index(dvref1)
        @test_throws ErrorException InfiniteOpt._upper_bound_index(rvref2)
    end
    # UpperBoundRef
    @testset "JuMP.UpperBoundRef" begin
        @test UpperBoundRef(rvref1) == UpperBoundRef(dvref1)
        @test_throws ErrorException UpperBoundRef(rvref2)
        @test_throws ErrorException UpperBoundRef(gvref2)
    end
    # is_fixed
    @testset "JuMP.is_fixed" begin
        @test is_fixed(rvref2)
        @test !is_fixed(rvref1)
        @test !is_fixed(gvref1)
    end
    # fix_value
    @testset "JuMP.fix_value" begin
        @test fix_value(rvref2) == 1
        @test_throws ErrorException fix_value(rvref1)
        @test_throws ErrorException fix_value(gvref1)
    end
    # _fix_index
    @testset "InfiniteOpt._fix_index" begin
        @test InfiniteOpt._fix_index(rvref2) == InfiniteOpt._fix_index(dvref2)
        @test_throws ErrorException InfiniteOpt._fix_index(rvref1)
    end
    # FixRef
    @testset "JuMP.FixRef" begin
        @test FixRef(rvref2) == FixRef(dvref2)
        @test FixRef(rvref2) == FixRef(dvref2)
        @test_throws ErrorException FixRef(rvref1)
    end
    # start_value
    @testset "JuMP.start_value" begin
        @test_throws ErrorException start_value(rvref2)
        @test_throws ErrorException start_value(gvref2)
    end
    # start_value_function
    @testset "start_value_function" begin
        @test start_value_function(rvref2)([0]) == 0
        @test start_value_function(gvref2)([0]) == 0
    end
    # is_binary
    @testset "JuMP.is_binary" begin
        @test is_binary(rvref2)
        @test is_binary(gvref2)
        @test !is_binary(rvref1)
    end
    # _binary_index
    @testset "InfiniteOpt._binary_index" begin
        @test InfiniteOpt._binary_index(rvref2) == InfiniteOpt._binary_index(dvref2)
        @test_throws ErrorException InfiniteOpt._binary_index(rvref1)
    end
    # BinaryRef
    @testset "JuMP.BinaryRef" begin
        @test BinaryRef(rvref2) == BinaryRef(dvref2)
        @test BinaryRef(gvref2) == BinaryRef(dvref2)
        @test_throws ErrorException BinaryRef(rvref1)
    end
    # is_integer
    @testset "JuMP.is_integer" begin
        @test is_integer(rvref1)
        @test !is_integer(rvref2)
        @test !is_integer(gvref2)
    end
    # _integer_index
    @testset "InfiniteOpt._integer_index" begin
        @test InfiniteOpt._integer_index(rvref1) == InfiniteOpt._integer_index(dvref1)
        @test_throws ErrorException InfiniteOpt._integer_index(rvref2)
    end
    # IntegerRef
    @testset "JuMP.IntegerRef" begin
        @test IntegerRef(rvref1) == IntegerRef(dvref1)
        @test IntegerRef(gvref1) == IntegerRef(dvref1)
        @test_throws ErrorException IntegerRef(rvref2)
    end
end

# test usage methods
@testset "Usage" begin
    # initialize model and stuff
    m = InfiniteModel()
    @infinite_parameter(m, t in [0, 1])
    @infinite_parameter(m, x[1:2] in [-1, 1])
    @variable(m, y, Infinite(t, x))
    eval_supps = Dict{Int, Float64}(1 => 0.5, 3 => 1)
    var = build_variable(error, y, eval_supps, check = false)
    gvref = add_variable(m, var)
    vref = dispatch_variable_ref(gvref)
    # test used_by_measure
    @testset "used_by_measure" begin
        @test !used_by_measure(vref)
        push!(InfiniteOpt._measure_dependencies(vref), MeasureIndex(1))
        @test used_by_measure(gvref)
        @test used_by_measure(vref)
        empty!(InfiniteOpt._measure_dependencies(vref))
    end
    # test used_by_constraint
    @testset "used_by_constraint" begin
        @test !used_by_constraint(vref)
        push!(InfiniteOpt._constraint_dependencies(vref), InfOptConstraintIndex(1))
        @test used_by_constraint(gvref)
        @test used_by_constraint(vref)
        empty!(InfiniteOpt._constraint_dependencies(vref))
    end
    # test used_by_derivative
    @testset "used_by_derivative" begin
        @test !used_by_derivative(vref)
        push!(InfiniteOpt._derivative_dependencies(vref), DerivativeIndex(1))
        @test used_by_derivative(gvref)
        @test used_by_derivative(vref)
        empty!(InfiniteOpt._derivative_dependencies(vref))
    end
    # test used_by_objective
    @testset "used_by_objective" begin
        @test !used_by_objective(gvref)
        @test !used_by_objective(vref)
    end
    # test is_used
    @testset "is_used" begin
        # test not used
        @test !is_used(gvref)
        # test used by constraint and/or measure
        push!(InfiniteOpt._constraint_dependencies(vref), InfOptConstraintIndex(1))
        @test is_used(y)
        empty!(InfiniteOpt._constraint_dependencies(vref))
        # test used by derivative
        func = (x) -> NaN
        num = 0.
        info = VariableInfo{Float64, Float64, Float64, Function}(true, num, true,
                                         num, true, num, false, func, true, true)
        deriv = Derivative(info, true, y, t)
        object = VariableData(deriv)
        idx = DerivativeIndex(1)
        dref = DerivativeRef(m, idx)
        @test InfiniteOpt._add_data_object(m, object) == idx
        push!(InfiniteOpt._derivative_dependencies(vref), idx)
        @test !is_used(vref)
        push!(InfiniteOpt._constraint_dependencies(dref), InfOptConstraintIndex(2))
        @test is_used(vref)
        empty!(InfiniteOpt._derivative_dependencies(vref))
    end
end

# Test variable queries for models
@testset "Model Queries" begin
    # initialize model, parameter, and variables
    m = InfiniteModel()
    @infinite_parameter(m, a in [0, 1])
    @infinite_parameter(m, b[1:2] in [0, 1], independent = true)
    @infinite_parameter(m, c[1:2] in [0, 1])
    @variable(m, ivref, Infinite(a, b, c))
    @variable(m, pvref, Point(ivref, 0, [0, 0], [0, 0]))
    @variable(m, hvref)
    eval_supps = Dict{Int, Float64}(1 => 0.5, 3 => 1, 4 => 0, 5 => 0)
    var = build_variable(error, ivref, eval_supps, check = false)
    rvref = add_variable(m, var)
    # num_variables
    @testset "JuMP.num_variables" begin
        @test num_variables(m, InfiniteVariable) == 1
        @test num_variables(m, PointVariable) == 1
        @test num_variables(m, FiniteVariable) == 1
        @test num_variables(m, SemiInfiniteVariable) == 1
        @test num_variables(m) == 4
    end
    # all_variables
    @testset "JuMP.all_variables" begin
        @test all_variables(m, InfiniteVariable) == [ivref]
        @test all_variables(m, PointVariable) == [pvref]
        @test all_variables(m, FiniteVariable) == [hvref]
        @test all_variables(m, SemiInfiniteVariable) == [rvref]
        @test all_variables(m) == [ivref, rvref, pvref, hvref]
    end
end


@testset "Cut Type" begin
    p = Pattern(strengthened_benders=1, lagrangian=0)
    @test SDDiP.getcuttype(1, p) == :strengthened_benders
    @test SDDiP.getcuttype(2, p) == :strengthened_benders

    p = Pattern(benders=1, lagrangian=0)
    @test SDDiP.getcuttype(1, p) == :benders
    @test SDDiP.getcuttype(2, p) == :benders

    p = Pattern(benders=1, strengthened_benders=0, lagrangian=1)
    @test SDDiP.getcuttype(1, p) == :lagrangian
    @test SDDiP.getcuttype(2, p) == :benders
    @test SDDiP.getcuttype(3, p) == :lagrangian
    @test SDDiP.getcuttype(4, p) == :benders

    p = Pattern(benders=2, strengthened_benders=1, lagrangian=0)
    @test SDDiP.getcuttype(1, p) == :benders
    @test SDDiP.getcuttype(2, p) == :strengthened_benders
    @test SDDiP.getcuttype(3, p) == :benders
    @test SDDiP.getcuttype(4, p) == :benders
    @test SDDiP.getcuttype(5, p) == :strengthened_benders

    p = Pattern(benders=1, strengthened_benders=1, lagrangian=1)
    @test SDDiP.getcuttype(1, p) == :strengthened_benders
    @test SDDiP.getcuttype(2, p) == :lagrangian
    @test SDDiP.getcuttype(3, p) == :benders
    @test SDDiP.getcuttype(4, p) == :strengthened_benders
end

@testset "Binary Expansion" begin
    @test_throws Exception binexpand(0)
    @test binexpand(1) == [1]
    @test binexpand(2) == [0, 1]
    @test binexpand(3) == [1, 1]
    @test binexpand(4) == [0, 0, 1]
    @test binexpand(5) == [1, 0, 1]
    @test binexpand(6) == [0, 1, 1]
    @test binexpand(7) == [1, 1, 1]

    @test binexpand(typemax(Int)) == ones(Int, SDDiP._2i_L)

    @test 0 == bincontract([0])
    @test 1 == bincontract([1])
    @test 0 == bincontract([0, 0])
    @test 1 == bincontract([1, 0])
    @test 2 == bincontract([0, 1])
    @test 3 == bincontract([1, 1])
    @test 2 == bincontract([0, 1, 0])
    @test 3 == bincontract([1, 1, 0])
    @test 4 == bincontract([0, 0, 1])
    @test 5 == bincontract([1, 0, 1])
    @test 6 == bincontract([0, 1, 1])
    @test 7 == bincontract([1, 1, 1])
    @test typemax(Int) == bincontract(ones(Int, SDDiP._2i_L))

    @test binexpand(1, length=3) == [1, 0, 0]
    @test binexpand(2, length=3) == [0, 1, 0]
    @test binexpand(4, length=3) == [0, 0, 1]
    @test_throws Exception binexpand(8, length=3)

    @test binexpand(1, maximum=7) == [1, 0, 0]
    @test binexpand(2, maximum=7) == [0, 1, 0]
    @test binexpand(4, maximum=7) == [0, 0, 1]
    @test binexpand(4, maximum=7, length=2) == [0, 0, 1]
    @test_throws Exception binexpand(8, maximum=7)

    @test_throws(Exception, bincontract(Float64, [1, 0], -0.1))
    @test isapprox(bincontract(Float64, [0],       0.1), 0.0, atol=1e-4)
    @test isapprox(bincontract(Float64, [1],       0.1), 0.1, atol=1e-4)
    @test isapprox(bincontract(Float64, [0, 1],    0.1), 0.2, atol=1e-4)
    @test isapprox(bincontract(Float64, [1, 1],    0.1), 0.3, atol=1e-4)
    @test isapprox(bincontract(Float64, [0, 1, 0], 0.1), 0.2, atol=1e-4)
    @test isapprox(bincontract(Float64, [1, 1, 0], 0.1), 0.3, atol=1e-4)
    @test isapprox(bincontract(Float64, [1, 0, 1], 0.1), 0.5, atol=1e-4)
    @test binexpand(0.5)        == binexpand(5)
    @test binexpand(0.54)       == binexpand(5)
    @test binexpand(0.56, 0.1)  == binexpand(6)
    @test binexpand(0.5, 0.01)  == binexpand(50)
    @test binexpand(0.54, 0.01) == binexpand(54)
    @test binexpand(0.56, 0.01) == binexpand(56)

    @test binexpand(0.5, length=5)        == binexpand(5, length=5)
    @test binexpand(0.56, 0.01, length=8) == binexpand(56, length=8)

    @test binexpand(0.5, maximum=3.1)         == binexpand(5, length=5)
    @test binexpand(0.56, 0.01, maximum=2.55) == binexpand(56, length=8)
end

@testset "SDDiP Examples" begin
for example in [
        "SDDiPnewsvendor.jl",
        "all_blacks.jl",
        "generation_expansion.jl",
        "stochastic_all_blacks.jl",
        "booking_management.jl",
        "airconditioning.jl",
        "vehicle_location.jl",
        "integer_optimality.jl"
    ]
    @testset "$example" begin
        include(joinpath(examples_dir, example))
    end
end
end


### ============================================================================
### Opcodes, and their AMPL <-> Julia conversions.
### ============================================================================

include("opcode.jl")

"""
    _JULIA_TO_AMPL

This dictionary is manualy curated, based on the list of opcodes in `opcode.jl`.

The goal is to map Julia functions to their AMPL opcode equivalent.

Sometimes, there is ambiguity, such as the `:+`, which Julia uses for unary,
binary, and n-ary addition, while AMPL doesn't support unary addition, uses
OPPLUS for binary, and OPSUMLIST for n-ary. In these cases, introduce a
different symbol to disambiguate them in the context of this dictionary, and add
logic to `_process_expr!` to rewrite the Julia expression.

Commented out lines are opcodes supported by AMPL that don't have a clear Julia
equivalent. If you can think of one, feel free to add it. But then go and make
similar changes to `_AMPL_TO_JULIA` and `_NARY_OPCODES`.
"""
const _JULIA_TO_AMPL = Dict{Symbol,Int}(
    :+ => OPPLUS,  # binary-plus
    :- => OPMINUS,
    :* => OPMULT,
    :/ => OPDIV,
    :rem => OPREM,
    :^ => OPPOW,
    # OPLESS = 6
    :min => MINLIST,  # n-ary
    :max => MAXLIST,  # n-ary
    # FLOOR = 13
    # CEIL = 14
    :abs => ABS,
    :neg => OPUMINUS,
    :|| => OPOR,
    :&& => OPAND,
    :(<) => LT,
    :(<=) => LE,
    :(==) => EQ,
    :(>=) => GE,
    :(>) => GT,
    :(!=) => NE,
    :(!) => OPNOT,
    :ifelse => OPIFnl,
    :tanh => OP_tanh,
    :tan => OP_tan,
    :sqrt => OP_sqrt,
    :sinh => OP_sinh,
    :sin => OP_sin,
    :log10 => OP_log10,
    :log => OP_log,
    :exp => OP_exp,
    :cosh => OP_cosh,
    :cos => OP_cos,
    :atanh => OP_atanh,
    # OP_atan2 = 48,
    :atan => OP_atan,
    :asinh => OP_asinh,
    :asin => OP_asin,
    :acosh => OP_acosh,
    :acos => OP_acos,
    :sum => OPSUMLIST,  # n-ary plus
    # OPintDIV = 55
    # OPprecision = 56
    # OPround = 57
    # OPtrunc = 58
    # OPCOUNT = 59
    # OPNUMBEROF = 60
    # OPNUMBEROFs = 61
    # OPATLEAST = 62
    # OPATMOST = 63
    # OPPLTERM = 64
    # OPIFSYM = 65
    # OPEXACTLY = 66
    # OPNOTATLEAST = 67
    # OPNOTATMOST = 68
    # OPNOTEXACTLY = 69
    # ANDLIST = 70
    # ORLIST = 71
    # OPIMPELSE = 72
    # OP_IFF = 73
    # OPALLDIFF = 74
    # OPSOMESAME = 75
    # OP1POW = 76
    # OP2POW = 77
    # OPCPOW = 78
    # OPFUNCALL = 79
    # OPNUM = 80
    # OPHOL = 81
    # OPVARVAL = 82
    # N_OPS = 83
)

"""
    _AMPL_TO_JULIA

This dictionary is manualy curated, based on the list of supported opcodes
`_JULIA_TO_AMPL`.

The goal is to map AMPL opcodes to their Julia equivalents. In addition, we need
to know the arity of each opcode.

If the opcode is an n-ary function, use `-1`.
"""
const _AMPL_TO_JULIA = Dict{Int,Tuple{Int,Function}}(
    OPPLUS => (2, +),
    OPMINUS => (2, -),
    OPMULT => (2, *),
    OPDIV => (2, /),
    OPREM => (2, rem),
    OPPOW => (2, ^),
    # OPLESS = 6
    MINLIST => (-1, minimum),
    MAXLIST => (-1, maximum),
    # FLOOR = 13
    # CEIL = 14
    ABS => (1, abs),
    OPUMINUS => (1, -),
    OPOR => (2, |),
    OPAND => (2, &),
    LT => (2, <),
    LE => (2, <=),
    EQ => (2, ==),
    GE => (2, >=),
    GT => (2, >),
    NE => (2, !=),
    OPNOT => (1, !),
    OPIFnl => (3, ifelse),
    OP_tanh => (1, tanh),
    OP_tan => (1, tan),
    OP_sqrt => (1, sqrt),
    OP_sinh => (1, sinh),
    OP_sin => (1, sin),
    OP_log10 => (1, log10),
    OP_log => (1, log),
    OP_exp => (1, exp),
    OP_cosh => (1, cosh),
    OP_cos => (1, cos),
    OP_atanh => (1, atanh),
    # OP_atan2 = 48,
    OP_atan => (1, atan),
    OP_asinh => (1, asinh),
    OP_asin => (1, asin),
    OP_acosh => (1, acosh),
    OP_acos => (1, acos),
    OPSUMLIST => (-1, sum),
    # OPintDIV = 55
    # OPprecision = 56
    # OPround = 57
    # OPtrunc = 58
    # OPCOUNT = 59
    # OPNUMBEROF = 60
    # OPNUMBEROFs = 61
    # OPATLEAST = 62
    # OPATMOST = 63
    # OPPLTERM = 64
    # OPIFSYM = 65
    # OPEXACTLY = 66
    # OPNOTATLEAST = 67
    # OPNOTATMOST = 68
    # OPNOTEXACTLY = 69
    # ANDLIST = 70
    # ORLIST = 71
    # OPIMPELSE = 72
    # OP_IFF = 73
    # OPALLDIFF = 74
    # OPSOMESAME = 75
    # OP1POW = 76
    # OP2POW = 77
    # OPCPOW = 78
    # OPFUNCALL = 79
    # OPNUM = 80
    # OPHOL = 81
    # OPVARVAL = 82
    # N_OPS = 83
)

"""
    _NARY_OPCODES

A manually curated list of n-ary opcodes, taken from Table 8 of "Writing .nl
files."

Not all of these are implemented. See `_REV_OPCODES` for more detail.
"""
const _NARY_OPCODES = Set([
    MINLIST,
    MAXLIST,
    OPSUMLIST,
    OPCOUNT,
    OPNUMBEROF,
    OPNUMBEROFs,
    ANDLIST,
    ORLIST,
    OPALLDIFF,
])

"""
    _UNARY_SPECIAL_CASES

This dictionary defines a set of unary functions that are special-cased. They
don't exist in the NL file format, but they may be called from Julia, and
they can easily be converted into NL-compatible expressions.

If you have a new unary-function that you want to support, add it here.
"""
const _UNARY_SPECIAL_CASES = Dict{Symbol,Function}(
    :cbrt => (x) -> :($x^(1 / 3)),
    :abs2 => (x) -> :($x^2),
    :inv => (x) -> :(1 / $x),
    :log2 => (x) -> :(log($x) / log(2)),
    :log1p => (x) -> :(log(1 + $x)),
    :exp2 => (x) -> :(2^$x),
    :expm1 => (x) -> :(exp($x) - 1),
    :sec => (x) -> :(1 / cos($x)),
    :csc => (x) -> :(1 / sin($x)),
    :cot => (x) -> :(1 / tan($x)),
    :asec => (x) -> :(acos(1 / $x)),
    :acsc => (x) -> :(asin(1 / $x)),
    :acot => (x) -> :(pi / 2 - atan($x)),
    :sind => (x) -> :(sin(pi / 180 * $x)),
    :cosd => (x) -> :(cos(pi / 180 * $x)),
    :tand => (x) -> :(tan(pi / 180 * $x)),
    :secd => (x) -> :(1 / cos(pi / 180 * $x)),
    :cscd => (x) -> :(1 / sin(pi / 180 * $x)),
    :cotd => (x) -> :(1 / tan(pi / 180 * $x)),
    :asind => (x) -> :(asin($x) * 180 / pi),
    :acosd => (x) -> :(acos($x) * 180 / pi),
    :atand => (x) -> :(atan($x) * 180 / pi),
    :asecd => (x) -> :(acos(1 / $x) * 180 / pi),
    :acscd => (x) -> :(asin(1 / $x) * 180 / pi),
    :acotd => (x) -> :((pi / 2 - atan($x)) * 180 / pi),
    :sech => (x) -> :(1 / cosh($x)),
    :csch => (x) -> :(1 / sinh($x)),
    :coth => (x) -> :(1 / tanh($x)),
    :asech => (x) -> :(acosh(1 / $x)),
    :acsch => (x) -> :(asinh(1 / $x)),
    :acoth => (x) -> :(atanh(1 / $x)),
)

"""
    _BINARY_SPECIAL_CASES

This dictionary defines a set of binary functions that are special-cased. They
don't exist in the NL file format, but they may be called from Julia, and
they can easily be converted into NL-compatible expressions.

If you have a new binary-function that you want to support, add it here.
"""
const _BINARY_SPECIAL_CASES = Dict{Symbol,Function}(:\ => (x, y) -> :($y / $x))

### ============================================================================
### Nonlinear expressions
### ============================================================================

# TODO(odow): This type isn't great. We should experiment with something that is
# type-stable, like
#
# @enum(_NLType, _INTEGER, _DOUBLE, _VARIABLE)
# struct _NLTerm
#     type::_NLType
#     data::Int64
# end
# _NLTerm(x::Int) = _NLTerm(_INTEGER, x)
# _NLTerm(x::Float64) = _NLTerm(_DOUBLE, reinterpret(Int64, x))
# _NLTerm(x::MOI.VariableIndex) = _NLTerm(_VARIABLE, x.value)
# function _value(x::_NLTerm)
#     if x.type == _INTEGER
#         return x.data
#     elseif x.type == _DOUBLE
#         return reinterpret(Float64, x.data)
#     else
#         @assert x.type == _VARIABLE
#         return MOI.VariableIndex(x.data)
#     end
# end

const _NLTerm = Union{Int,Float64,MOI.VariableIndex}

struct _NLExpr
    is_linear::Bool
    nonlinear_terms::Vector{_NLTerm}
    linear_terms::Dict{MOI.VariableIndex,Float64}
    constant::Float64
end

function Base.:(==)(x::_NLExpr, y::_NLExpr)
    return x.is_linear == y.is_linear &&
           x.nonlinear_terms == y.nonlinear_terms &&
           x.linear_terms == y.linear_terms &&
           x.constant == y.constant
end

_NLExpr(x::MOI.VariableIndex) = _NLExpr(true, _NLTerm[], Dict(x => 1.0), 0.0)

function _add_or_set(dict, key, value)
    if haskey(dict, key)
        dict[key] += value
    else
        dict[key] = value
    end
    return
end

function _NLExpr(x::MOI.ScalarAffineFunction)
    linear = Dict{MOI.VariableIndex,Float64}()
    for (i, term) in enumerate(x.terms)
        _add_or_set(linear, term.variable, term.coefficient)
    end
    return _NLExpr(true, _NLTerm[], linear, x.constant)
end

function _NLExpr(x::MOI.ScalarQuadraticFunction)
    linear = Dict{MOI.VariableIndex,Float64}()
    for (i, term) in enumerate(x.affine_terms)
        _add_or_set(linear, term.variable, term.coefficient)
    end
    terms = _NLTerm[]
    N = length(x.quadratic_terms)
    if N == 0 || N == 1
        # If there are 0 or 1 terms, no need for an addition node.
    elseif N == 2
        # If there are two terms, use binary addition.
        push!(terms, OPPLUS)
    elseif N > 2
        # If there are more, use n-ary addition.
        push!(terms, OPSUMLIST)
        push!(terms, N)
    end
    for term in x.quadratic_terms
        coefficient = term.coefficient
        # MOI defines quadratic as 1/2 x' Q x :(
        if term.variable_1 == term.variable_2
            coefficient *= 0.5
        end
        # Optimization: no need for the OPMULT if the coefficient is 1.
        if !isone(coefficient)
            push!(terms, OPMULT)
            push!(terms, coefficient)
        end
        push!(terms, OPMULT)
        push!(terms, term.variable_1)
        push!(terms, term.variable_2)
        # For the Jacobian sparsity patterns, we need to add a linear term, even
        # if the variable only appears nonlinearly.
        _add_or_set(linear, term.variable_1, 0.0)
        _add_or_set(linear, term.variable_2, 0.0)
    end
    return _NLExpr(false, terms, linear, x.constant)
end

function _NLExpr(expr::Expr)
    nlexpr = _NLExpr(false, _NLTerm[], Dict{MOI.VariableIndex,Float64}(), 0.0)
    _process_expr!(nlexpr, expr)
    return nlexpr
end

function _process_expr!(expr::_NLExpr, arg::Real)
    return push!(expr.nonlinear_terms, Float64(arg))
end

# Assume the symbol is a numeric constant like `pi`.
_process_expr!(expr::_NLExpr, arg::Symbol) = _process_expr!(expr, eval(arg))

function _process_expr!(expr::_NLExpr, arg::MOI.VariableIndex)
    _add_or_set(expr.linear_terms, arg, 0.0)
    return push!(expr.nonlinear_terms, arg)
end

# TODO(odow): these process_expr! functions use recursion. For large models,
# this may exceed the stack. At some point, we may have to rewrite this to not
# use recursion.
function _process_expr!(expr::_NLExpr, arg::Expr)
    if arg.head == :call
        if length(arg.args) == 2
            f = get(_UNARY_SPECIAL_CASES, arg.args[1], nothing)
            if f !== nothing
                return _process_expr!(expr, f(arg.args[2]))
            end
        elseif length(arg.args) == 3
            f = get(_BINARY_SPECIAL_CASES, arg.args[1], nothing)
            if f !== nothing
                return _process_expr!(expr, f(arg.args[2], arg.args[3]))
            end
        end
        return _process_expr!(expr, arg.args)
    elseif arg.head == :ref
        return _process_expr!(expr, arg.args[2])
    elseif arg == :()
        return  # Some evalators return a null objective of `:()`.
    end
    return error("Unsupported expression: $(arg)")
end

function _process_expr!(expr::_NLExpr, args::Vector{Any})
    op = first(args)
    N = length(args) - 1
    # Before processing the arguments, do some re-writing.
    if op == :+
        if N == 1  # +x, so we can just drop the op and process the args.
            return _process_expr!(expr, args[2])
        elseif N > 2  # nary-addition!
            op = :sum
        end
    elseif op == :- && N == 1
        op = :neg
    elseif op == :* && N > 2  # nary-multiplication.
        # NL doesn't define an nary multiplication operator, so we need to
        # rewrite our expression as a sequence of chained binary operators.
        while N > 2
            # Combine last term with previous to form a binary * expression
            arg = pop!(args)
            args[end] = Expr(:call, :*, args[end], arg)
            N = length(args) - 1
        end
    end
    # Now convert the Julia expression into an _NLExpr.
    opcode = get(_JULIA_TO_AMPL, op, nothing)
    if opcode === nothing
        error("Unsupported operation $(op)")
    end
    push!(expr.nonlinear_terms, opcode)
    if opcode in _NARY_OPCODES
        push!(expr.nonlinear_terms, N)
    end
    for i in 1:N
        _process_expr!(expr, args[i+1])
    end
    return
end

### ============================================================================
### Evaluate nonlinear expressions
### ============================================================================

function _evaluate(expr::_NLExpr, x::Dict{MOI.VariableIndex,Float64})
    y = expr.constant
    for (v, c) in expr.linear_terms
        y += c * x[v]
    end
    if length(expr.nonlinear_terms) > 0
        ret, n = _evaluate(expr.nonlinear_terms[1], expr.nonlinear_terms, x, 1)
        @assert n == length(expr.nonlinear_terms) + 1
        y += ret
    end
    return y
end

function _evaluate(
    head::MOI.VariableIndex,
    ::Vector{_NLTerm},
    x::Dict{MOI.VariableIndex,Float64},
    head_i::Int,
)::Tuple{Float64,Int}
    return x[head], head_i + 1
end

function _evaluate(
    head::Float64,
    ::Vector{_NLTerm},
    ::Dict{MOI.VariableIndex,Float64},
    head_i::Int,
)::Tuple{Float64,Int}
    return head, head_i + 1
end

function _evaluate(
    head::Int,
    terms::Vector{_NLTerm},
    x::Dict{MOI.VariableIndex,Float64},
    head_i::Int,
)::Tuple{Float64,Int}
    N, f = _AMPL_TO_JULIA[head]
    is_nary = (N == -1)
    head_i += 1
    if is_nary
        N = terms[head_i]::Int
        head_i += 1
    end
    args = Vector{Float64}(undef, N)
    for n in 1:N
        args[n], head_i = _evaluate(terms[head_i], terms, x, head_i)
    end
    return is_nary ? f(args) : f(args...), head_i
end