# The following is largely inspired from JuMP/test/JuMPExtension.jl

@enum Level BOTH LOWER_ONLY UPPER_ONLY DUAL_OF_LOWER

abstract type AbstractBilevelModel <: JuMP.AbstractModel end

Base.broadcastable(model::AbstractBilevelModel) = Ref(model)

mutable struct BilevelModel <: AbstractBilevelModel
    # Structured data
    upper::JuMP.AbstractModel
    lower::JuMP.AbstractModel

    # Model data
    nextvaridx::Int                                 # Next variable index is nextvaridx+1
    variables::Dict{Int, JuMP.AbstractVariable}     # Map varidx -> variable
    varnames::Dict{Int, String}                     # Map varidx -> name
    varnames_rev::Dict{String, Int}                 # Map varidx -> name
    var_level::Dict{Int, Level}
    var_upper::Dict{Int, JuMP.AbstractVariableRef}
    var_lower::Dict{Int, JuMP.AbstractVariableRef} #
    var_info::Dict{Int, VariableInfo}

    # upper level decisions that are "parameters" of the second level
    upper_to_lower_link::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}
    # lower level decisions that are input to upper
    lower_to_upper_link::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}
    # lower level decisions that are input to upper
    upper_var_to_lower_ctr_link::Dict{JuMP.AbstractVariableRef, JuMP.ConstraintRef}
    # joint link
    link::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}

    nextconidx::Int                                 # Next constraint index is nextconidx+1
    constraints::Dict{Int, JuMP.AbstractConstraint} # Map conidx -> variable
    connames::Dict{Int, String}                     # Map varidx -> name
    connames_rev::Dict{String, Int}                     # Map varidx -> name
    ctr_level::Dict{Int, Level}
    ctr_upper::Dict{Int, JuMP.ConstraintRef}
    ctr_lower::Dict{Int, JuMP.ConstraintRef}
    ctr_info::Dict{Int, ConstraintInfo}

    upper_objective_sense::MOI.OptimizationSense
    upper_objective_function::JuMP.AbstractJuMPScalar

    lower_objective_sense::MOI.OptimizationSense
    lower_objective_function::JuMP.AbstractJuMPScalar

    # solution data
    need_rebuild::Bool
    need_rebuild_names_ctr::Bool
    need_rebuild_names_var::Bool
    solver#::MOI.ModelLike
    mode
    upper_to_sblm
    lower_to_sblm
    lower_dual_to_sblm
    sblm_to_solver
    lower_primal_dual_map

    solve_time::Float64
    build_time::Float64

    copy_names::Bool

    objdict::Dict{Symbol, Any}    # Same that JuMP.Model's field `objdict`

    function BilevelModel()

        model = new(
            JuMP.Model(),
            JuMP.Model(),

            # var
            0, Dict{Int, JuMP.AbstractVariable}(),
            Dict{Int, String}(),Dict{String, Int}(),
            Dict{Int, Level}(), Dict{Int, JuMP.AbstractVariable}(), Dict{Int, JuMP.AbstractVariable}(),
            Dict{Int, VariableInfo}(),
            # links
            Dict{JuMP.AbstractVariable, JuMP.AbstractVariable}(), Dict{JuMP.AbstractVariable, JuMP.AbstractVariable}(),
            Dict{JuMP.AbstractVariable, JuMP.ConstraintRef}(),
            Dict{JuMP.AbstractVariable, JuMP.AbstractVariable}(),
            #ctr
            0, Dict{Int, JuMP.AbstractConstraint}(),
            Dict{Int, String}(),Dict{String, Int}(),
            Dict{Int, Level}(), Dict{Int, JuMP.AbstractConstraint}(), Dict{Int, JuMP.AbstractConstraint}(),
            Dict{Int, ConstraintInfo}(),
            #obj
            MOI.FEASIBILITY_SENSE,
            zero(JuMP.GenericAffExpr{Float64, BilevelVariableRef}), # Model objective
            MOI.FEASIBILITY_SENSE,
            zero(JuMP.GenericAffExpr{Float64, BilevelVariableRef}), # Model objective

            true,
            true,
            true,
            nothing,
            NoMode{Float64},
            nothing,
            nothing,
            nothing,
            nothing,
            nothing,

            NaN,
            NaN,
            false,
            Dict{Symbol, Any}(),
            )

        return model
    end
end
function BilevelModel(optimizer_constructor;
    bridge_constraints::Bool=true, mode::BilevelSolverMode = SOS1Mode())
    bm = BilevelModel()
    set_mode(bm, mode)
    JuMP.set_optimizer(bm, optimizer_constructor; bridge_constraints=bridge_constraints)
    return bm
end
function set_mode(bm::BilevelModel, mode::BilevelSolverMode)
    bm.mode = mode
    return bm
end

abstract type InnerBilevelModel <: AbstractBilevelModel end
struct UpperModel <: InnerBilevelModel
    m::BilevelModel
end
Upper(m::BilevelModel) = UpperModel(m)
UpperToLower(m::BilevelModel) = UpperModel(m)
struct LowerModel <: InnerBilevelModel
    m::BilevelModel
end
Lower(m::BilevelModel) = LowerModel(m)
LowerToUpper(m::BilevelModel) = LowerModel(m)
bilevel_model(m::InnerBilevelModel) = m.m
mylevel_model(m::UpperModel) = bilevel_model(m).upper
mylevel_model(m::LowerModel) = bilevel_model(m).lower
level(m::LowerModel) = LOWER_ONLY
level(m::UpperModel) = UPPER_ONLY
mylevel_ctr_list(m::LowerModel) = bilevel_model(m).ctr_lower
mylevel_ctr_list(m::UpperModel) = bilevel_model(m).ctr_upper
mylevel_var_list(m::LowerModel) = bilevel_model(m).var_lower
mylevel_var_list(m::UpperModel) = bilevel_model(m).var_upper

# obj

mylevel_obj_sense(m::LowerModel) = bilevel_model(m).lower_objective_sense
mylevel_obj_function(m::LowerModel) = bilevel_model(m).lower_objective_function
mylevel_obj_sense(m::UpperModel) = bilevel_model(m).upper_objective_sense
mylevel_obj_function(m::UpperModel) = bilevel_model(m).upper_objective_function

set_mylevel_obj_sense(m::LowerModel, val) = bilevel_model(m).lower_objective_sense = val
set_mylevel_obj_function(m::LowerModel, val) = bilevel_model(m).lower_objective_function = val
set_mylevel_obj_sense(m::UpperModel, val) = bilevel_model(m).upper_objective_sense = val
set_mylevel_obj_function(m::UpperModel, val) = bilevel_model(m).upper_objective_function = val

function set_link!(m::UpperModel, upper::JuMP.AbstractVariableRef, lower::JuMP.AbstractVariableRef)
    bilevel_model(m).upper_to_lower_link[upper] = lower
    bilevel_model(m).link[upper] = lower
    nothing
end
function set_link!(m::LowerModel, upper::JuMP.AbstractVariableRef, lower::JuMP.AbstractVariableRef)
    bilevel_model(m).lower_to_upper_link[lower] = upper
    bilevel_model(m).link[upper] = lower
    nothing
end

# Models to deal with variables that are not exchanged between models
abstract type SingleBilevelModel <: AbstractBilevelModel end
struct UpperOnlyModel <: SingleBilevelModel
    m::BilevelModel
end
UpperOnly(m::BilevelModel) = UpperOnlyModel(m)
struct LowerOnlyModel <: SingleBilevelModel
    m::BilevelModel
end
LowerOnly(m::BilevelModel) = LowerOnlyModel(m)

bilevel_model(m::SingleBilevelModel) = m.m
mylevel_model(m::UpperOnlyModel) = bilevel_model(m).upper
mylevel_model(m::LowerOnlyModel) = bilevel_model(m).lower
level(::LowerOnlyModel) = LOWER_ONLY
level(::UpperOnlyModel) = UPPER_ONLY
mylevel_var_list(m::LowerOnlyModel) = bilevel_model(m).var_lower
mylevel_var_list(m::UpperOnlyModel) = bilevel_model(m).var_upper

in_upper(l::Level) = l == BOTH || l == UPPER_ONLY || l == DUAL_OF_LOWER
in_lower(l::Level) = l == BOTH || l == LOWER_ONLY

#### Model ####

# Variables
struct BilevelVariableRef <: JuMP.AbstractVariableRef
    model::BilevelModel # `model` owning the variable
    idx::Int       # Index in `model.variables`
    level::Level
end
function BilevelVariableRef(model, idx)
    return BilevelVariableRef(model, idx, model.var_level[idx])
end

mylevel(v::BilevelVariableRef) = v.level
in_level(v::BilevelVariableRef, level::Level) = (
    v.level === BOTH ||
    v.level === level ||
    (v.level === DUAL_OF_LOWER && level === UPPER_ONLY))

in_upper(v::BilevelVariableRef) = in_upper(mylevel(v))
in_lower(v::BilevelVariableRef) = in_lower(mylevel(v))
upper_ref(v::BilevelVariableRef) = v.model.var_upper[v.idx]
lower_ref(v::BilevelVariableRef) = v.model.var_lower[v.idx]

function bound_ref(v::BilevelVariableRef)
    if mylevel(v) == LOWER_ONLY
        return lower_ref(v)
    elseif mylevel(v) == UPPER_ONLY || mylevel(v) == DUAL_OF_LOWER
        return upper_ref(v)
    elseif mylevel(v) == BOTH
        m = v.model
        i = v.idx
        upper_ref = m.var_upper[i]
        # this checks actual owner:
        if haskey(m.upper_to_lower_link, upper_ref)
            return upper_ref
        else
            return m.var_lower[i]
        end
    else
        error("Unknown level")
    end
end

function solver_ref(v::BilevelVariableRef)
    m = v.model
    if mylevel(v) == LOWER_ONLY
        return m.sblm_to_solver[
            m.lower_to_sblm[JuMP.index(lower_ref(v))]]
    else
        return m.sblm_to_solver[
            m.upper_to_sblm[JuMP.index(upper_ref(v))]]
    end
end

Base.broadcastable(v::BilevelVariableRef) = Ref(v)
Base.copy(v::BilevelVariableRef) = v
Base.:(==)(v::BilevelVariableRef, w::BilevelVariableRef) =
    v.model === w.model && v.idx == w.idx && v.level == w.level
JuMP.owner_model(v::BilevelVariableRef) = v.model
JuMP.isequal_canonical(v::BilevelVariableRef, w::BilevelVariableRef) = v == w
JuMP.variable_type(::AbstractBilevelModel) = BilevelVariableRef
# add in BOTH levels
function JuMP.add_variable(inner::InnerBilevelModel, v::JuMP.AbstractVariable, name::String="")
    m = bilevel_model(inner)
    m.nextvaridx += 1
    vref = BilevelVariableRef(m, m.nextvaridx, BOTH)
    # break info so that bounds go to correct level
    var_upper, var_lower = split_variable(inner, v)
    v_upper = JuMP.add_variable(m.upper, var_upper, name)
    m.var_upper[vref.idx] = v_upper
    v_lower = JuMP.add_variable(m.lower, var_lower, name)
    m.var_lower[vref.idx] = v_lower
    m.var_level[vref.idx] = BOTH
    set_link!(inner, v_upper, v_lower)
    m.variables[vref.idx] = v # save complete data
    JuMP.set_name(vref, name)
    m.var_info[vref.idx] = empty_info(v)
    vref
end
function JuMP.add_variable(single::SingleBilevelModel, v::JuMP.AbstractVariable, name::String="")
    m = bilevel_model(single)
    m.nextvaridx += 1
    vref = BilevelVariableRef(m, m.nextvaridx, level(single))
    v_level = JuMP.add_variable(mylevel_model(single), v, name)
    mylevel_var_list(single)[vref.idx] = v_level
    m.var_level[vref.idx] = level(single)
    m.variables[vref.idx] = v
    JuMP.set_name(vref, name)
    m.var_info[vref.idx] = empty_info(v)
    vref
end
function MOI.delete!(m::AbstractBilevelModel, vref::BilevelVariableRef)
    error("No deletion on bilevel models")
    delete!(m.variables, vref.idx)
    delete!(m.varnames, vref.idx)
end
MOI.is_valid(m::BilevelModel, vref::BilevelVariableRef) = vref.idx in keys(m.variables)
JuMP.num_variables(m::BilevelModel) = length(m.variables)
JuMP.num_variables(m::UpperModel) = length(m.m.var_upper)
JuMP.num_variables(m::LowerModel) = length(m.m.var_lower)
function empty_info(::JuMP.AbstractVariable)
    return VariableInfo{Float64}()
end

"""
Split variable because actual owner of the variable should be the one holding the bounds.
"""
function split_variable(::UpperModel, v::JuMP.AbstractVariable)
    var_upper = v
    var_lower = JuMP.ScalarVariable(JuMP.VariableInfo(
        false, NaN,
        false, NaN,
        false, NaN,
        v.info.has_start, v.info.start,
        false, false))
    return var_upper, var_lower
end
function split_variable(::LowerModel, v::JuMP.AbstractVariable)
    var_lower = v
    var_upper = JuMP.ScalarVariable(JuMP.VariableInfo(
        false, NaN,
        false, NaN,
        false, NaN,
        v.info.has_start, v.info.start,
        false, false))
    return var_upper, var_lower
end

# Internal function
variable_info(vref::BilevelVariableRef) = vref.model.variables[vref.idx].info
function update_variable_info(vref::BilevelVariableRef, info::JuMP.VariableInfo)
    vref.model.variables[vref.idx] = JuMP.ScalarVariable(info)
end

function JuMP.has_lower_bound(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        return !isnan(get_dual_lower_bound(get_constrain_ref(vref)))
    end
    return variable_info(vref).has_lb
end
function JuMP.lower_bound(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        return get_dual_lower_bound(get_constrain_ref(vref))
    end
    @assert !JuMP.is_fixed(vref)
    variable_info(vref).lower_bound
end
function JuMP.set_lower_bound(vref::BilevelVariableRef, lower)
    if mylevel(vref) == DUAL_OF_LOWER
        set_dual_lower_bound(get_constrain_ref(vref), lower)
        return vref.model.variables[vref.idx]
    end
    info = variable_info(vref)
    JuMP.set_lower_bound(bound_ref(vref), lower)
    update_variable_info(vref,
                         JuMP.VariableInfo(true, lower,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
function JuMP.delete_lower_bound(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        set_dual_lower_bound(get_constrain_ref(vref), NaN)
        return vref.model.variables[vref.idx]
    end
    info = variable_info(vref)
    JuMP.delete_lower_bound(bound_ref(vref))
    update_variable_info(vref,
                         JuMP.VariableInfo(false, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
function JuMP.has_upper_bound(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        return !isnan(get_dual_upper_bound(get_constrain_ref(vref)))
    end
    return variable_info(vref).has_ub
end
function JuMP.upper_bound(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        return get_dual_upper_bound(get_constrain_ref(vref))
    end
    @assert !JuMP.is_fixed(vref)
    variable_info(vref).upper_bound
end
function JuMP.set_upper_bound(vref::BilevelVariableRef, upper)
    if mylevel(vref) == DUAL_OF_LOWER
        set_dual_upper_bound(get_constrain_ref(vref), upper)
        return vref.model.variables[vref.idx]
    end
    info = variable_info(vref)
    JuMP.set_upper_bound(bound_ref(vref), upper)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           true, upper,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
function JuMP.delete_upper_bound(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        set_dual_upper_bound(get_constrain_ref(vref), NaN)
        return vref.model.variables[vref.idx]
    end
    info = variable_info(vref)
    JuMP.delete_upper_bound(bound_ref(vref))
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           false, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
JuMP.is_fixed(vref::BilevelVariableRef) = variable_info(vref).has_fix
JuMP.fix_value(vref::BilevelVariableRef) = variable_info(vref).fixed_value
function JuMP.fix(vref::BilevelVariableRef, value; force::Bool=false)
    if mylevel(vref) == DUAL_OF_LOWER
        error("Dual variable cannot be fixed.")
    end
    info = variable_info(vref)
    JuMP.fix(bound_ref(vref), value; force=force)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           true, value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
function JuMP.unfix(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        error("Dual variable cannot be fixed.")
    end
    info = variable_info(vref)
    JuMP.unfix(bound_ref(vref))
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           false, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
function JuMP.start_value(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        return get_dual_start(get_constrain_ref(vref))
    end
    variable_info(vref).start
end
function JuMP.set_start_value(vref::BilevelVariableRef, start)
    if mylevel(vref) == DUAL_OF_LOWER
        set_dual_start(get_constrain_ref(vref), start)
        return vref.model.variables[vref.idx]
    end
    info = variable_info(vref)
    in_upper(vref) && JuMP.set_start_value(upper_ref(vref), start)
    in_lower(vref) && JuMP.set_start_value(lower_ref(vref), start)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           true, start,
                                           info.binary, info.integer))
end
JuMP.is_binary(vref::BilevelVariableRef) = variable_info(vref).binary
function JuMP.set_binary(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        error("Dual variable cannot be binary.")
    end
    @assert !JuMP.is_integer(vref)
    info = variable_info(vref)
    JuMP.set_binary(bound_ref(vref))
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           true, info.integer))
end
function JuMP.unset_binary(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        error("Dual variable cannot be binary.")
    end
    info = variable_info(vref)
    JuMP.unset_binary(bound_ref(vref))
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           false, info.integer))
end
JuMP.is_integer(vref::BilevelVariableRef) = variable_info(vref).integer
function JuMP.set_integer(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        error("Dual variable cannot be integer.")
    end
    @assert !JuMP.is_binary(vref)
    info = variable_info(vref)
    JuMP.set_integer(bound_ref(vref))
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, true))
end
function JuMP.unset_integer(vref::BilevelVariableRef)
    if mylevel(vref) == DUAL_OF_LOWER
        error("Dual variable cannot be integer.")
    end
    info = variable_info(vref)
    JuMP.unset_integer(bound_ref(vref))
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, false))
end

# Constraints
# struct BilevelConstraintRef
#     model::BilevelModel # `model` owning the constraint
#     idx::Int       # Index in `model.constraints`
#     level::Level
# end
const BilevelConstraintRef = JuMP.ConstraintRef{BilevelModel, Int}#, Shape <: AbstractShape
function raw_ref(model::BilevelModel, idx::Int)
    if haskey(model.ctr_upper, idx)
        return model.ctr_upper[idx]
    elseif haskey(model.ctr_lower, idx)
        return model.ctr_lower[idx]
    else
        error("Index $(idx) does not belong to BilevelModel")
    end
end
raw_ref(cref::BilevelConstraintRef) = raw_ref(cref.model, cref.index)

function BilevelConstraintRef(model, idx)
    raw = raw_ref(model, idx)
    JuMP.ConstraintRef(model, idx, raw.shape)
end
JuMP.constraint_type(::AbstractBilevelModel) = BilevelConstraintRef
level(cref::BilevelConstraintRef) = cref.model.ctr_level[cref.index]
function JuMP.add_constraint(::BilevelModel, ::JuMP.AbstractConstraint, ::String="")
    error(
        "Can't add constraint directly to the bilevel model `m`, "*
        "attach the constraint to the upper or lower model "*
        "with @constraint(Upper(m), ...) or @constraint(Lower(m), ...)")
end
function JuMP.constraint_object(con_ref::ConstraintRef{BilevelModel, Int})
    raw = raw_ref(con_ref)
    return JuMP.constraint_object(raw)
end
# JuMP.add_constraint(m::UpperModel, c::JuMP.VectorConstraint, name::String="") =
#     error("no vec ctr")
function JuMP.add_constraint(m::InnerBilevelModel, c::Union{JuMP.ScalarConstraint{F,S},JuMP.VectorConstraint{F,S}}, name::String="") where {F,S}
    blm = bilevel_model(m)
    blm.nextconidx += 1
    cref = JuMP.ConstraintRef(blm, blm.nextconidx, JuMP.shape(c))
    func = JuMP.jump_function(c)
    # @show c
    # @show func
    level_func = replace_variables(func, bilevel_model(m), mylevel_model(m), mylevel_var_list(m), level(m))
    level_c = JuMP.build_constraint(error, level_func, c.set)
    level_cref = JuMP.add_constraint(mylevel_model(m), level_c, name)
    blm.ctr_level[cref.index] = level(m)
    mylevel_ctr_list(m)[cref.index] = level_cref
    blm.constraints[cref.index] = c
    blm.ctr_info[cref.index] = empty_info(c)
    JuMP.set_name(cref, name)
    cref
end
function MOI.delete!(m::AbstractBilevelModel, cref::BilevelConstraintRef)
    error("can't delete")
    m.need_rebuild = true
    delete!(m.constraints, cref.index)
    delete!(m.connames, cref.index)
end
MOI.is_valid(m::BilevelModel, cref::BilevelConstraintRef) = cref.index in keys(m.constraints)
MOI.is_valid(m::InnerBilevelModel, cref::BilevelConstraintRef) =
    MOI.is_valid(bilevel_model(m), cref) && level(cref) == level(m)
function JuMP.constraint_object(cref::BilevelConstraintRef, F::Type, S::Type)
    c = cref.model.constraints[cref.index]
    # `TypeError` should be thrown is `F` and `S` are not correct
    # This is needed for the tests in `constraints.jl`
    c.func::F
    c.set::S
    c
end
function empty_info(c::JuMP.ScalarConstraint{F,S}) where {F,S}
    return ConstraintInfo{Float64}()
end
function empty_info(c::JuMP.VectorConstraint{F,S}) where {F,S}
    return ConstraintInfo{Vector{Float64}}(MOI.dimension(c.set))
end

function set_dual_start(cref::BilevelConstraintRef, value::T) where T<:Number
    cref.model.ctr_info[cref.index].start = value
end
function set_dual_start(cref::BilevelConstraintRef, value::T) where T<:Vector{S} where S
    array = cref.model.ctr_info[cref.index].start
    @assert length(array) == length(value)
    copyto!(array, value)
end
function get_dual_start(cref::BilevelConstraintRef)
    cref.model.ctr_info[cref.index].start
end
function set_dual_upper_bound(cref::BilevelConstraintRef, value::T) where T<:Number
    cref.model.ctr_info[cref.index].upper = value
end
function set_dual_upper_bound(cref::BilevelConstraintRef, value::T) where T<:Vector{S} where S
    array = cref.model.ctr_info[cref.index].upper
    @assert length(array) == length(value)
    copyto!(array, value)
end
function get_dual_upper_bound(cref::BilevelConstraintRef)
    cref.model.ctr_info[cref.index].upper
end
function set_dual_lower_bound(cref::BilevelConstraintRef, value::T) where T<:Number
    cref.model.ctr_info[cref.index].lower = value
end
function set_dual_lower_bound(cref::BilevelConstraintRef, value::T) where T<:Vector{S} where S
    array = cref.model.ctr_info[cref.index].lower
    @assert length(array) == length(value)
    copyto!(array, value)
end
function get_dual_lower_bound(cref::BilevelConstraintRef)
    cref.model.ctr_info[cref.index].lower
end

function set_primal_upper_bound_hint(vref::BilevelVariableRef, value::T) where T<:Number
    vref.model.var_info[vref.idx].upper = value
end
function get_primal_upper_bound_hint(vref::BilevelVariableRef)
    vref.model.var_info[vref.idx].upper
end
function set_primal_lower_bound_hint(vref::BilevelVariableRef, value::T) where T<:Number
    vref.model.var_info[vref.idx].lower = value
end
function get_primal_lower_bound_hint(vref::BilevelVariableRef)
    vref.model.var_info[vref.idx].lower
end

function JuMP.value(cref::BilevelConstraintRef; result::Int = 1)
    if level(cref) == BilevelJuMP.LOWER_ONLY
        # Constraint index on the lower model
        con_lower_idx = cref.model.ctr_lower[cref.index].index
        # Single bilevel model constraint associated with the lower level constraint
        con_sblm_idx = cref.model.lower_to_sblm[con_lower_idx]
    else
        # Constraint index on the lower model
        con_upper_idx = cref.model.ctr_upper[cref.index].index
        # Single bilevel model constraint associated with the lower level constraint
        con_sblm_idx = cref.model.upper_to_sblm[con_upper_idx]
    end
    # Solver constraint associated with the single bilevel model constraint
    con_solver_idx = cref.model.sblm_to_solver[con_sblm_idx]
    return MOI.get(cref.model.solver, MOI.ConstraintPrimal(result), con_solver_idx)
end
# variables again (duals)
# code for using dual variables associated with lower level constraints
# in the upper level

function JuMP.num_constraints(model::BilevelModel)
    return length(model.ctr_level)
end
function JuMP.num_constraints(model::LowerModel)
    return length(model.m.ctr_lower)
end
function JuMP.num_constraints(model::UpperModel)
    return length(model.m.ctr_upper)
end

struct DualOf
    ci::BilevelConstraintRef
end
struct DualVariableInfo
    info::JuMP.VariableInfo
    ci::BilevelConstraintRef
end
function JuMP.build_variable(
    _error::Function,
    info::JuMP.VariableInfo,
    dual_of::DualOf;
    extra_kw_args...,
)
    for (kwarg, _) in extra_kw_args
        _error("Unrecognized keyword argument $kwarg")
    end

    if info.has_lb
        set_dual_lower_bound(dual_of.ci, info.lower_bound)
        # info.has_lb = false
        # info.lower_bound = NaN
    end
    if info.has_ub
        set_dual_upper_bound(dual_of.ci, info.upper_bound)
        # info.has_ub = false
        # info.upper_bound = NaN
    end
    info.has_fix   && _error("Dual variable does not support fixing")
    if info.has_start
        set_dual_start(dual_of.ci, info.start)
        # info.has_start = false
        # info.start = NaN
    end
    info.binary    && _error("Dual variable cannot be binary")
    info.integer   && _error("Dual variable cannot be integer")

    info = JuMP.VariableInfo(false, NaN, false, NaN,
                             false, NaN, false, NaN,
                             false, false)

    if level(dual_of.ci) != LOWER_ONLY
        error("Variables can only be tied to LOWER level constraints, got $(dual_of.ci.level) level")
    end

    return DualVariableInfo(
        info,
        dual_of.ci
    )
end
function JuMP.add_variable(inner::UpperModel, dual_info::DualVariableInfo, name::String="")
    # TODO vector version
    m = bilevel_model(inner)
    m.nextvaridx += 1
    vref = BilevelVariableRef(m, m.nextvaridx, DUAL_OF_LOWER)
    v_upper = JuMP.add_variable(m.upper, JuMP.ScalarVariable(dual_info.info), name)
    m.var_upper[vref.idx] = v_upper
    m.var_level[vref.idx] = DUAL_OF_LOWER
    m.upper_var_to_lower_ctr_link[v_upper] = m.ctr_lower[dual_info.ci.index] # TODO improve this
    m.variables[vref.idx] = JuMP.ScalarVariable(dual_info.info)
    JuMP.set_name(vref, name)
    m.var_info[vref.idx] = empty_info(dual_info)
    vref
end
function empty_info(::DualVariableInfo)
    return VariableInfo{Float64}()
end

function get_constrain_ref(vref::BilevelVariableRef)
    model = vref.model
    ctr_ref = model.upper_var_to_lower_ctr_link[model.var_upper[vref.idx]]
    idx = -1
    for (ind, ref) in model.ctr_lower
        if ref == ctr_ref
            idx = ind
        end
    end
    @assert idx != -1
    return BilevelConstraintRef(model, idx, DUAL_OF_LOWER)
end

# Objective
function JuMP.set_objective(m::InnerBilevelModel, sense::MOI.OptimizationSense,
                            f::JuMP.AbstractJuMPScalar)
    set_mylevel_obj_sense(m, sense)
    set_mylevel_obj_function(m, f)
    level_f = replace_variables(f, bilevel_model(m), mylevel_model(m), mylevel_var_list(m), level(m))
    JuMP.set_objective(mylevel_model(m), sense, level_f)
end
JuMP.objective_sense(m::InnerBilevelModel) = mylevel_obj_sense(m)
JuMP.objective_function_type(m::InnerBilevelModel) = typeof(mylevel_obj_function(m))
JuMP.objective_function(m::InnerBilevelModel) = mylevel_obj_function(m)
function JuMP.objective_function(m::InnerBilevelModel, FT::Type)
    mylevel_obj_function(m) isa FT || error("The objective function is not of type $FT")
    mylevel_obj_function(m)
end

function JuMP.relative_gap(bm::BilevelModel)::Float64
    _check_solver(bm)
    return MOI.get(bm.solver, MOI.RelativeGap())
end
function JuMP.relative_gap(bm::UpperModel)::Float64
    return JuMP.relative_gap(bm.m)
end
function JuMP.dual_objective_value(bm::BilevelModel; result::Int = 1)::Float64
    _check_solver(bm)
    return MOI.get(bm.solver, MOI.DualObjectiveValue(result))
end
function JuMP.objective_bound(bm::BilevelModel)::Float64
    _check_solver(bm)
    return MOI.get(bm.solver, MOI.ObjectiveBound())
end
function JuMP.objective_bound(bm::UpperModel)::Float64
    return JuMP.objective_bound(bm.m)
end

JuMP.num_variables(m::AbstractBilevelModel) = JuMP.num_variables(bilevel_model(m))
_plural(n) = (isone(n) ? "" : "s")
function JuMP.show_constraints_summary(io::IO, model::BilevelModel)
    JuMP.show_constraints_summary(io, Upper(model))
    JuMP.show_constraints_summary(io, Lower(model), true)
end
function JuMP.show_constraints_summary(io::IO, model::UpperModel)
    n = JuMP.num_constraints(model)
    println(io, "Upper Constraint", _plural(n), ": ", n)
end
function JuMP.show_constraints_summary(io::IO, model::LowerModel, line_break = false)
    n = JuMP.num_constraints(model)
    print(io, "Lower Constraint", _plural(n), ": ", n, ifelse(line_break, "\n",""))
end
JuMP.show_backend_summary(io::IO, model::InnerBilevelModel) = JuMP.show_backend_summary(io, model.m)
function JuMP.show_backend_summary(io::IO, model::BilevelModel)
    println(io, "Bilevel Model")
    println(io, "Solution method: ", model.mode)
    if model.solver === nothing
        print(io, "No solver attached")
    else
        name = JuMP.solver_name(model::BilevelModel)
        print(io, "Solver name: ", name)
    end
end
function JuMP.solver_name(model::BilevelModel)
    name = try
        MOI.get(model.solver, MOI.SolverName())::String
    catch ex
        if isa(ex, ArgumentError)
            return "SolverName() attribute not implemented by the optimizer."
        else
            rethrow(ex)
        end
    end
end

JuMP.object_dictionary(m::BilevelModel) = m.objdict
JuMP.object_dictionary(m::AbstractBilevelModel) = JuMP.object_dictionary(bilevel_model(m))

function JuMP.show_objective_function_summary(io::IO, model::UpperModel)
    println(io, "Upper objective function type: \n",
            JuMP.objective_function_type(model))
end
function JuMP.show_objective_function_summary(io::IO, model::LowerModel)
    println(io, "Lower objective function type: \n",
            JuMP.objective_function_type(model))
end
function JuMP.show_objective_function_summary(io::IO, model::BilevelModel)
    JuMP.show_objective_function_summary(io, Upper(model))
    JuMP.show_objective_function_summary(io, Lower(model))
end

bilevel_obj_error() = error("There is no objective for BilevelModel use Upper(.) and Lower(.)")

function JuMP.set_objective(m::BilevelModel, sense::MOI.OptimizationSense,
    f::JuMP.AbstractJuMPScalar)
    bilevel_obj_error()
end
JuMP.objective_sense(m::BilevelModel) = JuMP.objective_sense(m.upper)#bilevel_obj_error()
JuMP.objective_function_type(model::BilevelModel) = bilevel_obj_error()
JuMP.objective_function(model::BilevelModel) = bilevel_obj_error()
function JuMP.objective_function(model::BilevelModel, FT::Type)
    bilevel_obj_error()
end

# Names
JuMP.name(vref::BilevelVariableRef) = vref.model.varnames[vref.idx]
function JuMP.set_name(vref::BilevelVariableRef, name::String)
    vref.model.varnames[vref.idx] = name
end
JuMP.name(cref::BilevelConstraintRef) = cref.model.connames[cref.index]
function JuMP.set_name(cref::BilevelConstraintRef, name::String)
    cref.model.connames[cref.index] = name
end
function JuMP.variable_by_name(model::BilevelModel, name::String)
    if model.need_rebuild_names_var
        model.varnames_rev = Dict(v => k for (k,v) in model.varnames)
    end
    idx = model.varnames_rev[name]
    return BilevelVariableRef(model, idx)
end
function JuMP.constraint_by_name(model::BilevelModel, name::String)
    if model.need_rebuild_names_ctr
        model.connames_rev = Dict(v => k for (k,v) in model.connames)
    end
    idx = model.connames_rev[name]
    return BilevelConstraintRef(model, idx)
end

# replace variables
function replace_variables(var::BilevelVariableRef,
    model::BilevelModel,
    inner::JuMP.AbstractModel,
    variable_map::Dict{Int, V},
    level::Level) where {V<:JuMP.AbstractVariableRef}
    if var.model === model && in_level(var, level)
        return variable_map[var.idx]
    elseif var.model === model
        error("Variable $(var) belonging Only to $(var.level) level, was added in the $(level) level.")
    else
        error("A BilevelModel cannot have expression using variables of a BilevelModel different from itself")
    end
end
function replace_variables(aff::JuMP.GenericAffExpr{C, BilevelVariableRef},
    model::BilevelModel,
    inner::JuMP.AbstractModel,
    variable_map::Dict{Int, V},
    level::Level) where {C,V<:JuMP.AbstractVariableRef}
    result = JuMP.GenericAffExpr{C, JuMP.VariableRef}(0.0)#zero(aff)
    result.constant = aff.constant
    for (coef, var) in JuMP.linear_terms(aff)
        JuMP.add_to_expression!(result,
        coef,
        replace_variables(var, model, model, variable_map, level))
    end
    return result
end
function replace_variables(quad::JuMP.GenericQuadExpr{C, BilevelVariableRef},
    model::BilevelModel,
    inner::JuMP.AbstractModel,
    variable_map::Dict{Int, V},
    level::Level) where {C,V<:JuMP.AbstractVariableRef}
    aff = replace_variables(quad.aff, model, model, variable_map, level)
    quadv = JuMP.GenericQuadExpr{C, JuMP.VariableRef}(aff)
    for (coef, var1, var2) in JuMP.quad_terms(quad)
        JuMP.add_to_expression!(quadv,
        coef,
        replace_variables(var1, model, model, variable_map, level),
        replace_variables(var2, model, model, variable_map, level))
    end
    return quadv
end
replace_variables(funcs::Vector, args...) = map(f -> replace_variables(f, args...), funcs)

function print_lp(m, name)
    dest = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_MOF)
    MOI.copy_to(dest, m)
    MOI.write_to_file(dest, name)
end

JuMP.optimize!(::T) where {T<:AbstractBilevelModel} =
    error("Can't solve a model of type: $T ")
function JuMP.optimize!(model::BilevelModel;
    lower_prob = "", upper_prob = "", bilevel_prob = "", solver_prob = "")

    if model.mode === nothing
        error("No solution mode selected, use `set_mode(model, mode)` or initialize with `BilevelModel(optimizer_constructor, mode = some_mode)`")
    else
        mode = model.mode
    end

    _check_solver(model)

    solver = model.solver #optimizer#MOI.Bridges.full_bridge_optimizer(optimizer, Float64)
    if true#!MOI.is_empty(solver)
        MOI.empty!(solver)
    end

    upper = JuMP.backend(model.upper)
    lower = JuMP.backend(model.lower)

    if length(lower_prob) > 0
        print_lp(lower, lower_prob)
    end
    if length(upper_prob) > 0
        print_lp(upper, upper_prob)
    end

    t0 = time()

    moi_upper = JuMP.index.(
        collect(values(model.upper_to_lower_link)))
    moi_link = JuMP.index(model.link)
    moi_link2 = index2(model.upper_var_to_lower_ctr_link)

    # build bound for FortunyAmatMcCarlMode
    build_bounds!(model, mode)

    single_blm, upper_to_sblm, lower_to_sblm, lower_primal_dual_map, lower_dual_to_sblm =
    build_bilevel(upper, lower, moi_link, moi_upper, mode, moi_link2)

    # pass lower level dual variables info (start, upper, lower)
    for (idx, info) in model.ctr_info
        if haskey(model.ctr_lower, idx)
            ctr = model.ctr_lower[idx]
            pre_duals = lower_primal_dual_map.primal_con_dual_var[JuMP.index(ctr)] # vector
            duals = map(x->lower_dual_to_sblm[x], pre_duals)
            pass_dual_info(single_blm, duals, info)
        end
    end
    # pass lower & upper level primal variables info (upper, lower)
    for (idx, info) in model.var_info
        if haskey(model.var_lower, idx)
            var = lower_to_sblm[JuMP.index(model.var_lower[idx])]
        elseif haskey(model.var_upper, idx)
            var = upper_to_sblm[JuMP.index(model.var_upper[idx])]
        else
            continue
        end
        pass_primal_info(single_blm, var, info)
    end
    if length(bilevel_prob) > 0
        print_lp(single_blm, bilevel_prob)
    end
    # print_lp(single_blm, "bilevel_orig.mof")

    sblm_to_solver = MOI.copy_to(solver, single_blm, copy_names = model.copy_names)
    # print_lp(solver, "bilevel_bridge.mof")
    # print_lp(solver.model, "bilevel_cache.mof")

    if length(solver_prob) > 0
        print_lp(solver, solver_prob)
    end

    # model.mode = mode
    # model.solver = solver#::MOI.ModelLike
    model.upper_to_sblm = upper_to_sblm
    model.lower_to_sblm = lower_to_sblm
    model.lower_dual_to_sblm = lower_dual_to_sblm
    model.lower_primal_dual_map = lower_primal_dual_map
    model.sblm_to_solver = sblm_to_solver

    t1 = time()
    model.build_time = t1 - t0

    MOI.optimize!(solver)

    model.solve_time = time() - t1

    return nothing
end
function pass_dual_info(single_blm, dual, info::ConstraintInfo{Float64})
    if !isnan(info.start)
        MOI.set(single_blm, MOI.VariablePrimalStart(), dual[], info.start)
    end
    if !isnan(info.upper) &&
        !MOI.is_valid(single_blm, CI{SVF,LT{Float64}}(dual[].value))
        MOI.add_constraint(single_blm,
            SVF(dual[]), LT{Float64}(info.upper))
    end
    if !isnan(info.lower) &&
        !MOI.is_valid(single_blm, CI{SVF,GT{Float64}}(dual[].value))
        MOI.add_constraint(single_blm,
            SVF(dual[]), GT{Float64}(info.lower))
    end
    return
end
function pass_dual_info(single_blm, dual, info::ConstraintInfo{Vector{Float64}})
    for i in eachindex(dual)
        if !isnan(info.start[i])
            MOI.set(single_blm, MOI.VariablePrimalStart(), dual[i], info.start[i])
        end
        if !isnan(info.upper[i]) &&
            !MOI.is_valid(single_blm, CI{SVF,LT{Float64}}(dual[i].value))
            MOI.add_constraint(single_blm,
                SVF(dual[i]), LT{Float64}(info.upper[i]))
        end
        if !isnan(info.lower[i]) &&
            !MOI.is_valid(single_blm, CI{SVF,GT{Float64}}(dual[i].value))
            MOI.add_constraint(single_blm,
                SVF(dual[i]), MOI.GreaterThan{Float64}(info.lower[i]))
        end
    end
    return
end

function pass_primal_info(single_blm, primal, info::VariableInfo{Float64})
    if !isnan(info.upper) &&
        !MOI.is_valid(single_blm, CI{SVF,LT{Float64}}(primal.value))
        MOI.add_constraint(single_blm,
            SVF(primal), LT{Float64}(info.upper))
    end
    if !isnan(info.lower) &&
        !MOI.is_valid(single_blm, CI{SVF,GT{Float64}}(primal.value))
        MOI.add_constraint(single_blm,
            SVF(primal), GT{Float64}(info.lower))
    end
    return
end

function JuMP.index(d::Dict)
    ret = Dict{VI,VI}()
    # sizehint!(ret, length(d))
    for (k,v) in d
        ret[JuMP.index(k)] = JuMP.index(v)
    end
    return ret
end
function index2(d::Dict)
    ret = Dict{VI,CI}()
    # sizehint!(ret, length(d))
    for (k,v) in d
        ret[JuMP.index(k)] = JuMP.index(v)
    end
    return ret
end

function JuMP.value(v::BilevelVariableRef; result::Int = 1)::Float64
    m = owner_model(v)
    solver = m.solver
    ref = solver_ref(v)
    return MOI.get(solver, MOI.VariablePrimal(result), ref)
end

function JuMP.dual(cref::BilevelConstraintRef)
    # Right now this code assumes there is no possibility for vectorized constraints
    if level(cref) == BilevelJuMP.LOWER_ONLY
        # Constraint index on the lower model
        con_lower_ref = cref.model.ctr_lower[cref.index]
        con_lower_idx = con_lower_ref.index
        # Dual variable associated with constraint index
        model_var_idxs = cref.model.lower_primal_dual_map.primal_con_dual_var[con_lower_idx]
        # Single bilevel model variable associated with the dual variable
        sblm_var_idxs = MOI.VariableIndex[]
        for vi in model_var_idxs
            push!(sblm_var_idxs, cref.model.lower_dual_to_sblm[vi])
        end
        # Solver variable associated withe the sblm model
        solver_var_idxs = MOI.VariableIndex[]
        for vi in sblm_var_idxs
            push!(solver_var_idxs, cref.model.sblm_to_solver[vi])
        end
        pre_duals = MOI.get(cref.model.solver, MOI.VariablePrimal(), solver_var_idxs)
        return JuMP.reshape_vector(
            pre_duals,
            JuMP.dual_shape(con_lower_ref.shape)
            )
    elseif level(cref) == BilevelJuMP.UPPER_ONLY
        m = cref.model
        con_upper_ref = cref.model.ctr_upper[cref.index]
        solver_ctr_idx = m.sblm_to_solver[m.upper_to_sblm[JuMP.index(con_upper_ref)]]
        pre_duals = MOI.get(cref.model.solver, MOI.ConstraintDual(), solver_ctr_idx)
        return JuMP.reshape_vector(
            pre_duals,
            JuMP.dual_shape(con_upper_ref.shape)
            )
    else
        error("Dual solutions of upper level constraints are not available. Either the solution method does nto porvide duals or or the solver failed to get one.")
    end
end

function JuMP.primal_status(model::BilevelModel)
    _check_solver(model)
    return MOI.get(model.solver, MOI.PrimalStatus())
end
function JuMP.primal_status(model::InnerBilevelModel)
    return JuMP.primal_status(model.m)
end

JuMP.dual_status(::BilevelModel) = error(
    "Dual status cant be queried for BilevelModel, but you can query for Upper and Lower models.")
function JuMP.dual_status(model::UpperModel)
    _check_solver(model.m)
    return MOI.get(model.m.solver, MOI.DualStatus())
end
function JuMP.dual_status(model::LowerModel)
    _check_solver(model.m)
    return MOI.get(model.m.solver, MOI.PrimalStatus())
end

function JuMP.termination_status(model::BilevelModel)
    _check_solver(model)
    return MOI.get(model.solver, MOI.TerminationStatus())
end
function JuMP.raw_status(model::BilevelModel)
    _check_solver(model)
    return MOI.get(model.solver, MOI.RawStatusString())
end

function JuMP.objective_value(model::BilevelModel)
    _check_solver(model)
    return MOI.get(model.solver, MOI.ObjectiveValue())
end
function JuMP.objective_value(model::UpperModel)
    return JuMP.objective_value(model.m)
end
function JuMP.objective_value(model::LowerModel)
    return lower_objective_value(model.m)
end

# function inner_ref_to_value(lm::LowerModel, var)
#     m = lm.m
#     ref = m.sblm_to_solver[m.lower_to_sblm[JuMP.index(var)]]
#     return MOI.get(m.solver, MOI.VariablePrimal(), ref)
# end
function lower_objective_value(model::BilevelModel; result::Int = 1)
    f = JuMP.objective_function(Lower(model))
    # Evaluate the lower objective expression
    return JuMP.value(f, v -> JuMP.value(v, result = result))
    # return JuMP.value(v -> inner_ref_to_value(Lower(model), v), f)
end

function build_bounds!(::BilevelModel, ::BilevelSolverMode{T}) where T
    return nothing
end

function bigM(is_dual, mode::FortunyAmatMcCarlMode)
    return ifelse(is_dual, mode.dual_big_M, mode.primal_big_M)
end
function inf_if_nan(::typeof(+), val)
    ifelse(isnan(val), Inf, val)
end
function inf_if_nan(::typeof(-), val)
    ifelse(isnan(val), -Inf, val)
end
function build_bounds!(model::BilevelModel, mode::FortunyAmatMcCarlMode{T}) where T
    # compute variable bounds for FA mode
    fa_vi_up = mode.upper
    fa_vi_lo = mode.lower
    fa_vi_ld = mode.ldual
    empty!(fa_vi_up)
    empty!(fa_vi_lo)
    empty!(fa_vi_ld)
    for (idx, _info) in model.var_info
        if haskey(model.var_lower, idx)
            var = JuMP.index(model.var_lower[idx])
            is_lower = true
        elseif haskey(model.var_upper, idx)
            var = JuMP.index(model.var_upper[idx])
            is_lower = false
        else
            continue
        end
        vref = BilevelVariableRef(model, idx)

        is_dual = vref.level == DUAL_OF_LOWER

        info = deepcopy(_info)

        lb = JuMP.has_lower_bound(vref) ? JuMP.lower_bound(vref) : -Inf
        ub = JuMP.has_upper_bound(vref) ? JuMP.upper_bound(vref) : +Inf
        info.upper = min(ub, inf_if_nan(+, info.upper))
        if info.upper == +Inf
            info.upper = +bigM(is_dual, mode)
        end
        info.lower = max(lb, inf_if_nan(-, info.lower))
        if info.lower == -Inf
            info.lower = -bigM(is_dual, mode)
        end
        if info.upper == +Inf && mode.safe
            error("Problem with UB of variable $vref")
        end
        if info.lower == -Inf && mode.safe
            error("Problem with LB of variable $vref")
        end
        if is_lower
            fa_vi_lo[var] = info
        else
            fa_vi_up[var] = info
        end
        @assert info.lower <= info.upper

    end
    for (idx, _info) in model.ctr_info
        if haskey(model.ctr_lower, idx)
            ctr = JuMP.index(model.ctr_lower[idx])
            info = deepcopy(_info)
            cref = BilevelConstraintRef(model, idx)
            # scalar
            ub = dual_upper_bound(ctr)
            lb = dual_lower_bound(ctr)
            info.upper = min(ub, inf_if_nan(+, info.upper))
            if info.upper == +Inf
                info.upper = +bigM(true, mode)
            end
            info.lower = max(lb, inf_if_nan(-, info.lower))
            if info.lower == -Inf
                info.lower = -bigM(true, mode)
            end
            if info.upper == +Inf && mode.safe
                error("Problem with UB of dual of constraint $cref")
            end
            if info.lower == -Inf && mode.safe
                error("Problem with LB of dual of constraint $cref")
            end
            @assert info.lower <= info.upper
            # TODO vector
            fa_vi_ld[ctr] = info
        end
    end
    return nothing
end

dual_lower_bound(::CI{F,LT{T}}) where {F,T} = -Inf
dual_upper_bound(::CI{F,LT{T}}) where {F,T} =  0.0

dual_lower_bound(::CI{F,GT{T}}) where {F,T} =  0.0
dual_upper_bound(::CI{F,GT{T}}) where {F,T} = +Inf

dual_lower_bound(::CI{F,ET{T}}) where {F,T} =  0.0
dual_upper_bound(::CI{F,ET{T}}) where {F,T} =  0.0

dual_lower_bound(::CI{F,S}) where {F,S} = -Inf
dual_upper_bound(::CI{F,S}) where {F,S} = +Inf

function JuMP.solve_time(bm::BilevelModel)
    return bm.solve_time
end
function build_time(bm::BilevelModel)
    return bm.build_time
end

function JuMP.set_optimizer_attribute(bm::BilevelModel, name::String, value)
    _check_solver(bm)
    return JuMP.set_optimizer_attribute(bm, MOI.RawParameter(name), value)
end
function JuMP.set_optimizer_attribute(
    bm::BilevelModel, attr::MOI.AbstractOptimizerAttribute, value
)
    _check_solver(bm)
    return MOI.set(bm.solver, attr, value)
end
function JuMP.set_optimizer_attributes(bm::BilevelModel, pairs::Pair...)
    for (name, value) in pairs
        JuMP.set_optimizer_attribute(bm.solver, name, value)
    end
end

function JuMP.get_optimizer_attribute(bm::BilevelModel, name::String)
    _check_solver(bm)
    return JuMP.get_optimizer_attribute(bm.solver, MOI.RawParameter(name))
end
function JuMP.get_optimizer_attribute(
    bm::BilevelModel, attr::MOI.AbstractOptimizerAttribute
)
    _check_solver(bm)
    return MOI.get(bm.solver, attr)
end

function JuMP.set_silent(bm::BilevelModel)
    _check_solver(bm)
    return MOI.set(bm.solver, MOI.Silent(), true)
end

function JuMP.unset_silent(bm::BilevelModel)
    _check_solver(bm)
    return MOI.set(bm.solver, MOI.Silent(), false)
end

function JuMP.set_time_limit_sec(bm::BilevelModel, limit)
    _check_solver(bm)
    return MOI.set(bm.solver, MOI.TimeLimitSec(), limit)
end

function JuMP.unset_time_limit_sec(bm::BilevelModel)
    _check_solver(bm)
    return MOI.set(bm.solver, MOI.TimeLimitSec(), nothing)
end

function JuMP.time_limit_sec(bm::BilevelModel)
    _check_solver(bm)
    return MOI.get(bm.solver, MOI.TimeLimitSec())
end

function JuMP.simplex_iterations(bm::BilevelModel)
    _check_solver(bm)
    return MOI.get(bm.solver, MOI.SimplexIterations())
end

function JuMP.barrier_iterations(bm::BilevelModel)
    _check_solver(bm)
    return MOI.get(bm.solver, MOI.BarrierIterations())
end

function JuMP.node_count(bm::BilevelModel)
    _check_solver(bm)
    return MOI.get(bm.solver, MOI.NodeCount())
end

function JuMP.normalized_rhs(cref::BilevelConstraintRef)
    return JuMP.normalized_rhs(raw_ref(cref))
end

function JuMP.result_count(bm::BilevelModel)::Int
    _check_solver(bm)
    return MOI.get(bm.solver, MOI.ResultCount())
end

function _check_solver(bm::BilevelModel)
    if bm.solver === nothing
        error("No solver attached, use `set_optimizer(model, optimizer_constructor)` or initialize with `BilevelModel(optimizer_constructor)`")
    end
end

function JuMP.set_optimizer(bm::BilevelModel, optimizer_constructor;
    bridge_constraints::Bool=true)
    # error_if_direct_mode(model, :set_optimizer)
    if bridge_constraints
        # We set `with_names=false` because the names are handled by the first
        # caching optimizer. If `default_copy_to` without names is supported,
        # no need for a second cache.
        optimizer = MOI.instantiate(optimizer_constructor, with_bridge_type=Float64, with_names=false)
        # for bridge_type in model.bridge_types
        #     _moi_add_bridge(optimizer, bridge_type)
        # end
    else
        optimizer = MOI.instantiate(optimizer_constructor)
    end

    bm.solver = optimizer
    if !MOI.is_empty(bm.solver)
        error("Calling the `optimizer_constructor` must return an empty optimizer")
    end
    return bm
end


using HorizonSideRobots

function main(r::Robot)
    num_vert = moves!(r, Sud)
    num_hor = moves!(r, West)
    print(num_vert)
    print(num_hor)

    height_box = moves!(r, Nord)
    print(height_box)

    z_down(height_box)
    vygol()
    home(num_vert,num_hor)
end

function home(num_vert,num_hor)
    moves!(r, Nord, num_vert)
    moves!(r, Ost, num_hor)
end

function z_down(height_box)
    for i in 1:height_box
        putmarker!(r)
        putmarkers!(r, Ost)
        move!(r,Sud)

        moves!(r,West)
    end
    putmarker!(r)
    putmarkers!(r, Ost)
end

function vygol()
    moves!(r, Sud)
    moves!(r, West)
end

function putmarkers!(r::Robot, side::HorizonSide)
    while isborder(r,side)==false
        move!(r,side)
        putmarker!(r)
    end
end

r=Robot(5,5, animate=true)


function make_function_dataframe(symbols::Array{Symbol,1}, function_array::Union{Array{Any,1},Array{Function,1},Array{NamedArray,1},Array{Union{NamedArray,Function},1}}, types::Array{Symbol,1}) where T<:Real
    if sum(isa.(function_array, Union{Real,Function,NamedArray})) < length(function_array) error("Function array can only contain reals, Functions or NamedArrays") end
    return DataFrame(name = symbols,
                   func = function_array,
                   type = types)
end


# native pointer interface
a = ones(Float32, 2, 3)
ccall((:make_twice_ptr, "myffi"), Cvoid, (Int32, Ptr{Float32}), 2*3, a)
println(a)

# jl_value_t* interface
b = ones(Float32, 2, 3)
ccall((:make_twice_array, "myffi"), Cvoid, (Any,), b)
println(b)


module TestConstraints

# TODO: Rewrite this

import Wordle
import Test: @test, @test_throws

constraints = Wordle.Constraints()

@test all(c -> c === 0, constraints.lower_bound.counts)
@test all(c -> c === false, constraints.global_ban.flags)
@test all(c -> c === false, constraints.is_exact_match)
@test all(c -> isa(c, Char), constraints.match)
for i in 1:5
    @test all(c -> c === false, constraints.local_ban[i].flags)
end

@test size(constraints.lower_bound.counts) === (26, )
@test size(constraints.global_ban.flags) === (26, )
@test size(constraints.is_exact_match) === (5, )
@test size(constraints.match) === (5, )
for i in 1:5
    @test size(constraints.local_ban[i].flags) === (26, )
end

end # module


module TestNAArrayOperators

using FactCheck
using MultidimensionalTables
using MultidimensionalTables.AbstractArrayWrapper

facts("NAArrayOperators tests") do
  context("AbstractArrayWrapper tests") do
    @fact 1 .+ AbstractArrayWrapper(nalift([1,2,3])) --> AbstractArrayWrapper(nalift([2,3,4]))
    @fact AbstractArrayWrapper(nalift([1,2,3])) .- 1 --> AbstractArrayWrapper(nalift([0,1,2]))
    @fact AbstractArrayWrapper(nalift([1,2,3])) .*
            AbstractArrayWrapper(nalift([5.0,6.0,7.0])) --> AbstractArrayWrapper(nalift([5.0,12.0,21.0]))
    @fact map(x->Nullable(x.value*2), AbstractArrayWrapper(nalift([1,2,3]))) --> AbstractArrayWrapper(nalift([2,4,6]))
    @fact (x=AbstractArrayWrapper([5,1,3,2,4]);sort!(x);x) --> AbstractArrayWrapper([1,2,3,4,5])
    @fact sort(AbstractArrayWrapper([5 1 3 2 4]),2) --> AbstractArrayWrapper([1 2 3 4 5])
    @fact sort(AbstractArrayWrapper([5,1,3,2,4])) --> AbstractArrayWrapper([1,2,3,4,5])
    @fact map((x,y)->MultidimensionalTables.naop_plus(x,y), nalift([1,2,3,4,5]), @nalift([1,2,NA,4,5])) --> @nalift([2,4,NA,8,10])
    @fact @nalift([1,2,NA]) .> 1 --> @nalift([false, true, NA])
    @fact 1 .< @nalift([1,2,NA]) --> @nalift([false, true, NA])
    @fact @nalift([1,2,3,NA]) .< @nalift([NA,3,1,2]) --> @nalift([NA,true,false,NA])
    @fact @nalift([1.0,2.0,NA]) .- 1 --> @nalift([0.0,1.0,NA])
    @fact 1 .- @nalift([1.0,2.0,NA]) --> @nalift([0.0,-1.0,NA])
    @fact @nalift([1.0,2.0,NA]) .* @nalift([NA,2.0,3.0]) --> @nalift([NA,4.0,NA])
    @fact -@nalift([1.0,2.0,NA]) --> @nalift([-1.0,-2.0,NA])
    @fact ~@nalift([1,2,NA]) --> @nalift([~1,~2,NA])
    @fact AbstractArrayWrapper([1,2,3]) .> 1 --> AbstractArrayWrapper([false,true,true])
    @fact AbstractArrayWrapper([1,2,3]) .> 1.5 --> AbstractArrayWrapper([false,true,true])
    @fact 1 .< AbstractArrayWrapper([1,2,3]) --> AbstractArrayWrapper([false,true,true])
    @fact AbstractArrayWrapper([1,2,3]) .> Nullable(1) --> nalift([false,true,true])
    @fact AbstractArrayWrapper([1,2,3]) + 1.0 --> AbstractArrayWrapper([2.0,3.0,4.0])
    @fact AbstractArrayWrapper([1,2,3]) + Nullable(1.0) --> nalift([2.0,3.0,4.0])
    @fact Nullable(1) .< AbstractArrayWrapper([1,2,3]) --> nalift([false,true,true])
    @fact AbstractArrayWrapper([3.0,2.0,1.0]) .< AbstractArrayWrapper([1.0,2.0,3.0]) --> AbstractArrayWrapper([false,false,true])
    @fact AbstractArrayWrapper([3,2,1]) .< nalift([1,2,3]) --> nalift([false,false,true])
    @fact @nalift([3,NA,1]) .< AbstractArrayWrapper([1.0,2.0,3.0]) --> @nalift([false,NA,true])
    @fact nalift([3,2,1]) .< nalift([1.0,2.0,3.0]) --> nalift([false,false,true])
    @fact nalift([3,2,1]) + nalift([1.0,2.0,3.0]) --> nalift([4.0,4.0,4.0])
    @fact nalift([3,2,1]) + AbstractArrayWrapper([1.0,2.0,3.0]) --> nalift([4.0,4.0,4.0])
    @fact AbstractArrayWrapper([3,2,1]) + AbstractArrayWrapper([1.0,2.0,3.0]) --> AbstractArrayWrapper([4.0,4.0,4.0])
    @fact AbstractArrayWrapper([3,2,1]) + 5.0 --> AbstractArrayWrapper([8.0,7.0,6.0])
    @fact AbstractArrayWrapper([3,2,1]) + Nullable(5.0) --> nalift([8.0,7.0,6.0])
    @fact -AbstractArrayWrapper([3,2,1]) --> AbstractArrayWrapper([-3,-2,-1])
    @fact -AbstractArrayWrapper([3.0,2.0,1.0]) --> AbstractArrayWrapper([-3.0,-2.0,-1.0])
    @fact -nalift([3.0,2.0,1.0]) --> nalift([-3.0,-2.0,-1.0])
    @fact -darr(a=[3.0,2.0,1.0]) --> darr(a=[-3.0,-2.0,-1.0])
    @fact -larr(a=[3.0,2.0,1.0], axis1=[:x,:y,:z]) --> larr(a=[-3.0,-2.0,-1.0], axis1=[:x,:y,:z])
    @fact nalift([1.0,2.0,3.0]) + nalift([1,2,3]) --> nalift([2.0,4.0,6.0])
    @fact nalift([1.0,2.0,3.0]) + nalift([1.0,2.0,3.0]) --> nalift([2.0,4.0,6.0])
    @fact AbstractArrayWrapper(map(Nullable, [1.0,2.0,3.0])) + nalift([1.0,2.0,3.0]) --> nalift([2.0,4.0,6.0])
    @fact AbstractArrayWrapper(map(Nullable, [1.0,2.0,3.0])) + nalift([1,2,3]) --> nalift([2.0,4.0,6.0])
    @fact AbstractArrayWrapper(map(Nullable, [1.0,2.0,3.0])) + AbstractArrayWrapper([1,2,3]) --> nalift([2.0,4.0,6.0])
    @fact nalift([1.0,2.0,3.0]) + 1.0 --> nalift([2.0,3.0,4.0])
    @fact 1.0 + nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0])
    @fact nalift([1.0,2.0,3.0]) + 1 --> nalift([2.0,3.0,4.0])
    @fact 1 + nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0])
    @fact 1.0 + nalift([1,2,3]) --> nalift([2.0,3.0,4.0])
    @fact nalift([1,2,3]) + 1.0 --> nalift([2.0,3.0,4.0])
    @fact nalift([1.0,2.0,3.0]) + Nullable(1.0) --> nalift([2.0,3.0,4.0])
    @fact Nullable(1.0) + nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0])
    @fact nalift([1.0,2.0,3.0]) + Nullable(1) --> nalift([2.0,3.0,4.0])
    @fact Nullable(1) + nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0])
    @fact Nullable(1.0) + nalift([1,2,3]) --> nalift([2.0,3.0,4.0])
    @fact nalift([1,2,3]) + Nullable(1.0) --> nalift([2.0,3.0,4.0])
    @fact nalift([1.0,2.0,3.0]) * 1.0 --> nalift([2.0,3.0,4.0]) - 1
    @fact 1.0 * nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0]) - 1
    @fact nalift([1.0,2.0,3.0]) * 1 --> nalift([2.0,3.0,4.0]) - 1
    @fact 1 * nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0]) - 1
    @fact 1.0 * nalift([1,2,3]) --> nalift([2.0,3.0,4.0]) - 1
    @fact nalift([1,2,3]) * 1.0 --> nalift([2.0,3.0,4.0]) - 1
    @fact nalift([1.0,2.0,3.0]) * Nullable(1.0) --> nalift([2.0,3.0,4.0]) - 1
    @fact Nullable(1.0) * nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0]) - 1
    @fact nalift([1.0,2.0,3.0]) * Nullable(1) --> nalift([2.0,3.0,4.0]) - 1
    @fact Nullable(1) * nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0]) - 1
    @fact Nullable(1.0) * nalift([1,2,3]) --> nalift([2.0,3.0,4.0]) - 1
    @fact nalift([1,2,3]) * Nullable(1.0) --> nalift([2.0,3.0,4.0]) - 1
  end
end

end


# Install scikit-learn if not installed
ENV["PYTHON"]=""
using Pkg
Pkg.build("PyCall")

import PyCall
PyCall.pyimport_conda("sklearn", "scikit-learn")

using WaveletInformation
using Test


"""
A `Solution` comprises a single solution vector and its parent
"""
type Solution
	data::Vector{Float64}
	parent # ::Branch # circular type dependency...
	Solution(d::Vector{Float64}) = new(d,nothing)
	Solution(d::Vector{Float64}, p) = new(d,p)
	Solution(s::Solution, p) = new(deepcopy(s.data), p)
end
@relay(Solution, :data)

Base.show(io::Base.IO, P::Solution) = write(io, "***SOLUTION***")


using ArgParse
include("Common.jl")

function parse(edge_feats_list, sentences, weights, fpath_out)
	info("Start parsing ...")
	f_out = open(fpath_out, "w")

	for (i, sent) in enumerate(sentences)
		n = length(sent)
		edge_feats = edge_feats_list[i]
		edge_scores = getedgescores(edge_feats, weights, n)
		heads = eisner(edge_scores, n)

		for (j, token) in enumerate(sent)
			token_array = [token.id,
							token.form,
							token.lemma,
							token.cpostag,
							token.postag,
							token.feats,
							heads[j], # predicted head
							token.deprel,
							"\n"]
			write(f_out, join(token_array, "\t") )
		end
		write(f_out, "\n")
	end
end

function getoptions(args)
	s = ArgParseSettings("Dependency parser in Julia.")
	@add_arg_table s begin
		"input"
			help = "input file"
			arg_type = String
			required = true
		"model"
			help = "model file"
			arg_type = String
			required = true
		"output"
			help = "parsed output file"
			arg_type = String
			required = true
	end
	parsed_args = parse_args(args, s)

	@assert isreadable(parsed_args["input"]) # check if training file readable
	@assert isreadable(parsed_args["model"]) # check if model file readable

	return parsed_args["input"], parsed_args["model"], parsed_args["output"]
end

function main(args)
	info("PARSING")
	const (fpath_in, fpath_model, fpath_out) = getoptions(args)
	info("Input file:\t$fpath_in")
	info("Model file:\t$fpath_model")
	info("Output file:\t$fpath_out")
	info("-------------------")

	const sentences = readconlldata(fpath_in)
	info("-------------------")

	info("Loading model ...")
	const model = open(deserialize, fpath_model)
	info("-------------------")

	const edge_feats_list = createfeatures(sentences, model.featdict)
	info("-------------------")

	parse(edge_feats_list, sentences, model.weights, fpath_out)

	info("... All DONE!")
end

main(ARGS)


using BinDeps
using Compat

vers = "2.0" # Use version number in setup.py file of pyEMD: https://raw.githubusercontent.com/garydoranjr/pyemd/master/setup.py

tagfile = "installed_vers"
target = "libemd.$(Libdl.dlext)"
srcdir = joinpath(dirname(@__FILE__()), "c_src")

if !isfile(target) || !isfile(tagfile) || readchomp(tagfile) != "$vers $(Sys.WORD_SIZE)"
    cd(srcdir) do
        println("Compiling libemd...")
        run(`make -f make.emd LIB=../$target`)
        run(`make -f make.emd clean`)
    end
    open(tagfile, "w") do f
        println(f, "$vers $(Sys.WORD_SIZE)")
    end
end


using TimeZones
import Dates

@testset "calendar" begin
    @testset "basic" begin
        @system SCalendar(Calendar, Controller)
        d = Dates.Date(2011, 10, 29)
        t0 = ZonedDateTime(d, tz"Asia/Seoul")
        o = :Calendar => :init => t0
        s = instance(SCalendar; config=o)
        @test s.init' == t0
        @test s.time' == t0
        @test s.date' == d
        @test s.step' == Dates.Hour(1)
        update!(s)
        @test s.time' == t0 + Dates.Hour(1)
        update!(s)
        @test s.time' == t0 + Dates.Hour(2)
    end

    @testset "stop" begin
        @system SCalendarStop(Calendar, Controller)
        t0 = ZonedDateTime(2011, 10, 29, tz"Asia/Seoul")
        t1 = t0 + Dates.Day(1)
        o = :Calendar => (init=t0, last=t1)
        s = instance(SCalendarStop; config=o)
        @test s.init' == t0
        @test s.last' == t1
        @test s.time' == t0
        @test s.step' == Dates.Hour(1)
        @test s.stop' == false
        for i in 1:24
            update!(s)
        end
        @test s.time' == t1
        @test s.stop' == true
    end

    @testset "count" begin
        @system SCalendarCount(Calendar, Controller)
        t0 = ZonedDateTime(2011, 10, 29, tz"Asia/Seoul")
        t1 = t0 + Dates.Day(1)
        n = 24
        o = :Calendar => (init=t0, last=t1)
        s = instance(SCalendarCount; config=o)
        @test s.count' == n
        r1 = simulate(SCalendarCount; config=o, stop=n)
        r2 = simulate(SCalendarCount; config=o, stop=:stop)
        r3 = simulate(SCalendarCount; config=o, stop=:count)
        @test r1 == r2 == r3
    end

    @testset "count nothing" begin
        @system SCalendarCountNothing(Calendar, Controller)
        t0 = ZonedDateTime(2011, 10, 29, tz"Asia/Seoul")
        o = :Calendar => (init=t0, last=nothing)
        s = instance(SCalendarCountNothing; config=o)
        @test s.count' === nothing
        @test s.stop' == false
    end

    @testset "count seconds" begin
        @system SCalendarCountSeconds(Calendar, Controller)
        t0 = ZonedDateTime(2011, 10, 29, tz"Asia/Seoul")
        t1 = t0 + Dates.Hour(1)
        n = 60*60
        o = (
            :Calendar => (init=t0, last=t1),
            :Clock => (step=1u"s",),
        )
        s = instance(SCalendarCountSeconds; config=o)
        @test s.count' == n
    end
end


#specific stuff for su3+higgs (fundamental and adjoint)


using Test
using CellListsBenchmarks

n = 100
d = 2
r = 0.1
seed = 1
iterations = 1
seconds = 1.0
Benchmark("cell_list_serial", n, d, r, seed, iterations, seconds)
Benchmark("cell_list_threads", n, d, r, seed, iterations, seconds)
Benchmark("brute_force", n, d, r, seed, iterations, seconds)
Benchmark("cell_lists", n, d, r, seed, iterations, seconds)
Benchmark("cell_lists", n, d, Float32(r), seed, iterations, seconds)
Benchmark("near_neighbors_serial", n, d, r, seed, iterations, seconds)
Benchmark("near_neighbors_threads", n, d, r, seed, iterations, seconds)
@test true


module FluxTikz

using Flux: Chain
import TikzPictures: TikzPicture
export TikzPicture

include("layer.jl")
include("network.jl")
include("tikzpicture.jl")

end


@test @inferred(ArrayInterface.size(sv5)) === (StaticInt(5),)
@test @inferred(ArrayInterface.size(v5)) === (5,)
@test @inferred(ArrayInterface.size(A)) === (3, 4, 5)
@test @inferred(ArrayInterface.size(Ap)) === (2, 5)
@test @inferred(ArrayInterface.size(A)) === size(A)
@test @inferred(ArrayInterface.size(Ap)) === size(Ap)
@test @inferred(ArrayInterface.size(R)) === (StaticInt(2),)
@test @inferred(ArrayInterface.size(Rnr)) === (StaticInt(4),)
@test @inferred(ArrayInterface.known_length(Rnr)) === 4
@test @inferred(ArrayInterface.size(A2)) === (4, 3, 5)
@test @inferred(ArrayInterface.size(A2r)) === (2, 3, 5)

@test @inferred(ArrayInterface.size(view(rand(4), reshape(1:4, 2, 2)))) == (2, 2)
@test @inferred(ArrayInterface.size(irev)) === (StaticInt(2), StaticInt(3), StaticInt(4))
@test @inferred(ArrayInterface.size(iprod)) === (StaticInt(2), StaticInt(3), StaticInt(4))
@test @inferred(ArrayInterface.size(iflat)) === (static(72),)
@test @inferred(ArrayInterface.size(igen)) === (StaticInt(2), StaticInt(3), StaticInt(4))
@test @inferred(ArrayInterface.size(iacc)) === (StaticInt(2), StaticInt(3), StaticInt(4))
@test @inferred(ArrayInterface.size(ienum)) === (StaticInt(2), StaticInt(3), StaticInt(4))
@test @inferred(ArrayInterface.size(ipairs)) === (StaticInt(2), StaticInt(3), StaticInt(4))
@test @inferred(ArrayInterface.size(izip)) === (StaticInt(2), StaticInt(3), StaticInt(4))
@test @inferred(ArrayInterface.size(zip(S, A_trailingdim))) === (StaticInt(2), StaticInt(3), StaticInt(4), static(1))
@test @inferred(ArrayInterface.size(zip(A_trailingdim, S))) === (StaticInt(2), StaticInt(3), StaticInt(4), static(1))
@test @inferred(ArrayInterface.size(S)) === (StaticInt(2), StaticInt(3), StaticInt(4))
@test @inferred(ArrayInterface.size(Sp)) === (2, 2, StaticInt(3))
@test @inferred(ArrayInterface.size(Sp2)) === (2, StaticInt(3), StaticInt(2))
@test @inferred(ArrayInterface.size(S)) == size(S)
@test @inferred(ArrayInterface.size(Sp)) == size(Sp)
@test @inferred(ArrayInterface.size(parent(Sp2))) === (static(4), static(3), static(2))
@test @inferred(ArrayInterface.size(Sp2)) == size(Sp2)
@test @inferred(ArrayInterface.size(Sp2, StaticInt(1))) === 2
@test @inferred(ArrayInterface.size(Sp2, StaticInt(2))) === StaticInt(3)
@test @inferred(ArrayInterface.size(Sp2, StaticInt(3))) === StaticInt(2)
@test @inferred(ArrayInterface.size(Wrapper(Sp2), StaticInt(3))) === StaticInt(2)
@test @inferred(ArrayInterface.size(Diagonal([1, 2]))) == size(Diagonal([1, 2]))
@test @inferred(ArrayInterface.size(Mp)) === (StaticInt(3), StaticInt(4))
@test @inferred(ArrayInterface.size(Mp2)) === (StaticInt(2), 2)
@test @inferred(ArrayInterface.size(Mp)) == size(Mp)
@test @inferred(ArrayInterface.size(Mp2)) == size(Mp2)

@test @inferred(ArrayInterface.known_size(1)) === ()
@test @inferred(ArrayInterface.known_size(view(rand(4), reshape(1:4, 2, 2)))) == (nothing, nothing)
@test @inferred(ArrayInterface.known_size(A)) === (nothing, nothing, nothing)
@test @inferred(ArrayInterface.known_size(Ap)) === (nothing, nothing)
@test @inferred(ArrayInterface.known_size(Wrapper(Ap))) === (nothing, nothing)
@test @inferred(ArrayInterface.known_size(R)) === (2,)
@test @inferred(ArrayInterface.known_size(Wrapper(R))) === (2,)
@test @inferred(ArrayInterface.known_size(Rnr)) === (4,)
@test @inferred(ArrayInterface.known_size(Rnr, static(1))) === 4
@test @inferred(ArrayInterface.known_size(Ar)) === (nothing, nothing, nothing,)
@test @inferred(ArrayInterface.known_size(Ar, static(1))) === nothing
@test @inferred(ArrayInterface.known_size(Ar, static(4))) === 1
@test @inferred(ArrayInterface.known_size(A2)) === (nothing, nothing, nothing)
@test @inferred(ArrayInterface.known_size(A2r)) === (nothing, nothing, nothing)

@test @inferred(ArrayInterface.known_size(irev)) === (2, 3, 4)
@test @inferred(ArrayInterface.known_size(igen)) === (2, 3, 4)
@test @inferred(ArrayInterface.known_size(iprod)) === (2, 3, 4)
@test @inferred(ArrayInterface.known_size(iflat)) === (72,)
@test @inferred(ArrayInterface.known_size(iacc)) === (2, 3, 4)
@test @inferred(ArrayInterface.known_size(ienum)) === (2, 3, 4)
@test @inferred(ArrayInterface.known_size(izip)) === (2, 3, 4)
@test @inferred(ArrayInterface.known_size(ipairs)) === (2, 3, 4)
@test @inferred(ArrayInterface.known_size(zip(S, A_trailingdim))) === (2, 3, 4, 1)
@test @inferred(ArrayInterface.known_size(zip(A_trailingdim, S))) === (2, 3, 4, 1)
@test @inferred(ArrayInterface.known_length(Iterators.flatten(((x, y) for x in 0:1 for y in 'a':'c')))) === nothing

@test @inferred(ArrayInterface.known_size(S)) === (2, 3, 4)
@test @inferred(ArrayInterface.known_size(Wrapper(S))) === (2, 3, 4)
@test @inferred(ArrayInterface.known_size(Sp)) === (nothing, nothing, 3)
@test @inferred(ArrayInterface.known_size(Wrapper(Sp))) === (nothing, nothing, 3)
@test @inferred(ArrayInterface.known_size(Sp2)) === (nothing, 3, 2)
@test @inferred(ArrayInterface.known_size(Sp2, StaticInt(1))) === nothing
@test @inferred(ArrayInterface.known_size(Sp2, StaticInt(2))) === 3
@test @inferred(ArrayInterface.known_size(Sp2, StaticInt(3))) === 2
@test @inferred(ArrayInterface.known_size(Mp)) === (3, 4)
@test @inferred(ArrayInterface.known_size(Mp2)) === (2, nothing)

@testset "known_length" begin
    @test ArrayInterface.known_length(1:2) === nothing
    @test ArrayInterface.known_length((1,)) == 1
    @test ArrayInterface.known_length((a=1, b=2)) == 2
    @test ArrayInterface.known_length([]) === nothing
    @test ArrayInterface.known_length(CartesianIndex((1, 2, 3))) === 3
    @test @inferred(ArrayInterface.known_length(NDIndex((1, 2, 3)))) === 3

    itr = StaticInt(1):StaticInt(10)
    @inferred(ArrayInterface.known_length((i for i in itr))) == 10
end

@testset "is_lazy_conjugate" begin
    a = rand(ComplexF64, 2)
    @test @inferred(ArrayInterface.is_lazy_conjugate(a)) == false
    b = a'
    @test @inferred(ArrayInterface.is_lazy_conjugate(b)) == true
    c = transpose(b)
    @test @inferred(ArrayInterface.is_lazy_conjugate(c)) == true
    d = c'
    @test @inferred(ArrayInterface.is_lazy_conjugate(d)) == false
    e = permutedims(d)
    @test @inferred(ArrayInterface.is_lazy_conjugate(e)) == false
    @test @inferred(ArrayInterface.is_lazy_conjugate([1, 2, 3]')) == false # We don't care about conj on `<:Real`
end


"""
    StandardDriver(csampler::AbstractContextSampler, policy::AbstractPolicy,
                   rsampler::AbstractRewardSampler[,
                   metrics::Tuple{Vararg{<:AbstractMetric}}])

A simple driver that samples contexts, passes them to the policy to generate actions,
then observes the rewards.

# Arguments
- `csampler::AbstractContextSampler`: Context sampler.
- `policy::AbstractPolicy`: Policy to generate actions given contexts.
- `rsampler::AbstractRewardSampler`: Sampler for rewards, given the contexts and actions.
- `metrics::Tuple{Vararg{<:AbstractMetric}}`: A tuple of metrics that will each be
    called as `metric(X, a, r)`.
"""
mutable struct StandardDriver{
    T1<:AbstractContextSampler,
    T2<:AbstractRewardSampler,
    T3<:AbstractPolicy,
    T4<:Tuple{Vararg{<:AbstractMetric}},
} <: AbstractDriver
    csampler::T1
    policy::T3
    rsampler::T2
    metrics::T4
end

function StandardDriver(
    csampler::AbstractContextSampler,
    policy::AbstractPolicy,
    rsampler::AbstractRewardSampler,
)
    return StandardDriver(csampler, policy, rsampler, ())
end

function (driver::StandardDriver)(batch_size::Int64)
    X = driver.csampler(batch_size)
    a = driver.policy(X)
    r = driver.rsampler(X, a)
    for met in driver.metrics
        met(X, a, r)
    end
    return X, a, r
end

mutable struct LatentDriver{
    T1<:AbstractContextSampler,
    T2<:AbstractRewardSampler,
    T3<:AbstractPolicy,
    T4<:Tuple{Vararg{<:AbstractMetric}},
    T5<:Function,
} <: AbstractDriver
    csampler::T1
    policy::T3
    rsampler::T2
    tform::T5
    metrics::T4
end

function LatentDriver(
    csampler::AbstractContextSampler,
    policy::AbstractPolicy,
    rsampler::AbstractRewardSampler,
    tform::Function,
)
    return LatentDriver(csampler, policy, rsampler, tform, ())
end

function (driver::LatentDriver)(batch_size::Int64)
    Z = driver.csampler(batch_size)
    X = mapslices(driver.tform, Z; dims=1)
    a = driver.policy(X)
    r = driver.rsampler(Z, a)
    for met in driver.metrics
        met(Z, a, r)
    end
    return X, a, r
end

"""
    run!(num_batches::Int64, batch_size::Int64, driver::AbstractDriver; <keyword arguments>)

Run a `driver` for `num_batches` batches.
"""
function run!(
    num_batches::Int64, batch_size::Int64, driver::AbstractDriver; verbose::Bool=true
)
    if num_batches <= 0 || batch_size <= 0
        throw(ArgumentError("num_batches and batch_size must be positive"))
    end
    for i in 1:num_batches
        if verbose
            print("\rStep $i/$num_batches")
        end
        X, a, r = driver(batch_size)
        update!(driver.policy, X, a, r)
    end
end


module EEGTest

# package code goes here
export AnalogData, DigitalData, Spectrogram

include("eegtypes.jl")

export ad_equals, normalize_data, threshold_01, lowpass, highpass,
down_sample, toDecibels, debounce_discrete_signal, dd_equals,
truncate_by_index, truncate_by_value

include("eegutilities.jl")

end # module


# Testing the hypothesis that evolution under a varying environment increases evolvability.

# The purpose of the function run_run_pop_evolvability() is to explore settings of popsize,
# mutrate, and ngens for the population-based evolutionary algorithm.  The function allows
# for different settings of popsize and mutrate via range parameters.  The parameter
# ngens_popsize is the product of ngens and popsize.  Instead of using ngens as a parameter,
# ngens is computed by the formula  ngens = Int(round(ngens_popsize/popsize))  which
# specifies a number of function evaluations for the run.
function run_run_pop_evolvability( nreps::Int64, p::Parameters, popsize_rng::CGP.IntRange, gl::GoalList, ngens_popsize::Int64,
      start_phmut_gen::Int64, phmut_interval::Int64, mutrate_rng::CGP.FloatRange;
    csvfile::String="" )
  sumdf = DataFrame()
  sumdf.gen = Int64[]
  sumdf.popsize = Int64[]
  sumdf.mutrate = Float64[]
  sumdf.max_fitness = Float64[]
  sumdf.evolvability = Float64[]
  sumdf.robustness= Float64[]
  run_tuples = Tuple[]
  for popsize in popsize_rng
    ngens = Int(round(ngens_popsize/popsize))
    for mutrate in mutrate_rng
      push!(run_tuples, (popsize,ngens,mutrate))
    end
  end
  results=pmap(x->run_pop_evolvability(nreps,p,x[1],gl,x[2],start_phmut_gen, phmut_interval,x[3],use_pmap=false),run_tuples)
  for res in results
    push!(sumdf,res)
  end
  if length(csvfile) > 0
    open( csvfile, "w" ) do f
      hostname = chomp(open("/etc/hostname") do f read(f,String) end)
      println(f,"# date and time: ",Dates.now())
      println(f,"# host: ",hostname," with ",nprocs()-1,"  processes: " )
      println(f,"# funcs: ", Main.CGP.default_funcs(p.numinputs))
      print_parameters(f,p,comment=true)
      println(f,"# goallist: ",gl)
      println(f,"# nreps: ",nreps)
      println(f,"# start_phmut_gen: ",start_phmut_gen)
      println(f,"# phmut_interval: ",phmut_interval)
      CSV.write( f, sumdf, append=true, writeheader=true )
    end
  end
  sumdf
end

# Averages results over reps
# use_pmap must be false when called by run_run_pop_evolvability
# If length(gl) == 0, generate goallist within each call to pop_evolvability
function run_pop_evolvability( nreps::Int64, p::Parameters, popsize::Int64, gl::GoalList, ngens::Int64,
    start_phmut_gen::Int64, phmut_interval::Int64,  mutrate::Float64=1.0; numgoals::Int64=1,
    csvfile::String="", use_pmap::Bool=false, all_gens::Bool=false )
  #@assert p.numoutputs == length(gl[1])
  println("mutrate: ",mutrate,"  gl: ",gl,"  length(gl): ",length(gl))
  avgdf = DataFrame()
  avgdf.gen = collect(1:ngens)
  avgdf.popsize = fill(popsize,ngens)
  avgdf.mutrate = fill(mutrate,ngens)
  avgdf.max_fitness = zeros(Float64,ngens)
  avgdf.evolvability = zeros(Float64,ngens)
  avgdf.robustness = zeros(Float64,ngens)
  if use_pmap
    dflist = pmap(x->pop_evolvability( p, popsize, gl, ngens, start_phmut_gen, phmut_interval, mutrate,numgoals=numgoals), collect(1:nreps))
  else
    dflist = map(x->pop_evolvability( p, popsize, gl, ngens, start_phmut_gen, phmut_interval, mutrate,numgoals=numgoals), collect(1:nreps))
  end
  for df in dflist
    avgdf.max_fitness += df.max_fitness
    avgdf.evolvability += df.evolvability
    avgdf.robustness += df.robustness
  end
  avgdf.max_fitness /= nreps
  avgdf.evolvability /= nreps
  avgdf.robustness /= nreps
  if length(csvfile) > 0
    open( csvfile, "w" ) do f
      hostname = chomp(open("/etc/hostname") do f read(f,String) end)
      println(f,"# date and time: ",Dates.now())
      println(f,"# host: ",hostname," with ",nprocs()-1,"  processes: " )
      println(f,"# funcs: ", Main.CGP.default_funcs(p.numinputs))
      print_parameters(f,p,comment=true)
      println(f,"# goallist: ",gl)
      println(f,"# nreps: ",nreps)
      CSV.write( f, avgdf, append=true, writeheader=true )
    end
  end
  if all_gens
    avgdf
  else
    avgdf[end,:]
  end
end

# If length(gl)==0, generate goallist within each call to pop_evolvability
function pop_evolvability( p::Parameters, popsize::Int64, gl::GoalList, ngens::Int64,
    start_phmut_gen::Int64, phmut_interval::Int64,  mutrate::Float64=1.0;
    csvfile::String="", numgoals::Int64=1 )
  if length(gl) > 0
    glmut = deepcopy(gl)
  else
    glmut = randgoallist( numgoals, p )
  end
  #println("glmut: ",glmut)
  df = DataFrame()
  df.gen = Int64[]
  df.max_fitness = Float64[]
  df.evolvability = Float64[]
  df.robustness = Float64[]
  funcs = default_funcs( p.numinputs )
  pop = [ random_chromosome(p,funcs) for i = 1: popsize ]
  count_mutations = 0
  fitness_vector = zeros(Float64,popsize)
  maxfit = 0.0
  for i in 1:popsize
    c = pop[i]
    c.fitness = fitness_funct( p, c, glmut )
    fitness_vector[i] = pop[i].fitness
    maxfit = fitness_vector[i] > maxfit ? fitness_vector[i] : maxfit
  end
  ( evolvability, robustness ) = pop_evolvability_robustness( pop )
  push!( df.gen, 1 )
  push!( df.max_fitness, maxfit )
  push!( df.evolvability, evolvability )
  push!( df.robustness, robustness )
  for gen = 2:ngens
    if gen >= start_phmut_gen && gen % phmut_interval == 0
      phmut!( glmut, p )
      println("phmut gen: ",gen,"  glmut: ",glmut)
    end
    for i in 1:popsize
      c = pop[i]
      if rand() <= mutrate
        pop[i] = c = mutate_chromosome!(deepcopy(c),funcs)[1]
        c.fitness = fitness_funct( p, c, glmut )
        #println("mutation count: ",count_mutations,"  phenotype: ",@sprintf("0x%x",(output_values(c)[1])),"  fitness: ",c.fitness)
        count_mutations += 1
      end
    end
    fitness_vector = ([ pop[i].fitness for i = 1:popsize ])
    maxfit = findmax(fitness_vector)[1]
    propsel!( pop, fitness_vector, maxfit=maxfit )
    ( evolvability, robustness ) = pop_evolvability_robustness( pop )
    push!( df.gen, gen )
    push!( df.max_fitness, maxfit )
    push!( df.evolvability, evolvability )
    push!( df.robustness, robustness )
  end
  println("glmut: ",glmut,"  mutrate: ",mutrate,"  max_fit: ",maxfit)
  df
end

function phmut!( glmut::GoalList, p::Parameters )
  gl_component = length(glmut) > 1 ? rand(1:length(glmut)) : 1
  goal_index = length(glmut[1]) > 1 ? rand(1:length(glmut[1])) : 1
  max_shift = 2^p.numinputs - 1
  shift = rand(0:max_shift)
  println("gl_component: ",gl_component,"  goal_index: ",goal_index,"  shift: ",shift)
  mut_mask = MyInt(1) << shift
  #@printf("mut_mask: 0x%x\n",mut_mask)
  # A more elegant version of the next line is:
  # glmut[gl_component][goal_index] ⊻= mut_mask
  glmut[gl_component][goal_index] = xor(glmut[gl_component][goal_index], mut_mask)
  return glmut
end

function fitness_funct( p::Parameters, c::Chromosome, gl::GoalList )
  maximum( 1.0-hamming_distance( g, output_values(c), p.numinputs ) for g in gl )
end


function MCTS.estimate_value(est::BasicPOMCP.SolvedFORollout,
                             bmdp::GenerativeBeliefMDP,
                             belief,
                             d::Int64)
    sim = RolloutSimulator(est.rng, Nullable{Any}(), Nullable{Float64}(), Nullable(d))
    return simulate(sim, bmdp.pomdp, est.policy, rand(est.rng, belief))
end

function MCTS.estimate_value(est::BasicPOMCP.SolvedFOValue{P},
                             bmdp::MeanRewardBeliefMDP,
                             b::AbstractParticleBelief,
                             d::Int64) where P <: ValueIterationPolicy
    r = [estimate_value(est, bmdp.pomdp, s, d) for s in particles(b)]
    mean(r, weights(weights(b)))
end

function MCTS.estimate_value(estimator::BasicPOMCP.SolvedFOValue{P}, pomdp::POMDP, start_state, steps::Int) where P <: ValueIterationPolicy
    POMDPs.value(estimator.policy, start_state)
end

function MCTS.estimate_value(est::BasicPOMCP.SolvedFOValue{P},
                             bmdp::MeanRewardBeliefMDP,
                             b::AbstractParticleBelief,
                             d::Int64) where P <: AlphaVectorPolicy
    estimate_value(est, bmdp.pomdp, b, d)
end

function MCTS.estimate_value(estimator::BasicPOMCP.SolvedFOValue{P}, pomdp::POMDP, b, steps::Int) where P <: AlphaVectorPolicy
    POMDPs.value(estimator.policy, b)
end

Random.seed!(planner::Policy, x) = planner


# update integrator when u is modified by callbacks
function handle_callback_modifiers!(integrator::DDEIntegrator)
    integrator.reeval_fsal = true # recalculate fsalfirst after applying step

    # update heap of discontinuities
    # discontinuity is assumed to be of order 0, i.e. solution x is discontinuous
    push!(integrator.opts.d_discontinuities, Discontinuity(integrator.t, 0))
end

"""
    reeval_internals_due_to_modification!(integrator::DDEIntegrator)

Recalculate interpolation data and update ODE integrator after changes by callbacks.
"""
function DiffEqBase.reeval_internals_due_to_modification!(integrator::DDEIntegrator,
            x::Type{Val{not_initialization}} = Val{true}) where not_initialization

    # update interpolation data of DDE integrator using old interpolation data
    # of ODE integrator in evaluation of history function that was calculated in
    # `perform_step!`
    if not_initialization
        addsteps!(integrator, integrator.f, true, true, true)
        # copy interpolation data to ODE integrator
        recursivecopy!(integrator.integrator.k, integrator.k)
    end

    # move ODE integrator to new time interval of DDE integrator
    integrator.integrator.t = integrator.t
    integrator.integrator.dt = integrator.dt
    if isinplace(integrator.sol.prob)
        recursivecopy!(integrator.integrator.u, integrator.u)
    else
        integrator.integrator.u = integrator.u
    end

    integrator.u_modified = false
end

# track propagated discontinuities for dependent delays

struct DiscontinuityCallback{F,D<:Discontinuity,A,R,I} <: AbstractContinuousCallback
    lags::F
    discontinuities::Vector{D}
    interp_points::Int
    abstol::A
    reltol::R
    initialize::I
    idxs::Nothing
end

"""
    DiscontinuityCallback(lags, discontinuities::Vector{<:Discontinuity};
                          [interp_points::Int=10, abstol=1e-12, reltol=0])

Callback that tracks `discontinuities` that are propagated by dependent `lags` of the form
`(u,p,t) -> lag(u,p,t)`.

Hereby a number `interp_points` of interpolation points are used to first check for
different signs of functions ``f(t) = T + lag(u(t),p,t) - t``, where ``T`` is time point of a
previous discontinuity and ``t`` is contained in the current time interval. This shows that
the current time interval contains propagated discontinuities of which the exact time point
is then determined by a root finding algorithm. The sign at the lower bound of the time
interval, i.e. at `tprev`, is set to 0 with absolute tolerance `abstol` and relative
tolerance `reltol`.
"""
function DiscontinuityCallback(lags, discontinuities::Vector{<:Discontinuity};
                               interp_points::Int=10, abstol=1e-12, reltol=0)
    DiscontinuityCallback(lags, discontinuities, interp_points, abstol,
                          reltol, initialize!, nothing)
end

# do not initialize discontinuity callback
initialize!(c::DiscontinuityCallback, u, t, integrator::DEIntegrator) = (integrator.u_modified=false)

# find time of first discontinuity in the current time interval (if existent)
function DiffEqBase.find_callback_time(integrator::DDEIntegrator, callback::DiscontinuityCallback, counter)
    # initialize time and order of first discontinuity in the current time interval
    tmin = zero(integrator.t)
    order = 0
    contains_discontinuity = false

    # update interpolation data and calculate interpolation points
    if callback.interp_points != 0
      addsteps!(integrator)
    end
    Θs = range(0, stop=one(integrator.t), length=callback.interp_points)

    for lag in callback.lags
        # define function f that calculates T + lag(u(t),p,t) - t, where t = tprev + Θ*dt is
        # a time point in the time interval of the current step and T ≤ tprev is the time
        # point of a discontinuity
        # hence roots t of this function for fixed T are propagated discontinuities
        function f(Θ, T)
            if typeof(integrator.cache) <: OrdinaryDiffEq.OrdinaryDiffEqMutableCache
                # use temporary array of mutable caches in calculation of interpolants
                tmp = integrator.cache.tmp
                OrdinaryDiffEq.ode_interpolant!(tmp, Θ, integrator, nothing, Val{0})
            else
                tmp = OrdinaryDiffEq.ode_interpolant(Θ, integrator, nothing, Val{0})
            end
            t = integrator.tprev + Θ*integrator.dt
            T + lag(tmp,integrator.p,t) - t
        end

        for d in callback.discontinuities
            # fix time of discontinuity
            T = d.t
            g(Θ) = f(Θ, T)

            # use start and end point of last time interval to check for discontinuities
            previous_condition = T + lag(integrator.uprev, integrator.p, integrator.tprev) -
                integrator.tprev
            if isapprox(previous_condition, 0, rtol=callback.reltol, atol=callback.abstol)
                prev_sign = 0
            else
                prev_sign = cmp(previous_condition, 0)
            end
            next_sign = cmp(T + lag(integrator.u, integrator.p, integrator.t) - integrator.t, 0)

            # find time of discontinuity if one exists
            t,contains_discontinuity = find_discontinuity_time(integrator, callback, prev_sign, next_sign, Θs, g)

            # update time and order of first discontinuity in the current time interval
            if contains_discontinuity && (integrator.tdir * t < integrator.tdir * tmin || iszero(tmin))
                tmin = t
                order = d.order + 1
            end
        end
    end

    tmin, order, contains_discontinuity
end

"""
    find_discontinuity_time(integrator::DDEIntegrator, callback::DiscontinuityCallback,
                            prev_sign::Int, next_sign::Int, Θs, f)

Find time of propagated discontinuity for a certain dependent delay and previous
discontinuity, which is root of the function `f`, in the current time interval of
`integrator` with interpolation points `Θs`. Hereby `f` shows signs `prev_sign` and
`next_sign` at both ends of the time interval.
"""
function find_discontinuity_time(integrator::DDEIntegrator, callback::DiscontinuityCallback,
                                 prev_sign::Int, next_sign::Int, Θs, f)
    if prev_sign * next_sign < 0
        # if signs are different we already know that a discontinuity exists
        contains_discontinuity = true
        interp_index = callback.interp_points
        prev_sign_index = 1
    elseif callback.interp_points != 0
        # recheck interpolation intervals if no discontinuity found yet
        contains_discontinuity, interp_index, prev_sign_index =
            determine_discontinuity_existence(prev_sign, next_sign, Θs, f)
    end

    if contains_discontinuity
        if interp_index != 0
            top_Θ = Θs[interp_index] # Top at the smallest
            bottom_θ = Θs[prev_sign_index]
        else
            # without interpolation points
            top_Θ = one(integrator.t)
            bottom_θ = typeof(integrator.t)(0)
        end

        if f(top_Θ) == 0
          Θ = top_Θ
        else
          Θ = prevfloat(find_zero(f,(bottom_θ,top_Θ),Roots.AlefeldPotraShi(),atol = callback.abstol/100))
        end
        #Θ = prevfloat(...)
        # prevfloat guerentees that the new time is either 1 floating point
        # numbers just before the event or directly at zero, but not after.
        # If there's a barrier which is never supposed to be crossed,
        # then this will ensure that
        # The item never leaves the domain. Otherwise Roots.jl can return
        # a float which is slightly after, making it out of the domain, causing
        # havoc.
        new_t = integrator.dt*Θ
    else
        # no discontinuity found for given combination of lag and previous discontinuity
        new_t = zero(integrator.t)
    end

    new_t,contains_discontinuity
end

"""
    determine_discontinuity_existence(prev_sign::Int, next_sign::Int, Θs, f)

Determine whether function `f` has a root in the interval [0, 1] by checking signs of `f`
at 0 and 1 (`prev_sign` and `next_sign`, respectively) and at interpolation points in `Θs`.

This corresponds to the existence of a propagated discontinuity.
"""
function determine_discontinuity_existence(prev_sign::Int, next_sign::Int, Θs, f)
    contains_discontinuity = false
    interp_index = 0
    prev_sign_index = 1

    # use interpolation intervals to check for discontinuities
    for i in 2:length(Θs)-1
        new_sign = cmp(f(Θs[i]), 0)

        if prev_sign == 0
            prev_sign = new_sign
            prev_sign_index = i
        elseif prev_sign * new_sign < 0
            contains_discontinuity = true
            interp_index = i
            break
        end
    end

    contains_discontinuity, interp_index, prev_sign_index
end

"""
    apply_callback!(integrator::DDEIntegrator, callback::DiscontinuityCallback, cb_time,
                    order)

Handle discontinuity of order `order` at time `integrator.tprev + cb_time` in the current
time interval of `integrator` that was found by `callback`. Cause the current step to fail,
and add the found discontinuity to both the heap of discontinuities and of time stops.
"""
function DiffEqBase.apply_callback!(integrator::DDEIntegrator, callback::DiscontinuityCallback,
                         cb_time, order)
    # do not accept current step
    integrator.t = integrator.tprev
    integrator.force_stepfail = true
    integrator.accept_step = false

    # add new discontinuity of correct order at time of callback
    d = Discontinuity(integrator.t + cb_time, order)
    push!(integrator.opts.d_discontinuities, d)
    push!(integrator.opts.tstops, d.t)

    # u is not modified and step not saved
    # nevertheless callback set `save_cb` to `true` to avoid undesired saving in
    # `handle_callbacks!`
    false, true
end


# # Introduction

# A very daunting thing to program is a feedback loop with parallelism. Parallelism introduces stochasticity and thus debugging a feedback loop, or the part which learns in such a system is painful. On the other hand, we have so many different kinds of systems which implement parallelism - processes, threads, GPUs, job schedulers, GRID, etc. And so one ends up writing non-reusable code a case by a case.

# A TaskMater is an abstraction for all of those issues through two critical concepts - Master and Learner. Master is a process which takes input values from a channel `Master.tasks` and evaluates in an arbitrary fashion/order and puts that in `Master.results` channel as a tuple (input, output). One makes a concrete implementation which uses processes, threads, GPUs, TPUs, job scheduler, etc. for evaluation. Or one can treat Master as some process which comes from a piece of experimental equipment, for example, from a multi-head scanning tunnelling microscope. Or one could try to find optimal parameters for plant growth where parallelism is very natural.

# The other concept is Learner which tries to learn from the Master by asking questions and receiving answers. Nature is that Master has received multiple questions, and he answers them in arbitrary order. Thus the Learner needs to be smart to optimize the objective. Again the Learner could be a computer program, animal (if you can teach them parallelism), or human pressing buttons.

# Particularly in the case of a computer program, there is quite a variety. There is a class of learners where the programmer had programmed all cases of how the system should behave. My Python colleagues make a very great example in the [adaptive package](https://github.com/python-adaptive/adaptive), which allows adaptive function evaluation for reducing computational needs to make a beautiful figure (see Adaptive.jl for a wrapper). Another type of learners had been brainstormed in [this reedit](https://www.reddit.com/r/dataisbeautiful/comments/b8vv2p/i_wrote_a_python_package_to_do_adaptive_sampling/). The other class which might gain traction is a machine-learned Learner, for example, a plant state recognition algorithm with some ML to optimize the growth.

# # Interface

# For what follows we need to load and execute:
using Distributed
addprocs(2)
# before loading TaskMaster. That would give us two workers to proceed.

# To load the package, install it from the Julia central registry and execute:

using TaskMaster

# further on, we assume that it is loaded.

# ## Learner

# To see how to implement Learner, let's consider IgnorantLearner from the package. To initiate it, we do:
learner = IgnorantLearner(1:4)

# The first part of the interface is asking part. To `ask!` a point we execute:

x1 = ask!(learner,4)
x2 = ask!(learner,2)

# x1 and x2 now would give us points which Learner thinks as most beneficial to reach the objective. The numbers 4 and 2 represent the input, for example, a random number in that purifying Learner from randomness. That is particularly useful when one wants to debug the Learner from output values alone (see debugging section).

# The second part of the interface is telling about masters evaluated points to Learner. Let's say that x2=3 and thus y2=9 represent the results of the evaluation. Then to tell Learner about them we do:

tell!(learner,(3,9))

# Which would affect the state of the Learner, giving us better predictions on where exciting things would happen even when x1 is still being evaluated. (That is the reason why ask is written with exclamation mark ask!). Ignorant Learner as the name implies would ignore this knowledge and would proceed with evaluating points given by iterator.

# ## Master

# To see how Master works, let's consider WorkMaster from the package. To initiate it, we need to define a function on all workers and then start Master:

@everywhere f(x) = x^2
master = WorkMaster(f)

# Now we can evaluate the function with specific values as simple as:

put!(master.tasks,3)
put!(master.tasks,5)

# And take out the results:

take!(master.results)

# As one can see, it is pretty apparent to make ThreadMaster and other different kinds of Master implementations.

# ## Loop

# The third and the final concept is the Loop which represents the execution of master on the Loop. That can be initiated as follows:

master = WorkMaster(f)
learner = IgnorantLearner(1:10)
loop = Loop(master,learner)

# Also if one wishes to follow the learning process, it is possible to pass a function `iterhook(::Loop)` is executed in every iteration with constructor `Loop(::Master,::Learner,iterhook(::Loop))`.

# Now the central part is the execution. If one knows the collection beforehand on what one wants to execute the Loop (so it is finite) one can do:

output1 = evaluate!(loop,1:4)

# Often, however, one wants to learn until some convergence criteria are being met. That one can also do by passing a stopping condition which is executed every time before a new point is asked:

output2 = evaluate!(loop,learner->learner.state==7,5:9)

# which will terminate when the Learner's state would be 7. A note is that evaluate would continue to execute the previous state of the Loop (thus exclamation mark !).

# ## Debugging Learner

# Let's imagine a situation where one had spent hours evaluating the function with a Learner. For some particular reason looking at the output, the Learner seems had misbehaved. The question then is how one could debug that?

# A way the package overcomes such pitfall is by ensuring a deterministic process of `evaluate!` function which communicates with Master. This is why it is crucial that `ask!` does not have stochasticity inside but if needed, takes that from the input. That allows us to replay the history and explore the Learner's state as it evolved.

# In TaskMaster that is implemented with `HisotyMaster` type. To see how to use it, let's apply it on the previous execution:
output = [output1...,output2...]
master = HistoryMaster(output,2)
# where 2 is the number of unresolved points which were allowed during the original Masters run. Now to repeat the history, we do:
learner = IgnorantLearner(1:10)
loop = Loop(master,learner,loop->@show loop.learner.state)
evaluate!(loop,learner->learner.state==7,1:9)
# As you can see the `iterhook(::Loop)` makes a lot of sense for debugging. Also, HistoryMaster could be useful to write tests for Learner, so when code changes, one would immediately see the effects of that.