#  Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
#  This Source Code Form is subject to the terms of the Mozilla Public
#  License, v. 2.0. If a copy of the MPL was not distributed with this
#  file, You can obtain one at http://mozilla.org/MPL/2.0/.

# Returns the block expression inside a :let that holds the code to be run.
# The other block (not returned) is for declaring variables in the scope of the
# let.
function _let_code_block(ex::Expr)
    @assert isexpr(ex, :let)
    return ex.args[2]
end

function _error_curly(x)
    Base.error("The curly syntax (sum{},prod{},norm2{}) is no longer supported. Expression: $x.")
end

# generates code which converts an expression into a NodeData array (tape)
# parent is the index of the parent expression
# values is the name of the list of constants which appear in the expression
function _parse_NL_expr(m, x, tapevar, parent, values)
    if isexpr(x, :call) &&
       length(x.args) >= 2 &&
       (isexpr(x.args[2], :generator) || isexpr(x.args[2], :flatten))
        header = x.args[1]
        if _is_sum(header)
            operatorid = operator_to_id[:+]
        elseif _is_prod(header)
            operatorid = operator_to_id[:*]
        else
            error("Unrecognized expression $header(...)")
        end
        codeblock = :(
            let
            end
        )
        block = _let_code_block(codeblock)
        push!(block.args, :(push!($tapevar, NodeData(CALL, $operatorid, $parent))))
        parentvar = gensym()
        push!(block.args, :($parentvar = length($tapevar)))

        code = _MA.rewrite_generator(
            x.args[2],
            t -> _parse_NL_expr(m, t, tapevar, parentvar, values),
        )
        push!(block.args, code)
        return codeblock
    end

    if isexpr(x, :call)
        if isexpr(x.args[1], :.)
            # Functions like foo.bar cannot possibly be registered, because you
            # can register only with a symbol name.
            errorstring =
                "Unexpected function $(x.args[1]). See the " *
                "documentation on how to register a function."
            return :(error($errorstring))
        end
        if _is_sum(x.args[1]) || _is_prod(x.args[1])
            opname = x.args[1]
            errorstring =
                "$opname() can appear in nonlinear expressions " *
                " only if the argument is a generator statement, for example, " *
                "$opname(x[i] for i in 1:N)."
            return :(error($errorstring))
        end
        if length(x.args) == 2 && !isexpr(x.args[2], :...) # univariate
            code = :(
                let
                end
            )
            block = _let_code_block(code)
            @assert isexpr(block, :block)
            if haskey(univariate_operator_to_id, x.args[1])
                operatorid = univariate_operator_to_id[x.args[1]]
                push!(
                    block.args,
                    :(push!($tapevar, NodeData(CALLUNIVAR, $operatorid, $parent))),
                )
            else
                opname = quot(x.args[1])
                errorstring = "Unrecognized function \"$(x.args[1])\" used in nonlinear expression."
                errorstring2 = "Incorrect number of arguments for \"$(x.args[1])\" in nonlinear expression."
                lookupcode = quote
                    if $(esc(m)).nlp_data === nothing
                        error($errorstring)
                    end
                    if !haskey(
                        $(esc(m)).nlp_data.user_operators.univariate_operator_to_id,
                        $opname,
                    )
                        if haskey(
                            $(esc(m)).nlp_data.user_operators.multivariate_operator_to_id,
                            $opname,
                        )
                            error($errorstring2)
                        else
                            error($errorstring)
                        end
                    end
                    operatorid =
                        $(esc(m)).nlp_data.user_operators.univariate_operator_to_id[$opname] +
                        _Derivatives.USER_UNIVAR_OPERATOR_ID_START - 1
                end
                push!(
                    block.args,
                    :($lookupcode;
                    push!($tapevar, NodeData(CALLUNIVAR, operatorid, $parent))),
                )
            end
            parentvar = gensym()
            push!(block.args, :($parentvar = length($tapevar)))
            push!(block.args, _parse_NL_expr(m, x.args[2], tapevar, parentvar, values))
            return code
        else
            code = :(
                let
                end
            )
            block = _let_code_block(code)
            @assert isexpr(block, :block)
            if haskey(operator_to_id, x.args[1]) # fast compile-time lookup
                operatorid = operator_to_id[x.args[1]]
                push!(block.args, :(push!($tapevar, NodeData(CALL, $operatorid, $parent))))
            elseif haskey(comparison_operator_to_id, x.args[1])
                operatorid = comparison_operator_to_id[x.args[1]]
                push!(
                    block.args,
                    :(push!($tapevar, NodeData(COMPARISON, $operatorid, $parent))),
                )
            else # could be user defined
                opname = quot(x.args[1])
                errorstring = "Unrecognized function \"$(x.args[1])\" used in nonlinear expression."
                errorstring2 = "Incorrect number of arguments for \"$(x.args[1])\" in nonlinear expression."
                lookupcode = quote
                    if $(esc(m)).nlp_data === nothing
                        error($errorstring)
                    end
                    if !haskey(
                        $(esc(m)).nlp_data.user_operators.multivariate_operator_to_id,
                        $opname,
                    )
                        if haskey(
                            $(esc(m)).nlp_data.user_operators.univariate_operator_to_id,
                            $opname,
                        )
                            error($errorstring2)
                        else
                            error($errorstring)
                        end
                    end
                    operatorid =
                        $(esc(m)).nlp_data.user_operators.multivariate_operator_to_id[$opname] +
                        _Derivatives.USER_OPERATOR_ID_START - 1
                end
                push!(
                    block.args,
                    :($lookupcode; push!($tapevar, NodeData(CALL, operatorid, $parent))),
                )
            end
            parentvar = gensym()
            push!(block.args, :($parentvar = length($tapevar)))
            for i = 1:length(x.args)-1
                arg = x.args[i+1]
                if isexpr(arg, :...)
                    if !isa(arg.args[1], Symbol)
                        error(
                            "Unexpected expression in $x. JuMP supports " *
                            "splatting only symbols. For example, x... is " *
                            "ok, but (x + 1)..., [x; y]... and g(f(y)...) " *
                            "are not.",
                        )
                    end
                    push!(
                        block.args,
                        quote
                            for val in $(esc(arg.args[1]))
                                _parse_NL_expr_runtime(
                                    $(esc(m)),
                                    val,
                                    $tapevar,
                                    $parentvar,
                                    $values,
                                )
                            end
                        end,
                    )
                else
                    push!(block.args, _parse_NL_expr(m, arg, tapevar, parentvar, values))
                end
            end
            return code
        end
    end
    if isexpr(x, :comparison)
        code = :(
            let
            end
        )
        block = _let_code_block(code)
        op = x.args[2]
        operatorid = comparison_operator_to_id[op]
        for k = 2:2:length(x.args)-1
            @assert x.args[k] == op # don't handle a <= b >= c
        end
        parentvar = gensym()
        push!(block.args, :(push!($tapevar, NodeData(COMPARISON, $operatorid, $parent))))
        push!(block.args, :($parentvar = length($tapevar)))
        for k = 1:2:length(x.args)
            push!(block.args, _parse_NL_expr(m, x.args[k], tapevar, parentvar, values))
        end
        return code
    end
    if isexpr(x, :&&) || isexpr(x, :||)
        code = :(
            let
            end
        )
        block = _let_code_block(code)
        op = x.head
        operatorid = logic_operator_to_id[op]
        parentvar = gensym()
        push!(block.args, :(push!($tapevar, NodeData(LOGIC, $operatorid, $parent))))
        push!(block.args, :($parentvar = length($tapevar)))
        push!(block.args, _parse_NL_expr(m, x.args[1], tapevar, parentvar, values))
        push!(block.args, _parse_NL_expr(m, x.args[2], tapevar, parentvar, values))
        return code
    end
    if isexpr(x, :curly)
        _error_curly(x)
    end
    if isexpr(x, :...)
        error("Unexpected splatting expression $x.")
    end
    # at the lowest level?
    return :(_parse_NL_expr_runtime($(esc(m)), $(esc(x)), $tapevar, $parent, $values))

end

function _parse_NL_expr_runtime(m::Model, x::Real, tape, parent, values)
    push!(values, x)
    push!(tape, NodeData(VALUE, length(values), parent))
    nothing
end

function _parse_NL_expr_runtime(m::Model, x::VariableRef, tape, parent, values)
    if owner_model(x) !== m
        error(
            "Variable in nonlinear expression does not belong to the " *
            "corresponding model",
        )
    end
    push!(tape, NodeData(MOIVARIABLE, x.index.value, parent))
    nothing
end

function _parse_NL_expr_runtime(m::Model, x::NonlinearExpression, tape, parent, values)
    push!(tape, NodeData(SUBEXPRESSION, x.index, parent))
    nothing
end

function _parse_NL_expr_runtime(m::Model, x::NonlinearParameter, tape, parent, values)
    push!(tape, NodeData(PARAMETER, x.index, parent))
    nothing
end

function _parse_NL_expr_runtime(m::Model, x::AbstractArray, tape, parent, values)
    error("Unexpected array $x in nonlinear expression. Nonlinear expressions may contain only scalar expressions.")
end

function _parse_NL_expr_runtime(m::Model, x::GenericQuadExpr, tape, parent, values)
    error(
        "Unexpected quadratic expression $x in nonlinear expression. " *
        "Quadratic expressions (e.g., created using @expression) and " *
        "nonlinear expressions cannot be mixed.",
    )
end

function _parse_NL_expr_runtime(m::Model, x::GenericAffExpr, tape, parent, values)
    error(
        "Unexpected affine expression $x in nonlinear expression. " *
        "Affine expressions (e.g., created using @expression) and " *
        "nonlinear expressions cannot be mixed.",
    )
end

function _parse_NL_expr_runtime(m::Model, x, tape, parent, values)
    error("Unexpected object $x (of type $(typeof(x)) in nonlinear expression.")
end

function _expression_complexity(ex::Expr)
    return isempty(ex.args) ? 1 : sum(_expression_complexity, ex.args)
end
_expression_complexity(other) = 1

# This is separated from the macro version to make it available for other @NL*
# macros.
function _process_NL_expr(model, ex)
    # This is an arbitrary cutoff. See issue #1355.
    if _expression_complexity(ex) > 5000
        @warn "Processing a very large nonlinear expression with " *
              "@NLexpression/@NLconstraint/@NLobjective. This may be very " *
              "slow. Consider using setNLobjective() and addNLconstraint() " *
              "instead of the macros or reformulating the expressions using " *
              "sum() and prod() to make them more compact. The macros are " *
              "designed to process smaller, human-readable expressions."
    end
    parsed = _parse_NL_expr(model, ex, :tape, -1, :values)
    return quote
        tape = NodeData[]
        values = Float64[]
        $parsed
        _NonlinearExprData(tape, values)
    end
end

macro _process_NL_expr(model, ex)
    return _process_NL_expr(model, ex)
end

function _Derivatives.expr_to_nodedata(
    ex::VariableRef,
    nd::Vector{NodeData},
    values::Vector{Float64},
    parentid,
    r::_Derivatives.UserOperatorRegistry,
)
    push!(nd, NodeData(MOIVARIABLE, ex.index.value, parentid))
    nothing
end

function _Derivatives.expr_to_nodedata(
    ex::NonlinearExpression,
    nd::Vector{NodeData},
    values::Vector{Float64},
    parentid,
    r::_Derivatives.UserOperatorRegistry,
)
    push!(nd, NodeData(SUBEXPRESSION, ex.index, parentid))
    nothing
end

function _Derivatives.expr_to_nodedata(
    ex::NonlinearParameter,
    nd::Vector{NodeData},
    values::Vector{Float64},
    parentid,
    r::_Derivatives.UserOperatorRegistry,
)
    push!(nd, NodeData(PARAMETER, ex.index, parentid))
    nothing
end

# Construct a _NonlinearExprData from a Julia expression.
# VariableRef objects should be spliced into the expression.
function _NonlinearExprData(m::Model, ex::Expr)
    _init_NLP(m)
    _check_expr(m, ex)
    nd, values = _Derivatives.expr_to_nodedata(ex, m.nlp_data.user_operators)
    return _NonlinearExprData(nd, values)
end
_NonlinearExprData(m::Model, ex) = _NonlinearExprData(m, :($ex + 0))

# Error if:
# 1) Unexpected expression
# 2) VariableRef doesn't match the model
function _check_expr(m::Model, ex::Expr)
    if ex.head == :ref # if we have x[1] already in there, something is wrong
        error("Unrecognized expression $ex. JuMP variable objects and input coefficients should be spliced directly into expressions.")
    end
    for e in ex.args
        _check_expr(m, e)
    end
    return
end
function _check_expr(m::Model, v::VariableRef)
    owner_model(v) === m || error("Variable $v does not belong to this model.")
    return
end
_check_expr(m::Model, ex) = nothing


using PdfFraud
using GenerativeModels
using Flux
using ValueHistories
using ArgParse
using BSON
using GMExtensions
using CuArrays
using DelimitedFiles, DataFrames, CSV

# this is for running the script from REPL - default values
if length(ARGS) == 0
	push!(ARGS, "test")
end
# parse arguments
s = ArgParseSettings()
@add_arg_table s begin
    "modelpath"
    	default = ""
    	arg_type = String
    	help = "a master dir of where the models are saved"
    "--imodels"
    	default = [0]
    	help = "only compute models given by indices"
    	arg_type = Int
    	nargs = '+'
end
parsed_args = parse_args(ARGS, s)

# extract settings from the parsed arguments
modelpath = abspath(parsed_args["modelpath"])
reprpath = joinpath(dirname(modelpath), "representations")
mkpath(reprpath)
imodels = parsed_args["imodels"]

# get labeled data
data, labels, sha256, page_nums = PdfFraud.load_pdf_data()
data = data |> gpu;

# save the page info
lf = joinpath(reprpath, "index.csv")
index = DataFrame()
index[!,:sha256] = vcat(map(x->repeat([x[1]], x[2]), zip(sha256, page_nums))...)
index[!,:page] = vcat(map(x->1:x, page_nums)...)
CSV.write(lf, index)

# load model(s)
model_files = joinpath.(modelpath, readdir(modelpath))
@info "Found $(length(model_files)) saved models."
if imodels[1] != 0
	model_files = model_files[imodels]
end
@info "Continuing with $(length(model_files)) models."

# now get the representations
for mf in model_files
	model, history, exp_params = PdfFraud.load_model(mf)
	model = model |> gpu
	Z = PdfFraud.encode(model, data, 32, mean) |> cpu;

	# and save them so they can be easily read back
	zf = joinpath(reprpath, replace(basename(mf), ".bson"=>"_encodings.csv"))
	DelimitedFiles.writedlm(zf, Z', ',')
	@info "Succesfuly written $zf"
end


##############
#Cards, Rules and helper functions
##############
include("IntSet32.jl")
using Memoize
import Base: show, <, copy, print, isequal, ==, length
DEBUG = true
UNSAFE = false #@assert , type safety, etc. off

#The 32 Cards
#Tok 7es-tol (t7) piros Aszig (pA).
#Note: az also U, mint "Unter Knabe" from the original German deck

const t7 = CardSet32(UInt32(1) << 0)
const t8 = CardSet32(UInt32(1) << 1)
const t9 = CardSet32(UInt32(1) << 2)
const tU = CardSet32(UInt32(1) << 3)
const tF = CardSet32(UInt32(1) << 4)
const tK = CardSet32(UInt32(1) << 5)
const tT = CardSet32(UInt32(1) << 6)
const tA = CardSet32(UInt32(1) << 7)

const z7 = CardSet32(UInt32(1) << 8 )
const z8 = CardSet32(UInt32(1) << 9 )
const z9 = CardSet32(UInt32(1) << 10)
const zU = CardSet32(UInt32(1) << 11)
const zF = CardSet32(UInt32(1) << 12)
const zK = CardSet32(UInt32(1) << 13)
const zT = CardSet32(UInt32(1) << 14)
const zA = CardSet32(UInt32(1) << 15)

const m7 = CardSet32(UInt32(1) << 16)
const m8 = CardSet32(UInt32(1) << 17)
const m9 = CardSet32(UInt32(1) << 18)
const mU = CardSet32(UInt32(1) << 19)
const mF = CardSet32(UInt32(1) << 20)
const mK = CardSet32(UInt32(1) << 21)
const mT = CardSet32(UInt32(1) << 22)
const mA = CardSet32(UInt32(1) << 23)

const p7 = CardSet32(UInt32(1) << 24)
const p8 = CardSet32(UInt32(1) << 25)
const p9 = CardSet32(UInt32(1) << 26)
const pU = CardSet32(UInt32(1) << 27)
const pF = CardSet32(UInt32(1) << 28)
const pK = CardSet32(UInt32(1) << 29)
const pT = CardSet32(UInt32(1) << 30)
const pA = CardSet32(UInt32(1) << 31)

const fulldeck = anycard = xX = 🌠 = CardSet32(0xFFFFFFFF)
const emptydeck = nocard = ⬜ =CardSet32(0x00000000)

const deck = Dict([
    t7 => ("t7", "🎃 7"), t8 => ("t8", "🎃 8"), t9 => ("t9", "🎃 9"), tU => ("tU", "🎃 U"), tF => ("tF", "🎃 F"), tK => ("tK", "🎃 K"), tT => ("tT", "🎃 T"), tA => ("tA", "🎃 A"),
    z7 => ("z7", "🍃 7"), z8 => ("z8", "🍃 8"), z9 => ("z9", "🍃 9"), zU => ("zU", "🍃 U"), zF => ("zF", "🍃 F"), zK => ("zK", "🍃 K"), zT => ("zT", "🍃 T"), zA => ("zA", "🍃 A"),
    m7 => ("m7", "🌰 7"), m8 => ("m8", "🌰 8"), m9 => ("m9", "🌰 9"), mU => ("mU", "🌰 U"), mF => ("mF", "🌰 F"), mK => ("mK", "🌰 K"), mT => ("mT", "🌰 T"), mA => ("mA", "🌰 A"),
    p7 => ("p7", "❤ 7"), p8 => ("p8", "❤ 8"), p9 => ("p9", "❤ 9"), pU => ("pU", "❤ U"), pF => ("pF", "❤ F"), pK => ("pK", "❤ K"), pT => ("pT", "❤ T"), pA => ("pA", "❤ A")])

function print(io::IO, cs::CardSet32, shortForm=true)
    if isempty(cs) return end

    if shortForm
        currentSuit = notrump
        for card in cs
            suit, face = SuitFace(card)
            if suit == currentSuit
                print(io, faceProperties[face][end])
            else
                print(io, deck[card][1])
                currentSuit = suit
            end
        end
    else
        for card in cs
          print(io, deck[card][2], " ")
        end
    end
end

function print(io::IO, cs::Vector{CardSet32}, shortForm=true)
    for crd in cs
      print(io, crd, shortForm); println()
    end
end
function print(io::IO, cs::Vector{Vector{CardSet32}}, shortForm=true)
    for crd in cs
      print(io, crd, shortForm); println("---")
    end
end

display(cs::CardSet32) = print(stdout, cs, false)
display(cs::Vector{CardSet32}) = print(stdout, cs, false)
display(cs::Vector{Vector{CardSet32}}) = print(stdout, cs, false)

const Suit = CardSet32
    const t = union(t7,t8,t9,tU,tF,tK,tT,tA) #Tök
    const z = union(z7,z8,z9,zU,zF,zK,zT,zA) #Zöld
    const m = union(m7,m8,m9,mU,mF,mK,mT,mA) #Makk
    const p = union(p7,p8,p9,pU,pF,pK,pT,pA) #Piros
    const nosuit = notrump = CardSet32() #Szín nélküli (ászkirályos) vagy még nem tudjuk (pl. hátulról bemondott négy tízesnélÖ
    const anySuit = union(t, z, m, p) #any
    const a = b = c = d = CardSet32() #undefined... yet

const suitProperties = Dict([
    (t, (["Tök", "tok", "🎃", "t"], 3)), #Halloween-kor TOK = 7 :)
    (z, (["Zöld", "zold", "🍃", "z"], 4)),
    (m, (["Makk", "🌰", "m"], 5)),
    (p, (["Piros", "❤", "p"], 6)),
    (notrump, (["Színtelen", "notrump", "sz", "n"], 1)),
    ])

const suitStrings = Dict([
    ('t', t),
    ('z', z),
    ('m', m),
    ('p', p),
    ('n', notrump),
    ('x', anySuit),
    ('a', a), #adu?
    ('b', b),
    ('c', c),
    ('d', d),
    ])

const Face = CardSet32
    const _7 = union(t7, z7, m7, p7) #hetes
    const _8 = union(t8, z8, m8, p8) #nyolcas
    const _9 = union(t9, z9, m9, p9) #kilences
    const U  = union(tU, zU, mU, pU) #alsó
    const F  = union(tF, zF, mF, pF) #felső
    const K  = union(tK, zK, mK, pK) #király
    const T  = union(tT, zT, mT, pT) #tízes
    const A  = union(tA, zA, mA, pA) #ász
    const AT = union(T, A)
    const FK = union(F, K)
    const _78 = union(_7, _8)
    const X  = union(_7, _8, _9, U, F, K, T, A) #barmelyik
    const Y  = union(_7, _8, _9, U, F, K) #kicsi
    const Z  = union(_9, U, F, K)   #nem fontos

const faceProperties = Dict(
    _7 => ["Hetes", "7"],
    _8 => ["Nyolcas", "8"],
    _9 => ["Kilences", "9"],
    U => ["Alsó", "U"],
    F => ["Felső", "F"],
    K => ["Király", "K"],
    T => ["Tízes", "T"],
    A => ["Ász", "A"],
)

const faceStrings = Dict(
    '7' => _7,
    '8' => _8,
    '9' => _9,
    'U' => U,
    'F' => F,
    'K' => K,
    'T' => T,
    'A' => A,
    "AT" => AT,
    "FK" => FK,
    "78" => _78,
    'X' => X,
    'Y' => Y,
    'Z' => Z,
)
#TODO const faceProperties = Dict([(t, ("Tök", 3)), (z, ("Zöld", 4)), (m, ("Makk", 5)), (p, ("Piros", 6))]) #Halloween-kor TOK = 7 :)

const suitFace = Dict([
    t7 => (t, _7), t8 => (t, _8), t9 => (t, _9), tU => (t, U), tF => (t, F), tK => (t, K), tT => (t, T), tA => (t, A),
    z7 => (z, _7), z8 => (z, _8), z9 => (z, _9), zU => (z, U), zF => (z, F), zK => (z, K), zT => (z, T), zA => (z, A),
    m7 => (m, _7), m8 => (m, _8), m9 => (m, _9), mU => (m, U), mF => (m, F), mK => (m, K), mT => (m, T), mA => (m, A),
    p7 => (p, _7), p8 => (p, _8), p9 => (p, _9), pU => (p, U), pF => (p,F), pK => (p, K), pT => (p, T), pA => (p, A),
    (t, _7) => t7, (t, _8) => t8, (t, _9) => t9, (t, U) => tU, (t, F) => tF, (t, K) => tK, (t, T) => tT, (t, A) => tA,
    (z, _7) => z7, (z, _8) => z8, (z, _9) => z9, (z, U) => zU, (z, F) => zF, (z, K) => zK, (z, T) => zT, (z, A) => zA,
    (m, _7) => m7, (m, _8) => m8, (m, _9) => m9, (m, U) => mU, (m, F) => mF, (m, K) => mK, (m, T) => mT, (m, A) => mA,
    (p, _7) => p7, (p, _8) => p8, (p, _9) => p9, (p, U) => pU, (p, F) => pF, (p, K) => pK, (p, T) => pT, (p, A) => pA])

SuitFace(card::Card) = suitFace[card]
suitof(card::Card) = suitFace[card][1]
faceof(card::Card) = suitFace[card][2]
# Card(suit::Suit, face::Face) = suitFace[(suit, face)]
Card(suit::Suit, face::Face) = intersect(suit, face)

#compare two cards using the trump suit
@memoize Dict function trumps(card1::Card, card2::Card, trump::Suit)
    suit1, face1 = SuitFace(card1)
    suit2, face2 = SuitFace(card2)

    if suit1 != suit2
        return suit1 == trump
    else
        face1 = UInt64(face1.cs) #to make sure the <<= will not overflow
        face2 = UInt64(face2.cs)
        if trump == notrump #szintelen jateknal a tizes alulra megy, a kilences es az also koze
            if UInt64(_9.cs) < face1 != T face1 <<= 4 end #UFK a T fole, A meg feljebb
            if UInt64(_9.cs) < face2 != T face2 <<= 4 end #UFK a T fole, A meg feljebb
        end
        return face1 > face2 #MSBs are larger
    end
end

#TODO: trumping is a mess - rewrite
#support structure for trumping
largerThan = Dict{Tuple{Card, Suit}, CardSet32}()
for (card1, s1) in deck
    for (card2, s2) in deck
        for trump in [t, z, m, p, notrump]
            if !haskey(largerThan, (card1, trump))
                largerThan[(card1, trump)] = CardSet32()
            end
            if trumps(card2, card1, trump)
                largerThan[(card1, trump)] = union(card2, largerThan[(card1, trump)])
            end
        end
    end
end
function whichTrumps(cards::CardSet32, card::Card, trump::Suit)
    intersect(cards, largerThan[(card, trump)])
end

#returns the largest card in a set using the trump suit
@memoize Dict function largestCard(cards::CardSet32, trump::Suit, suit::Suit=trump) #TODO add currentSuit handling
    if length(cards) <= 1 return cards end
    card = first(cards)
    largerThanFirst = whichTrumps(cards, card, trump)
    if isempty(largerThanFirst)
        if trumpsAll(card, setdiff(cards, card), trump) #if we have no trump, there is no largest card
            return card
        else
            throw(ArgumentError("no largest card in set - define suit!"))
            # return CardSet32()
        end
    else
        return largestCard(largerThanFirst, trump)
    end
end

@memoize Dict function smallestCard(cards::CardSet32, trump::Suit, suit::Suit=trump) #TODO add currentSuit handling
    if length(cards) <= 1 return cards end
    card = last(cards)
    if length(whichTrumps(cards, card, trump)) == length(cards) - 1 #trumped by all
        return card
    else
        return smallestCard(cards - card, trump) #TODO only works for sameSuit and trump
    end
end

#compare one card to a set using the trump suit
@memoize Dict function trumpsAll(card1::Card, cards::CardSet32, trump::Suit)
    for card in cards
        if trumps(card, card1, trump) return false end
    end
    return true #no card trumped me
    # isempty(intersect(cards, largerThan[(card1, trump)])) #problem: suit (mA does not trump zT if trump is p)
end

@memoize Dict function trumpsNone(card1::Card, cards::CardSet32, trump::Suit)
    for card in cards
        if trumps(card1, card, trump) return false end
    end
    return true #I trumped no card
    # isempty(intersect(cards, largerThan[(card1, trump)])) #problem: suit (mA does not trump zT if trump is p)
end

#Bemondasok
# abban a sorrendben, ahogy egymashoz fuzik oket (ulti-repulo-40_100-negyAsz-durchmars)
const AlapBemondas = UInt8
    semmi = UInt8(0) #amig nincs semmi
    ulti = UInt8(1)
    repulo = UInt8(2)
    negyvenSzaz = UInt8(3)
    huszSzaz = UInt8(4)
    negyAsz = UInt8(5)
    durchmars = UInt8(6)
    redurchmars = UInt8(7)
    parti = passz = UInt8(8)
    betli = UInt8(9)
    rebetli = UInt8(10)
    negyTizes = UInt8(11)
    csendesUlti = UInt8(12)
    csendesDuri = UInt8(13)

#Bemondasok erteke es nevei (elso nev lesz kiirva)
#TODO: value -> (licit, max, min)
const alapBemondasProperties = Dict([
  (semmi,       (0,  ["", "semmi", ""])),
  (ulti,        (4,  ["Ulti", "ultimo", "U", "Ult"])),
  (repulo,      (4,  ["Repülő", "repulo", "R", "Rep"], )),
  (negyvenSzaz, (4,  ["40-100", "40 100", "40_100", "negyvenSzaz", "negyvenSzáz", "Negyven Szaz", "Negyven Száz", "40s"])),
  (huszSzaz,    (8,  ["20-100", "20 100", "20_100", "huszSzaz", "húszSzáz", "Husz Szaz", "Húsz Száz", "20s"])),
  (negyAsz,     (4,  ["4 Ász", "4 Asz", "negyAsz", "NégyÁsz", "Negy Asz", "Négy Ász", "4A"])),
  (durchmars,   (6,  ["Durchmars", "Dur"])),
  (redurchmars, (12, ["Terített Durchmars", "redurchmars", "TDur"])),
  (parti,       (1,  ["Parti", "Passz", "P", "Par"])),
  (betli,       (30, ["Betli", "B", "Bet"])), #mert szintelen, nincs szorzo
  (rebetli,     (20, ["Terített Betli", "rebetli", "TBet"])),
  (negyTizes,   (55, ["4 Tízes", "4 Tizes", "negyTizes", "NégyTízes", "Negy Tizes", "Négy Tízes", "4T"])),
  (csendesUlti, (2,  ["Csendes Ulti", "CsendesUlti", "Csu", "CsU"])),
  # csendesDuri disabled due to computational needs and rarity
  # (csendesDuri, (3,  ["Csendes Durchmars", "Csendes Duri", "CsendesDuri", "Csd", "CsD"])),
])

#Modosito szorzok elolrol bemondott vagy ramondott bemondasokra
const Modosito = UInt8
    elolrol =  UInt8(4)
    ramondva = UInt8(2)
    hatulrol = UInt8(1)
    sehonnan = UInt8(0)

const modositoProperties = Dict([
  (elolrol,  ["Elölről",  "elolrol",  "e", "E"]),
  (ramondva, ["Rámondva", "ramondva", "r", "R"]),
  (hatulrol, ["Hátulról", "hatulrol", "h", "H"]),
  (sehonnan, [""]),
])

#Players
const Player = UInt8
    p1=Player(1)
    p2=Player(2)
    p3=Player(3)
    p4=Player(4)
    p5=Player(5)
    p6=Player(6)
    pX=Player(7)
    pN=Player(7)
const playerNames = Dict([
    (p1, "Alfa "),
    (p2, "Béla "),
    (p3, "Gamma"),
    (p4, "Delta"),
    (p5, "Epszi"),
    (p6, "Tétova"),
    (pX, ""),    #barmelyik jatekos (null)
    (pN, ""),    #semelyik jatekos
    ])

nextPlayer(pl::Player, numberOfPlayers=3) = Player(pl % numberOfPlayers + 1)
nextPlayerOffset(pl::Player, offset::Int, numberOfPlayers=3) = Player(mod(pl - 1 + offset, numberOfPlayers) + 1)
previousPlayer(pl::Player, numberOfPlayers=3) = nextPlayer(pl, -1)
# const FELVEVO = p1
# const ELLENVONAL = [p2, p3]
# const ELLENFEL1 = p2
# const ELLENFEL2 = p3

#TODO - test
const Kontra = UInt8
KE = UInt8(4)
KH = UInt8(2)

const kontraProperties = Dict([
  (KE, ["Elölről kontra",  "KE"]),
  (KH, ["Hátulról kontra", "KH"]),
])

struct KontraElement
    kontrazo::Player #pX ha kozos kontra, kulon re/mordkontranal az eredeti kontrazo van itt
    kon::Vector{Kontra} #pl. Kontrak([elolrol,elolrol,hatulrol]) az elolrol kontra, elolrol rekontra es hatulrol szub - 32x
end

struct Kontrak
    kon::Vector{KontraElement} #pl. Kontrak([elolrol,elolrol,hatulrol]) az elolrol kontra, elolrol rekontra es hatulrol szub - 32x
end

function Kontrak()
    return Kontrak(Vector{KontraElement}())
end

function length(kontrak::Kontrak)
    return length(kontrak.kon)
end

function multiplier(kontrak::Kontrak)
    if length(kontrak) == 0
        return 1
    end
    if length(kontrak) == 1
        return prod(kontrak.kon[1].kon)
    end
    #TODO - test
    if length(kontrak) > 1
        result = [prod(ktr.kon) for ktr in kontrak.kon]
        if min(result...) == max(result...)
            return max(result...)
        end
        throw("ambigous multiplier")
    end
end

# const Kontrak = Array{Kontra, 1} #pl. Kontrak([KE,KE,KH]) az elolrol kontra, elolrol rekontra es hatulrol szub - 32x

#TODO
function show(Kontrak) end #kiirja a kontra, re, szub, mord, stb. -t

#Egy alapbemondas modositokkal
struct ContractElement
    modosito::Modosito
    bem::AlapBemondas
    kon::Kontrak
    val::Number
end

function ContractElement(suit, modosito, bem, kon, val)
    modosito = modosito == nothing ? sehonnan : modosito
    bem = bem == nothing ? semmi : bem
    kon = kon == nothing ? Kontrak() : kon
    val = val == nothing ? contractValues[(suit, bem, modosito)] * multiplier(kon) : val

    ContractElement(modosito, bem, kon, val)
end

function isequal(ce1::ContractElement, ce2::ContractElement)
    return  isequal(ce1.bem, ce2.bem) &&
            isequal(ce1.modosito, ce2.modosito) &&
            isequal(ce1.val, ce2.val) &&
            isequal(length(ce1.kon), length(ce2.kon))
end

==(ce1::ContractElement, ce2::ContractElement) = isequal(ce1::ContractElement, ce2::ContractElement)
hash(ce::ContractElement) = xor(hash(ce.bem), hash(ce.modosito), hash(ce.val), hash(ce.kon.kontrazo), hash(length(ce.kon)))
function print(io::IO, ce::ContractElement, shortFormat::Bool=true)
    print(io,
        shortFormat ? modositoProperties[ce.modosito][end] : modositoProperties[ce.modosito][1] * " ",
        shortFormat ? alapBemondasProperties[ce.bem][2][end] : alapBemondasProperties[ce.bem][2][1] * " ")

    for kontrael in ce.kon.kon
        if kontrael.kontrazo != pN

            if !shortFormat
                print(io, playerNames[kontrael.kontrazo]); print(io, ":")
            else
                print(io, kontrael.kontrazo)
            end

            for k in kontrael.honnan
                print(io, kontraProperties[k][2])
                if !shortFormat
                    print(io, " ")
                end
            end
        end
    end
end

display(ce::ContractElement) = print(stdout, ce, false)

#A bemondas
struct Contract
    felvevo::Player
    suit::Suit
    contracts::Vector{ContractElement}
    totalvalue::Number
end

function Contract(felvevo::Player, suit::Suit, contracts::Vector{ContractElement})
    #total value is the sum of contractElements, except for csendes bemondas
    totalvalue = sum(ce.val for ce in contracts if !(ce.bem in [csendesUlti, csendesDuri]))
    #contracts sorted in canonical order:
    #first modosito (elolrol->ramondva->hatulrol) then bemondas
    sort!(contracts, by=x->(-x.modosito, x.bem))
    return Contract(felvevo, suit, contracts, totalvalue)
end

const nocontract = Contract(pX, notrump,[],0)
isequal(c1::Contract, c2::Contract) = isequal(c1.suit, c2.suit) && isequal(c1.contracts, c2.contracts)
==(c1::Contract, c2::Contract) = isequal(c1::Contract, c2::Contract)

function copy(contract::Contract)
    #TODO: deep copy of contracts? - maybe not for performance reasons
    Contract(contract.felvevo, contract.suit, contract.contracts, contract.totalvalue)
end

@memoize function isUlti(contract::Contract)
    for ce in contract.contracts
        if ce.bem == ulti
            return true
        end
    end
    return false
end

@memoize function isRepulo(contract::Contract)
    for ce in contract.contracts
        if ce.bem == repulo
            return true
        end
    end
    return false
end

@memoize isUltiRepulo(contract::Contract) = isUlti(contract) && isRepulo(contract)

@memoize function isPartiNeeded(contract::Contract)
    if contract.suit == notrump
        return false
    end
    for ce in contract.contracts
        if ce.bem in [parti,negyvenSzaz,huszSzaz,durchmars,redurchmars,betli,rebetli,negyTizes]
            return false
        end
    end
    return true
end

@memoize function isKontra(contract::Contract)
    for ce in contract.contracts
        if length(ce.kon) > 1
            return true
        end
    end
    return false
end

@memoize function isParti(contract::Contract)
    for ce in contract.contracts
        if ce.bem == parti
            return true
        end
    end
    return false
end

@memoize function maxModosito(contract::Contract)
    result = sehonnan
    for ce in contract.contracts
        result = max(result, ce.modosito)
    end
    return result
end

contractValues = Dict{Tuple{Suit,AlapBemondas,Modosito}, Int}()
#TODO mit lehet elolrol hatulrol es ramondva bemondani
#szines bemondasok
for suit in [t, z, m, p]
    for bem in [parti, negyvenSzaz, ulti, repulo, negyAsz, durchmars, huszSzaz, rebetli, redurchmars, csendesUlti]
        for honnan in [elolrol, ramondva, hatulrol]
            contractValues[(suit, bem, honnan)] = UInt8(honnan) * suitProperties[suit][2] * alapBemondasProperties[bem][1]
        end
    end
end
#negy tizes
for suit in [t,z,m,p]
    contractValues[(suit, negyTizes, ramondva)] = 192
    contractValues[(suit, negyTizes, hatulrol)] = 55
end
#szintelen bemondasok
contractValues[(notrump, negyTizes, hatulrol)] = 55 #TODO: has to be declared and set later
contractValues[(notrump, betli, hatulrol)] = 30
contractValues[(notrump, redurchmars, hatulrol)] = 144

#all the strings needed for parsing lookups
# parseTokens = Vector()
# for properties in [suitProperties, faceProperties, deck, alapBemondasProperties, modositoProperties, kontraProperties]
#     for (key, val) in properties
#       #TODO
#         if isa(String, val)
#     end
# end
#
function print(io::IO, contract::Contract, shortFormat::Bool=true)

    if contract == nocontract return "" end
    if !shortFormat
        print(io, playerNames[contract.felvevo]); print(io, ":")
        print(io, suitProperties[contract.suit][1][1], " ")
        for ce in contract.contracts
            print(io, ce, shortFormat)
        end
        print(io, " ($(contract.totalvalue))")
    else
        print(io, contract.felvevo); print(io, ":")
        print(io, suitProperties[contract.suit][1][end])
        for ce in contract.contracts
            print(io, ce, shortFormat)
        end
    end

end

display(contract::Contract) = print(stdout, contract, false)

#TODO: write test cases for this
#This will raise an ArgumentException if not OK
function validateContract(contract::Contract, player::Player=pX, previousBemondas::Contract=nocontract, stage::Modosito=sehonnan)

    bemondasok = [ce.bem for ce in contract.contracts]
    previousBemondasok = [ce.bem for ce in previousBemondas.contracts]

    #validity checks
    # 1. no duplicate (ulti ulti)
    if unique(bemondasok) != bemondasok
        throw(ArgumentError("dupla bemondás"))
    end
    # 2. szintelen - szines (no notrump ulti)
    if contract.suit == notrump &&
        !isempty(bemondasok ∩ [ulti, parti, repulo, negyvenSzaz, huszSzaz, negyAsz, durchmars, csendesUlti])
        throw(ArgumentError("színtelen ulti/parti/repulo/negyvenSzaz/huszSzaz/durchmars/csendesUlti"))
    end
    if contract.suit != notrump && betli in bemondasok
        throw(ArgumentError("színes betli"))
    end
    # 3. parti vs. 40s, 20s, stb.
    if parti in bemondasok && !isempty(bemondasok ∩ [negyvenSzaz, huszSzaz, durchmars, redurchmars])
        throw(ArgumentError("Parti + negyvenSzaz/huszSzaz/durchmars/redurchmars"))
    end
    # 4. 4T/betli/reduri + semmi
    if length(bemondasok) > 1 &&
        (!isempty([negyTizes, betli, rebetli, semmi] ∩ bemondasok) ||
        contract.suit == notrump && redurchmars in bemondasok)

        throw(ArgumentError("4 Tízes/betli/rebetli/szintelen redurchmars csak egyedül állhat"))
    end
    #5: 40-100 + 20-100 = NO
    if negyvenSzaz in bemondasok && huszSzaz in bemondasok
        throw(ArgumentError("40-100 es 20-100 nem lehet együtt"))
    end

    #6. Elolrol, ramondva, hatulrol modosito stimmeljen
    modositok = [ce.modosito for ce in contract.contracts]
    if hatulrol in modositok && !isempty([elolrol, ramondva] ∩ modositok)
        throw(ArgumentError("Hátulról és elölről/rámondva bemondások együtt"))
    end
    if sehonnan in modositok && !isempty([elolrol, ramondva, hatulrol] ∩ modositok)
        throw(ArgumentError("Sehonnan és elölről/rámondva/hátulról bemondások együtt"))
    end
    for ce in contract.contracts
        if ce.modosito == ramondva &&
            (
                ce.bem in [parti, csendesUlti, betli, rebetli] ||
                (contract.suit == notrump && ce.bem == redurchmars)
            )
            throw(ArgumentError("Rámondva parti/csendesUlti/betli/szintelen redurchmars?"))
        end
        if ce.modosito == elolrol &&
            (
                ce.bem == negyTizes ||
                (contract.suit == notrump && ce.bem == redurchmars)
            )

            throw(ArgumentError("Elölről 4 Tízes/szintelen redurchmars?"))
        end
    end

    #7. Ramondas stimmeljen az elozo bemondassal
    #minden elolrol bemondas ott legyen, kiveve parti vs. 40sz 20sz, dur
    if stage == ramondva
        ramondottak = [ce.bem for ce in contract.contracts if ce.modosito == ramondva]
        @assert (player == previousBemondas.felvevo)
        for ce in contract.contracts
            if ce.modosito == elolrol && !(ce in previousBemondas.contracts)
                #parti elhagyhato ha van 40s, 20s vagy duri
                if !(ce == parti && !isempty([negyvenSzaz, huszSzaz, durchmars, redurchmars] ∩ ramondottak))
                    throw(ArgumentError("Elölről bemondás hova tűnt?"))
                end
            end
        end
    end

    # 8. Kontrak (csak azt lehet megkontrazni ami van, es ahonnan ahol vagyunk)
    if isKontra(contract)
        #make sure all bem are the same
        if bemondasok != previousBemondasok
            throw(ArgumentError("Kontránál bemondás nem tűnhet el vagy jelenhet meg"))
        end
        for i in 1:length(bemondasok)
            ce = contract.contracts[i]
            pce = previousBemondasok.contracts[i]
            if ce.modosito != pce.modosito
                throw(ArgumentError("Kontránál bemondás ugyanonnan kell hogy jöjjön (elölről, rámondvá, hátulról)"))
            end
            if length(ce.kon) < length(pce.kon)
                throw(ArgumentError("Kevesebb Kontra?"))
            elseif length(ce.kon) > length(pce.kon) + 1
                throw(ArgumentError("Dupla Kontra?"))
            elseif length(ce.kon) > length(pce.kon) #megkontrazta
                ujkontra = ce.kon.kontrazo[end]
                if stage == ramondva
                    throw(ArgumentError("rossz kontra"))
                elseif stage == elolrol && ujkontra != KE
                    throw(ArgumentError("Elölről kontra kéne ide"))
                elseif stage in [hatulrol, ramondva] && ujkontra != KH
                    throw(ArgumentError("Hátulról kontra kéne ide"))
                elseif stage == sehonnan
                    #NOP, probably just parsed
                end

                if ce.bem in [csendesUlti, csendesDuri]
                    throw(ArgumentError("Csendes Ulti/Duri kontra?"))
                end
    # 9. Sajat bemondast nem lehet kontrazni/szubkontrazni csak rekontrazni/mordkontrazni
                if  player == previousBemondas.felvevo && length(ce.kon) % 2 == 1
                    throw(ArgumentError("Saját bemondást nem lehet kontrázni/szubkontrázni csak rekontrázni/mordkontrázni"))
                end
    # TODO: 10. Hatulrol kontrak kezelese:
    # ha a jatek elkezdodott, lehet kontrazni, felvevo mindig rekontrazhat, + szub/mord stb.

    # TODO: 11. Kulon kontrazodo bemondasok (betli, negyTizes) kezelese:
            end
        end
    end

    #11. (nem ramondott) uj bemondas nagyobb legyen mint az elozo
    if contract.totalvalue < previousBemondas.totalvalue
        throw(ArgumentError("Bemondás kisebb mint az előző"))
    end

    #12. ramondott uj bemondas nem kell, hogy nagyobb legyen

    # previousStage = sehonnan
    # for ce in previousBemondas.contracts
    #     previousStage = max(previousStage, ce.modosito)
    # end
    if contract.totalvalue == previousBemondas.totalvalue &&
        !(player == previousBemondas.felvevo && stage == ramondva)  #&& previousStage == elolrol

        throw(ArgumentError("Bemondás ugyanakkora mint az előző"))
    end
end

#TODO: heuristic parsing
function parseContract(contractS::AbstractString, player::Player=pX, previousBemondas::Contract=nocontract, stage::Modosito=sehonnan)
    contractS = chomp(contractS)

    if isempty(contractS) || contractS=="n" || contractS=="nocontract"
        return nocontract
    end

    playerSep = split(contractS, ":")
    @assert (length(playerSep) <= 2)

    if length(playerSep) == 2
        player = parse(Player, playerSep[1])
        contractS = playerSep[2]
    end

    contractElements = Vector{ContractElement}()

    #TODO: test kontrazo, kontrak
    regexp = r"(?<suit>(t|z|m|p|a|b|c|d|s|n|nt|sz)?)(?<modosito>(E|R|H)?)(?<bemondas>(Passz|Parti|semmi|Par|Ult|Rep|4A|Dur|Bet|40s|20s|4T|Tbet|Tdur|Terb|Terd|TDur|TBet|CsU|Csu|ulti|Ulti|rep|U|u|R|r)?)(?<kontrazo>([0-9])?)(?<kontrak>((pX|pN|p1|p2|p3|p4|p5|p6)?(KE|KH|k|K|Kontra|kontra|KRe|KSub|KMord))*)*\s*" #(?<value>([0-9]*)?)
    # regexp_canonical = r"(?<suit>(t|z|m|p|a|b|c|d|n|x)?)(?<modosito>(E|R|H)?)(?<bemondas>(Par|Ult|Rep|4A|Dur|Bet|40s|20s|4T|TDur|TBet|CsU)?)(?<kontrak>(KE|KH)*)\s*" #(?<value>([0-9]*)?)

    suit = nosuit #remembered between contractElements
    modosito = sehonnan #remembered between contractElements

    for mtch in eachmatch(regexp, contractS)
        #DEBUG
        # matches = collect(eachmatch(regexp, contractS))
        # mtch=matches[1]
        # println(mtch)
        #DEBUG END

        #empty string match
        if isempty(mtch.match)
            continue
        end

        for (key,val) in suitProperties
            if mtch[:suit] in val[1]
                suit = key
            end
        end
        for (key,val) in modositoProperties
            if mtch[:modosito] in val
                modosito = key
            end
        end
        bemondas = semmi #this does not carry over
        for (key,val) in alapBemondasProperties
            if mtch[:bemondas] in val[2]
                bemondas = key
            end
        end

        kontrak = Kontrak() #this does not carry over
        # TODO - one more level, double-check
        allKontrakRegexp = r"(?<player>(pX|pN|p1|p2|p3|p4|p5|p6)?)(<allKontrak>(KE|KH|k|K|Kontra|kontra|KRe|KSub|KMord)*)\s*"
        for allKontrakMatch in eachmatch(allKontrakRegexp, String(mtch[:kontrak]))
            kontraRegexp = r"(?<kontra>(KE|KH|k|K|Kontra|kontra|KRe|KSub|KMord)?)\s*"
            for kontraMatch in eachmatch(kontraRegexp, String(allKontrakMatch[:allKontrak]))
                for (key,val) in kontraProperties
                    if kontraMatch[:kontra] in val
                        push!(kontrak, key)
                    end
                end
            end
            # push!(kontrak, key)
        end
        # value = isempty(mtch["value"]) ? nothing : parse(Int, mtch["value"])
        # ce = ContractElement(suit, modosito, bemondas, kontrak, value)

        ce = ContractElement(suit, modosito, bemondas, kontrak, nothing)
        push!(contractElements, ce)
    end

    #total value will be sum of contractElements, except for csendes bemondas
    contract = Contract(player, suit, contractElements)

    #add parti bemondas if needed - note: totalvalue does change!
    if isPartiNeeded(contract)
        extraParti = ContractElement(suit, maxModosito(contract), parti, nothing, nothing)
        push!(contract.contracts, extraParti)
        contract = Contract(contract.felvevo, contract.suit, contract.contracts)
    end

    #add csendes bemondas if needed - note: totalvalue does not change!
    if suit != notrump && !isUlti(contract)
        for ce in contract.contracts
            if ce.bem == parti
                csU = ContractElement(suit, ce.modosito, csendesUlti, nothing, nothing)
                push!(contract.contracts, csU)
                break
            end
        end
    end

    #This will raise an ArgumentException if not OK
    validateContract(contract, player, previousBemondas, stage)

    return contract
end

##############
#Tests
##############


export SimpleSimulatedDAQ, SimpleSimulatedDAQParams, simulateLangevinInduced

Base.@kwdef struct SimpleSimulatedDAQParams <: DAQParams
  "All configured channels of this DAQ device."
  channels::Dict{String, DAQChannelParams}

  temperatureRise::Union{Dict{String, typeof(1.0u"K")}, Nothing} = nothing
  temperatureRiseSlope::Union{Dict{String, typeof(1.0u"s")}, Nothing} = nothing
  phaseChange::Union{Dict{String, typeof(1.0u"rad/K")}, Nothing} = nothing
  amplitudeChange::Union{Dict{String, typeof(1.0u"T/K")}, Nothing} = nothing
end

"Create the params struct from a dict. Typically called during scanner instantiation."
function SimpleSimulatedDAQParams(dict::Dict{String, Any})
  mainSectionFields = ["temperatureRise", "temperatureRiseSlope", "phaseChange", "amplitudeChange"]
  return createDAQParams(SimpleSimulatedDAQParams, dict)
end

Base.@kwdef mutable struct SimpleSimulatedDAQ <: AbstractDAQ
  "Unique device ID for this device as defined in the configuration."
  deviceID::String
  "Parameter struct for this devices read from the configuration."
  params::SimpleSimulatedDAQParams
  "Flag if the device is optional."
	optional::Bool = false
  "Flag if the device is present."
  present::Bool = false
  "Vector of dependencies for this device."
  dependencies::Dict{String, Union{Device, Missing}}

  # The following fields are only used for the simulation state!
  # The desired values for phase and amplitude are left within the sequence controller.
  "Base frequency to derive drive field frequencies."
  baseFrequency::typeof(1.0u"Hz") = 125.0u"MHz"
  "Divider of the baseFrequency to determine the drive field frequencies"
  divider::Union{Array{Int64, 2}, Missing} = missing
  "Applied drive field phase."
  phase::Union{Array{typeof(1.0u"rad"), 3}, Missing} = missing
  "Applied drive field voltage."
  amplitude::Union{Array{typeof(1.0u"T"), 3}, Array{typeof(1.0u"V"), 3}, Missing} = missing
  "Waveform type: sine, triangle or custom"
  waveform::Union{Array{Waveform, 2}, Missing} = missing

  txRunning::Bool = false

  "Number of periods within a frame."
  numPeriodsPerFrame::Int64 = 1
  "Number of samples per period."
  rxNumSamplingPoints::Int64 = 1

  rxChannelIDMapping::Dict{String, Int64} = Dict{String, Int64}()
  rxChanIDs::Vector{String} = []
  refChanIDs::Vector{String} = []

  currentFrame::Int64 = 1
  currentPeriod::Int64 = 1
end

function init(daq::SimpleSimulatedDAQ)
  @debug "Initializing simple simulated DAQ with ID `$(daq.deviceID)`."

  daq.present = true
end

neededDependencies(::SimpleSimulatedDAQ) = [SimulationController]
optionalDependencies(::SimpleSimulatedDAQ) = [TxDAQController, SurveillanceUnit]

Base.close(daq::SimpleSimulatedDAQ) = nothing

function setup(daq::SimpleSimulatedDAQ, sequence::Sequence)
  setupTx(daq, sequence)
  setupRx(daq, sequence)
end

function setupTx(daq::SimpleSimulatedDAQ, sequence::Sequence)
  daq.baseFrequency = txBaseFrequency(sequence)

  channels = electricalTxChannels(sequence)
  periodicChannels = [channel for channel in channels if channel isa PeriodicElectricalChannel]
  stepwiseChannels = [channel for channel in channels if channel isa StepwiseElectricalTxChannel]

  if !isempty(stepwiseChannels)
    @warn "The simple simulated DAQ can only process periodic channels. Other channels are ignored."
  end

  if any([length(component.amplitude) > 1 for channel in periodicChannels for component in channel.components])
    error("The simple simulated DAQ cannot work with more than one period in a frame or frequency sweeps yet.")
  end

  # Initialize fields
  numChannels_ = length(channels)
  numComponents_ = length([component for channel in periodicChannels for component in channel.components])
  daq.divider = zeros(Int64, (numComponents_, numChannels_))
  daq.phase = fill(0.0u"rad", (numComponents_, numChannels_, 1))
  daq.amplitude = fill(0.0u"T", (numComponents_, numChannels_, 1))
  daq.waveform = fill(WAVEFORM_SINE, (numComponents_, numChannels_))

  for channel in periodicChannels
    scannerChannel = daq.params.channels[channel.id]
    channelMapping = scannerChannel.channelIdx # Switch to getter?

    # Activate corresponding receive channels
    if !isnothing(scannerChannel.feedback)
      feedbackChannelID = scannerChannel.feedback.channelID
      scannerFeedbackChannel = daq.params.channels[feedbackChannelID]
      feedbackChannelIdx = scannerFeedbackChannel.channelIdx # Switch to getter?
      push!(daq.refChanIDs, feedbackChannelID)
      daq.rxChannelIDMapping[feedbackChannelID] = feedbackChannelIdx
    end

    componentIndex = 1
    for component in channel.components
      daq.divider[componentIndex, channelMapping, 1] = component.divider
      daq.phase[componentIndex, channelMapping, 1] = component.phase[1] # Only one period is allowed for now
      daq.waveform[componentIndex, channelMapping] = component.waveform # Only one period is allowed for now
      daq.amplitude[componentIndex, channelMapping, 1] = component.amplitude[1] # Only one period is allowed for now
      componentIndex += 1
    end
  end
end

function setupRx(daq::SimpleSimulatedDAQ, sequence::Sequence)
  daq.numPeriodsPerFrame = acqNumPeriodsPerFrame(sequence)
  daq.rxNumSamplingPoints = rxNumSamplesPerPeriod(sequence)

  for channel in rxChannels(sequence)
    scannerChannel = daq.params.channels[channel.id]
    push!(daq.rxChanIDs, channel.id)
    daq.rxChannelIDMapping[channel.id] = scannerChannel.channelIdx
  end
end

function startTx(daq::SimpleSimulatedDAQ)
  daq.txRunning = true
end

function stopTx(daq::SimpleSimulatedDAQ)
  daq.txRunning = false
end

function setTxParams(daq::SimpleSimulatedDAQ, Γ; postpone=false)
# Needs to update period and frame
end

function currentFrame(daq::SimpleSimulatedDAQ)
  return daq.currentFrame
end

function currentPeriod(daq::SimpleSimulatedDAQ)
  return daq.currentPeriod
end

function disconnect(daq::SimpleSimulatedDAQ)
end

enableSlowDAC(daq::SimpleSimulatedDAQ, enable::Bool, numFrames=0,
              ffRampUpTime=0.4, ffRampUpFraction=0.8) = 1

function readData(daq::SimpleSimulatedDAQ, startFrame::Integer, numFrames::Integer, numBlockAverages::Integer=1)
  startPeriod = (startFrame-1)*daq.numPeriodsPerFrame+1
  numPeriods = numFrames*daq.numPeriodsPerFrame

  uMeasPeriods, uRefPeriods, t = readDataPeriods(daq, startPeriod, numPeriods)

  measShape = (size(uMeasPeriods, 1), size(uMeasPeriods, 2), daq.numPeriodsPerFrame, numFrames)
  uMeas = reshape(uMeasPeriods, measShape)
  refShape = (size(uRefPeriods, 1), size(uRefPeriods, 2), daq.numPeriodsPerFrame, numFrames)
  uRef = reshape(uRefPeriods, refShape)
  tShape = (size(t, 1), daq.numPeriodsPerFrame, numFrames)
  t = reshape(t, tShape)

  return uMeas, uRef, t
end

function readDataPeriods(daq::SimpleSimulatedDAQ, startPeriod::Integer, numPeriods::Integer, numBlockAverages::Integer=1)
  simCont = dependencies(daq, SimulationController)[1]

  uMeas = zeros(daq.rxNumSamplingPoints, length(daq.rxChanIDs), numPeriods)u"V"
  uRef = zeros(daq.rxNumSamplingPoints, length(daq.refChanIDs), numPeriods)u"V"

  numSendChannels = size(daq.divider, 2)
  if numSendChannels != length(daq.rxChanIDs) || numSendChannels != length(daq.refChanIDs)
    error("For the simple simulated DAQ we assume a matching number of send, feedback and receive channels.")
  end

  cycle = lcm(daq.divider)/daq.baseFrequency
  startTime = upreferred(cycle*(startPeriod-1))
  t = collect(range(startTime, stop=startTime+numPeriods*cycle-1/daq.baseFrequency, length=daq.rxNumSamplingPoints*numPeriods))

  for (sendChannelID, sendChannel) in [(id, channel) for (id, channel) in daq.params.channels if channel isa DAQTxChannelParams]
    sendChannelIdx = sendChannel.channelIdx

    # Work with field for now
    if dimension(daq.amplitude[1]) == dimension(u"T")
      factor = 1.0u"T/T"
    else
      factor = 1/scannerChannel.calibration
    end

    temperatureRise = daq.params.temperatureRise[sendChannelID]
    temperatureRiseSlope = daq.params.temperatureRiseSlope[sendChannelID]
    phaseChange = daq.params.phaseChange[sendChannelID]
    amplitudeChange = daq.params.amplitudeChange[sendChannelID]

    # The temperature rises asymptotically to Tᵢₙᵢₜ+temperatureRise
    Tᵢₙᵢₜ = initialCoilTemperatures(simCont, sendChannelID)
    if t[1] == 0.0u"s"
      ΔT = fill(0.0u"K", size(t))
      ΔT[2:end] = temperatureRise*t[2:end]./(t[2:end].+temperatureRiseSlope)
    else
      ΔT = temperatureRise*t./(t.+temperatureRiseSlope)
    end
    T = Tᵢₙᵢₜ .+ ΔT
    ΔB = ΔT*amplitudeChange
    Δϕ = ΔT*phaseChange

    uₜₓ = zeros(length(t))u"V"
    uᵣₓ = zeros(length(t))u"V"
    uᵣₑ = zeros(length(t))u"V"
    for componentIdx in 1:size(daq.amplitude, 1)
      f = daq.baseFrequency/daq.divider[componentIdx, sendChannelIdx]
      Bₘₐₓ = daq.amplitude[componentIdx, sendChannelIdx, 1]*factor
      ϕ = daq.phase[componentIdx, sendChannelIdx, 1]

      Bᵢ = Bₘₐₓ.*sin.(2π*f*t.+ϕ) # Desired, ideal field without drift
      Bᵣ = (Bₘₐₓ.+ΔB).*sin.(2π*f*t.+ϕ.+Δϕ) # Field with drift of phase and amplitude

      uₜₓ .+= Bᵢ.*sendChannel.calibration
      uᵣₓ .+= simulateLangevinInduced(t, Bᵣ, f, ϕ.+Δϕ) # f is not completely correct due to the phase change, but this is only a rough approximation anyways

      # Assumes the same induced voltage from the field as given out with uₜₓ,
      # just with a slight change in phase and amplitude
      uᵣₑ .+= Bᵣ.*sendChannel.calibration
    end

    # Assumes one reference and one measurement channel for each send channel
    uMeas[:, sendChannelIdx, :] = reshape(uᵣₓ, (daq.rxNumSamplingPoints, 1, numPeriods))
    uRef[:, sendChannelIdx, :] = reshape(uᵣₑ, (daq.rxNumSamplingPoints, 1, numPeriods))

    # Update temperature in simulation controller for further use in e.g. simulated temperature sensors
    currentCoilTemperatures(simCont, sendChannelID, T[end])
  end

  totalPeriods = daq.currentPeriod+numPeriods
  daq.currentPeriod = totalPeriods % daq.numPeriodsPerFrame
  daq.currentFrame = totalPeriods ÷ daq.numPeriodsPerFrame

  return uMeas, uRef, reshape(t, (daq.rxNumSamplingPoints, numPeriods))
end

numTxChannelsTotal(daq::SimpleSimulatedDAQ) = 10 # Arbitrary number, since we are just simulating
numRxChannelsTotal(daq::SimpleSimulatedDAQ) = 10 # Arbitrary number, since we are just simulating
numTxChannelsActive(daq::SimpleSimulatedDAQ) = length([channel for (id, channel) in daq.params.channels if channel isa TxChannelParams])
numRxChannelsActive(daq::SimpleSimulatedDAQ) = numRxChannelsReference(daq)+numRxChannelsMeasurement(daq)
numRxChannelsReference(daq::SimpleSimulatedDAQ) = length(daq.refChanIDs)
numRxChannelsMeasurement(daq::SimpleSimulatedDAQ) = length(daq.rxChanIDs)
numComponentsMax(daq::SimpleSimulatedDAQ) = 1
canPostpone(daq::SimpleSimulatedDAQ) = false
canConvolute(daq::SimpleSimulatedDAQ) = false

"Very, very basic simulation of an MPI signal using the Langevin function."
function simulateLangevinInduced(t::Vector{typeof(1.0u"s")}, B::Vector{typeof(1.0u"T")}, f::typeof(1.0u"Hz"), ϕ::Union{typeof(1.0u"rad"), Vector{typeof(1.0u"rad")}})
  Bₘₐₓ = maximum(B)

  c = 0.5e-6 # mol/m^3
  ν = 22.459e3 # mol/m^3
  Mₛ = 477e3 # A/m (for magnetite)
  Dₖ = 30/1e9 # m
  μ₀ = 4π*1e-7 # Vs/Am

  k_B = 1.38064852*1e-23 # J/K (Boltzmann constant)
  Tₐ = 273.15+20 # K

  ξ = (π*Dₖ^3*Mₛ*ustrip.(u"T", B))/(3*k_B*Tₐ)
  ξ̇ = (2π^2*Dₖ^3*Mₛ*ustrip.(u"Hz", f)*ustrip.(u"T", Bₘₐₓ))/(3*k_B*Tₐ)*cos.(2π*ustrip.(u"Hz", f)*ustrip.(u"s", t).+ustrip.(u"rad", ϕ))
  Ṁ = c/(3*ν)*Mₛ*(-ξ̇.*csch.(ξ).^2 + ξ̇./(ξ.^2))

  u = -Ṁ; # Yes, this is wrong since it doesn't reflect the coil geometry or anything else, but I don't care at this point

  scalingFactor = 1/maximum(x->isnan(x) ? -Inf : x, u)
  u = u.*scalingFactor # Scale to ±1 for the following correction and leave it like that; we assume a nice LNA ;)

  # This at least makes the function work for most values of ϕ.
  # I don't think we have to care about all special cases here.
  if ϕ == 0.0
    indices = findall(x->isnan(x), u)
    for index in indices
      if index < length(u)
        u[index] = sign(u[index+1])
      else
        u[index] = sign(u[index-1])
      end
    end

    # Detect jumps, and yes this is crude...
    for i in 1:length(u)-1
      du = u[i]-u[i+1]
      if du > 0.2
        u[i+1] = sign(u[i+2])
      end
    end
  end

  return u*u"V"
end


using GLVisualize, GLWindow, Reactive, GeometryTypes, GLAbstraction

const RS    = GLVisualize.ROOT_SCREEN

function y_partition(area, percent)
	amount = percent / 100.0
	p = lift(area) do r
		(Rectangle{Int}(r.x, r.y, r.w, round(Int, r.h*amount)),
			Rectangle{Int}(r.x, round(Int, r.h*amount), r.w, round(Int, r.h*(1-amount))))
	end
	return lift(first, p), lift(last, p)
end
function x_partition(area, percent)
	amount = percent / 100.0
	p = lift(area) do r
		(Rectangle{Int}(r.x, r.y, round(Int, r.w*amount), r.h ),
			Rectangle{Int}(round(Int, r.w*amount), r.y, round(Int, r.w*(1-amount)), r.h))
	end
	return lift(first, p), lift(last, p)
end

main_area, write_area = y_partition(RS.area, 20.0)

const write_screen 	= Screen(RS, area=main_area)
const main_screen 	= Screen(RS, area=write_area)
function fixedscreen(parent, area)
	inputs = parent.inputs
	area = lift(intersect, parent.area, area)
	children = Screen[]
	#checks if mouse is inside screen
	insidescreen = lift(inputs[:mouseposition]) do mpos
		isinside(area.value, mpos...) && !any(children) do screen
			isinside(screen.area.value, mpos...)
		end
	end
	wsize = lift(x->Vector4(x.x, x.y, x.w, x.h), area)
	ocamera = OrthographicCamera(
		wsize,
		Input(eye(Mat4)),
		Input(-1000f0), # nearclip
		Input(1000f0)	# farclip
	)
	camera_input = merge(inputs, Dict(
		:mouseposition 	=> keepwhen(insidescreen, Vector2(0.0), inputs[:mouseposition]),
		:scroll_x 		=> keepwhen(insidescreen, 0.0, 			inputs[:scroll_x]),
		:scroll_y 		=> keepwhen(insidescreen, 0.0, 			inputs[:scroll_y]),
		:window_size 	=> lift(x->Vector4(x.x, x.y, x.w, x.h), area)
	))
	pcamera = PerspectiveCamera(camera_input, Vec3(2), Vec3(0))
    screen = Screen(area, parent, children, camera_input, RenderObject[], Input(true), parent.hasfocus, pcamera, ocamera, parent.nativewindow)
	push!(parent.children, screen)
	screen
end

text_stream 		= lift(x->"Ima push this so hard\nDude yeah neva now rite?$x\n", fpswhen(RS.inputs[:open], 1.0))
const scroll_window = foldl(+, 0.0, main_screen.inputs[:scroll_y])
visualize(text_stream.value, screen=write_screen)
lift(text_stream) do teeext
	area_handle	= Input(Rectangle{Int}(0,200*length(main_screen.children), main_screen.area.value.w, 180))
	area  		= lift(area_handle, main_screen.area, scroll_window) do h_area, m_area, y
		Rectangle{Int}(h_area.x, h_area.y+(round(Int, y)*50), m_area.w, h_area.h)
	end

	new_screen  = fixedscreen(main_screen, area)
	robj 		= visualize(teeext, screen=new_screen)

	view(visualize(lift(x->Rectangle(0,0,x.w, x.h), area), screen=new_screen), new_screen)
	view(robj, new_screen)
	nothing
end

renderloop()


# DOCOPT faster than argparse, but a little sensitive to spacing
doc = """We use this to find an MPPI trajectory. UW CSE Motion Control Lab.

Usage:
   PISQTrain.jl [(<experiment> <name>)]
   PISQTrain.jl -h | --help

Options:
   -h --help   Show this screen.

"""

using DocOpt
opt = docopt(doc, ARGS)

@everywhere push!(LOAD_PATH, "./")

using Common: plotlines

prefix="/tmp/"
modeldir="$(pwd())/models"
meshfile = "$(modeldir)/meshes/" # hack for now...

expfile=opt["<experiment>"]
expname=opt["<name>"]
myseed = 12345

@eval @everywhere const SEED = $myseed
@everywhere push!(LOAD_PATH, "$(pwd())")

println("Experiment called $(expname)")
println("Loading experiment parameters from $(expfile)")

################################################### start work by loading files
using mjWrap
using MPPI
using ExpFunctions
using LearningStrategies

@eval @everywhere const mjw = mjWrap # scoping issues...

const METHOD = :MPPI
include("$(pwd())/$(expfile)") # LOADING TASK FILE with PARAMETERS
@assert isdefined(:mppi)
@assert isdefined(:mppi_specs)
@assert isdefined(:myfuncs)

# plot stoc & mean every other iter
evaluatepolicy = IterFunction((model,i)->push!(model.trace[:evalscore], myfuncs.evaluate(model.mjsys, model.samples)))

const NITER = 100
plotR = IterFunction(NITER, (model,i)->plotlines(i,"Costs",
                                                 (model.trace[:meanR],"Min"),
                                                 (model.trace[:stocR],"Avg")))

plotEval = IterFunction(NITER, (model,i)->plotlines(i,"Evaluation",
                                                    (model.trace[:evalscore],"")))

plotCTRL = IterFunction(NITER, (model,i)->plotlines(i,"Ctrl",
                                                    model.samples.ctrl[:,:,1]))

plotState = IterFunction(NITER, (model,i)->plotlines(i,"State",
                                                     model.traj.state[1:3,1:i,1])) # X axis plot for now

# meta strategies: order matters!!
mppi_strat = strategy(ControlExp(prefix, expfile,
                                 expname, meshfile), # setup some things, hook for save
                      MPPIRollout(myfuncs),
                      mppi_specs,                  # main loop

                      #evaluatepolicy,             # hooks

                      plotR,

                      #plotEval,
                      plotCTRL,

                      #plotState,
                      MaxIter(mppi.niter)) #Verbose(MaxIter(mppi.niter)))

@time learn!(mppi, mppi_strat)

########################## done


module IncompressibleModels

using KernelAbstractions: @index, @kernel, Event, MultiEvent
using KernelAbstractions.Extras.LoopInfo: @unroll

using Oceananigans.Utils: launch!

import Oceananigans: fields

#####
##### IncompressibleModel definition
#####

include("incompressible_model.jl")
include("non_dimensional_model.jl")
include("show_incompressible_model.jl")
include("set_incompressible_model.jl")

#####
##### Time-stepping IncompressibleModels
#####

"""
    fields(model::IncompressibleModel)

Returns a flattened `NamedTuple` of the fields in `model.velocities` and `model.tracers`.
"""
fields(model::IncompressibleModel) = merge(model.velocities, model.tracers)

include("update_hydrostatic_pressure.jl")
include("update_incompressible_model_state.jl")
include("pressure_correction.jl")
include("velocity_and_tracer_tendencies.jl")
include("calculate_tendencies.jl")
include("correct_incompressible_immersed_tendencies.jl")

end # module


module ElPhDynamics

using LinearAlgebra
using FFTW
using TOML

#######################
## INCLUDING MODULES ##
#######################

include("Utilities.jl")

include("UnitCells.jl")

include("Lattices.jl")

include("Checkerboard.jl")

include("IterativeSolvers.jl")

include("TimeFreqFFTs.jl")

include("Models.jl")

include("KPMPreconditioners.jl")

include("InitializePhonons.jl")

include("PhononAction.jl")

include("FourierAcceleration.jl")

include("LangevinDynamics.jl")

include("HMC.jl")

include("SpecialUpdates.jl")

include("GreensFunctions.jl")

include("SimulationParams.jl")

include("Measurements.jl")

include("MuFinder.jl")

include("RunSimulation.jl")

include("SimulationSummary.jl")

include("ProcessInputFile.jl")

####################################
## DEFINING HIGHET LEVEL FUNCTION ##
##     TO RUN A SIMULATION        ##
####################################

using ..RunSimulation: run_simulation!
using ..ProcessInputFile: process_input_file, process_checkpoint, initialize_model
using ..Models: read_phonons!
using ..MuFinder: MuTuner, update_μ!
using ..SimulationSummary: write_simulation_summary!

export simulate, load_model

"""
Highest level function used to run a QMC simulation.
To run a simulation execute the following command:
`julia -O3 -e "using ElPhDynamics; simulate(ARGS)" -- input.toml`
"""
function simulate(args)

    # set the number of threads used by BLAS and FFTW libraries
    BLAS.set_num_threads(1)
    FFTW.set_num_threads(1)

    ######################
    ## READ CONFIG FILE ##
    ######################

    # getting iput filename
    config_file = args[1]

    # read input file
    input = TOML.parsefile(config_file)

    # construct data folder name
    filepath   = input["simulation"]["filepath"]
    foldername = input["simulation"]["foldername"]
    if length(args)==1
        datafolder = name_datafolder(filepath,foldername)
    else
        id = parse(Int,args[2])
        datafolder = name_datafolder(filepath,foldername,id)
    end
    input["simulation"]["datafolder"] = datafolder

    ###########################
    ## INITIALIZE SIMULATION ##
    ###########################

    if isdir(datafolder) # resume previous simulation

        # extract state from checkpoint
        (model, Gr, μ_tuner, sim_params, simulation_dynamics, burnin_dynamics,
         burnin_reflect_update, sim_reflect_update, burnin_swap_update, sim_swap_update,
         fourier_accelerator, preconditioner, container, burnin_start, sim_start, sim_stats) = process_checkpoint(input)

    else # start new simulation

        # initialize new simulation
        (model, Gr, μ_tuner, sim_params, simulation_dynamics, burnin_dynamics,
         burnin_reflect_update, sim_reflect_update, burnin_swap_update, sim_swap_update,
         fourier_accelerator, preconditioner, container, burnin_start, sim_start, sim_stats) = process_input_file(config_file,input)
    end

    ####################
    ## RUN SIMULATION ##
    ####################

    if sim_start < sim_params.nsteps
        sim_stats = run_simulation!(model, Gr, μ_tuner, sim_params, simulation_dynamics, burnin_dynamics,
                                    sim_reflect_update, burnin_reflect_update, sim_swap_update, burnin_swap_update,
                                    fourier_accelerator, container, preconditioner, sim_stats, burnin_start, sim_start)
    end

    #################################
    ## SUMARIZE SIMULATION RESULTS ##
    #################################

    if sim_start < sim_params.nsteps
        write_simulation_summary!(datafolder)
    end

    return nothing
end

"""
Pass this function a directory name generated by a simulation and it returns a HolsteinModel object
with the phonons intialized to the final phonon configuration sampled in the simulation.
"""
function load_model(dir::String)

    files  = readdir(dir)
    config = findall(f -> endswith(f, r"\.toml|\.TOML"), files)
    phonon = findall(f -> endswith(f, "_config.out"), files)
    @assert length(config) == length(phonon) == 1
    config_file = joinpath(dir, files[config[1]])
    phonon_file = joinpath(dir, files[phonon[1]])
    input = TOML.parsefile(config_file)
    input["simulation"]["datafolder"] = dir
    model = initialize_model(input)
    read_phonons!(model, phonon_file)

    return model
end

###########################################
## PRIVATE METHODS THAT ARE NOT EXPORTED ##
###########################################

"""
Name the data folder.
"""
function name_datafolder(filepath::String,foldername::String,id::Int=0)

    # data folder name without id at end
    key = joinpath(filepath,foldername)

    if id>0 # if ID given then attach to end of key
        datafolder = "$key-$id"
    else # determine the ID
        id = 1
        while true
            datafolder = "$key-$id"
            if !isdir(datafolder)
                break
            else
                id += 1
            end
        end
    end

    return datafolder
end

end


# Largely inspired by CPLEXCP.jl.
# Main differences:
# - no support for quadratic functions (for now?)
# - no support for adding int/bool constraint on variables (only at creation)
# TODOs:
# - objective
# - var/cons name
# - parameters: number of threads, time limit, raw
# - looks like JaCoP is as immutable as possible: looks like it's impossible to change the name of a variable, e.g.

@enum(VariableType, CONTINUOUS, BINARY, INTEGER, SET, CIRCUIT)

mutable struct VariableInfo
    index::MOI.VariableIndex
    variable::Variable
    name::String # TODO: remove me?
    type::VariableType

    # Variable bounds: either integers or floats. Needed for is_valid on VariableIndex
    # and bound constraints.
    lb::Union{Nothing, Integer, Float64}
    ub::Union{Nothing, Integer, Float64}
end

function VariableInfo(index::MOI.VariableIndex, variable::Variable)
    return VariableInfo(
        index,
        variable,
        "",
        INTEGER,
        nothing,
        nothing,
    )
end

mutable struct ConstraintInfo
    # Only necessary information to access an existing constraint, delete it when needed.
    index::MOI.ConstraintIndex
    constraint::Union{Constraint, Nothing}
    f::Union{MOI.AbstractScalarFunction, MOI.AbstractVectorFunction}
    set::MOI.AbstractSet
    name::String
end

function ConstraintInfo(
    index::MOI.ConstraintIndex,
    constraint::Union{Constraint, Nothing},
    f::Union{MOI.AbstractScalarFunction, MOI.AbstractVectorFunction},
    set::MOI.AbstractSet,
)
    return ConstraintInfo(index, constraint, f, set, "")
end

mutable struct Optimizer <: MOI.AbstractOptimizer
    # The low-level JaCoP store.
    inner::Store

    # The model name.
    name::String

    # A mapping from the MOI.VariableIndex to the CPLEX variable object.
    # VariableInfo also stores some additional fields like the type of variable.
    variable_info::CleverDicts.CleverDict{MOI.VariableIndex, VariableInfo}

    # A mapping from the MOI.ConstraintIndex to the CPLEX variable object.
    # VariableInfo also stores some additional fields like the type of variable.
    constraint_info::Dict{MOI.ConstraintIndex, ConstraintInfo}

    # # Memorise the objective sense and the function separately, as the Concert
    # # API forces to give both at the same time.
    # objective_sense::MOI.OptimizationSense
    # objective_function::Union{Nothing, MOI.AbstractScalarFunction}
    # objective_function_cp::Union{Nothing, NumExpr}
    # objective_cp::Union{Nothing, IloObjective}

    # # Cache parts of a solution.
    # cached_solution_state::Union{Nothing, Bool}

    # # Mappings from variable and constraint names to their indices. These are
    # # lazily built on-demand, so most of the time, they are `nothing`.
    # # The solver's functionality is not useful in this case, as it can only
    # # handle integer variables. Moreover, bound constraints do not have names
    # # for the solver.
    # name_to_variable::Union{Nothing, Dict{String, MOI.VariableIndex}}
    # name_to_constraint::Union{Nothing, Dict{String, MOI.ConstraintIndex}}

    """
        Optimizer()

    Create a new Optimizer object.
    """
    function Optimizer()
        model = new()
        model.inner = Store()

        model.variable_info =
            CleverDicts.CleverDict{MOI.VariableIndex, VariableInfo}()
        model.constraint_info = Dict{MOI.ConstraintIndex, ConstraintInfo}()

        # model.objective_sense = MOI.FEASIBILITY_SENSE
        # model.objective_function = nothing
        # model.objective_function_cp = nothing
        # model.objective_cp = nothing

        # model.cached_solution_state = nothing
        # model.callback_state = CB_NONE

        MOI.empty!(model)
        return model
    end
end

Base.show(io::IO, model::Optimizer) = show(io, model.inner)

function MOI.empty!(model::Optimizer)
    model.inner = Store()
    model.name = ""
    empty!(model.variable_info)
    empty!(model.constraint_info)

    # model.objective_sense = MOI.FEASIBILITY_SENSE
    # model.objective_function = nothing
    # model.objective_function_cp = nothing
    # model.objective_cp = nothing

    # model.cached_solution_state = nothing
    # model.name_to_variable = nothing
    # model.name_to_constraint = nothing
    return
end

function MOI.is_empty(model::Optimizer)
    !isempty(model.name) && return false
    !isempty(model.variable_info) && return false
    !isempty(model.constraint_info) && return false
    # model.objective_sense != MOI.FEASIBILITY_SENSE && return false
    # model.objective_function !== nothing && return false
    # model.objective_function_cp !== nothing && return false
    # model.objective_cp !== nothing && return false
    # model.name_to_variable !== nothing && return false
    # model.name_to_constraint !== nothing && return false
    # model.cached_solution_state !== nothing && return false
    return true
end

MOI.get(::Optimizer, ::MOI.SolverName) = "JaCoP"

## Types of objectives and constraints that are supported.

function MOI.supports(
    ::Optimizer,
    ::MOI.ObjectiveFunction{F},
) where {
    F <: Union{
        MOI.VariableIndex,
        MOI.ScalarAffineFunction{Float64},
    },
}
    return true
end

function MOI.supports_constraint(
    ::Optimizer,
    ::Type{MOI.VariableIndex},
    ::Type{F},
) where {
    T <: Union{Int32, Float64},
    F <: Union{
        MOI.EqualTo{T},
        MOI.LessThan{T},
        MOI.GreaterThan{T},
        MOI.Interval{T},
    },
}
    return true
end

function MOI.supports_constraint(
    ::Optimizer,
    ::Type{MOI.ScalarAffineFunction{T}},
    ::Type{F},
) where {
    T <: Union{Int32, Float64},
    F <:
    Union{MOI.EqualTo{T}, MOI.LessThan{T}, MOI.GreaterThan{T}},
    # No interval!
}
    return true
end

MOI.supports(::Optimizer, ::MOI.VariableName, ::Type{MOI.VariableIndex}) = true
function MOI.supports(
    ::Optimizer,
    ::MOI.ConstraintName,
    ::Type{<:MOI.ConstraintIndex},
)
    return true
end

MOI.supports(::Optimizer, ::MOI.Name) = true
# MOI.supports(::Optimizer, ::MOI.NumberOfThreads) = true
# MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true
# MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true
# MOI.supports(::Optimizer, ::MOI.RawOptimizerAttribute) = true

function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike)
    return MOI.Utilities.default_copy_to(dest, src)
end

function MOI.get(::Optimizer, ::MOI.ListOfVariableAttributesSet)
    return MOI.AbstractVariableAttribute[MOI.VariableName()]
end

function MOI.get(model::Optimizer, ::MOI.ListOfModelAttributesSet)
    attributes = Any[MOI.ObjectiveSense()]
    typ = MOI.get(model, MOI.ObjectiveFunctionType())
    if typ !== nothing
        push!(attributes, MOI.ObjectiveFunction{typ}())
    end
    if MOI.get(model, MOI.Name()) != ""
        push!(attributes, MOI.Name())
    end
    return attributes
end

function MOI.get(::Optimizer, ::MOI.ListOfConstraintAttributesSet)
    return MOI.AbstractConstraintAttribute[MOI.ConstraintName()]
end


module MKL

    using Libdl
    using LinearAlgebra: BlasInt

    include(joinpath("..", "deps", "deps.jl"))

    @enum Threading begin
        THREADING_INTEL
        THREADING_SEQUENTIAL
        THREADING_PGI
        THREADING_GNU
        THREADING_TBB
    end
    @enum Interface begin
        INTERFACE_LP64
        INTERFACE_ILP64
        INTERFACE_GNU
    end

    function set_threading_layer(layer::Threading = THREADING_INTEL)
        err = ccall((:MKL_Set_Threading_Layer, libmkl_rt), Cint,
            (Cint,), layer)
        if err == -1
            throw(ErrorException("return value was -1"))
        end
        return nothing
    end

    function set_interface_layer(interface = BlasInt == Int64 ? INTERFACE_ILP64 : INTERFACE_LP64)
        err = ccall((:MKL_Set_Interface_Layer, libmkl_rt), Cint,
            (Cint,), interface)
        if err == -1
            throw(ErrorException("return value was -1"))
        end
        return nothing
    end

    function __init__()

        if Sys.iswindows()
            # On Windows, we have to open the threading library before calling libmkl_rt
            dlopen(libmkl_intel_thread)
        end

        set_threading_layer()
        set_interface_layer()
    end

    mklnorm(x::Vector{Float64}) = ccall((:dnrm2_, libmkl_rt), Float64,
        (Ref{BlasInt}, Ptr{Float64}, Ref{BlasInt}),
        length(x), x, 1)

    # Include installation routines as well
    include("install.jl")
end