# RUN: julia -e "import Brutus; Brutus.lit(:emit_translated)" --startup-file=no %s 2>&1 | FileCheck %s

function loop(N)
    acc = 1
    for i in 1:N
        acc += i
    end
    return acc
end
emit(loop, Int64)

# CHECK-LABEL:   func nested @"Tuple{typeof(Main.loop), Int64}"(
# CHECK-SAME:                                                   %[[VAL_0:.*]]: !jlir<"typeof(Main.loop)">,
# CHECK-SAME:                                                   %[[VAL_1:.*]]: !jlir.Int64) -> !jlir.Int64 attributes {llvm.emit_c_interface} {
# CHECK:           "jlir.goto"()[^bb1] : () -> ()
# CHECK:         ^bb1:
# CHECK:           %[[VAL_2:.*]] = "jlir.constant"() {value = #jlir<"#<intrinsic #29 sle_int>">} : () -> !jlir<"typeof(Core.IntrinsicFunction)">
# CHECK:           %[[VAL_3:.*]] = "jlir.constant"() {value = #jlir<"1">} : () -> !jlir.Int64
# CHECK:           %[[VAL_4:.*]] = "jlir.call"(%[[VAL_2]], %[[VAL_3]], %[[VAL_1]]) : (!jlir<"typeof(Core.IntrinsicFunction)">, !jlir.Int64, !jlir.Int64) -> !jlir.Bool
# CHECK:           %[[VAL_5:.*]] = "jlir.constant"() {value = #jlir.ifelse} : () -> !jlir<"typeof(ifelse)">
# CHECK:           %[[VAL_6:.*]] = "jlir.constant"() {value = #jlir<"0">} : () -> !jlir.Int64
# CHECK:           %[[VAL_7:.*]] = "jlir.call"(%[[VAL_5]], %[[VAL_4]], %[[VAL_1]], %[[VAL_6]]) : (!jlir<"typeof(ifelse)">, !jlir.Bool, !jlir.Int64, !jlir.Int64) -> !jlir.Int64
# CHECK:           %[[VAL_8:.*]] = "jlir.constant"() {value = #jlir<"#<intrinsic #27 slt_int>">} : () -> !jlir<"typeof(Core.IntrinsicFunction)">
# CHECK:           %[[VAL_9:.*]] = "jlir.constant"() {value = #jlir<"1">} : () -> !jlir.Int64
# CHECK:           %[[VAL_10:.*]] = "jlir.call"(%[[VAL_8]], %[[VAL_7]], %[[VAL_9]]) : (!jlir<"typeof(Core.IntrinsicFunction)">, !jlir.Int64, !jlir.Int64) -> !jlir.Bool
# CHECK:           "jlir.gotoifnot"(%[[VAL_10]])[^bb3, ^bb2] {operand_segment_sizes = dense<[1, 0, 0]> : vector<3xi32>} : (!jlir.Bool) -> ()
# CHECK:         ^bb2:
# CHECK:           %[[VAL_11:.*]] = "jlir.constant"() {value = #jlir.nothing} : () -> !jlir.Nothing
# CHECK:           %[[VAL_12:.*]] = "jlir.constant"() {value = #jlir.true} : () -> !jlir.Bool
# CHECK:           %[[VAL_13:.*]] = "jlir.undef"() : () -> !jlir.Int64
# CHECK:           %[[VAL_14:.*]] = "jlir.undef"() : () -> !jlir.Int64
# CHECK:           "jlir.goto"(%[[VAL_12]], %[[VAL_13]], %[[VAL_14]])[^bb4] : (!jlir.Bool, !jlir.Int64, !jlir.Int64) -> ()
# CHECK:         ^bb3:
# CHECK:           %[[VAL_15:.*]] = "jlir.constant"() {value = #jlir.false} : () -> !jlir.Bool
# CHECK:           %[[VAL_16:.*]] = "jlir.constant"() {value = #jlir<"1">} : () -> !jlir.Int64
# CHECK:           %[[VAL_17:.*]] = "jlir.constant"() {value = #jlir<"1">} : () -> !jlir.Int64
# CHECK:           "jlir.goto"(%[[VAL_15]], %[[VAL_16]], %[[VAL_17]])[^bb4] : (!jlir.Bool, !jlir.Int64, !jlir.Int64) -> ()
# CHECK:         ^bb4(%[[VAL_18:.*]]: !jlir.Bool, %[[VAL_19:.*]]: !jlir.Int64, %[[VAL_20:.*]]: !jlir.Int64):
# CHECK:           %[[VAL_21:.*]] = "jlir.constant"() {value = #jlir<"#<intrinsic #43 not_int>">} : () -> !jlir<"typeof(Core.IntrinsicFunction)">
# CHECK:           %[[VAL_22:.*]] = "jlir.call"(%[[VAL_21]], %[[VAL_18]]) : (!jlir<"typeof(Core.IntrinsicFunction)">, !jlir.Bool) -> !jlir.Bool
# CHECK:           %[[VAL_23:.*]] = "jlir.constant"() {value = #jlir<"1">} : () -> !jlir.Int64
# CHECK:           %[[VAL_24:.*]] = "jlir.constant"() {value = #jlir<"1">} : () -> !jlir.Int64
# CHECK:           "jlir.gotoifnot"(%[[VAL_22]], %[[VAL_24]], %[[VAL_19]], %[[VAL_20]], %[[VAL_23]])[^bb10, ^bb5] {operand_segment_sizes = dense<[1, 1, 3]> : vector<3xi32>} : (!jlir.Bool, !jlir.Int64, !jlir.Int64, !jlir.Int64, !jlir.Int64) -> ()
# CHECK:         ^bb5(%[[VAL_25:.*]]: !jlir.Int64, %[[VAL_26:.*]]: !jlir.Int64, %[[VAL_27:.*]]: !jlir.Int64):
# CHECK:           %[[VAL_28:.*]] = "jlir.constant"() {value = #jlir<"#<intrinsic #2 add_int>">} : () -> !jlir<"typeof(Core.IntrinsicFunction)">
# CHECK:           %[[VAL_29:.*]] = "jlir.call"(%[[VAL_28]], %[[VAL_27]], %[[VAL_25]]) : (!jlir<"typeof(Core.IntrinsicFunction)">, !jlir.Int64, !jlir.Int64) -> !jlir.Int64
# CHECK:           %[[VAL_30:.*]] = "jlir.constant"() {value = #jlir<"===">} : () -> !jlir<"typeof(===)">
# CHECK:           %[[VAL_31:.*]] = "jlir.call"(%[[VAL_30]], %[[VAL_26]], %[[VAL_7]]) : (!jlir<"typeof(===)">, !jlir.Int64, !jlir.Int64) -> !jlir.Bool
# CHECK:           "jlir.gotoifnot"(%[[VAL_31]])[^bb7, ^bb6] {operand_segment_sizes = dense<[1, 0, 0]> : vector<3xi32>} : (!jlir.Bool) -> ()
# CHECK:         ^bb6:
# CHECK:           %[[VAL_32:.*]] = "jlir.constant"() {value = #jlir.nothing} : () -> !jlir.Nothing
# CHECK:           %[[VAL_33:.*]] = "jlir.undef"() : () -> !jlir.Int64
# CHECK:           %[[VAL_34:.*]] = "jlir.undef"() : () -> !jlir.Int64
# CHECK:           %[[VAL_35:.*]] = "jlir.constant"() {value = #jlir.true} : () -> !jlir.Bool
# CHECK:           "jlir.goto"(%[[VAL_33]], %[[VAL_34]], %[[VAL_35]])[^bb8] : (!jlir.Int64, !jlir.Int64, !jlir.Bool) -> ()
# CHECK:         ^bb7:
# CHECK:           %[[VAL_36:.*]] = "jlir.constant"() {value = #jlir<"#<intrinsic #2 add_int>">} : () -> !jlir<"typeof(Core.IntrinsicFunction)">
# CHECK:           %[[VAL_37:.*]] = "jlir.constant"() {value = #jlir<"1">} : () -> !jlir.Int64
# CHECK:           %[[VAL_38:.*]] = "jlir.call"(%[[VAL_36]], %[[VAL_26]], %[[VAL_37]]) : (!jlir<"typeof(Core.IntrinsicFunction)">, !jlir.Int64, !jlir.Int64) -> !jlir.Int64
# CHECK:           %[[VAL_39:.*]] = "jlir.constant"() {value = #jlir.false} : () -> !jlir.Bool
# CHECK:           "jlir.goto"(%[[VAL_38]], %[[VAL_38]], %[[VAL_39]])[^bb8] : (!jlir.Int64, !jlir.Int64, !jlir.Bool) -> ()
# CHECK:         ^bb8(%[[VAL_40:.*]]: !jlir.Int64, %[[VAL_41:.*]]: !jlir.Int64, %[[VAL_42:.*]]: !jlir.Bool):
# CHECK:           %[[VAL_43:.*]] = "jlir.constant"() {value = #jlir<"#<intrinsic #43 not_int>">} : () -> !jlir<"typeof(Core.IntrinsicFunction)">
# CHECK:           %[[VAL_44:.*]] = "jlir.call"(%[[VAL_43]], %[[VAL_42]]) : (!jlir<"typeof(Core.IntrinsicFunction)">, !jlir.Bool) -> !jlir.Bool
# CHECK:           "jlir.gotoifnot"(%[[VAL_44]], %[[VAL_29]])[^bb10, ^bb9] {operand_segment_sizes = dense<[1, 1, 0]> : vector<3xi32>} : (!jlir.Bool, !jlir.Int64) -> ()
# CHECK:         ^bb9:
# CHECK:           "jlir.goto"(%[[VAL_40]], %[[VAL_41]], %[[VAL_29]])[^bb5] : (!jlir.Int64, !jlir.Int64, !jlir.Int64) -> ()
# CHECK:         ^bb10(%[[VAL_45:.*]]: !jlir.Int64):
# CHECK:           "jlir.return"(%[[VAL_45]]) : (!jlir.Int64) -> ()
# CHECK:         }


# import functions to be extended
import FoodCachingExperiments: add!, remove!, wait!, countfooditems
# import some types and functions
import FoodCachingExperiments: Tray, Food, FoodItem, InspectionObserver, MaintenanceDiet, observe!

# Define a DummyModel that can keep track of available caching trays, food items
# and inspection observers that record the number of inspections a bird makes to
# a certain tray.
struct DummyModel
    trays::Vector{Tray}
    foodtypes::Vector{FoodItem}
    observers::Vector{InspectionObserver}
end
DummyModel() = DummyModel([], [], [])

# For the DummyModel, adding different objects appends them to the appropriate list.
add!(m::DummyModel, f::FoodItem) = push!(m.foodtypes, f)
add!(m::DummyModel, t::Tray) = push!(m.trays, t)
add!(m::DummyModel, o::InspectionObserver) = push!(m.observers, o)
add!(::DummyModel, ::Type{MaintenanceDiet}) = nothing

# For the DummyModel, removing objects pops them from the respective lists.
remove!(m::DummyModel, f::FoodItem) = removefromlist!(m.foodtypes, f)
remove!(m::DummyModel, f::Food) = removefromlist!(m.foodtypes, x -> x.id == f)
remove!(m::DummyModel, t::Tray) = removefromlist!(m.trays, t)
remove!(m::DummyModel, o::InspectionObserver) = removefromlist!(m.observers, o)
remove!(::DummyModel, ::Type{MaintenanceDiet}) = nothing
function remove!(m::DummyModel, ::Type{Any})
    for list in (:trays, :foodtypes, :observers)
        empty!(getproperty(m, list))
    end
end
# helper functions for removal
removefromlist!(list, obj) = removefromlist!(list, x -> x == obj)
removefromlist!(list, f::Function) = removefromlist!(list, findfirst(f, list))
removefromlist!(list, i::Int) = splice!(list, i)
removefromlist!(::Any, ::Nothing) = nothing

# Count all food items that are freely available and cached in trays.
function countfooditems(m::DummyModel, typ)
	n = countfooditems(m.foodtypes, typ)
	for tray in m.trays
		n += countfooditems(tray, typ)
	end
	n
end

# The DummyModel removes random numbers of items from the freely available ones
# and caches random numbers of items in the available caching trays. The
# difference between removed and cached items gets interpreted as eaten items.
# Also, when trays are available the DummyModel inspects randomly one of them.
function wait!(m::DummyModel, delta, extracondition = nothing)
    for foodtype in m.foodtypes
        n = rand(1:20) # number of items to remove
        if n > foodtype.n  # trying to remove more items than available
            n = foodtype.n
            removefromlist!(m.foodtypes, foodtype)
        else
            foodtype.n -= n # decrement the number of avaiable items
        end
        for tray in m.trays
            n == 0 && break
            n_cache = rand(1:n)
            add!(tray, foodtype.id, n_cache) # cache some items
            n -= n_cache
        end
    end
    for o in m.observers
        if length(m.trays) > 0
            observe!(m.observers[1], rand(m.trays))
        end
    end
end


ComponentArray{A}(::UndefInitializer, ax::Axes) where {A<:StaticArrays.StaticArray,Axes<:Tuple} =
    ComponentArray(similar(A), ax...)

# Sometimes you just need to get things done.
function Base.Broadcast.BroadcastStyle(::Type{s66}) where s66<:Transpose{T,s19} where s19<:ComponentArray{s62,s63,s64,s65} where s65<:Tuple{Vararg{AbstractAxis,N}} where s64<:StaticArrays.StaticArray{S,s62,N} where S<:Tuple where s63<:N where s62<:T where {T, N}
    return CAStyle(StaticArrays.StaticArrayStyle{2}(), getaxes(s19), 2)
end
function Base.Broadcast.BroadcastStyle(::Type{s66}) where s66<:Adjoint{T,s19} where s19<:ComponentArray{s62,s63,s64,s65} where s65<:Tuple{Vararg{AbstractAxis,N}} where s64<:StaticArrays.StaticArray{S,s62,N} where S<:Tuple where s63<:N where s62<:T where {T, N}
    return CAStyle(StaticArrays.StaticArrayStyle{2}(), getaxes(s19), 2)
end

# This is pretty unideal. It gets the answer right, but it allocates in the process. Need to
# figure this out because what's the point of StaticArrays if they are going to allocate?
function Base.copy(bc::Base.Broadcast.Broadcasted{Style,<:Ax,<:F,s19}) where {s19<:Tuple, Axes, Ax, F, Style<:CAStyle{s18,<:s17,<:s12}} where {s12, s17, s18<:StaticArrays.StaticArrayStyle}
    args = map(getdata, bc.args)
    # # style = BC.combine_styles(BC.BroadcastStyle.(typeof.(args))...)
    return ComponentArray{s17}(Base.copy(BC.broadcasted(bc.f, args...)))
end


"""
`module  JAC.Semiempirical`  ... a submodel of JAC that contains all methods to set-up and process (simple)  semiempirical estimations of
                                 atomic properties that cannot be calculated so easily in a rigorious manner; it is using JAC,
                                 JAC.ManyElectron, JAC.ImpactIonization.
"""
module Semiempirical

    using JAC, JAC.ManyElectron, JAC.ImpactIonization

    """
    `@enum   AtomicCrossSection`  ... defines a enumeration for (supported) cross section estimation.
    """
    @enum   AtomicCrossSection    NoCrossSection    ImpactIonizationCS

    """
    `JAC.AtomicCrossSection(sa::String)`  ... constructor for a given String.
    """
    function AtomicCrossSection(sa::String)
        if       sa == "No cross section"      wa = NoCrossSection
        elseif   sa == "impact-ionization"     wa = ImpactIonizationCS
        else
            error("stop a")
        end

        return( wa )
    end

    # `Base.show(io::IO, cs::AtomicCrossSection)`  ... prepares a proper printout of the variable cs::AtomicCrossSection.
    function Base.show(io::IO, cs::AtomicCrossSection)
        print(io, string(cs) )
    end

    # `Base.string(cs::AtomicCrossSection)`  ... provides a proper printout of the variable cs::AtomicCrossSection.
    function Base.string(cs::AtomicCrossSection)
        if       cs == NoCrossSection       return("No cross section")
        elseif   cs == ImpactIonizationCS   return("impact-ionization cross section")
        else     error("stop a")
        end
    end

    """
    `struct  Estimation`  ... defines a type for defining  (a framework for) semiempirical estimates of different properties that cannot
                              be calculated rigoriously in JAC.

        + calcCrossSections    ::Bool                        ... True, if atomic cross sections of some kind are to be calculated
        + crossSection         ::AtomicCrossSection          ... Selected cross sections type.
        + config               ::Configuration               ... A non-relativistic configurations for which computations are to be carried out.
        + crossSectionSettings ::Union{JAC.ImpactIonization.Settings} ... Provides the settings for the selected cross section estimates.
    """
    struct  Estimation
        calcCrossSections      ::Bool
        crossSection           ::AtomicCrossSection
        config                 ::Configuration
        crossSectionSettings   ::Union{JAC.ImpactIonization.Settings}
    end

    """
    `JAC.Semiempirical.Estimation()`  ... constructor for an 'empty' instance::Semiempirical.Estimation.
    """
    function Estimation()
        Estimation(false, NoCrossSection, Configuration(), JAC.ImpactIonization.Settings() )
    end

    # `Base.show(io::IO, estimation::Semiempirical.Estimation)`  ... prepares a proper printout of the variable estimation::Semiempirical.Estimation.
    function Base.show(io::IO, estimation::Semiempirical.Estimation)
        println(io, "calcCrossSections:        $(estimation.calcCrossSections)  ")
        println(io, "crossSection:             $(estimation.crossSection)  ")
        println(io, "config:                   $(estimation.config)  ")
        println(io, "crossSectionSettings:     $(estimation.crossSectionSettings)  ")
    end

    """
    `JAC.Semiempirical.Estimation("interactive")`  ... constructor to generate a new instance of Semiempirical.Estimation interactively by
         replying to some detailed dialog.  **Not yet fully implemented !**
    """
    function Semiempirical.Estimation(sa::String)
        sa != "interactive"   &&    error("Unsupported keystring = $sa.")
    end

end # module


export
    _complete,
    depth,
    expander_complete,
    expansions,
    expanded,
    expansion,
    subruntime,
    clear_analysis!,
    ensure_expansion_state!,
    expand!,
    expander_probs

function expander_complete(runtime :: Runtime, v :: Variable, parent_ranges)
    for p in parent_ranges[1]
        if !expanded(runtime, v, p) return false end
        subnet = expansion(runtime, v, p)
        subrun = subruntime(runtime, v, subnet)
        if !_complete(subrun) return false end
    end
    return true
end

# This method _only_ works if the model being operated upon contains an
# operated called 'support_quality'
function _complete(runtime :: Runtime)
    net = runtime.network
    order = topsort(get_initial_graph(net))
    for var in order
        if !has_instance(runtime, var)
            return false
        end
        inst = current_instance(runtime, var)
        sf = get_sfunc(inst)

        if !has_range(runtime, inst)
            return false
        end
        parranges = parent_ranges(runtime, var)
        imp = get_imp(MultiInterface.get_policy(), Support, sf, parranges, 0, output_type(sf)[])
        # if is_fixed(var.model) && isa(sf, Expander)
        if isa(sf, Expander)
            if !expander_complete(runtime, var, Tuple(parranges))
                return false
            end
        elseif !(support_quality(imp, sf, parranges) == :CompleteSupport)
            return false
        end
    end
    return true
end

function expansions(runtime::InstantRuntime, var::Variable) :: Dict
    exp :: Expander = make_initial(var.model)
    mod = var.model
    has_state(runtime, :subnets) || return Dict()
    exps = get_state(runtime, :subnets)
    mod in keys(exps) || return Dict()
    return exps[mod]
end

function expanded(runtime::Runtime, var::Variable, arg)
    return arg in keys(expansions(runtime, var))
end

function expansion(runtime :: Runtime, var :: Variable, arg) :: Network
    return expansions(runtime, var)[arg]
end

function subruntime(runtime::Runtime, var::Variable, net)
    subruns = get_state(runtime, :subruntimes)
    return subruns[net]
end

function depth(runtime::Runtime) :: Int
    if has_state(runtime, :depth)
        get_state(runtime, :depth)
    else
        0
    end
end

function ensure_expansion_state!(runtime::Runtime)
    has_state(runtime, :subnets) || set_state!(runtime, :subnets, Dict())
    has_state(runtime, :subruntimes) || set_state!(runtime, :subruntimes, Dict())
end

# Returns the expanded subnet and a flag indicating whether it was newly expanded
function expand!(runtime::InstantRuntime, var::Variable, arg)
    ensure_expansion_state!(runtime)
    exp :: Expander = make_initial(var.model)
    mod = var.model
    exps = get_state(runtime, :subnets)
    subruns = get_state(runtime, :subruntimes)
    if !(mod in keys(exps))
        exps[mod] = Dict()
    elseif arg in keys(exps[mod])
        return (exps[mod][arg], false)
    end

    subnet = apply(exp, arg)
    subrun = Runtime(subnet)
    # push all parent variables to subruntime
    set_state!(subrun, :parent_env, get_env(runtime))
    # we associate a subrun with the network, in case the same network is
    # produced for multiple arguments, so we avoid repeating computation for all
    #  the arguments that produce the same network
    subruns[subnet] = subrun
    ensure_all!(subrun)
    exps[mod][arg] = subnet
    return (subnet, true)
end

# analysis is 'per network', so we have to make sure we track per-network
network_analysis = Dict{Network, Dict{Symbol, Any}}()

clear_analysis!() = empty!(network_analysis)

function has_analysis(net,sym)
    haskey(network_analysis, net) && haskey(network_analysis[net], sym)
end

get_analysis(net,sym) = network_analysis[net][sym]

function add_analysis!(net,sym,val)
    get!(network_analysis, net, Dict{Symbol, Any}())[sym] = val
end

function expander_probs(runtime::Runtime, fn::Function, v::Variable, depth::Int) where {I,O}
    net = get_network(runtime)
    par = get_parents(net, v)[1]
    parinst = current_instance(runtime, par)
    inst = current_instance(runtime, v)
    if has_range(runtime, parinst)
        parrange = get_range(runtime, parinst, depth)
    else
        parrange = []
    end
    if has_range(runtime, inst)
        range = get_range(runtime, inst, depth)
    else
        range = []
    end
    lowers = Float64[]
    uppers = Float64[]

    function make_unexpanded_row()
        for i in 1:length(range)
            push!(lowers, 0)
            push!(uppers, 1)
        end
    end

    for p in parrange
        if depth < 2 || !expanded(runtime, v, p)
            make_unexpanded_row()
        else
            subnet = expansion(runtime, v, p)
            subrun = subruntime(runtime, v, subnet)
            output = get_node(subnet, :out)
            need_to_compute = true
            if has_analysis(subnet, :depthsolution)
                (saved_depth, saved_range, solution) =
                    get_analysis(subnet, :depthsolution)
                # To be able to reuse the solution to define this node's CPD,
                # it must have the required depth and range
                if saved_depth >= depth && saved_range == range
                    need_to_compute = false
                    (lfact, ufact) = solution
                end
            end
            if need_to_compute
                order = topsort(get_initial_graph(subnet))
                ((lfact, ufact), _) = fn(subrun, order, [output], depth - 1)
                add_analysis!(subnet, :depthsolution,
                              (depth, range, (lfact, ufact)))
            end
            lsum = sum(lfact.entries)
            output_range = get_range(subrun, output, depth - 1)
            inds = indexin(range, output_range)
            for i in 1:length(range)
                if isempty(inds) || isnothing(inds[i])
                    # If this value is not in the output range, then it
                    # might not be in the range of the output, so has
                    # probability 0, or it might be in the range but not
                    # expanded yet. But it's probability cannot be more than
                    # 1 - lsum
                    push!(lowers, 0)
                    push!(uppers, 1 - lsum)
                else
                    if isempty(lfact.entries)
                        push!(lowers, 0)
                    else
                        push!(lowers, lfact.entries[inds[i]])
                    end
                    if isempty(ufact.entries)
                        push!(uppers, 0)
                    else
                        push!(uppers, ufact.entries[inds[i]])
                    end
                end
            end
        end
    end
    return (lowers, uppers)
end


function RCCD{T}(ref::Fermi.HartreeFock.RHF.RHFWavefunction,alg::Fermi.CoupledCluster.DPD,int::Fermi.CoupledCluster.Conventional) where T <: AbstractFloat
    error("RCCD with cc_alg $alg and ints_type $int is not implemented. Sorry!")
end


@testset "Test Reserves from Thermal Dispatch" begin
    devices = Dict{Symbol, DeviceModel}(
        :Generators => DeviceModel(ThermalStandard, ThermalDispatch),
        :Loads => DeviceModel(PowerLoad, PSI.StaticPowerLoad),
    )
    branches = Dict{Symbol, DeviceModel}()
    services_template = Dict{Symbol, PSI.ServiceModel}(
        :Reserve => ServiceModel(VariableReserve{ReserveUp}, RangeReserve),
        :DownReserve => ServiceModel(VariableReserve{ReserveDown}, RangeReserve),
    )
    model_template = OperationsProblemTemplate(
        CopperPlatePowerModel,
        devices,
        branches,
        services_template,
    )
    c_sys5_uc = build_system("c_sys5_uc"; add_reserves = true)
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, model_template, c_sys5_uc; use_parameters = p)
        moi_tests(op_problem, p, 384, 0, 120, 192, 24, false)
    end
end

@testset "Test Reserves from Thermal Standard UC" begin
    devices = Dict{Symbol, DeviceModel}(
        :Generators => DeviceModel(ThermalStandard, ThermalBasicUnitCommitment),
        :Loads => DeviceModel(PowerLoad, PSI.StaticPowerLoad),
    )
    branches = Dict{Symbol, DeviceModel}()
    services_template = Dict{Symbol, PSI.ServiceModel}(
        :UpReserve => ServiceModel(VariableReserve{ReserveUp}, RangeReserve),
        :DownReserve => ServiceModel(VariableReserve{ReserveDown}, RangeReserve),
    )
    model_template = OperationsProblemTemplate(
        CopperPlatePowerModel,
        devices,
        branches,
        services_template,
    )
    c_sys5_uc = build_system("c_sys5_uc"; add_reserves = true)
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, model_template, c_sys5_uc; use_parameters = p)
        moi_tests(op_problem, p, 744, 0, 240, 192, 144, true)
    end
end

@testset "Test Upwards Reserves from Renewable Dispatch" begin
    devices = Dict{Symbol, DeviceModel}(
        :Generators => DeviceModel(RenewableDispatch, RenewableFullDispatch),
        :Loads => DeviceModel(PowerLoad, PSI.StaticPowerLoad),
    )
    branches = Dict{Symbol, DeviceModel}()
    services_template = Dict{Symbol, PSI.ServiceModel}(
        :Reserve => ServiceModel(VariableReserve{ReserveUp}, RangeReserve),
    )
    model_template = OperationsProblemTemplate(
        CopperPlatePowerModel,
        devices,
        branches,
        services_template,
    )
    c_sys5_re = build_system("c_sys5_re"; add_reserves = true)
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, model_template, c_sys5_re; use_parameters = p)
        moi_tests(op_problem, p, 144, 0, 72, 24, 24, false)
    end
end

@testset "Test Reserves from Storage" begin
    devices = Dict{Symbol, DeviceModel}(
        :Generators => DeviceModel(ThermalStandard, ThermalDispatch),
        :Loads => DeviceModel(PowerLoad, PSI.StaticPowerLoad),
        :Storage => DeviceModel(GenericBattery, BookKeeping),
        # Added here to test it doesn't add reserve variables
        :Ren => DeviceModel(RenewableDispatch, FixedOutput),
    )
    branches = Dict{Symbol, DeviceModel}()
    services_template = Dict{Symbol, PSI.ServiceModel}(
        :Reserve => ServiceModel(VariableReserve{ReserveUp}, RangeReserve),
        :DownReserve => ServiceModel(VariableReserve{ReserveDown}, RangeReserve),
    )
    model_template = OperationsProblemTemplate(
        CopperPlatePowerModel,
        devices,
        branches,
        services_template,
    )
    c_sys5_bat = build_system("c_sys5_bat"; add_reserves = true)
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, model_template, c_sys5_bat; use_parameters = p)
        moi_tests(op_problem, p, 240, 0, 192, 240, 48, false)
    end
end

@testset "Test Reserves from Hydro" begin
    devices = Dict{Symbol, DeviceModel}(
        :Generators => DeviceModel(HydroEnergyReservoir, HydroDispatchRunOfRiver),
        :Loads => DeviceModel(PowerLoad, PSI.StaticPowerLoad),
    )
    branches = Dict{Symbol, DeviceModel}()
    services_template = Dict{Symbol, PSI.ServiceModel}(
        :Reserve => ServiceModel(VariableReserve{ReserveUp}, RangeReserve),
        :DownReserve => ServiceModel(VariableReserve{ReserveDown}, RangeReserve),
    )
    model_template = OperationsProblemTemplate(
        CopperPlatePowerModel,
        devices,
        branches,
        services_template,
    )
    c_sys5_hyd = build_system("c_sys5_hyd"; add_reserves = true)
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, model_template, c_sys5_hyd; use_parameters = p)
        moi_tests(op_problem, p, 72, 0, 24, 72, 24, false)
    end
end


# Needs CXX, Julia-0.5
ENV["PATH"]="/usr/local/cuda-7.5/bin:"*ENV["PATH"]
ENV["LD_LIBRARY_PATH"]="/usr/local/cuda-7.5/lib64:/usr/local/cuda/nvvm/lib64:"*get(ENV,"LD_LIBRARY_PATH","")
ENV["AFMODE"] = "CUDA"  # this example works with CUDA
using ArrayFire, CUDArt, GLAbstraction, Cxx, ModernGL

type CUDAGLBuffer{T} <: GPUArray{T, 1}
    buffer::GLBuffer{T}
    graphics_resource::Ref{CUDArt.rt.cudaGraphicsResource_t}
    ismapped::Bool
end

function CUDAGLBuffer(buffer::GLBuffer, flag = 0)
    cuda_resource = Ref{CUDArt.rt.cudaGraphicsResource_t}(C_NULL)
    CUDArt.rt.cudaGraphicsGLRegisterBuffer(cuda_resource, buffer.id, flag)
    CUDAGLBuffer(buffer, cuda_resource, false)
end
function map_resource(buffer::CUDAGLBuffer)
    if !buffer.ismapped
        CUDArt.rt.cudaGraphicsMapResources(1, buffer.graphics_resource, C_NULL)
        buffer.ismapped = true;
    end
    nothing
end

function unmap_resource(buffer::CUDAGLBuffer)
    if buffer.ismapped
        CUDArt.rt.cudaGraphicsUnmapResources(1, buffer.graphics_resource, C_NULL)
        buffer.ismapped = false
    end
    nothing
end

function copy_from_device_pointer{T}(
        cuda_mem_ptr::Ptr{T},
        cuda_gl_buffer::CUDAGLBuffer,
    )
    map_resource(cuda_gl_buffer)
    buffersize = length(cuda_gl_buffer.buffer)*sizeof(eltype(cuda_gl_buffer.buffer))
    if cuda_gl_buffer.buffer.buffertype == GL_RENDERBUFFER
        array_ptr = Ref{CUDArt.rt.cudaArray_t}(C_NULL)
        CUDArt.rt.cudaGraphicsSubResourceGetMappedArray(array_ptr, cuda_gl_buffer.graphics_resource[], 0, 0)
        CUDArt.rt.cudaMemcpyToArray(array_ptr[], 0, 0, cuda_mem_ptr, buffersize, CUDArt.rt.cudaMemcpyDeviceToDevice)
    else
        opengl_ptr = Ref{Ptr{Void}}(C_NULL); size_ref = Ref{Csize_t}(buffersize)
        CUDArt.rt.cudaGraphicsResourceGetMappedPointer(opengl_ptr, size_ref, cuda_gl_buffer.graphics_resource[])
        CUDArt.rt.cudaMemcpy(opengl_ptr[], cuda_mem_ptr, buffersize, CUDArt.rt.cudaMemcpyDeviceToDevice)
    end
    unmap_resource(cuda_gl_buffer)
end

"""
 Gets the device pointer from the mapped resource
 Sets is_mapped to true
"""
function copy_to_device_pointer{T}(
        cuda_mem_ptr::Ptr{T},
        cuda_gl_buffer::CUDAGLBuffer,
    )
    map_resource(cuda_gl_buffer)
    is_mapped = true
    buffersize = length(cuda_gl_buffer.buffer)*sizeof(eltype(cuda_gl_buffer.buffer))
    if cuda_gl_buffer.buffer.buffertype == GL_RENDERBUFFER
        array_ptr = Ref{CUDArt.rt.cudaArray_t}(C_NULL);
        CUDArt.rt.cudaGraphicsSubResourceGetMappedArray(array_ptr, cuda_gl_buffer.graphics_resource[], 0, 0)
        CUDArt.rt.cudaMemcpyFromArray(cuda_mem_ptr, array_ptr[], 0, 0, buffersize, CUDArt.rt.cudaMemcpyDeviceToDevice)
    else
        opengl_ptr = Ref{Ptr{Void}}(C_NULL); size_ref = Ref{Csize_t}(buffersize)
        CUDArt.rt.cudaGraphicsResourceGetMappedPointer(opengl_ptr, size_ref, cuda_gl_buffer.graphics_resource[])
        CUDArt.rt.cudaMemcpy(cuda_mem_ptr, opengl_ptr, buffersize, CUDArt.rt.cudaMemcpyDeviceToDevice)
    end
    unmap_resource(cuda_gl_buffer)
end

# ArrayFire.AFArray
function Base.copy(source::ArrayFire.AFArray, target::CUDAGLBuffer)
    d_ptr = ArrayFire.af_device(source)
    copy_from_device_pointer(d_ptr, target)
end
function Base.copy(source::CUDAGLBuffer, target::ArrayFire.AFArray)
    d_ptr = ArrayFire.af_device(target)
    copy_to_device_pointer(d_ptr, target)
end

using GLVisualize, GeometryTypes, Colors
w=glscreen();@async renderloop(w)
cat = loadasset("cat.obj")
colors = RGBA{Float32}[RGBA{Float32}(rand(), rand(), rand(), 1) for i=1:length(vertices(cat))]
catmesh = GLNormalVertexcolorMesh(
    vertices=vertices(cat), faces=faces(cat), normals=normals(cat),
    color=colors
)
_view(visualize(catmesh))

vertsvec = reinterpret(Float32, vertices(cat), (3,length(vertices(cat))));
colorsvec = reinterpret(Float32, colors, (4,length(colors)));
af_vertices = AFArray(vertsvec)
af_colors = AFArray(colorsvec)

gl_verts = w.renderlist[1][1][:vertices]
gl_colors = w.renderlist[1][1][:color]

cu_gl_verts = CUDAGLBuffer(gl_verts)
cu_gl_colors = CUDAGLBuffer(gl_colors)
b = af_vertices .* 2f0
copy(b, cu_gl_verts)


@testset "Renewable DCPLossLess FullDispatch" begin
    ps_model = PSI._canonical_model_init(bus_numbers, nothing, PM.AbstractPowerFormulation, time_range)
    PSI.construct_device!(ps_model, PSY.RenewableGen, PSI.RenewableFullDispatch, PM.DCPlosslessForm, sys5b, time_range);
    @test JuMP.num_variables(ps_model.JuMPmodel) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.Interval{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 0
    # No Parameters Testing
    ps_model = PSI._canonical_model_init(bus_numbers, nothing, PM.AbstractPowerFormulation, time_range; parameters = false)
    PSI.construct_device!(ps_model, PSY.RenewableGen, PSI.RenewableFullDispatch, PM.DCPlosslessForm, sys5b, time_range; parameters = false);
    @test !(:params in keys(ps_model.JuMPmodel.ext))
    @test JuMP.num_variables(ps_model.JuMPmodel) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.Interval{Float64}) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 0
end

#@testset "Renewable Testing" begin
#ps_model = PSI._canonical_model_init(bus_numbers, nothing, PM.AbstractPowerFormulation, time_range)
#PSI.construct_device!(ps_model, PSY.RenewableGen, PSI.RenewableFullDispatch, PM.StandardACPForm, sys5b, time_range);
#end

@testset "Renewable DCPLossLess ConstantPowerFactor" begin
    ps_model = PSI._canonical_model_init(bus_numbers, nothing, PM.AbstractPowerFormulation, time_range)
    PSI.construct_device!(ps_model, PSY.RenewableGen, PSI.RenewableConstantPowerFactor, PM.DCPlosslessForm, sys5b, time_range);
    @test JuMP.num_variables(ps_model.JuMPmodel) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.Interval{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 0
    # No Parameters Testing
    ps_model = PSI._canonical_model_init(bus_numbers, nothing, PM.AbstractPowerFormulation, time_range; parameters = false)
    PSI.construct_device!(ps_model, PSY.RenewableGen, PSI.RenewableConstantPowerFactor, PM.DCPlosslessForm, sys5b, time_range; parameters = false);
    @test !(:params in keys(ps_model.JuMPmodel.ext))
    @test JuMP.num_variables(ps_model.JuMPmodel) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.Interval{Float64}) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 0
end

@testset "Renewable ACP ConstantPowerFactor" begin
    ps_model = PSI._canonical_model_init(bus_numbers, nothing, PM.AbstractPowerFormulation, time_range)
    PSI.construct_device!(ps_model, PSY.RenewableGen, PSI.RenewableConstantPowerFactor, PM.StandardACPForm, sys5b, time_range);
    @test JuMP.num_variables(ps_model.JuMPmodel) == 144
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.Interval{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 72
    # No Parameters Testing
    ps_model = PSI._canonical_model_init(bus_numbers, nothing, PM.AbstractPowerFormulation, time_range; parameters = false)
    PSI.construct_device!(ps_model, PSY.RenewableGen, PSI.RenewableConstantPowerFactor, PM.StandardACPForm, sys5b, time_range; parameters = false);
    @test !(:params in keys(ps_model.JuMPmodel.ext))
    @test JuMP.num_variables(ps_model.JuMPmodel) == 144
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.Interval{Float64}) == 72
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 72
end

@testset "Renewable DCPLossLess FixedOutput" begin
    ps_model = PSI._canonical_model_init(bus_numbers, nothing, PM.AbstractPowerFormulation, time_range)
    PSI.construct_device!(ps_model, PSY.RenewableGen, PSI.RenewableFixed, PM.DCPlosslessForm, sys5b, time_range);
    @test JuMP.num_variables(ps_model.JuMPmodel) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 0
    # No Parameters Testing
    ps_model = PSI._canonical_model_init(bus_numbers, nothing, PM.AbstractPowerFormulation, time_range; parameters = false)
    PSI.construct_device!(ps_model, PSY.RenewableGen, PSI.RenewableFixed, PM.DCPlosslessForm, sys5b, time_range; parameters = false);
    @test !(:params in keys(ps_model.JuMPmodel.ext))
    @test JuMP.num_variables(ps_model.JuMPmodel) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 0
end

@testset "Renewable ACP FixedOutput" begin
    ps_model = PSI._canonical_model_init(bus_numbers, nothing, PM.AbstractPowerFormulation, time_range)
    PSI.construct_device!(ps_model, PSY.RenewableGen, PSI.RenewableFixed, PM.StandardACPForm, sys5b, time_range);
    @test JuMP.num_variables(ps_model.JuMPmodel) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 0
    # No Parameters Testing
    ps_model = PSI._canonical_model_init(bus_numbers, nothing, PM.AbstractPowerFormulation, time_range; parameters = false)
    PSI.construct_device!(ps_model, PSY.RenewableGen, PSI.RenewableFixed, PM.StandardACPForm, sys5b, time_range; parameters = false);
    @test !(:params in keys(ps_model.JuMPmodel.ext))
    @test JuMP.num_variables(ps_model.JuMPmodel) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 0
end


# using Base: AlwaysLockedST
function Benchmark.run(env::MuEnv, eval::AlphaZero.Benchmark.Evaluation)
  # net() = Network.copy(env.bestnns, on_gpu=eval.sim.use_gpu, test_mode=true)
  device = eval.sim.use_gpu ? Flux.gpu : Flux.cpu
  function net()
    nns = deepcopy(env.bestnns) |> device |> Flux.testmode!
    return (InitialOracle(nns), RecurrentOracle(nns))
  end
  if isa(eval, Benchmark.Single)
    simulator = Simulator(net, record_trace) do net
      Benchmark.instantiate(eval.player, env.gspec, net)
    end
  else
    @assert isa(eval, Benchmark.Duel)
    simulator = Simulator(net, record_trace) do net
      player = Benchmark.instantiate(eval.player, env.gspec, net)
      baseline = Benchmark.instantiate(eval.baseline, env.gspec, net)
      return TwoPlayers(player, baseline)
    end
  end
  samples, elapsed = @timed simulate(
    simulator, env.gspec, eval.sim)
  # gamma = env.params.self_play.mcts.gamma
  rewards, redundancy = rewards_and_redundancy(samples, gamma=1.0)
  opponent_won, draw, mu_won = count_wins(rewards) ./ length(rewards)
  # @info "Benchmark" white_lost draw white_won time=elapsed
  return (; opponent_won, draw, mu_won, time_benchmarked=elapsed)
#   return Report.Evaluation(
#     name(eval), mean(rewards), redundancy, rewards, nothing, elapsed)
end

struct Mu <: Benchmark.Player
  params :: MctsParams
  # make_oracles # Function make_oracles()
end

# name(::Mu) = "MuZero"

function Benchmark.instantiate(p::Mu, gspec::AbstractGameSpec, nns)
  return MuPlayer(nns, p.params)
end

function run_duel(env, benchmark)
  # report = []
  report = Dict()
  for (name,duel) in pairs(benchmark)
    outcome = Benchmark.run(env, duel)
    # push!(report, (;name=outcome))
    report[name] = outcome
  end
  return report
end
# benchmark
# env
# run_duel(env, benchmark)

# benchmark_sim = SimParams(
#     num_games=400,
#     num_workers=1,
#     batch_size=4,
#     use_gpu=false,
#     reset_every=1,
#     flip_probability=0.5,
#     alternate_colors=true)

# benchmark = [
#   Benchmark.Duel(
#     Mu(self_play.mcts),
#     Benchmark.MctsRollouts(self_play.mcts),
#     benchmark_sim)]

function count_wins(rewards)
  lost, draw, won = (count(==(i), rewards) for i in -1:1)
  @assert sum([lost, draw, won]) == length(rewards)
  # @info "Benchmark" lost draw won
  return lost, draw, won
end


abstract type Camera{T} end
const Q = Quaternions # save some writing!

mutable struct OrthographicCamera{T} <: Camera{T}
    window_size     ::Signal{SimpleRectangle{Int}}
    view            ::Signal{Mat4{T}}
    projection      ::Signal{Mat4{T}}
    projectionview  ::Signal{Mat4{T}}
end

mutable struct PerspectiveCamera{T} <: Camera{T}
    window_size     ::Signal{SimpleRectangle{Int}}
    nearclip        ::Signal{T}
    farclip         ::Signal{T}
    fov             ::Signal{T}
    view            ::Signal{Mat4{T}}
    projection      ::Signal{Mat4{T}}
    projectionview  ::Signal{Mat4{T}}
    eyeposition     ::Signal{Vec{3, T}}
    lookat          ::Signal{Vec{3, T}}
    up              ::Signal{Vec{3, T}}
    trans           ::Signal{Vec{3, T}}
    theta           ::Signal{Vec{3, T}}
    projectiontype  ::Signal{Projection}
end

mutable struct DummyCamera{T, IT} <: Camera{T}
    window_size     ::Signal{SimpleRectangle{IT}}
    view            ::Signal{Mat4{T}}
    projection      ::Signal{Mat4{T}}
    projectionview  ::Signal{Mat4{T}}
end

function DummyCamera(;
        window_size    = Signal(SimpleRectangle(-1, -1, 1, 1)),
        view           = Signal(eye(Mat4f0)),
        nearclip       = Signal(-10_000f0),
        farclip        = Signal(10_000f0),
        projection     = const_lift(orthographicprojection, window_size, nearclip, farclip),
        projectionview = const_lift(*, projection, view)
    )
    DummyCamera(window_size, view, projection, projectionview)
end

function Base.collect(camera::Camera, collected = Dict{Symbol, Any}())
    names = fieldnames(camera)
    for name in (:view, :projection, :projectionview, :eyeposition)
        if name in names
            collected[name] = getfield(camera, name)
        end
    end
    return collected
end

"""
Creates an orthographic camera with the pixel perfect plane in z == 0
Signals needed:
Dict(
    :window_size           => Signal(SimpleRectangle{Int}),
    :buttons_pressed       => Signal(Int[]),
    :mouse_buttons_pressed => Signal(Int[]),
    :mouseposition         => mouseposition, -> Panning
    :scroll_y              => Signal(0) -> Zoomig
)
"""
function OrthographicPixelCamera(
        inputs;
        fov=41f0, near=0.01f0, up=Vec3f0(0,1,0),
        translation_speed = Signal(1), theta = Signal(Vec3f0(0)), keep = Signal(true)
    )
    @materialize mouseposition, mouse_buttons_pressed, buttons_pressed, scroll = inputs
    left_ctrl     = Set([GLFW.KEY_LEFT_CONTROL])
    use_cam       = map(AND, const_lift(==, buttons_pressed, left_ctrl), keep)

    mouseposition = droprepeats(map(Vec2f0, mouseposition))
    left_pressed  = const_lift(pressed, mouse_buttons_pressed, GLFW.MOUSE_BUTTON_LEFT)
    xytranslate   = dragged_diff(mouseposition, left_pressed, use_cam)

    ztranslate    = filterwhen(use_cam, 0f0, map(x->Float32(x[2]), scroll))

    trans = map(translationlift, xytranslate, ztranslate, translation_speed)
    OrthographicPixelCamera(
        theta, trans, Signal(up), Signal(fov), Signal(near),
        inputs[:window_area],
    )
end
function OrthographicPixelCamera(
        theta, trans, up, fov_s, near_s, area_s
    )
    fov, near = Reactive.value(fov_s), Reactive.value(near_s)

    # lets calculate how we need to adjust the camera, so that it mapps to
    # the pixel of the window (area)
    area = Reactive.value(area_s)
    h = Float32(tan(fov / 360.0 * pi) * near)
    w_, h_ = area.w / 2f0, area.h / 2f0
    zoom = min(h_, w_) / h
    x, y = w_, h_
    eyeposition = Signal(Vec3f0(x, y, zoom))
    lookatvec   = Signal(Vec3f0(x, y, 0))
    far         = Signal(zoom * 10f0) # this should probably be not calculated
    # since there is no scene independant, well working far clip

    PerspectiveCamera(
        theta,
        trans,
        lookatvec,
        eyeposition,
        up,
        area_s,
        fov_s, # Field of View
        near_s,  # Min distance (clip distance)
        far, # Max distance (clip distance)
        Signal(GLAbstraction.ORTHOGRAPHIC)
    )
end

pressed(keys, key) = key in keys
singlepressed(keys, key) = length(keys) == 1 && first(keys) == key

mouse_dragg(v0, args) = mouse_dragg(v0..., args...)
function mouse_dragg(
        started::Bool, waspressed::Bool, startpoint, diff,
        ispressed::Bool, position, start_condition::Bool
    )
    started && ispressed && return (true, ispressed, startpoint, position-startpoint)
    if !started && !waspressed && ispressed && start_condition
        return (true, true, position, Vec2f0(0))
    end
    (false, ispressed, Vec2f0(0), Vec2f0(0))
end
mouse_dragg_diff(v0, args) = mouse_dragg_diff(v0..., args...)
function mouse_dragg_diff(
        started::Bool, waspressed, position0, diff,
        ispressed::Bool, position, start_condition::Bool
    )
    if !started && ispressed && (start_condition && !waspressed)
        return (true, ispressed, position, Vec2f0(0))
    end
    started && ispressed && return (true, ispressed, position, position0-position)
    (false, ispressed, Vec2f0(0), Vec2f0(0))
end

function dragged(mouseposition, key_pressed, start_condition = true)
    v0 = (false, false, Vec2f0(0), Vec2f0(0))
    args = const_lift(tuple, key_pressed, mouseposition, start_condition)
    dragg_sig = foldp(mouse_dragg, v0, args)
    is_dragg = map(first, dragg_sig)
    dragg = map(last, dragg_sig)
    dragg_diff = filterwhen(is_dragg, Reactive.value(dragg), dragg)
    dragg_diff
end
function dragged_diff(mouseposition, key_pressed, start_condition=true)
    v0 = (false, false, Vec2f0(0), Vec2f0(0))
    args = const_lift(tuple, key_pressed, mouseposition, start_condition)
    dragg_sig = foldp(mouse_dragg_diff, v0, args)
    is_dragg = map(first, dragg_sig)
    dragg_diff = map(last, dragg_sig)
    dragg_diff
end

"""
Transforms a mouse drag into a selection from drag start to drag end
"""
function drag2selectionrange(v0, selection)
    mousediff, id_start, current_id = selection
    if mousediff != Vec2f0(0) # Mouse Moved
        if current_id[1] == id_start[1]
            return min(id_start[2],current_id[2]):max(id_start[2],current_id[2])
        end
    else # if mouse did not move while dragging, make a single point selection
        if current_id.id == id_start.id
            return current_id.index:0 # this is the type stable way of indicating, that the selection is between currend_index
        end
    end
    v0
end

"""
Returns two signals, one boolean signal if clicked over `robj` and another
one that consists of the object clicked on and another argument indicating that it's the first click
"""
function clicked(robj::RenderObject, button::MouseButton, window)
    @materialize mouse_hover, mousebuttonspressed = window.inputs
    clicked_on = const_lift(mouse_hover, mousebuttonspressed) do mh, mbp
        mh.id == robj.id && in(button, mbp)
    end
    clicked_on_obj = keepwhen(clicked_on, false, clicked_on)
    clicked_on_obj = const_lift((mh, x)->(x,robj,mh), mouse_hover, clicked_on)
    clicked_on, clicked_on_obj
end
export is_same_id
is_same_id(id_index, id::Int) = id_index.id == id
is_same_id(id_index, robj::RenderObject) = id_index.id == robj.id
is_same_id(id_index, ids::Tuple) = id_index.id in ids
"""
Returns a boolean signal indicating if the mouse hovers over `robj`
"""
function is_hovering(robj::RenderObject, window)
    droprepeats(const_lift(is_same_id, window.inputs[:mouse_hover], robj))
end

"""
returns a signal which becomes true whenever there is a doublecklick
"""
function doubleclick(mouseclick, threshold::Real)
    ddclick = foldp((time(), Reactive.value(mouseclick), false), mouseclick) do v0, mclicked
        t0, lastc, _ = v0
        t1 = time()
        isclicked = (length(mclicked) == 1 &&
            length(lastc) == 1 &&
            first(lastc) == first(mclicked) &&
            t1-t0 < threshold
        )
        return (t1, mclicked, isclicked)
    end
    dd = const_lift(last, ddclick)
    return dd
end

export doubleclick
function default_camera_control(
        inputs, rotation_speed, translation_speed, keep = Signal(true)
    )
    @materialize mouseposition, mouse_buttons_pressed, scroll = inputs

    mouseposition = droprepeats(map(Vec2f0, mouseposition))
    left_pressed  = const_lift(pressed, mouse_buttons_pressed, GLFW.MOUSE_BUTTON_LEFT)
    right_pressed = const_lift(pressed, mouse_buttons_pressed, GLFW.MOUSE_BUTTON_RIGHT)
    xytheta       = dragged_diff(mouseposition, left_pressed, keep)
    xytranslate   = dragged_diff(mouseposition, right_pressed, keep)

    ztranslate    = filterwhen(keep, 0f0,
        map(last, scroll)
    )
    translate_theta(
        xytranslate, ztranslate, xytheta,
        rotation_speed, translation_speed
    )
end

function thetalift(xy, speed)
    Vec3f0(xy[1], -xy[2], 0f0).*speed
end
function translationlift(up_left, zoom, speed)
    Vec3f0(zoom, up_left[1], up_left[2]).*speed
end
function diff_vector(v0, p1)
    p0, diff = v0
    p1, p0-p1
end
function translate_theta(
        xytranslate, ztranslate, xytheta,
        rotation_speed, translation_speed
    )
    theta = map(thetalift, xytheta, rotation_speed)
    trans = map(translationlift, xytranslate, ztranslate, translation_speed)
    theta, trans
end

"""
Creates a perspective camera from a dict of signals
Args:

inputs: Dict of signals, looking like this:
[
    :window_size            => Signal(Vec{2, Int}),
    :buttons_pressed        => Signal(Int[]),
    :mouse_buttons_pressed  => Signal(Int[]),
    :mouseposition          => mouseposition, -> Panning + Rotation
    :scroll_y               => Signal(0) -> Zoomig
]
eyeposition: Position of the camera
lookatvec: Point the camera looks at
"""
function PerspectiveCamera(
        inputs::Dict{Symbol,Any},
        eyeposition::Vec{3, T}, lookatvec::Vec{3, T};
        upvector = Vec3f0(0, 0, 1),
        keep=Signal(true), theta=nothing, trans=nothing
    ) where T
    lookat, eyepos = Signal(lookatvec), Signal(eyeposition)
    # TODO make this more elegant!
    _theta, _trans = default_camera_control(
        inputs, Signal(0.1f0), Signal(1f0), keep
    )
    theta = theta == nothing ? _theta : theta
    trans = trans == nothing ? _trans : trans
    farclip = map(eyepos, lookat) do a,b
        max(norm(b-a) * 5f0, 30f0)
    end
    minclip = map(eyepos, lookat) do a,b
        norm(b-a) * 0.007f0
    end
    PerspectiveCamera(
        theta,
        trans,
        lookat,
        eyepos,
        Signal(upvector),
        inputs[:window_area],
        Signal(41f0), # Field of View
        Signal(0.001f0),  # Min distance (clip distance)
        farclip # Max distance (clip distance)
    )
end
function PerspectiveCamera(
        area,
        eyeposition::Signal{Vec{3, T}}, lookatvec::Signal{Vec{3, T}}, upvector
    ) where T
    PerspectiveCamera(
        Signal(Vec3f0(0)),
        Signal(Vec3f0(0)),
        lookatvec,
        eyeposition,
        upvector,
        area,
        Signal(41f0), # Field of View
        Signal(0.1f0),  # Min distance (clip distance)
        Signal(50f0) # Max distance (clip distance)
    )
end

function projection_switch(
        wh::SimpleRectangle,
        fov::T, near::T, far::T,
        projection::Projection, zoom::T
    ) where T<:Real
    aspect = T(wh.w/wh.h)
    h      = T(tan(fov / 360.0 * pi) * near)
    w      = T(h * aspect)
    projection == PERSPECTIVE && return frustum(-w, w, -h, h, near, far)
    h, w   = h*zoom, w*zoom
    orthographicprojection(-w, w, -h, h, near, far)
end

w_component(::Point{N, T}) where {N, T} = T(1)
w_component(::Vec{N, T}) where {N, T} = T(0)

function to_worldspace(point::T, cam) where T <: StaticVector
    to_worldspace(
        point,
        Reactive.value(cam.projection) * Reactive.value(cam.view),
        T(widths(Reactive.value(cam.window_size)))
    )
end
function to_worldspace(
        p::StaticVector{N, T},
        projectionview::Mat4,
        cam_res::StaticVector
    ) where {N, T}
    VT = typeof(p)
    prj_view_inv = inv(projectionview)
    clip_space = T(4) * (VT(p) ./ VT(cam_res))
    pix_space = Vec{4, T}(
        clip_space[1],
        clip_space[2],
        T(0), w_component(p)
    )
    ws = prj_view_inv * pix_space
    ws # ./ ws[4]
end

"""
Takes a point and a camera and transforms it from mouse (imagespace) to world space
"""
function imagespace(pos, camera)
    # Setup transformation matrix
    pv = Reactive.value(camera.projection) * Reactive.value(camera.view)
    inv_pv = inv(pv)
    width, height = widths(Reactive.value(camera.window_size)) # get pixel resolution
    x, y = pos
    # transform to normalized device coordinates [-1, 1]
    device_space = Vec4f0(
        2 * (x / width)  - 1,
        2 * (y / height) - 1,
        0.0,
        1.0
    )
    pos = inv_pv * device_space
    Point2f0(pos[1], pos[2]) / pos[4]
end

function translate_cam(
        translate, proj, view, window_size, prj_type,
        eyepos_s, lookat_s, up_s,
    )
    translate == Vec3f0(0) && return nothing # nothing to do

    lookat, eyepos, up, prjt = map(value, (lookat_s, eyepos_s, up_s, prj_type))
    dir = eyepos - lookat
    dir_len = norm(dir)
    cam_res = Vec2f0(widths(Reactive.value(window_size)))

    zoom, x, y = translate
    zoom *= 0.1f0 * dir_len
    if prjt != PERSPECTIVE
        x, y = to_worldspace(Vec2f0(x, y), Reactive.value(proj) * Reactive.value(view), cam_res)
    else
        x, y = (Vec2f0(x, y) ./ cam_res) .* dir_len
    end
    dir_norm = normalize(dir)
    right = normalize(cross(dir_norm, up))
    zoom_trans = dir_norm*zoom
    side_trans = right * (-x) + normalize(up) * y
    push!(eyepos_s, eyepos + side_trans + zoom_trans)
    push!(lookat_s, lookat + side_trans)
    nothing
end

function rotate_cam(
        theta::Vec{3, T},
        cam_right::Vec{3,T}, cam_up::Vec{3,T}, cam_dir::Vec{3, T}
    ) where T
    rotation = one(Q.Quaternion{T})
    # first the rotation around up axis, since the other rotation should be relative to that rotation
    if theta[1] != 0
        rotation *= Q.qrotation(cam_up, theta[1])
    end
    # then right rotation
    if theta[2] != 0
        rotation *= Q.qrotation(cam_right, theta[2])
    end
    # last rotation around camera axis
    if theta[3] != 0
        rotation *= Q.qrotation(cam_dir, theta[3])
    end
    rotation
end
"""
Creates a perspective camera from signals, controlling the camera
Args:

`window_size`: Size of the window

fov: Field of View
nearclip: Near clip plane
farclip: Far clip plane
`theta`: rotation around camera axis
`trans`: translation in camera space (xyz are the camera axes)
`lookatposition`: point the camera looks at
`eyeposition`: the actual position of the camera (the lense, the \"eye\")
"""
function PerspectiveCamera(
        theta,
        trans::Signal{T},
        lookatposition::Signal{T},
        eyeposition::Signal{T},
        upvector::Signal{T},
        window_size,
        fov,
        nearclip,
        farclip,
        projectiontype = Signal(PERSPECTIVE)
    ) where T<:Vec3
    # we have three ways to manipulate the camera: rotation, lookat/eyeposition and translation
    positions = (eyeposition, lookatposition, upvector)

    zoomlen = map(norm, map(-, lookatposition, eyeposition))
    projectionmatrix = map(projection_switch,
        window_size, fov, nearclip,
        farclip, projectiontype, zoomlen
    )

    # create the vievmatrix with the help of the lookat function
    viewmatrix = map(lookat, eyeposition, lookatposition, upvector)
    projectionview = map(*, projectionmatrix, viewmatrix)

    preserve(map(translate_cam,
       trans, Signal(projectionmatrix), Signal(viewmatrix), Signal(window_size),
       Signal(projectiontype),  Signal(eyeposition), Signal(lookatposition),
       Signal(upvector)
    ))

    preserve(map(theta) do theta_v
        theta_v == Vec3f0(0) && return nothing #nothing to do!
        eyepos_v, lookat_v, up_v = map(value, positions)

        dir = normalize(eyepos_v - lookat_v)
        right_v = normalize(cross(up_v, dir))
        up_v  = normalize(cross(dir, right_v))

        rotation = rotate_cam(theta_v, right_v, Vec3f0(0, 0, sign(up_v[3])), dir)
        r_eyepos = lookat_v + rotation*(eyepos_v - lookat_v)
        r_up = normalize(rotation*up_v)
        push!(eyeposition, r_eyepos)
        push!(upvector, r_up)
    end)

    PerspectiveCamera{eltype(T)}(
        window_size,
        nearclip,
        farclip,
        fov,
        viewmatrix,
        projectionmatrix,
        projectionview,
        eyeposition, lookatposition, upvector,
        trans,
        theta,
        projectiontype
    )
end

"""
get's the boundingbox of a render object.
needs value, because boundingbox will always return a boundingbox signal
"""
signal_boundingbox(robj) = Reactive.value(boundingbox(robj))

"""
Calculates union boundingbox of all elements in renderlist
(Can't do ::Vector{RenderObject{T}}, because t is not always the same)
"""
function renderlist_boundingbox(renderlist::Vector)
    renderlist = filter(x-> x != nothing, renderlist)
    isempty(renderlist) && return AABB(Vec3f0(NaN), Vec3f0(0)) # nothing to do here
    robj1 = first(renderlist)
    bb = Reactive.value(robj1[:model])*signal_boundingbox(robj1)
    for elem in renderlist[2:end]
        bb = union(Reactive.value(elem[:model])*signal_boundingbox(elem), bb)
    end
    bb
end

"""
Centers the camera on a list of render objects
"""
function center!(camera::PerspectiveCamera, renderlist::Vector; border = 0)
    bb = renderlist_boundingbox(renderlist)
    bb = AABB(minimum(bb) .- border, widths(bb) .+ 2border)
    center!(camera, bb)
end
"""
Centers a camera onto a boundingbox
"""
function center!(camera::PerspectiveCamera, bb::AABB)
    width        = widths(bb)
    half_width   = width/2f0
    lower_corner = minimum(bb)
    middle       = maximum(bb) - half_width
    if Reactive.value(camera.projectiontype) == ORTHOGRAPHIC
        area, fov, near, far = map(value,
            (camera.window_size, camera.fov, camera.nearclip, camera.farclip)
        )
        aspect = Float32(area.w/area.h)
        h = Float32(tan(fov / 360.0 * pi) * near)
        w = h * aspect
        w_, h_, _ = half_width
        zoom = Vec2f0(w_, h_)./Vec2f0(w,h)
        x, y, _ = middle
        push!(camera.eyeposition, Vec3f0(x, y, maximum(zoom)))
        push!(camera.lookat, Vec3f0(x, y, 0))
        push!(camera.up, Vec3f0(0,1,0))
    else
        push!(camera.lookat, middle)
        neweyepos = middle + (width*1.2f0)
        push!(camera.eyeposition, neweyepos)
        push!(camera.up, Vec3f0(0,0,1))
        push!(camera.nearclip, 0.1f0 * norm(widths(bb)))
        push!(camera.farclip, 3f0 * norm(widths(bb)))
    end
    return
end
function robj_from_camera() end
function renderlist() end
export renderlist, robj_from_camera

"""
Centers the camera(=:perspective) on all render objects in `window`
"""
function center!(window, camera::Symbol=:perspective; border = 0)
    rl = robj_from_camera(window, camera)
    center!(window.cameras[camera], rl, border = border)
end


#####
# Example evaluation script for TintiNet.jl
# Model IGBT (InceptGOR-Bert): 3-state SS Classifier and PHI, PSI and ACC regressor.
#####

#####
# 0. Loading Packages and defining Hyperparameters
#####

using CUDA, Flux, Transformers, JSON, BSON, Random, Dates, Printf
using IterTools: ncycle;
using Transformers.Basic, Transformers.Pretrain, Transformers.Datasets, Transformers.Stacks, Transformers.BidirectionalEncoder
using BSON: @load, @save
using Flux: @functor, onehot, onecold, mse, shuffle, sigmoid, gradient, unsqueeze
using Flux.Optimise: update!
using Statistics: mean, std

using TintiNet
Random.seed!(102528)

hyperpar = Dict(
    "max_seq_len"       => 128,     # Maximum sequence length on the model (default 192)
    "batch_size"        => 128,     # Batch size for training (default 128)
)

inputfile_path = ARGS[1] ## First argument is the location of the sequence input file
output_path = ARGS[2] ## Second argument is the directory to write the outputs
#print_header_prediction()

#####
# 1. Obtaining the data
#####

headers, sequences = read_sequences_from_file(inputfile_path, hyperpar["max_seq_len"]; filetype="fasta")

#####
# 2. Loading the saved models
#####

@load "./example_classifier_model.bson" checkpoint_model
classifier_model = checkpoint_model |> gpu
Flux.testmode!(classifier_model)

@load "./example_regressor_model.bson" checkpoint_model
regressor_model = checkpoint_model |> gpu
Flux.testmode!(regressor_model)

#####
# 3. Model evaluation
#####

ss_predictions = compute_classifier_predictions(
    classifier_model,
    sequences;
    batched=true,
    eval_batch_size=64,
    use_gpu=true,
    sleep_time_seconds=0.01)

(phi_predictions, psi_predictions, acc_predictions) = compute_regressor_predictions(
    regressor_model,
    sequences;
    batched=false,
    eval_batch_size=64,
    use_gpu=true,
    sleep_time_seconds=0.01)

#####
# 4. Saving Prediction Results
#####

write_csv_predictions(output_path, headers, sequences, ss_predictions, phi_predictions, psi_predictions, acc_predictions)


global const MODE_H = Int8(1)
global const MODE_A = Int8(-1)
global const MODE_HA = Int8(0)

global const GRB_ENV = Gurobi.Env()
global const GRB_ENV_2 = Gurobi.Env()
global const GRB_ENV_3 = Gurobi.Env()

struct IdxSet
    binrepr::Int64
end

struct RR2
    nteams::Int
    nslots::Int
    isphased::Bool
end

abstract type Constraint end

struct CAcon <: Constraint
    teams1::IdxSet
    teams2::IdxSet
    slots::IdxSet
    ub::Int
    mode::Int
    pen::Int
end

struct BRcon <: Constraint
    teams::IdxSet
    slots::IdxSet
    ub::Int
    mode::Int
    pen::Int
end

struct GAcon <: Constraint
    slots::IdxSet
    lb::Int
    ub::Int
    meetings::Vector{Tuple{Int,Int}}
    pen::Int
end

struct FAcon <: Constraint
    teams::IdxSet
    slots::IdxSet
    ub::Int
    pen::Int
end

struct SEcon <: Constraint
    teams::IdxSet
    lb::Int
    pen::Int
end

struct Instance
    rr2::RR2
    constrCA::Vector{CAcon}
    constrBR::Vector{BRcon}
    constrGA::Vector{GAcon}
    constrFA::Vector{FAcon}
    constrSE::Vector{SEcon}
end

struct TabuSearchOptimizer
    maxiter::Int
    tabulength::Int
    infeaslength::Int
    rr2::RR2
    constraints::Vector{Constraint}
    conentries::BitArray{3}
end

const Move = Tuple{Symbol, Int64, Int64, Vararg{Int64, N} where N}

mutable struct SearchState
    it::Int # iteration count
    hpen::Int
    spen::Int # current value
    sched::Matrix{Int}
    optval::Int # best known value
    optsched::Matrix{Int} # best known solution
    lastimprov::Int # last iteration in which global improvement of obj function was fou
    offset::Vector{Int}
    moves::Vector{Move}
    tabu::Dict{Move,Int}
    blocked::Dict{Move,Int}
    pen::Vector{Float64} # penalty for each constraint
end

function IdxSet(A::Vector{T}) where {T<:Integer}
    binrepr = 0
    for i in A
        if binrepr & (1 << (i - 1)) !== 0
            throw(ArgumentError("Invalid Array: has duplicates"))
        end
        if i < 1 || i > 64
            throw(ArgumentError("Invalid Array: has values not in range 1:64"))
        end
        binrepr += 2^(i - 1)
    end
    return IdxSet(binrepr)
end

Base.eltype(::IdxSet) = Int
@inline Base.in(n::Int64, s::IdxSet) = s.binrepr & (1 << (n - 1)) !== 0
@inline Base.length(s::IdxSet) = count_ones(s.binrepr)

function Base.iterate(s::IdxSet, state = 1)
    trunc = s.binrepr >>> (state - 1)
    trunc === 0 && return nothing
    c = trailing_zeros(trunc)
    return state + c, state + c + 1
end

show(io::IO, x::IdxSet) = join(string.(i for i in x), ' ')

function Instance(rr2::RR2, cons::Vector{Constraint})
    D = Dict(CAcon => 1, BRcon => 2, GAcon => 3, FAcon => 4, SEcon => 5)
    groupedcons = (
        Vector{CAcon}(),
        Vector{BRcon}(),
        Vector{GAcon}(),
        Vector{FAcon}(),
        Vector{SEcon}(),
    )
    for c in cons
        push!(groupedcons[D[typeof(c)]], c)
    end
    return Instance(rr2, groupedcons...)
end

struct PGBACut
    ub::Int
    assignments::Vector{Tuple{Int,Int}}
end


mutable struct GFUpdateState
    prev_trace::DynamicDSLTrace
    trace::DynamicDSLTrace
    constraints::Any
    weight::Float64
    visitor::AddressVisitor
    params::Dict{Symbol,Any}
    discard::DynamicChoiceMap
end

function GFUpdateState(gen_fn, args, prev_trace, constraints, params)
    visitor = AddressVisitor()
    discard = choicemap()
    trace = DynamicDSLTrace(gen_fn, args)
    GFUpdateState(prev_trace, trace, constraints,
        0., visitor, params, discard)
end

function traceat(state::GFUpdateState, dist::Distribution{T},
                 args::Tuple, key) where {T}

    local prev_retval::T
    local retval::T

    # check that key was not already visited, and mark it as visited
    visit!(state.visitor, key)

    # check for previous choice at this key
    has_previous = has_choice(state.prev_trace, key)
    if has_previous
        prev_choice = get_choice(state.prev_trace, key)
        prev_retval = prev_choice.retval
        prev_score = prev_choice.score
    end

    # check for constraints at this key
    constrained = has_value(state.constraints, key)
    !constrained && check_no_submap(state.constraints, key)

    # record the previous value as discarded if it is replaced
    if constrained && has_previous
        set_value!(state.discard, key, prev_retval)
    end

    # get return value
    if constrained
        retval = get_value(state.constraints, key)
    elseif has_previous
        retval = prev_retval
    else
        retval = random(dist, args...)
    end

    # compute logpdf
    score = logpdf(dist, retval, args...)

    # update the weight
    if has_previous
        state.weight += score - prev_score
    elseif constrained
        state.weight += score
    end

    # add to the trace
    add_choice!(state.trace, key, retval, score)

    retval
end

function traceat(state::GFUpdateState, gen_fn::GenerativeFunction{T,U},
                 args::Tuple, key) where {T,U}

    local prev_subtrace::U
    local subtrace::U
    local retval::T

    # check key was not already visited, and mark it as visited
    visit!(state.visitor, key)

    # check for constraints at this key
    check_no_value(state.constraints, key)
    constraints = get_submap(state.constraints, key)

    # get subtrace
    has_previous = has_call(state.prev_trace, key)
    if has_previous
        prev_call = get_call(state.prev_trace, key)
        prev_subtrace = prev_call.subtrace
        get_gen_fn(prev_subtrace) === gen_fn || gen_fn_changed_error(key)
        (subtrace, weight, _, discard) = update(prev_subtrace,
            args, map((_) -> UnknownChange(), args), constraints)
    else
        (subtrace, weight) = generate(gen_fn, args, constraints)
    end

    # update the weight
    state.weight += weight

    # update discard
    if has_previous
        set_submap!(state.discard, key, discard)
    end

    # add to the trace
    add_call!(state.trace, key, subtrace)

    # get return value
    retval = get_retval(subtrace)

    retval
end

function splice(state::GFUpdateState, gen_fn::DynamicDSLFunction,
                args::Tuple)
    prev_params = state.params
    state.params = gen_fn.params
    retval = exec(gen_fn, state, args)
    state.params = prev_params
    retval
end

function update_delete_recurse(prev_trie::Trie{Any,ChoiceOrCallRecord},
                               visited::EmptySelection)
    score = 0.
    for (key, choice_or_call) in get_leaf_nodes(prev_trie)
        score += choice_or_call.score
    end
    for (key, subtrie) in get_internal_nodes(prev_trie)
        score += update_delete_recurse(subtrie, EmptySelection())
    end
    score
end

function update_delete_recurse(prev_trie::Trie{Any,ChoiceOrCallRecord},
                               visited::DynamicSelection)
    score = 0.
    for (key, choice_or_call) in get_leaf_nodes(prev_trie)
        if !(key in visited)
            score += choice_or_call.score
        end
    end
    for (key, subtrie) in get_internal_nodes(prev_trie)
        subvisited = visited[key]
        score += update_delete_recurse(subtrie, subvisited)
    end
    score
end

function update_delete_recurse(prev_trie::Trie{Any,ChoiceOrCallRecord},
                               visited::AllSelection)
    0.
end

function add_unvisited_to_discard!(discard::DynamicChoiceMap,
                                   visited::DynamicSelection,
                                   prev_choices::ChoiceMap)
    for (key, value) in get_values_shallow(prev_choices)
        if !(key in visited)
            @assert !has_value(discard, key)
            @assert isempty(get_submap(discard, key))
            set_value!(discard, key, value)
        end
    end
    for (key, submap) in get_submaps_shallow(prev_choices)
        @assert !has_value(discard, key)
        if key in visited
            # the recursive call to update already handled the discard
            # for this entire submap
            continue
        else
            subvisited = visited[key]
            if isempty(subvisited)
                # none of this submap was visited, so we discard the whole thing
                @assert isempty(get_submap(discard, key))
                set_submap!(discard, key, submap)
            else
                subdiscard = get_submap(discard, key)
                add_unvisited_to_discard!(
                    isempty(subdiscard) ? choicemap() : subdiscard,
                    subvisited, submap)
                set_submap!(discard, key, subdiscard)
            end
        end
    end
end

function update(trace::DynamicDSLTrace, arg_values::Tuple, arg_diffs::Tuple,
                constraints::ChoiceMap)
    gen_fn = trace.gen_fn
    state = GFUpdateState(gen_fn, arg_values, trace, constraints, gen_fn.params)
    retval = exec(gen_fn, state, arg_values)
    set_retval!(state.trace, retval)
    visited = get_visited(state.visitor)
    state.weight -= update_delete_recurse(trace.trie, visited)
    add_unvisited_to_discard!(state.discard, visited, get_choices(trace))
    if !all_visited(visited, constraints)
        error("Did not visit all constraints")
    end
    (state.trace, state.weight, UnknownChange(), state.discard)
end


using GLAbstraction, GeometryTypes, ColorTypes, GLFW, Reactive, ModernGL
const GLA = GLAbstraction

const window = GLFW.Window(name="Example Geometry Shader")
GLFW.MakeContextCurrent(window)
GLA.set_context!(window)

const vert = GLA.vert"""
#version 330
in vec2 pos;
out vec2 g_position;

void main() {
    g_position = pos;
}
"""

const frag = GLA.frag"""
#version 330

out vec4 outColor;

void main() {
    outColor = vec4(1,0,0,1);
}
"""

const geom = GLA.geom"""
#version 330

layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
in vec2 g_position[];

void main(void)
{
  vec2 p0 = g_position[0];

  gl_Position = vec4(p0, 0, 1);
  EmitVertex();

  gl_Position = vec4(p0+vec2(0,0.1), 0, 1);
  EmitVertex();

  gl_Position = vec4(p0+vec2(0.1,0), 0, 1);
  EmitVertex();

  gl_Position = vec4(p0+vec2(0.1), 0, 1);
  EmitVertex();

  EndPrimitive();
}
"""

const b = Point2f0[(-0.5,0),(0.0, 0.0),(0.4, 0.3)]

program = GLA.Program(vert, geom, frag)
robj = GLA.VertexArray(GLA.generate_buffers(program, GLA.GEOMETRY_DIVISOR, pos=b))

glClearColor(0,0,0,1)

GLA.bind(program)
while !GLFW.WindowShouldClose(window)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    GLA.bind(robj)
    GLA.draw(robj)
    GLFW.SwapBuffers(window)
    GLFW.PollEvents()
    if GLFW.GetKey(window, GLFW.KEY_ESCAPE) == GLFW.PRESS
        GLFW.SetWindowShouldClose(window, true)
    end
end
GLFW.DestroyWindow(window)


using DFTK
using Unitful
using UnitfulAtomic
using JLD2
using PyCall
using MPI

if mpi_nprocs() == 1
    setup_threading()
else
    disable_threading()
end

# This tries to replicate the computational setup used for generating
# the materials project data as closely as possible. For details on
# the parameter choices see
#
#    https://docs.materialsproject.org/methodology/total-energies/
#
#
# All functions are MPI-ready, i.e. can be used through an MPI-parallelised julia session.

#
# If convergence issues on a system, try one of them (in this order and separately)
#    (a) is_metal=false
#    (b) use_experimental=false
#    (c) use_experimental=false, lower the damping (to 0.6, 0.4 and below if needed.)
#

function compute_formation_energy_per_atom(file::String; kwargs_bulk=(; ),
                                           kwargs_compound=(; ), kwargs...)
    scfres = run_if_needed(file; kwargs..., kwargs_compound...)

    bulk_energy_per_atom = Dict(map(scfres.basis.model.atoms) do (element, _)
        scfres_bulk = run_if_needed(element.symbol; kwargs..., kwargs_bulk...)
        n_atoms_bulk = sum(el_pos -> length(el_pos[2]), scfres_bulk.basis.model.atoms)
        element => scfres_bulk.energies.total / n_atoms_bulk
    end)

    atoms   = scfres.basis.model.atoms
    n_atoms = sum(el_pos -> length(el_pos[2]), atoms)
    energy_bulk = sum(el_pos -> bulk_energy_per_atom[el_pos[1]] * length(el_pos[2]), atoms)
    formation_energy_hartree = (scfres.energies.total - energy_bulk) / n_atoms
    auconvert(u"eV", formation_energy_hartree)
end

function run_if_needed(file::String; kwargs...)
    outfile = joinpath(@__DIR__, file * ".jld2")
    if !isfile(outfile)
        mpi_master() && println("#\n# -- $file\n#")
        scfres = run_from_file(file; kwargs...)
        store_results(scfres, outfile)
        scfres
    else
        load_results(outfile)
    end
end
function run_if_needed(element::Symbol; kwargs...)
    mkpath(joinpath(@__DIR__, "reference"))
    outfile = joinpath(@__DIR__, "reference", "$element.jld2")
    if !isfile(outfile)
        mpi_master() && println("#\n# -- $element\n#")
        scfres = run_bulk(element; kwargs...)
        store_results(scfres, outfile)
        scfres
    else
        load_results(outfile)
    end
end

function run_bulk(element::Symbol; kwargs...)
    bulkstructure = pyimport("ase.build").bulk(string(element))
    lattice = load_lattice(bulkstructure)
    atoms   = load_atoms(bulkstructure)
    run_dftk(lattice, atoms; kwargs...)
end

function run_from_file(file::AbstractString; kwargs...)
    lattice = load_lattice(file)
    atoms   = load_atoms(file)
    run_dftk(lattice, atoms; kwargs...)
end

# Store the results from `run_from_file`
function store_results(scfres, file)
    basis_master = DFTK.gather_kpts(scfres.basis)
    if mpi_master()
        JLD2.jldopen(file, "w") do jld
            jld["basis"]    = basis_master
            jld["ρ"]        = scfres.ρ
            jld["energies"] = scfres.energies
        end
    end
end

function load_results(file)
    JLD2.jldopen(file, "r") do jld
        (energies=jld["energies"], basis=jld["basis"], ρ=jld["ρ"])
    end
end

function run_dftk(lattice, atoms; use_experimental=true, is_metal=true, damping=0.8, debug=false, kwargs...)
    n_atoms = sum(el_pos -> length(el_pos[2]), atoms)

    # Check there are no atoms in the atoms list that require GGA+U
    has_ggau_metal = any(atoms) do (element, positions)
        element.symbol in (:V, :Cr, :Mn, :Fe, :Co, :Ni, :W, :Mo)
    end
    has_oxygen   = any(el_pos -> el_pos[1].symbol == :O, atoms)
    has_flouride = any(el_pos -> el_pos[1].symbol == :F, atoms)
    if (has_oxygen || has_flouride) && has_ggau_metal
        error("This compound requires GGA+U which is not implemented in DFTK.")
    end

    # Pseudos: Select the ones with largest number of electrons
    atoms = map(atoms) do (element, positions)
        symbol = element.symbol
        pspid = sort(list_psp(symbol, functional="pbe"), by=t->t.n_elec_valence)[end]
        ElementPsp(symbol, psp=load_psp(pspid.identifier)) => positions
    end

    magnetic_moments = map(atoms) do (element, positions)
        element => fill(default_magnetic_moment(element.symbol), length(positions))
    end
    # https://docs.materialsproject.org/methodology/total-energies/#calculation-details

    # They used the Tetrahedron method + Blöchl corrections ... which we don't have in DFTK.
    smearing = Smearing.Gaussian()
    temperature = 0.2u"eV"
    model = model_PBE(lattice, atoms; temperature, magnetic_moments, smearing)

    if debug
        kgrid = kgrid_from_minimal_n_kpoints(lattice, 6)
        Ecut = 10
    else
        kgrid = kgrid_from_minimal_n_kpoints(lattice, min(500, ceil(Int, 1000 / n_atoms)))
        Ecut  = 520u"eV"
    end
    basis = PlaneWaveBasis(model; Ecut, kgrid)

    ρ       = guess_density(basis, magnetic_moments)
    tol     = austrip(n_atoms * 5e-5u"eV")
    n_bands = DFTK.default_n_bands(basis.model)

    if use_experimental
        mixing    = is_metal ? KerkerMixing() : SimpleMixing()
        damping   = :adaptive
        algorithm = ("adaptive damping", DFTK.scf_potential_mixing_adaptive)
        extra     = (; )
    else
        mixing  = LdosMixing()
        extra = (damping=damping, )
        algorithm = ("fixed damping", self_consistent_field)
    end

    if mpi_master()
        magmom_compact = [element.symbol => values for (element, values) in magnetic_moments]
        display(basis)
        println()
        println()
        println("Solver parameters:")
        println("    magmom     : $magmom_compact")
        println("    tol        : $tol")
        println("    n_bands    : $n_bands")
        println("    algorithm  : $(algorithm[1])")
        println("    mixing     : $mixing")
        println("    damping    : $damping")
        println("    kwargs     : $(Dict(kwargs...))")
        println()
        flush(stdout)
    end

    DFTK.reset_timer!(DFTK.timer)
    run_scf = algorithm[2]
    scfres = run_scf(basis; ρ, tol, mixing, n_bands, maxiter=200, extra..., kwargs...)
    if mpi_master()
        println()
        println(DFTK.timer)
        println()
    end
    scfres
end

function default_magnetic_moment(element::Symbol)
    # TODO Consider adding this to PeriodicTable
    #      ... or DFTK ... or both
    get(Dict(
        :H  => 1.0,
        :He => 0.0,
        :Li => 1.0,
        :Be => 0.0,
        :B  => 1.0,
        :C  => 2.0,
        :N  => 3.0,
        :O  => 2.0,
        :F  => 1.0,
        :Ne => 0.0,
        :Na => 1.0,
        :Mg => 0.0,
        :Al => 1.0,
        :Si => 2.0,
        :P  => 3.0,
        :S  => 2.0,
        :Cl => 1.0,
        :Ar => 0.0,
        :K  => 1.0,
        :Ca => 0.0,
        :Sc => 1.0,
        :Ti => 2.0,
        :V  => 3.0,
        :Cr => 6.0,
        :Mn => 5.0,
        :Fe => 4.0,
        :Co => 3.0,
        :Ni => 2.0,
        :Cu => 1.0,
        :Zn => 0.0,
        :Ga => 1.0,
        :Ge => 2.0,
        :As => 3.0,
        :Se => 2.0,
        :Br => 1.0,
        :Kr => 0.0,
        :Rb => 1.0,
        :Sr => 0.0,
        :Y  => 1.0,
        :Zr => 2.0,
        :Nb => 5.0,
        :Mo => 6.0,
        :Tc => 5.0,
        :Ru => 4.0,
        :Rh => 3.0,
        :Pd => 0.0,
        :Ag => 1.0,
        :Cd => 0.0,
        :In => 1.0,
        :Sn => 2.0,
        :Sb => 3.0,
        :Te => 2.0,
        :I  => 1.0,
        :Xe => 0.0,
        :Cs => 1.0,
        :Ba => 0.0,
        :La => 1.0,
        :Ce => 1.0,
        :Pr => 3.0,
        :Nd => 4.0,
        :Pm => 5.0,
        :Sm => 6.0,
        :Eu => 7.0,
        :Gd => 8.0,
        :Tb => 5.0,
        :Dy => 4.0,
        :Ho => 3.0,
        :Er => 2.0,
        :Tm => 1.0,
        :Yb => 0.0,
        :Lu => 1.0,
        :Hf => 2.0,
        :Ta => 3.0,
        :W  => 4.0,
        :Re => 5.0,
        :Os => 4.0,
        :Ir => 3.0,
        :Pt => 2.0,
        :Au => 1.0,
        :Hg => 0.0,
        :Tl => 1.0,
        :Pb => 2.0,
        :Bi => 3.0,
        :Po => 2.0,
        :At => 1.0,
        :Rn => 0.0,
        :Fr => 1.0,
        :Ra => 0.0,
        :Ac => 1.0,
        :Th => 2.0,
        :Pa => 3.0,
        :U  => 4.0,
        :Np => 5.0,
        :Pu => 6.0,
        :Am => 7.0,
        :Cm => 8.0,
        :Bk => 5.0,
        :Cf => 4.0,
        :Es => 4.0,
        :Fm => 2.0,
        :Md => 1.0,
        :No => 0.0,
    ), element, 0)
end


using Levenshtein

function randString(len)
    return string([Char(round(Int,rand() * 255 + 1)) for _ in 1:len]...)
end

function run_huge_string(l1, l2)
    # setup long string against short one to expose element copy performance
    s = randString(l1)
    t = randString(l2)
    ins, del, sub = rand() * 1000, rand() * 1000, rand() * 1000

    # run a long string against a not so long one
    println("\nRunning $l1-char String against $l2-char String")
    @time levenshtein(s, t, ins, del, sub)
end

function run_multiple_times(l1, l2, runs)
    # setup
    s = randString(l1)
    t = randString(l2)
    ins, del, sub = rand() * 1000, rand() * 1000, rand() * 1000
    costs = Array{Float64}(2, max(length(s), length(t)) + 1)

    # run without reusing cost matrix
    println("\nRunning $runs runs without reusing cost matrix for $l1-char String vs $l2-char String")
    @time for i in 1:runs
        levenshtein(s, t, ins, del, sub)
    end

    # run without allocating cost matrix everytime
    println("\nRunning $runs runs with resuable cost matrix for $l1-char String vs $l2-char String")
    @time for i in 1:runs
        levenshtein!(s, t, ins, del, sub, costs)
    end
end

run_huge_string(10^7, 10^2)
run_huge_string(10^6, 10^3)
run_huge_string(10^5, 10^4)
run_multiple_times(10^3, 10^3, 10^3)
run_multiple_times(10^2, 10^2, 10^5)
run_multiple_times(10^1, 10^1, 10^7)


"""
    dfr = session_frame(ai; odortiming=1, vaccuum=2, soundlick=3, camera=4, sniff=5, lickometer=6, odordirection=7, lickbinary=8, fs=$default_fs)

Construct a trial session DataFrame from an input matrix `ai`. The keyword arguments provide the column index for each signal.
The return value is a `DataFrameRate` which also encodes the sampling frequency (as supplied by `fs`).
"""
function session_frame(ai; odortiming=1, vaccuum=2, soundlick=3, camera=4, sniff=5, lickometer=6, odordirection=7, lickbinary=8, fs=default_fs)
    df = DataFrame()
    if soundlick ∈ axes(ai, 2)
        df.soundlick = view(ai, :, soundlick)
    end
    if lickometer ∈ axes(ai, 2)
        df.lickometer = view(ai, :, lickometer)
    end
    if odortiming ∈ axes(ai, 2)
        df.odortiming = view(ai, :, odortiming)
    end
    if odordirection ∈ axes(ai, 2)
        df.odordirection = view(ai, :, odordirection)
    end
    if sniff ∈ axes(ai, 2)
        df.sniff = view(ai, :, sniff)
    end
    return DataFrameRate(df, fs)
end

"""
    trialdatas(dfr::DataFrameRate; fcarrierlick=60Hz, soundduration=1s, trialoffset=-1s, trialduration=10s, minlickduration=0.005s, maxlickamp=0.1, lohi_sound=(1, 3), lohi_odor=(0.5, 3.0), tprecision=0.1)
    trialdatas(df::DataFrame; fs=$default_fs, fcarrierlick=60Hz, soundduration=1s, trialoffset=-1s, trialduration=10s, minlickduration=0.005s, maxlickamp=0.1, lohi_sound=(1, 3), lohi_odor=(0.5, 3.0), tprecision=0.1)

Collect data on all trials in `df`, which should be a DataFrame with colums "soundlick", "lickometer", "odortiming", and "odordirection".
Each should be an analog signal with timing data.
Supplying `dfr` provides the DataFrame and the sampling rate.

The keyword arguments allow you to set:
- `fs`: the sampling frequency (affects conversion to physical units)
- `fcarrierlick`: the frequency of the carrier signal for lick detection (grounding this signal indicates a lick)
- `soundduration`: the expected duration of the sound pulse. This is used to find trials.
- `tprecision`: fractional "slop" in timing permitted for identifying a sound pulse.
- `trialoffset`: start of the trial relative to the sound pulse onset
- `trialduration`: length of the trial
- `minlickduration`: the minimum length of grounding the lick carrier signal to count as a lick
- `maxlickamp`: maximum allowed deviation of carrier voltage during a lick (as a fraction of lick carrier stddev)
- `lohi_sound`: voltage thresholds for sound lo and hi (a sound pulse starts when this goes hi)
- `lohi_odor`: voltage threshold for odor timing (odor pulse starts when this goes hi)
"""
function trialdatas(df::DataFrame; fs=default_fs, fcarrierlick=60Hz, soundduration=1s, trialoffset=-1s, trialduration=10s, minlickduration=0.005s, maxlickamp=0.1, lohi_sound=(1, 3), lohi_odor=(0.5, 3.0), tprecision=0.1, kwargs...)
    soundlick, lick, odortiming, dir = df.soundlick, df.lickometer, df.odortiming, df.odordirection
    checkcarrier(lick, fs, fcarrierlick; kwargs...)
    lickthresh = std(lick) * maxlickamp
    alltrs = TrialData[]
    i, n = 1, length(soundlick)
    i = advance_to_lo(soundlick, i, n, lohi_sound)
    while i < n
        i = advance_to_hi(soundlick, i, n, lohi_sound)
        i >= n && break
        # We're at a rising edge. When does it fall again?
        j = advance_to_lo(soundlick, i, n, lohi_sound)
        thi = (j-i)/fs   # duration, in time units, of the hi pulse
        if (1-tprecision)*soundduration <= thi <= (1+tprecision)*soundduration
            # We're in a sound marker. Initiate a trial.
            tr = clamp(i + round(Int, float(trialoffset*fs)), 1, n) : clamp(i + round(Int, (trialduration + trialoffset) * float(fs)), 1, n)
            si = (i-first(tr))/fs .. (j-first(tr))/fs
            icue = advance_to_hi(soundlick, j, n, lohi_sound)
            iodor = advance_to_hi(odortiming, tr, lohi_odor)
            jodor = advance_to_lo(odortiming, iodor, last(tr), lohi_odor)
            oi = (iodor-first(tr))/fs .. (jodor-first(tr))/fs
            trs = TrialData(tr, si, (icue - first(tr))/fs, oi, dir[i])
            addlicks!(trs.lickonsets, view(lick, tr); fs, minlickduration, lickthresh)

            push!(alltrs, trs)
            i = last(tr)
        else
            error("soundlick pulse of duration $(uconvert(s, thi)) starting at $i not recognized")
        end
    end
    return alltrs
end
trialdatas(dfr::DataFrameRate; kwargs...) = trialdatas(dfr.df; fs=dfr.fs, kwargs...)

# Detect all licks in the trial. `lick` should be just the voltages recorded during a single trial.
function addlicks!(out, lick; fs, minlickduration, lickthresh)
    k = k1 = firstindex(lick)
    while k < lastindex(lick)
        k += 1
        abs(lick[k]) < lickthresh || continue        # if we're not close to ground, don't look for a lick
        kend = k
        while kend < lastindex(lick)
            kend += 1
            abs(lick[kend]) < lickthresh || break    # keep looking forward until an above-threshold deviation from ground
        end
        if kend - k > fs*minlickduration
            push!(out, (k - k1)/fs)                  # we stayed grounded for long enough, count it as a lick
        end
        k = kend
    end
    return out
end

"""
    is_lick(td::TrialData; deadline=4s)

Return `true` if the animal licked. Only consider licks occurring after the cue time and within `deadline` thereafter.
"""
function is_lick(td::TrialData; deadline=4s)
    tstart = td.cuetime # minimum(td.odorinterval)
    firstidx = searchsortedfirst(td.lickonsets, tstart)
    firstidx > lastindex(td.lickonsets) && return false
    return td.lickonsets[firstidx] - tstart < deadline
end

"""
    collect_by_trialtype(ispos::Function, data, tds::AbstractVector{TrialData})

Categorize `data` by trial type (true positive, true negative, false positive, false negative).
`ispos(td)` should return `true` if this trial should be a positive (lick) response.
"""
function collect_by_trialtype(ispos, data, tds::AbstractVector{TrialData})
    T = typeof(first(data))
    out = Dict("TP" => T[], "TN" => T[], "FP" => T[], "FN" => T[])
    for (val, td) in zip(data, tds)
        isp, isl = ispos(td), is_lick(td)
        key =  isp &  isl ? "TP" :
               isp & !isl ? "FN" :
              !isp &  isl ? "FP" : "TN"
        push!(out[key], val)
    end
    return out
end

"""
    rng = trialrange(ispos::AbstractVector{Bool}, fractionpositive=0.5, pval=0.01)

Compute the largest range over which the probability of `true` values within `ispos` is consistent with `fractionpositive`.
`pval` sets the p-value for determining inconsistency.
"""
function trialrange(ispos, fractionpositive=0.5, pval=0.05)
    # It's not obvious that one can easily compute the distribution of consistent stretches,
    # so do it by simulation. We cache simulation results to save computation time.
    key = (ceil(Int, log2(length(ispos))), Float64(fractionpositive))
    mindist, maxdist = get!(_trialdistribution, key) do
        simulate_trialdist(key...)
    end
    minbounds = getindex_fraction.(mindist, pval/2)
    maxbounds = getindex_fraction.(maxdist, 1-pval/2)
    # Block out the bad stretches
    cispos = cumsum(ispos)
    isok = fill(true, eachindex(ispos))
    n = length(ispos)
    for k = 1:n-1
        mnb, mxb = minbounds[k], maxbounds[k]
        for start = 1:n-k
            all(isok[start:start+k]) || continue
            np = cispos[start+k] - cispos[start]
            if !(mnb <= np <= mxb)
                isok[start:start+k] .= false
            end
        end
    end
    # Find the longest contiguous stretch
    startbest, stopbest = 1, 0
    istart = firstindex(isok)
    while istart < lastindex(isok)
        if !isok[istart]
            istart += 1
            continue
        end
        iend = istart
        while iend < lastindex(isok) && isok[iend+1]
            iend += 1
        end
        if iend - istart > stopbest - startbest
            startbest, stopbest = istart, iend
        end
        istart = iend+1
    end
    return startbest:stopbest
end

const VVI = Vector{Vector{Int}}
const _trialdistribution = Dict{Tuple{Int,Float64},Tuple{VVI,VVI}}()  # cached simulation results

function simulate_trialdist(log2n::Int, fractionpositive::Float64, nsim::Int=10^4)
    n = 2^log2n
    npmindists = [fill(n+1, nsim) for _ = 1:n-1]
    npmaxdists = [fill( -1, nsim) for _ = 1:n-1]
    for isim = 1:nsim
        b = rand(n) .<= fractionpositive
        cb = cumsum(b)
        for k = 1:n-1
            npmin, npmax = typemax(Int), typemin(Int)
            @inbounds @simd for start = 1:n-k
                np = cb[start+k] - cb[start]
                npmin = min(npmin, np)
                npmax = max(npmax, np)
            end
            npmindists[k][isim] = npmin
            npmaxdists[k][isim] = npmax
        end
    end
    foreach(sort!, npmindists)
    foreach(sort!, npmaxdists)
    return npmindists, npmaxdists
end


module GLHelper

using ModernGL

glUniform(index::GLint, value::Float32) = glUniform1f(index, value)
glUniform(index::GLint, value::Tuple{Float32, Float32}) = glUniform2f(index, value[1], value[2])
glUniform(index::GLint, value::Tuple{Float32, Float32, Float32}) = glUniform3f(index, value[1], value[2], value[3])
glUniform(index::GLint, value::Tuple{Float32, Float32, Float32, Float32}) = glUniform4f(index, value[1], value[2], value[3], value[4])

glUniform(index::GLint, value::Int32) = glUniform1i(index, value)
glUniform(index::GLint, value::Tuple{Int32, Int32}) = glUniform2i(index, value[1], value[2])
glUniform(index::GLint, value::Tuple{Int32, Int32, Int32}) = glUniform3i(index, value[1], value[2], value[3])
glUniform(index::GLint, value::Tuple{Int32, Int32, Int32, Int32}) = glUniform4i(index, value[1], value[2], value[3], value[4])

glUniform(index::GLint, value::UInt32) = glUniform1ui(index, value)
glUniform(index::GLint, value::Tuple{UInt32, UInt32}) = glUniform2ui(index, value[1], value[2])
glUniform(index::GLint, value::Tuple{UInt32, UInt32, UInt32}) = glUniform3ui(index, value[1], value[2], value[3])
glUniform(index::GLint, value::Tuple{UInt32, UInt32, UInt32, UInt32}) = glUniform4ui(index, value[1], value[2], value[3], value[4])

nofunc(x...) = nothing

macro notranspose(func)
	quote
		function (location::GLint, count::GLsizei, value::Ptr{GLfloat})
			$func(location, count, GL_FALSE, value)
		end
	end
end

const uniformFuncTable =
	[glUniform1fv		 nofunc							 nofunc							 nofunc							;
	 glUniform2fv		 @notranspose(glUniformMatrix2fv)   @notranspose(glUniformMatrix3x2fv) @notranspose(glUniformMatrix4x2fv);
	 glUniform3fv		 @notranspose(glUniformMatrix2x3fv) @notranspose(glUniformMatrix3fv)   @notranspose(glUniformMatrix4x3fv);
	 glUniform4fv		 @notranspose(glUniformMatrix2x4fv) @notranspose(glUniformMatrix3x4fv) @notranspose(glUniformMatrix4fv)  ]

function get_uniform_func_float(rows::Int, cols::Int)
	func = uniformFuncTable[rows, cols]
	@assert func != nofunc
	return func
end

function glUniform(index::GLint, value::Vector{Float32})
	glUniformFunc = get_uniform_func_float(length(value), 1)
	glUniformFunc(index, one(GLsizei), convert(Ptr{Float32}, value))
end

function glUniform(index::GLint, value::Matrix{Float32})
	rows, cols = size(value)
	glUniformMatrixFunc = get_uniform_func_float(rows, cols)
	glUniformMatrixFunc(index, one(GLsizei), convert(Ptr{Float32}, value))
end

function glUniform(index::GLint, value::Array{Float32, 3})
	rows, cols, elements = size(value)
	local glUniformFunc
	glUniformFunc = get_uniform_func_float(rows, cols)
	glUniformFunc(index, convert(GLsizei, elements), convert(Ptr{Float32}, value))
end

# todo: add glUniform for Vector{Int32 & UInt32}

function gl_get_current_program()
	currentProgram = GLint[0]
	glGetIntegerv(GL_CURRENT_PROGRAM, currentProgram)
	return currentProgram[1]
end

function gl_clear_buffers(color, depth, stencil)
	mask = 0
	if color !== nothing
		glClearColor(color...)
		mask |= GL_COLOR_BUFFER_BIT
	end
	if depth !== nothing
		glClearDepthf(depth)
		mask |= GL_DEPTH_BUFFER_BIT
	end
	if stencil !== nothing
		glClearStencil(stencil)
		mask |= GL_STENCIL_BUFFER_BIT
	end
	glClear(mask)
end

function gl_info()
	vendor = unsafe_string(glGetString(GL_VENDOR))
	version = unsafe_string(glGetString(GL_VERSION))
	renderer = unsafe_string(glGetString(GL_RENDERER))
	@info("OpenGL Vendor: $vendor, Version: $version, Renderer: $renderer")
end

export glUniform, gl_get_current_program, gl_clear_buffers, gl_info

end