"""
    DecisionVariable <: AbstractVariableRef

Identifier for a decision in a stochastic program. Holds a reference to the stochastic program, the stage the decision is taken in, and its corresponding MOI.VariableIndex.
"""
struct DecisionVariable <: JuMP.AbstractVariableRef
    stochasticprogram::StochasticProgram
    stage::Int
    index::MOI.VariableIndex
end

# Getters #
# ========================== #
"""
    decision(dvar::DecisionVariable)

Return the internal `Decision` associated with the first-stage `dvar`.
"""
function decision(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `decision(dvar, scenario_index)`.")
    return decision(structure(owner_model(dvar)), index(dvar), stage(dvar))
end
"""
    decision(dvar::DecisionVariable, scenario_index::Integer)

Return the scenario-dependent internal `Decision` associated with `dvar` at `scenario_index`.
"""
function decision(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `decision(dvar)`.")
    return decision(structure(owner_model(dvar)), index(dvar), stage(dvar), scenario_index)
end
"""
    stage(dvar::DecisionVariable)

Return the stage of `dvar`.
"""
function stage(dvar::DecisionVariable)
    return dvar.stage
end
"""
    state(dvar::DecisionVariable)

Return the `DecisionState` of the first-stage `dvar`.
"""
function state(dvar::DecisionVariable)
    return decision(dvar).state
end
"""
    state(dvar::DecisionVariable, scenario_index::Integer)

Return the scenario-dependent `DecisionState` of `dvar` at `scenario_index`.
"""
function state(dvar::DecisionVariable, scenario_index::Integer)
    return decision(dvar,scenario_index).state
end

# Setters #
# ========================== #
function take_decisions!(stochasticprogram::StochasticProgram, dvars::Vector{DecisionVariable}, vals::AbstractVector)
    if isempty(dvars)
        @warn "No decision variables specified. Nothing to do."
        return nothing
    end
    # Check that all given decisions are in model
    map(dvar -> check_belongs_to_model(dvar, stochasticprogram), dvars)
    # Check that all decisions belong to the same stage
    all(dvar -> stage(dvar) == stage(dvars[1]), dvars) || error("All decisions should be taken in the same stage.")
    # Check that decisions are first-stage
    stage(dvars[1]) == 1 || error("Decisions are scenario-dependent, consider `take_decisions!(sp, dvars, vals, scenario_index)`")
    # Check decision length
    length(dvars) == length(vals) || error("Given decision of length $(length(vals)) not compatible with number of decision variables $(length(dvars)).")
    # Update decisions
    for (dvar, val) in zip(dvars, vals)
        fix(dvar, val)
    end
    return nothing
end
function take_decisions!(stochasticprogram::StochasticProgram, dvars::Vector{DecisionVariable}, vals::AbstractVector, scenario_index::Integer)
    if isempty(dvars)
        @warn "No decision variables specified. Nothing to do."
        return nothing
    end
    # Check that all given decisions are in model
    map(dvar -> check_belongs_to_model(dvar, stochasticprogram), dvars)
    # Check that all decisions belong to the same stage
    all(dvar -> stage(dvar) == stage(dvars[1]), dvars) || error("All decisions should be taken in the same stage.")
    # Check that decisions are not first-stage
    stage(dvars[1]) > 1 || error("Decisions are not scenario-dependent, consider `take_decisions!(sp, dvars, vals)`")
    # Check decision length
    length(dvars) == length(vals) || error("Given decision of length $(length(vals)) not compatible with number of decision variables $(length(dvars)).")
    # Update decisions
    for (dvar, val) in zip(dvars, vals)
        fix(dvar, scenario_index, val)
    end
    return nothing
end

function untake_decisions!(stochasticprogram::StochasticProgram, dvars::Vector{DecisionVariable})
    if isempty(dvars)
        @warn "No decision variables specified. Nothing to do."
        return nothing
    end
    # Check that all given decisions are in model
    map(dvar -> check_belongs_to_model(dvar, model), dvars)
    # Check that all decisions belong to the same stage
    all(dvar -> stage(dvar) == stage(dvars[1]), dvars) || error("All decisions should be taken in the same stage.")
    # Check that decisions are first-stage
    stage(dvars[1]) == 1 || error("Decisions are scenario-dependent, consider `unttake_decisions!(sp, dvars, scenario_index)`")
    # Update decisions
    need_update = false
    for dvar in dvars
        unfix(dvar)
    end
    return nothing
end
function untake_decisions!(stochasticprogram::StochasticProgram, dvars::Vector{DecisionVariable}, scenario_index::Integer)
    if isempty(dvars)
        @warn "No decision variables specified. Nothing to do."
        return nothing
    end
    # Check that all given decisions are in model
    map(dvar -> check_belongs_to_model(dvar, model), dvars)
    # Check that all decisions belong to the same stage
    all(dvar -> stage(dvar) == stage(dvars[1]), dvars) || error("All decisions should be taken in the same stage.")
    # Check that decisions are first-stage
    stage(dvars[1]) > 1 || error("Decisions are not scenario-dependent, consider `take_decisions!(sp, dvars, vals)`")
    # Update decisions
    for dvar in dvars
        unfix(dvar, scenario_index)
    end
    return nothing
end

# MOI #
# ========================== #
function MOI.get(stochasticprogram::StochasticProgram, attr::MOI.AbstractVariableAttribute,
                 dvar::DecisionVariable)
    check_belongs_to_model(dvar, stochasticprogram)
    if MOI.is_set_by_optimize(attr)
        # Check if there is a cached solution
        cache = solutioncache(stochasticprogram)
        if haskey(cache, :solution)
            # Returned cached solution if possible
            try
                return MOI.get(cache[:solution], attr, index(dvar))
            catch
            end
        end
        if haskey(cache, :node_solution_1)
            # Value was possibly only cached in first-stage solution
            try
                return MOI.get(cache[:node_solution_1], attr, index(dvar))
            catch
            end
        end
        check_provided_optimizer(stochasticprogram.optimizer)
        if MOI.get(stochasticprogram, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
            throw(OptimizeNotCalled())
        end
        return MOI.get(optimizer(stochasticprogram), attr, index(dvar))
    end
    return MOI.get(backend(proxy(stochasticprogram, stage(dvar))), attr, index(dvar))
end
function MOI.get(stochasticprogram::StochasticProgram, attr::ScenarioDependentVariableAttribute,
                 dvar::DecisionVariable)
    check_belongs_to_model(dvar, stochasticprogram)
    if MOI.is_set_by_optimize(attr)
        # Check if there is a cached solution
        cache = solutioncache(stochasticprogram)
        key = Symbol(:node_solution_, attr.stage, :_, attr.scenario_index)
        if haskey(cache, key)
            # Returned cached solution if possible
            try
                return MOI.get(cache[key], attr.attr, index(dvar))
            catch
            end
        end
        check_provided_optimizer(stochasticprogram.optimizer)
        if MOI.get(stochasticprogram, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
            throw(OptimizeNotCalled())
        end
        try
            # Try to get scenario-dependent value directly
            return MOI.get(optimizer(stochasticprogram), attr, index(dvar))
        catch
            # Fallback to resolving scenario-dependence in structure if
            # not supported natively by optimizer
            MOI.get(structure(stochasticprogram), attr, index(dvar))
        end
    end
    # Get value from structure if not set by optimizer
    return MOI.get(structure(stochasticprogram), attr, index(dvar))
end

function MOI.set(stochasticprogram::StochasticProgram, attr::MOI.AbstractVariableAttribute,
                 dvar::DecisionVariable, value)
    check_belongs_to_model(dvar, stochasticprogram)
    MOI.set(structure(stochasticprogram), attr, index(dvar), value)
    return nothing
end

# JuMP variable interface #
# ========================== #
"""
    name(dvar::DecisionVariable, scenario_index::Integer)::String

Get the name of the decision variable `dvar`.
"""
function JuMP.name(dvar::DecisionVariable)
    return MOI.get(owner_model(dvar), MOI.VariableName(), dvar)::String
end
"""
    name(dvar::DecisionVariable, scenario_index::Integer)::String

Get the name of the scenario-dependent decision variable `dvar` in scenario `scenario_index`.
"""
function JuMP.name(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `name(dvar)`.")
    attr = ScenarioDependentVariableAttribute(stage(dvar), scenario_index, MOI.VariableName())
    return MOI.get(owner_model(dvar), attr, dvar)::String::String
end
"""
    set_name(dvar::DecisionVariable, scenario_index::Integer, name::String)

Set the name of the decision variable `dvar` to `name`.
"""
function JuMP.set_name(dvar::DecisionVariable, name::String)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `set_name(dvar, scenario_index, name)`.")
    return MOI.set(owner_model(dvar), MOI.VariableName(), dvar, name)
end
"""
    set_name(dvar::DecisionVariable, scenario_index::Integer, name::String)

Set the name of the scenario-dependent decision variable `dvar` in scenario `scenario_index` to `name`.
"""
function JuMP.set_name(dvar::DecisionVariable, scenario_index::Integer, name::String)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `set_name(dvar, name)`.")
    attr = ScenarioDependentVariableAttribute(stage(dvar), scenario_index, MOI.VariableName())
    return MOI.set(owner_model(dvar), attr, dvar, name)
end
"""
    decision_by_name(stochasticprogram::Stochasticprogram,
                     stage::Integer,
                     name::String)::Union{AbstractVariableRef, Nothing}

Returns the reference of the variable with name attribute `name` at `stage` of `stochasticprogram` or `Nothing` if
no variable has this name attribute. Throws an error if several variables have
`name` as their name attribute at stage `s`.
"""
function decision_by_name(stochasticprogram::StochasticProgram{N}, stage::Integer, name::String) where N
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    index = MOI.get(backend(proxy(stochasticprogram, stage)), MOI.VariableIndex, name)
    if index isa Nothing
        return nothing
    else
        return DecisionVariable(stochasticprogram, stage, index)
    end
end
"""
    index(dvar::DecisionVariable)::MOI.VariableIndex

Return the index of the decision variable that corresponds to `dvar` in the MOI backend.
"""
JuMP.index(dvar::DecisionVariable) = dvar.index
"""
    optimizer_index(dvar::DecisionVariable)::MOI.VariableIndex

Return the index of the variable that corresponds to `dvar` in the optimizer model.
"""
function JuMP.optimizer_index(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `optimizer_index(dvar, scenario_index)`.")
    return JuMP._moi_optimizer_index(structure(owner_model(dvar)), index(dvar))
end
"""
    optimizer_index(dvar::DecisionVariable, scenario_index)::MOI.VariableIndex

Return the index of the variable that corresponds to the scenario-dependent `dvar` in the optimizer model at `scenario_index`.
"""
function JuMP.optimizer_index(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `optimizer_index(dvar)`.")
    return JuMP._moi_optimizer_index(structure(owner_model(dvar)), index(dvar), scenario_index)
end
"""
    has_duals(stochasticprogram::StochasticProgram; result::Int = 1)

Return `true` if the solver has a primal solution in the first-stage of
`stochasticprogram` in result index `result` available to query,
otherwise return `false`.
"""
function JuMP.has_values(stochasticprogram::StochasticProgram; result::Int = 1)
    return primal_status(stochasticprogram; result = result) != MOI.NO_SOLUTION
end
"""
    has_values(stochasticprogram::StochasticProgram, stage::Integer, scenario_index::Integer; result::Int = 1)

Return `true` if the solver has a primal solution in the node at stage
`stage` and scenario `scenario_index` in result index `result`
available to query, otherwise return `false`.
"""
function JuMP.has_values(stochasticprogram::StochasticProgram, stage::Integer, scenario_index::Integer; result::Int = 1)
    return primal_status(stochasticprogram, stage, scenario_index; result = result) != MOI.NO_SOLUTION
end
"""
    has_values(stochasticprogram::TwoStageStochasticProgram, scenario_index::Integer; result::Int = 1)

Return `true` if the solver has a primal solution in scenario `scenario_index`
in result index `result` available to query, otherwise return `false`.
"""
function has_values(stochasticprogram::TwoStageStochasticProgram, scenario_index::Integer; result::Int = 1)
    return has_values(stochasticprogram, 2, scenario_index; result = result)
end
"""
    value(dvar::DecisionVariable; result = 1)

Return the value of the first-stage decision variable `dvar`
associated with result index `result` of the most-recent
returned by the solver.
"""
function JuMP.value(dvar::DecisionVariable; result::Int = 1)::Float64
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `value(dvar, scenario_index)`.")
    d = decision(dvar)
    if d.state == Taken
        # If decision has been fixed the value can be fetched
        # directly
        return d.value
    end
    return MOI.get(owner_model(dvar), MOI.VariablePrimal(result), dvar)
end
"""
    value(dvar::DecisionVariable, scenario_index::Integer; result = 1)

Return the scenario-dependent value of the decision variable `dvar`
associated with result index `result` at `scenario_index` of the
most-recent returned by the solver.
"""
function JuMP.value(dvar::DecisionVariable, scenario_index::Integer; result::Int = 1)::Float64
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `is_fixed(dvar)`.")
    d = decision(dvar, scenario_index)
    if d.state == Taken
        # If decision has been fixed the value can be fetched
        # directly
        return d.value
    end
    attr = ScenarioDependentVariableAttribute(stage(dvar), scenario_index, MOI.VariablePrimal(result))
    return MOI.get(owner_model(dvar), attr, dvar)
end
"""
    value(dvar_expr::Union{GenericAffExpr{T,DecisionVariable}, GenericQuadExpr{T,DecisionVariable}}, stage_to_scenario::Dict{Int,Int}) where T

Evaluate `dvar_expr` where the value of a given `dvar` is found in the scenario returned by the provided `stage_to_scenario` map.
"""
function JuMP.value(dvar_expr::Union{GenericAffExpr{T,DecisionVariable}, GenericQuadExpr{T,DecisionVariable}}, stage_to_scenario::Dict{Int,Int}; result::Int = 1)::Float64 where T
    var_value = (dvar) -> begin
        if stage(dvar) == 1
            return value(dvar; result = result)
        end
        return value(dvar, stage_to_scenario[stage(dvar)]; result = result)
    end
    return value(dvar_expr, var_value)
end
"""
    reduced_cost(dvar::DecisionVariable)::Float64

Return the reduced cost associated with the decision variable `dvar`.
"""
function JuMP.reduced_cost(dvar::DecisionVariable)::Float64
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `reduced_cost(dvar, scenario_index)`.")
    sp = owner_model(dvar)
    if !has_duals(sp)
        error("Unable to query reduced cost of variable because stochastic program does" *
              " not have duals available in the first stage.")
    end
    sign = objective_sense(sp) == MOI.MIN_SENSE ? 1.0 : -1.0
    if is_fixed(dvar)
        return sign * dual(FixRef(dvar))
    end
    rc = 0.0
    if has_upper_bound(dvar)
        rc += dual(UpperBoundRef(dvar))
    end
    if has_lower_bound(dvar)
        rc += dual(LowerBoundRef(dvar))
    end
    return sign * rc
end
"""
    reduced_cost(dvar::DecisionVariable)::Float64

Return the reduced cost associated with the scenario-dependent decision variable `dvar` at `scenario_index`.
"""
function JuMP.reduced_cost(dvar::DecisionVariable, scenario_index::Integer)::Float64
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `reduced_cost(dvar)`.")
    sp = owner_model(dvar)
    if !has_duals(sp, scenario_index)
        error("Unable to query reduced cost of variable because stochastic program does" *
              " not have duals available in scenario `scenario_index`.")
    end
    sign = objective_sense(sp) == MOI.MIN_SENSE ? 1.0 : -1.0
    if is_fixed(dvar, scenario_index)
        return sign * dual(FixRef(dvar), scenario_index)
    end
    rc = 0.0
    if has_upper_bound(dvar, scenario_index)
        rc += dual(UpperBoundRef(dvar), scenario_index)
    end
    if has_lower_bound(dvar, scenario_index)
        rc += dual(LowerBoundRef(dvar), scenario_index)
    end
    return sign * rc
end
"""
    is_fixed(dvar::DecisionVariable)

Return `true` if `dvar` is a fixed first-stage decision variable.
"""
function JuMP.is_fixed(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `is_fixed(dvar, scenario_index)`.")
    if state(dvar) == Taken
        return true
    end
    return false
end
"""
    is_fixed(dvar::DecisionVariable, scenario_index)

Return `true` if `dvar` is a fixed decision variable in `scenario_index`.
"""
function JuMP.is_fixed(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `is_fixed(dvar)`.")
    if state(dvar, scenario_index) == Taken
        return true
    end
    return false
end
"""
    fix(dvar::DecisionVariable, val::Number)

Fix the first-stage decision associated with `dvar` to `val`. In contexts
where `dvar` is a variable, the variable is fixed to the value. In
contexts where `dvar` is a known parameter value, the value is updated.
"""
function JuMP.fix(dvar::DecisionVariable, val::Number)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `fix(dvar, scenario_index, val)`.")
    fix(structure(owner_model(dvar)), index(dvar), stage(dvar), val)
    return nothing
end
"""
    fix(dvar::DecisionVariable, scenario_index::Integer, val::Number)

Fix the scenario-dependent decision associated with `dvar` at `scenario_index`
to `val`. In contexts where `dvar` is a variable, the variable is fixed to the
value. In contexts where `dvar` is a known parameter value, the value is updated.
"""
function JuMP.fix(dvar::DecisionVariable, scenario_index::Integer, val::Number)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `fix(dvar, val)`.")
    fix(structure(owner_model(dvar)), index(dvar), stage(dvar), scenario_index, val)
    return nothing
end
"""
    unfix(dvar::DecisionVariable)

Unfix the first-stage decision associated with `dvar`. If the decision
is already in a `NotTaken` state, this does nothing.
"""
function JuMP.unfix(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `unfix(dvar, scenario_index)`.")
    if state(dvar) == NotTaken
        # Nothing to do, just return
        return nothing
    end
    unfix(structure(owner_model(dvar)), index(dvar), stage(dvar))
    return nothing
end
"""
    unfix(dvar::DecisionVariable, scenario_index::Integer)

Unfix the scenario-dependent decision associated with `dvar`
at `scenario_index`. If the decision is already in a `NotTaken`
state, this does nothing.
"""
function JuMP.unfix(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `unfix(dvar)`.")
    if state(dvar, scenario_index) == NotTaken
        # Nothing to do, just return
        return nothing
    end
    unfix(structure(owner_model(dvar)), index(dvar), stage(dvar), scenario_index)
    return nothing
end

"""
    FixRef(dvar::DecisionVariable)

Return a constraint reference to the constraint fixing the value of the decision `dvar`. Errors
if one does not exist.
"""
function JuMP.FixRef(dvar::DecisionVariable)
    moi_fix =  MOI.ConstraintIndex{SingleDecision, MOI.EqualTo{Float64}}
    return SPConstraintRef{moi_fix, ScalarShape}(owner_model(dvar),
                                                 stage(dvar),
                                                 moi_fix(index(dvar).value),
                                                 ScalarShape())
end

JuMP.owner_model(dvar::DecisionVariable) = dvar.stochasticprogram

struct DecisionVariableNotOwned <: Exception
    dvar::DecisionVariable
end

function JuMP.check_belongs_to_model(dvar::DecisionVariable, stochasticprogram::StochasticProgram)
    if owner_model(dvar) !== stochasticprogram
        throw(DecisionNotOwned(dvar))
    end
end
"""
    is_valid(stochasticprogram::StochasticProgram, dvar::DecisionVariable)

Return `true` if `dvar` refers to a valid first-stage decision variable in `stochasticprogram`.
"""
function JuMP.is_valid(stochasticprogram::StochasticProgram, dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `is_valid(dvar, scenario_index)`.")
    return stochasticprogram === owner_model(dvar) &&
        MOI.is_valid(structure(stochasticprogram), index(dvar), stage(dvar))
end
"""
    is_valid(stochasticprogram::StochasticProgram, dvar::DecisionVariable, scenario_index::Integer)

Return `true` if the scenario-dependent `dvar` refers to a valid decision variable in `stochasticprogram` at `scenario_index`.
"""
function JuMP.is_valid(stochasticprogram::StochasticProgram, dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `is_valid(dvar)`.")
    return stochasticprogram === owner_model(dvar) &&
        MOI.is_valid(structure(stochasticprogram), index(dvar), stage(dvar), scenario_index)
end
"""
    delete(stochasticprogram::StochasticProgram, dvar::DecisionVariable)

Delete the first-stage decision variable associated with `dvar` from the `stochasticprogram`.
"""
function JuMP.delete(stochasticprogram::StochasticProgram, dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `delete(stochasticprogram, dvar, scenario_index)`.")
    if stochasticprogram !== owner_model(dvar)
        error("The decision variable you are trying to delete does not " *
              "belong to the stochastic program.")
    end
    proxy_ = proxy(stochasticprogram, stage(dvar))
    JuMP.delete(proxy_, DecisionRef(proxy_, index(dvar)))
    MOI.delete(structure(stochasticprogram), index(dvar), stage(dvar))
    return nothing
end
"""
    delete(stochasticprogram::StochasticProgram, dvar::DecisionVariable, scenario_index::Integer)

Delete the scenario-dependent decision variable associated with `dvar` from the `stochasticprogram` at `scenario_index`.
"""
function JuMP.delete(stochasticprogram::StochasticProgram, dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `delete(stochasticprogram, dvar)`.")
    if stochasticprogram !== owner_model(dvar)
        error("The decision variable you are trying to delete does not " *
              "belong to the stochastic program.")
    end
    MOI.delete(structure(stochasticprogram), index(dvar), stage(dvar), scenario_index)
    return nothing
end
"""
    delete(stochasticprogram::StochasticProgram, dvars::Vector{DecisionVariable})

Delete the decisions associated with `dvars` from the `stochasticprogram`.
"""
function JuMP.delete(stochasticprogram::StochasticProgram, dvars::Vector{DecisionVariable})
    all(stage.(dvars) .== stage(dvars[1])) || error("$dvars are not all from the same stage")
    stage(dvars[1]) > 1 && error("Some of $dvars are scenario dependent, consider `delete(stochasticprogram, dvars, scenario_index)`.")
    if any(stochasticprogram !== owner_model(dvar) for dvar in dvars)
        error("A decision variable you are trying to delete does not " *
              "belong to the stochastic program.")
    end
    proxy_ = proxy(stochasticprogram, stage(dvars[1]))
    JuMP.delete(proxy_, DecisionRef.(proxy_, index.(dvars)))
    MOI.delete(structure(stochasticprogram), index.(dvars), stage(dvars[1]))
    return nothing
end
"""
    delete(stochasticprogram::StochasticProgram, dvars::Vector{DecisionVariable}, scenario_index::Integer)

Delete the scenario-dependent decisions associated with `dvars` from the `stochasticprogram` at `scenario_index`.
"""
function JuMP.delete(stochasticprogram::StochasticProgram, dvars::Vector{DecisionVariable}, scenario_index::Integer)
    all(stage.(dvars) .== stage(dvars[1])) || error("$dvars are not all from the same stage")
    stage(dvars[1]) == 1 && error("$dvars are not scenario dependent, consider `delete(stochasticprogram, dvars)`.")
    if any(stochasticprogram !== owner_model(dvar) for dvar in dvars)
        error("A decision variable you are trying to delete does not " *
              "belong to the stochastic program.")
    end
    MOI.delete(structure(stochasticprogram), index.(dvars), stage(dvars[1]), scenario_index)
    return nothing
end
"""
    has_lower_bound(dvar::DecisionVariable)

Return `true` if the first-stage decision variable `dvar` has a lower bound.
"""
function JuMP.has_lower_bound(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `has_lower_bound(dvar, scenario_index)`.")
    ci = MOI.ConstraintIndex{SingleDecision, MOI.GreaterThan{Float64}}(index(dvar).value)
    return MOI.is_valid(structure(owner_model(dvar)), ci, stage(dvar))
end
"""
    has_lower_bound(dvar::DecisionVariable, scenario_index::Integer)

Return `true` if the scenario-dependent decision variable `dvar` has a lower bound at `scenario_index`.
"""
function JuMP.has_lower_bound(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `has_lower_bound(dvar)`.")
    ci = MOI.ConstraintIndex{SingleDecision, MOI.GreaterThan{Float64}}(index(dvar).value)
    return MOI.is_valid(structure(owner_model(dvar)), ci, stage(dvar), scenario_index)
end
"""
    LowerBoundRef(dvar::DecisionVariable)

Return a constraint reference to the lower bound constraint of the decision variable `dvar`.
Errors if one does not exist.
"""
function JuMP.LowerBoundRef(dvar::DecisionVariable)
    moi_lb =  MOI.ConstraintIndex{SingleDecision, MOI.GreaterThan{Float64}}
    return SPConstraintRef{moi_lb, ScalarShape}(owner_model(dvar),
                                                stage(dvar),
                                                moi_lb(index(dvar).value),
                                                ScalarShape())
end
"""

    set_lower_bound(dvar::DecisionVariable)

Set the lower bound of the first-stage decision variable `dvar` to `lower`. If one does not exist, create a new lower bound constraint.
"""
function JuMP.set_lower_bound(dvar::DecisionVariable, lower::Number)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `set_lower_bound(dvar, scenario_index, lower)`.")
    new_set = MOI.GreaterThan(convert(Float64, lower))
    # Get proxy
    proxy_ = proxy(owner_model(dvar), stage(dvar))
    # Check if bound exists already
    if has_lower_bound(dvar)
        # Update existing bound
        cindex = MOI.ConstraintIndex{SingleDecision, MOI.GreaterThan{Float64}}(index(dvar).value)
        MOI.set(structure(owner_model(dvar)), MOI.ConstraintSet(), cindex, new_set)
        # Update proxy
        MOI.set(backend(proxy_), MOI.ConstraintSet(), cindex, new_set)
    else
        # Add new lower bound constraint
        MOI.add_constraint(structure(owner_model(dvar)), SingleDecision(index(dvar)), new_set)
        # Update proxy
        MOI.add_constraint(backend(proxy_), SingleDecision(index(dvar)), new_set)
    end
    return nothing
end
"""
    set_lower_bound(dvar::DecisionVariable, scenario_index::Integer, lower::Number)

Set the lower bound of the scenario-dependent decision variable `dvar` at `scenario_index` to `lower`. If one does not exist, create a new lower bound constraint.
"""
function JuMP.set_lower_bound(dvar::DecisionVariable, scenario_index::Integer, lower::Number)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `set_lower_bound(dvar, lower)`.")
    new_set = MOI.GreaterThan(convert(Float64, lower))
    # Get proxy
    proxy_ = proxy(owner_model(dvar), stage(dvar))
    # Check if bound exists already
    if has_lower_bound(dvar, scenario_index)
        # Update existing bound
        cindex = MOI.ConstraintIndex{SingleDecision, MOI.GreaterThan{Float64}}(index(dvar).value)
        attr = ScenarioDependentConstraintAttribute(stage(dvar), scenario_index, MOI.ConstraintSet())
        MOI.set(structure(owner_model(dvar)), attr, cindex, new_set)
        # Update proxy
        MOI.set(backend(proxy_), MOI.ConstraintSet(), cindex, new_set)
    else
        # Add new lower bound constraint
        MOI.add_constraint(structure(owner_model(dvar)), SingleDecision(index(dvar)), new_set, stage(dvar), scenario_index)
        # Update proxy if the scenario-dependent decision does not have a lower bound yet
        if !has_lower_bound(DecisionRef(proxy_, index(dvar)))
            MOI.add_constraint(backend(proxy_), SingleDecision(index(dvar)), new_set)
        end
    end
    return nothing
end
"""
    delete_lower_bound(dvar::DecisionVariable)

Delete the lower bound constraint of the first-stage decision variable `dvar`.
"""
function JuMP.delete_lower_bound(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `delete_lower_bound(dvar, scenario_index)`.")
    JuMP.delete(owner_model(dvar), LowerBoundRef(dvar))
end
"""
    delete_lower_bound(dvar::DecisionVariable, scenario_index::Integer)

Delete the lower bound constraint of the scenario-dependent decision variable `dvar` at `scenario_index`.
"""
function JuMP.delete_lower_bound(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `delete_lower_bound(dvar)`.")
    JuMP.delete(owner_model(dvar), LowerBoundRef(dvar), scenario_index)
end
"""
    lower_bound(dvar::DecisionVariable)

Return the lower bound of the first-stage decision variable `dvar`. Error if one does not exist.
"""
function JuMP.lower_bound(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `lower_bound(dvar, scenario_index)`.")
    if !has_lower_bound(dvar)
        error("Decision variable $(dvar) does not have a lower bound.")
    end
    cset = MOI.get(owner_model(dvar), MOI.ConstraintSet(),
                   LowerBoundRef(dvar))::MOI.GreaterThan{Float64}
    return cset.lower
end
"""
    lower_bound(dvar::DecisionVariable, scenario_index::Integer)

Return the lower bound of the scenario-dependent decision variable `dvar` at `scenario_index`. Error if one does not exist.
"""
function JuMP.lower_bound(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `lower_bound(dvar)`.")
    if !has_lower_bound(dvar, scenario_index)
        error("Decision variable $(dvar) at $scenario_index does not have a lower bound.")
    end
    attr = ScenarioDependentConstraintAttribute(stage(dvar), scenario_index, MOI.ConstraintSet())
    cset = MOI.get(owner_model(dvar), attr,
                   LowerBoundRef(dvar))::MOI.GreaterThan{Float64}
    return cset.lower
end
"""
    has_upper_bound(dvar::DecisionVariable)

Return `true` if the first-stage decision variable `dvar` has a upper bound.
"""
function JuMP.has_upper_bound(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `upper_bound(dvar, scenario_index)`.")
    ci = MOI.ConstraintIndex{SingleDecision, MOI.LessThan{Float64}}(index(dvar).value)
    return MOI.is_valid(structure(owner_model(dvar)), ci, stage(dvar))
end
"""
    has_upper_bound(dvar::DecisionVariable, scenario_index::Integer)

Return `true` if the scenario-dependent decision variable `dvar` has a upper bound at `scenario_index`.
"""
function JuMP.has_upper_bound(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `upper_bound(dvar)`.")
    ci = MOI.ConstraintIndex{SingleDecision, MOI.LessThan{Float64}}(index(dvar).value)
    return MOI.is_valid(structure(owner_model(dvar)), ci, stage(dvar), scenario_index)
end
"""
    LowerBoundRef(dvar::DecisionVariable)

Return a constraint reference to the upper bound constraint of the decision variable `dvar`.
Errors if one does not exist.
"""
function JuMP.UpperBoundRef(dvar::DecisionVariable)
    moi_ub =  MOI.ConstraintIndex{SingleDecision, MOI.LessThan{Float64}}
    return SPConstraintRef{moi_ub, ScalarShape}(owner_model(dvar),
                                                stage(dvar),
                                                moi_ub(index(dvar).value),
                                                ScalarShape())
end
"""
    set_upper_bound(dvar::DecisionVariable, upper::Number)

Set the upper bound of the first-stage decision variable `dvar` to `upper`. If one does not exist, create a new upper bound constraint.
"""
function JuMP.set_upper_bound(dvar::DecisionVariable, upper::Number)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `set_upper_bound(dvar, scenario_index, upper)`.")
    new_set = MOI.LessThan(convert(Float64, upper))
    # Get proxy
    proxy_ = proxy(owner_model(dvar), stage(dvar))
    # Check if bound exists already
    if has_upper_bound(dvar)
        # Update existing bound
        cindex = MOI.ConstraintIndex{SingleDecision, MOI.LessThan{Float64}}(index(dvar).value)
        MOI.set(structure(owner_model(dvar)), MOI.ConstraintSet(), cindex, new_set)
        # Update proxy
        MOI.set(backend(proxy_), MOI.ConstraintSet(), cindex, new_set)
    else
        # Add new upper bound constraint
        MOI.add_constraint(structure(owner_model(dvar)), SingleDecision(index(dvar)), new_set)
        # Update proxy
        MOI.add_constraint(backend(proxy_), SingleDecision(index(dvar)), new_set)
    end
    return nothing
end
"""
    set_upper_bound(dvar::DecisionVariable, scenario_index::Integer, upper::Number)

Set the upper bound of the scenario-dependent decision variable `dvar` at `scenario_index` to `upper`. If one does not exist, create a new upper bound constraint.
"""
function JuMP.set_upper_bound(dvar::DecisionVariable, scenario_index::Integer, upper::Number)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `set_upper_bound(dvar, upper)`.")
    new_set = MOI.LessThan(convert(Float64, upper))
    # Get proxy
    proxy_ = proxy(owner_model(dvar), stage(dvar))
    # Check if bound exists already
    if has_upper_bound(dvar, scenario_index)
        # Update existing bound
        cindex = MOI.ConstraintIndex{SingleDecision, MOI.LessThan{Float64}}(index(dvar).value)
        attr = ScenarioDependentConstraintAttribute(stage(dvar), scenario_index, MOI.ConstraintSet())
        MOI.set(structure(owner_model(dvar)), attr, cindex, new_set)
        # Update proxy
        MOI.set(backend(proxy_), MOI.ConstraintSet(), cindex, new_set)
    else
        # Add new upper bound constraint
        MOI.add_constraint(structure(owner_model(dvar)), SingleDecision(index(dvar)), new_set, stage(dvar), scenario_index)
        # Update proxy if the scenario-dependent decision does not have a upper bound yet
        if !has_upper_bound(DecisionRef(proxy_, index(dvar)))
            MOI.add_constraint(backend(proxy_), SingleDecision(index(dvar)), new_set)
        end
    end
    return nothing
end
"""
    delete_upper_bound(dvar::DecisionVariable)

Delete the upper bound constraint of the first-stage decision variable `dvar`.
"""
function JuMP.delete_upper_bound(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `delete_upper_bound(dvar, scenario_index)`.")
    JuMP.delete(owner_model(dvar), UpperBoundRef(dvar))
end
"""
    delete_upper_bound(dvar::DecisionVariable, scenario_index::Integer)

Delete the upper bound constraint of the scenario-dependent decision variable `dvar` at `scenario_index`.
"""
function JuMP.delete_upper_bound(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `delete_upper_bound(dvar)`.")
    JuMP.delete(owner_model(dvar), UpperBoundRef(dvar), scenario_index)
end
"""
    upper_bound(dvar::DecisionVariable)

Return the upper bound of the first-stage decision variable `dvar`. Error if one does not exist.
"""
function JuMP.upper_bound(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `upper_bound(dvar, scenario_index)`.")
    if !has_upper_bound(dvar)
        error("Decision $(dvar) does not have a upper bound.")
    end
    cset = MOI.get(owner_model(dvar), MOI.ConstraintSet(),
                   UpperBoundRef(dvar))::MOI.LessThan{Float64}
    return cset.upper
end
"""
    upper_bound(dvar::DecisionVariable, scenario_index::Integer)

Return the upper bound of the scenario-dependent decision variable `dvar` at `scenario_index`. Error if one does not exist.
"""
function JuMP.upper_bound(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `upper_bound(dvar)`.")
    if !has_upper_bound(dvar, scenario_index)
        error("Decision $(dvar) at `scenario_index` does not have a upper bound.")
    end
    attr = ScenarioDependentConstraintAttribute(stage(dvar), scenario_index, MOI.ConstraintSet())
    cset = MOI.get(owner_model(dvar), attr,
                   UpperBoundRef(dvar))::MOI.LessThan{Float64}
    return cset.upper
end
"""
    is_integer(dvar::DecisionVariable)

Return `true` if the first-stage decision variable `dvar` is constrained to be integer.
"""
function JuMP.is_integer(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `is_integer(dvar, scenario_index)`.")
    ci = MOI.ConstraintIndex{SingleDecision, MOI.Integer}(index(dvar).value)
    return MOI.is_valid(structure(owner_model(dvar)), ci, stage(dvar))
end
"""
    is_integer(dvar::DecisionVariable, scenario_index::Integer)

Return `true` if the scenario-dependent decision variable `dvar` is constrained to be integer at `scenario_index`.
"""
function JuMP.is_integer(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `is_integer(dvar)`.")
    ci = MOI.ConstraintIndex{SingleDecision, MOI.Integer}(index(dvar).value)
    return MOI.is_valid(structure(owner_model(dvar)), ci, stage(dvar), scenario_index)
end
"""
    set_integer(dvar::DecisionVariable)

Add an integrality constraint on the first-stage decision variable `dvar`.
"""
function JuMP.set_integer(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `set_integer(dvar, scenario_index)`.")
    # Get proxy
    proxy_ = proxy(owner_model(dvar), stage(dvar))
    # Check if bound exists already
    if is_integer(dvar)
        return nothing
    elseif is_binary(dvar)
        error("Cannot set the decision $(dvar) to integer as it is already binary.")
    else
        # Add new integer constraint
        MOI.add_constraint(structure(owner_model(dvar)), SingleDecision(index(dvar)), MOI.Integer())
        # Update proxy
        MOI.add_constraint(backend(proxy_), SingleDecision(index(dvar)), MOI.Integer())
    end
    return nothing
end
"""
    set_integer(dvar::DecisionVariable, scenario_index::Integer)

Add an integrality constraint on the scenario-dependent decision variable `dvar` at `scenario_index`.
"""
function JuMP.set_integer(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `set_integer(dvar)`.")
    # Get proxy
    proxy_ = proxy(owner_model(dvar), stage(dvar))
    # Check if bound exists already
    if is_integer(dvar, scenario_index)
        return nothing
    elseif is_binary(dvar, scenario_index)
        error("Cannot set the decision $(dvar) at `scenario_index` to integer as it is already binary.")
    else
        # Add new integer constraint
        MOI.add_constraint(structure(owner_model(dvar)), SingleDecision(index(dvar)), MOI.Integer(), stage(dvar), scenario_index)
        # Update proxy if the scenario-dependent decision does not have an integer bound yet
        if !is_integer(DecisionRef(proxy_, index(dvar)))
            MOI.add_constraint(backend(proxy_), SingleDecision(index(dvar)), MOI.Integer())
        end
    end
    return nothing
end
"""
    unset_integer(dvar::DecisionVariable)

Delete the integrality constraint of the first-stage decision variable `dvar`.
"""
function JuMP.unset_integer(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `unset_integer(dvar, scenario_index)`.")
    JuMP.delete(owner_model(dvar), IntegerRef(dvar))
    return nothing
end
"""
    unset_integer(dvar::DecisionVariable, scenario_index::Integer)

Delete the integrality constraint of the scenario-dependent decision variable `dvar` at `scenario_index`.
"""
function JuMP.unset_integer(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `unset_integer(dvar)`.")
    JuMP.delete(owner_model(dvar), IntegerRef(dvar), scenario_index)
    return nothing
end
"""
    IntegerRef(dvar::DecisionVariable)

Return a constraint reference to the integrality constraint of the decision variable `dvar`.
Errors if one does not exist.
"""
function JuMP.IntegerRef(dvar::DecisionVariable)
    moi_int =  MOI.ConstraintIndex{SingleDecision, MOI.Integer}
    return SPConstraintRef{moi_int, ScalarShape}(owner_model(dvar),
                                                 stage(dvar),
                                                 moi_int(index(dvar).value),
                                                 ScalarShape())
end
"""
    is_binary(dvar::DecisionVariable)

Return `true` if the first-stage decision variable `dvar` is constrained to be binary.
"""
function JuMP.is_binary(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `is_binary(dvar, scenario_index)`.")
    ci = MOI.ConstraintIndex{SingleDecision, MOI.ZeroOne}(index(dvar).value)
    return MOI.is_valid(structure(owner_model(dvar)), ci, stage(dvar))
end
"""
    is_binary(dvar::DecisionVariable, scenario_index::Integer)

Return `true` if the scenario-dependent decision variable `dvar` is constrained to be binary at `scenario_index`.
"""
function JuMP.is_binary(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `is_binary(dvar)`.")
    ci = MOI.ConstraintIndex{SingleDecision, MOI.ZeroOne}(index(dvar).value)
    return MOI.is_valid(structure(owner_model(dvar)), ci, stage(dvar), scenario_index)
end
"""
    set_binary(dvar::DecisionVariable)

Constrain the first-stage decision variable `dvar` to the set ``\\{0,1\\}``.
"""
function JuMP.set_binary(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `set_binary(dvar, scenario_index)`.")
    # Get proxy
    proxy_ = proxy(owner_model(dvar), stage(dvar))
    # Check if bound exists already
    if is_binary(dvar)
        return nothing
    elseif is_integer(dvar)
        error("Cannot set the decision $(dvar) to binary as it is already integer.")
    else
        # Add new binary constraint
        MOI.add_constraint(structure(owner_model(dvar)), SingleDecision(index(dvar)), MOI.ZeroOne())
        # Update proxy
        MOI.add_constraint(backend(proxy_), SingleDecision(index(dvar)), MOI.ZeroOne())
    end
    return nothing
end
"""
    set_binary(dvar::DecisionVariable, scenario_index::Integer)

Constrain the scenario-dependent decision variable `dvar` to the set ``\\{0,1\\}`` at `scenario_index`.
"""
function JuMP.set_binary(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `set_binary(dvar)`.")
    # Get proxy
    proxy_ = proxy(owner_model(dvar), stage(dvar))
    # Check if bound exists already
    if is_binary(dvar, scenario_index)
        return nothing
    elseif is_integer(dvar, scenario_index)
        error("Cannot set the decision $(dvar) at `scenario_index` to binary as it is already integer.")
    else
        # Add new binary constraint
        MOI.add_constraint(structure(owner_model(dvar)), SingleDecision(index(dvar)), MOI.ZeroOne(), stage(dvar), scenario_index)
        # Update proxy if the scenario-dependent decision does not have a binary bound yet
        if !is_binary(DecisionRef(proxy_, index(dvar)))
            MOI.add_constraint(backend(proxy_), SingleDecision(index(dvar)), MOI.ZeroOne())
        end
    end
end
"""
    unset_binary(dvar::DecisionVariable)

Delete the binary constraint of the first-stage decision variable `dvar`.
"""
function JuMP.unset_binary(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `unset_binary(dvar, scenario_index)`.")
    JuMP.delete(owner_model(dvar), BinaryRef(dvar))
    return nothing
end
"""
    unset_binary(dvar::DecisionVariable, scenario_index::Integer)

Delete the binary constraint of the scenario-dependent decision variable `dvar` at `scenario_index`.
"""
function JuMP.unset_binary(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `unset_binary(dvar)`.")
    JuMP.delete(owner_model(dvar), BinaryRef(dvar), scenario_index)
    return nothing
end
"""
    BinaryRef(dvar::DecisionVariable)

Return a constraint reference to the binary constraint of the decision variable `dvar`.
Errors if one does not exist.
"""
function JuMP.BinaryRef(dvar::DecisionVariable)
    moi_bin =  MOI.ConstraintIndex{SingleDecision, MOI.ZeroOne}
    return SPConstraintRef{moi_bin, ScalarShape}(owner_model(dvar),
                                                 stage(dvar),
                                                 moi_bin(index(dvar).value),
                                                 ScalarShape())
end
"""
    start_value(dvar::DecisionVariable)

Return the start value of the first-stage decision variable `dvar`.
"""
function JuMP.start_value(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `start_value(dvar, scenario_index)`.")
    return MOI.get(owner_model(dvar), MOI.VariablePrimalStart(), dvar)
end
"""
    start_value(dvar::DecisionVariable, scenario_index::Integer)

Return the start value of the scenario-dependent decision variable `dvar`
at `scenario_index`.
"""
function JuMP.start_value(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `start_value(dvar)`.")
    attr = ScenarioDependentVariableAttribute(stage(dvar), scenario_index, MOI.VariablePrimalStart())
    return MOI.get(owner_model(dvar), attr, dvar)
end
"""
    set_start_value(dvar::DecisionVariable)

Set the start value of the first-stage decision variable `dvar` to `value`.
"""
function JuMP.set_start_value(dvar::DecisionVariable, value::Number)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `set_start_value(dvar, scenario_index, value)`.")
    MOI.set(backend(proxy(owner_model(dvar), stage(dvar))), MOI.VariablePrimalStart(), index(dvar), Float64(value))
    MOI.set(owner_model(dvar), MOI.VariablePrimalStart(), dvar, Float64(value))
end
"""
    set_start_value(dvar::DecisionVariable, scenario_index::Integer, value::Number)

Set the start value of the scenario-dependent decision variable `dvar`
at `scenario_index` to `value`.
"""
function JuMP.set_start_value(dvar::DecisionVariable, scenario_index::Integer, value::Number)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `set_start_value(dvar, value)`.")
    attr = ScenarioDependentVariableAttribute(stage(dvar), scenario_index, MOI.VariablePrimalStart())
    MOI.set(owner_model(dvar), attr, dvar, Float64(value))
end

function Base.hash(dvar::DecisionVariable, h::UInt)
    return hash(objectid(owner_model(dvar)), hash(dvar.index, h))
end

JuMP.isequal_canonical(d::DecisionVariable, other::DecisionVariable) = isequal(d, other)
function Base.isequal(dvar::DecisionVariable, other::DecisionVariable)
    return owner_model(dvar) === owner_model(other) && dvar.index == other.index
end

Base.iszero(::DecisionVariable) = false
Base.copy(dvar::DecisionVariable) = DecisionVariable(dvar.stochasticprogram, stage(dvar), dvar.index)
Base.broadcastable(dvar::DecisionVariable) = Ref(dvar)

function DecisionRef(dvar::DecisionVariable)
    stage(dvar) > 1 && error("$dvar is scenario dependent, consider `DecisionRef(dvar, scenario_index)`.")
    sp = owner_model(dvar)
    return DecisionRef(proxy(sp, stage(dvar)), structure(sp), index(dvar))
end
function DecisionRef(dvar::DecisionVariable, scenario_index::Integer)
    stage(dvar) == 1 && error("$dvar is not scenario dependent, consider `DecisionRef(dvar)`.")
    sp = owner_model(dvar)
    return DecisionRef(proxy(sp, stage(dvar)), structure(sp), index(dvar), scenario_index)
end
function DecisionRef(dvar::DecisionVariable, at_stage::Integer, scenario_index::Integer)
    at_stage > stage(dvar) || error("$dvar can only be known after stage $(stage(dvar)).")
    sp = owner_model(dvar)
    return DecisionRef(proxy(sp, at_stage), structure(sp), index(dvar), at_stage, scenario_index)
-end

is_decision_type(::Type{DecisionVariable}) = true

function JuMP.moi_function_type(::Type{DecisionVariable})
    return SingleDecision
end
function JuMP.moi_function_type(::Type{<:Vector{<:DecisionVariable}})
    return VectorOfDecisions
end


using DiffusionMCMC
using ExtensibleMCMC
using Test

const dMCMC = DiffusionMCMC
const eMCMC = ExtensibleMCMC

@testset "schedule.jl" begin
    schedule_params = (
        num_mcmc_iter = 10,
        num_params = 4,
        exclude_params = [(1,3:8), (2,4:2:10), (4,6:10)]
    )
    ∅ = nothing
    t,f = true, false

    schedule = eMCMC.MCMCSchedule(
        schedule_params...;
        start=(
            prev_mcmciter=∅,
            prev_pidx=∅,
            mcmciter=1,
            pidx=1,
            same_layout=(t,t,t)
        ),
        backend=DiffusionMCMCBackend(),
        extra_info=(
            layout_types=(
                (1,1,1),
                (1,2,2),
                (1,1,3),
                (1,2,3),
            ),
            no_blocking=false,
            blocking_layout=:not_needed,
        )
    )

    expected = (
        (∅,∅,1,1,(t,t,t)),  (1,1,1,2,(t,f,f)),  (1,2,1,3,(t,f,f)),  (1,3,1,4,(t,f,t)),
        (1,4,2,1,(t,f,f)),  (2,1,2,2,(t,f,f)),  (2,2,2,3,(t,f,f)),  (2,3,2,4,(t,f,t)),
                            (2,4,3,2,(t,t,f)),  (3,2,3,3,(t,f,f)),  (3,3,3,4,(t,f,t)),
                                                (3,4,4,3,(t,f,t)),  (4,3,4,4,(t,f,t)),
                            (4,4,5,2,(t,t,f)),  (5,2,5,3,(t,f,f)),  (5,3,5,4,(t,f,t)),
                                                (5,4,6,3,(t,f,t)),
                            (6,3,7,2,(t,f,f)),  (7,2,7,3,(t,f,f)),
                                                (7,3,8,3,(t,t,t)),
        (8,3,9,1,(t,t,f)),  (9,1,9,2,(t,f,f)),  (9,2,9,3,(t,f,f)),
        (9,3,10,1,(t,t,f)),                     (10,1,10,3,(t,t,f)),
    )
    for (i,s) in enumerate(schedule)
        @test expected[i] == tuple(s...)
    end
end

@testset "DiffusionMCMC.jl" begin
    # Write your own tests here.
end


"""
FluxiData has fields:
  net::Network
  measurements::Dict{String,Vector{MassDistributionMeasurement}}
  substrate_configs::Dict{String,Vector{ConfiguredSEMUMixture}}
  linear_constraints::Dict{String,Vector{LinConstr
"""
struct FluxiData
    net::Network
    measurements::Dict{String,Vector{MassDistributionMeasurement}}
    substrate_configs::Dict{String,Vector{ConfiguredSEMUMixture}} #
    linear_constraints::Dict{String,Vector{LinConstr}}
end

"""
parseFluxi(data::String)
returns FluxiData object

parses fluxi input
"""
function parseFluxi(data::String)

    ## Parse species:
    species::Vector{Species} = Vector{Species}()
    # 1: Find Species Section:
    d_species = extractSection( data , "Species" )

    # 2. split and loop over lines:
    s_lines = split(d_species,"\n")
    for ss in s_lines
        ss::String = preprocessLine(ss)
        if(isempty(ss)); continue; end
        ss_s = split(ss,":")
        push!(species,Species(strip(ss_s[1]),parse(Int64,strip(ss_s[2]))) )
    end

    ## Parse fluxes:
    fluxes::Vector{Flux} = Vector{Flux}()
    # 1: Find Species Section:
    d_fluxes = extractSection( data , "Fluxes" )

    # 2. split and loop over lines:
    f_lines = split(d_fluxes,"\n")
    for fs in f_lines
        fs::String = preprocessLine(fs)
        if(isempty(fs)); continue; end
        fs_s = split(fs,":")
        push!(fluxes, parseFlux( strip(String(fs_s[1])) , strip(String(fs_s[2])) ) )
    end

    ## create network from species and fluxes:
    fluxes_str::Vector{String}        = Vector{String}()
    fluxes_db::Dict{String,Flux}     = Dict{String,Flux}()
    species_str::Vector{String}       = Vector{String}()
    species_db::Dict{String,Species} = Dict{String,Species}()
    for fi in fluxes
        push!(fluxes_str,fi.id); fluxes_db[fi.id] = fi;
    end
    for si in species
        push!(species_str,si.id); species_db[si.id] = si;
    end
    net::Network = Network(fluxes_str,species_str,fluxes_db,species_db)

    ## Parse all measurements sections
    d_ms = extractNamedSections(String(data), "MeasurementsSet")
    dict_measurements = Dict{String,Vector{MassDistributionMeasurement}}()
    for msname in keys(d_ms)
        str_msi = d_ms[msname]
        mdml::Vector{MassDistributionMeasurement} = parseMeasurementSet(str_msi)
        dict_measurements[msname] = mdml
    end

    ## Parse all substrate config sections
    d_scs = extractNamedSections(String(data), "SubstrateConfiguration")
    #print(d_scs)
    dict_scs = Dict{String,Vector{ConfiguredSEMUMixture}}()
    for scname in keys(d_scs)
        str_scsi = d_scs[scname]
        scsi::Vector{ConfiguredSEMUMixture} = parseSubstrateMixture(str_scsi)
        dict_scs[scname] = scsi
    end

    ## Parse all linear constraints sections
    d_lcs = extractNamedSections(String(data), "LinearConstraintsSet")
    dict_lcs = Dict{String,Vector{LinConstr}}()
    for lcname in keys(d_lcs)
        str_lcsi = d_lcs[lcname]
        lcsi::Vector{LinConstr} = parseLinConstr(str_lcsi)
        dict_lcs[lcname] = lcsi
    end

    return FluxiData(net,dict_measurements,dict_scs,dict_lcs)
end

function extractSection(data::String, name::String)
    r_find_s = "$(name)Start:"
    r_find_e = "$(name)End:"
    mm_s     = search(data,r_find_s)
    mm_e     = search(data,r_find_e)
    str_s    = data[ (mm_s.stop+1) : (mm_e.start-1) ]
    #print(str_s)
    return str_s
end

function extractNamedSections(data::String, name::String)

    named_data::Dict{String,String} = Dict{String,String}()

    last_pos::Int64 = 1
    while(true)
        r_find_s = Regex("$(name)Start:{.+?}")
        ms = search(data,r_find_s,last_pos)
        if(ms.stop<0)
            break
        end
        last_pos = ms.stop+1
        # find section name..
        sss = data[ms]
        sss = replace(sss,"$(name)Start:","")
        sss = replace(sss,"{","")
        sss = replace(sss,"}","")
        sec_name = strip(sss)

        # find end..
        r_find_e = Regex("$(name)End:{$(sec_name)}")
        ms2 = search(data,r_find_e)

        str_s = data[ (ms.stop+1) : (ms2.start-1) ]
        #println(str_s)
        named_data[sec_name] = str_s
    end
    #println(named_data)
    return named_data
end

#removes comments, strips everything etc..
function preprocessLine(li::String)
    li = replace(li,"\r","")
    lis = split(li,"//")
    if(length(li)>1)
        li = String(lis[1])
    end
    li = strip(li)
    return li
end

function parseFlux(id::String, rs::String)
    sp1 = split(rs,"-->")
    rs  = sp1[1]
    ps  = sp1[2]

    rr  = split(rs,"+")
    pp  = split(ps,"+")

    rasd = Dict{String,RAS}()
    pasd = Dict{String,PAS}()

    # keep track of number of reactands of specific species:
    species_count_r::LC{String} = LC(String)
    for ri in rr
        (stoich::Float64,sid::String,mat::String) = parseReactand(String(ri))
        species_count_r = add( species_count_r , sid )
        if( (!isempty(mat)) && (stoich!=1) ); error("Non-one stoich. for reactand with atom transition map"); end
        # now parse atom map
        if(!isempty(mat))
            mats = split(mat,"-")
            sci::Int64  = 1
            for zs::String in mats
                sas1::SAS = SAS(sid,sci)
                #ras1::RAS = RAS( sas1 , Int64(round(species_count_r.map[sid])) )
                ras1::RAS = RAS( id , sas1 , Int64(round(species_count_r[sid])) )
                rasd[zs] = ras1
                sci = sci + 1
            end
        end
        #println("$(rasd)")
    end
    species_count_p::LC{String} = LC(String)
    for ri in pp
        (stoich::Float64,sid::String,mat::String) = parseReactand(String(ri))
        species_count_p = add( species_count_p , sid )
        if( (!isempty(mat)) && (stoich!=1) ); error("Non-one stoich. for reactand with atom transition map"); end
        # now parse atom map
        if(!isempty(mat))
            mats = split(mat,"-")
            sci::Int64  = 1
            for zs::String in mats
                sas1::SAS = SAS(sid,sci)
                #pas1::PAS = PAS( sas1 , Int64(round(species_count_p.map[sid])) )
                pas1::PAS = PAS( id , sas1 , Int64(round(species_count_p[sid])) )
                pasd[zs] = pas1
                sci = sci + 1
            end
        end
        #println("$(pasd)")
    end
    # Create atom transition map:
    tm::Dict{RAS,PAS} = Dict{RAS,PAS}()
    if( haskey(species_count_r,"*") || haskey(species_count_p,"*") )
        # no tm..
    else
        # compute tm
        if( Set(keys( rasd )) != Set(keys( pasd )) ); error("Parsing TM failed.. : $(id) : $(rs)"); end
        #keys_r = sort( keys( rasd ) )
        #keys_p = sort( keys( pasd ) )
        for ki in keys(rasd)
            tm[ rasd[ki] ] = pasd[ki]
        end
    end
    fx::Flux = Flux(id,species_count_r,species_count_p,tm)
    return fx
end

#returns string representation of flux
function write_atom_transition(f::Flux)
    transition_inv = map(reverse,f.transition)

    atom_placeholders = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" # i.e. exporting map with > 52 atoms will fail..
    str_ed::IOBuffer = IOBuffer()

    educts = sort(collect(keys(f.E)))
    map_atom_identifiers = Dict{RAS,String}()
    atom_counter = 1
    for ei in educts
        #find sas of ei:
        sas_ei = filter( s -> isequal( s.sas.species , ei ) , collect(keys(f.transition)) )
        #find max slot pos:
        num_slots = maximum( map( s -> s.sas.pos , sas_ei ) )

        for zs in 1:f.E[ei]
            print( str_ed , ei)
            print( str_ed , "(")
            for zx in 1:num_slots
                print( str_ed , atom_placeholders[atom_counter:atom_counter] )
                map_atom_identifiers[ RAS(f.id,SAS(ei,zx),zs) ] = atom_placeholders[atom_counter:atom_counter]
                atom_counter += 1
                if(zx<num_slots)
                    print( str_ed , "-")
                end
            end
            print( str_ed , ") + ")
        end
    end
    str_educts = String(take!(str_ed))
    str_educts = str_educts[1:end-2]

    products = sort(collect(keys(f.P)))
    str_prod::IOBuffer = IOBuffer()
    for pi in products
        #find sas of pi:
        sas_pi = filter( s -> isequal( s.sas.species , pi ) , collect(values(f.transition)) )
        #find max slot pos:
        num_slots = maximum( map( s -> s.sas.pos , sas_pi ) )

        for zs in 1:f.P[pi]
            print( str_prod , pi)
            print( str_prod , "(")
            for zx in 1:num_slots
                # find correct identifier:
                pas_i = PAS(f.id,SAS(pi,zx),zs)
                str_i = map_atom_identifiers[ transition_inv[pas_i] ]
                print( str_prod , str_i )
                if(zx<num_slots)
                    print( str_prod , "-")
                end
            end
            print( str_prod , ") + ")
        end
    end
    str_products = String(take!(str_prod))
    str_products = str_products[1:end-2]

    return string( str_educts , " --> " , str_products )
end

#return stoichiometry, "species id" and the atom transition map part
#i.e.:
# "sA()" --> (1.0,"sA","")
# " 4.0 sB " --> (4.0,"sB","")
# " sC(a-b-X) " --> (1.0,"sC","a-b-X")
function parseReactand(rs::String)
    debug = 0

    stoich::Float64 = 1.0
    ri1 = strip(rs)
    ri2 = split( rs , "(" )
    if(size(ri2,1)==1)
        ri1 = string(ri1,"()")
    end
    #extract species sid
    if(debug>0); println("ri: $(ri1)") ; end
    sid::String = ri1[ 1:(searchindex(ri1,"(")-1)]
    sid = strip(sid)

    # try to split sid..
    sid_s = split(sid,r"\s+")
    if(length(sid_s)>2); error("parseReactand: error for : \"$(rs)\""); end
    if(length(sid_s)==1); end
    if(length(sid_s)==2)
        sid = sid_s[2]
        stoich = float(String(sid_s[1]))
    end
    if(debug>0); println("stoich: $(stoich) sid: $(sid)") ; end
    #species_count = add( species_count , sid )
    #println(species_count)
    # extract atom map
    mat_a = match( r"\((.*)\)" , ri1 )
    mat = mat_a[1]
    mat = replace(mat,"(","")
    mat = replace(mat,")","")
    if(debug>0); println("mat: $(mat)"); end
    return (stoich,sid,mat)
end

function parseMeasurementSet(ms::String)

    mss = split(ms,"\n")

    in_mdset::Bool = false # are we currently parsing measurements

    parsed_measurements = Vector{MassDistributionMeasurement}()
    current_ms::MassDistributionMeasurement = MassDistributionMeasurement( Set{SAS}() , Dict{Int64,Array{Float64,1}}() )# = MassDistributionMeasurement(Set{SAS}(),Dict{}())

    for li in mss
        li = String(li)
        li = preprocessLine(li)
        if(isempty(li)); continue; end

        #println("Process line: $(String(li))")

        if(in_mdset)
            if(li=="}")
                push!(parsed_measurements,current_ms)
                in_mdset = false
                continue
            else
                lis = split(li,":")
                lis_a = lis[1]
                if(search(lis_a,"M=")[2]<1); error("Parsing of Mass wrong syntax.."); end
                lis_a = replace( lis_a , "M=" , "" )
                imass = parse(Int64,lis_a)
                # parse 3 floats for lis_b
                lis_b = lis[2]
                lis_b_s = split(lis_b,",")
                lis_b_values::Array{Float64,1} = [ parse(Float64,lis_b_s[1]) ; parse(Float64,lis_b_s[2]) ; parse(Float64,lis_b_s[3]) ]
                current_ms.data[imass] = lis_b_values
            end
        else
            lis = split(li,"{")
            lis = lis[1]
            liss = split(lis,"[")
            liss_a = liss[1]
            ms_name::String = strip(liss_a)
            liss_b = liss[2]
            liss_b = replace(liss_b,"]","")
            svals = split( liss_b ,"," )
            sass::Set{SAS} = Set{SAS}()
            for svi in svals
                push!( sass , SAS( ms_name , parse(Int64,svi) ) )
            end
            current_ms = MassDistributionMeasurement( sass , Dict{Int64,Array{Float64,1}}() )
            in_mdset = true
        end
    end
    return parsed_measurements
end

function parseSubstrateMixture(sm_str::String)

    sm_s = split(sm_str,"\n")

    parsed_species = Vector{ConfiguredSEMUMixture}()

    for li in sm_s
        li = String(li)
        li = preprocessLine(li)
        if(isempty(li)); continue; end

        #println("Process line: $(String(li))")

        li_s = split(li,":")
        if(length(li_s)!=2); error("Syntax Error in Substrate Mixture Def. : $(String(li))"); end
        li_a = strip(li_s[1]) # species id

        li_b = strip(li_s[2])
        li_b_s = split(li_b,";")

        csasm::ConfiguredSEMUMixture = ConfiguredSEMUMixture()

        for zi=1:length(li_b_s)
            li_b_s[zi] = strip(li_b_s[zi])
            #println(li_b_s[zi])
            mixci = split(li_b_s[zi],"=")
            if(length(mixci)!=2); error("Syntax Error in Substrate Mixture Def. : $(String(li))"); end
            mixci_a = mixci[1]
            mixci_a = replace(String(mixci_a),"(","")
            mixci_a = replace(String(mixci_a),")","")
            mixci_a_s = split(mixci_a,",")
            config_i::Array{Int64,1} = Array{Int64,1}()
            for isi in mixci_a_s
                push!(config_i,parse(Int64,isi))
            end
            sas_i::Array{SAS,1} = Array{SAS,1}()
            for zj=1:length(mixci_a_s)
                push!(sas_i,SAS(li_a,zj))
            end
            cr::ConfiguredSEMU = ConfiguredSEMU(Set{SAS}(sas_i),config_i)
            csasm[cr] = parse(Float64,mixci[2])
        end
        push!( parsed_species , csasm)
    end
    return parsed_species
end

function parseLinConstr(data::String)

    lcs = split(data,"\n")

    lin_constr = Vector{LinConstr}()

    for li in lcs
        li = String(li)
        li = preprocessLine(li)
        if(isempty(li)); continue; end
        #println("Process line: $(String(li))")
        push!( lin_constr , parse_linconstr(li) )
    end

    return lin_constr
end

function get_configured_reactands( fd::FluxiData , substr_conf::String)
    sc = fd.substrate_configs[substr_conf]
    csemus::Vector{ConfiguredSEMU} = Vector{ConfiguredSEMU}()
    for sci in sc
        append!( csemus , collect(keys(sci)) )
    end
    #print(csemus)

    all_sas::Vector{SAS} = Vector{SAS}()
    for cse in csemus
        append!( all_sas , cse.semu)
    end
    return Set(all_sas)
end


"""
        BoundaryFunction{B, X1, X2}(func, parameters=nothing)

A wrapper for the user-defined boundary condition function `func`, on the
boundary specified by symbol `B` and at location `(X1, X2)`, and with `parameters`.

Example
=======

julia> using Oceananigans, Oceananigans.BoundaryConditions, Oceananigans.Fields

julia> top_tracer_flux = BoundaryFunction{:z, Cell, Cell}((x, y, t) -> cos(2π*x) * cos(t))
(::BoundaryFunction{:z,Cell,Cell,var"#7#8",Nothing}) (generic function with 1 method)

julia> top_tracer_bc = BoundaryCondition(Flux, top_tracer_flux);

julia> flux_func(x, y, t, p) = cos(p.k * x) * cos(p.ω * t); # function with parameters

julia> parameterized_u_velocity_flux = BoundaryFunction{:z, Face, Cell}(flux_func, (k=4π, ω=3.0))
(::BoundaryFunction{:z,Face,Cell,typeof(flux_func),NamedTuple{(:k, :ω),Tuple{Float64,Float64}}}) (generic function with 1 method)

julia> top_u_bc = BoundaryCondition(Flux, parameterized_u_velocity_flux);
"""
struct BoundaryFunction{B, X1, X2, F, P} <: Function
          func :: F
    parameters :: P

    function BoundaryFunction{B, X1, X2}(func; parameters=nothing) where {B, X1, X2}

        B ∈ (:x, :y, :z) || throw(ArgumentError("The boundary B at which the BoundaryFunction is
                                                to be applied must be either :x, :y, or :z."))

        return new{B, X1, X2, typeof(func), typeof(parameters)}(func, parameters)
    end
end

@inline call_boundary_function(func, ξ, η, t, ::Nothing) = func(ξ, η, t)
@inline call_boundary_function(func, ξ, η, t, parameters) = func(ξ, η, t, parameters)

@inline (bc::BoundaryFunction{:x, Y, Z})(j, k, grid, clock, state) where {Y, Z} =
    call_boundary_function(bc.func, ynode(Y, j, grid), znode(Z, k, grid), clock.time, bc.parameters)

@inline (bc::BoundaryFunction{:y, X, Z})(i, k, grid, clock, state) where {X, Z} =
    call_boundary_function(bc.func, xnode(X, i, grid), znode(Z, k, grid), clock.time, bc.parameters)

@inline (bc::BoundaryFunction{:z, X, Y})(i, j, grid, clock, state) where {X, Y} =
    call_boundary_function(bc.func, xnode(X, i, grid), ynode(Y, j, grid), clock.time, bc.parameters)


using Flux
using FluxModels
using Test

@testset "Vision Models" begin
    include("VisionModel/alexnet.jl")
    include("VisionModel/resnet.jl")
    include("VisionModel/vgg.jl")
    include("VisionModel/googlenet.jl")
end


module FactorNodeTest

using Test
using ForneyLab: FactorGraph, FactorNode, Clamp, Terminal, Variable, Interface, PointMass, GaussianMixture, Nonlinear, Unscented, Sampling, Extended, MomentConstraint, ChanceConstraint
using InteractiveUtils: subtypes

@testset "FactorNode" begin
    g = FactorGraph()

    # Collect all node types
    node_types = Type[Nonlinear{Unscented}, Nonlinear{Sampling}, Nonlinear{Extended}] # Initialze with non-concrete types
    stack = Type[FactorNode]

    while !isempty(stack)
        for node_type in subtypes(pop!(stack))
            if isconcretetype(node_type)
                (node_type == PointMass) && continue # skip PointMass
                push!(node_types, node_type)
            else
                push!(stack, node_type)
            end
        end
    end

    for node_type in node_types
        # Instantiate a test node
        if node_type == Clamp
            test_node = Clamp(Variable(), 0.0)
        elseif node_type == Terminal
            test_node = Terminal(Variable(), Clamp(Variable(), 0.0).interfaces[1])
        elseif node_type == GaussianMixture # Required for Vararg argument
            test_node = GaussianMixture(Variable(), Variable(), Variable(), Variable(), Variable(), Variable())
        elseif node_type <: Nonlinear
            test_node = node_type(Variable(), Variable(), g=()->())
        elseif node_type == MomentConstraint
            test_node = MomentConstraint(Variable(), g=()->(), G=0.0, eta_init=0.0)
        elseif node_type == ChanceConstraint
            test_node = ChanceConstraint(Variable(), G=(), epsilon=0.05)
        else
            constructor_argument_length = length(first(methods(node_type)).sig.parameters) - 1
            vars = [Variable() for v = 1:constructor_argument_length]
            test_node = node_type(vars...)
        end

        # Node fields should be of correct types
        @test isa(test_node.id, Symbol)
        @test isa(test_node.interfaces, Vector{Interface})
        @test isa(test_node.i, Dict)

        # Node constructor should automatically assign an id and check uniqueness
        @test !isempty(string(test_node.id))
        if isa(node_type, Clamp)
            @test_throws Exception Clamp(Variable(), 0.0; id=test_node.id)
        else
            @test_throws Exception node_type(vars...; id=test_node.id)
        end

        # Node constructor should assign interfaces to itself
        for iface in test_node.interfaces
            @test ===(iface.node, test_node)
        end

        # Node constructor should add node to graph
        @test ===(g.nodes[test_node.id], test_node)

    end

    # isless should be defined for nodes
    @test isless(Clamp(Variable(), 0.0; id=:a), Clamp(Variable(), 0.0; id=:b))
end

end #module


#=
    -------------------------------------------------------------------------
    Implements functionalities for setting up the Markov chain Monte Carlo
    algorithm. The main object is `MCMCSetup` and its members comprise of
    all objects that need to be passed to the `mcmc` function from `mcmc.jl`.
    The remaining routines are used to populate instances of `MCMCSetup` and
    verify its fields in a structured manner.
    --------------------------------------------------------------------------
=#

"""
    MCMCSetup

Groups together all objects that need to be passed to `mcmc` function from
`mcmc.jl`.
"""
mutable struct MCMCSetup{ObsScheme}
    P˟                  # Target diffusion law
    P̃                   # Vector of auxiliary diffusion laws
    blocking            # Blocking type
    blocking_params     # Parameters for blocking
    setup_completion    # (internal) Indicates progress of setting-up MCMCSetup
    Ls                  # Vector of observation operators
    Σs                  # Vector of covariance matrices
    obs                 # Observations
    obs_times           # Recorded times of observations
    fpt                 # Vector with information about first-passage times
    dt                  # Granularity of the imputation grid
    τ                   # Time transformation for the imputation grid
    #TODO remember to change to collection
    t_kernel            # Collection of transition kernels for param updt step
    ρ                   # Memory parameter of the precond Crank-Nicolson scheme
    param_updt          # Flag for whether to update parameters at all
    updt_coord          # Collection indicating which coordinates to update
    updt_type           # Collection indicating types of parameter updates
    adaptive_prop       # Object for adapting guided proposals
    priors              # Priors over parameters
    x0_prior            # Prior over the starting position
    x0_guess            # Guess for a position of a starting point
    num_mcmc_steps      # Total number of steps of the mcmc sampler
    save_iter           # Save the path every ... iteration
    verb_iter           # Print progress message to console every ... iteration
    skip_for_save       # When saving path, thin the grid by a factor of ...
    warm_up             # Number of steps of the chain in which no param update is made
    pCN_readjust_param  # Parameters for readjustment of the pCN memory parameter
    θ_readjust_param    # Parameters for readjustment of the transition kernels for θ updt
    solver              # Type of ODE solver
    change_pt           # Change point between ODEs for M,L,μ and H,Hν,c
    Wnr                 # Definition of the driving Wiener law
    XX                  # Container for the path
    WW                  # Container for the driving noise
    P                   # Container for the guided proposals

    """
        MCMCSetup(P˟, P̃, ::ObsScheme)

    Initialise `MCMCSetup` with a given target law `P˟`, auxiliary laws `P̃` and
    observation scheme `ObsScheme`.
    """
    function MCMCSetup(P˟, P̃, ::ObsScheme) where ObsScheme <: AbstractObsScheme
        setup_completion = Dict(:obs => false,
                                :imput => false,
                                :tkern => false,
                                :prior => false,
                                :mcmc => false,
                                :solv => false)
        new{ObsScheme}(P˟, P̃, NoBlocking(), ([], 0.1, NoChangePt()),
                       setup_completion)
    end
end

"""
    set_observations!(setup::MCMCSetup, Ls, Σs, obs, obs_times,
                      fpt=fill(nothing, length(obs)-1))

Store observations to `setup`. The observations follow the scheme `V=LX+η`,
where V are the observations in `obs`, observed at times given in `obs_times`,
`L` are the observation operators given in `Ls`, `X` is the unobserved,
underlying diffusion and `η` is a Gaussian noise with mean `0` and covariance
`Σ` with the last stored in `Σs`. `fpt` provides additional information in case
the nature of observations has to do with first-passage times
"""
function set_observations!(setup::MCMCSetup, Ls, Σs, obs, obs_times,
                           fpt=fill(nothing, length(obs)-1))
    setup.Ls = Ls
    setup.Σs = Σs
    setup.obs = obs
    setup.obs_times = obs_times
    setup.fpt = fpt
    setup.setup_completion[:obs] = true
end

"""
    incomplete_message(::Val{:obs})

Print message to a console for incomplete step of setting observations
"""
function incomplete_message(::Val{:obs})
    print("\nThe observations have not been set up. Please call ",
          "set_observations!() passing the data on observations and the type ",
          "of observation scheme.\n")
end

"""
    set_imputation_grid!(setup::MCMCSetup, dt,
                         time_transf=(t₀,T) -> ((x) ->  t₀ + (x-t₀) * (2-(x-t₀)/(T-t₀))))

Define the imputation grid in `setup`. `dt` defines the granulatrity of the
imputation grid and `time_transf` defines a time transformation to use for
transforming equidistant grid.
"""
function set_imputation_grid!(setup::MCMCSetup, dt,
                              time_transf=(t₀,T) -> ((x) ->  t₀ + (x-t₀) * (2-(x-t₀)/(T-t₀))))
    setup.dt = dt
    setup.τ = time_transf
    setup.setup_completion[:imput] = true
end

"""
    incomplete_message(::Val{:imput})

Print message to a console for incomplete step of setting imputation grid
"""
function incomplete_message(::Val{:imput})
    print("\nThe imputation grid has not been set up. Please call ",
          "set_imputation_grid!() passing the `delta-t` and the time ",
          "transformation.\n")
end

"""
    set_transition_kernels!(setup::MCMCSetup, transition_kernel,
                            crank_nicolson_memory=0.0, param_updt::Bool=true,
                            updt_coord=(Val((true,)),),
                            updt_type=(MetropolisHastingsUpdt(),),
                            adaptive_proposals=NoAdaptation())

Store the transition kernels for parameter update steps as well path imputation
step in the `setup` object.
...
# Arguments
- `setup`: object to be set up
- `transition_kernel`: collection of transition kernels (one for each param updt)
- `crank_nicolson_memory`: memory parameter for random walk on a path space
- `param_updt`: flag for whether to update parameters at all
- `updt_coord`: collection indicating which coordinates to update
- `updt_type`: collection indicating types of parameter updates
- `adaptive_proposals`: object for adapting guided proposals
...
"""
function set_transition_kernels!(setup::MCMCSetup, transition_kernel,
                                 crank_nicolson_memory=0.0,
                                 param_updt::Bool=false,
                                 updt_coord=nothing,
                                 updt_type=(MetropolisHastingsUpdt(),),
                                 adaptive_proposals=NoAdaptation())
    setup.t_kernel = transition_kernel
    setup.ρ = crank_nicolson_memory
    setup.param_updt = param_updt
    setup.updt_coord = reformat_updt_coord(updt_coord, params(setup.P˟))
    setup.updt_type = updt_type
    @assert length(setup.updt_type) == length(setup.updt_coord)
    setup.adaptive_prop = adaptive_proposals
    setup.setup_completion[:tkern] = true
end

"""
    reformat_updt_coord(updt_coord::Nothing, θ)

Chosen not to update parameters, returned object is not important
"""
reformat_updt_coord(updt_coord::Nothing, θ) = (Val((true,)),)

IntContainer = Union{Number,NTuple{N,<:Integer},Vector{<:Integer}} where N
IntLongContainer = Union{NTuple{N,<:Integer},Vector{<:Integer}} where N
DoubleContainer = Union{Vector{<:IntLongContainer},NTuple{N,Tuple}} where N
"""
    reformat_updt_coord(updt_coord::S, θ) where S<:IntContainer

Single joint update of multiple parameters at once
"""
function reformat_updt_coord(updt_coord::S, θ) where S<:IntContainer
    (int_to_val(updt_coord, θ),)
end

"""
    reformat_updt_coord(updt_coord::S, θ) where S<:DoubleContainer

Multiple updates, reformat from indices to update to a tuple of Val{...}()
"""
function reformat_updt_coord(updt_coord::S, θ) where S<:DoubleContainer
    Tuple([int_to_val(uc, θ) for uc in updt_coord])
end

"""
    int_to_val(updt_coord::S, θ)

Transform from a container of indices to update to a
Val{tuple of true/false one-hot-encoding}()
"""
function int_to_val(updt_coord::S, θ) where S<:IntContainer
    @assert all([1 <= uc <= length(θ) for uc in updt_coord])
    Val{Tuple([i in updt_coord for i in 1:length(θ)])}()
end

"""
    reformat_updt_coord(updt_coord::Nothing, θ)

If the user does not use indices of coordinates to be updated it is assumed that
appropriate Val{(...)}() object is passed and nothing is done, use at your own risk
"""
reformat_updt_coord(updt_coord, θ) = updt_coord

"""
    incomplete_message(::Val{:tkern})

Print message to a console for incomplete step of setting transition kernels
"""
function incomplete_message(::Val{:tkern})
    print("\nThe transition kernels have not been set up. Please call ",
          "set_transition_kernels!() passing the information about transition ",
          "kernels for the parameters updates and path imputation.\n")
end

"""
    set_priors!(setup::MCMCSetup, priors, x0_prior, x0_guess=nothing)

Store the priors over parameters (in `priors`) and over the starting point (in
`x0_prior`) into the object `setup`.
"""
function set_priors!(setup::MCMCSetup, priors, x0_prior, x0_guess=nothing)
    setup.priors = priors
    setup.x0_prior = x0_prior
    setup.setup_completion[:prior] = true

    if x0_guess == nothing
        @assert typeof(x0_prior) <: KnownStartingPt
        x0_guess = start_pt(nothing, x0_prior)
    end
    setup.x0_guess = x0_guess
end

"""
    incomplete_message(::Val{:prior})

Print message to a console for incomplete step of setting priors
"""
function incomplete_message(::Val{:prior})
    print("\nThe priors have not been set up. Please call set_priors!() ",
          "passing the priors on the parameters and on the starting point.\n")
end

"""
    set_mcmc_params!(setup::MCMCSetup, num_mcmc_steps, save_iter=NaN,
                     verb_iter=NaN, skip_for_save=1, warm_up=0)

Define the parametrisation of the mcmc sampler.
...
# Arguments
- `setup`: object to be set up
- `num_mcmc_steps`: total number of steps of the mcmc sampler
- `save_iter`: save the path every ... iteration
- `verb_iter`: print progress message to console every ... iteration
- `skip_for_save`: when saving path, thin the grid by a factor of ...
- `uwarm_up`: number of steps of the chain in which no param update is made
...
"""
function set_mcmc_params!(setup::MCMCSetup, num_mcmc_steps, save_iter=NaN,
                          verb_iter=NaN, skip_for_save=1, warm_up=0,
                          pCN_readjust_param=(100, 0.1, 0.00001, 0.99999, 0.234, 50),
                          θ_readjust_param=(100, 0.1, -999, 999, 0.234, 50, (-1,), (-1,)))
    setup.num_mcmc_steps = num_mcmc_steps
    setup.save_iter = save_iter
    setup.verb_iter = verb_iter
    setup.skip_for_save = skip_for_save
    setup.warm_up = warm_up
    setup.pCN_readjust_param = named_pCN(pCN_readjust_param)
    setup.θ_readjust_param = named_θparam(θ_readjust_param)
    setup.setup_completion[:mcmc] = true
end

function named_pCN(pCN)
    @assert length(pCN) == 6
    (step=pCN[1], scale=pCN[2], minδ=pCN[3], maxρ=pCN[4], trgt=pCN[5], offset=pCN[6])
end

function named_θparam(θ_rp)
    @assert length(θ_rp) == 8
    (step=θ_rp[1], scale=θ_rp[2], minδ=θ_rp[3], maxδ=θ_rp[4], trgt=θ_rp[5],
     offset=θ_rp[6], idx_MH=θ_rp[7], idx_θ=θ_rp[8])
end

"""
    incomplete_message(::Val{:mcmc})

Print message to a console for incomplete step of setting mcmc parametrisation
"""
function incomplete_message(::Val{:mcmc})
    print("\nThe parameters for MCMC have not been set up. Please call ",
          "set_mcmc_params!() passing the auxiliary parameters required ",
          "to set up the mcmc chain.\n")
end

"""
    set_blocking!(setup::MCMCSetup, blocking::Blocking=NoBlocking(),
                  blocking_params=([], 0.1, NoChangePt()))

Store information about blocking in `setup`. `blocking` indicates the type of
blocking and `blocking_params` passes additional necessary parameters
"""
function set_blocking!(setup::MCMCSetup, blocking::Blocking=NoBlocking(),
                       blocking_params=([], 0.1, NoChangePt())
                       ) where {Blocking <: BlockingSchedule}
    setup.blocking = blocking
    setup.blocking_params = blocking_params
end

"""
    set_solver!(setup::MCMCSetup, solver=Ralston3(), change_pt=NoChangePt())

Define the ODE solvers in `setup`. `solver` defines the type of ODE solver and
`change_pt` gives information about the change point for switching between the
solvers for M,L,μ and H,Hν,c
"""
function set_solver!(setup::MCMCSetup, solver=Ralston3(),
                     change_pt=NoChangePt())
    setup.solver = solver
    setup.change_pt = change_pt
    setup.setup_completion[:solv] = true
end

"""
    incomplete_message(::Val{:solv})

Print message to a console for incomplete step of setting ODE solvers
"""
function incomplete_message(::Val{:solv})
    print("\nThe types of solvers have not been determined. Please call ",
          "set_solver!() to choose the ODE solver.\n")
end

"""
    check_if_complete(setup::MCMCSetup, labels=keys(setup.setup_completion))

Check if all the set-up steps listed in `labels` have been finalised in `setup`
"""
function check_if_complete(setup::MCMCSetup,
                           labels=keys(setup.setup_completion))
    complete = true
    for label in labels
        if !setup.setup_completion[label]
            incomplete_message(Val(label))
            complete = false
        end
    end
    complete
end

"""
    determine_data_type(setup::MCMCSetup)

Determine the data type of the containers with path and driving noise
"""
function determine_data_type(setup::MCMCSetup)
    check_if_complete(setup, [:prior]) || throw(UndefRefError())
    x = setup.x0_guess
    drift = b(0.0, x, setup.P˟)
    vola = σ(0.0, x, setup.P˟)
    # @assert typeof(x) == typeof(drift) # maybe this assertion is too strong
    typeof(drift), typeof(vola' * drift)
end

"""
    prepare_containers!(setup::MCMCSetup)

Set-up the containers for paths, driving noise, observations, observation noise
and observation operators, automatically choosing an appropriate data type based
on the return values of the drift and volatility coefficients
"""
function prepare_containers!(setup::MCMCSetup)
    T, S = determine_data_type(setup)
    Wnr = Wiener{S}()
    TW = typeof(sample([0], Wnr))
    TX = typeof(SamplePath([], zeros(T, 0)))

    # TODO modify so that it will work with GPUarrays
    m = length(setup.obs)-1
    WW = Vector{TW}(undef,m)
    XX = Vector{TX}(undef,m)

    setup.Wnr = Wnr
    setup.WW = WW
    setup.XX = XX

    prepare_obs_containers!(T, setup)
end

"""
    prepare_obs_containers!(::Type{T}, setup::MCMCSetup)

Define containers for the observations, observation noise and observation
operators based on the passed data-type
"""
function prepare_obs_containers!(::Type{T}, setup::MCMCSetup) where T <: Number
    correct_data_type!(setup, T, T)
end

"""
    prepare_obs_containers!(::Type{T}, setup::MCMCSetup)

Define containers for the observations, observation noise and observation
operators based on the passed data-type
"""
function prepare_obs_containers!(::Type{T}, setup::MCMCSetup) where T <: Array
    correct_data_type!(setup, Array, Array)
end

"""
    prepare_obs_containers!(::Type{T}, setup::MCMCSetup)

Define containers for the observations, observation noise and observation
operators based on the passed data-type
"""
function prepare_obs_containers!(::Type{T}, setup::MCMCSetup) where T <:SArray
    f(x) = SMatrix{_dim(x)...}(x)

    g(x, ::Val{0}=Val{ndims(x)}()) = SVector{1}(x)
    g(x, ::Val{1}=Val{ndims(x)}()) = SVector{size(x)...}(x)
    g(x, ::Val{2}=Val{ndims(x)}()) = SMatrix{size(x)...}(x)

    correct_data_type!(setup, f, g)
end

"""
    correct_data_type!(setup::MCMCSetup, f, g)

Transform the elements of observations, observation noise and observation
operators collections stored in `setup` according to functions `f` and `g`
"""
function correct_data_type!(setup::MCMCSetup, f, g)
    setup.Σs = map(f, setup.Σs)
    setup.Ls = map(f, setup.Ls)
    setup.obs = map(g, setup.obs)
end

"""
    _dim(mat::T) where T <: Number

Size of a matrix corresponding to a scalar is (1,1)
"""
_dim(mat::T) where T <: Number = 1, 1

"""
    _dim(mat::T) where T <: Union{Array, SArray}

Size of a matrix corresponding to an element `mat`
"""
_dim(mat::T) where T <: Union{Array, SArray} = _dim(mat, Val{ndims(mat)}())

"""
    _dim(mat, ::Val{1})
Size of a matrix corresponding to a vector is (vector length, 1)
"""
_dim(mat, ::Val{1}) = size(mat)[1], 1

"""
    _dim(mat, ::Val{1})
Size of a matrix corresponding to a matrix `mat` is just size(mat)
"""
_dim(mat, ::Val{2}) = size(mat)

"""
    _dim(mat, ::T) where T
Size of a matrix corresponding to tensor with dimension larger than matrix is
undefined
"""
_dim(mat, ::T) where T = throw(ArgumentError())

function _build_time_grid(τ, dt, t0, T)
    num_pts = Int64(ceil((T-t0)/dt))+1
    tt = τ(t0, T).( range(t0, stop=T, length=num_pts) )
    tt
end

 #TODO remove this K, it's most likely not needed
"""
    find_proposal_law!(::Type{K}, setup::MCMCSetup) where K

Initialise the object with proposal law and all the necessary containers needed
for the simulation of the guided proposals
"""
function find_proposal_law!(::Type{K}, setup::MCMCSetup) where K
    xx, tt, P˟, P̃ = setup.obs, setup.obs_times, setup.P˟, setup.P̃
    Ls, Σs, solver, dt, τ = setup.Ls, setup.Σs, setup.solver, setup.dt, setup.τ
    change_pt = typeof(setup.change_pt)(get_change_pt(setup.blocking_params[3]))

    m = length(xx)-1
    t = _build_time_grid(τ, dt, tt[m], tt[m+1])
    P = GuidPropBridge(K, t, P˟, P̃[m], Ls[m], xx[m+1], Σs[m];
                       change_pt=change_pt, solver=solver)
    #P = Array{ContinuousTimeProcess,1}(undef,m)
    Ps = [deepcopy(P)]

    for i in m-1:-1:1
        t = _build_time_grid(τ, dt, tt[i], tt[i+1])
        P = GuidPropBridge(K, t, P˟, P̃[i], Ls[i], xx[i+1], Σs[i], P.H[1],
                           P.Hν[1], P.c[1]; change_pt=change_pt, solver=solver)
        push!(Ps, deepcopy(P))
    end
    setup.P = reverse(Ps)
end

"""
    initialise(::Type{K}, setup::MCMCSetup; verbose=false)
Initialise the internal containers of `setup`. Check if all the necessary data
has been passed to `setup`
"""
function initialise!(::Type{K}, setup::MCMCSetup, verbose=false) where K
    verbose && print("Initialising MCMC setup...\nPreparing containers...\n")
    prepare_containers!(setup)
    verbose && print("Initialising proposal laws...\n")
    find_proposal_law!(K, setup) #TODO remove this K, it's most likely not needed
    #TODO initialise for computation of gradients
end

"""
    check_if_recompute_ODEs(setup::MCMCSetup)

Utility function for checking if H,Hν,c need to be re-computed for a respective
parameter update
"""
function check_if_recompute_ODEs(setup::MCMCSetup)
    [any([e in depends_on_params(setup.P[1].Pt) for e
          in idx(uc)]) for uc in setup.updt_coord]
end

#NOTE code to adapt:
#θ = params(P[1].Target)
#ϑs = [[θ[j] for j in idx(uc)] for uc in updtCoord]
#result = [DiffResults.GradientResult(ϑ) for ϑ in ϑs]
#resultᵒ = [DiffResults.GradientResult(ϑ) for ϑ in ϑs]
#Q = eltype(result)


"""
```
type AltPolicy
```

Type defining an alternative policy rule.

### Fields

- `key::Symbol`: alternative policy identifier

- `eqcond::Function`: a version of `DSGE.eqcond` which computes the equilibrium
  condition matrices under the alternative policy. Like `eqcond`, it should take
  in one argument of type `AbstractModel` and return the `Γ0`, `Γ1`, `C`, `Ψ`,
  and `Π` matrices.

- `solve::Function`: a version of `DSGE.solve` which solves the model under the
  alternative policy. Like `DSGE.solve`, it should take in one argument of type
  `AbstractModel` and return the `TTT`, `RRR`, and `CCC` matrices.

- `forecast_init::Function`: a function that initializes forecasts under the
  alternative policy rule. Specifically, it accepts a model, an `nshocks` x
  `n_forecast_periods` matrix of shocks to be applied in the forecast, and a
  vector of initial states for the forecast. It must return a new matrix of
  shocks and a new initial state vector. If no adjustments to shocks or initial
  state vectors are necessary under the policy rule, this field may be omitted.

- `color::Colorant`: color to plot this alternative policy in. Defaults to blue.

- `linestyle::Symbol`: line style for forecast plots under this alternative
  policy. See options from `Plots.jl`. Defaults to `:solid`.

"""
type AltPolicy
    key::Symbol
    eqcond::Function
    solve::Function
    forecast_init::Function
    color::Colorant
    linestyle::Symbol
end

function AltPolicy(key::Symbol, eqcond_fcn::Function, solve_fcn::Function;
                   forecast_init::Function = identity,
                   color::Colorant = RGB(0., 0., 1.),
                   linestyle::Symbol = :solid)

    AltPolicy(key, eqcond_fcn, solve_fcn, forecast_init, color, linestyle)
end

Base.string(a::AltPolicy) = string(a.key)


using AbInitioSoftwareBase: parentdir
using AbInitioSoftwareBase.Inputs: Setter
using Crystallography: MonkhorstPackGrid
using Dates: format, now
using QuantumESPRESSO.Commands: pw
using QuantumESPRESSO.Inputs.PWscf: KMeshCard, PWInput, VerbositySetter
using Setfield: @set!
using UnifiedPseudopotentialFormat  # To work with `download_potential`
using Unitful: ustrip, @u_str
using UnitfulAtomic

import Express.ConvergenceTestWorkflow: MakeInput, RunCmd

(::MakeInput)(template::PWInput, args...) = (customizer(args...) ∘ normalizer())(template)

struct CutoffEnergySetter <: Setter
    wfc::Number
end
function (x::CutoffEnergySetter)(template::PWInput)
    @set! template.system.ecutwfc = ustrip(u"Ry", x.wfc)
    return template
end

normalizer() = VerbositySetter("high")

struct OutdirSetter <: Setter
    timefmt::String
end
function (x::OutdirSetter)(template::PWInput)
    @set! template.control.outdir = abspath(
        joinpath(
            template.control.outdir,
            join((template.control.prefix, format(now(), x.timefmt), rand(UInt)), '_'),
        ),
    )
    mkpath(template.control.outdir)
    return template
end

struct MonkhorstPackGridSetter <: Setter
    mesh::Vector{Int}
    shift::Vector{Int}
end
function (x::MonkhorstPackGridSetter)(template::PWInput)
    @set! template.k_points = KMeshCard(MonkhorstPackGrid(x.mesh, x.shift))
    return template
end

customizer(mesh, shift, timefmt = "Y-m-d_H:M:S") =
    OutdirSetter(timefmt) ∘ MonkhorstPackGridSetter(mesh, shift)
customizer(energy::Number, timefmt::AbstractString = "Y-m-d_H:M:S") =
    OutdirSetter(timefmt) ∘ CutoffEnergySetter(energy)

(x::RunCmd)(input, output = mktemp(parentdir(input))[1]; kwargs...) =
    pw(input, output; kwargs...)


abstract type AbstractGrid{N,T,Names} end
struct LatticeGrid{N} end
struct ParticleGrid end

include("latticegrids.jl")
include("particlegrids.jl")
include("abstractgrid.jl")
include("utils.jl")
include("units.jl")


#Run this in Escher to see a rotating cube in the browser.

import ThreeJS
using FileIO

main(window) =  begin
    push!(window.assets,("ThreeJS","threejs"))
    push!(window.assets,"widgets")
    x = Signal(1.0)
    y = Signal(1.0)
    z = Signal(1.0)
    mesh_geom = load("cat.obj")
    vbox(
        "ThreeJS Example",
        vskip(2em),
        vbox(
            "Scale",
            hbox("x",slider(1.0:5.0) >>> x),
            hbox("y",slider(1.0:5.0) >>> y),
            hbox("z",slider(1.0:5.0) >>> z),
        ),
        vskip(2em),
        map(x,y,z) do x,y,z
        ThreeJS.outerdiv() <<
            (ThreeJS.initscene() <<
                [
                    ThreeJS.mesh(0.0, 0.0, 0.0) <<
                    [
                        ThreeJS.geometry(mesh_geom, scale=(x,y,z)), ThreeJS.material(Dict(:kind=>"lambert",:color=>"red"))
                    ],
                    ThreeJS.pointlight(10.0, 10.0, 10.0),
                    ThreeJS.camera(2.0, 2.0, 2.0)
                ]
            )
        end
    ) |> pad(2em)
end