"""
    build a single histogram containing all grads and weight information
"""
function hist_kernel_grad!(h::CuDeviceArray{T,3}, δ𝑤::CuDeviceMatrix{T}, xid::CuDeviceMatrix{UInt8},
    𝑖::CuDeviceVector{S}, 𝑗::CuDeviceVector{S}) where {T,S}

    nbins = size(h, 2)

    it = threadIdx().x
    id, jd = blockDim().x, blockDim().y
    ib, j = blockIdx().x, blockIdx().y
    ig, jg = gridDim().x, gridDim().y

    shared = @cuDynamicSharedMem(T, (size(h, 1), size(h, 2)))
    fill!(shared, 0)
    sync_threads()

    i_tot = length(𝑖)
    iter = 0
    @inbounds while iter * id * ig < i_tot
        i = it + id * (ib - 1) + iter * id * ig
        @inbounds if i <= length(𝑖) && j <= length(𝑗)
            i_idx = 𝑖[i]
            CUDA.atomic_add!(pointer(shared, (xid[i_idx, 𝑗[j]] - 1) * 3 + 1), δ𝑤[1, i_idx])
            CUDA.atomic_add!(pointer(shared, (xid[i_idx, 𝑗[j]] - 1) * 3 + 2), δ𝑤[2, i_idx])
            CUDA.atomic_add!(pointer(shared, (xid[i_idx, 𝑗[j]] - 1) * 3 + 3), δ𝑤[3, i_idx])
        end
        iter += 1
    end
    sync_threads()
    # loop over i blocks
    if it <= nbins
        @inbounds hid = Base._to_linear_index(h, 1, it, 𝑗[j])
        @inbounds CUDA.atomic_add!(pointer(h, hid), shared[1, it])

        @inbounds hid = Base._to_linear_index(h, 2, it, 𝑗[j])
        @inbounds CUDA.atomic_add!(pointer(h, hid), shared[2, it])

        @inbounds hid = Base._to_linear_index(h, 3, it, 𝑗[j])
        @inbounds CUDA.atomic_add!(pointer(h, hid), shared[3, it])
    end
    sync_threads()
    return nothing
end

# base approach - block built along the cols first, the rows (limit collisions)
function update_hist_gpu!(
    ::L,
    h::CuArray{T,3},
    δ𝑤::CuMatrix{T},
    X_bin::CuMatrix{UInt8},
    𝑖::CuVector{S},
    𝑗::CuVector{S}, K;
    MAX_THREADS=256) where {L <: GradientRegression,T,S}

    nbins = size(h, 2)
    thread_i = max(nbins, min(MAX_THREADS, length(𝑖)))
    threads = (thread_i, 1)
    blocks = (8, length(𝑗))
    # println("threads: ", threads, " | blocks: ", blocks)
    @cuda blocks = blocks threads = threads shmem = sizeof(T) * size(h, 1) * nbins hist_kernel_grad!(h, δ𝑤, X_bin, 𝑖, 𝑗)
    CUDA.synchronize()
    return nothing
end

"""
    build a single histogram containing all grads and weight information
"""
function hist_kernel_gauss!(h::CuDeviceArray{T,3}, δ𝑤::CuDeviceMatrix{T}, xid::CuDeviceMatrix{UInt8},
    𝑖::CuDeviceVector{S}, 𝑗::CuDeviceVector{S}) where {T,S}

    nbins = size(h, 2)

    it, k = threadIdx().x, threadIdx().y
    id, jd = blockDim().x, blockDim().y
    ib, j = blockIdx().x, blockIdx().y
    ig, jg = gridDim().x, gridDim().y

    shared = @cuDynamicSharedMem(T, (size(h, 1), size(h, 2)))
    fill!(shared, 0)
    sync_threads()

    i_tot = length(𝑖)
    iter = 0
    @inbounds while iter * id * ig < i_tot
        i = it + id * (ib - 1) + iter * id * ig
        @inbounds if i <= length(𝑖) && j <= length(𝑗)
            i_idx = 𝑖[i]
            CUDA.atomic_add!(pointer(shared, (xid[i_idx, 𝑗[j]] - 1) * 5 + k), δ𝑤[k, i_idx])
        end
        iter += 1
    end
    sync_threads()
    # loop over i blocks
    if it <= nbins
        @inbounds hid = Base._to_linear_index(h, k, it, 𝑗[j])
        @inbounds CUDA.atomic_add!(pointer(h, hid), shared[k, it])
    end
    sync_threads()
    return nothing
end

# base approach - block built along the cols first, the rows (limit collisions)
function update_hist_gpu!(
    ::L,
    h::CuArray{T,3},
    δ𝑤::CuMatrix{T},
    X_bin::CuMatrix{UInt8},
    𝑖::CuVector{S},
    𝑗::CuVector{S}, K;
    MAX_THREADS=128) where {L <: GaussianRegression,T,S}

    nbins = size(h, 2)
    thread_i = max(nbins, min(MAX_THREADS, length(𝑖)))
    threads = (thread_i, 5)
    blocks = (8, length(𝑗))
    @cuda blocks = blocks threads = threads shmem = sizeof(T) * size(h, 1) * nbins hist_kernel_gauss!(h, δ𝑤, X_bin, 𝑖, 𝑗)
    CUDA.synchronize()
    return nothing
end

"""
    Multi-threads split_set!
        Take a view into left and right placeholders. Right ids are assigned at the end of the length of the current node set.
"""
function split_chunk_kernel!(left::CuDeviceVector{S}, right::CuDeviceVector{S}, 𝑖::CuDeviceVector{S}, X_bin, feat, cond_bin, offset, chunk_size, lefts, rights) where {S}

    it = threadIdx().x
    bid = blockIdx().x
    gdim = gridDim().x

    left_count = 0
    right_count = 0

    i = chunk_size * (bid - 1) + 1
    bid == gdim ? bsize = length(𝑖) - chunk_size * (bid - 1) : bsize = chunk_size
    i_max = i + bsize - 1

    @inbounds while i <= i_max
        @inbounds if X_bin[𝑖[i], feat] <= cond_bin
            left_count += 1
            left[offset + chunk_size * (bid - 1) + left_count] = 𝑖[i]
        else
            right_count += 1
            right[offset + chunk_size * (bid - 1) + right_count] = 𝑖[i]
        end
        i += 1
    end
    @inbounds lefts[bid] = left_count
    @inbounds rights[bid] = right_count
    sync_threads()
    return nothing
end

function split_views_kernel!(out::CuDeviceVector{S}, left::CuDeviceVector{S}, right::CuDeviceVector{S}, offset, chunk_size, lefts, rights, sum_lefts, cumsum_lefts, cumsum_rights) where {S}

    it = threadIdx().x
    bid = blockIdx().x
    gdim = gridDim().x

    # bsize = lefts[bid] + rights[bid]
    bid == 1 ? cumsum_left = 0 : cumsum_left = cumsum_lefts[bid-1]
    bid == 1 ? cumsum_right = 0 : cumsum_right = cumsum_rights[bid-1]

    iter = 1
    i_max = lefts[bid]
    @inbounds while iter <= i_max
        out[offset + cumsum_left + iter] = left[offset + chunk_size * (bid - 1) + iter]
        iter += 1
    end

    iter = 1
    i_max = rights[bid]
    @inbounds while iter <= i_max
        out[offset + sum_lefts + cumsum_right + iter] = right[offset + chunk_size * (bid - 1) + iter]
        iter += 1
    end
    sync_threads()
    return nothing
end

function split_set_threads_gpu!(out, left, right, 𝑖, X_bin, feat, cond_bin, offset)
    𝑖_size = length(𝑖)

    nblocks = ceil(Int, min(length(𝑖) / 128, 2^10))
    chunk_size = floor(Int, length(𝑖) / nblocks)

    lefts = CUDA.zeros(Int, nblocks)
    rights = CUDA.zeros(Int, nblocks)

    # threads = 1
    @cuda blocks = nblocks threads = 1 split_chunk_kernel!(left, right, 𝑖, X_bin, feat, cond_bin, offset, chunk_size, lefts, rights)
    CUDA.synchronize()

    sum_lefts = sum(lefts)
    cumsum_lefts = cumsum(lefts)
    cumsum_rights = cumsum(rights)
    @cuda blocks = nblocks threads = 1 split_views_kernel!(out, left, right, offset, chunk_size, lefts, rights, sum_lefts, cumsum_lefts, cumsum_rights)

    CUDA.synchronize()
    return (view(out, offset + 1:offset + sum_lefts), view(out, offset + sum_lefts + 1:offset + length(𝑖)))
end

"""
    update_gains!
        GradientRegression
"""
function update_gains_gpu!(
    loss::L,
    node::TrainNodeGPU{T},
    𝑗::AbstractVector{S},
    params::EvoTypes, K;
    MAX_THREADS=512) where {L<:GradientRegression,T,S}

    cumsum!(node.hL, node.h, dims=2)
    node.hR .= view(node.hL, :, params.nbins:params.nbins, :) .- node.hL
    # cumsum!(view(node.hL, :, :, :), view(node.h, :, :, :), dims=2)
    # cumsum!(view(histR, :, :, :, nid), reverse!(view(hist, :, :, :, nid), dims=2), dims=2)
    # view(node.hR, :, :, :) .= view(node.hL, :, params.nbins:params.nbins, :) .- view(node.hL, :, :, :)

    threads = min(params.nbins, MAX_THREADS)
    blocks = length(𝑗)
    @cuda blocks = blocks threads = threads hist_gains_gpu_kernel!(node.gains, node.hL, node.hR, 𝑗, params.nbins, params.λ, params.min_weight)
    # hist_gains_gpu!(loss, node.gains, node.hL, node.hR, 𝑗, params.nbins, params.λ)
    CUDA.synchronize()
    return nothing
end

function hist_gains_gpu_kernel!(gains::CuDeviceMatrix{T}, hL::CuDeviceArray{T,3}, hR::CuDeviceArray{T,3}, 𝑗::CuDeviceVector{S}, nbins, λ::T, min_𝑤::T) where {T,S}

    i = threadIdx().x
    @inbounds j = 𝑗[blockIdx().x]

    if i == nbins
        gains[i, j] = hL[1, i, j]^2 / (hL[2, i, j] + λ * hL[3, i, j]) / 2
    elseif hL[3, i, j] > min_𝑤 && hR[3, i, j] > min_𝑤
        gains[i, j] = (hL[1, i, j]^2 / (hL[2, i, j] + λ * hL[3, i, j]) +
        hR[1, i, j]^2 / (hR[2, i, j,] + λ * hR[3, i, j])) / 2
    end
    sync_threads()
    return nothing
end

"""
    update_gains!
        GaussianRegression
"""
function update_gains_gpu!(
    loss::L,
    node::TrainNodeGPU{T},
    𝑗::AbstractVector{S},
    params::EvoTypes, K;
    MAX_THREADS=512) where {L<:GaussianRegression,T,S}

    cumsum!(node.hL, node.h, dims=2)
    node.hR .= view(node.hL, :, params.nbins:params.nbins, :) .- node.hL

    thread_i = min(params.nbins, MAX_THREADS)
    threads = thread_i
    blocks = length(𝑗)
    @cuda blocks = blocks threads = threads hist_gains_gpu_kernel_gauss!(node.gains, node.hL, node.hR, 𝑗, params.nbins, params.λ, params.min_weight)
    # hist_gains_gpu!(loss, node.gains, node.hL, node.hR, 𝑗, params.nbins, params.λ)
    CUDA.synchronize()
    return nothing
end

function hist_gains_gpu_kernel_gauss!(gains::CuDeviceMatrix{T}, hL::CuDeviceArray{T,3}, hR::CuDeviceArray{T,3}, 𝑗::CuDeviceVector{S}, nbins, λ::T, min_𝑤::T) where {T,S}

    i = threadIdx().x
    @inbounds j = 𝑗[blockIdx().x]

    if i == nbins
        gains[i, j] = (hL[1, i, j]^2 / (hL[3, i, j] + λ * hL[5, i, j]) + hL[2, i, j]^2 / (hL[4, i, j] + λ * hL[5, i, j])) / 2
    elseif hL[5, i, j] > min_𝑤 && hR[5, i, j] > min_𝑤
        gains[i, j] = (hL[1, i, j]^2 / (hL[3, i, j] + λ * hL[5, i, j]) +
        hR[1, i, j]^2 / (hR[3, i, j,] + λ * hR[5, i, j])) / 2 +
        (hL[2, i, j]^2 / (hL[4, i, j] + λ * hL[5, i, j]) +
        hR[2, i, j]^2 / (hR[4, i, j,] + λ * hR[5, i, j])) / 2
    end
    sync_threads()
    return nothing
end


module ZmqCaesar
    using Requires
    using ZMQ
    using JSON
    using Distributions, IncrementalInference, DistributedFactorGraphs, Caesar
    using Unmarshal
    using Dates
    using DocStringExtensions

    # import ZMQ: ls

    include("models/distributions.jl")
    include("models/factors.jl")
    include("models/config.jl")
    include("models/session.jl")
    include("models/plotting.jl")
    include("models/additional.jl")
    include("models/ZmqServer.jl")
    include("services/factors/distributionSerialization.jl")
    include("services/factors/factorSerialization.jl")
    include("services/config.jl")
    include("services/session.jl")
    include("services/additional.jl")
    include("services/ZmqServer.jl")

    # conditional loading for RoMPlotting
    function __init__()
        @require RoMEPlotting="238d586b-a4bf-555c-9891-eda6fc5e55a2" include("services/plotting.jl")
    end
end


function create_generator(typ::Int=CURAND_RNG_PSEUDO_DEFAULT)
    ptr = Ref{curandGenerator_t}()
    @check ccall((:curandCreateGenerator, libcurand),
                 curandStatus_t,
                 (Ptr{curandGenerator_t}, Cint), ptr, typ)
    r = RNG(ptr[], typ)
    finalizer(destroy_generator, r)
    return r
end

function destroy_generator(rng::RNG)
    @check ccall((:curandDestroyGenerator, libcurand),
                 curandStatus_t,
                 (curandGenerator_t,), rng)
end

function get_version()
    ver = Ref{Cint}()
    @check ccall((:curandGetVersion, libcurand),
                 curandStatus_t,
                 (Ref{Cint},), ver)
    return ver[]
end

# TODO: curandSetStream

function set_pseudo_random_generator_seed(rng::RNG, seed::Int64)
    @check ccall((:curandSetPseudoRandomGeneratorSeed, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Clonglong), rng, seed)
end

function set_generator_offset(rng::RNG, offset::Int64)
    @check ccall((:curandSetGeneratorOffset, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Clonglong), rng, offset)
end

function set_generator_ordering(rng::RNG, order::Int)
    @check ccall((:curandSetGeneratorOrdering, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Cint), rng, order)
end

function set_quasi_random_generator_dimensions(rng::RNG, num_dimensions::UInt)
    @check ccall((:curandSetQuasiRandomGeneratorDimensions, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Cuint),
                 rng, num_dimensions)
end

"""
Generate 64-bit quasirandom numbers.
"""
function generate(rng::RNG, arr::CuArray, n::UInt)
    @check ccall((:curandGenerate, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Ptr{UInt32}, Csize_t),
                 rng, arr, length(arr))
    return arr
end

"""
Generate uniformly distributed floats.

Valid RNG types are:
 - CURAND_RNG_QUASI_SOBOL64
 - CURAND_RNG_QUASI_SCRAMBLED_SOBOL64
"""
function generate_long_long(rng::RNG, arr::CuArray)
    @check ccall((:curandGenerateLongLong, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Ptr{Culonglong}, Csize_t),
                 rng, arr, length(arr))
    return arr
end

# uniform
function generate_uniform(rng::RNG, arr::CuArray)
    @check ccall((:curandGenerateUniform, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Ptr{Float32}, Csize_t),
                 rng, arr, length(arr))
    return arr
end

function generate_uniform_double(rng::RNG, arr::CuArray)
    @check ccall((:curandGenerateUniformDouble, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Ptr{Float64}, Csize_t),
                 rng, arr, length(arr))
    return arr
end

# normal
function generate_normal(rng::RNG, arr::CuArray, mean, stddev)
    @check ccall((:curandGenerateNormal, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Ptr{Cfloat}, Csize_t, Cfloat, Cfloat),
                 rng, arr, length(arr), mean, stddev)
    return arr
end

function generate_normal_double(rng::RNG, arr::CuArray, mean, stddev)
    @check ccall((:curandGenerateNormalDouble, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Ptr{Cdouble}, Csize_t, Cdouble, Cdouble),
                 rng, arr, length(arr), mean, stddev)
    return arr
end

# lognormal
function generate_log_normal(rng::RNG, arr::CuArray, mean, stddev)
    @check ccall((:curandGenerateLogNormal, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Ptr{Cfloat}, Csize_t, Cfloat, Cfloat),
                 rng, arr, length(arr), mean, stddev)
    return arr
end

function generate_log_normal_double(rng::RNG, arr::CuArray, mean, stddev)
    @check ccall((:curandGenerateLogNormalDouble, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Ptr{Cdouble}, Csize_t, Cdouble, Cdouble),
                 rng, arr, length(arr), mean, stddev)
    return arr
end

# Poisson
"""Construct the histogram array for a Poisson distribution."""
function create_poisson_distribtion(lambda)
    ptr = Ref{curandDiscreteDistribution_t}()
    @check ccall((:curandCreatePoissonDistribution, libcurand),
                 curandStatus_t,
                 (Cdouble, Ptr{curandDiscreteDistribution_t}),
                 lambda, ptr)
    dist = DiscreteDistribution(ptr[])
    finalizer(destroy_distribution, dist)
    return dist
end

"""Destroy the histogram array for a discrete distribution (e.g. Poisson)."""
function destroy_distribution(dist::DiscreteDistribution)
    @check ccall((:curandDestroyDistribution, libcurand),
                 curandStatus_t,
                 (curandDiscreteDistribution_t,),
                 dist)
end

"""Generate Poisson-distributed unsigned ints."""
function generate_poisson(rng::RNG, arr::CuArray, lambda)
    @check ccall((:curandGeneratePoisson, libcurand),
                 curandStatus_t,
                 (curandGenerator_t, Ptr{Cuint}, Csize_t, Cdouble),
                 rng, arr, length(arr), lambda)
    return arr
end

# seeds
"""Generate the starting state of the generator. """
function generate_seeds(rng::RNG)
    @check ccall((:curandGenerateSeeds, libcurand),
                 curandStatus_t,
                 (curandGenerator_t,), rng)
end

# TODO: curandGetDirectionVectors32
# TODO: curandGetScrambleConstants32
# TODO: curandGetDirectionVectors64
# TODO: curandGetScrambleConstants64


include("MeshData.jl")

function createMesh(this::Model, id::Symbol)
	if !haskey(this.meshes, id)
		mesh = MeshManager.Mesh()
		this.meshes[id]=mesh
	else
		mesh=this.meshes[id]
	end
	mesh
end

function addMesh(this::Model, typ::Symbol)
	mesh=createMesh(this, :DEFAULT)
	if MeshManager.isEmpty(mesh) update(mesh, typ) end
	upload(this)
	mesh
end

function update(mesh::Mesh, typ)
	if typ == :PLANE
		MeshManager.update(mesh, typ, MeshDataPlaneQuad)
	elseif typ == :CUBE
		MeshManager.update(mesh, typ, MeshDataCube, 5)
	elseif typ == :IPLANE
		MeshManager.update(mesh, typ, ()->createPlaneData(5,5))
	elseif typ == :ICOSPHERE
		MeshManager.update(mesh, typ, ()->createTetrahedronSphere(1))
	end
end

function prepare(this::Model, mesh::Mesh)
	vlen=length(mesh.vertices.data.values)
	uvlen=length(mesh.uvs.data.values)
	nlen=length(mesh.normals.data.values)
	ilen=length(mesh.indicies.data.values)

	if vlen>0 || uvlen>0 || nlen>0
		va=StorageManager.create(this.group, :VA, :BLOCK, :ARRAY_BLOCK)
		vao=StorageManager.create(va, :VAO, :STORAGE, :ARRAY)

		vb=StorageManager.create(this.group, :VB, :BLOCK, :BUFFER_BLOCK)
		vbo=StorageManager.create(vb, :VBO, :STORAGE, :ARRAY_BUFFER)

		if vlen>0	StorageManager.link(vbo, mesh.vertices, :VERTEX) end
		if uvlen>0 StorageManager.link(vbo, mesh.uvs, :UV) end
		if nlen>0	StorageManager.link(vbo, mesh.normals, :NORMAL)	end
		if ilen==0 setMainBuffer(this,vbo) end
	end

	if ilen>0
		ib=StorageManager.create(this.group, :IB, :BLOCK, :INDEX_BLOCK)
		ibo=StorageManager.create(ib, :IBO, :STORAGE, :ELEMENT_ARRAY_BUFFER)
		StorageManager.link(ibo, mesh.indicies, :INDEX)
		setMainBuffer(this,ibo)
	end
end


using Graphs:
    AbstractGraph,
    DiGraph,
    add_edge!,
    add_vertices!,
    nv,
    is_cyclic,
    is_connected,
    edges,
    topological_sort_by_dfs,
    src,
    dst

export Workflow, dependencies, chain, lfork, rfork, diamond, ▷, ⋲, ⋺, ⋄

const DEPENDENCIES = Dict{Job,Vector{AtomicJob}}()

struct Workflow
    graph::DiGraph{Int}
    jobs::Vector{AtomicJob}
    function Workflow(graph, jobs)
        @assert !is_cyclic(graph) "`graph` must be an acyclic graph!"
        @assert is_connected(graph) "`graph` is not connected!"
        if nv(graph) != length(jobs)
            throw(DimensionMismatch("`graph`'s size is different from `jobs`!"))
        end
        @assert unique(jobs) == jobs "at least two jobs are identical!"
        return new(graph, jobs)
    end
end
function Workflow(jobs::Job...)
    graph = DiGraph(length(jobs))
    dict = Dict(zip(jobs, 1:length(jobs)))
    for (i, job) in enumerate(jobs)
        for dependency in dependencies(job)
            j = dict[dependency]
            add_edge!(graph, j, i)
        end
    end
    order = topological_sort_by_dfs(graph)
    new = collect(jobs[order])
    graph = DiGraph(length(jobs))
    dict = Dict(zip(jobs, 1:length(jobs)))
    for (i, job) in enumerate(jobs)
        for dependency in dependencies(job)
            j = dict[dependency]
            add_edge!(graph, j, i)
        end
    end
    return Workflow(graph, new)
end

dependencies(job::Job) = get(DEPENDENCIES, job, AtomicJob[])

function chain(a::Job, b::Job)
    if a == b
        throw(ArgumentError("a job cannot have itself as a dependency!"))
    else
        if haskey(DEPENDENCIES, b)
            if a ∉ DEPENDENCIES[b]  # Duplicate running will push the same job multiple times
                push!(DEPENDENCIES[b], a)
            end
        else
            push!(DEPENDENCIES, b => [a])  # Initialization
        end
        return b
    end
end
function chain(xs::AbstractVector{<:Job}, ys::AbstractVector{<:Job})
    for (x, y) in zip(xs, ys)
        x ▷ y
    end
    return ys
end
const ▷ = chain

function lfork(x::Job, ys::AbstractVector{<:Job})
    if x in ys
        throw(ArgumentError("a job cannot have itself as a dependency!"))
    else
        for y in ys
            x ▷ y
        end
    end
    return ys
end
const ⋲ = lfork

function rfork(xs::AbstractVector{<:Job}, y::Job)
    if y in xs
        throw(ArgumentError("a job cannot have itself as a dependency!"))
    else
        for x in xs
            x ▷ y
        end
    end
    return y
end
const ⋺ = rfork

diamond(x::Job, ys::AbstractVector{<:Job}, z::Job) = (x ⋲ ys) ⋺ z
const ⋄ = diamond

function run!(w::Workflow; nap_job = 3, attempts = 5, nap = 3)
    @assert isinteger(attempts) && attempts >= 1
    for _ in 1:attempts
        inner_run!(w; nap_job = nap_job)
        if any(!issucceeded(job) for job in w.jobs)
            inner_run!(w; nap_job = nap_job)
            all(issucceeded(job) for job in w.jobs) ? break : sleep(nap)
        end
    end
    return w
end
function inner_run!(w::Workflow; nap_job)
    for job in w.jobs  # The nodes have been topologically sorted.
        if !issucceeded(job)
            if isrunning(job)
                wait(job)
                sleep(nap_job)
            else
                run!(job; attempts = 1)
                wait(job)
                sleep(nap_job)
            end
        end
    end
    return w
end

function initialize!()
    empty!(DEPENDENCIES)
    empty!(JOB_REGISTRY)
    return
end
function initialize!(w::Workflow)
    for node in w.jobs
        initialize!(node)
    end
    return w
end

function Base.show(io::IO, w::Workflow)
    if get(io, :compact, false) || get(io, :typeinfo, nothing) == typeof(w)
        Base.show_default(IOContext(io, :limit => true), w)  # From https://github.com/mauro3/Parameters.jl/blob/ecbf8df/src/Parameters.jl#L556
    else
        println(io, summary(w))
        println(io, " ", w.graph)
        println(io, "jobs:")
        for (i, job) in enumerate(w.jobs)
            println(io, " (", i, ") ", "id: ", job.id)
            if !isempty(job.desc)
                print(io, ' '^5, "description: ")
                show(io, job.desc)
                println(io)
            end
            print(io, ' '^5, "def: ")
            show(io, job.def)
            print(io, '\n', ' '^5, "status: ")
            printstyled(io, getstatus(job); bold = true)
            if !ispending(job)
                println(
                    io,
                    '\n',
                    ' '^5,
                    "from: ",
                    format(starttime(job), "dd-u-YYYY HH:MM:SS.s"),
                )
                print(io, ' '^5, "to: ")
                if isrunning(job)
                    print(io, "still running...")
                else
                    println(io, format(stoptime(job), "dd-u-YYYY HH:MM:SS.s"))
                    print(io, ' '^5, "uses: ", elapsed(job))
                end
            end
            println(io)
        end
    end
end


function deserialize_tensor_summary(summary)
    metadata    = summary.metadata
    plugin_data = metadata.plugin_data

    plugin_name = plugin_data.plugin_name

    dim_szs, dim_nms = deserialize_tensor_shape(summary.tensor)

    if plugin_name == "text"
        val = deserialize_text_tensor(summary, dim_szs, dim_nms)
    end
    return val
end

function deserialize_text_tensor(summary, sizes, names)
    version_data = readproto(PipeBuffer(summary.metadata.plugin_data.content),
                             TextPluginData())
    tensor = summary.tensor
    strings = Array{String, length(sizes)}(undef, sizes...)

    # Ignore dimension names
    if version_data.version == 0
        for (i, in) = enumerate(tensor.string_val)
            #TODO should undo `markdown_repr`, because all strings
            # will be displayed with a starting and ending \"
            strings[i] = String(in)
            delimiter = '\"'
            if delimiter === first(strings[i]) === last(strings[i])
                strings[i] = strings[i][2:end-1]
            end
        end
    else
        @warn "Unknown string version $(version_data.version)"
    end

    return strings
end

function deserialize_tensor_shape(tensor)
    t_shape = tensor.tensor_shape

    dim_names = Union{String,Nothing}[]
    dim_sz    = Int[]
    for dim = t_shape.dim
        push!(dim_sz, dim.size)
        isempty(dim.name) ? push!(dim_names, nothing) : push!(dim_names, dim.name)
    end

    return tuple(dim_sz...), tuple(dim_names...)
end


module DLLBFGS

using JSOSolvers, NLPModels, SolverTools, LinearOperators

using LinearAlgebra, Logging

include("solver.jl")
end # module


using ContinuumWorld
using POMDPs
using POMDPModelTools
using POMDPModels
using POMDPSimulators
using Test
using Plots
using Random

w = CWorld()

@inferred @gen(:sp, :r)(w, [3,2], [4,2], MersenneTwister(19))

sol = CWorldSolver(rng=MersenneTwister(7))
pol = solve(sol, w)

sim = HistoryRecorder(rng=MersenneTwister(5), max_steps=30)
@show state_hist(simulate(sim, w, pol))

#=
write_file(CWorldVis(w, f=norm), joinpath(tempdir(), "test.png"))
@time write_file(CWorldVis(w, f=norm, g=sol.grid), joinpath(tempdir(), "test_timed.png"))
@time write_file(CWorldVis(w, f=norm), joinpath(tempdir(), "test_timed.tif"))
@time write_file(CWorldVis(w, f=norm, g=sol.grid), joinpath(tempdir(), "test_timed.tif"))
@time write_file(CWorldVis(w, f=s->action_ind(pol, s), g=sol.grid, title="Policy"), joinpath(tempdir(), "policy.png"))
@time write_file(CWorldVis(w, f=s->value(pol, s), g=sol.grid, title="Value"), joinpath(tempdir(), "value.png"))
=#
plot(CWorldVis(w, f=s->value(pol, s), g=sol.grid, title="Value"))


#####
##### GameInterface Tests
#####

# Generate a set of reachable states randomly
function generate_states(gspec, n)
  traces = [play_game(gspec, RandomPlayer()) for i in 1:n]
  return Set(s for t in traces for s in t.states)
end

same_state(game1, game2) = GI.current_state(game1) == GI.current_state(game2)

# Test that the same (symmetric) actions are still available in a symmetric state
# function test_symmetry(gspec, state, (symstate, aperm))
#   mask = GI.actions_mask(GI.init(gspec, state))
#   symmask = GI.actions_mask(GI.init(gspec, symstate))
#   v = falses(length(symmask))
#   v[mask] .= true
#   v = v[aperm]
#   return all(v[symmask]) && !any(v[.~symmask])
# end

function test_symmetry(gspec, state, (symstate, aperm))
  mask = GI.actions_mask(GI.init(gspec, state))
  symmask = GI.actions_mask(GI.init(gspec, symstate))
  return symmask == mask[aperm]
end

"""

    test_game(::AbstractGameSpec)
    test_game(experiment)

Perform some sanity checks regarding the compliance of a game with the
AlphaZero.jl [Game Interface](@ref game_interface).
"""
function test_game(gspec::AbstractGameSpec; n=100)
  @assert isa(GI.two_players(gspec), Bool)

  # Testing initial states
  if all(same_state(GI.init(gspec), GI.init(gspec)) for _ in 1:n)
    @info "There appears to be a unique initial state."
  else
    @info "There appears to be a nontrivial distribution of initial states."
  end
  @assert all(GI.white_playing(GI.init(gspec)) for _ in 1:n)

  # Save the game static properties
  State = GI.state_type(gspec)
  state_dim = GI.state_dim(gspec)
  Action = GI.action_type(gspec)
  actions = GI.actions(gspec)
  @assert isa(actions, AbstractVector{Action})
  @assert !isempty(actions)
  num_actions = GI.num_actions(gspec)
  two_players = GI.two_players(gspec)

  # Check properties on random states
  for state in generate_states(gspec, n)
    game = GI.init(gspec, state)
    @assert GI.current_state(game) == state
    GI.set_state!(game, state)
    @assert GI.current_state(game) == state
    @assert isa(GI.current_state(game), State)
    @assert same_state(GI.clone(game), GI.init(gspec, state))

    @assert isa(GI.white_playing(game), Bool)
    @assert GI.white_playing(game) || two_players

    @assert isa(GI.game_terminated(game), Bool)
    @assert GI.game_terminated(game) || !isempty(GI.available_actions(game))

    @assert eltype(GI.actions_mask(game)) == Bool
    @assert length(GI.actions_mask(game)) == num_actions

    vect = GI.vectorize_state(gspec, state)
    @assert isa(vect, Array{Float32})
    @assert size(vect) == state_dim

    # Playing a move
    if !GI.game_terminated(game)
      @assert isa(GI.heuristic_value(game), Float64)
      state_copy = deepcopy(state)
      a = rand(GI.available_actions(game))
      @assert isa(GI.action_string(gspec, a), String)
      GI.play!(game, a)
      @assert state_copy == state "States must appear as persistent."
      @assert isa(GI.white_reward(game), Float64)
    end

    # Testing symmetries
    syms = GI.symmetries(gspec, state)
    @assert isa(syms, Vector)
    for sym in syms
      s, σ = sym
      @assert isa(s, State)
      @assert isa(σ, Vector{Int})
      @assert length(σ) == num_actions
      @assert test_symmetry(gspec, state, sym)
    end

    # Verifying that static properties are state invariant
    @assert GI.num_actions(GI.spec(game)) == num_actions
    @assert GI.actions(GI.spec(game)) == actions
    @assert GI.state_type(GI.spec(game)) == State
    @assert GI.action_type(GI.spec(game)) == Action
    @assert GI.two_players(GI.spec(game)) == two_players
    @assert GI.state_dim(GI.spec(game)) == state_dim
  end
end


#!/usr/bin/env julia

for c in ARGS[1] print(c == '\n' ? c : 'z' - (c - 'a')) end


using Dates
using JuliaDB

path = "./test/Sample data/PatientOverview_test.xlsx"
sheet = "Sheet1"
startdate = Date("2019-01-01")
enddate = Date("2019-02-01")
mastercalendar = HP.MasterCalendar(startdate,enddate)

columns = Dict(:Consultation => "Consultation" , :Telefon =>"Telephone" , :TTE => "TTE", :AEKG => "AEKG",  :MR => "MR", :Holter=>"Holter")
patients, visits = HP.readPatientTable(path,sheet,columns,mastercalendar)

path_resourceOverview = "./test/Sample data/GUCHamb_Timeslot_test.xlsx"
sheet_amb = "GUCH AMB"
sheet_resources = "External"
GUCHAmb_resources,timeslots,workpattern = Hospitalplanning.readWorkPattern(path_resourceOverview,sheet_amb)
external_resources = Hospitalplanning.readWorkPattern!(GUCHAmb_resources,timeslots,workpattern,path_resourceOverview,sheet_resources,)
GUCHAmb_resources[1]
path_TimeDelta = "./test/Sample data/GUCHamb_timeDelta.csv"
timeDelta = HP.loadTimeDelta(path_TimeDelta)
HP.generateCalendarFromPattern!(timeslots,GUCHAmb_resources,workpattern,mastercalendar,0.5)
timeslots2 = ndsparse(timeslots)

testpath_resources = "./test/Sample data/resourcetest_output.csv"
HP.save(GUCHAmb_resources,testpath_resources)
resources_read = HP.loadresources(testpath_resources)
@test GUCHAmb_resources == resources_read

testpath_patient = "./test/Sample data/patienttest_output.csv"
HP.save(patients,testpath_patient)
patients_read = HP.loadpatients(testpath_patient)
@test patients == patients_read

testpath_visits = "./test/Sample data/visittest_output.csv"
HP.save(visits,testpath_visits)
visits_read = HP.loadvisits(testpath_visits)
@test visits == visits_read

testpath_timeslots = "./test/Sample data/timeslottest_output.csv"
HP.save(timeslots,testpath_timeslots)
timeslots_read = HP.loadtimeslots(testpath_timeslots)
@test timeslots == timeslots_read
timeslots2_read = ndsparse(timeslots_read)
@test timeslots2 == timeslots2_read

testpath_solution_input = "./test/Sample data/solutiontest_input.csv"
testpath_solution_output = "./test/Sample data/solutiontest_output.csv"
solution_read = HP.loadsolution(testpath_solution_input)

HP.save(solution_read,testpath_solution_output)
solution_read2 = HP.loadsolution(testpath_solution_output)

@test solution_read2 == solution_read


using Pkg
Pkg.add("DataFrames")
Pkg.add("CSV")

using DataFrames
using CSV

savepath = "../data/sushi3-2016/train.csv"

# Load datasets
df_idata = CSV.read("../data/sushi3-2016/sushi3.idata", header=false, delim='\t')
df_udata = CSV.read("../data/sushi3-2016/sushi3.udata", header=false, delim='\t')
df_score = CSV.read("../data/sushi3-2016/sushi3b.5000.10.score", header=false, delim=' ')

df_train = DataFrame(user_id = df_udata[1])
for j in 1:length(df_udata[1])
    df = copy(df_idata)
    df[:user_id] = df_udata[1][j]
    df = hcat(df,convert(DataFrame,convert(Array,df_score[j, :])'))

    global df_train
    df = df[df[:x1].>-1,:]
    if j == 1
        df_train = join(df_train,df,on=:user_id)
    else
        df_train = vcat(df_train,df)
    end
end
delete!(df_train,:Column2)
rename!(df_train,:x1 => :score)

rename!(df_udata,:Column1 => :user_id)
df_train = join(df_udata,df_train,on=:user_id)

df_train |> CSV.write(savepath,delim=' ',writeheader=false)


import Yao: gatecount

function gatecount!(c::Union{PauliString, Bag, Sequence}, storage::AbstractDict)
    (gatecount!.(c |> subblocks, Ref(storage)); storage)
end

function gatecount!(c::ConditionBlock, storage::AbstractDict)
    k = typeof(c)
    if haskey(storage, k)
        storage[k] += 1
    else
        storage[k] = 1
    end
    storage
end

gatecount!(c::TrivialGate, storage::AbstractDict) = storage


using Distributed
using Dates
using RoME
using RoMEPlotting
using Gadfly
@everywhere using RoME, RoMEPlotting, Gadfly

total_meas = parse(Int, ARGS[1])

# Let's load the Manhattan scenario using the g2o file.
file = (normpath(Base.find_package("RoME"), "../..", "examples", "manhattan_incremental.g2o"))
global instructions = importG2o(file)

# Make sure plots look a bit nicer.
latex_fonts = Theme(major_label_font="CMU Serif", major_label_font_size=16pt,
                    minor_label_font="CMU Serif", minor_label_font_size=14pt,
                    key_title_font="CMU Serif", key_title_font_size=12pt,
                    key_label_font="CMU Serif", key_label_font_size=10pt)
Gadfly.push_theme(latex_fonts)

function solve_batch(total_meas::Integer)
    # Choose where to save the data.
    manhattan_total_meas = total_meas
    data_logpath = ENV["HOME"]*"/Documents/wafr/manhattan-batch-$(total_meas)-$(now())"

    # Create initial factor graph with specified logging path.
    fg = LightDFG{SolverParams}(solverParams=SolverParams(logpath=data_logpath))

    # Add initial variable with a prior measurement to anchor the graph.
    addVariable!(fg, :x0, Pose2)
    initial_pose = MvNormal([0.0; 0.0; 0.0], Matrix(Diagonal([0.1;0.1;0.05].^2)))
    addFactor!(fg, [:x0], PriorPose2(initial_pose))

    # Add all variables and measurements.
    # manhattan_total_meas = 5453
    for i in 1:manhattan_total_meas
        parseG2oInstruction!(fg, instructions[i])
    end

    # Solve the graph, and save a copy of the tree.
    saveDFG(fg, "$(getLogPath(fg))/fg-before-solve")
    getSolverParams(fg).maxincidence =
    tree, smt, hist = solveTree!(fg)
    saveDFG(fg, "$(getLogPath(fg))/fg-after-solve")
    saveTree(tree, "$(getLogPath(fg))/tree$(manhattan_total_meas).jld2")
    drawTree(tree, show=false, filepath="$(getLogPath(fg))/bt.pdf")

    # Just store some quick plots.
    pl1 = drawPoses(fg, spscale=0.6, lbls=false)
    Gadfly.draw(PDF("$(getLogPath(fg))/poses$(manhattan_total_meas).pdf", 20cm, 10cm), pl1)

    # Run the garbage collector.
    GC.gc()
end

solve_batch(total_meas)


function add_drag(w, range, point_id, slider_length, slideridx_s)
    m2id = mouse2id(w)
    # interaction
    @materialize mouse_buttons_pressed, mouseposition = w.inputs
    isoverpoint = const_lift(is_same_id, m2id, point_id)
    # single left mousekey pressed (while no other mouse key is pressed)
    key_pressed = const_lift(GLAbstraction.singlepressed, mouse_buttons_pressed, GLFW.MOUSE_BUTTON_LEFT)
    # dragg while key_pressed. Drag only starts if isoverpoint is true
    mousedragg = GLAbstraction.dragged(mouseposition, key_pressed, isoverpoint)
    preserve(foldp(value(slideridx_s), mousedragg) do v0, dragg
        if dragg != Vec2f0(0)
            idx_steps = round(Int, (dragg[1]/slider_length)*length(range))
            new_idx = clamp(v0 + idx_steps, 1, length(range))
            push!(slideridx_s, new_idx)
            return v0
        else # dragging started
            return value(slideridx_s)
        end
    end)
    slideridx_s
end
function add_play(slideridx_s, play_signal, range, rate=30.0)
    play_s = fpswhen(play_signal, rate)
    preserve(map(play_s, init=nothing) do t
        push!(slideridx_s, mod1(value(slideridx_s)+1, length(range)))
        nothing
    end)
end
function slider(
        range, window;
        startidx::Int=1,
        play_signal=Signal(false), slider_length=50mm
    )
    startpos = 2.1mm
    height = value(icon_size)
    point_id = Signal((0,0))
    slideridx_s = Signal(startidx)
    slider_s = map(slideridx_s) do idx
        range[clamp(idx, 1, length(range))]
    end
    add_drag(window, range, point_id, slider_length, slideridx_s)
    add_play(slideridx_s, play_signal, range)
    bb = map(icon_size) do is
        AABB(Vec3f0(0), Vec3f0(slider_length, is, 1))
    end
    line_pos = map(icon_size) do is
        Point2f0[(startpos, is/2), (slider_length, is/2)]
    end
    line = visualize(
        line_pos, :linesegment,
        boundingbox=bb, thickness=1.5mm
    ).children[]
    i = Signal(0)
    pos = Point2f0[(0, 0)]
    position = map(slideridx_s) do idx
        x = ((idx-1)/length(range-1))*slider_length
        pos[1] = (x, 0)
        pos
    end
    knob_scale = map(is->Vec2f0(is/3), icon_size)
    offset = map(line_pos, icon_size) do lp, is
        p = first(lp)
        Vec2f0(p - (is/6)) # - minus half knob scale
    end
    point_robj = visualize(
        (Circle, position),
        scale_primitive=true,
        offset=offset, scale=knob_scale,
        boundingbox=bb
    ).children[]
    push!(point_id, (point_robj.id, line.id))

    slider_s, Context(point_robj, line)
end

function maxdigits(range)
    if eltype(range) <: AbstractFloat
        return 7
    else
        return max(ndigits(first(range)), ndigits(last(range)))
    end
end
function play_widget(
        range, window=GLPlot.widget_screen!();
        startidx::Int=1
    )
    glyph_scale = GLVisualize.glyph_scale!('X')
    numberbox = map(icon_size) do is
        AABB(Vec3f0(0,-1mm, 0), Vec3f0(2is, is, 1))
    end
    target_s = value(icon_size) * 0.4
    scale = (target_s ./ glyph_scale)
    sliderlen = 70mm-(3*value(icon_size))-2mm
    play_button, play_stop_signal = GLVisualize.toggle_button(
        rot180(GLPlot.imload("play.png")), GLPlot.imload("break.png"), window
    )
    play_s = map(!, play_stop_signal)
    slider_s, slider_w = slider(range, window,
        startidx=startidx, play_signal=play_s,
        slider_length=sliderlen
    )
    number = visualize(
        map(GLVisualize.printforslider, slider_s),
        color=RGBA{Float32}(0.6, 0.6, 0.6,1),
        boundingbox=numberbox,
        relative_scale=scale
    )
    GLPlot.add_widget!(play_button, number, slider_w, window=window)
    slider_s
end
export play_widget
export add_widget!
function add_widget!(widgets...;
        delete=Signal(false),
        window=widget_screen!(delete=delete),
        height=value(icon_size)
    )
    scalings = map(widgets) do widget
        bb = value(boundingbox(widget))
        w = widths(bb)
        if w[2] > height # only scale when to big
            s = height/w[2]
            w.*s, minimum(bb)
        else
            w, minimum(bb)
        end
    end
    last_x = 0f0
    foreach(zip(scalings, widgets)) do scaling_widget
        scaling, widget = scaling_widget
        scale, offset = scaling
        xwidth = scale[1]-1mm
        place = SimpleRectangle{Float32}(last_x-offset[1]+0.5mm, -offset[2]+0.5mm, xwidth, scale[2]-1mm)
        layout!(place, widget)
        _view(widget, window, camera=:fixed_pixel)
        last_x += xwidth
    end
end

function item_area(la, deleted, item_height)
    y = la.y-item_height-2
    deleted && return SimpleRectangle(la.x, la.y, la.w, 0)
    return SimpleRectangle(la.x, y, la.w, item_height)
end

function widget_screen!(parentscreen=edit_screen; left_gap=1.5mm, delete=Signal(false))
    scroll = parentscreen.inputs[:menu_scroll]
    if isempty(parentscreen.children)
        last_area = map(parentscreen.area, icon_size, scroll) do a, ih, s
            return SimpleRectangle{Int}(left_gap, a.h-ih+s, a.w-left_gap, ih)
        end
    else
        last_area = last(parentscreen.children).area
    end
    itemarea = map(GLPlot.item_area, last_area, delete, icon_size)
    Screen(parentscreen, area=itemarea)
end

function choices(x::Vector)
    signal, vis = GLVisualize.choice_widget(x, edit_screen, area=(48mm, 8mm))
    w = widget_screen!()
    _view(vis, w, camera=:fixed_pixel)
    signal
end

function edit_toggle()
    edit_button, no_edit_signal = toggle_button(
        imload("play.png"), rotr90(imload("play.png")), edit_screen
    )
end

function visible_toggle()
    toggle_button(
        imload("showing.png"), imload("notshowing.png"), edit_screen
    )
end
function delete_toggle()
    delete_button, del_signal = button(
        imload("delete.png"), edit_screen
    )
end


using Adapt
using Statistics
using Oceananigans.Grids
using Oceananigans.Grids: interior_parent_indices

struct AveragedField{X, Y, Z, S, A, D, G, T, N, O} <: AbstractReducedField{X, Y, Z, A, G, T, N}
            data :: D
    architecture :: A
            grid :: G
            dims :: NTuple{N, Int}
         operand :: O
          status :: S

    function AveragedField{X, Y, Z}(data::D, arch::A, grid::G, dims, operand::O;
                                    recompute_safely=true) where {X, Y, Z, D, A, G, O}

        dims = validate_reduced_dims(dims)
        validate_reduced_locations(X, Y, Z, dims)
        validate_field_data(X, Y, Z, data, grid)

        status = recompute_safely ? nothing : FieldStatus(0.0)

        S = typeof(status)
        N = length(dims)
        T = eltype(grid)

        return new{X, Y, Z, S, A, D, G, T, N, O}(data, arch, grid, dims, operand, status)
    end

    function AveragedField{X, Y, Z}(data::D, arch::A, grid::G, dims, operand::O, status::S) where {X, Y, Z, D, A, G, O, S}
        return new{X, Y, Z, S, A, D, G, eltype(grid), length(dims), O}(data, arch, grid, dims, operand, status)
    end
end

"""
    AveragedField(operand::AbstractField; dims, data=nothing, recompute_safely=false)

Returns an AveragedField.
"""
function AveragedField(operand::AbstractField; dims, data=nothing, recompute_safely=true)

    arch = architecture(operand)
    loc = reduced_location(location(operand), dims=dims)
    grid = operand.grid

    if isnothing(data)
        data = new_data(arch, grid, loc)
        recompute_safely = false
    end

    return AveragedField{loc[1], loc[2], loc[3]}(data, arch, grid, dims, operand,
                                                 recompute_safely=recompute_safely)
end

"""
    compute!(avg::AveragedField, time=nothing)

Compute the average of `avg.operand` and store the result in `avg.data`.
"""
function compute!(avg::AveragedField, time=nothing)
    compute_at!(avg.operand, time)
    avg .= 0 # should we have to do this?
    mean!(avg, avg.operand)
    return nothing
end

compute_at!(avg::AveragedField{X, Y, Z, <:FieldStatus}, time) where {X, Y, Z} =
    conditional_compute!(avg, time)

#####
##### Adapt
#####

Adapt.adapt_structure(to, averaged_field::AveragedField{X, Y, Z}) where {X, Y, Z} =
    AveragedField{X, Y, Z}(Adapt.adapt(to, averaged_field.data), nothing,
                           nothing, averaged_field.dims, nothing, nothing)


################################################################################
# NodeMap data structure.
################################################################################
# Keeps track of where nodes in the taskgraph are mapped to the architecture
# as well as any applicable metadata.
const PPLC = Union{Path{Link},Path{Port},Path{Component}}

"""
Record of how [`Taskgraph`](@ref)s and [`TaskgraphEdge`](@ref)s in a
[`Taskgraph`](@ref) map to a [`TopLevel`](@ref).
"""
mutable struct Mapping
    """
    `Dict{String, Path{Component}}` - Takes a node name and returns the path
    to the [`Component`](@ref) where that node is mapped.
    """
    nodes::Dict{String,Path{Component}}

    """
    `Vector{SparseDiGraph{Union{Path{Link},Path{Port},Path{Component}}}}` -
    Takes a integer index for an edge in the parent [`Taskgraph`](@ref) and
    returns a graph whose node types `Path`s to architectural compoennts.o

    Edge connectivity in the graph describes how the [`TaskgraphEdge`](@ref) is
    routed through the [`TopLevel`](@ref)
    """
    edges::Vector{SparseDiGraph{PPLC}}
end

getpath(m::Mapping, nodename::String) = m.nodes[nodename]
getpath(m::Mapping, i::Integer) = m.edges[i]
Base.setindex!(m::Mapping, v, i::Integer) = setindex!(m.edges, v, i)

"""
Top level data structure. Summary of parameters:

* `T` - The [`RuleSet`](@ref) used to control placement and routing.
* `D` - The number of dimensions in the architecture (will usually be 2 or 3).
"""
mutable struct Map{D,T<:RuleSet}
    "[`RuleSet`](@ref) for assigning `taskgraph` to `toplevel`."
    ruleset::T

    "[`TopLevel{A,D}`](@ref) - The `TopLevel` to be used for the mapping."
    toplevel::TopLevel{D}

    "The [`Taskgraph`](@ref) to map to the `toplevel`."
    taskgraph::Taskgraph
    options::Dict{Symbol,Any}

    "How `taskgraph` is mapped to `toplevel`."
    mapping::Mapping
    metadata::Dict{String,Any}
end

function Map(
    ruleset::T,
    toplevel::TopLevel{D},
    taskgraph::Taskgraph;
    options = Dict{Symbol,Any}(),
    metadata = Dict{String,Any}(),
) where {T<:RuleSet,D}

    # Create a new Mapping data type for the new map
    # Get the node names
    nodes = Dict(n => Path{Component}() for n in nodenames(taskgraph))
    edges = [SparseDiGraph{PPLC}() for i in 1:num_edges(taskgraph)]
    mapping = Mapping(nodes, edges)
    return Map(ruleset, toplevel, taskgraph, options, mapping, metadata)
end

rules(map::Map) = map.ruleset

function Base.show(io::IO, map::Map{T,D}) where {T,D}
    print(io, "Map{$T,$D} with $(num_nodes(map.taskgraph)) nodes")
    return nothing
end

################################################################################
# Methods for interacting with the Map.
################################################################################
getpath(m::Map, nodename::String) = getpath(m.mapping, nodename)
getpath(m::Map, i::Integer) = getpath(m.mapping, i)
getaddress(map::Map, nodename::String) = getaddress(map.toplevel, getpath(map, nodename))

function isused(m::Map{D}, addr::CartesianIndex{D}) where {D}
    for path in values(m.mapping.nodes)
        getaddress(m.toplevel, path) == addr && return true
    end
    return false
end

function gettask(m::Map{D}, addr::CartesianIndex{D}) where {D}
    for (taskname, path) in m.mapping.nodes
        if getaddress(m.toplevel, path) == addr
            return getnode(m.taskgraph, taskname)
        end
    end
    return nothing
end

################################################################################
# Saving/loading
################################################################################

function makejls(filepath)
    dir, file = splitdir(filepath)

    filename = "$(first(split(file, "."))).jls"
    finalpath = joinpath(dir, filename)
    return finalpath
end

function save(m::Map, filepath)
    f = open(makejls(filepath), "w")
    serialize(f, m.mapping)
    return close(f)
end

function load(m::Map, filepath)
    f = open(makejls(filepath), "r")
    mapping = deserialize(f)
    close(f)
    # bind deserialized result
    return m.mapping = mapping
end


module ReactiveMPRuleTest

using Test
using ReactiveMP
using MacroTools

import ReactiveMP: rule_macro_parse_on_tag, rule_macro_parse_fn_args, call_rule_macro_parse_fn_args

@testset "rule" begin

    @testset "rule_macro_parse_on_tag(expression)" begin
        @test rule_macro_parse_on_tag(:(:out)) == (:(Type{ Val{ :out } }), nothing, nothing)
        @test rule_macro_parse_on_tag(:(:mean)) == (:(Type{ Val{ :mean } }), nothing, nothing)
        @test rule_macro_parse_on_tag(:(:mean, k)) == (:(Tuple{ Val{ :mean }, Int }), :k, :(k = on[2]))
        @test rule_macro_parse_on_tag(:(:precision, r)) == (:(Tuple{ Val{ :precision }, Int }), :r, :(r = on[2]))

        @test_throws ErrorException rule_macro_parse_on_tag(:(out))
        @test_throws ErrorException rule_macro_parse_on_tag(:(123))
        @test_throws ErrorException rule_macro_parse_on_tag(:(:mean, 1))
        @test_throws ErrorException rule_macro_parse_on_tag(:(precision, r))
    end

    @testset "rule_macro_parse_fn_args(inputs; specname, prefix, proxy)" begin
        names, types, init = rule_macro_parse_fn_args([ (:m_out, :PointMass), (:m_mean, :NormalMeanPrecision) ]; specname = :messages, prefix = :m_, proxy = :(ReactiveMP.Message))

        @test names == :(Type{ Val{ (:out, :mean) } })
        @test types == :(Tuple{ ReactiveMP.Message{ <: PointMass }, ReactiveMP.Message{ <: NormalMeanPrecision } })
        @test init == Expr[:(m_out = getdata(messages[1])), :(m_mean = getdata(messages[2]))]

        names, types, init = rule_macro_parse_fn_args([ (:m_out, :PointMass), (:m_mean, :NormalMeanPrecision) ]; specname = :marginals, prefix = :q_, proxy = :(ReactiveMP.Marginal))

        @test names == :Nothing
        @test types == :Nothing
        @test init == Expr[]

        names, types, init = rule_macro_parse_fn_args([ (:m_out, :PointMass), (:q_mean, :NormalMeanPrecision) ]; specname = :marginals, prefix = :q_, proxy = :(ReactiveMP.Marginal))

        @test names == :(Type{ Val{ (:mean, ) } })
        @test types == :(Tuple{ ReactiveMP.Marginal{ <: NormalMeanPrecision }, })
        @test init == Expr[ :(q_mean = getdata(marginals[1])) ]
    end

    @testset "call_rule_macro_parse_fn_args(inputs; specname, prefix, proxy)" begin
        names, values = call_rule_macro_parse_fn_args([ (:m_out, :(PointMass(1.0))), (:m_mean, :(NormalMeanPrecision(0.0, 1.0))) ]; specname = :messages, prefix = :m_, proxy = :(ReactiveMP.Message))

        @test names == :(Val{ (:out, :mean) })
        @test values == :(ReactiveMP.Message(PointMass(1.0), false, false), ReactiveMP.Message(NormalMeanPrecision(0.0, 1.0), false, false))

        names, values = call_rule_macro_parse_fn_args([ (:m_out, :(PointMass(1.0))), (:m_mean, :(NormalMeanPrecision(0.0, 1.0))) ]; specname = :marginals, prefix = :q_, proxy = :(ReactiveMP.Marginal))

        @test names == :nothing
        @test values == :nothing

        names, values = call_rule_macro_parse_fn_args([ (:m_out, :(PointMass(1.0))), (:q_mean, :(NormalMeanPrecision(0.0, 1.0))) ]; specname = :marginals, prefix = :q_, proxy = :(ReactiveMP.Marginal))

        @test names == :(Val{ (:mean, ) })
        @test values == :((ReactiveMP.Marginal(NormalMeanPrecision(0.0, 1.0), false, false),))
    end

end

end


export BitFlippingEnv

"""
In Bit Flipping Environment we have n bits. The actions are 1 to n where executing i-th action flips the i-th bit of the state. For every episode we sample uniformly and initial state as well as the target state.

Refer [Hindsight Experience Replay paper](https://arxiv.org/pdf/1707.01495.pdf) for the motivation behind the environment.
"""

mutable struct BitFlippingEnv <: AbstractEnv
    N::Int
    rng::AbstractRNG
    state::BitArray{1}
    goal_state::BitArray{1}
    max_steps::Int
    t::Int
end

function BitFlippingEnv(; N = 8, T = N, rng = Random.GLOBAL_RNG)
    state = bitrand(rng, N)
    goal_state = bitrand(rng, N)
    max_steps = T
    BitFlippingEnv(N, rng, state, goal_state, max_steps, 0)
end

Random.seed!(env::BitFlippingEnv, s) = Random.seed!(env.rng, s)

RLBase.action_space(env::BitFlippingEnv) = Space(OneTo(env.N))

RLBase.legal_action_space(env::BitFlippingEnv) = Base.OneTo(env.N)

function (env::BitFlippingEnv)(action::Int)
    env.t += 1
    if 1 <= action <= env.N
        env.state[action] = !env.state[action]
        nothing
    else
        @error "Invalid Action"
    end
end

RLBase.state(env::BitFlippingEnv) = state(env::BitFlippingEnv, Observation{BitArray{1}}())
RLBase.state(env::BitFlippingEnv, ::Observation) = env.state
RLBase.state(env::BitFlippingEnv, ::GoalState) = env.goal_state
RLBase.state_space(env::BitFlippingEnv, ::Observation) = Space(Bool, env.N)
RLBase.state_space(env::BitFlippingEnv, ::GoalState) = Space(Bool, env.N)
RLBase.is_terminated(env::BitFlippingEnv) =
    (env.state == env.goal_state) || (env.t >= env.max_steps)

function RLBase.reset!(env::BitFlippingEnv)
    env.t = 0
    env.state .= bitrand(env.rng, env.N)
    env.goal_state .= bitrand(env.rng, env.N)
end

function RLBase.reward(env::BitFlippingEnv)
    if env.state == env.goal_state
        0.0
    else
        -1.0
    end
end

RLBase.NumAgentStyle(::BitFlippingEnv) = SINGLE_AGENT
RLBase.DynamicStyle(::BitFlippingEnv) = SEQUENTIAL
RLBase.ActionStyle(::BitFlippingEnv) = MINIMAL_ACTION_SET
RLBase.InformationStyle(::BitFlippingEnv) = PERFECT_INFORMATION
RLBase.StateStyle(::BitFlippingEnv) = (Observation{BitArray{1}}(), GoalState{BitArray{1}}())
RLBase.RewardStyle(::BitFlippingEnv) = STEP_REWARD
RLBase.UtilityStyle(::BitFlippingEnv) = GENERAL_SUM
RLBase.ChanceStyle(::BitFlippingEnv) = STOCHASTIC


#
# Implementation of AtomsBase interface in a struct-of-arrays style.
#
export FastSystem

struct FastSystem{D, L <: Unitful.Length, M <: Unitful.Mass} <: AbstractSystem{D}
    box::SVector{D, SVector{D, L}}
    boundary_conditions::SVector{D, BoundaryCondition}
    positions::Vector{SVector{D, L}}
    atomic_symbols::Vector{Symbol}
    atomic_numbers::Vector{Int}
    atomic_masses::Vector{M}
end

# Constructor to fetch the types
function FastSystem(box, boundary_conditions, positions, atomic_symbols, atomic_numbers, atomic_masses)
    FastSystem{length(box),eltype(eltype(positions)),eltype(atomic_masses)}(
        box, boundary_conditions, positions, atomic_symbols, atomic_numbers, atomic_masses
    )
end

# Constructor to take data from another system
function FastSystem(system::AbstractSystem)
    FastSystem(bounding_box(system), boundary_conditions(system), position(system),
               atomic_symbol(system), atomic_number(system), atomic_mass(system))
end

# Convenience constructor where we don't have to preconstruct all the static stuff...
function FastSystem(particles, box, boundary_conditions)
    D = length(box)
    if !all(length.(box) .== D)
        throw(ArgumentError("Box must have D vectors of length D=$D."))
    end
    if length(boundary_conditions) != D
        throw(ArgumentError("Boundary conditions should be of length D=$D."))
    end
    if !all(n_dimensions.(particles) .== D)
        throw(ArgumentError("Particles must have positions of length D=$D."))
    end
    FastSystem(box, boundary_conditions, position.(particles), atomic_symbol.(particles),
               atomic_number.(particles), atomic_mass.(particles))
end

function Base.show(io::IO, system::FastSystem)
    print(io, "FastSystem")
    show_system(io, system)
end

bounding_box(sys::FastSystem)        = sys.box
boundary_conditions(sys::FastSystem) = sys.boundary_conditions
Base.length(sys::FastSystem)         = length(sys.positions)
Base.size(sys::FastSystem)           = size(sys.positions)

species_type(sys::FS) where {FS <: FastSystem} = AtomView{FS}
Base.getindex(sys::FastSystem, index::Int)     = AtomView(sys, index)

position(s::FastSystem)       = s.positions
atomic_symbol(s::FastSystem)  = s.atomic_symbols
atomic_number(s::FastSystem)  = s.atomic_numbers
atomic_mass(s::FastSystem)    = s.atomic_masses
velocity(s::FastSystem)       = missing

position(s::FastSystem, i)      = s.positions[i]
atomic_symbol(s::FastSystem, i) = s.atomic_symbols[i]
atomic_number(s::FastSystem, i) = s.atomic_numbers[i]
atomic_mass(s::FastSystem, i)   = s.atomic_masses[i]
velocity(s::FastSystem, i)      = missing


using PyCall
unshift!(PyVector(pyimport("sys")["path"]), "")
@pyimport ncdata
len = length
filename = "./mcm32_nhept.nc"
filename1 = "./cri2_nhept.nc"

data = ncdata.get(filename)
specs = names!(DataFrame(data["spec"]),[Symbol(i)for i in data["sc"]])

data = ncdata.get(filename1)
specs1 = names!(DataFrame(data["spec"]),[Symbol(i)for i in data["sc"]])

using Plots

spec = "O3"
plot(specs[Symbol(spec)])
plot!(specs1[Symbol(spec)])

print("fini")


# Packages needed for parallel processing need to be loaded "twice":s
using Bio.Seq: DNASequence, DNAKmer, canonical, FASTASeqRecord, FASTAReader, each, neighbors
@everywhere using Bio.Seq: DNASequence, DNAKmer, canonical, FASTASeqRecord, FASTAReader, each, neighbors
using DataStructures: DefaultDict
@everywhere using DataStructures: DefaultDict
using FastaIO
@everywhere using FastaIO

@everywhere function twin{k}(::Type{DNAKmer{k}}, km)
    DNAKmer{k}(Bio.Seq.reverse_complement(DNASequence("$km")))
end

function contig_to_string(c)
  return "$(c[1])" * join(["$(x[end])" for x in c[2:end]])
end

@everywhere function get_contig{k}(::Type{DNAKmer{k}}, kmers, km)
  contig_fw = get_contig_forward(DNAKmer{k}, kmers, km)
  contig_bw = get_contig_forward(DNAKmer{k}, kmers, twin(DNAKmer{k}, km))

  if km in neighbors(contig_fw[end])
    return contig_fw
  else
    return [[twin(DNAKmer{k}, x) for x in reverse(contig_bw)[1:end-1]] ; contig_fw]
  end
end

@everywhere function get_contig_forward{k}(::Type{DNAKmer{k}}, kmers, km)
    c_fw = DNAKmer{k}[km]
    while true
        # check if forward exists, and only 1:
        fw_neighbors = [kmer for kmer in neighbors(c_fw[end]) if canonical(kmer) in kmers]
        if length(fw_neighbors) != 1
          break
        end
        candidate = fw_neighbors[1]
        if candidate == km || candidate == twin(DNAKmer{k}, km) || candidate == twin(DNAKmer{k}, c_fw[end])
          break
        end
        bw_neighbors = [kmer for kmer in neighbors(twin(DNAKmer{k}, candidate)) if canonical(kmer) in kmers]
        if length(bw_neighbors) != 1
          break
        end
        push!(c_fw, candidate)
    end
    return c_fw
end

@everywhere function all_contigs{k}(::Type{DNAKmer{k}}, kmers)
  done = Set{DNAKmer{k}}()
  contigs = []
  for kmer in kmers
    if kmer in done
      continue
    end
    contig = get_contig(DNAKmer{k}, kmers, kmer)
      for c_kmer in contig
      push!(done, canonical(c_kmer))
    end
    push!(contigs, contig)
  end
  return contigs
end

### classifies each kmer with his colors (present alleles)
@everywhere function find_all_kmer_colors{k}(::Type{DNAKmer{k}}, fastafile)
  record = FASTASeqRecord{DNASequence}()
  kmer_class = DefaultDict{DNAKmer{k}, Vector{Int16}}(() -> Int16[])
  allele_ids = Int16[]
  allele_idx::Int16 = 1
  open(FASTAReader{DNASequence}, fastafile) do reader
    while !eof(reader)
      read!(reader, record)
      seen = Set{DNAKmer{k}}()
      for (pos, kmer) in each(DNAKmer{k}, record.seq)
        can_kmer = canonical(kmer)
        if !in(can_kmer, seen)
          push!(kmer_class[can_kmer], allele_idx)
          push!(seen, can_kmer)
        end
      end
      # find the separator; will assume that if I see a "_", that's it, otherwise try "-";
      separator = in('_', record.name) ? "_" : "-"
      # update idx; the counter idx is incremental (1,2, ...) because we need the array sorted.
      # But this is not always sin the allele ordering, so we have to save the original id to restore it later;
      allele_id = parse(Int16,split(record.name, separator)[end])
      push!(allele_ids, allele_id)
      allele_idx += 1
    end
  end
  return kmer_class, allele_ids
end

@everywhere function build_db_graph{k}(::Type{DNAKmer{k}}, fastafile)
  # get kmers and colors (alleles)
  kmer_class, allele_ids = find_all_kmer_colors(DNAKmer{k}, fastafile)

  contig_list = all_contigs(DNAKmer{k}, keys(kmer_class))
  # select 1 kmer per contig, and set weights:
  kmer_weights = Dict{UInt64, Int}() # Kmer is converted to UInt64, because DNAKmer apparently does not support write(), needed for the parallel pmap() call;
  filtered_kmer_class = Dict{UInt64, Vector{Int16}}()
  for contig in contig_list
      kmer = canonical(contig[1])
      kmer_uint = convert(UInt64,kmer)
      kmer_weights[kmer_uint] = length(contig)
      filtered_kmer_class[kmer_uint] = kmer_class[kmer]
  end
  return (filtered_kmer_class, allele_ids, kmer_weights)
end


using ..Tagging, ..Traits, ..Var, ..Space
# @inline hasintervene(ω) = hastag(ω, Val{:intervene})
@inline tagintervene(ω, intervention) = tag(ω, (intervene = intervention,), mergetags)
@inline (x::Intervened)(ω) = x.x(tagintervene(ω, x.i))
@inline (x::Intervened{X, <:HigherIntervention})(ω) where X =
  x.x(tagintervene(ω, x.i(ω)))

@inline function passintervene(traits, i::Intervention{X, V}, x::X, ω) where {X, V}
  # If the variable ` x` to be applied to ω is the variable to be replaced
  # then replace it, and apply its replacement to ω instead.
  # Othewise proceed as normally
  if i.x == x
    i.v(ω)
  else
    ctxapply(traits, x, ω)
  end
end

function passintervene(traits,
                       i::Union{MultiIntervention{Tuple{Intervention{X1, V1},
                                                        Intervention{X2, V2}}},
                                MultiIntervention{Tuple{Intervention{X1, V1},
                                                        Intervention{X2, V2},
                                                        Intervention{X3, V3}}},
                                MultiIntervention{Tuple{Intervention{X1, V1},
                                                        Intervention{X2, V2},
                                                        Intervention{X3, V3},
                                                        Intervention{X4, V4}}},
                                MultiIntervention{Tuple{Intervention{X1, V1},
                                                        Intervention{X2, V2},
                                                        Intervention{X3, V3},
                                                        Intervention{X4, V4},
                                                        Intervention{X5, V5}}}},
                       x::Union{X1, X2, X3, X4, X5},
                       ω) where {X1, X2, X3, X4, X5, V1, V2, V3, V4, V5}
  if x == i.is[1].x
    i.is[1].v(ω)
  elseif x == i.is[2].x
    i.is[2].v(ω)
  elseif length(i.is) >= 3 && x == i.is[3].x
    i.is[3].v(ω)
  elseif length(i.is) >= 4 && x == i.is[4].x
    i.is[4].v(ω)
  elseif length(i.is) >= 5 && x == i.is[5].x
    i.is[5].v(ω)
  else
    ctxapply(traits, x, ω)
  end
end

function passintervene(traits,
                       i::MultiIntervention{XS},
                       x,
                       ω) where XS
  index = 1;
  while (index <= length(i.is) && x != i.is[index].x)
    index += 1
  end
  if (index <= length(i.is))
    i.is[index].v(ω)
  else
    ctxapply(traits, x, ω)
  end
end

# We only consider intervention if the intervention types match
@inline passintervene(traits, i::AbstractIntervention, x, ω) =
  ctxapply(traits, x, ω)

(f::Vari)(traits::trait(Intervene), ω::AbstractΩ) =
  passintervene(traits, ω.tags.intervene, f, ω)


import Base: map, merge, filter

if isdefined(Base, :foreach)
    import Base.foreach
end

export map,
       probe,
       filter,
       filterwhen,
       foldp,
       sampleon,
       merge,
       previous,
       delay,
       droprepeats,
       flatten,
       bind!,
       unbind!

"""
    map(f, s::Signal...) -> signal

Transform signal `s` by applying `f` to each element. For multiple signal arguments, apply `f` elementwise.
"""
function map(f, input::Signal, inputsrest::Signal...;
             init=f(map(value, (input,inputsrest...))...), typ=typeof(init))

    n = Signal(typ, init, (input,inputsrest...))
    connect_map(f, n, input, inputsrest...)
    n
end

function connect_map(f, output, inputs...)
    let prev_timestep = 0
        for inp in inputs
            add_action!(inp, output) do output, timestep
                if prev_timestep != timestep
                    result = f(map(value, inputs)...)
                    send_value!(output, result, timestep)
                    prev_timestep = timestep
                end
            end
        end
    end
end

probe(node, name, io=STDERR) =
    map(x -> println(io, name, " >! ", x), node)

"""
    foreach(f, inputs...)

Same as `map`, but will be prevented from gc until all the inputs have gone out of scope. Should be used in cases where `f` does a side-effect.
"""
foreach(f, inputs::Signal...) = preserve(map(f, inputs...))

"""
    filter(f, signal)

remove updates from the signal where `f` returns `false`.
"""
function filter{T}(f::Function, default, input::Signal{T})
    n = Signal(T, f(value(input)) ? value(input) : default, (input,))
    connect_filter(f, default, n, input)
    n
end

function connect_filter(f, default, output, input)
    add_action!(input, output) do output, timestep
        val = value(input)
        f(val) && send_value!(output, val, timestep)
    end
end

"""
    filterwhen(switch::Signal{Bool}, default, input)

Keep updates to `input` only when `switch` is true.

If switch is false initially, the specified default value is used.
"""
function filterwhen{T}(predicate::Signal{Bool}, default, input::Signal{T})
    n = Signal(T, value(predicate) ? value(input) : default, (input,))
    connect_filterwhen(n, predicate, input)
    n
end

function connect_filterwhen(output, predicate, input)
    add_action!(input, output) do output, timestep
        value(predicate) && send_value!(output, value(input), timestep)
    end
end

"""
    foldp(f, init, input)

[Fold](http://en.wikipedia.org/wiki/Fold_(higher-order_function)) over past values.

Accumulate a value as the `input` signal changes. `init` is the initial value of the accumulator.
`f` should take 2 arguments: the current accumulated value and the current update, and result in the next accumulated value.
"""
function foldp(f::Function, v0, inputs...; typ=typeof(v0))
    n = Signal(typ, v0, inputs)
    connect_foldp(f, v0, n, inputs)
    n
end

function connect_foldp(f, v0, output, inputs)
    let acc = v0
        for inp in inputs
            add_action!(inp, output) do output, timestep
                vals = map(value, inputs)
                acc = f(acc, vals...)
                send_value!(output, acc, timestep)
            end
        end
    end
end

"""
    sampleon(a, b)

Sample the value of `b` whenever `a` updates.
"""
function sampleon{T}(sampler, input::Signal{T})
    n = Signal(T, value(input), (sampler, input))
    connect_sampleon(n, sampler, input)
    n
end

function connect_sampleon(output, sampler, input)
    add_action!(sampler, output) do output, timestep
        send_value!(output, value(input), timestep)
    end
end

"""
    merge(input...)

Merge many signals into one. Returns a signal which updates when
any of the inputs update. If many signals update at the same time,
the value of the *youngest* input signal is taken.
"""
function merge(inputs...)
    @assert length(inputs) >= 1
    n = Signal(typejoin(map(eltype, inputs)...), value(inputs[1]), inputs)
    connect_merge(n, inputs...)
    n
end

function connect_merge(output, inputs...)
    let prev_timestep = 0
        for inp in inputs
            add_action!(inp, output) do output, timestep
                # don't update twice in the same timestep
                if prev_timestep != timestep
                    send_value!(output, value(inp), timestep)
                    prev_time = timestep
                end
            end
        end
    end
end

"""
    previous(input, default=value(input))

Create a signal which holds the previous value of `input`.
You can optionally specify a different initial value.
"""
function previous{T}(input::Signal{T}, default=value(input))
    n = Signal(T, default, (input,))
    connect_previous(n, input)
    n
end

function connect_previous(output, input)
    let prev_value = value(input)
        add_action!(input, output) do output, timestep
            send_value!(output, prev_value, timestep)
            prev_value = value(input)
        end
    end
end

"""
    delay(input, default=value(input))

Schedule an update to happen after the current update propagates
throughout the signal graph.

Returns the delayed signal.
"""
function delay{T}(input::Signal{T}, default=value(input))
    n = Signal(T, default, (input,))
    connect_delay(n, input)
    n
end

function connect_delay(output, input)
    add_action!(input, output) do output, timestep
        push!(output, value(input))
    end
end

"""
    droprepeats(input)

Drop updates to `input` whenever the new value is the same
as the previous value of the signal.
"""
function droprepeats{T}(input::Signal{T})
    n = Signal(T, value(input), (input,))
    connect_droprepeats(n, input)
    n
end

function connect_droprepeats(output, input)
    let prev_value = value(input)
        add_action!(input, output) do output, timestep
            if prev_value != value(input)
                send_value!(output, value(input), timestep)
                prev_value = value(input)
            end
        end
    end
end

"""
    flatten(input::Signal{Signal}; typ=Any)

Flatten a signal of signals into a signal which holds the
value of the current signal. The `typ` keyword argument specifies
the type of the flattened signal. It is `Any` by default.
"""
function flatten(input::Signal; typ=Any)
    n = Signal(typ, value(value(input)), (input,))
    connect_flatten(n, input)
    n
end

function connect_flatten(output, input)
    let current_node = value(input),
        callback = (output, timestep) -> begin
            send_value!(output, value(value(input)), timestep)
        end

        add_action!(callback, current_node, output)

        add_action!(input, output) do output, timestep

            # Move around action from previous node to current one
            remove_action!(callback, current_node, output)
            current_node = value(input)
            add_action!(callback, current_node, output)

            send_value!(output, value(current_node), timestep)
        end
    end
end

const _bindings = Dict()

"""
    bind!(a,b,twoway=true)

for every update to `a` also update `b` with the same value and vice-versa.
To only bind updates from b to a, pass in a third argument as `false`
"""
function bind!(a::Signal, b::Signal, twoway=true)

    let current_timestep = 0
        action = add_action!(b, a) do a, timestep
            if current_timestep != timestep
                current_timestep = timestep
                send_value!(a, value(b), timestep)
            end
        end
        _bindings[a=>b] = action
    end

    if twoway
        bind!(b, a, false)
    end
end

"""
    unbind!(a,b,twoway=true)

remove a link set up using `bind!`
"""
function unbind!(a::Signal, b::Signal, twoway=true)
    if !haskey(_bindings, a=>b)
        return
    end

    action = _bindings[a=>b]
    a.actions = filter(x->x!=action, a.actions)
    delete!(_bindings, a=>b)

    if twoway
        unbind!(b, a, false)
    end
end


"""
Code for creating, loading and maniuplating line lists and masks.

Author: Eric Ford
Created: August 2020
"""

"""
    Read line list in ESPRESSO csv format.
ESPRESSO format: lambda and weight.
convert_air_to_vacuum determines whether to convert to vacuum wavelengths.
Warning: ESPRESSO masks don't provide line depth and sometimes include one entry for a blend of lines.
"""
function read_linelist_espresso(fn::String; convert_air_to_vacuum::Bool = true)
    local df = CSV.read(fn,DataFrame,threaded=false,header=["lambda","weight"],delim=' ',ignorerepeated=true)
    @assert hasproperty(df, :lambda)
    @assert hasproperty(df, :weight)
    if convert_air_to_vacuum
        df[!,:lambda] .= λ_air_to_vac.(df[!,:lambda])
    end
    return df
end

""" Read line list in VALD csv format.
   VALD format: lambda_lo, lambdaa_hi and depth.
   convert_air_to_vacuum determines whether to convert to vacuum wavelengths.
"""
function read_linelist_vald(fn::String; convert_air_to_vacuum::Bool = true)
    local df = CSV.read(fn,DataFrame,threaded=false,header=["lambda","depth"])
    @assert hasproperty(df, :lambda)
    @assert hasproperty(df, :depth)
    if convert_air_to_vacuum
        df[!,:lambda] .= λ_air_to_vac.(df[!,:lambda])
    end
    df[!,:weight] = df[!,:depth] # TODO: Decide out what we want to do about tracking depths and weights sepoarately
    return df
end

""" Read line list in csv format.
   format: lambda, weight, lambda_lo, lambdaa_hi.
   Assumes air to vacuumb wavelength conversion has already been applied.
"""
function read_linelist_rvspectml(fn::String)
    local df = CSV.read(fn,DataFrame,threaded=false)
    @assert hasproperty(df, :lambda)
    @assert hasproperty(df, :weight) || hasproperty(df, :depth)
    if hasproperty(df, :depth) && !hasproperty(df, :weight)
        df[!,:weight] = df[!,:depth] # TODO: Decide out what we want to do about tracking depths and weights sepoarately
    end
    return df
end


# Key types

struct AES256Key <: AbstractAESKey
	key::Array{UInt8, 1}
end

struct AES192Key <: AbstractAESKey
	key::Array{UInt8, 1}
end

struct AES128Key <: AbstractAESKey
	key::Array{UInt8, 1}
end

@inline convert_key(key::AbstractAESKey, key_length) = key
@inline convert_key(key::Array{UInt8,1}, ::Val{128}) = AES128Key(key)
@inline convert_key(key::Array{UInt8,1}, ::Val{192}) = AES192Key(key)
@inline convert_key(key::Array{UInt8,1}, ::Val{256}) = AES256Key(key)
@inline convert_key(key::String, key_length) = convert_key(Array{UInt8}(key), key_length)

@inline Base.getindex(k::AbstractAESKey, i) = k.key[i]

@inline get_key_length(key::Array{UInt8}) = 8 * length(key)
@inline get_key_length(key::AES128Key) = 128
@inline get_key_length(key::AES192Key) = 192
@inline get_key_length(key::AES256Key) = 256

# Cache Type
mutable struct CipherCache{kType,tmpType,modecacheType} <: AbstractAESCache
	""" registers to contain intermediate keys """
	K::kType
	""" temporary word """
	tmp::tmpType
	""" cache specific to mode """
	modecache::modecacheType
end

@inline get_intermediate_words(key::AES128Key) = @SArray[ zeros(UInt8, 4) for i in 1:4 ]
@inline get_intermediate_words(key::AES192Key) = @SArray[ zeros(UInt8, 4) for i in 1:6 ]
@inline get_intermediate_words(key::AES256Key) = @SArray[ zeros(UInt8, 4) for i in 1:4 ]

@inline get_modecache(m::Val{CTR}) = similar(Array{UInt8}, 16)
@inline get_modecache(m) = nothing

function gen_cache(key::AbstractAESKey, mode=CBC)
	K = get_intermediate_words(key)
	tmp = zeros(UInt8, 4)
	modecache = get_modecache(Val(mode))
	return CipherCache(K, tmp, modecache)
end

# Ciphertext type
mutable struct CipherText{ivType,mode}
	data::Array{UInt8}
	iv::ivType
	keylength::Int
end
CipherText(data, iv::IV, keylength, mode::MODE) where {IV} = CipherText{IV,mode}(data, iv, keylength)
const CT = CipherText

# Cipher Type
mutable struct Cipher{mode,cacheType,keyType} <: AbstractCipher
	cache::cacheType
	key::keyType
end
const AESCipher = Cipher

function Cipher(;key_length=128,mode=CBC,key=keygen(key_length))
	if !is_valid_key_length(key_length)
		error("$key_length is an invalid key length. Key length can be 128, 196 or 256.")
	end

	if get_key_length(key) !==  key_length
		error("Provided Key and Key Length do not agree.")
	end

	aes_key = convert_key(key, Val(key_length))

	cache = gen_cache(aes_key, mode)

	Cipher{mode,typeof(cache),typeof(aes_key)}(cache,aes_key)
end

@inline isecb(cipher::Cipher{m,c,k}) where {m,c,k} = m == ECB
@inline iscbc(cipher::Cipher{m,c,k}) where {m,c,k} = m == CBC
@inline iscfb(cipher::Cipher{m,c,k}) where {m,c,k} = m == CFB
@inline isofb(cipher::Cipher{m,c,k}) where {m,c,k} = m == OFB
@inline isctr(cipher::Cipher{m,c,k}) where {m,c,k} = m == CTR

@inline get_mode(cipher::Cipher{m,c,k}) where {m,c,k} = m
@inline get_key_length(cipher::Cipher) = get_key_length(cipher.key)

@inline needs_iv(cipher) = iscbc(cipher) || isctr(cipher)

# Block type

mutable struct AESBlock{dataType,offsetType} <: AbstractArray{UInt8, 1}
	data::dataType
	offset::offsetType
	function AESBlock(data, offset=1, len=16)
		new{typeof(data), typeof(offset)}(data, offset)
	end
end

function Base.getindex(block::B, i) where {B<:AESBlock}
	return UInt8(block.data[16*(block.offset - 1) + i])
end

function Base.setindex!(block::B, val, i) where {B<:AESBlock}
	block.data[16*(block.offset - 1) + i] = eltype(block.data)(val)
end

function Base.copyto!(block::B, arr::AbstractArray{T2,1}) where {B<:AESBlock, T2}
	for i in 1:16
		block.data[16*(block.offset - 1) + i] = eltype(block.data)(arr[i])
	end
end

@inline Base.size(block::AESBlock) = (16,)
@inline Base.length(block::AESBlock) = 16

increment!(block::B) where {B<:AESBlock} = block.offset += 1


using JuMP; using NCDatasets; using AxisArrays; using CalliopeJuMP.Util;

function load_conversion_plus_constraints(model_dict)
    sets = model_dict["sets"]
    constraint_dict = Dict()

    constraint_dict["balance_conversion_plus_primary_constraint"] = (
        build_constraint(
            ["loc_techs_balance_conversion_plus_primary_constraint",
             "timesteps"], "balance_conversion_plus_primary", model_dict
        )
    )

    constraint_dict["carrier_production_max_conversion_plus_constraint"] = (
        build_constraint(
            ["loc_techs_carrier_production_max_conversion_plus_constraint",
             "timesteps"], "carrier_production_max_conversion_plus", model_dict
        )
    )

    if haskey(sets, "loc_techs_carrier_production_min_conversion_plus_constraint")
        constraint_dict["carrier_production_min_conversion_plus_constraint"] = (
            build_constraint(
                ["loc_techs_carrier_production_min_conversion_plus_constraint",
                "timesteps"], "carrier_production_min_conversion_plus", model_dict
            )
        )
    end

    if haskey(sets, "loc_techs_cost_var_conversion_plus_constraint")
        constraint_dict["cost_var_conversion_plus_constraint"] = (
            build_constraint(
                ["costs", "loc_techs_cost_var_conversion_plus_constraint", "timesteps"],
                "cost_var_conversion_plus", model_dict
            )
        )
    end

    if haskey(sets, "loc_techs_balance_conversion_plus_in_2_constraint")
        constraint_dict["balance_conversion_plus_in_2_constraint"] = (
            build_constraint(
                ["loc_techs_balance_conversion_plus_in_2_constraint", "timesteps"],
                "balance_conversion_plus_tiers", model_dict, tier="in_2"
            )
        )
    end

    if haskey(sets, "loc_techs_balance_conversion_plus_in_3_constraint")
        constraint_dict["balance_conversion_plus_in_3_constraint"] = (
            build_constraint(
                ["loc_techs_balance_conversion_plus_in_3_constraint", "timesteps"],
                "balance_conversion_plus_tiers", model_dict, tier="in_3"
            )
        )
    end

    if haskey(sets, "loc_techs_balance_conversion_plus_out_2_constraint")
        constraint_dict["balance_conversion_plus_out_2_constraint"] = (
            build_constraint(
                ["loc_techs_balance_conversion_plus_out_2_constraint", "timesteps"],
                "balance_conversion_plus_tiers", model_dict, tier="out_2"
            )
        )
    end

    if haskey(sets, "loc_techs_balance_conversion_plus_out_3_constraint")
        constraint_dict["balance_conversion_plus_out_3_constraint"] = (
            build_constraint(
                ["loc_techs_balance_conversion_plus_out_3_constraint", "timesteps"],
                "balance_conversion_plus_tiers", model_dict, tier="out_3"
            )
        )
    end

    return constraint_dict
end

function balance_conversion_plus_primary_constraint_rule(backend_model, set_indices, model_dict)
    """
    Balance energy carrier consumption and production for carrier_in and carrier_out
    """
    loc_tech, timestep = set_indices
    variables = model_dict["variables"]
    parameters = model_dict["parameters"]

    loc_tech_carriers_out = split_comma_list(
        parameters["lookup_loc_techs_conversion_plus"][loc_tech, "out"]
    )
    loc_tech_carriers_in = split_comma_list(
        parameters["lookup_loc_techs_conversion_plus"][loc_tech, "in"]
    )

    energy_eff = get_param(model_dict, "energy_eff", [loc_tech], timestep)

    carrier_prod = sum(
        variables["carrier_prod"][loc_tech_carrier, timestep] /
        get_param(model_dict, "carrier_ratios", [loc_tech_carrier, "out"])
        for loc_tech_carrier in loc_tech_carriers_out
    )
    carrier_con = sum(
        variables["carrier_con"][loc_tech_carrier, timestep] *
        get_param(model_dict, "carrier_ratios", [loc_tech_carrier, "in"])
        for loc_tech_carrier in loc_tech_carriers_in
    )

    return @constraint(backend_model, carrier_prod == -1 * carrier_con * energy_eff)
end

function carrier_production_max_conversion_plus_constraint_rule(backend_model, set_indices, model_dict)
    """
    Set maximum conversion_plus carrier production.
    """
    loc_tech, timestep = set_indices
    variables = model_dict["variables"]
    parameters = model_dict["parameters"]

    timestep_resolution = model_dict["parameters"]["timestep_resolution"][timestep]
    loc_tech_carriers_out = split_comma_list(
        parameters["lookup_loc_techs_conversion_plus"][loc_tech, "out"]
    )

    carrier_prod = sum(variables["carrier_prod"][loc_tech_carrier, timestep]
                 for loc_tech_carrier in loc_tech_carriers_out)

    return @constraint(backend_model, carrier_prod <= timestep_resolution * variables["energy_cap"][loc_tech])
end

function carrier_production_min_conversion_plus_constraint_rule(backend_model, set_indices, model_dict)
    """
    Set minimum conversion_plus carrier production.
    """
    loc_tech, timestep = set_indices
    variables = model_dict["variables"]
    parameters = model_dict["parameters"]

    timestep_resolution = model_dict["parameters"]["timestep_resolution"][timestep]
    min_use = get_param(model_dict, "energy_cap_min_use", [loc_tech], timestep)

    loc_tech_carriers_out = split_comma_list(
        parameters["lookup_loc_techs_conversion_plus"][loc_tech, "out"]
    )

    carrier_prod = sum(variables["carrier_prod"][loc_tech_carrier, timestep]
                       for loc_tech_carrier in loc_tech_carriers_out)

    return @constraint(backend_model, carrier_prod >=
        timestep_resolution * variables["energy_cap"][loc_tech] * min_use
    )
end

function cost_var_conversion_plus_constraint_rule(backend_model, set_indices, model_dict)
    """
    Add time-varying conversion_plus technology costs
    """
    cost, loc_tech, timestep = set_indices
    variables = model_dict["variables"]
    parameters = model_dict["parameters"]
    sets = model_dict["sets"]
    expressions = model_dict["expressions"]

    weight = parameters["timestep_weights"][timestep]

    loc_tech_carrier = parameters["lookup_primary_loc_tech_carriers"][loc_tech]

    var_cost = 0

    if loc_tech_carrier in sets["loc_tech_carriers_prod"]
        cost_om_prod = get_param(model_dict, "cost_om_prod", [loc_tech, cost], timestep)
        if cost_om_prod > 0
            var_cost += (
                cost_om_prod * weight *
                variables["carrier_prod"][loc_tech_carrier, timestep]
            )
        end
    end

    if loc_tech_carrier in sets["loc_tech_carriers_con"]
        cost_om_con = get_param(model_dict, "cost_om_con", [loc_tech, cost], timestep)
        if cost_om_con > 0
            var_cost += (
                cost_om_con * weight *
                variables["carrier_con"][loc_tech_carrier, timestep]
            )
        end
    end

    expressions["cost_var_rhs"][cost, loc_tech, timestep] = var_cost

    return @constraint(backend_model,
        variables["cost_var"][cost, loc_tech, timestep]
        == expressions["cost_var_rhs"][cost, loc_tech, timestep]
    )
end

function balance_conversion_plus_tiers_constraint_rule(backend_model, set_indices, model_dict, tier)
    """
    Force all carrier_in_2/carrier_in_3 and carrier_out_2/carrier_out_3 to follow
    carrier_in and carrier_out (respectively).
    """
    loc_tech, timestep = set_indices
    parameters = model_dict["parameters"]
    primary_tier, decision_variable = get_conversion_plus_io(model_dict, tier)

    loc_tech_carriers_1 = split_comma_list(
        parameters["lookup_loc_techs_conversion_plus"][loc_tech, primary_tier]
    )
    loc_tech_carriers_2 = split_comma_list(
        parameters["lookup_loc_techs_conversion_plus"][loc_tech, tier]
    )

    c_1 = sum(decision_variable[loc_tech_carrier, timestep]
        / get_param(model_dict, "carrier_ratios", [loc_tech_carrier, primary_tier])
        for loc_tech_carrier in loc_tech_carriers_1)
    c_2 = sum(decision_variable[loc_tech_carrier, timestep]
        / get_param(model_dict, "carrier_ratios", [loc_tech_carrier, tier])
        for loc_tech_carrier in loc_tech_carriers_2)
    c_min = parameters["carrier_ratios_min"][loc_tech, tier]

    return @constraint(backend_model, c_1 * c_min == c_2)
end


@testset "Variational Student-T Processes" begin
    N = 50

    x = rand(N)
    X = collect(reshape(x, :, 1))
    y = rand(N)
    ν = 3.0
    k = SqExponentialKernel()
    l = LaplaceLikelihood()
    vi = AnalyticVI()
    m = VStP(x, y, k, l, vi, ν)
    @test_throws ErrorException VStP(x, y, k, l, vi, 0.5)
    @test_throws ErrorException VStP(x, y, k, l, QuadratureVI(), 0.5)
    @test repr(m) ==
          "Variational Student-T Process with a Laplace likelihood (β=1.0) infered by Analytic Variational Inference "
    train!(m, 20)
end


#### thermo states for subdomains

using KernelAbstractions: @print

export new_thermo_state_up,
    new_thermo_state_en,
    recover_thermo_state_all,
    recover_thermo_state_up,
    recover_thermo_state_en

####
#### Interface
####

"""
    new_thermo_state_up(bl, state, aux)

Updraft thermodynamic states given:
 - `bl`, parent `BalanceLaw`
 - `state`, state variables
 - `aux`, auxiliary variables

!!! note
    This method calls saturation adjustment for
    EquilMoist models.
"""
new_thermo_state_up(
    bl::AtmosModel,
    state::Vars,
    aux::Vars,
    ts::ThermodynamicState = recover_thermo_state(bl, state, aux),
) = new_thermo_state_up(bl, moisture_model(bl), state, aux, ts)

"""
    new_thermo_state_en(bl, state, aux)

Environment thermodynamic state given:
 - `bl`, parent `BalanceLaw`
 - `state`, state variables
 - `aux`, auxiliary variables

!!! note
    This method calls saturation adjustment for
    EquilMoist models.
"""
new_thermo_state_en(
    bl::AtmosModel,
    state::Vars,
    aux::Vars,
    ts::ThermodynamicState = recover_thermo_state(bl, state, aux),
) = new_thermo_state_en(bl, moisture_model(bl), state, aux, ts)

"""
    recover_thermo_state_all(bl, state, aux)

Recover NamedTuple of all thermo states

# TODO: Define/call `recover_thermo_state` when it's safely implemented
  (see https://github.com/CliMA/ClimateMachine.jl/issues/1648)
"""
function recover_thermo_state_all(bl, state, aux)
    ts = new_thermo_state(bl, state, aux)
    return (
        gm = ts,
        en = new_thermo_state_en(bl, moisture_model(bl), state, aux, ts),
        up = new_thermo_state_up(bl, moisture_model(bl), state, aux, ts),
    )
end

"""
    recover_thermo_state_up(bl, state, aux, ts = new_thermo_state(bl, state, aux))

Recover the updraft thermodynamic states given:
 - `bl`, parent `BalanceLaw`
 - `state`, state variables
 - `aux`, auxiliary variables

!!! warn
    Right now we are directly calling new_thermo_state_up to avoid
    inconsistent aux states in kernels where the aux states are
    out of sync with the boundary state.

# TODO: Define/call `recover_thermo_state` when it's safely implemented
  (see https://github.com/CliMA/ClimateMachine.jl/issues/1648)
"""
function recover_thermo_state_up(
    bl,
    state,
    aux,
    ts = new_thermo_state(bl, state, aux),
)
    return new_thermo_state_up(bl, moisture_model(bl), state, aux, ts)
end

"""
    recover_thermo_state_en(bl, state, aux, ts = recover_thermo_state(bl, state, aux))

Recover the environment thermodynamic state given:
 - `bl`, parent `BalanceLaw`
 - `state`, state variables
 - `aux`, auxiliary variables

!!! warn
    Right now we are directly calling new_thermo_state_up to avoid
    inconsistent aux states in kernels where the aux states are
    out of sync with the boundary state.

# TODO: Define/call `recover_thermo_state` when it's safely implemented
  (see https://github.com/CliMA/ClimateMachine.jl/issues/1648)
"""
function recover_thermo_state_en(
    bl,
    state,
    aux,
    ts = new_thermo_state(bl, state, aux),
)
    return new_thermo_state_en(bl, moisture_model(bl), state, aux, ts)
end

####
#### Implementation
####

function new_thermo_state_up(
    m::AtmosModel{FT},
    moist::DryModel,
    state::Vars,
    aux::Vars,
    ts::ThermodynamicState,
) where {FT}
    N_up = n_updrafts(turbconv_model(m))
    up = state.turbconv.updraft
    p = air_pressure(ts)
    param_set = parameter_set(m)
    # compute thermo state for updrafts
    ts_up = vuntuple(N_up) do i
        ρa_up = up[i].ρa
        ρaθ_liq_up = up[i].ρaθ_liq
        θ_liq_up =
            fix_void_up(ρa_up, ρaθ_liq_up / ρa_up, liquid_ice_pottemp(ts))
        PhaseDry_pθ(param_set, p, θ_liq_up)
    end
    return ts_up
end

function new_thermo_state_up(
    m::AtmosModel{FT},
    moist::EquilMoist,
    state::Vars,
    aux::Vars,
    ts::ThermodynamicState,
) where {FT}

    N_up = n_updrafts(turbconv_model(m))
    up = state.turbconv.updraft
    p = air_pressure(ts)
    param_set = parameter_set(m)
    # compute thermo state for updrafts
    ts_up = vuntuple(N_up) do i
        ρa_up = up[i].ρa
        ρaθ_liq_up = up[i].ρaθ_liq
        ρaq_tot_up = up[i].ρaq_tot
        θ_liq_up =
            fix_void_up(ρa_up, ρaθ_liq_up / ρa_up, liquid_ice_pottemp(ts))
        q_tot_up = fix_void_up(
            ρa_up,
            ρaq_tot_up / ρa_up,
            total_specific_humidity(ts),
        )
        PhaseEquil_pθq(param_set, p, θ_liq_up, q_tot_up)
    end
    return ts_up
end

function new_thermo_state_en(
    m::AtmosModel,
    moist::DryModel,
    state::Vars,
    aux::Vars,
    ts::ThermodynamicState,
)
    turbconv = turbconv_model(m)
    N_up = n_updrafts(turbconv)
    up = state.turbconv.updraft

    # diagnose environment thermo state
    ρ_inv = 1 / state.ρ
    p = air_pressure(ts)
    θ_liq = liquid_ice_pottemp(ts)
    a_en = environment_area(state, N_up)
    ρaθ_liq_up = vuntuple(N_up) do i
        fix_void_up(up[i].ρa, up[i].ρaθ_liq)
    end
    θ_liq_en = (θ_liq - sum(vuntuple(j -> ρaθ_liq_up[j] * ρ_inv, N_up))) / a_en
    a_min = turbconv.subdomains.a_min
    a_max = turbconv.subdomains.a_max
    param_set = parameter_set(m)
    if !(0 <= θ_liq_en)
        @print("ρaθ_liq_up = ", ρaθ_liq_up[Val(1)], "\n")
        @print("θ_liq = ", θ_liq, "\n")
        @print("θ_liq_en = ", θ_liq_en, "\n")
        error("Environment θ_liq_en out-of-bounds in new_thermo_state_en")
    end
    ts_en = PhaseDry_pθ(param_set, p, θ_liq_en)
    return ts_en
end

function new_thermo_state_en(
    m::AtmosModel,
    moist::EquilMoist,
    state::Vars,
    aux::Vars,
    ts::ThermodynamicState,
)
    turbconv = turbconv_model(m)
    N_up = n_updrafts(turbconv)
    up = state.turbconv.updraft

    # diagnose environment thermo state
    ρ_inv = 1 / state.ρ
    p = air_pressure(ts)
    θ_liq = liquid_ice_pottemp(ts)
    q_tot = total_specific_humidity(ts)
    a_en = environment_area(state, N_up)
    θ_liq_en = (θ_liq - sum(vuntuple(j -> up[j].ρaθ_liq * ρ_inv, N_up))) / a_en
    q_tot_en = (q_tot - sum(vuntuple(j -> up[j].ρaq_tot * ρ_inv, N_up))) / a_en
    a_min = turbconv.subdomains.a_min
    a_max = turbconv.subdomains.a_max
    param_set = parameter_set(m)
    if !(0 <= θ_liq_en)
        @print("θ_liq_en = ", θ_liq_en, "\n")
        error("Environment θ_liq_en out-of-bounds in new_thermo_state_en")
    end
    if !(0 <= q_tot_en <= 1)
        @print("q_tot_en = ", q_tot_en, "\n")
        error("Environment q_tot_en out-of-bounds in new_thermo_state_en")
    end
    ts_en = PhaseEquil_pθq(param_set, p, θ_liq_en, q_tot_en)
    return ts_en
end


module Cytof5
#= Note:
Julia uses Gamma(shape, scale) and InverseGamma(shape, scale).
Note that InverseGamma(shape, scale) IS my preferred parameterization.
It has a mean of scale / (shape - 1) for shape > 1.
=#

export MCMC

include("Util/Util.jl")
include("MCMC/MCMC.jl")
include("Model/Model.jl")
include("VB/VB.jl")

# TODO: get rid of RCall, use Plots with GR backend.
# include("Plot/Plot.jl")

# For R plots
include("R/Plots.jl")

end # module


module color
include("__text_utils.jl")
include("_ansi.jl")

export NamedColor, BitColor, RGBColor, get_color

# ----------------------------- types definition ----------------------------- #
"""
    AbstractColor

Abstract color type.
"""
abstract type AbstractColor end

Base.show(io::IO, color::AbstractColor) = print(io, "$(typeof(color))('$(color.color)')")

struct NamedColor <: AbstractColor
    color::String
end

struct BitColor <: AbstractColor
    color::String
end

struct RGBColor <: AbstractColor
    color::String
    r::Int
    g::Int
    b::Int
end
function RGBColor(s::AbstractString)
    r, g, b = _rgb(s)
    if r < 1
        r *= 255
        g *= 255
        b *= 255
    end
    return RGBColor(s, (Int64 ∘ round)(r), (Int64 ∘ round)(g), (Int64 ∘ round)(b))
end

# --------------------------------- is color? -------------------------------- #
"""
    _rgb(numbertype, txt)

Tries to parse r,g,b out of a string based on number type.
"""
_rgb(number_type, txt) = begin
    rgb = split(remove_brackets(nospaces(txt)), ",")
    r = parse(number_type, rgb[1])
    g = parse(number_type, rgb[2])
    b = parse(number_type, rgb[3])
    return r, g, b
end

"""
    _rgb(numbertype, txt)

Tries to parse r,g,b out of a string.
"""
function _rgb(txt)
    try
        r, g, b = _rgb(Float64, txt)

    catch
        return _rgb(Int64, txt)
    end
end

"""
    is_named_color(string::AbstractString)::Bool

Check if a string represents a named color.
"""
function is_named_color(string::AbstractString)::Bool
    if string ∈ NAMED_COLORS || string ∈ COLORS_16b
        return true
    else
        return false
    end
end

"""
    is_rgb_color(string::AbstractString)::Bool

Check if a string represents a RGB color.
"""
function is_rgb_color(string::AbstractString)::Bool
    try
        _rgb(string)
        return true
    catch
        return false
    end
end

"""
    is_hex_color(string::AbstractString)::Bool

Check if a string represents a hex color.
"""
function is_hex_color(string::AbstractString)::Bool
    stripped = nospaces(string)
    l = length(stripped)
    if stripped[1] == '#' && l == 7
        return true
    else
        return false
    end
end

"""
    is_color(string::AbstractString)::Bool

Check if a string represents color information, of any type.
"""
function is_color(string::AbstractString)::Bool

    is_named = is_named_color(string)
    is_rgb = is_rgb_color(string)
    is_hex = is_hex_color(string)

    return is_named || is_rgb || is_hex
end

"""
    is_background(string::AbstractString)::Bool

Check if a string represents background color information, of any type.
"""
function is_background(string::AbstractString)::Bool
    stripped = nospaces(string)
    if length(stripped) < 3
        return false
    else
        return stripped[1:3] == "on_" && is_color(stripped[4:end])
    end
end

# --------------------------------- get color -------------------------------- #
"""
    hex2rgb(hex::AbstractString)

Converts a string hex color code to a RGB color
"""
function hex2rgb(hex::AbstractString)::RGBColor
    to_int(h::AbstractString) = parse(Int, h, base = 16)
    r, g, b = [to_int(hex[i:i+1]) for i in (2, 4, 6)]
    return RGBColor("($r, $g, $b)", r, g, b)
end

"""
    get_color(string::AbstractString; bg=false)::AbstractColor

Extract a color type from a string with color information.
"""
function get_color(string::AbstractString; bg = false)::AbstractColor
    if bg
        string = nospaces(string)[4:end]
    end

    if is_named_color(string)
        if string ∈ COLORS_16b
            idx = findfirst((c) -> c == string, COLORS_16b)[1]
            return BitColor(COLORS_16b[idx])
        else
            idx = findfirst((c) -> c == string, NAMED_COLORS)[1]
            return NamedColor(NAMED_COLORS[idx])
        end
    elseif is_rgb_color(string)
        return RGBColor(string)
    else
        # convert hex to rgb
        return hex2rgb(string)
    end
end

end


const TA0_DN = UInt8[
	0x30, 0x1C, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13,
	0x02, 0x43, 0x41, 0x31, 0x0D, 0x30, 0x0B, 0x06, 0x03, 0x55, 0x04, 0x03,
	0x13, 0x04, 0x52, 0x6F, 0x6F, 0x74
]

const TA0_RSA_N = UInt8[
	0xB6, 0xD9, 0x34, 0xD4, 0x50, 0xFD, 0xB3, 0xAF, 0x7A, 0x73, 0xF1, 0xCE,
	0x38, 0xBF, 0x5D, 0x6F, 0x45, 0xE1, 0xFD, 0x4E, 0xB1, 0x98, 0xC6, 0x60,
	0x83, 0x26, 0xD2, 0x17, 0xD1, 0xC5, 0xB7, 0x9A, 0xA3, 0xC1, 0xDE, 0x63,
	0x39, 0x97, 0x9C, 0xF0, 0x5E, 0x5C, 0xC8, 0x1C, 0x17, 0xB9, 0x88, 0x19,
	0x6D, 0xF0, 0xB6, 0x2E, 0x30, 0x50, 0xA1, 0x54, 0x6E, 0x93, 0xC0, 0xDB,
	0xCF, 0x30, 0xCB, 0x9F, 0x1E, 0x27, 0x79, 0xF1, 0xC3, 0x99, 0x52, 0x35,
	0xAA, 0x3D, 0xB6, 0xDF, 0xB0, 0xAD, 0x7C, 0xCB, 0x49, 0xCD, 0xC0, 0xED,
	0xE7, 0x66, 0x10, 0x2A, 0xE9, 0xCE, 0x28, 0x1F, 0x21, 0x50, 0xFA, 0x77,
	0x4C, 0x2D, 0xDA, 0xEF, 0x3C, 0x58, 0xEB, 0x4E, 0xBF, 0xCE, 0xE9, 0xFB,
	0x1A, 0xDA, 0xA3, 0x83, 0xA3, 0xCD, 0xA3, 0xCA, 0x93, 0x80, 0xDC, 0xDA,
	0xF3, 0x17, 0xCC, 0x7A, 0xAB, 0x33, 0x80, 0x9C, 0xB2, 0xD4, 0x7F, 0x46,
	0x3F, 0xC5, 0x3C, 0xDC, 0x61, 0x94, 0xB7, 0x27, 0x29, 0x6E, 0x2A, 0xBC,
	0x5B, 0x09, 0x36, 0xD4, 0xC6, 0x3B, 0x0D, 0xEB, 0xBE, 0xCE, 0xDB, 0x1D,
	0x1C, 0xBC, 0x10, 0x6A, 0x71, 0x71, 0xB3, 0xF2, 0xCA, 0x28, 0x9A, 0x77,
	0xF2, 0x8A, 0xEC, 0x42, 0xEF, 0xB1, 0x4A, 0x8E, 0xE2, 0xF2, 0x1A, 0x32,
	0x2A, 0xCD, 0xC0, 0xA6, 0x46, 0x2C, 0x9A, 0xC2, 0x85, 0x37, 0x91, 0x7F,
	0x46, 0xA1, 0x93, 0x81, 0xA1, 0x74, 0x66, 0xDF, 0xBA, 0xB3, 0x39, 0x20,
	0x91, 0x93, 0xFA, 0x1D, 0xA1, 0xA8, 0x85, 0xE7, 0xE4, 0xF9, 0x07, 0xF6,
	0x10, 0xF6, 0xA8, 0x27, 0x01, 0xB6, 0x7F, 0x12, 0xC3, 0x40, 0xC3, 0xC9,
	0xE2, 0xB0, 0xAB, 0x49, 0x18, 0x3A, 0x64, 0xB6, 0x59, 0xB7, 0x95, 0xB5,
	0x96, 0x36, 0xDF, 0x22, 0x69, 0xAA, 0x72, 0x6A, 0x54, 0x4E, 0x27, 0x29,
	0xA3, 0x0E, 0x97, 0x15
]

const TA0_RSA_E = UInt8[
	0x01, 0x00, 0x01
]

const TA1_DN = UInt8[
	0x30, 0x1C, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13,
	0x02, 0x43, 0x41, 0x31, 0x0D, 0x30, 0x0B, 0x06, 0x03, 0x55, 0x04, 0x03,
	0x13, 0x04, 0x52, 0x6F, 0x6F, 0x74
]

const TA1_EC_Q = UInt8[
	0x04, 0x71, 0x74, 0xBA, 0xAB, 0xB9, 0x30, 0x2E, 0x81, 0xD5, 0xE5, 0x57,
	0xF9, 0xF3, 0x20, 0x68, 0x0C, 0x9C, 0xF9, 0x64, 0xDB, 0xB4, 0x20, 0x0D,
	0x6D, 0xEA, 0x40, 0xD0, 0x4A, 0x6E, 0x42, 0xFD, 0xB6, 0x9A, 0x68, 0x25,
	0x44, 0xF6, 0xDF, 0x7B, 0xC4, 0xFC, 0xDE, 0xDD, 0x7B, 0xBB, 0xC5, 0xDB,
	0x7C, 0x76, 0x3F, 0x41, 0x66, 0x40, 0x6E, 0xDB, 0xA7, 0x87, 0xC2, 0xE5,
	0xD8, 0xC5, 0xF3, 0x7F, 0x8D
]

struct br_x500_name
    data::Vector{UInt8}
    len::Csize_t
end

abstract type PublicKey end

struct br_rsa_public_key <: PublicKey
    n::Vector{UInt8}
    nlen::Csize_t
    e::Vector{UInt8}
    elen::Csize_t
end

struct br_ec_public_key <: PublicKey
    curve::Cint
    q::Vector{UInt8}
    qlen::Csize_t
end

struct br_x509_pkey{T <: PublicKey}
    key_type::UInt8
    key::T
end

struct br_x509_trust_anchor{T <: PublicKey}
    dn::br_x500_name
    flags::Cuint
    pkey::br_x509_pkey{T}
end

const BR_X509_TA_CA = Cuint(0x0001)
const BR_KEYTYPE_RSA = 0x01
const BR_KEYTYPE_EC = 0x02
const BR_EC_secp256r1 = Cint(23)

const TAs = br_x509_trust_anchor[
	br_x509_trust_anchor(
		br_x500_name(TA0_DN, sizeof(TA0_DN)),
		BR_X509_TA_CA,
		br_x509_pkey(
			BR_KEYTYPE_RSA,
			br_rsa_public_key(
				TA0_RSA_N, sizeof(TA0_RSA_N),
				TA0_RSA_E, sizeof(TA0_RSA_E),
            )
		)
	),
	br_x509_trust_anchor(
		br_x500_name(TA1_DN, sizeof(TA1_DN)),
		BR_X509_TA_CA,
		br_x509_pkey(
			BR_KEYTYPE_EC,
			br_ec_public_key(
				BR_EC_secp256r1,
                TA1_EC_Q, sizeof(TA1_EC_Q)
            )
        )
    )
]

const TAs_NUM = 2