precedence(op::Int) = op < Tokens.end_assignments ?  AssignmentOp :
                       op < Tokens.end_pairarrow ? 2 :
                       op < Tokens.end_conditional ? ConditionalOp :
                       op < Tokens.end_arrow ?       ArrowOp :
                       op < Tokens.end_lazyor ?      LazyOrOp :
                       op < Tokens.end_lazyand ?     LazyAndOp :
                       op < Tokens.end_comparison ?  ComparisonOp :
                       op < Tokens.end_pipe ?        PipeOp :
                       op < Tokens.end_colon ?       ColonOp :
                       op < Tokens.end_plus ?        PlusOp :
                       op < Tokens.end_bitshifts ?   BitShiftOp :
                       op < Tokens.end_times ?       TimesOp :
                       op < Tokens.end_rational ?    RationalOp :
                       op < Tokens.end_power ?       PowerOp :
                       op < Tokens.end_decl ?        DeclarationOp :
                       op < Tokens.end_where ?       WhereOp : DotOp

precedence(kind::Tokens.Kind) = kind == Tokens.DDDOT ? DddotOp :
                        kind < Tokens.begin_assignments ? 0 :
                        kind < Tokens.end_assignments ?   AssignmentOp :
                        kind < Tokens.end_pairarrow ?   2 :
                       kind < Tokens.end_conditional ?    ConditionalOp :
                       kind < Tokens.end_arrow ?          ArrowOp :
                       kind < Tokens.end_lazyor ?         LazyOrOp :
                       kind < Tokens.end_lazyand ?        LazyAndOp :
                       kind < Tokens.end_comparison ?     ComparisonOp :
                       kind < Tokens.end_pipe ?           PipeOp :
                       kind < Tokens.end_colon ?          ColonOp :
                       kind < Tokens.end_plus ?           PlusOp :
                       kind < Tokens.end_bitshifts ?      BitShiftOp :
                       kind < Tokens.end_times ?          TimesOp :
                       kind < Tokens.end_rational ?       RationalOp :
                       kind < Tokens.end_power ?          PowerOp :
                       kind < Tokens.end_decl ?           DeclarationOp :
                       kind < Tokens.end_where ?          WhereOp :
                       kind < Tokens.end_dot ?            DotOp :
                       kind == Tokens.ANON_FUNC ? AnonFuncOp :
                       kind == Tokens.PRIME ?             PrimeOp : 20

precedence(x) = 0
precedence(x::AbstractToken) = precedence(x.kind)
precedence(x::OPERATOR) = precedence(x.kind)

isunaryop(op) = false
isunaryop(op::OPERATOR) = isunaryop(op.kind)
isunaryop(t::AbstractToken) = isunaryop(t.kind)
isunaryop(kind::Tokens.Kind) = kind == Tokens.ISSUBTYPE ||
                  kind == Tokens.ISSUPERTYPE ||
                  kind == Tokens.PLUS ||
                  kind == Tokens.MINUS ||
                  kind == Tokens.NOT ||
                  kind == Tokens.APPROX ||
                  kind == Tokens.NOT_SIGN ||
                  kind == Tokens.AND ||
                  kind == Tokens.SQUARE_ROOT ||
                  kind == Tokens.CUBE_ROOT ||
                  kind == Tokens.QUAD_ROOT ||
                  kind == Tokens.DECLARATION ||
                  kind == Tokens.EX_OR ||
                  kind == Tokens.COLON

isunaryandbinaryop(t) = false
isunaryandbinaryop(t::AbstractToken) = isunaryandbinaryop(t.kind)
isunaryandbinaryop(kind::Tokens.Kind) = kind == Tokens.PLUS ||
                           kind == Tokens.MINUS ||
                           kind == Tokens.EX_OR ||
                           kind == Tokens.ISSUBTYPE ||
                           kind == Tokens.ISSUPERTYPE ||
                           kind == Tokens.AND ||
                           kind == Tokens.APPROX ||
                           kind == Tokens.DECLARATION ||
                           kind == Tokens.COLON

isbinaryop(op) = false
isbinaryop(op::OPERATOR) = isbinaryop(op.kind)
isbinaryop(t::AbstractToken) = isbinaryop(t.kind)
isbinaryop(kind::Tokens.Kind) = isoperator(kind) &&
                    !(kind == Tokens.SQUARE_ROOT ||
                    kind == Tokens.CUBE_ROOT ||
                    kind == Tokens.QUAD_ROOT ||
                    kind == Tokens.NOT ||
                    kind == Tokens.NOT_SIGN)

isassignment(t::AbstractToken) = Tokens.begin_assignments < t.kind < Tokens.end_assignments

function non_dotted_op(t::AbstractToken)
    k = t.kind
    return (k == Tokens.COLON_EQ ||
            k == Tokens.PAIR_ARROW ||
            k == Tokens.EX_OR_EQ ||
            k == Tokens.CONDITIONAL ||
            k == Tokens.LAZY_OR ||
            k == Tokens.LAZY_AND ||
            k == Tokens.ISSUBTYPE ||
            k == Tokens.ISSUPERTYPE ||
            k == Tokens.LPIPE ||
            k == Tokens.RPIPE ||
            k == Tokens.EX_OR ||
            k == Tokens.COLON ||
            k == Tokens.DECLARATION ||
            k == Tokens.IN ||
            k == Tokens.ISA ||
            k == Tokens.WHERE ||
            (isunaryop(k) && !isbinaryop(k) && !(k == Tokens.NOT)))
end

issyntaxcall(op) = false
function issyntaxcall(op::OPERATOR)
    K = op.kind
    P = precedence(K)
    P == AssignmentOp && !(K == Tokens.APPROX || K == Tokens.PAIR_ARROW) ||
    K == Tokens.RIGHT_ARROW ||
    P == LazyOrOp ||
    P == LazyAndOp ||
    K == Tokens.ISSUBTYPE ||
    K == Tokens.ISSUPERTYPE ||
    K == Tokens.COLON ||
    K == Tokens.DECLARATION ||
    K == Tokens.DOT ||
    K == Tokens.DDDOT ||
    K == Tokens.PRIME ||
    K == Tokens.WHERE
end

issyntaxunarycall(op) = false
function issyntaxunarycall(op::OPERATOR)
    K = op.kind
    !op.dot && (K == Tokens.EX_OR ||
    K == Tokens.AND ||
    K == Tokens.DECLARATION ||
    K == Tokens.ISSUBTYPE ||
    K == Tokens.ISSUPERTYPE)
end

LtoR(prec::Int) = AssignmentOp ≤ prec ≤ LazyAndOp || prec == PowerOp

"""
    parse_unary(ps)

Having hit a unary operator at the start of an expression return a call.
"""
function parse_unary(ps::ParseState, op::OPERATOR)
    K,dot = op.kind, op.dot
    if op isa OPERATOR && op.kind == Tokens.COLON
        ret = parse_unary_colon(ps, op)
    elseif (is_plus(op) || is_minus(op)) && (ps.nt.kind == Tokens.INTEGER || ps.nt.kind == Tokens.FLOAT) && isemptyws(ps.ws) && ps.nnt.kind!=Tokens.CIRCUMFLEX_ACCENT
        arg = LITERAL(next(ps))
        ret = LITERAL(op.fullspan + arg.fullspan, (op.fullspan + arg.span), string(is_plus(op) ? "+" : "-" , val(ps.t, ps)), ps.t.kind)
    else
        P = precedence(K)
        prec = P == DeclarationOp ? DeclarationOp :
                    K == Tokens.AND ? DeclarationOp :
                    K == Tokens.EX_OR ? 20 : PowerOp
        arg = @closer ps unary @precedence ps prec parse_expression(ps)
        if issyntaxunarycall(op)
            ret = UnarySyntaxOpCall(op, arg)
        else
            ret = UnaryOpCall(op, arg)
        end
    end

    return ret
end

function parse_unary_colon(ps::ParseState, op::OPERATOR)
    if Tokens.begin_keywords < ps.nt.kind < Tokens.end_keywords
        ret = EXPR{Quotenode}(Any[op, IDENTIFIER(next(ps))])
    elseif Tokens.begin_literal < ps.nt.kind < Tokens.end_literal ||
        isoperator(ps.nt.kind) || ps.nt.kind == Tokens.IDENTIFIER
        ret = EXPR{Quotenode}(Any[op, INSTANCE(next(ps))])
    elseif closer(ps)
        ret = op
    else
        arg = @precedence ps 20 parse_expression(ps)
        ret = EXPR{Quote}(Any[op, arg])
    end
    return ret
end

function parse_operator_eq(ps::ParseState, @nospecialize(ret), op)
    nextarg = @precedence ps AssignmentOp - LtoR(AssignmentOp) parse_expression(ps)

    if is_func_call(ret) && !(nextarg isa EXPR{Begin} || (nextarg isa EXPR{InvisBrackets} && nextarg.args[2] isa EXPR{Block}))
        nextarg = EXPR{Block}(Any[nextarg])
    end
    return BinarySyntaxOpCall(ret, op, nextarg)
end

# Parse conditionals
function parse_operator_cond(ps::ParseState, @nospecialize(ret), op)
    nextarg = @closer ps ifop parse_expression(ps)
    op2 = OPERATOR(next(ps))
    nextarg2 = @closer ps comma @precedence ps 0 parse_expression(ps)

    return ConditionalOpCall(ret, op, nextarg, op2, nextarg2)
end

# Parse comparisons
function parse_comp_operator(ps::ParseState, @nospecialize(ret), op)
    nextarg = @precedence ps ComparisonOp - LtoR(ComparisonOp) parse_expression(ps)

    if ret isa EXPR{Comparison}
        push!(ret, op)
        push!(ret, nextarg)
    elseif ret isa BinaryOpCall && precedence(ret.op) == ComparisonOp
        ret = EXPR{Comparison}(Any[ret.arg1, ret.op, ret.arg2, op, nextarg])
    elseif ret isa BinarySyntaxOpCall && (is_issubt(ret.op) || is_issupt(ret.op))
        ret = EXPR{Comparison}(Any[ret.arg1, ret.op, ret.arg2, op, nextarg])
    elseif (is_issubt(op) || is_issupt(op))
        ret = BinarySyntaxOpCall(ret, op, nextarg)
    else
        ret = BinaryOpCall(ret, op, nextarg)
    end
    return ret
end

# Parse ranges
function parse_operator_colon(ps::ParseState, @nospecialize(ret), op)
    nextarg = @precedence ps ColonOp - LtoR(ColonOp) parse_expression(ps)

    if ret isa BinaryOpCall && is_colon(ret.op)
        ret = EXPR{ColonOpCall}(Any[ret.arg1, ret.op, ret.arg2, op, nextarg])
    else
        ret = BinaryOpCall(ret, op, nextarg)
    end
    return ret
end

# Parse power (special case for preceding unary ops)
function parse_operator_power(ps::ParseState, @nospecialize(ret), op)
    nextarg = @precedence ps PowerOp - LtoR(PowerOp) @closer ps inwhere parse_expression(ps)

    if ret isa UnaryOpCall
        nextarg = BinaryOpCall(ret.arg, op, nextarg)
        ret = UnaryOpCall(ret.op, nextarg)
    else
        ret = BinaryOpCall(ret, op, nextarg)
    end
    return ret
end

# parse where
function parse_operator_where(ps::ParseState, @nospecialize(ret), op)
    nextarg = @precedence ps LazyAndOp @closer ps inwhere parse_expression(ps)

    if nextarg isa EXPR{Braces}
        args = nextarg.args
    else
        args = Any[nextarg]
    end
    return WhereOpCall(ret, op, args)
end

function parse_operator_dot(ps::ParseState, @nospecialize(ret), op)
    if ps.nt.kind == Tokens.LPAREN
        @static if VERSION > v"1.1-"
            iserred = ps.ws.kind != Tokens.EMPTY_WS
            sig = @default ps parse_call(ps, ret)
            nextarg = EXPR{TupleH}(sig.args[2:end])
            if iserred
                nextarg = ErrorToken(nextarg)
            end
        else
            sig = @default ps parse_call(ps, ret)
            nextarg = EXPR{TupleH}(sig.args[2:end])
        end
    elseif iskw(ps.nt) || ps.nt.kind == Tokens.IN || ps.nt.kind == Tokens.ISA || ps.nt.kind == Tokens.WHERE
        nextarg = IDENTIFIER(next(ps))
    elseif ps.nt.kind == Tokens.COLON
        op2 = OPERATOR(next(ps))
        if ps.nt.kind == Tokens.LPAREN
            nextarg = @closeparen ps @precedence ps DotOp - LtoR(DotOp) parse_expression(ps)
            nextarg = EXPR{Quote}(Any[op2, nextarg])
        else
            nextarg = @precedence ps DotOp - LtoR(DotOp) parse_unary(ps, op2)
        end
    elseif ps.nt.kind == Tokens.EX_OR && ps.nnt.kind == Tokens.LPAREN
        op2 = OPERATOR(next(ps))
        nextarg = parse_call(ps, op2)
    else
        nextarg = @precedence ps DotOp - LtoR(DotOp) parse_expression(ps)
    end

    if nextarg isa IDENTIFIER || nextarg isa EXPR{Vect} || (nextarg isa UnarySyntaxOpCall && is_exor(nextarg.arg1))
        ret = BinarySyntaxOpCall(ret, op, Quotenode(nextarg))
    elseif nextarg isa EXPR{MacroCall}
        mname = BinarySyntaxOpCall(ret, op, Quotenode(nextarg.args[1]))
        ret = EXPR{MacroCall}(Any[mname])
        for i = 2:length(nextarg.args)
            push!(ret, nextarg.args[i])
        end
    else
        ret = BinarySyntaxOpCall(ret, op, nextarg)
    end
    return ret
end

function parse_operator_anon_func(ps::ParseState, @nospecialize(ret), op)
    arg = @closer ps comma @precedence ps 0 parse_expression(ps)

    if !(arg isa EXPR{Begin} || (arg isa EXPR{InvisBrackets} && arg.args[2] isa EXPR{Block}))
        arg = EXPR{Block}(Any[arg])
    end
    return BinarySyntaxOpCall(ret, op, arg)
end

function parse_operator(ps::ParseState, @nospecialize(ret), op)
    K,dot = op.kind, op.dot
    P = precedence(K)

    if ret isa EXPR{ChainOpCall} && (is_star(op) || is_plus(op)) && op.kind == ret.args[2].kind
        nextarg = @precedence ps P - LtoR(P) parse_expression(ps)
        push!(ret, op)
        push!(ret, nextarg)
        ret = ret
    elseif ret isa BinaryOpCall && (is_star(op) || is_plus(op)) && op.kind == ret.op.kind && !ret.op.dot && ret.op.span > 0
        nextarg = @precedence ps P - LtoR(P) parse_expression(ps)
        ret = EXPR{ChainOpCall}(Any[ret.arg1, ret.op, ret.arg2, op, nextarg])
    elseif is_eq(op)
        ret = parse_operator_eq(ps, ret, op)
    elseif is_cond(op)
        ret = parse_operator_cond(ps, ret, op)
    elseif is_colon(op)
        ret = parse_operator_colon(ps, ret, op)
    elseif is_where(op)
        ret = parse_operator_where(ps, ret, op)
    elseif is_anon_func(op)
        ret = parse_operator_anon_func(ps, ret, op)
    elseif is_dot(op)
        ret = parse_operator_dot(ps, ret, op)
    elseif is_dddot(op) || is_prime(op)
        ret = UnarySyntaxOpCall(ret, op)
    elseif P == ComparisonOp
        ret = parse_comp_operator(ps, ret, op)
    elseif P == PowerOp
        ret = parse_operator_power(ps, ret, op)
    else
        ltor = K == Tokens.LPIPE ? true : LtoR(P)
        nextarg = @precedence ps P - ltor parse_expression(ps)

        if issyntaxcall(op)
            ret = BinarySyntaxOpCall(ret, op, nextarg)
        else
            ret = BinaryOpCall(ret, op, nextarg)
        end
    end
    return ret
end


mutable struct UnfoldUpdateState{T,U}
    init_state::T
    weight::Float64
    score::Float64
    noise::Float64
    subtraces::PersistentVector{U}
    retval::PersistentVector{T}
    discard::DynamicChoiceMap
    num_nonempty::Int
    updated_retdiffs::Dict{Int,Diff}
end

function process_retained!(gen_fn::Unfold{T,U}, params::Tuple,
                           choices::ChoiceMap, key::Int, kernel_argdiffs::Tuple,
                           state::UnfoldUpdateState{T,U}) where {T,U}
    local subtrace::U
    local prev_subtrace::U
    local prev_state::T
    local new_state::T

    submap = get_submap(choices, key)
    prev_state = (key == 1) ? state.init_state : state.retval[key-1]
    kernel_args = (key, prev_state, params...)

    # get new subtrace with recursive call to update()
    prev_subtrace = state.subtraces[key]
    (subtrace, weight, retdiff, discard) = update(
        prev_subtrace, kernel_args, kernel_argdiffs, submap)

    # retrieve retdiff
    if retdiff != NoChange()
        state.updated_retdiffs[key] = retdiff
    end

    # update state
    state.weight += weight
    set_submap!(state.discard, key, discard)
    state.score += (get_score(subtrace) - get_score(prev_subtrace))
    state.noise += (project(subtrace, EmptySelection()) - project(subtrace, EmptySelection()))
    state.subtraces = assoc(state.subtraces, key, subtrace)
    new_state = get_retval(subtrace)
    state.retval = assoc(state.retval, key, new_state)
    subtrace_empty = isempty(get_choices(subtrace))
    prev_subtrace_empty = isempty(get_choices(prev_subtrace))
    if !subtrace_empty && prev_subtrace_empty
        state.num_nonempty += 1
    elseif subtrace_empty && !prev_subtrace_empty
        state.num_nonempty -= 1
    end

    retdiff
end

function process_new!(gen_fn::Unfold{T,U}, params::Tuple, choices, key::Int,
                      state::UnfoldUpdateState{T,U}) where {T,U}
    local subtrace::U
    local prev_state::T
    local new_state::T

    submap = get_submap(choices, key)
    prev_state = (key == 1) ? state.init_state : state.retval[key-1]
    kernel_args = (key, prev_state, params...)

    # get subtrace and weight
    (subtrace, weight) = generate(gen_fn.kernel, kernel_args, submap)

    # update state
    state.weight += weight
    state.score += get_score(subtrace)
    new_state = get_retval(subtrace)
    @assert key > length(state.subtraces)
    state.subtraces = push(state.subtraces, subtrace)
    state.retval = push(state.retval, new_state)
    @assert length(state.subtraces) == key
    if !isempty(get_choices(subtrace))
        state.num_nonempty += 1
    end
end

function update(trace::VectorTrace{UnfoldType,T,U},
                args::Tuple, argdiffs::Tuple,
                choices::ChoiceMap) where {T,U}
    gen_fn = trace.gen_fn
    (new_length, init_state, params) = unpack_args(args)
    check_length(new_length)
    prev_args = get_args(trace)
    prev_length = prev_args[1]
    retained_and_constrained = get_retained_and_constrained(choices, prev_length, new_length)

    # handle removed applications
    (discard, num_nonempty, score_decrement, noise_decrement) = vector_update_delete(
        new_length, prev_length, trace)
    (subtraces, retval) = vector_remove_deleted_applications(
        trace.subtraces, trace.retval, prev_length, new_length)
    score = trace.score - score_decrement
    noise = trace.noise - noise_decrement

    # handle retained and new applications
    state = UnfoldUpdateState{T,U}(init_state, -score_decrement, score, noise,
        subtraces, retval, discard, num_nonempty, Dict{Int,Diff}())
    process_all_retained!(gen_fn, params, argdiffs, choices, prev_length, new_length,
                          retained_and_constrained, state)
    process_all_new!(gen_fn, params, choices, prev_length, new_length, state)

    # retdiff
    retdiff = vector_compute_retdiff(state.updated_retdiffs, new_length, prev_length)

    # new trace
    new_trace = VectorTrace{UnfoldType,T,U}(gen_fn, state.subtraces, state.retval, args,
        state.score, state.noise, new_length, state.num_nonempty)

    (new_trace, state.weight, retdiff, discard)
end


@testset "Problems" begin
    @testset "Can create fixed dimensional problem with search space as array" begin
        p = BlackBoxOptim.fixeddim_problem((x)->x; search_space = [(0.0, 1.0)])
        @test typeof(p) === BlackBoxOptim.FixedDimProblem
    end

    @testset "Exception if no valid search space supplied when creating fixed dimensional problem" begin
        @test_throws BlackBoxOptim.fixeddim_problem((x)->x; search_space = (0.0, 1.0))
    end

end


@testset "DeepONet Training Accuracy" begin
    ϵ = Burgers.train_don()

    @test ϵ < 0.4
end


# ------------------------------------------------------------------------------
#                              VML SERVICE FUNCTION
# ------------------------------------------------------------------------------

if debug println("MKL : VML SERVICE") end
for fname in (  :vml_get_mode, :vml_set_mode,
                :vml_get_accuracy, :vml_set_accuracy,
                :vml_check_error)

                if debug println("export : ", string(fname)) end
                @eval export $fname
end

# ------------------------------------------------------------------------------

# credit : Simon Kornblith (VML.jl)
immutable VMLAccuracy
    mode::UInt
end

const VML_LA = VMLAccuracy(0x00000001)
const VML_HA = VMLAccuracy(0x00000002)
const VML_EP = VMLAccuracy(0x00000003)

Base.show(io::IO, m::VMLAccuracy) = print(io, m == VML_LA ? "VML_LA" :
                                              m == VML_HA ? "VML_HA" : "VML_EP")

vml_get_mode() = ccall((:_vmlGetMode, libvml), Cuint, ())
vml_set_mode(mode::Integer) = (ccall((:_vmlSetMode, libvml), Cuint, (UInt,), mode); nothing)

vml_set_accuracy(m::VMLAccuracy) = vml_set_mode((vml_get_mode() & ~0x03) | m.mode)
vml_get_accuracy() = VMLAccuracy(vml_get_mode() & 0x3)

vml_set_mode((vml_get_mode() & ~0x0000FF00))

function vml_check_error()
    vml_error = ccall((:_vmlClearErrStatus, libvml), Cint, ())
    if vml_error != 0
        if vml_error == 1
            error(DomainError())
        elseif vml_error == 2 || vml_error == 3 || vml_error == 4
            # Singularity, overflow, or underflow
            # I don't think Base throws on these
        elseif vml_error == 1000
            warn("VML does not support $(vml_get_accuracy); lower accuracy used instead")
        else
            error("an unexpected error occurred in VML ($vml_error)")
        end
    end
end

#=
/* file: mkl_vml_defines.h */
/*
//  Copyright(C) 2006-2011 Intel Corporation. All Rights Reserved.
//
//  The source code, information  and  material ("Material") contained herein is
//  owned  by Intel Corporation or its suppliers or licensors, and title to such
//  Material remains  with Intel Corporation  or its suppliers or licensors. The
//  Material  contains proprietary information  of  Intel or  its  suppliers and
//  licensors. The  Material is protected by worldwide copyright laws and treaty
//  provisions. No  part  of  the  Material  may  be  used,  copied, reproduced,
//  modified, published, uploaded, posted, transmitted, distributed or disclosed
//  in any way  without Intel's  prior  express written  permission. No  license
//  under  any patent, copyright  or  other intellectual property rights  in the
//  Material  is  granted  to  or  conferred  upon  you,  either  expressly,  by
//  implication, inducement,  estoppel or  otherwise.  Any  license  under  such
//  intellectual  property  rights must  be express  and  approved  by  Intel in
//  writing.
//
//  *Third Party trademarks are the property of their respective owners.
//
//  Unless otherwise  agreed  by Intel  in writing, you may not remove  or alter
//  this  notice or  any other notice embedded  in Materials by Intel or Intel's
//  suppliers or licensors in any way.
*/
/*
//++
//  Macro definitions visible on user level.
//--
*/

#ifndef __MKL_VML_DEFINES_H__
#define __MKL_VML_DEFINES_H__

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

/*
//++
//  MACRO DEFINITIONS
//  Macro definitions for VML mode and VML error status.
//
//  VML mode controls VML function accuracy, floating-point settings (rounding
//  mode and precision) and VML error handling options. Default VML mode is
//  VML_HA | VML_ERRMODE_DEFAULT, i.e. VML high accuracy functions are
//  called, and current floating-point precision and the rounding mode is used.
//
//  Error status macros are used for error classification.
//--
*/

/*
//  VML FUNCTION ACCURACY CONTROL
//  VML_HA - when VML_HA is set, high accuracy VML functions are called
//  VML_LA - when VML_LA is set, low accuracy VML functions are called
//  VML_EP - when VML_EP is set, enhanced performance VML functions are called
//
//  NOTE: VML_HA, VML_LA and VML_EP must not be used in combination
*/
#define VML_LA 0x00000001
#define VML_HA 0x00000002
#define VML_EP 0x00000003

/*
//  SETTING OPTIMAL FLOATING-POINT PRECISION AND ROUNDING MODE
//  Definitions below are to set optimal floating-point control word
//  (precision and rounding mode).
//
//  For their correct work, VML functions change floating-point precision and
//  rounding mode (if necessary). Since control word changing is typically
//  expensive operation, it is recommended to set precision and rounding mode
//  to optimal values before VML function calls.
//
//  VML_FLOAT_CONSISTENT  - use this value if the calls are typically to single
//                          precision VML functions
//  VML_DOUBLE_CONSISTENT - use this value if the calls are typically to double
//                          precision VML functions
//  VML_RESTORE           - restore original floating-point precision and
//                          rounding mode
//  VML_DEFAULT_PRECISION - use default (current) floating-point precision and
//                          rounding mode
//  NOTE: VML_FLOAT_CONSISTENT, VML_DOUBLE_CONSISTENT, VML_RESTORE and
//        VML_DEFAULT_PRECISION must not be used in combination
*/
#define VML_DEFAULT_PRECISION 0x00000000
#define VML_FLOAT_CONSISTENT  0x00000010
#define VML_DOUBLE_CONSISTENT 0x00000020
#define VML_RESTORE           0x00000030

/*
//  VML ERROR HANDLING CONTROL
//  Macros below are used to control VML error handler.
//
//  VML_ERRMODE_IGNORE   - ignore errors
//  VML_ERRMODE_ERRNO    - errno variable is set on error
//  VML_ERRMODE_STDERR   - error description text is written to stderr on error
//  VML_ERRMODE_EXCEPT   - exception is raised on error
//  VML_ERRMODE_CALLBACK - user's error handler function is called on error
//  VML_ERRMODE_DEFAULT  - errno variable is set, exceptions are raised and
//                         user's error handler is called on error
//  NOTE: VML_ERRMODE_IGNORE must not be used in combination with
//        VML_ERRMODE_ERRNO, VML_ERRMODE_STDERR, VML_ERRMODE_EXCEPT,
//        VML_ERRMODE_CALLBACK and VML_ERRMODE_DEFAULT.
*/
#define VML_ERRMODE_IGNORE   0x00000100
#define VML_ERRMODE_ERRNO    0x00000200
#define VML_ERRMODE_STDERR   0x00000400
#define VML_ERRMODE_EXCEPT   0x00000800
#define VML_ERRMODE_CALLBACK 0x00001000
#define VML_ERRMODE_DEFAULT  \
VML_ERRMODE_ERRNO | VML_ERRMODE_CALLBACK | VML_ERRMODE_EXCEPT

/*
//  OpenMP(R) number of threads mode macros
//  VML_NUM_THREADS_OMP_AUTO   - Maximum number of threads is determined by
//                               environmental variable OPM_NUM_THREADS or
//                               omp_set_num_threads() function
//  VML_NUM_THREADS_OMP_FIXED  - Number of threads is determined by
//                               environmental variable OPM_NUM_THREADS
//                               omp_set_num_threads() functions
*/
#define VML_NUM_THREADS_OMP_AUTO   0x00000000
#define VML_NUM_THREADS_OMP_FIXED  0x00010000

/*
//  FTZ & DAZ mode macros
//  VML_FTZDAZ_ON   - FTZ & DAZ MXCSR mode enabled
//                    for faster (sub)denormal values processing
//  VML_FTZDAZ_OFF  - FTZ & DAZ MXCSR mode disabled
//                    for accurate (sub)denormal values processing
*/
#define VML_FTZDAZ_ON   0x00280000
#define VML_FTZDAZ_OFF  0x00140000

/*
//  ACCURACY, FLOATING-POINT CONTROL, FTZDAZ AND ERROR HANDLING MASKS
//  Accuracy, floating-point and error handling control are packed in
//  the VML mode variable. Macros below are useful to extract accuracy and/or
//  floating-point control and/or error handling control settings.
//
//  VML_ACCURACY_MASK           - extract accuracy bits
//  VML_FPUMODE_MASK            - extract floating-point control bits
//  VML_ERRMODE_MASK            - extract error handling control bits
//                                (including error callback bits)
//  VML_ERRMODE_STDHANDLER_MASK - extract error handling control bits
//                                (not including error callback bits)
//  VML_ERRMODE_CALLBACK_MASK   - extract error callback bits
//  VML_NUM_THREADS_OMP_MASK    - extract OpenMP(R) number of threads mode bits
//  VML_FTZDAZ_MASK             - extract FTZ & DAZ bits
*/
#define VML_ACCURACY_MASK           0x0000000F
#define VML_FPUMODE_MASK            0x000000F0
#define VML_ERRMODE_MASK            0x0000FF00
#define VML_ERRMODE_STDHANDLER_MASK 0x00000F00
#define VML_ERRMODE_CALLBACK_MASK   0x0000F000
#define VML_NUM_THREADS_OMP_MASK    0x00030000
#define VML_FTZDAZ_MASK             0x003C0000

/*
//  ERROR STATUS MACROS
//  VML_STATUS_OK        - no errors
//  VML_STATUS_BADSIZE   - array dimension is not positive
//  VML_STATUS_BADMEM    - invalid pointer passed
//  VML_STATUS_ERRDOM    - at least one of arguments is out of function domain
//  VML_STATUS_SING      - at least one of arguments caused singularity
//  VML_STATUS_OVERFLOW  - at least one of arguments caused overflow
//  VML_STATUS_UNDERFLOW - at least one of arguments caused underflow
//  VML_STATUS_ACCURACYWARNING - function doesn't support set accuracy mode,
//                               lower accuracy mode was used instead
*/
#define VML_STATUS_OK                    0
#define VML_STATUS_BADSIZE              -1
#define VML_STATUS_BADMEM               -2
#define VML_STATUS_ERRDOM                1
#define VML_STATUS_SING                  2
#define VML_STATUS_OVERFLOW              3
#define VML_STATUS_UNDERFLOW             4
#define VML_STATUS_ACCURACYWARNING       1000

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* __MKL_VML_DEFINES_H__ */
=#


const dust_regular = (5, 20, 100, 400)
const dust_golden = (50, 100, 400, 1600)

const craft_regular = (40, 100, 400, 1600)

function hssim(
        nreps::Int,
        decklist::DecklistArray,
        sets::Vector{Symbol},
        parameters::HearthstoneSimParameters = HearthstoneSimParameters()
    )

    sets_in_decklist = mapreduce(i -> collect(keys(decklist[i])), union, 1:4)

    @assert issubset(sets, sets_in_decklist)

    collection = Collection(sets, parameters.rarity_by_set)
    fresh_collection = deepcopy(collection)

    account_state::AccountState = AccountState()
    account_state.bonus_packs[:EXPERT1] = 7
    for s in parameters.sets_with_bonus
        account_state.bonus_packs[s] += 3
    end
    account_state.dust += parameters.bonus_dust
    if parameters.welcome_bundle
        account_state.dust += 400
        account_state.bonus_packs[:EXPERT1] += 10
    end
    fresh_account = deepcopy(account_state)

    ndeck = length(decklist)

    output = SimOutput(sets, nreps, ndeck)

    pack_table = Dict(
        map(x -> (x, packtablegen(ncard_db[x], parameters.rarity_probs)), sets)
    )

    pack_contents = map(i -> (0,0), 1:5)

    deckiter = collect(1:length(decklist))
    setiter = deepcopy(sets)

    for rep = 1:nreps

        if parameters.startfresh
            account_state = deepcopy(fresh_account)
            collection = deepcopy(fresh_collection)
        end

        for deckidx in deckiter
            for set in setiter
                sim_state::SimState = SimState(rep = rep, deck = deckidx, set = set, npacks = 0)

                subtarget = SingleSetDeckArray(decklist, set, deckidx)

                while missing_cards(collection, subtarget)

                    if account_state.dust >= remaining_dust(collection, subtarget)
                        spend_dust!(collection, subtarget, account_state, sim_state)
                        break
                    end

                    if account_state.bonus_packs[set] == 0
                        sim_state.npacks += 1
                    else
                        account_state.bonus_packs[set] -= 1
                    end

                    open_pack!(pack_contents,pack_table, collection, decklist, parameters,
                    account_state, sim_state, output)

                end

                output.total_packs_for_deck[set][deckidx, rep] = sim_state.npacks
                output.total_packs[set][rep] += sim_state.npacks

            end
        end

    end

    output
end


export gadplot_axes

split_gadargs(args...) = (),args
split_gadargs(ax::AxisId,args...) = (ax,),args
split_gadargs(ax1::AxisId,ax2::AxisId,args...) = (ax1,ax2),args
split_gadargs(ax1::AxisId,ax2::AxisId,ax3::AxisId,args...) = (ax1,ax2,ax3),args
split_gadargs(ax1::AxisId,ax2::AxisId,ax3::AxisId,ax4::AxisId,args...) =
  (ax1,ax2,ax3,ax4),args
function split_gadargs(ax1::AxisId,ax2::AxisId,ax3::AxisId,ax4::AxisId,
                      ax5::AxisId,args...)
  error("Plotting data using 5 or more axes is not supported by Gadfly backend.")
end

function gadplot_axes(data,args...;kwds...)
  ax, args = split_gadargs(args...)
  df, axes = asplotable(data,ax...;kwds...)
  if eltype(df.value) <: Complex
    df.value = abs.(df.value)
    @warn("Ignoring phase of complex value")
  end

  gadplot_axes_(df,axes,names(df)[2:end]...;args=args)
end

available_backends[:gadfly] = gadplot_axes
set_backend!(:gadfly)

function gadplot_axes_(df,axes,x;args)
  plot(df,x=x,y=:value,Geom.line,args...)
end

function gadplot_axes_(df,axes,x,y;args)
  (xmin,xmax) = extrema(df[!,x])
  (ymin,ymax) = extrema(df[!,y])
  plot(df,x=x,y=y,color=:value,Geom.rectbin,
       Coord.cartesian(xmin=xmin,xmax=xmax),args...)
end

function gadplot_axes_(df,axes,x,y,z;args)
  xmin,xmax = extrema(df[!,x])
  ymin,ymax = extrema(df[!,y])
  plot(df,x=x,y=y,color=:value,xgroup=z,
       Geom.subplot_grid(Geom.rectbin,
                         Coord.cartesian(xmin=xmin,xmax=xmax,
                                         ymin=ymin,ymax=ymax)),args...)
end

function gadplot_axes_(df,axes,x,y,z,w;args)
  xmin,xmax = extrema(df[!,x])
  ymin,ymax = extrema(df[!,y])
  plot(df,x=x,y=y,color=:value,xgroup=z,ygroup=w,
       Geom.subplot_grid(Geom.rectbin,
                         Coord.cartesian(xmin=xmin,xmax=xmax,
                                         ymin=ymin,ymax=ymax)),args...)
end
function gadplot_axes_(df,axes,args...;kwds...)
  error("Plotting data with $(length(axes)) dims along
        $(length(args)) axes is not supported.")
end


using BenchmarkTools
include("boid.jl")

# ## Running the model
function benchmark()
    n_steps = 100
    results = []
    for N in 100:100:400
        b = @benchmarkable step!(model, agent_step!, $n_steps) setup=(model =
        initialize_model(n_birds=$N,
        speed=1.0,
        cohere_factor=.25,
        separation=4.0,
        seperate_factor=.25,
        match_factor=.01,
        visual_distance=5.0,
        dims=(100,100)))

        j = run(b)
        push!(results, minimum(j.times)/1e9)
    end
    return results
end

print(benchmark())


function _backend_services_create_jwt(config::BackendServicesConfig)
    jti = Random.randstring(150)

    now = TimeZones.now(TimeZones.localzone())
    expiration_time = now + Dates.Minute(4)
    expiration_time_seconds_since_epoch_utc = integer_seconds_since_the_epoch_utc(expiration_time)

    jwt_payload_claims_dict = Dict(
        "iss" => config.iss,
        "sub" => config.sub,
        "aud" => config.token_endpoint,
        "jti" => jti,
        "exp" => expiration_time_seconds_since_epoch_utc,
    )
    jwt = JSONWebTokens.encode(config.private_key, jwt_payload_claims_dict)

    return jwt
end

"""
    backend_services(config::BackendServicesConfig)
"""
function backend_services(config::BackendServicesConfig)
    jwt = _backend_services_create_jwt(config)

    body_params = Dict{String, String}()
    body_params["grant_type"] = "client_credentials"
    body_params["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
    body_params["client_assertion"] = jwt

    if config.scope !== nothing
        body_params["scope"] = config.scope
    end

    _response = HTTP.request(
        "POST",
        config.token_endpoint;
        headers = Dict("Content-Type" => "application/x-www-form-urlencoded"),
        body = URIs.escapeuri(body_params),
    )

    access_token_response = JSON3.read(String(_response.body))
    access_token = access_token_response.access_token

    access_token_is_jwt, access_token_jwt_decoded = try_decode_jwt(access_token)

    backend_services_result = BackendServicesResult(;
        access_token             = access_token,
        access_token_is_jwt      = access_token_is_jwt,
        access_token_jwt_decoded = access_token_jwt_decoded,
        access_token_response    = access_token_response,
    )

    return backend_services_result
end


# Simulate a 2D actomyosin network
# Alex Tam, 12/10/2020

# Simulate network
"Control function for actomyosin network simulations"
function actomyosin_network(parN, parA, parM, par, trial)
    # Specify domain width
    Lxx::Float64 = 2.5; Lxy::Float64 = 0; Lyx::Float64 = 0; Lyy::Float64 = 2.5; # Actual domain widths
    # Pre-allocate
    Force = [[0.0, 0.0, 0.0, 0.0] for idx in 1:parN.nT]; # [pN] Network force
    Curvature = [0.0 for idx in 1:parN.nT]; # Mean network curvature
    Index = [0.0 for idx in 1:parN.nT]; # Mean two-filament index
    Filament_Speed = [0.0 for idx in 1:parN.nT]; # [μm/s] Mean filament node speed
    Motor_Speed = [0.0 for idx in 1:parN.nT]; # [μm/s] Mean motor head speed
    Angle_ROC = [0.0 for idx in 1:parN.nT]; # [rad/s] Mean rate of change of angle
    Motor_Pos = [0.0 for idx in 1:parN.nT]; # [--] Mean myosin motor head position
    Motor_Angle = [0.0 for idx in 1:parN.nT]; # [rad] Mean angle between filaments at motor binding site
    Bins = Vector{Float64}(); Hist = Vector{Int}(); # [μm, --] Pre-allocate paired-distance histogram
    # Generate initial conditions
    state = State{Float64}(Vector{Vector{Vector}}(), Vector{Vector}()); # Initialise empty State struct
    mm = Vector{Myosin_Motor}(); # Pre-allocate empty myosin motors
    af, state = actin_ic(state, parN, parA, Lxx, Lyy); # Initialise actin filaments
    xl = intersection_search(state, af, mm); # Initialise cross-links
    mm, xl, state = myosin_ic(state, mm, parN, xl, Lxx, Lxy, Lyx, Lyy); # Initialise myosin motors
    state_old = state; # Store initial state to compute force
    # Time-stepping
    animation = @animate for i = 1:parN.nT
        # Write final DOF vector to file
        if i == parN.nT
            dof = build_dof(state);
            writedlm("dof-$par-trial-$trial-$i.csv", dof);
        end
        # Compute network characteristics
        Curvature[i] = curvature(af, state, Lxx, Lxy, Lyx, Lyy); # savefig("actomyosin_curvature-$par-trial-$trial.svg"); # Mean filament curvature
        Index[i] = two_filament_index(mm, state, Lxx, Lxy, Lyx, Lyy); # savefig("actomyosin_2f_index-$par-trial-$trial.svg"); # Mean two-filament index
        Bins, Hist = pcf(af, state, Lxx, Lyy); savefig("actomyosin_pcf-$par-trial-$trial.svg"); # Paired distances between filament nodes
        Filament_Speed[i] = filament_speed(parN, af, state, state_old, Lxx, Lxy, Lyx, Lyy); # savefig("actomyosin_filament_speed-$par-trial-$trial.svg"); # Filament node speeds
        Motor_Speed[i] = motor_speed(parN, mm, state, state_old, Lxx, Lxy, Lyx, Lyy); # savefig("actomyosin_motor_speed-$par-trial-$trial.svg"); # Motor head speeds
        Angle_ROC[i] = motor_angle_roc(parN, mm, state, state_old, Lxx, Lxy, Lyx, Lyy); # savefig("actomyosin_angle_roc-$par-trial-$trial.svg"); # Angle rate of change
        Motor_Pos[i] = motor_position(mm, state); # savefig("actomyosin_angle_roc-$par-trial-$trial.svg"); # Myosin motor head positions
        Motor_Angle[i] = motor_angle(mm, state, parN, Lxx, Lxy, Lyx, Lyy); # savefig("actomyosin_angle_roc-$par-trial-$trial.svg"); # Myosin motor angles
        Force[i] = network_force(state, state_old, af, xl, mm, parN, parA, parM, Lxx, Lxy, Lyx, Lyy); # Force
        # Draw network
        draw_network(state, af, xl, mm, parN, parA, Force[i], Lxx, Lxy, Lyx, Lyy);
        if i == 1
            savefig("actomyosin_ic-par-$par-trial-$trial.svg"); # Save image of initial condition
        end
        # Simulate one time step
        if i != parN.nT # Ensure correct looping sequence
            # Turnover and polymerisation
            af, mm, state = actin_turnover(state, af, mm, parN, parA, Lxx, Lyy);
            af = segment_translations(state, af); # Update filament translations
            xl = intersection_search(state, af, mm); # Update cross-links
            mm, xl, state = myosin_turnover(state, xl, mm, parN, parM, Lxx, Lxy, Lyx, Lyy); # Myosin binding/unbinding
            # Compute solution and store data
            state_old = state; # Store current state for energy functional
            new_dof = optimise_network(state_old, af, xl, mm, parN, parA, parM, Lxx, Lxy, Lyx, Lyy); # Compute network solution
            state = build_state(new_dof, af, mm); # Construct State from vector
        end
    end
    # Output .gif and image of the network
    gif(animation, "actomyosin-par-$par-trial-$trial.gif", fps = 10); savefig("actomyosin_end-par-$par-trial-$trial.svg");
    # Plot quantities versus time
    draw_stress(parN, Force, parN.nT, Lxx, Lyy); savefig("actomyosin_stress-par-$par-trial-$trial.svg"); # Stress components
    times, Bulk_Stress, Bulk_Stress_Int = draw_bulk_stress(parN, Force, parN.nT, Lxx, Lyy); savefig("actomyosin_bulk_stress-par-$par-trial-$trial.svg"); # Bulk stress
    @printf("Time-averaged bulk stress is %f pN/μm*s.\n", Bulk_Stress_Int[end]/times[end])
    # Compute time-integrated network statistics
    Curvature_Int, Index_Int, Filament_Speed_Int, Motor_Speed_Int, Angle_ROC_Int, Motor_Pos_Int, Motor_Angle_Int = integrated_statistics(parN, Curvature, Index, Filament_Speed, Motor_Speed, Angle_ROC, Motor_Pos, Motor_Angle)
    # Save mean stress and spatial measures time series data to files
    writedlm("times.csv", times);
    writedlm("stress-par-$par-trial-$trial.csv", Bulk_Stress);
    writedlm("curvature-par-$par-trial-$trial.csv", Curvature);
    writedlm("index-par-$par-trial-$trial.csv", Index);
    writedlm("filament_speed-par-$par-trial-$trial.csv", Filament_Speed);
    writedlm("motor_speed-par-$par-trial-$trial.csv", Motor_Speed);
    writedlm("motor_pos-par-$par-trial-$trial.csv", Motor_Pos);
    writedlm("motor_angle-par-$par-trial-$trial.csv", Motor_Angle);
    return state, af, mm, xl, Bulk_Stress_Int[end]/times[end], Curvature_Int[end]/times[end], Index_Int[end]/times[end], Filament_Speed_Int[end]/times[end], Motor_Speed_Int[end]/times[end], Angle_ROC_Int[end]/times[end], Motor_Pos_Int[end]/times[end], Motor_Angle_Int[end]/times[end], Bins, Hist
end


using MathOptInterface
const MOI = MathOptInterface
const AFF = MOI.ScalarAffineFunction{Cdouble}
const EQ = MOI.EqualTo{Cdouble}
const AFFEQ = MOI.ConstraintIndex{AFF,EQ}

mutable struct Optimizer <: MOI.AbstractOptimizer
    objective_constant::Cdouble
    objective_sign::Int
    blockdims::Vector{CSDP_INT}
    varmap::Vector{Tuple{Int, Int, Int}} # Variable Index vi -> blk, i, j
    num_entries::Dict{Tuple{Int, Int}, Int}
    b::Vector{Cdouble}
    C::blockmatrix
    problem::Union{Nothing, LoadingProblem}
    X::blockmatrix
    y::Union{Nothing, Vector{Cdouble}}
    Z::blockmatrix
    status::CSDP_INT
    pobj::Cdouble
    dobj::Cdouble
    solve_time::Float64
    silent::Bool
    options::Dict{Symbol, Any}
    function Optimizer(; kwargs...)
        optimizer = new(
            zero(Cdouble), 1, CSDP_INT[], Tuple{Int, Int, Int}[],
            Dict{Tuple{Int, Int}, Int}(), Cdouble[],
            blockmatrix(), nothing, blockmatrix(), nothing, blockmatrix(),
            -1, NaN, NaN, NaN, false, Dict{Symbol, Any}())
        for (key, value) in kwargs
            MOI.set(optimizer, MOI.RawOptimizerAttribute(String(key)), value)
        end
        # May need to call `free_loaded_prob` and `free_loading_prob`.
        finalizer(MOI.empty!, optimizer)
        return optimizer
    end
end

varmap(optimizer::Optimizer, vi::MOI.VariableIndex) = optimizer.varmap[vi.value]

function MOI.supports(optimizer::Optimizer, param::MOI.RawOptimizerAttribute)
    return Symbol(param.name) in ALLOWED_OPTIONS
end
function MOI.set(optimizer::Optimizer, param::MOI.RawOptimizerAttribute, value)
    if !(param.name isa String)
        Base.depwarn(
            "passing `$(param.name)` to `MOI.RawOptimizerAttribute` as type " *
            "`$(typeof(param.name))` is deprecated. Use a string instead.",
            Symbol("MOI.set")
        )
    end
    if !MOI.supports(optimizer, param)
        throw(MOI.UnsupportedAttribute(param))
    end
    optimizer.options[Symbol(param.name)] = value
end
function MOI.get(optimizer::Optimizer, param::MOI.RawOptimizerAttribute)
    # TODO: This gives a poor error message if the name of the parameter is invalid.
    if !(param.name isa String)
        Base.depwarn(
            "passing `$(param.name)` to `MOI.RawOptimizerAttribute` as type " *
            "`$(typeof(param.name))` is deprecated. Use a string instead.",
            Symbol("MOI.set")
        )
    end
    return optimizer.options[Symbol(param.name)]
end

MOI.supports(::Optimizer, ::MOI.Silent) = true
function MOI.set(optimizer::Optimizer, ::MOI.Silent, value::Bool)
    optimizer.silent = value
end
MOI.get(optimizer::Optimizer, ::MOI.Silent) = optimizer.silent

MOI.get(::Optimizer, ::MOI.SolverName) = "CSDP"
# See table "Return codes for easy_sdp() and CSDP" in `doc/csdpuser.pdf`.
const RAW_STATUS = [
    "Problem solved to optimality.",
    "Problem is primal infeasible.",
    "Problem is dual infeasible.",
    "Problem solved to near optimality.",
    "Maximum iterations reached.",
    "Stuck at edge of primal feasibility.",
    "Stuck at edge of dual feasibility.",
    "Lack of progress.",
    "X, Z, or O is singular.",
    "NaN or Inf values encountered.",
    "Program stopped by signal (SIXCPU, SIGTERM, etc.)"]

function MOI.get(optimizer::Optimizer, ::MOI.RawStatusString)
    return RAW_STATUS[optimizer.status + 1]
end
function MOI.get(optimizer::Optimizer, ::MOI.SolveTimeSec)
    return optimizer.solve_time
end

function MOI.is_empty(optimizer::Optimizer)
    return iszero(optimizer.objective_constant) &&
        isone(optimizer.objective_sign) &&
        isempty(optimizer.blockdims) &&
        isempty(optimizer.varmap) &&
        isempty(optimizer.num_entries) &&
        isempty(optimizer.b) &&
        iszero(optimizer.C.nblocks) &&
        optimizer.C.blocks == C_NULL &&
        optimizer.problem === nothing
end

function MOI.empty!(optimizer::Optimizer)
    optimizer.objective_constant = zero(Cdouble)
    optimizer.objective_sign = 1
    empty!(optimizer.blockdims)
    empty!(optimizer.varmap)
    empty!(optimizer.num_entries)
    empty!(optimizer.b)
    if optimizer.problem !== nothing
        if optimizer.y !== nothing
            free_loaded_prob(optimizer.problem, optimizer.X, optimizer.y, optimizer.Z)
        end
        free_loading_prob(optimizer.problem)
    end
    optimizer.problem = nothing
    optimizer.C.nblocks = 0
    optimizer.C.blocks = C_NULL
    optimizer.X.nblocks = 0
    optimizer.X.blocks = C_NULL
    optimizer.y = nothing
    optimizer.Z.nblocks = 0
    optimizer.Z.blocks = C_NULL
    optimizer.status = -1
    optimizer.pobj = 0.0
    optimizer.dobj = 0.0
end

function MOI.supports(
    optimizer::Optimizer,
    ::Union{MOI.ObjectiveSense,
            MOI.ObjectiveFunction{AFF}})
    return true
end

MOI.supports_add_constrained_variables(::Optimizer, ::Type{MOI.Reals}) = false

const SupportedSets = Union{MOI.Nonnegatives, MOI.PositiveSemidefiniteConeTriangle}
MOI.supports_add_constrained_variables(::Optimizer, ::Type{<:SupportedSets}) = true
function MOI.supports_constraint(::Optimizer, ::Type{AFF}, ::Type{EQ})
    return true
end

function _new_block(optimizer::Optimizer, set::MOI.Nonnegatives)
    push!(optimizer.blockdims, -MOI.dimension(set))
    blk = length(optimizer.blockdims)
    for i in 1:MOI.dimension(set)
        push!(optimizer.varmap, (blk, i, i))
    end
end

function _new_block(optimizer::Optimizer, set::MOI.PositiveSemidefiniteConeTriangle)
    push!(optimizer.blockdims, set.side_dimension)
    blk = length(optimizer.blockdims)
    for i in 1:set.side_dimension
        for j in 1:i
            push!(optimizer.varmap, (blk, i, j))
        end
    end
end

function _add_constrained_variables(optimizer::Optimizer, set::SupportedSets)
    offset = length(optimizer.varmap)
    _new_block(optimizer, set)
    ci = MOI.ConstraintIndex{MOI.VectorOfVariables, typeof(set)}(offset + 1)
    return [MOI.VariableIndex(i) for i in offset .+ (1:MOI.dimension(set))], ci
end

function _error(start, stop)
    error(start, ". Use `MOI.instantiate(CSDP.Optimizer, with_bridge_type = Float64)` ", stop)
end

function constrain_variables_on_creation(
    dest::MOI.ModelLike,
    src::MOI.ModelLike,
    index_map::MOI.Utilities.IndexMap,
    ::Type{S},
) where {S<:MOI.AbstractVectorSet}
    for ci_src in
        MOI.get(src, MOI.ListOfConstraintIndices{MOI.VectorOfVariables,S}())
        f_src = MOI.get(src, MOI.ConstraintFunction(), ci_src)
        if !allunique(f_src.variables)
            _error("Cannot copy constraint `$(ci_src)` as variables constrained on creation because there are duplicate variables in the function `$(f_src)`",
                   "to bridge this by creating slack variables.")
        elseif any(vi -> haskey(index_map, vi), f_src.variables)
            _error("Cannot copy constraint `$(ci_src)` as variables constrained on creation because some variables of the function `$(f_src)` are in another constraint as well.",
                   "to bridge constraints having the same variables by creating slack variables.")
        else
            set = MOI.get(src, MOI.ConstraintSet(), ci_src)::S
            vis_dest, ci_dest = _add_constrained_variables(dest, set)
            index_map[ci_src] = ci_dest
            for (vi_src, vi_dest) in zip(f_src.variables, vis_dest)
                index_map[vi_src] = vi_dest
            end
        end
    end
end

function count_entry(optimizer::Optimizer, con_idx::Integer, blk::Integer)
    key = (con_idx, blk)
    optimizer.num_entries[key] = get(optimizer.num_entries, key, 0) + 1
end

# Loads objective coefficient α * vi
function load_objective_term!(optimizer::Optimizer, α, vi::MOI.VariableIndex)
    blk, i, j = varmap(optimizer, vi)
    # in SDP format, it is max and in MPB Conic format it is min
    coef = optimizer.objective_sign * α
    if i != j
        coef /= 2
    end
    addentry(optimizer.problem, 0, blk, i, j, coef, true)
end

function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike)
    MOI.empty!(dest)
    index_map = MOI.Utilities.IndexMap()

    # Step 1) Compute the dimensions of what needs to be allocated
    constrain_variables_on_creation(
        dest,
        src,
        index_map,
        MOI.Nonnegatives,
    )
    constrain_variables_on_creation(
        dest,
        src,
        index_map,
        MOI.PositiveSemidefiniteConeTriangle,
    )
    vis_src = MOI.get(src, MOI.ListOfVariableIndices())
    if length(vis_src) < length(index_map.var_map)
        _error("Free variables are not supported by CSDP",
               "to bridge free variables into `x - y` where `x` and `y` are nonnegative.")
    end
    cis_src = MOI.get(src, MOI.ListOfConstraintIndices{AFF,EQ}())
    dest.b = Vector{Cdouble}(undef, length(cis_src))
    funcs = Vector{AFF}(undef, length(cis_src))
    for (k, ci_src) in enumerate(cis_src)
        funcs[k] = MOI.get(src, MOI.CanonicalConstraintFunction(), ci_src)
        set = MOI.get(src, MOI.ConstraintSet(), ci_src)
        if isempty(funcs[k].terms)
            throw(ArgumentError("Empty constraint $cis_src: $(funcs[k])-in-$set. Not supported by CSDP."))
        end
        if !iszero(MOI.constant(funcs[k]))
            throw(MOI.ScalarFunctionConstantNotZero{
                Cdouble, AFF, EQ}(
                    MOI.constant(funcs[k])))
        end
        for t in funcs[k].terms
            if !iszero(t.coefficient)
                blk, _, _ = varmap(dest, index_map[t.variable])
                count_entry(dest, k, blk)
            end
        end
        dest.b[k] = MOI.constant(set)
        index_map[ci_src] = AFFEQ(k)
    end

    # Step 2) Allocate CSDP datastructures
    dummy = isempty(dest.b)
    if dummy
        # See https://github.com/coin-or/Csdp/issues/2
        dest.b = [one(Cdouble)]
        dest.blockdims = [dest.blockdims; CSDP_INT(-1)]
        count_entry(dest, 1, length(dest.blockdims))
    end
    dest.C.nblocks = length(dest.blockdims)
    num_entries = zeros(CSDP_INT, length(dest.b), length(dest.blockdims))
    for (key, value) in dest.num_entries
        num_entries[key...] = value
    end
    dest.problem = allocate_loading_prob(Ref(dest.C), dest.blockdims, length(dest.b), num_entries, 3)
    if dummy
        # See https://github.com/coin-or/Csdp/issues/2
        duplicate = addentry(dest.problem, 1, length(dest.blockdims), 1, 1, 1.0, true)
        @assert !duplicate
    end

    # Step 3) Load data in the datastructures
    for k in eachindex(funcs)
        setconstant(dest.problem, k, dest.b[k])
        for term in funcs[k].terms
            if !iszero(term.coefficient)
                blk, i, j = varmap(dest, index_map[term.variable])
                coef = term.coefficient
                if i != j
                    coef /= 2
                end
                duplicate = addentry(dest.problem, k, blk, i, j, coef, true)
                @assert !duplicate
            end
        end
    end

    # Throw error for variable attributes
    MOI.Utilities.pass_attributes(dest, src, index_map, vis_src)
    # Throw error for constraint attributes
    MOI.Utilities.pass_attributes(dest, src, index_map, cis_src)

    # Pass objective attributes and throw error for other ones
    model_attributes = MOI.get(src, MOI.ListOfModelAttributesSet())
    for attr in model_attributes
        if attr != MOI.ObjectiveSense() && attr != MOI.ObjectiveFunction{AFF}()
            throw(MOI.UnsupportedAttribute(attr))
        end
    end
    # We make sure to set `objective_sign` first before setting the objective
    if MOI.ObjectiveSense() in model_attributes
        sense = MOI.get(src, MOI.ObjectiveSense())
        dest.objective_sign = sense == MOI.MIN_SENSE ? -1 : 1
    end
    if MOI.ObjectiveFunction{AFF}() in model_attributes
        func = MOI.get(src, MOI.ObjectiveFunction{AFF}())
        obj = MOI.Utilities.canonical(func)
        dest.objective_constant = obj.constant
        for term in obj.terms
            if !iszero(term.coefficient)
                load_objective_term!(dest, term.coefficient, index_map[term.variable])
            end
        end
    end
    return index_map
end

function MOI.optimize!(optimizer::Optimizer)
    write_prob(optimizer)

    start_time = time()
    optimizer.y = loaded_initsoln(optimizer.problem, length(optimizer.b), Ref(optimizer.X), Ref(optimizer.Z))

    options = optimizer.options
    if optimizer.silent
        options = copy(options)
        options[:printlevel] = 0
    end

    optimizer.status, optimizer.pobj, optimizer.dobj = loaded_sdp(
        optimizer.problem, optimizer.objective_sign * optimizer.objective_constant,
        Ref(optimizer.X), optimizer.y, Ref(optimizer.Z), options,
    )
    optimizer.solve_time = time() - start_time
    return
end

function MOI.get(m::Optimizer, ::MOI.TerminationStatus)
    status = m.status
    if status == -1
        return MOI.OPTIMIZE_NOT_CALLED
    elseif status == 0
        return MOI.OPTIMAL
    elseif status == 1
        return MOI.INFEASIBLE
    elseif status == 2
        return MOI.DUAL_INFEASIBLE
    elseif status == 3
        return MOI.ALMOST_OPTIMAL
    elseif status == 4
        return MOI.ITERATION_LIMIT
    elseif 5 <= status <= 7
        return MOI.SLOW_PROGRESS
    elseif 8 <= status <= 9
        return MOI.NUMERICAL_ERROR
    else
        error("Internal library error: status=$status")
    end
end

function MOI.get(m::Optimizer, attr::MOI.PrimalStatus)
    if attr.result_index > MOI.get(m, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    status = m.status
    if status == 0
        return MOI.FEASIBLE_POINT
    elseif status == 1
        return MOI.INFEASIBLE_POINT
    elseif status == 2
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == 3
        return MOI.NEARLY_FEASIBLE_POINT
    elseif 4 <= status <= 9
        return MOI.UNKNOWN_RESULT_STATUS
    else
        error("Internal library error: status=$status")
    end
end

function MOI.get(m::Optimizer, attr::MOI.DualStatus)
    if attr.result_index > MOI.get(m, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    status = m.status
    if status == 0
        return MOI.FEASIBLE_POINT
    elseif status == 1
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == 2
        return MOI.INFEASIBLE_POINT
    elseif status == 3
        return MOI.NEARLY_FEASIBLE_POINT
    elseif 4 <= status <= 9
        return MOI.UNKNOWN_RESULT_STATUS
    else
        error("Internal library error: status=$status")
    end
end

MOI.get(m::Optimizer, ::MOI.ResultCount) = 1
function MOI.get(m::Optimizer, attr::MOI.ObjectiveValue)
    MOI.check_result_index_bounds(m, attr)
    return m.objective_sign * m.pobj
end
function MOI.get(m::Optimizer, attr::MOI.DualObjectiveValue)
    MOI.check_result_index_bounds(m, attr)
    return m.objective_sign * m.dobj
end
struct PrimalSolutionMatrix <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::PrimalSolutionMatrix) = true
MOI.get(optimizer::Optimizer, ::PrimalSolutionMatrix) = optimizer.X

struct DualSolutionVector <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::DualSolutionVector) = true
MOI.get(optimizer::Optimizer, ::DualSolutionVector) = optimizer.y

struct DualSlackMatrix <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::DualSlackMatrix) = true
MOI.get(optimizer::Optimizer, ::DualSlackMatrix) = optimizer.Z

function block(optimizer::Optimizer, ci::MOI.ConstraintIndex{MOI.VectorOfVariables})
    return optimizer.varmap[ci.value][1]
end
function dimension(optimizer::Optimizer, ci::MOI.ConstraintIndex{MOI.VectorOfVariables})
    blockdim = optimizer.blockdims[block(optimizer, ci)]
    if blockdim < 0
        return -blockdim
    else
        return MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(blockdim))
    end
end
function vectorize_block(M, blk::Integer, s::Type{MOI.Nonnegatives})
    return diag(block(M, blk))
end
function vectorize_block(M::AbstractMatrix{Cdouble}, blk::Integer, s::Type{MOI.PositiveSemidefiniteConeTriangle}) where T
    B = block(M, blk)
    d = LinearAlgebra.checksquare(B)
    n = MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(d))
    v = Vector{Cdouble}(undef, n)
    k = 0
    for j in 1:d
        for i in 1:j
            k += 1
            v[k] = B[i, j]
        end
    end
    @assert k == n
    return v
end

function MOI.get(optimizer::Optimizer, attr::MOI.VariablePrimal, vi::MOI.VariableIndex)
    MOI.check_result_index_bounds(optimizer, attr)
    blk, i, j = varmap(optimizer, vi)
    return block(MOI.get(optimizer, PrimalSolutionMatrix()), blk)[i, j]
end

function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintPrimal,
                 ci::MOI.ConstraintIndex{MOI.VectorOfVariables, S}) where S<:SupportedSets
    MOI.check_result_index_bounds(optimizer, attr)
    return vectorize_block(MOI.get(optimizer, PrimalSolutionMatrix()), block(optimizer, ci), S)
end
function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintPrimal, ci::AFFEQ)
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.b[ci.value]
end

function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintDual,
                 ci::MOI.ConstraintIndex{MOI.VectorOfVariables, S}) where S<:SupportedSets
    MOI.check_result_index_bounds(optimizer, attr)
    return vectorize_block(MOI.get(optimizer, DualSlackMatrix()), block(optimizer, ci), S)
end
function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintDual, ci::AFFEQ)
    MOI.check_result_index_bounds(optimizer, attr)
    return -MOI.get(optimizer, DualSolutionVector())[ci.value]
end


function jamsd_reg_eqns(ctx, m::JAMSDMathProgModel, equil=false)
    if has_objective(m)
        obj_offset = 1
    else
        obj_offset = 0
    end

    jamsd_declare_eqns(ctx, m, equil)

    if has_objective(m)
        jamsd_add_obj_nl(ctx, m)
        jamsd_add_obj_lin(ctx, m)
        jamsd_add_obj_quad(ctx, m)
    end

    if m.ncon > 0 || length(m.quad_equs) > 0
#        write_nl_k_block(f, m)
        if length(m.nonquad_idx) != m.ncon
            error("The number of constraint and the equation index do not match")
        end
        jamsd_add_cons_lin(ctx, m, obj_offset)
        jamsd_add_cons_nl(ctx, m, obj_offset)
        jamsd_add_cons_quad(ctx, m, obj_offset)
        jamsd_add_contraint_sense(ctx, m, obj_offset)
    end

    # This is most likely broken, we need another way of getting this data if
    # it is important
#    return m.ncon+obj_offset+length(m.quad_equs)
end

function create_jamsd_ctx(m::JAMSDMathProgModel)
    # Not all model have an objective function
    if has_objective(m)
        obj_offset = 1
    else
        obj_offset = 0
    end

    ctx = ctx_create(m.nvar, length(m.quad_equs)+m.ncon+obj_offset)

    jamsd_declare_vars(ctx, m)
    jamsd_add_var_guess(ctx, m)

    jamsd_reg_eqns(ctx, m)

    return ctx
end

function has_objective(m::JAMSDMathProgModel)
    CONFIG[:debug] && println("DEBUG: has_objective $(m.lin_obj) $(m.obj) $(m.quad_obj)")
    res = !isempty(m.lin_obj) || (isa(m.obj, Expr)) || length(m.quad_obj) == 3
    if !res return res end
    res = any(v != 0. for v in values(m.lin_obj)) || isa(m.obj, Expr) && (m.obj != 0.) || length(m.quad_obj) == 3
    return res
end

# Nonlinear constraint trees
function jamsd_add_cons_nl(ctx, m::JAMSDMathProgModel, offset)
    for idx in 1:m.ncon
        if isa(m.constrs[idx], Expr) && (m.constrs[idx] != 0.)
            eidx = m.nonquad_idx[idx] + offset - 1 + m.offset
            tree, node = jamsd_get_treedata(ctx, eidx)
            CONFIG[:debug] && println("jamsd_add_cons_nl: storing expression $(eidx)")
            jamsd_add_nlexpr(ctx, tree, node, m, m.constrs[idx])
        end
    end
end

function jamsd_quad(ctx, m::JAMSDMathProgModel, idx, equ, offset, isObj::Bool=false)
    ##########################################################################
    #
    ##########################################################################
    if isObj
        eidx = m.offset
    else
        eidx = m.quad_idx[idx] - 1 + m.offset + offset
    end

    CONFIG[:debug] && println("jamsd_quad: $equ")
    lidx, lval, rowidx, colidx, qval = equ[1:5]
    CONFIG[:debug] && println("jamsd_quad: $lidx $lval $rowidx $colidx $qval")

    ##########################################################################
    # lin_dict collects all the linear terms
    #
    # we don't construct the dict directly, since it may have duplicate values
    ##########################################################################

    lin_dict = Dict(zip(lidx, Iterators.repeated(0.)))
    for i in 1:length(lidx)
        lin_dict[lidx[i]] += lval[i]
    end

    ###########################################################################
    # MPB is a bit insane here: via setquadobj! it provides a triangular matrix
    # representing .5 x^TQx ... But this makes no sense since we assume that
    # the multiplication is commutative here. Therefore the values of the off-
    # diagonal element have to be multiplied by 2
    ###########################################################################

    quad_dict = Dict(zip(zip(rowidx, colidx), Iterators.repeated(0.)))

    if isObj
        for i in 1:length(rowidx)
            if rowidx[i] != colidx[i]
                quad_dict[(rowidx[i], colidx[i])] += 2*qval[i]
            else
                quad_dict[(rowidx[i], colidx[i])] += qval[i]
            end
        end
    else
        for i in 1:length(rowidx)
            quad_dict[(rowidx[i], colidx[i])] += qval[i]
        end
    end

    qidxC = keys(quad_dict)
    rowidxU = [elt[1]-1 for elt in qidxC]
    colidxU = [elt[2]-1 for elt in qidxC]
    qvalU = collect(values(quad_dict))

    ##########################################################################
    # Add the linear part <c,x> for the purely linear variables
    ##########################################################################

    lidxS = Set(lidx)
    lin_vars = setdiff(lidxS, union(Set(rowidx), Set(colidx)))
    qidxS = setdiff(lidxS, lin_vars)
    for lindx in lin_vars
        # add as linear variable
        v = lin_dict[lindx]
        if abs(v) > 0.
            CONFIG[:debug] && println("DEBUG: in jamsd_quad, lin var $lindx with value $v")
            ctx_add_lin_var(ctx, eidx, m.v_index_map[lindx], v)
        end
    end

    ##########################################################################
    # Add the term <c,x> for the quadratic variables
    ##########################################################################

    if length(lin_dict) > 0 && length(qidxS) > 0
        # TODO(xhub) filter for qvals[i] = 0.
        qvals = collect(lin_dict[qidx] for qidx in qidxS)
        CONFIG[:debug] && println("DEBUG: in jamsd_quad, qidxS = $qidxS; qval= $qvals")
        abs_var = jamsd_avar(length(qidxS), collect(qidxS) .- 1)
        CONFIG[:debug] && println("DEBUG: in jamsd_quad, quad var $(collect(qidxS)) with value $qvals")
        jamsd_equ_add_lin_tree(ctx, eidx, qvals, abs_var, 1.)
        jamsd_avar_free(abs_var)
    end

    ##########################################################################
    # Add the quadratic terms <x, Mx>
    ##########################################################################

    if length(qvalU) > 0
        CONFIG[:debug] && println("DEBUG: in jamsd_quad, quad term $rowidxU $colidxU $qvalU")
        @assert length(qvalU) == length(rowidxU)
        @assert length(qvalU) == length(colidxU)
        mat = jamsd_mat_coo(rowidxU, colidxU, qvalU)
        midxS = union(BitSet(rowidx), BitSet(colidx))
        avar = jamsd_avar(length(midxS), collect(i-1 for i in midxS))
        if isObj
            c = 1.
        else
            c = 2.
        end
        # the coeff is 2 since this function adds .5 x^TMx
        jamsd_equ_add_quadratic(ctx, eidx, mat, avar, c)
        jamsd_avar_free(avar)
        jamsd_mat_free(mat)
    end
end

function jamsd_add_obj_quad(ctx, m::JAMSDMathProgModel)
    CONFIG[:debug] && println("DEBUG: quad_obj = $(m.quad_obj)")
    if length(m.quad_obj) == 3
        rowidx = m.quad_obj[1]
        colidx = m.quad_obj[2]
        quadval = m.quad_obj[3]
        jamsd_quad(ctx, m, 0, (Int[], Float64[], rowidx, colidx, quadval), 0, true)
    elseif length(m.quad_obj) == 0
        #doing noting here
    else
        error("jamsd_add_obj_quad :: invalid quad_obj object $(m.quad_obj)")
    end
end

function jamsd_add_cons_quad(ctx, m::JAMSDMathProgModel, offset)
    for (idx, equ) in enumerate(m.quad_equs)
        jamsd_quad(ctx, m, idx, equ, offset)
    end
end

# Nonlinear objective tree
function jamsd_add_obj_nl(ctx, m::JAMSDMathProgModel)
    # Get tree, node, ...
    if m.obj != 0.
        tree, node = jamsd_get_treedata(ctx, m.offset)
        CONFIG[:debug] && println("jamsd_add_obj_nl: storing expression $(m.offset)")
        jamsd_add_nlexpr(ctx, tree, node, m, m.obj)
    end
end

# Initial primal guesses
function jamsd_add_var_guess(ctx, m::JAMSDMathProgModel)
    for idx in 0:(m.nvar - 1)
        i = m.v_index_map_rev[idx]
        CONFIG[:debug] && println("Setting level value variable $i to $(m.x_0[i])")
        ctx_setvarlone(ctx, idx, m.x_0[i])
    end
end

# Constraint bounds
function jamsd_add_contraint_sense(ctx, m::JAMSDMathProgModel, offset)
    for idx in 1:m.ncon
        lower = m.g_l[idx]
        upper = m.g_u[idx]
        rel = m.r_codes[idx]
        # TODO(xhub) document
        if rel == -1
            error("Doubly constraint equation is not yet supported")
        elseif rel == 0         # ==
            value = lower
        elseif rel == 1         # >=
            value = lower
        elseif rel == 2         # <=
            value = upper
        elseif rel == 4         # ???
            value == upper
        else
            error("unsupported rel = $(rel) for equation index $i")
        end
        eidx = m.nonquad_idx[idx] + offset - 1 + m.offset
        CONFIG[:debug] && println("Setting sense and rhs for equation $eidx: $rel $value")
        jamsd_set_rhs(ctx, eidx, value)
        jamsd_set_equtype(ctx, eidx, rel)
    end

    for (idx, equ) in enumerate(m.quad_equs)
        rel, value = equ[end-1:end]
        eidx = m.quad_idx[idx] + offset - 1 + m.offset
        jamsd_set_rhs(ctx, eidx, value)
        jamsd_set_equtype(ctx, eidx, relation_to_jamsd[rel])
    end
end

function jamsd_declare_var(ctx, vtype, lower, upper)
    if lower == -Inf
        if upper == Inf
            jamsd_add_free_var(ctx, 1)
        elseif upper == 0
            jamsd_add_neg_var(ctx, 1)
        else
            jamsd_add_box_var(ctx, lower, upper)
        end
    else
        if lower == upper
            jamsd_add_box_var(ctx, lower, upper)
        elseif upper == Inf
            if lower == 0
                jamsd_add_pos_var(ctx, 1)
            else
                jamsd_add_box_var(ctx, lower, upper)
            end
        else
            jamsd_add_box_var(ctx, lower, upper)
        end
    end
    idx = hack_last_vidx(ctx)
    CONFIG[:debug] && println("DEBUG: var $(idx) has type $(vtype)")
    # We have no consistency check
    # TODO SOS, semicont
    if vtype == :Bin
        jamsd_set_vartype(ctx, idx, 1)
    elseif vtype == :Int
        jamsd_set_vartype(ctx, idx, 2)
    end

end

# Variable bounds
function jamsd_declare_vars(ctx, m::JAMSDMathProgModel)
    for idx in 0:(m.nvar - 1)
        i = m.v_index_map_rev[idx]
        lower = m.x_l[i]
        upper = m.x_u[i]
        jamsd_declare_var(ctx, m.vartypes[i], lower, upper)
    end

    # Set variable names
    if m.d.hasvalue
        ctx_setvarnames(ctx, m.d.value.m.colNames)
    end
end

# Jacobian counts
# TODO(xhub) resuse this to prealloc the model_repr?
#function write_nl_k_block(f, m::JAMSDMathProgModel)
#    println(f, "k$(m.nvar - 1)")
#    total = 0
#    for index = 0:(m.nvar - 2)
#        i = m.v_index_map_rev[index]
#        total += m.j_counts[i]
#        println(f, total)
#    end
#end

function jamsd_declare_eqns(ctx, m::JAMSDMathProgModel, equil=false)
    if has_objective(m)
        cor = 0
    else
        cor = -1
    end

    for idx in 0:m.ncon+cor+length(m.quad_equs)
        jamsd_decl_eqn(ctx, idx+m.offset)
    end
    if equil || !has_objective(m)
        jamsd_set_objeqn(ctx, -1)
    else
        jamsd_set_objeqn(ctx, 0)
    end
#    return m.ncon+cor+length(m.quad_equs)
end

# Linear constraint expressions
function jamsd_add_cons_lin(ctx, m::JAMSDMathProgModel, offset)
    for idx in 1:m.ncon
        num_vars = length(m.lin_constrs[idx])
        if num_vars > 0
#TODO(Xhub)            jamsd_reserve_lequ(ctx, idx, num_vars)
            eidx = m.nonquad_idx[idx] + offset - 1 + m.offset
            for (k,v) in m.lin_constrs[idx]
                if abs(v) > 0.
                    CONFIG[:debug] && println("DEBUG: add var $k with value $v in eqn $(eidx)")
                    ctx_add_lin_var(ctx, eidx, m.v_index_map[k], v)
                end
            end
        end
    end
end

# Linear objective expression
function jamsd_add_obj_lin(ctx, m::JAMSDMathProgModel)
    for (k,v) in m.lin_obj
        if abs(v) > 0.
            ctx_add_lin_var(ctx, m.offset, m.v_index_map[k], v)
        end
    end
end

function jamsd_add_quad_lin(ctx, m::JAMSDMathProgModel)
end

##############################################################################
# Low level functions
##############################################################################

function write_arithm_op(ctx, tree, node, m, fn, args)
    # TODO(xhub) implement better management of variable/constant base
    len = length(args)
    equtree_arithm(tree, node, arithm_ops[fn], len)
    pnode = equnode_deref(node)
    for i in 0:(len-1)
        child = equnode_get_child_addr(pnode, i)
        jamsd_add_nlexpr(ctx, tree, child, m, args[len-i])
    end
end

# Convert an expression tree
jamsd_add_nlexpr(ctx, tree, node, m, c) = error("Unrecognized expression $c")
# Handle numerical constants e.g. pi
jamsd_add_nlexpr(ctx, tree, node, m, c::Symbol) =  jamsd_add_nlexpr(ctx, tree, node, m, float(eval(c)))

# write down constant
function jamsd_add_nlexpr(ctx, tree, node, m, c::Real)
#    if abs(c) > 0.
        equtree_cst(ctx, tree, node, c)
#    end
end

jamsd_add_nlexpr(ctx, tree, node, m, c::LinearityExpr) = jamsd_add_nlexpr(ctx, tree, node, m, c.c)
function jamsd_add_nlexpr(ctx, tree, node, m, c::Expr)
    CONFIG[:debug] && println("DEBUG: encoding expr $c")
    if c.head == :ref
        CONFIG[:debug] && println("DEBUG: variable case: $c")
        # This is a variable
        if c.args[1] == :x
            @assert isa(c.args[2], Int)
            equtree_var(ctx, tree, node, m.v_index_map[c.args[2]], 1.)
        else
            error("Unrecognized reference expression $c")
        end
    elseif c.head == :call
        if c.args[1] in keys(arithm_ops)
            write_arithm_op(ctx, tree, node, m, c.args[1], c.args[2:end])
        elseif c.args[1] == :neg
            equtree_umin(ctx, tree, node)
            jamsd_add_nlexpr(ctx, tree, node, m, c.args[2])
        # TODO(xhub) support nary_functions
        elseif c.args[1] in nary_functions
            error("Unsupported n-ary function $(c.args[1])")
        else
            equtree_call(ctx, tree, node, func_to_jamsd[c.args[1]])
            len = length(c.args)
            pnode = equnode_deref(node)

            for i in 0:(len - 2)
                child = equnode_get_child_addr(pnode, i)
                jamsd_add_nlexpr(ctx, tree, child, m, c.args[i+2])
            end
        end

    elseif c.head == :comparison
        # .nl only handles binary comparison
        @assert length(c.args) == 3
        # Output comparison type first, followed by args
        CONFIG[:debug] && println(f, nl_operator(c.args[2]))
        map(arg -> jamsd_add_nlexpr(ctx, tree, node, m, arg), c.args[1:2:end])

    elseif c.head in [:&&, :||]
        error("unsupported binary condition")
    else
        error("Unrecognized expression $c")
    end
end


export BFFPS19

# ===============================================================
# Bogomolov, Forets, Frehse, Potomkin, Schilling.
# ===============================================================

function options_BFFPS19()
    return OptionSpec[
        # general options
        OptionSpec(:discretization, "forward", domain=String,
            info="model for bloating/continuous time analysis"),
        OptionSpec(:δ, 1e-2, domain=Float64, aliases=[:sampling_time],
            domain_check=(v  ->  v > 0.), info="time step"),
        OptionSpec(:vars, Int[], domain=AbstractVector{Int}, domain_check=(
            v  ->  length(v) > 0 && all(e -> e > 0, v)),
            info="variables of interest; default: all variables"),
        OptionSpec(:guards_proj, Vector(), domain=AbstractVector,
             info="internal storage of projected guard constraints"),
        OptionSpec(:partition, [Int[]],
            domain=AbstractVector{<:AbstractVector{Int}}, domain_check=
            ispartition,
            info="block partition; a block is represented by a vector " *
                 "containing its indices"),

        # discretization options
        OptionSpec(:sih_method, "concrete", domain=String,
            info="method to compute the symmetric interval hull in discretization"),
        OptionSpec(:exp_method, "base", domain=String,
            info="method to compute the matrix exponential"),
        OptionSpec(:assume_sparse, false, domain=Bool,
            info="use an analysis for sparse discretized matrices?"),

        # reachability options
        OptionSpec(:lazy_inputs_interval, lazy_inputs_interval_always,
            domain=Union{Int, Function},
            domain_check=(v  ->  !(v isa Int) || v >= -1),
            info="length of interval in which the inputs are handled as a " *
                 "lazy set (``-1`` for 'never'); may generally also be a " *
                 "predicate over indices; the default corresponds to ``-1``"),

        # approximation options
        OptionSpec(:block_options, nothing, domain=Any,
            info="short hand to set ':block_options_init' and " *
                 "':block_options_iter'"),
        OptionSpec(:block_options_init, nothing, domain=Any,
            info="option for the approximation of the initial states " *
                 "(during decomposition)"),
        OptionSpec(:block_options_iter, nothing, domain=Any,
            info="option for the approximation of the states ``X_k``, ``k>0``"),

        # convenience options
        OptionSpec(:assume_homogeneous, false, domain=Bool,
            info="ignore dynamic inputs during the analysis?"),
    ]
end

function normalization_BFFPS19!(𝑂::TwoLayerOptions)
    # :lazy_inputs_interval option: convert integers to functions
    if haskey_specified(𝑂, :lazy_inputs_interval)
        v = 𝑂[:lazy_inputs_interval]
        if v isa Int
            if v == -1
                𝑂.specified[:lazy_inputs_interval] = lazy_inputs_interval_never
            elseif v == 0
                𝑂.specified[:lazy_inputs_interval] = lazy_inputs_interval_always
            else
                𝑂.specified[:lazy_inputs_interval] = (k -> k % v == 0)
            end
        end
    end

    # :block_options options: use convenience option for '_init' and '_iter'
    if !haskey_specified(𝑂, :block_options_init) &&
            haskey_specified(𝑂, :block_options)
        𝑂.specified[:block_options_init] = 𝑂[:block_options]
    end
    if !haskey_specified(𝑂, :block_options_iter) &&
            haskey_specified(𝑂, :block_options)
        𝑂.specified[:block_options_iter] = 𝑂[:block_options]
    end

    nothing
end

function validation_BFFPS19(𝑂)
    # block_options
    for b_options in [:block_options, :block_options_init, :block_options_iter]
        if haskey_specified(𝑂, b_options)
            bo = 𝑂[b_options]
            if bo isa Type{<:LazySet} || bo isa Type{<:AbstractDirections}
                # uniform options
            elseif bo isa AbstractVector || bo isa Dict{Int, Any}
                # mapping
            elseif bo isa Real || bo isa Pair{<:UnionAll, <:Real}
                ε = bo isa Real ? bo : bo[2]
                if ε <= 0
                    throw(DomainError(ε, "the `$b_options` option must be " *
                                         "positive"))
                end
            elseif b_options == :block_options_iter && bo == nothing
                # no overapproximation
            else
                throw(DomainError(bo == nothing ? "nothing" : bo,
                    "the `$b_options` option does not accept the given input"))
            end
        end
    end

    nothing
end

"""
    BFFPS19 <: ContinuousPost

Implementation of the reachability algorithm for purely continuous linear
time-invariant systems using block decompositions by S. Bogomolov, M. Forets,
G. Frehse, A. Podelski, C. Schilling and F. Viry [1].

### Fields

- `options` -- an `Options` structure that holds the algorithm-specific options

### Notes

The following options are available:

```julia
$(print_option_spec(options_BFFPS19()))
```

### Algorithm

We refer to [1] for technical details.

[1] [Reachability analysis of linear hybrid systems via block
decomposition](https://arxiv.org/abs/1905.02458).
Sergiy Bogomolov, Marcelo Forets, Goran Frehse, Kostiantyn Potomkin, Christian Schilling.
"""
struct BFFPS19 <: ContinuousPost
    options::TwoLayerOptions

    function BFFPS19(𝑂::Options)
        normalized_𝑂 = validate_and_wrap_options(𝑂, options_BFFPS19();
            validation=validation_BFFPS19,
            normalization=normalization_BFFPS19!)
        return new(normalized_𝑂)
    end
end

# convenience constructor from pairs of symbols
BFFPS19(𝑂::Pair{Symbol,<:Any}...) = BFFPS19(Options(Dict{Symbol,Any}(𝑂)))

# default options
BFFPS19() = BFFPS19(Options())

include("reach.jl")
include("reach_blocks.jl")

init(𝒫::BFFPS19, 𝑆::AbstractSystem, 𝑂::Options) = init!(𝒫, 𝑆, copy(𝑂))

function init!(𝒫::BFFPS19, 𝑆::AbstractSystem, 𝑂::Options)
    # state dimension for (purely continuous or purely discrete systems)
    𝑂copy = copy(𝑂)
    𝑂copy[:n] = statedim(𝑆)

    # solver-specific options (adds default values for unspecified options)
    𝑂validated = validate_solver_options_and_add_default_values!(𝑂copy)

    # :vars option; default: all variables
    if haskey_specified(𝒫.options, :vars)
        𝑂validated[:vars] = 𝒫.options[:vars]
    else
        𝑂validated[:vars] = 1:𝑂validated[:n]
    end

    # :partition option: use 1D blocks
    if haskey_specified(𝒫.options, :partition)
        𝑂validated[:partition] = 𝒫.options[:partition]
    else
        𝑂validated[:partition] = [[i] for i in 1:𝑂validated[:n]]
    end

    opD = 𝒫.options[:opD]
    @assert opD isa DecomposedDiscretePost "this continuous post operator works " *
                                       "only with DecomposedDiscretePost"
    HS = 𝒫.options[:HS]
    constrained_dims = constrained_dimensions(HS)
    out_vars = opD.options[:out_vars]
    loc_id = 𝒫.options[:loc_id]
    temp_vars = unique([out_vars; constrained_dims[loc_id]])
    temp_vars = get_variables_from_relevant_blocks(𝑂validated[:partition], temp_vars)
    opD.options[:temp_vars] = temp_vars
    guards_constraints = [guard(HS, trans) for trans in out_transitions(HS, loc_id)]
    𝑂validated[:vars] = temp_vars
    𝑂validated[:guards_proj] = [project(c, temp_vars) for c in guards_constraints]
    𝑂validated[:blocks] = compute_blocks(𝑂validated[:vars], 𝑂validated[:partition])

    # :block_options_init & :block_options_iter options:
    # set default according to :partition
    if !haskey_specified(𝒫.options, :block_options_init)
        𝑂validated[:block_options_init] =
            compute_default_block_options(𝑂validated[:partition])
    end
    if !haskey_specified(𝒫.options, :block_options_iter)
        𝑂validated[:block_options_iter] =
            compute_default_block_options(𝑂validated[:partition])
    end

    if 𝑂validated[:project_reachset]
        𝑂validated[:output_function] = nothing
    else
        𝑂validated[:output_function] = 𝑂validated[:projection_matrix]
    end

    return 𝑂validated
end

"""
    post(𝒫::BFFPS19, 𝑆::AbstractSystem, 𝑂::Options)

Calculate the reachable states of the given initial value problem using `BFFPS19`.

### Input

- `𝒫` -- post operator of type `BFFPS19`
- `𝑆` -- sytem, initial value problem for a continuous ODE
- `𝑂` -- algorithm-specific options
"""
function post(𝒫::BFFPS19, 𝑆::AbstractSystem, 𝑂_input::Options)
    𝑂 = TwoLayerOptions(merge(𝑂_input, 𝒫.options.specified), 𝒫.options.defaults)

    @assert 𝑂[:mode] == "reach" "the mode $(𝑂[:mode]) is not supported"

    info("Reachable States Computation...")
    @timing begin
        Rsets = reach_mixed(𝑆, 𝑂)

        info("- Total")
    end

    # Projection
    if 𝑂[:project_reachset]
        info("Projection...")
        RsetsProj = @timing project(Rsets, 𝑂)
    else
        RsetsProj = Rsets
    end

    return ReachSolution(RsetsProj, 𝑂_input)
end

function get_variables_from_relevant_blocks(partition, vars)
    result = Vector{Int}()
    for ur in partition
        if any(v ∈ ur for v in vars)
            append!(result, collect(ur))
        end
    end
    return result
end


value(x) = x
cuify(x) = error("To use LinSolveGPUFactorize, you must do `using CuArrays`")
promote_u0(u0,p,t0) = u0
promote_tspan(u0,p,tspan,prob,kwargs) = tspan
get_tmp(x) = nothing
isdistribution(u0) = false

function SciMLBase.tmap(args...)
  error("Zygote must be added to differentiate Zygote? If you see this error, report it.")
end

function __init__()
  @require ApproxFun="28f2ccd6-bb30-5033-b560-165f7b14dc2f" begin
    eval_u0(u0::ApproxFun.Fun) = false
  end

  @require Measurements="eff96d63-e80a-5855-80a2-b1b0885c5ab7" begin

    promote_u0(u0::AbstractArray{<:Measurements.Measurement},p::AbstractArray{<:Measurements.Measurement},t0) = u0
    promote_u0(u0,p::AbstractArray{<:Measurements.Measurement},t0) = eltype(p).(u0)

    value(x::Type{Measurements.Measurement{T}}) where {T} = T
    value(x::Measurements.Measurement) = Measurements.value(x)

    @inline fastpow(x::Measurements.Measurement, y::Measurements.Measurement) = x^y

    # Support adaptive steps should be errorless
    @inline function ODE_DEFAULT_NORM(u::AbstractArray{<:Measurements.Measurement,N},t) where {N}
      sqrt(sum(x->ODE_DEFAULT_NORM(x[1],x[2]),zip((value(x) for x in u),Iterators.repeated(t))) / length(u))
    end
    @inline function ODE_DEFAULT_NORM(u::Array{<:Measurements.Measurement,N},t) where {N}
      sqrt(sum(x->ODE_DEFAULT_NORM(x[1],x[2]),zip((value(x) for x in u),Iterators.repeated(t))) / length(u))
    end
    @inline ODE_DEFAULT_NORM(u::Measurements.Measurement,t) = abs(Measurements.value(u))
  end

  @require MonteCarloMeasurements="0987c9cc-fe09-11e8-30f0-b96dd679fdca" begin

    promote_u0(u0::AbstractArray{<:MonteCarloMeasurements.AbstractParticles},p::AbstractArray{<:MonteCarloMeasurements.AbstractParticles},t0) = u0
    promote_u0(u0,p::AbstractArray{<:MonteCarloMeasurements.AbstractParticles},t0) = eltype(p).(u0)

    value(x::Type{MonteCarloMeasurements.AbstractParticles{T,N}}) where {T,N} = T
    value(x::MonteCarloMeasurements.AbstractParticles) = mean(x.particles)

    @inline fastpow(x::MonteCarloMeasurements.AbstractParticles, y::MonteCarloMeasurements.AbstractParticles) = x^y

    # Support adaptive steps should be errorless
    @inline function ODE_DEFAULT_NORM(u::AbstractArray{<:MonteCarloMeasurements.AbstractParticles,N},t) where {N}
      sqrt(mean(x->ODE_DEFAULT_NORM(x[1],x[2]),zip((value(x) for x in u),Iterators.repeated(t))))
    end
    @inline function ODE_DEFAULT_NORM(u::AbstractArray{<:MonteCarloMeasurements.AbstractParticles,N},t::AbstractArray{<:MonteCarloMeasurements.AbstractParticles,N}) where {N}
      sqrt(mean(x->ODE_DEFAULT_NORM(x[1],x[2]),zip((value(x) for x in u),Iterators.repeated(value.(t)))))
    end
    @inline ODE_DEFAULT_NORM(u::MonteCarloMeasurements.AbstractParticles,t) = abs(value(u))
  end

  @require Unitful="1986cc42-f94f-5a68-af5c-568840ba703d" begin
    # Support adaptive errors should be errorless for exponentiation
    value(x::Type{Unitful.AbstractQuantity{T,D,U}}) where {T,D,U} = T
    value(x::Unitful.AbstractQuantity) = x.val
    @inline function ODE_DEFAULT_NORM(u::AbstractArray{<:Unitful.AbstractQuantity,N},t) where {N}
      sqrt(sum(x->ODE_DEFAULT_NORM(x[1],x[2]),zip((value(x) for x in u),Iterators.repeated(t))) / length(u))
    end
    @inline function ODE_DEFAULT_NORM(u::Array{<:Unitful.AbstractQuantity,N},t) where {N}
      sqrt(sum(x->ODE_DEFAULT_NORM(x[1],x[2]),zip((value(x) for x in u),Iterators.repeated(t))) / length(u))
    end
    @inline ODE_DEFAULT_NORM(u::Unitful.AbstractQuantity,t) = abs(value(u))
    @inline UNITLESS_ABS2(x::Unitful.AbstractQuantity) = real(abs2(x)/oneunit(x)*oneunit(x))
  end

  @require Tracker="9f7883ad-71c0-57eb-9f7f-b5c9e6d3789c" begin
    include("tracker.jl")
  end

  # Piracy, should get upstreamed
  @require CUDA="052768ef-5323-5732-b1bb-66c8b64840ba" begin
    cuify(x::AbstractArray) = CUDA.CuArray(x)
    function LinearAlgebra.ldiv!(x::CUDA.CuArray,_qr::CUDA.CUSOLVER.CuQR,b::CUDA.CuArray)
      _x = UpperTriangular(_qr.R) \ (_qr.Q' * reshape(b,length(b),1))
      x .= vec(_x)
      CUDA.unsafe_free!(_x)
      return x
    end
    # make `\` work
    LinearAlgebra.ldiv!(F::CUDA.CUSOLVER.CuQR, b::CUDA.CuArray) = (x = similar(b); ldiv!(x, F, b); x)
    default_factorize(A::CUDA.CuArray) = qr(A)

    ODE_DEFAULT_NORM(u::CUDA.CuArray,t) = sqrt(real(sum(abs2,u))/length(u))

    @inline function ODE_DEFAULT_NORM(u::CUDA.CuArray{<:ForwardDiff.Dual},t)
      sqrt(sum(abs2,value.(u)) / length(u))
    end

    @inline function ODE_DEFAULT_NORM(u::CUDA.CuArray{<:ForwardDiff.Dual},t::ForwardDiff.Dual)
      sqrt(sum(abs2,u) / length(u))
    end
  end

  @require ReverseDiff="37e2e3b7-166d-5795-8a7a-e32c996b4267" begin
    include("reversediff.jl")
  end

  @require Zygote="e88e6eb3-aa80-5325-afca-941959d7151f" begin
    function ∇tmap(cx, f, args...)
      ys_and_backs = SciMLBase.tmap((args...) -> Zygote._pullback(cx, f, args...), args...)
      if isempty(ys_and_backs)
        ys_and_backs, _ -> (NoTangent(),NoTangent())
      else
        ys, backs = Zygote.unzip(ys_and_backs)
        function ∇tmap_internal(Δ)
          Δf_and_args_zipped = SciMLBase.tmap((f, δ) -> f(δ), backs, Δ)
          Δf_and_args = Zygote.unzip(Δf_and_args_zipped)
          Δf = reduce(Zygote.accum, Δf_and_args[1])
          (Δf, Δf_and_args[2:end]...)
        end
        ys,∇tmap_internal
      end
    end

    function ∇responsible_map(cx, f, args...)
      ys_and_backs = SciMLBase.responsible_map((args...) -> Zygote._pullback(cx, f, args...), args...)
      if isempty(ys_and_backs)
        ys_and_backs, _ -> (NoTangent(),NoTangent())
      else
        ys, backs = Zygote.unzip(ys_and_backs)
        ys, function ∇responsible_map_internal(Δ)
          # Apply pullbacks in reverse order. Needed for correctness if `f` is stateful.
          Δf_and_args_zipped = SciMLBase.responsible_map((f, δ) -> f(δ), Zygote._tryreverse(SciMLBase.responsible_map, backs, Δ)...)
          Δf_and_args = Zygote.unzip(Zygote._tryreverse(SciMLBase.responsible_map, Δf_and_args_zipped))
          Δf = reduce(Zygote.accum, Δf_and_args[1])
          (Δf, Δf_and_args[2:end]...)
        end
      end
    end
  end

  @require GeneralizedGenerated="6b9d7cbe-bcb9-11e9-073f-15a7a543e2eb" begin
    SciMLBase.numargs(::GeneralizedGenerated.RuntimeFn{Args}) where Args = GeneralizedGenerated.from_type(Args) |> length
  end

  @require Pardiso="46dd5b70-b6fb-5a00-ae2d-e8fea33afaf2" begin
    mutable struct MKLPardisoFactorize
      A
      ps::Pardiso.MKLPardisoSolver
      verbose::Bool
      firsttime::Bool
    end
    MKLPardisoFactorize(;verbose=false) = MKLPardisoFactorize(nothing,Pardiso.MKLPardisoSolver(),verbose,true)
    function (p::MKLPardisoFactorize)(x,A,b,update_matrix=false;kwargs...)
      if p.firsttime
        Pardiso.set_phase!(p.ps, Pardiso.ANALYSIS)
        Pardiso.pardiso(p.ps, x, A, b)
        p.firsttime = false
      end

      if update_matrix
        Pardiso.set_phase!(p.ps, Pardiso.NUM_FACT)
        Pardiso.pardiso(p.ps, x, A, b)
        p.A = A
      end

      Pardiso.set_phase!(p.ps, Pardiso.SOLVE_ITERATIVE_REFINE)
      Pardiso.pardiso(p.ps, x, A, b)
    end
    function (p::MKLPardisoFactorize)(::Type{Val{:init}},f,u0_prototype)
      if eltype(u0_prototype) <: Complex
        mattype = Pardiso.COMPLEX_NONSYM
      else
        mattype = Pardiso.REAL_NONSYM
      end
      Pardiso.set_matrixtype!(p.ps, mattype)
      if p.verbose
          Pardiso.set_msglvl!(p.ps, Pardiso.MESSAGE_LEVEL_ON)
      end
      MKLPardisoFactorize(nothing,p.ps,p.verbose,true)
    end

    mutable struct PardisoFactorize
      A
      ps::Pardiso.PardisoSolver
      verbose::Bool
      firsttime::Bool
    end
    PardisoFactorize(;verbose=false) = PardisoFactorize(nothing,Pardiso.PardisoSolver(),verbose,true)
    function (p::PardisoFactorize)(x,A,b,update_matrix=false;kwargs...)
      if p.firsttime
        Pardiso.set_phase!(p.ps, Pardiso.ANALYSIS)
        Pardiso.pardiso(p.ps, x, A, b)
        p.firsttime = false
      end

      if update_matrix
        Pardiso.set_phase!(p.ps, Pardiso.NUM_FACT)
        Pardiso.pardiso(p.ps, x, A, b)
        p.A = A
      end

      Pardiso.set_phase!(p.ps, Pardiso.SOLVE_ITERATIVE_REFINE)
      Pardiso.pardiso(p.ps, x, A, b)
    end
    function (p::PardisoFactorize)(::Type{Val{:init}},f,u0_prototype)
      if eltype(u0_prototype) <: Complex
        mattype = Pardiso.COMPLEX_NONSYM
      else
        mattype = Pardiso.REAL_NONSYM
      end
      Pardiso.set_matrixtype!(p.ps, mattype)
      if p.verbose
          Pardiso.set_msglvl!(p.ps, Pardiso.MESSAGE_LEVEL_ON)
      end
      PardisoFactorize(nothing,p.ps,p.verbose,true)
    end

    mutable struct PardisoIterate
      A
      ps::Pardiso.PardisoSolver
      verbose::Bool
      firsttime::Bool
    end
    PardisoIterate(;verbose=false) = PardisoIterate(nothing,Pardiso.PardisoSolver(),verbose,true)
    function (p::PardisoIterate)(x,A,b,update_matrix=false;kwargs...)
      if p.firsttime
        Pardiso.set_phase!(p.ps, Pardiso.ANALYSIS)
        Pardiso.pardiso(p.ps, x, A, b)
        p.firsttime = false
      end

      #=
      # Pardiso iterative solver doesn't need a separate factorization phase
      # It will auto-update as it needs to.
      if update_matrix
        Pardiso.set_phase!(p.ps, Pardiso.NUM_FACT)
        Pardiso.pardiso(p.ps, x, A, b)
        p.A = A
      end
      =#

      Pardiso.set_phase!(ps, Pardiso.NUM_FACT_SOLVE_REFINE)
      Pardiso.pardiso(ps, X, A, B)
    end
    function (p::PardisoIterate)(::Type{Val{:init}},f,u0_prototype)
      if eltype(u0_prototype) <: Complex
        mattype = Pardiso.COMPLEX_NONSYM
      else
        mattype = Pardiso.REAL_NONSYM
      end
      Pardiso.set_matrixtype!(p.ps, mattype)
      Pardiso.set_solver!(ps, Pardiso.ITERATIVE_SOLVER)
      if p.verbose
          Pardiso.set_msglvl!(p.ps, Pardiso.MESSAGE_LEVEL_ON)
      end
      PardisoIterate(nothing,p.ps,p.verbose,true)
    end
  end
end


using Flux
using Transformers.Basic

struct Decoder
    layers::Array{TransformerDecoder}
end

Flux.@functor Decoder
Flux.trainable(decoder::Decoder) = (decoder.layers,)

function (decoder::Decoder)(target_embedding, encoded_embedding)
    embedding = target_embedding
    for transformer ∈ decoder.layers
        embedding = transformer(embedding, encoded_embedding)
    end
    return embedding
end

function Decoder(
    size::Integer,
    head::Integer,
    hs::Integer,
    ps::Integer,
    layer::Integer;
    act=relu,
    pdrop=0.1
)::Decoder
    return Decoder([
        TransformerDecoder(size, head, hs, ps, act=act, pdrop=pdrop)
        for i ∈ 1:layer
    ])
end


"""
Basic discrete multivariate parameter state type

# Constructors

## BasicDiscMuvParameterState{NI<:Integer, NR<:Real}(value::Vector{NI}, <optional arguments>)

Construct a basic discrete multivariate parameter state with some ``value``.

###  Optional arguments:

* ``diagnostickeys::Vector{Symbol}=Symbol[]``: the diagnostic keys of the state.
* ``::Type{NR}=Float64``: the element type of target-related fields.
* ``diagnosticvalues::Vector=Array(Any, length(diagnostickeys))``: the diagnostic values of the state.

## BasicDiscMuvParameterState{NI<:Integer, NR<:Real}(size::Integer, <optional arguments>)

Construct a basic discrete multivariate parameter state with with a ``value`` of specified ``size``.

###  Optional arguments:

* ``diagnostickeys::Vector{Symbol}=Symbol[]``: the diagnostic keys of the state.
* ``::Type{NI}=Integer``: the element type of the state value.
* ``::Type{NR}=Float64``: the element type of target-related fields.
* ``diagnosticvalues::Vector=Array(Any, length(diagnostickeys))``: the diagnostic values of the state.

# Examples

```julia
julia> state = BasicDiscMuvParameterState(Int64[0, 1], [:accept], Float64, [false])
Klara.BasicDiscMuvParameterState{Int64,Float64}([0,1],NaN,NaN,NaN,Bool[false],2,[:accept])

julia> state.value
2-element Array{Int64,1}:
 0
 1

julia> diagnostics(state)
Dict{Symbol,Bool} with 1 entry:
  :accept => false
```
"""
type BasicDiscMuvParameterState{NI<:Integer, NR<:Real} <: ParameterState{Discrete, Multivariate}
  "Vector value of basic discrete multivariate parameter state"
  value::Vector{NI}
  "Value of log-likelihood at the state's value"
  loglikelihood::NR
  "Value of log-prior at the state's value"
  logprior::NR
  "Value of log-target at the state's value"
  logtarget::NR
  "Diagnostic values associated with the sampling of the state"
  diagnosticvalues::Vector
  "Integer representing the length of vector value of basic discrete multivariate parameter state"
  size::Integer
  "Diagnostic keys associated with the sampling of the state"
  diagnostickeys::Vector{Symbol}
end

function BasicDiscMuvParameterState{NI<:Integer, NR<:Real}(
  value::Vector{NI},
  diagnostickeys::Vector{Symbol}=Symbol[],
  ::Type{NR}=Float64,
  diagnosticvalues::Vector=Array(Any, length(diagnostickeys))
)
  v = convert(NR, NaN)
  BasicDiscMuvParameterState{NI, NR}(value, v, v, v, diagnosticvalues, length(value), diagnostickeys)
end

BasicDiscMuvParameterState{NI<:Integer, NR<:Real}(
  size::Integer,
  diagnostickeys::Vector{Symbol}=Symbol[],
  ::Type{NI}=Int64,
  ::Type{NR}=Float64,
  diagnosticvalues::Vector=Array(Any, length(diagnostickeys))
) =
  BasicDiscMuvParameterState(Array(NI, size), diagnostickeys, NR, diagnosticvalues)

value_support{NI<:Integer, NR<:Real}(::Type{BasicDiscMuvParameterState{NI, NR}}) = Discrete
value_support(::BasicDiscMuvParameterState) = Discrete

variate_form{NI<:Integer, NR<:Real}(::Type{BasicDiscMuvParameterState{NI, NR}}) = Univariate
variate_form(::BasicDiscMuvParameterState) = Univariate

Base.eltype{NI<:Integer, NR<:Real}(::Type{BasicDiscMuvParameterState{NI, NR}}) = (NI, NR)
Base.eltype{NI<:Integer, NR<:Real}(::BasicDiscMuvParameterState{NI, NR}) = (NI, NR)

generate_empty(state::BasicDiscMuvParameterState) =
  BasicDiscMuvParameterState(state.size, state.diagnostickeys, eltype(state)...)