function parse_header(io)
    natom = nbond = nangle = ndihedral = nimproper = 0
    ntypes = nbtypes = natypes = ndtypes = nitypes = 0

    while !isuppercase(peek(io, Char))
        datastr = readline(io) |> strip
        isempty(datastr) && continue
        if endswith(datastr, "atoms")
            natom = parse(Int, rstrip(!isdigit, datastr))
        elseif endswith(datastr, "bonds")
            nbond = parse(Int, rstrip(!isdigit, datastr))
        elseif endswith(datastr, "angles")
            nangle = parse(Int, rstrip(!isdigit, datastr))
        elseif endswith(datastr, "dihedrals")
            ndihedral = parse(Int, rstrip(!isdigit, datastr))
        elseif endswith(datastr, "impropers")
            nimproper = parse(Int, rstrip(!isdigit, datastr))
        elseif endswith(datastr, "atom types")
            ntypes = parse(Int, rstrip(!isdigit, datastr))
        elseif endswith(datastr, "bond types")
            nbtypes = parse(Int, rstrip(!isdigit, datastr))
        elseif endswith(datastr, "angle types")
            natypes = parse(Int, rstrip(!isdigit, datastr))
        elseif endswith(datastr, "dihedral types")
            ndtypes = parse(Int, rstrip(!isdigit, datastr))
        elseif endswith(datastr, "improper types")
            nitypes = parse(Int, rstrip(!isdigit, datastr))
        else
            @warn "Unused header line: $datastr"
        end
    end
    natom, nbond, nangle, ndihedral, nimproper,
    ntypes, nbtypes, natypes, ndtypes, nitypes
end

function read_masses!(container, io::IO, natoms::Integer)
    readline(io)

    for _ in 1:natoms
        ln = readline(io)
        if isempty(ln)
            error("Not all masses are given")
        end
        is, ms = split(ln)
        i = parse(Int16, is)
        m = parse(Float64, ms)
        container[i] = m
    end
    return container
end

function read_pcoeff!(pair_coeff, types, io::IO, natoms::Integer)
    readline(io)

    ntypes = 0
    for ia in 1:natoms
        ln = readline(io)
        if isempty(ln)
            error("Not all pair coeffs are given")
        end
        is, εs, σs = split(ln)
        i = parse(Int16, is)
        ε = parse(Float64, εs)
        σ = parse(Float64, σs)
        type = findfirst(==((ε, σ)), pair_coeff)
        if type !== nothing
            types[ia] = type
        else
            ntypes += 1
            push!(pair_coeff, (ε, σ))
            types[ia] = ntypes
        end
    end
    return types
end

function read_bcoeff!(bond_coeff, btypes, io::IO, nbonds::Integer)
    readline(io)

    ntypes = 0
    for ib in 1:nbonds
        ln = readline(io)
        if isempty(ln)
            error("Not all bond coeffs are given")
        end
        is, ks, rs = split(ln)
        i = parse(Int16, is)
        k = parse(Float64, ks)
        r0 = parse(Float64, rs)
        type = findfirst(==((k, r0)), bond_coeff)
        if k == 0
            btypes[ib] = 0
        elseif type !== nothing
            btypes[ib] = type
        else
            ntypes += 1
            push!(bond_coeff, (k, r0))
            btypes[ib] = ntypes
        end
    end
    return btypes
end

function read_acoeff!(angle_coeff, atypes, io::IO, nangles::Integer)
    readline(io)

    ntypes = 0
    for ia in 1:nangles
        ln = readline(io)
        if isempty(ln)
            error("Not all angle coeffs are given")
        end
        is, ks, θs = split(ln)
        i = parse(Int16, is)
        k = parse(Float64, ks)
        θ0 = parse(Float64, θs)
        type = findfirst(==((k, θ0)), angle_coeff)
        if k == 0
            atypes[ia] = 0
        elseif type !== nothing
            atypes[ia] = type
        else
            ntypes += 1
            push!(angle_coeff, (k, θ0))
            atypes[ia] = ntypes
        end
    end
    return atypes
end

function read_dcoeff!(dihed_coeff, dtypes, io::IO, ndihed::Integer)
    readline(io)

    ntypes = 0
    for id in 1:ndihed
        ln = readline(io)
        if isempty(ln)
            error("Not all dihedral coeffs are given")
        end
        is, c1s, c2s, c3s, c4s = split(ln)
        i = parse(Int16, is)
        c1, c2, c3, c4 = parse.(Float64, (c1s, c2s, c3s, c4s))

        type = findfirst(==((c1, c2, c3, c4)), dihed_coeff)
        if all(iszero, (c1, c2, c3, c4))
            dtypes[id] = 0
        elseif type !== nothing
            dtypes[id] = type
        else
            ntypes += 1
            push!(dihed_coeff, (c1, c2, c3, c4))
            dtypes[id] = ntypes
        end
    end
    return dtypes
end

function read_icoeff!(improper_coeff, itypes, io::IO, nimproper::Integer)
    readline(io)

    ntypes = 0
    for ii in 1:nimproper
        ln = readline(io)
        if isempty(ln)
            error("Not all improper coeffs are given")
        end
        is, ks, ds, ns = split(ln)
        i = parse(Int16, is)
        k = parse(Float64, ks)
        d = parse(Int8, ds)
        n = parse(Int8, ns)

        type = findfirst(==((k, d, n)), improper_coeff)
        if k == 0
            itypes[ii] = 0
        elseif type !== nothing
            itypes[ii] = type
        else
            ntypes += 1
            push!(improper_coeff, (k, d, n))
            itypes[ii] = ntypes
        end
    end
    return itypes
end

function read_atoms!(coord, charge, io::IO, natoms::Integer)
    readline(io)

    for _ in 1:natoms
        ln = readline(io)
        if isempty(ln)
            error("Missing atoms")
        end
        is, ms, ts, qs, xs, ys, zs = split(ln)
        i, type = parse.(Int16, (is, ts))
        q, x, y, z = parse.(Float64, (qs, xs, ys, zs))
        charge[i] = q
        coord[i] = (x, y, z)
    end

    total_charge = sum(charge)
    if abs(total_charge) > natoms * eps(Float64)
        @warn "Total charge is non-zero" charge=total_charge
    end

    return coord
end

function read_bonds!(bonds, io::IO, nbonds::Integer, bond_map)
    readline(io)

    for _ in 1:nbonds
        ln = readline(io)
        if isempty(ln)
            error("Missing bonds")
        end
        is, ts, a1s, a2s = split(ln)
        i, type, a1, a2 = parse.(Int16, (is, ts, a1s, a2s))
        type = bond_map[type]
        type > 0 && push!(bonds, (a1, a2) => type)
    end
    return bonds
end

function read_angles!(angles, io::IO, nangles::Integer, angle_map)
    readline(io)

    for _ in 1:nangles
        ln = readline(io)
        if isempty(ln)
            error("Missing angles")
        end
        is, ts, a1s, a2s, a3s = split(ln)
        i, type, a1, a2, a3 = parse.(Int16, (is, ts, a1s, a2s, a3s))
        type = angle_map[type]
        type > 0 && push!(angles, (a1, a2, a3) => type)
    end
    return angles
end

function read_dihed!(dihed, io::IO, ndihed::Integer, dihed_map)
    readline(io)

    for _ in 1:ndihed
        ln = readline(io)
        if isempty(ln)
            error("Missing dihedrals")
        end
        is, ts, a1s, a2s, a3s, a4s = split(ln)
        i, type, a1, a2, a3, a4 = parse.(Int16, (is, ts, a1s, a2s, a3s, a4s))
        type = dihed_map[type]
        type > 0 && push!(dihed, (a1, a2, a3, a4) => type)
    end
    return dihed
end

function read_impropers!(improper, io::IO, nimproper::Integer, improper_map)
    readline(io)

    for _ in 1:nimproper
        ln = readline(io)
        if isempty(ln)
            error("Missing impropers")
        end
        is, ts, a1s, a2s, a3s, a4s = split(ln)
        i, type, a1, a2, a3, a4 = parse.(Int16, (is, ts, a1s, a2s, a3s, a4s))
        type = improper_map[type]
        type > 0 && push!(improper, (a1, a2, a3, a4) => type)
    end
    return improper
end


using Parameters: @unpack
using ..Continuations: find_zero!
using ..BifurcationsBase: AbstractSpecialPoint, special_intervals,
    special_points, push_special_point!,
    contkind, FixedPointCont, ContinuationKind

function default_resolve_exception_handler(err, interval, warn_exceptions)
    if any(isa.(Ref(err), warn_exceptions))
        @warn(
            """
            Failed to find bifurcation point within:
            $(interval)
            """,
            exception = err)
        return true
    end
    return false
end

default_resolve_exception_handler(warn_exceptions::Tuple) =
    (args...) -> default_resolve_exception_handler(args..., warn_exceptions)

function resolving_points(
        solver::BifurcationSolver, args...;
        warn_exceptions = (SingularException,),
        exception_handler = default_resolve_exception_handler(warn_exceptions),
        )
    Iterators.filter(
        point -> point !== nothing,
        try
            resolve_point(interval, solver)
        catch err
            # [[../continuations/branching.jl::SingularException]]
            if ! exception_handler(err, interval)
                rethrow()
            end
            nothing
        end for interval in special_intervals(solver, args...))
end

resolved_points(solver::BifurcationSolver, args...; kwargs...) =
    collect(resolving_points(solver, args...; kwargs...))

function resolve_point(point::AbstractSpecialPoint, solver::BifurcationSolver)
    super = as(solver, ContinuationSolver)
    return resolve_point(point, super.cache, super.opts)
end

"""
    resolve_points!(solver; kwargs...)

Resolve special points if not.  It is a no-op if all points are
already resolved.
"""
function resolve_points!(
        solver::BifurcationSolver;
        warn_exceptions = (SingularException,),
        exception_handler = default_resolve_exception_handler(warn_exceptions),
        )
    for sweep in solver.sol.sweeps
        ofs = length(sweep.special_points) + 1
        for interval in @view sweep.special_intervals[ofs:end]
            point = try
                resolve_point!(interval,
                               as(solver.cache, ContinuationCache),
                               solver.opts)
            catch err
                if !exception_handler(err, interval)
                    rethrow()
                end
                continue
            end
            push_special_point!(sweep, point)
        end
    end
end

function special_points!(solver, args...; kwargs...)
    resolve_points!(solver; kwargs...)
    return special_points(solver, args...)
end

resolve_point(point::AbstractSpecialPoint,
              cache::ContinuationCache,
              args...) =
    resolve_point!(point, deepcopy(cache), args...)

resolve_point!(point::SpecialPoint, _...) = point  # already resolved

function resolve_point!(point::SpecialPointInterval,
                        cache::ContinuationCache,
                        opts::ContinuationOptions,
                        ) :: SpecialPoint
    @unpack point_type, point_index = point
    f = testfn_for(point_type, timekind(cache), contkind(cache),
                   cache.prob_cache)
    direction = as(point.sweep.value, ContinuationSweep).direction
    u, tJ, L, Q, J =
        find_zero!(cache, opts, f, point.u0, point.u1, direction)
    return SpecialPoint(timekind(point),
                        point_type, point_index, u, J,
                        WeakRef(point.sweep.value))
end

function testfn_for(point_type::Enum, tkind::TimeKind, ckind::ContinuationKind,
                    prob_cache)
    ptype = Val{point_type}()
    # For call signature of `f`, see:
    # [[../continuations/zero_point.jl::f(]]
    return (u, J, L, Q) -> testfn(ptype, tkind, ckind, prob_cache, u, J, L, Q)
end

testfn(::Val{PointTypes.saddle_node}, ::Continuous, ::FixedPointCont,
       prob_cache, u, J, L, Q) =
    det(ds_jacobian(J))

function testfn(::Val{PointTypes.hopf}, ::Continuous, ::FixedPointCont,
                prob_cache, u, J, L, Q)
    vals = _eigvals(ds_jacobian(J))
    _, i = findmin(abs.(real.(vals)))
    return real(vals[i])
end

testfn(::Val{PointTypes.saddle_node}, ::Discrete, ::FixedPointCont,
       prob_cache, u, J, L, Q) =
    det(ds_jacobian(J) - I)

testfn(::Val{PointTypes.period_doubling}, ::Discrete, ::FixedPointCont,
       prob_cache, u, J, L, Q) =
    det(ds_jacobian(J) + I)


# Arrange
z = MvNormal([0, 0, 0], eye(3))
examplePrior = ExamplePrior(z)

@testset "Factor Creation" begin
    # Assert
    @test examplePrior != nothing
end

@testset "Testing Sampler and Residual" begin
    # Assert
    @test size(getSample(examplePrior, 100)[1]) == (3,100)
end

@testset "Serialization and Deserialization" begin
    # JSON Pack
    jsonConvert = convert(Dict{String, Any}, examplePrior)
    # JSON Unpack
    backToFactor = convert(ExamplePrior, jsonConvert)
    backToJson = convert(Dict{String, Any}, backToFactor)
    # Assert
    @test JSON.json(backToJson) == JSON.json(jsonConvert)

    # Packing and Uunpacking
    packed = convert(PackedExamplePrior, examplePrior)
    unpacked = convert(ExamplePrior, packed)
    repacked = convert(PackedExamplePrior, unpacked)
    # Assert
    @test JSON.json(packed) === JSON.json(repacked)
end


"""
    BufferState(;busy, error, empty)

An object representing the state of the buffer.

# Fields

- `busy=false`: busy if true
- `error=false`: error if true
- `empty=true`: empty if true
"""
mutable struct BufferState
    busy::Bool
    error::Bool
    empty::Bool
end

BufferState(;busy=false, error=false, empty=true) = BufferState(busy, error, empty)

abstract type AbstractParms end

"""
    Parms(; kwargs...) -> Parms

ACT-R parameters with default values. Default values are overwritten with keyword arguments.

# Fields

- `d=0.5`: activation decay
- `τ=0.0`: retrieval threshold
- `s=0.2`: logistic scalar for activation noise.
- `γ=1.6`: maximum associative strength
- `blc=0.0`: base level constant
- `δ=0.0`: mismatch penalty
- `ter=0.0`: a constant for encoding and responding time
- `mmp_fun`: a mismatch penalty function. By default, `mmp_fun` subtracts `δ` from each non-matching slot value
- `sa_fun`: a function for spreading activation which requires arguments for actr and chunk
- `util_mmp_fun`: utility mismatch penalty function applied to each condition
- `lf=1.0:` latency factor parameter
- `u0=0.0`: initial utility value
- `σu=.2`: standard deviation of utility noise
- `δu=1.0`: mismatch penalty parameter for utility
- `bll=false`: base level learning on
- `mmp=false`: mismatch penalty on
- `sa=false`: spreading activatin on
- `noise=false`: noise on
- `mmp_utility=false`: mismatch penalty for procedural memory
- `utility_noise=false`: utility noise for procedural memory
- `misc`: `NamedTuple` of extra parameters
- `filtered:` a list of slots that must absolutely match with mismatch penalty. `isa` and `retrieval` are included
    by default
"""
@concrete mutable struct Parms <: AbstractParms
    d
    τ
    s
    γ
    δ
    blc
    ter
    mmp_fun
    sa_fun
    util_mmp_fun
    lf
    τ′
    u0
    σu
    δu
    bll::Bool
    mmp::Bool
    sa::Bool
    noise::Bool
    mmp_utility
    utility_noise
    misc
end

function Parms(;
    d = .5,
    τ = 0.0,
    s = .3,
    γ = 0.0,
    δ = 0.0,
    blc = 0.0,
    ter = 0.0,
    mmp_fun = default_penalty,
    sa_fun = spreading_activation!,
    util_mmp_fun = utility_match,
    lf = 1.0,
    τ′ = τ,
    u0 = 0.0,
    σu = .2,
    δu = 1.0,
    bll = false,
    mmp = false,
    sa = false,
    noise = false,
    mmp_utility = false,
    utility_noise = false,
    kwargs...
    )

    Parms(
        d,
        τ,
        s,
        γ,
        δ,
        blc,
        ter,
        mmp_fun,
        sa_fun,
        util_mmp_fun,
        lf,
        τ′,
        u0,
        σu,
        δu,
        bll,
        mmp,
        sa,
        noise,
        mmp_utility,
        utility_noise,
        NamedTuple(kwargs)
    )
end

function Base.show(io::IO, ::MIME"text/plain", parms::Parms)
    values = [getfield(parms, f) for f in fieldnames(Parms)]
    values = map(x->typeof(x)== Bool ? string(x) : x, values)
    return pretty_table(
        io,
        values;
        title="Model Parameters",
        row_name_column_title="Parameter",
        compact_printing=false,
        header=["Value"],
        row_name_alignment=:l,
        row_names=[fieldnames(Parms)...],
        formatters=ft_printf("%5.2f"),
        alignment=:l,
    )
end

"""
    Chunk(; kwargs...) -> Chunk

A declarative memory chunk.

# Fields

- `N=1.0`: number of uses
- `L=1.0`: lifetime of chunk
- `time_created=0.0`: time at which the chunk was created
- `k=1`: number of chunks in recent set (k=1 is sufficient)
- `act=0.0`: total activation
- `act_blc=0.0`: base level constant component of activation
- `act_bll=0.0`: base level learning component of activation
- `act_pm=0.0`: partial matching component of activation
- `act_sa=0.0`: spreading activation component of activation
- `act_noise=0.0`: noise component of activation
- `slots`: chunk slot-value pairs
- `reps=0`: number of identical chunks. This can be used in simple cases to speed up the code.
- `recent=[0.0]`: time stamps for k recent retrievals
- `lags=Float64[]`: lags for recent retrievals (L - recent)
- `bl=0.0`: baselevel constant added to chunks activation
"""
mutable struct Chunk{T1,T2}
  N::Int
  L::Float64
  time_created::Float64
  k::Int
  act_mean::T2
  act::T2
  act_blc::T2
  act_bll::T2
  act_pm::T2
  act_sa::T2
  act_noise::T2
  slots::T1
  reps::Int64
  recent::Array{Float64,1}
  lags::Array{Float64,1}
  bl::T2
end

function Chunk(;
    N=1,
    L=1.0,
    time_created=0.0,
    k=1,
    act=0.0,
    recent=[0.0],
    reps=0,
    lags=Float64[],
    bl=zero(typeof(act)),
    slots...
    )
    T = typeof(act)
    act_mean = zero(T)
    act_pm = zero(T)
    act_blc = zero(T)
    act_bll = zero(T)
    act_noise = zero(T)
    act_sa = zero(T)
    return Chunk(N, L, time_created, k, act_mean, act, act_blc, act_bll, act_pm, act_sa, act_noise,
        NamedTuple(slots), reps, recent, lags, bl)
end

"""
    Chunk(; kwargs...) -> Chunk

A declarative memory chunk with dynamic slot-value pairs.

# Fields

- `dynamic::Bool`: slot-value pairs are mutable if true
- `N=1.0`: number of uses
- `L=1.0`: lifetime of chunk
- `time_created=0.0`: time at which the chunk was created
- `k=1`: number of chunks in recent set (k=1 is sufficient)
- `act_mean`: mean activation computed as `act` - `act_noise`
- `act=0.0`: total activation computed as `act_mean` + `act_noise`
- `act_blc=0.0`: base level constant component of activation
- `act_bll=0.0`: base level learning component of activation
- `act_pm=0.0`: partial matching component of activation
- `act_sa=0.0`: spreading activation component of activation
- `act_noise=0.0`: noise component of activation
- `slots`: chunk slot-value pairs
- `reps=0`: number of identical chunks. This can be used in simple cases to speed up the code.
- `recent=[0.0]`: time stamps for k recent retrievals
- `lags=Float64[]`: lags for recent retrievals (L - recent)
- `bl=0.0`: baselevel constant added to chunks activation
"""
function Chunk(dynamic::Bool;
    N=1,
    L=1.0,
    time_created=0.0,
    k=1,
    act=0.0,
    recent=[0.0],
    reps=0,
    lags=Float64[],
    bl=zero(typeof(act)),
    slots...
    )
    T = typeof(act)
    act_mean = zero(T)
    act_pm = zero(T)
    act_blc = zero(T)
    act_bll = zero(T)
    act_noise = zero(T)
    act_sa = zero(T)
    slots = Dict(k=>v for (k,v) in pairs(slots))
    return Chunk(N, L, time_created, k, act_mean, act, act_blc, act_bll, act_pm, act_sa, act_noise,
        slots, reps, recent, lags, bl)
end

Broadcast.broadcastable(x::Chunk) = Ref(x)

const chunk_fields = (:slots,:N,:L,:time_created,:recent,:act_mean,:act,:act_blc,:bl,:act_bll,:act_pm,:act_noise)

function chunk_values(chunk)
    values = [getfield(chunk, f) for f in chunk_fields]
    return map(x->typeof(x)== Bool ? string(x) : x, values)
end

function Base.show(io::IO, ::MIME"text/plain", chunk::Chunk)
    values = chunk_values(chunk)
    return pretty_table(
        io,
        values;
        title="Chunk",
        row_name_column_title="Property",
        compact_printing=false,
        header=["Value"],
        row_name_alignment=:l,
        row_names=[chunk_fields...],
        formatters=ft_printf("%5.2f"),
        alignment=:l,
    )
end

function Base.show(io::IO, ::MIME"text/plain", chunks::Vector{<:Chunk})
    table = [chunk_values(chunk) for chunk in chunks]
    table = hcat(table...)
    table = permutedims(table)
    table = isempty(chunks) ? fill(Missing, 1, length(chunk_fields)) : table
    return pretty_table(
        io,
        table;
        title="Chunks",
        # row_name_column_title="Parameter",
        compact_printing=false,
        header=[chunk_fields...],
        row_name_alignment=:l,
        formatters=ft_printf("%5.2f"),
        alignment=:l,
    )
end

abstract type Mod end

"""
    Declarative(;memory=, filtered=(:isa,:retrieved))

Declarative memory module

# Fields

- `memory=Chunk[]`: array of chunks
- `filtered`: slots that must match exactly even when partial matching is on. By default,
    `filtered=(:isa,:retrieved)`
- `buffer`: an array containing one chunk
- `state`: buffer state
"""
mutable struct Declarative{T1,T2} <: Mod
    memory::Array{T1,1}
    filtered::T2
    buffer::Array{T1,1}
    state::BufferState
end

function Declarative(;memory=Chunk[], filtered=(:isa,:retrieved))
    state = BufferState()
    return  Declarative(memory, filtered, typeof(memory)(undef,1), state)
end

"""
    default_penalty(actr, chunk; request...)

A default function for mismatch penalty. Subtracts δ if
slot does not exist or slot value does not match

# Arguments

- `actr`: an ACTR model object
- `chunk`: memory chunk object

# Keywords

- `request`: a variable size collection of slot-value pairs for the retrieval request
"""
function default_penalty(actr, chunk; request...)
    slots = chunk.slots
    p = 0.0; δ = actr.parms.δ
    for (k,v) in request
        if !(k ∈ keys(slots)) || (slots[k] != v)
            p += δ
        end
     end
     return p
end

Broadcast.broadcastable(x::Declarative) = Ref(x)

"""
    Imaginal(;buffer=Chunk[], ω=1.0, denoms=Int64[])

Imaginal Module.

# Fields

- `buffer`: an array holding up to one chunk
- `state`: buffer state
- `ω=1.0`: fan weight. Default is 1.
- `denoms=Int64[]`: cached value for the denominator of the fan calculation
"""
mutable struct Imaginal{T1,T2} <: Mod
    buffer::Array{T1,1}
    state::BufferState
    ω::T2
    denoms::Vector{Int64}
end

function Imaginal(;buffer=Chunk[], ω=1.0, denoms=Int64[])
    state = BufferState()
    Imaginal(buffer, state, ω, denoms)
end

Imaginal(chunk::Chunk, state, ω, denoms) = Imaginal([chunk], state, ω, denoms)
Imaginal(T::DataType, state, ω, denoms) = Imaginal(T(undef,1), state, ω, denoms)

"""
    Visual(;chunk=Chunk())

Visual Module.

# Fields

- `buffer`: an array holding up to one chunk
- `state`: buffer state
- `focus`: coordinates of visual attention
"""
mutable struct Visual{T1} <: Mod
    buffer::Array{T1,1}
    state::BufferState
    focus::Vector{Float64}
end

Visual(;buffer=Chunk[]) = Visual(buffer, BufferState(), fill(0.0,2))
Visual(chunk::Chunk, state, focus) = Visual([chunk], state, focus)
Visual(T::DataType, state, focus) = Visual(T(undef,1), state, focus)

abstract type AbstractVisualObject end
"""
    VisualObject(;x=300.0, y=300.0, color=:black, text="", shape=:_, width=30.0, height=30.0)

A visible object in a task.

# Fields

- `x`: x coordinate of visual object. Default 0.
- `y`: y coordinate of visual object. Default 0.
- `color`: object color
- `shape`: object shape
- `text`: object text
- `width`: object width
- `height`: object height
"""
mutable struct VisualObject <: AbstractVisualObject
    x::Float64
    y::Float64
    color::Symbol
    shape::Symbol
    text::String
    width::Float64
    height::Float64
end

function VisualObject(;x=300.0, y=300.0, color=:black, text="", shape=:_, width=30.0, height=30.0)
    return VisualObject(x, y, color, shape, text, width, height)
end

"""
**VisualLocation**

Visual Location Module.
- `buffer`: an array holding up to one chunk
- `state`: buffer state

Constructor
````julia
VisualLocation(;buffer=Chunk[])

VisualLocation(chunk::Chunk, state)

VisualLocation(T::DataType, state)

VisualLocation(chunks, state)
````
"""
mutable struct VisualLocation{T1} <: Mod
    buffer::Array{T1,1}
    state::BufferState
    iconic_memory::Array{T1,1}
end

function VisualLocation(;buffer=Chunk[])
    VisualLocation(buffer, BufferState())
end

function VisualLocation(chunk::Chunk, state)
    T = typeof(chunk)
     VisualLocation([chunk], state, Vector{T}(undef,1))
end

function VisualLocation(T::DataType, state)
    VisualLocation(T(undef,1), state, T(undef,1))
end

function VisualLocation(chunks, state)
    c_chunks = copy(chunks)
    VisualLocation(chunks, state, c_chunks)
end

"""
    Rule(;utlity=0.0, conditions, action)

A production rule object.

# Fields

- `utility=0.0`: utility of the production rule
- `initial_utility=0.0`: initial utility
- `utility_mean`=0.0: mean utility
- `utility_penalty=0.0`: mismatch penalty term for utility
- `utlity_noise=0.0`: utility noise
- `conditions`: a function for checking conditions
- `action`: a function for performing an action
- `name`: name of production
"""
@concrete mutable struct Rule
    utility
    initial_utility
    utility_mean
    utility_penalty
    utility_noise
    conditions
    action
    can_pm
    name::String
end

"""
**Procedural**

Procedural Memory Module.
- `buffer`: an array holding up to one chunk
- `state`: buffer state

Constructor
````julia
Procedural(;chunk=Chunk())

Procedural(;rules=Rule[], id="")

Procedural(rule::Rule, state, id)

Procedural(T::DataType, state, id)
````
"""
mutable struct Procedural{R} <: Mod
    id::String
    rules::R
    state::BufferState
end

function Procedural(;rules=Rule[], id="")
    Procedural(id, rules, BufferState())
end

function Procedural(rule::Rule, state, id)
    Procedural(id, [rule], state)
end

function Procedural(T::DataType, state, id)
    Procedural(id, T(undef,1), state)
end

function utility_match(actr, condition)
    @error "a method must be defined for utility_match(actr::ACTR, condition)"
end

"""
    Goal(;chunk=Chunk())

Goal Module.

# Fields

- `buffer`: an array holding up to one chunk
- `state`: buffer state
"""
mutable struct Goal{T1} <: Mod
    buffer::Array{T1,1}
    state::BufferState
end

function Goal(;buffer=Chunk[])
    Goal(buffer, BufferState())
end

function Goal(chunk::Chunk, state)
    Goal([chunk], state)
end

function Goal(T::DataType, state)
    Goal(T(undef,1), state)
end

"""
    Motor(;chunk=Chunk())

Motor Module.

# Fields

- `buffer`: an array holding up to one chunk
- `state`: buffer state
- `mouse_position`: x,y coordinates of mouse position on screen
"""
mutable struct Motor{T1} <: Mod
    buffer::Array{T1,1}
    state::BufferState
    mouse_position::Vector{Float64}
end

function Motor(;buffer=Chunk[], mouse_position=[0.0,0.0])
    Motor(buffer, BufferState(), mouse_position)
end

function Motor(chunk::Chunk, state, mouse_position)
    Motor([chunk], state, mouse_position)
end

function Motor(T::DataType, state, mouse_position)
    Motor(T(undef,1), state, mouse_position)
end

mutable struct Scheduler
    time::Float64
end

Scheduler(;time=0.0) = Scheduler(time)

abstract type AbstractACTR end

"""
ACTR(; kwargs...) -> ACTR

ACTR model object

# Fields

- `name="model1"`: model name
- `declarative`: declarative memory module
- `imaginal`: imaginal memory module
- `visual`: visual module
- `goal`: goal module
- `visual_location`: visual location module
- `visicon`: a vector of VisualObjects available in the task
- `parms`: model parameters
- `scheduler`: event scheduler

# Example

````julia
parms = (noise=true, τ=-1.0)
chunks = [Chunk(;animal=:dog,name=:Sigma), Chunk(;animal=:rat,name=:Bonkers)]
declarative = Declarative(;memory=chunks)
actr = ACTR(;declarative, parms...)
````
"""
@concrete mutable struct ACTR <: AbstractACTR
    name
    declarative
    imaginal
    visual
    visual_location
    goal
    procedural
    motor
    visicon
    parms
    scheduler
end

Broadcast.broadcastable(x::ACTR) = Ref(x)

function ACTR(;name="model1", declarative=Declarative(), imaginal=Imaginal(),
    goal = Goal(), scheduler=Scheduler(), visual=nothing, visual_location=nothing,
    procedural=nothing, motor=nothing, visicon=init_visicon(), parms...)
    parms′ = Parms(;parms...)
    ACTR(name, declarative, imaginal, visual, visual_location, goal, procedural, motor, visicon, parms′, scheduler)
end

function init_visicon()
    Dict{String,VisualObject}()
end


using GPMLj, Test, Random, PyCall

Random.seed!(123)

@testset "Likelihoods" begin
    @testset "Bernoulli" begin
        likelihood = gpflow.likelihoods.Bernoulli()
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.Bernoulli
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "Beta" begin
        likelihood = gpflow.likelihoods.Beta()
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.Beta
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "Exponential" begin
        likelihood = gpflow.likelihoods.Exponential()
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.Exponential
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "Gamma" begin
        likelihood = gpflow.likelihoods.Gamma()
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.Gamma
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "Gaussian" begin
        likelihood = gpflow.likelihoods.Gaussian()
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.Gaussian
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "GaussianMC" begin
        @test_throws "GaussianMC not implemented" gpflow.likelihoods.GaussianMC()
    end

    @testset "Likelihood" begin
        likelihood = gpflow.likelihoods.Likelihood()
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.Likelihood
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "MonteCarloLikelihood" begin
        likelihood = gpflow.likelihoods.MonteCarloLikelihood()
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.MonteCarloLikelihood
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "MultiClass" begin
        likelihood = gpflow.likelihoods.MultiClass(3)
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.MultiClass
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "Ordinal" begin
        likelihood = gpflow.likelihoods.Ordinal([1,2])
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.Ordinal
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "Poisson" begin
        likelihood = gpflow.likelihoods.Poisson()
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.Poisson
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "RobustMax" begin
        likelihood = gpflow.likelihoods.RobustMax(3)
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.RobustMax
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "SoftMax" begin
        likelihood = gpflow.likelihoods.SoftMax(3)
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.SoftMax
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    @testset "StudentT" begin
        likelihood = gpflow.likelihoods.StudentT()
        @test typeof(likelihood)<:gpflow.AbstractLikelihood
        @test typeof(likelihood)<:gpflow.likelihoods.StudentT
        @test typeof(likelihood.o)<:PyObject
        temp = likelihood.o
        @test instantiate!(likelihood) == temp
    end

    # TODO
    # @testset "SwitchedLikelihood" begin
    #     likelihood = gpflow.likelihoods.SwitchedLikelihood()
    #     @test typeof(likelihood)<:gpflow.AbstractLikelihood
    #     @test typeof(likelihood)<:gpflow.likelihoods.SwitchedLikelihood
    #     @test typeof(likelihood.o)<:PyObject
    #     temp = likelihood.o
    #     @test instantiate!(likelihood) == temp
    # end

end #module


# Copyright 2020-2021 John T. Foster
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# %%
function generate_helix_test_tangents(r::Real=1, c::Real=1, number_of_points::Integer=100)
    t = LinRange(0, 100, number_of_points)
    dx = -r .* sin.(t)
    dy = r .* cos.(t)
    dz = c
    arr = zeros(Float64, (length(t), 3))
    arr[:, 1] = dx
    arr[:, 2] = dy
    arr[:, 3] .= dz
    arr
end

function create_knot_vector(number_of_data_points::Integer,
                            number_of_control_points::Integer=(number_of_data_points ÷ 2),
                            p::Integer=3,
                            Δū::Real=30)
    ū = collect(StepRange(0, Δū, number_of_data_points * Δū)) / (number_of_data_points * Δū)
    d = (number_of_data_points + 1) / (number_of_control_points - p + 1)
    kv = zeros(number_of_control_points + p + 2)
    for j in 1:(number_of_control_points - p)
        i = floor(Int, j * d)
        α = j * d - i
        kv[j + p] = (1 - α) * ū[i - 1] + α * ū[i]
    end
    kv
end

function predict(control_points::Array{<:AbstractFloat,2})

end

function loss(x)
end


function FieldAltitudeDetector(arg0::RealFieldElement, arg1::BodyShape)
    return FieldAltitudeDetector((RealFieldElement, BodyShape), arg0, arg1)
end

function FieldAltitudeDetector(arg0::RealFieldElement, arg1::RealFieldElement, arg2::BodyShape)
    return FieldAltitudeDetector((RealFieldElement, RealFieldElement, BodyShape), arg0, arg1, arg2)
end

function FieldAltitudeDetector(arg0::RealFieldElement, arg1::RealFieldElement, arg2::RealFieldElement, arg3::BodyShape)
    return FieldAltitudeDetector((RealFieldElement, RealFieldElement, RealFieldElement, BodyShape), arg0, arg1, arg2, arg3)
end

function g(obj::FieldAltitudeDetector, arg0::FieldSpacecraftState)
    return jcall(obj, "g", RealFieldElement, (FieldSpacecraftState,), arg0)
end

function get_altitude(obj::FieldAltitudeDetector)
    return jcall(obj, "getAltitude", RealFieldElement, ())
end

function get_body_shape(obj::FieldAltitudeDetector)
    return jcall(obj, "getBodyShape", BodyShape, ())
end


s = open("input.txt") do file
    read(file, String)
end

passports = split(s, "\n\n");

function ValidPassport(passport)
    requiredFields = [
        r"byr:(19[2-9]\d|200[0-2])(\s|$)",
        r"iyr:20(1\d|20)(\s|$)",
        r"eyr:20(2\d|30)(\s|$)",
        r"hgt:(1([5-8]\d|9[0-3])cm|(59|6\d|7[0-6])in)(\s|$)",
        r"hcl:#[0-9a-f]{6}(\s|$)",
        r"ecl:(amb|blu|brn|gry|grn|hzl|oth)(\s|$)",
        r"pid:\d{9}(\s|$)"
    ];

    for field in requiredFields
        if !occursin(field, passport)
            return 0;
        end
    end

    return 1;
end

sum(map(ValidPassport, passports))
ValidPassport("eyr:1972 cid:100\nhcl:#18171d ecl:amb hgt:170 pid:186cm iyr:2018 byr:1926")

ValidPassport("iyr:2019 hcl:#602927 eyr:1967 hgt:170cm ecl:grn pid:012533040 byr:1946")

ValidPassport("hcl:dab227 iyr:2012
ecl:brn hgt:182cm pid:021572410 eyr:2020 byr:1992 cid:277")

ValidPassport("hgt:59cm ecl:zzz
eyr:2038 hcl:74454a iyr:2023
pid:3556412378 byr:2007")

ValidPassport("pid:087499704 hgt:74in ecl:grn iyr:2012 eyr:2030 byr:1980
hcl:#623a2f")

ValidPassport("eyr:2029 ecl:blu cid:129 byr:1989
iyr:2014 pid:896056539 hcl:#a97842 hgt:165cm")

ValidPassport("hcl:#888785
hgt:164cm byr:2001 iyr:2015 cid:88
pid:545766238 ecl:hzl
eyr:2022")

ValidPassport("iyr:2010 hgt:158cm hcl:#b6652a ecl:blu byr:1944 eyr:2021 pid:093154719")

    # byr (Birth Year) - four digits; at least 1920 and at most 2002.
    # iyr (Issue Year) - four digits; at least 2010 and at most 2020.
    # eyr (Expiration Year) - four digits; at least 2020 and at most 2030.
    # hgt (Height) - a number followed by either cm or in:
    #     If cm, the number must be at least 150 and at most 193.
    #     If in, the number must be at least 59 and at most 76.
    # hcl (Hair Color) - a # followed by exactly six characters 0-9 or a-f.
    # ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.
    # pid (Passport ID) - a nine-digit number, including leading zeroes.
    # cid (Country ID) - ignored, missing or not.


module TestShow

using Phylo
using DataFrames

using Test

@info """
These tests only check that the show() commands
do not give warnings or errors, not that they are correct!
"""

ntips = 10
a = IOBuffer()
@testset "Nonultrametric{$TreeType}" for TreeType in
    [NamedTree, NamedPolytomousTree, RootedTree]

    nt = rand(Nonultrametric{TreeType}(ntips))
    @test_nowarn show(a, nt)
    @test_nowarn show(a, first(nodeiter(nt)))
    @test_nowarn show(a, first(branchiter(nt)))
    ts = rand(Nonultrametric{TreeType}(10), 10)
    @test_nowarn show(a, ts)
    ps = parsenewick("((,),(,,));", TreeType)
    @test_nowarn show(a, ps)
    @test_nowarn show(a, first(nodeiter(ps)))
    @test_nowarn show(a, first(branchiter(ps)))
end
end


##########
# Traces #
##########

"""
    Trace{T}

Abstract type for a trace of a generative function.
"""
abstract type Trace end

"""
    get_args(trace)

Return the argument tuple for a given execution.

Example:
```julia
args::Tuple = get_args(trace)
```
"""
function get_args end

"""
    get_retval(trace)

Return the return value of the given execution.

Example for generative function with return type `T`:
```julia
retval::T = get_retval(trace)
```
"""
function get_retval end

"""
    get_choices(trace)

Return a value implementing the assignment interface

Note that the value of any non-addressed randomness is not externally accessible.

Example:

```julia
choices::ChoiceMap = get_choices(trace)
z_val = choices[:z]
```
"""
function get_choices end

"""
    get_score(trace)

Return:
```math
\\log \\frac{p(t, r; x)}{q(r; x, t)}
```

When there is no non-addressed randomness, this simplifies to the log probability `\$\\log p(t; x)\$.
"""
function get_score end

"""
    gen_fn::GenerativeFunction = get_gen_fn(trace)

Return the generative function that produced the given trace.
"""
function get_gen_fn end

"""
    value = trace[addr]

Get the value of the random choice at address `addr`.
"""
function Base.getindex(trace::Trace, addr)
    get_choices(trace)[addr]
end

export get_args
export get_retval
export get_choices
export get_score
export get_gen_fn

######################
# GenerativeFunction #
######################

"""
    GenerativeFunction{T,U <: Trace}

Abstract type for a generative function with return value type T and trace type U.
"""
abstract type GenerativeFunction{T,U <: Trace} end

get_return_type(::GenerativeFunction{T,U}) where {T,U} = T
get_trace_type(::GenerativeFunction{T,U}) where {T,U} = U

"""
    bools::Tuple = has_argument_grads(gen_fn::Union{GenerativeFunction,Distribution})

Return a tuple of booleans indicating whether a gradient is available for each of its arguments.
"""
function has_argument_grads end

"""
    req::Bool = accepts_output_grad(gen_fn::GenerativeFunction)

Return a boolean indicating whether the return value is dependent on any of the *gradient source elements* for any trace.

The gradient source elements are:

- Any argument whose position is true in `has_argument_grads`

- Any trainable parameter

- Random choices made at a set of addresses that are selectable by `choice_gradients`.
"""
function accepts_output_grad end

"""
    get_params(gen_fn::GenerativeFunction)

Return an iterable over the trainable parameters of the generative function.
"""
get_params(::GenerativeFunction) = ()

"""
    trace = simulate(gen_fn, args)

Execute the generative function and return the trace.

Given arguments (`args`), sample \$t \\sim p(\\cdot; x)\$ and \$r \\sim p(\\cdot; x,
t)\$, and return a trace with choice map \$t\$.
"""
function simulate(::GenerativeFunction, ::Tuple)
    error("Not implemented")
end

"""
    (trace::U, weight) = generate(gen_fn::GenerativeFunction{T,U}, args::Tuple)

Return a trace of a generative function.

    (trace::U, weight) = generate(gen_fn::GenerativeFunction{T,U}, args::Tuple,
                                    constraints::ChoiceMap)

Return a trace of a generative function that is consistent with the given
constraints on the random choices.

Given arguments \$x\$ (`args`) and assignment \$u\$ (`constraints`) (which is empty for the first form), sample \$t \\sim
q(\\cdot; u, x)\$ and \$r \\sim q(\\cdot; x, t)\$, and return the trace \$(x, t, r)\$ (`trace`).
Also return the weight (`weight`):
```math
\\log \\frac{p(t, r; x)}{q(t; u, x) q(r; x, t)}
```

Example without constraints:
```julia
(trace, weight) = generate(foo, (2, 4))
```

Example with constraint that address `:z` takes value `true`.
```julia
(trace, weight) = generate(foo, (2, 4), choicemap((:z, true))
```
"""
function generate(::GenerativeFunction, ::Tuple, ::ChoiceMap)
    error("Not implemented")
end

function generate(gen_fn::GenerativeFunction, args::Tuple)
    generate(gen_fn, args, EmptyChoiceMap())
end

"""
    weight = project(trace::U, selection::Selection)

Estimate the probability that the selected choices take the values they do in a
trace.

Given a trace \$(x, t, r)\$ (`trace`) and a set of addresses \$A\$ (`selection`),
let \$u\$ denote the restriction of \$t\$ to \$A\$. Return the weight
(`weight`):
```math
\\log \\frac{p(r, t; x)}{q(t; u, x) q(r; x, t)}
```
"""
function project(trace, selection::Selection)
    error("Not implemented")
end

"""
    (choices, weight, retval) = propose(gen_fn::GenerativeFunction, args::Tuple)

Sample an assignment and compute the probability of proposing that assignment.

Given arguments (`args`), sample \$t \\sim p(\\cdot; x)\$ and \$r \\sim p(\\cdot; x,
t)\$, and return \$t\$
(`choices`) and the weight (`weight`):
```math
\\log \\frac{p(r, t; x)}{q(r; x, t)}
```
"""
function propose(gen_fn::GenerativeFunction, args::Tuple)
    trace = simulate(gen_fn, args)
    weight = get_score(trace)
    (get_choices(trace), weight, get_retval(trace))
end

"""
    (weight, retval) = assess(gen_fn::GenerativeFunction, args::Tuple, choices::ChoiceMap)

Return the probability of proposing an assignment

Given arguments \$x\$ (`args`) and an assignment \$t\$ (`choices`) such that
\$p(t; x) > 0\$, sample \$r \\sim q(\\cdot; x, t)\$ and
return the weight (`weight`):
```math
\\log \\frac{p(r, t; x)}{q(r; x, t)}
```
It is an error if \$p(t; x) = 0\$.
"""
function assess(gen_fn::GenerativeFunction, args::Tuple, choices::ChoiceMap)
    (trace, weight) = generate(gen_fn, args, choices)
    (weight, get_retval(trace))
end

"""
    (new_trace, weight, retdiff, discard) = update(trace, args::Tuple, argdiffs::Tuple,
                                                   constraints::ChoiceMap)

Update a trace by changing the arguments and/or providing new values for some
existing random choice(s) and values for some newly introduced random choice(s).

Given a previous trace \$(x, t, r)\$ (`trace`), new arguments \$x'\$ (`args`), and
a map \$u\$ (`constraints`), return a new trace \$(x', t', r')\$ (`new_trace`)
that is consistent with \$u\$.  The values of choices in \$t'\$ are
either copied from \$t\$ or from \$u\$ (with \$u\$ taking
precedence) or are sampled from the internal proposal distribution.  All choices in \$u\$ must appear in \$t'\$.  Also return an
assignment \$v\$ (`discard`) containing the choices in \$t\$ that were
overwritten by values from \$u\$, and any choices in \$t\$ whose address does
not appear in \$t'\$. Sample \$t' \\sim q(\\cdot; x', t + u)\$, and \$r' \\sim
q(\\cdot; x', t')\$, where \$t + u\$ is the choice map obtained by merging
\$t\$ and \$u\$ with \$u\$ taking precedence for overlapping addresses.  Also
return a weight (`weight`):
```math
\\log \\frac{p(r', t'; x') q(r; x, t)}{p(r, t; x) q(r'; x', t') q(t'; x', t + u)}
```
"""
function update(trace, ::Tuple, argdiffs::Tuple, ::ChoiceMap)
    error("Not implemented")
end

"""
    (new_trace, weight, retdiff) = regenerate(trace, args::Tuple, argdiffs::Tuple,
                                              selection::Selection)

Update a trace by changing the arguments and/or randomly sampling new values
for selected random choices using the internal proposal distribution family.

Given a previous trace \$(x, t, r)\$ (`trace`), new arguments \$x'\$ (`args`), and
a set of addresses \$A\$ (`selection`), return a new trace \$(x', t')\$
(`new_trace`) such that \$t'\$ agrees with \$t\$ on all addresses not in \$A\$
(\$t\$ and \$t'\$ may have different sets of addresses).  Let \$u\$ denote the
restriction of \$t\$ to the complement of \$A\$.  Sample \$t' \\sim Q(\\cdot;
u, x')\$ and sample \$r' \\sim Q(\\cdot; x', t')\$.
Return the new trace \$(x', t', r')\$ (`new_trace`) and the weight
(`weight`):
```math
\\log \\frac{p(r', t'; x') q(t; u', x) q(r; x, t)}{p(r, t; x) q(t'; u, x') q(r'; x', t')}
```
where \$u'\$ is the restriction of \$t'\$ to the complement of \$A\$.
"""
function regenerate(trace, args::Tuple, argdiffs::Tuple, selection::Selection)
    error("Not implemented")
end

"""
    arg_grads = accumulate_param_gradients!(trace, retgrad, scale_factor=1.)

Increment gradient accumulators for parameters by the gradient of the
log-probability of the trace, optionally scaled, and return the gradient with
respect to the arguments (not scaled).

Given a previous trace \$(x, t)\$ (`trace`) and a gradient with respect to the
return value \$∇_y J\$ (`retgrad`), return the following gradient (`arg_grads`)
with respect to the arguments \$x\$:
```math
∇_x \\left( \\log P(t; x) + J \\right)
```
Also increment the gradient accumulators for the trainable parameters \$Θ\$ of
the function by:
```math
∇_Θ \\left( \\log P(t; x) + J \\right)
```
"""
function accumulate_param_gradients!(trace, retgrad, scale_factor)
    error("Not implemented")
end

accumulate_param_gradients!(trace, retgrad) = accumulate_param_gradients!(trace, retgrad, 1.)

"""
    (arg_grads, choice_values, choice_grads) = choice_gradients(trace, selection::Selection,
                                                                retgrad)

Given a previous trace \$(x, t)\$ (`trace`) and a gradient with respect to the
return value \$∇_y J\$ (`retgrad`), return the following gradient (`arg_grads`)
with respect to the arguments \$x\$:
```math
∇_x \\left( \\log P(t; x) + J \\right)
```
Also given a set of addresses \$A\$ (`selection`) that are continuous-valued
random choices, return the folowing gradient (`choice_grads`) with respect to
the values of these choices:
```math
∇_A \\left( \\log P(t; x) + J \\right)
```
Also return the assignment (`choice_values`) that is the restriction of \$t\$ to \$A\$.
"""
function choice_gradients(trace, selection::Selection, retgrad)
    error("Not implemented")
end

export GenerativeFunction
export has_argument_grads
export accepts_output_grad
export get_params
export simulate
export generate
export project
export propose
export assess
export update
export regenerate
export accumulate_param_gradients!
export choice_gradients


using CUDA: has_cuda
using OrderedCollections: OrderedDict

using Oceananigans: AbstractModel, AbstractOutputWriter, AbstractDiagnostic

using Oceananigans.Architectures: AbstractArchitecture
using Oceananigans.Distributed: MultiArch
using Oceananigans.Advection: CenteredSecondOrder
using Oceananigans.BuoyancyModels: validate_buoyancy, regularize_buoyancy, SeawaterBuoyancy
using Oceananigans.BoundaryConditions: regularize_field_boundary_conditions
using Oceananigans.Fields: BackgroundFields, Field, tracernames, VelocityFields, TracerFields, PressureFields
using Oceananigans.Forcings: model_forcing
using Oceananigans.Grids: inflate_halo_size, with_halo, architecture
using Oceananigans.Solvers: FFTBasedPoissonSolver
using Oceananigans.TimeSteppers: Clock, TimeStepper, update_state!
using Oceananigans.TurbulenceClosures: with_tracers, DiffusivityFields, time_discretization, implicit_diffusion_solver
using Oceananigans.LagrangianParticleTracking: LagrangianParticles
using Oceananigans.Utils: tupleit
using Oceananigans.Grids: topology

import Oceananigans.Architectures: architecture

const ParticlesOrNothing = Union{Nothing, LagrangianParticles}

mutable struct NonhydrostaticModel{TS, E, A<:AbstractArchitecture, G, T, B, R, SD, U, C, Φ, F,
                                   V, S, K, BG, P, I, AF} <: AbstractModel{TS}

         architecture :: A        # Computer `Architecture` on which `Model` is run
                 grid :: G        # Grid of physical points on which `Model` is solved
                clock :: Clock{T} # Tracks iteration number and simulation time of `Model`
            advection :: V        # Advection scheme for velocities _and_ tracers
             buoyancy :: B        # Set of parameters for buoyancy model
             coriolis :: R        # Set of parameters for the background rotation rate of `Model`
         stokes_drift :: SD       # Set of parameters for surfaces waves via the Craik-Leibovich approximation
              forcing :: F        # Container for forcing functions defined by the user
              closure :: E        # Diffusive 'turbulence closure' for all model fields
    background_fields :: BG       # Background velocity and tracer fields
            particles :: P        # Particle set for Lagrangian tracking
           velocities :: U        # Container for velocity fields `u`, `v`, and `w`
              tracers :: C        # Container for tracer fields
            pressures :: Φ        # Container for hydrostatic and nonhydrostatic pressure
   diffusivity_fields :: K        # Container for turbulent diffusivities
          timestepper :: TS       # Object containing timestepper fields and parameters
      pressure_solver :: S        # Pressure/Poisson solver
    immersed_boundary :: I        # Models the physics of immersed boundaries within the grid
     auxiliary_fields :: AF       # User-specified auxiliary fields for forcing functions and boundary conditions
end

"""
    NonhydrostaticModel(;
                   grid,
           architecture = CPU(),
                  clock = Clock{eltype(grid)}(0, 0, 1),
              advection = CenteredSecondOrder(),
               buoyancy = nothing,
               coriolis = nothing,
           stokes_drift = nothing,
                forcing = NamedTuple(),
                closure = nothing,
    boundary_conditions = NamedTuple(),
                tracers = (),
            timestepper = :QuasiAdamsBashforth2,
      background_fields = NamedTuple(),
              particles = nothing,
             velocities = nothing,
              pressures = nothing,
     diffusivity_fields = nothing,
        pressure_solver = nothing,
      immersed_boundary = nothing,
       auxiliary_fields = NamedTuple(),
    )

Construct a model for a non-hydrostatic, incompressible fluid, using the Boussinesq approximation
when `buoyancy != nothing`. By default, all Bounded directions are rigid and impenetrable.

Keyword arguments
=================

  - `grid`: (required) The resolution and discrete geometry on which `model` is solved.
  - `architecture`: `CPU()` or `GPU()`. The computer architecture used to time-step `model`.
  - `advection`: The scheme that advects velocities and tracers. See `Oceananigans.Advection`.
  - `buoyancy`: The buoyancy model. See `Oceananigans.BuoyancyModels`.
  - `closure`: The turbulence closure for `model`. See `Oceananigans.TurbulenceClosures`.
  - `coriolis`: Parameters for the background rotation rate of the model.
  - `forcing`: `NamedTuple` of user-defined forcing functions that contribute to solution tendencies.
  - `boundary_conditions`: `NamedTuple` containing field boundary conditions.
  - `tracers`: A tuple of symbols defining the names of the modeled tracers, or a `NamedTuple` of
               preallocated `CenterField`s.
  - `timestepper`: A symbol that specifies the time-stepping method. Either `:QuasiAdamsBashforth2` or
                   `:RungeKutta3`.
"""
function NonhydrostaticModel(;    grid,
                                 clock = Clock{eltype(grid)}(0, 0, 1),
                             advection = CenteredSecondOrder(),
                              buoyancy = nothing,
                              coriolis = nothing,
                          stokes_drift = nothing,
                   forcing::NamedTuple = NamedTuple(),
                               closure = nothing,
       boundary_conditions::NamedTuple = NamedTuple(),
                               tracers = (),
                           timestepper = :QuasiAdamsBashforth2,
         background_fields::NamedTuple = NamedTuple(),
         particles::ParticlesOrNothing = nothing,
                            velocities = nothing,
                             pressures = nothing,
                    diffusivity_fields = nothing,
                       pressure_solver = nothing,
                     immersed_boundary = nothing,
                      auxiliary_fields = NamedTuple(),
    )

    arch = architecture(grid)

    if arch == GPU() && !has_cuda()
         throw(ArgumentError("Cannot create a GPU model. No CUDA-enabled GPU was detected!"))
    end

    tracers = tupleit(tracers) # supports tracers=:c keyword argument (for example)

    validate_buoyancy(buoyancy, tracernames(tracers))
    buoyancy = regularize_buoyancy(buoyancy)

    # Adjust halos when the advection scheme or turbulence closure requires it.
    # Note that halos are isotropic by default; however we respect user-input here
    # by adjusting each (x, y, z) halo individually.
    user_halo = grid.Hx, grid.Hy, grid.Hz
    required_halo = Hx, Hy, Hz = inflate_halo_size(user_halo..., topology(grid), advection, closure)
    if any(user_halo .< required_halo) # Replace grid
        @warn "Inflating model grid halo size to ($Hx, $Hy, $Hz) and recreating grid. " *
              "The model grid will be different from the input grid. To avoid this warning, " *
              "pass halo=($Hx, $Hy, $Hz) when constructing the grid."

        grid = with_halo((Hx, Hy, Hz), grid)
    end

    # Collect boundary conditions for all model prognostic fields and, if specified, some model
    # auxiliary fields. Boundary conditions are "regularized" based on the _name_ of the field:
    # boundary conditions on u, v, w are regularized assuming they represent momentum at appropriate
    # staggered locations. All other fields are regularized assuming they are tracers.
    # Note that we do not regularize boundary conditions contained in *tupled* diffusivity fields right now.
    #
    # First, we extract boundary conditions that are embedded within any _user-specified_ field tuples:
    embedded_boundary_conditions = merge(extract_boundary_conditions(velocities),
                                         extract_boundary_conditions(tracers),
                                         extract_boundary_conditions(pressures),
                                         extract_boundary_conditions(diffusivity_fields))

    # Next, we form a list of default boundary conditions:
    prognostic_field_names = (:u, :v, :w, tracernames(tracers)...)
    default_boundary_conditions = NamedTuple{prognostic_field_names}(FieldBoundaryConditions() for name in prognostic_field_names)

    # Finally, we merge specified, embedded, and default boundary conditions. Specified boundary conditions
    # have precedence, followed by embedded, followed by default.
    boundary_conditions = merge(default_boundary_conditions, embedded_boundary_conditions, boundary_conditions)
    boundary_conditions = regularize_field_boundary_conditions(boundary_conditions, grid, prognostic_field_names)

    # Ensure `closure` describes all tracers
    closure = with_tracers(tracernames(tracers), closure)

    # Either check grid-correctness, or construct tuples of fields
    velocities         = VelocityFields(velocities, grid, boundary_conditions)
    tracers            = TracerFields(tracers,      grid, boundary_conditions)
    pressures          = PressureFields(pressures,  grid, boundary_conditions)
    diffusivity_fields = DiffusivityFields(diffusivity_fields, grid, tracernames(tracers), boundary_conditions, closure)

    if isnothing(pressure_solver)
        pressure_solver = PressureSolver(arch, grid)
    end

    # Materialize background fields
    background_fields = BackgroundFields(background_fields, tracernames(tracers), grid, clock)

    # Instantiate timestepper if not already instantiated
    implicit_solver = implicit_diffusion_solver(time_discretization(closure), arch, grid)
    timestepper = TimeStepper(timestepper, grid, tracernames(tracers), implicit_solver=implicit_solver)

    # Regularize forcing for model tracer and velocity fields.
    model_fields = merge(velocities, tracers)
    forcing = model_forcing(model_fields; forcing...)

    model = NonhydrostaticModel(arch, grid, clock, advection, buoyancy, coriolis, stokes_drift,
                                forcing, closure, background_fields, particles, velocities, tracers,
                                pressures, diffusivity_fields, timestepper, pressure_solver, immersed_boundary,
                                auxiliary_fields)

    update_state!(model)

    return model
end

architecture(model::NonhydrostaticModel) = model.architecture

#####
##### Recursive util for building NamedTuples of boundary conditions from NamedTuples of fields
#####
##### Note: ignores tuples, including tuples of Symbols (tracer names) and
##### tuples of DiffusivityFields (which occur for tupled closures)
#####

extract_boundary_conditions(::Nothing) = NamedTuple()
extract_boundary_conditions(::Tuple) = NamedTuple()

function extract_boundary_conditions(field_tuple::NamedTuple)
    names = propertynames(field_tuple)
    bcs = Tuple(extract_boundary_conditions(field) for field in field_tuple)
    return NamedTuple{names}(bcs)
end

extract_boundary_conditions(field::Field) = field.boundary_conditions


abstract type MSM_Timeseries{T<:AbstractFloat} end

struct MSM_Timeseries_Point{T<:AbstractFloat} <: MSM_Timeseries{T}
    x::AbstractArray{T,2}
    residuals::AbstractArray{T,3}
    timesteps::AbstractArray{T,1}

    MSM_Timeseries_Point{T}(x::AbstractArray{T,2},
        residuals::AbstractArray{T,3},
        timesteps::AbstractArray{T,1}) where T <: Real =
            new(x,residuals,timesteps)
end

struct MSM_Timeseries_Dist{T<:AbstractFloat} <: MSM_Timeseries{T}
    x::AbstractArray{T,3}
    residuals::AbstractArray{T,4}
    timesteps::AbstractArray{T,1}

    ts_points::Array{MSM_Timeseries_Point{T},1}

    MSM_Timeseries_Dist{T}(x::AbstractArray{T,3},
        residuals::AbstractArray{T,4},
        timesteps::AbstractArray{T,1}) where T <: Real =
        new(x,residuals,timesteps,
            [MSM_Timeseries_Point{T}(
                view(x,:,:,i),
                view(residuals,:,:,:,i),
                view(timesteps,:))
                for i in 1:size(x,3)
            ])
end

function MSM_Timeseries_Point{T}(num_obs::Integer, num_params::Integer,
    num_layers::Integer, timesteps::Array{T,1}) where T <: Real

    MSM_Timeseries_Point{T}(Array{T}(undef,num_obs, num_params),
        Array{T}(undef, num_obs, num_params, num_layers), timesteps)
end

function MSM_Timeseries_Point{T}(num_obs::Integer, num_params::Integer,
    num_layers::Integer, timeStep::T = one(T)) where T <: Real

    MSM_Timeseries_Point{T}(num_obs, num_params, num_layers,
        repeat([timeStep], num_obs))
end

MSM_Timeseries_Point{T}(timeseries::MSM_Timeseries_Dist{T},
    aggregate_fun::Function)  where T <: Real =
        MSM_Timeseries_Point{T}(
            mapslices(aggregate_fun, timeseries.x; dims=(3))[:,:,1],
            mapslices(aggregate_fun, timeseries.residuals; dims=(4))[:,:,:,1],
            timeseries.timesteps
        )

MSM_Timeseries_Point{T}(timeseries::Array{MSM_Timeseries_Dist{T},1},
    aggregate_fun::Function)  where T <: Real =
                [MSM_Timeseries_Point{T}(ts,aggregate_fun)
                    for ts in timeseries]

function MSM_Timeseries_Dist{T}(num_obs::Integer, num_params::Integer,
    num_layers::Integer, num_samples::Integer, timesteps::Array{T,1}) where T <: Real

    MSM_Timeseries_Dist{T}(Array{T}(undef,num_obs, num_params, num_samples),
        Array{T}(undef, num_obs, num_params, num_layers, num_samples), timesteps)
end

function MSM_Timeseries(timeseries::S,
    num_preds::Integer,
    timeStep::T = one(T),
    num_samples::Integer = 1) where S <: MSM_Timeseries{T} where T<:Real

    MSM_Timeseries(S, params(timeseries),
        layers(timeseries), repeat([timeStep], num_preds), num_samples)
end

function MSM_Timeseries(timeseries::S,
    timeSteps::Array{T,1},
    num_samples::Integer = 1) where S <: MSM_Timeseries{T} where T<:Real

    MSM_Timeseries(S, params(timeseries),
        layers(timeseries), timeSteps, num_samples)
end

MSM_Timeseries(::Type{MSM_Timeseries_Point{T}},
    num_params::Integer, num_layers::Integer, timeSteps::Array{T,1},
    num_samples::Integer = 1) where T <: Real =
    ifelse(num_samples == 1,
        MSM_Timeseries_Point{T}(
            length(timeSteps),
            num_params,
            num_layers,
            timeSteps),
        MSM_Timeseries_Dist{T}(length(timeSteps),
            num_params,
            num_layers,
            num_samples,
            timeSteps
            )
    )

    MSM_Timeseries(::Type{MSM_Timeseries_Dist{T}},
        num_params::Integer, num_layers::Integer, timeSteps::Array{T,1},
        num_samples::Integer = 1) where T <: Real =
            MSM_Timeseries_Dist{T}(length(timeSteps),
                num_params,
                num_layers,
                num_samples,
                timeSteps
                )

timesteps(ts::S) where {S<:MSM_Timeseries} = ts.timesteps
values(ts::S) where {S<:MSM_Timeseries} = ts.x
residuals(ts::S) where {S<:MSM_Timeseries}  = ts.residuals

Base.length(ts::S) where {S<:MSM_Timeseries} = size(values(ts), 1)

params(ts::S) where {S<:MSM_Timeseries} = size(values(ts), 2)
layers(ts::S) where {S<:MSM_Timeseries} = size(residuals(ts), 3)
samples(ts::MSM_Timeseries_Dist) = size(values(ts),3)

function Base.copy(ts::S) where {S<:MSM_Timeseries{T}} where {T <:Real}
    S(copy(ts.x), copy(ts.residuals), copy(ts.timesteps))
end

function DataFrame(ts::MSM_Timeseries_Point)
    df = DataFrames.DataFrame()

    for p in 1:params(ts)
        df[!,Symbol("x-" * string(p))] = ts.x[:,p]
    end

    for p in 1:params(ts)
        for l in 1:layers(ts)
            df[!,Symbol("r-" * string(p) * "-" * string(l))] =
                ts.residuals[:,p,l]
        end
    end

    df[!,Symbol("t")] = accumulate(+, ts.timesteps) .- ts.timesteps[1]

    df
end

function DataFrame(ts::MSM_Timeseries_Dist)
    DataFrame.(ts.ts_points)
end

write(output::String, ts::MSM_Timeseries_Point) =
    CSV.write(output * ".csv", DataFrame(ts))

function write(output::String, ts::MSM_Timeseries_Dist)
    dfs = DataFrame(ts)

    for i in 1:length(dfs)
        CSV.write(output * "-" * string(i) * ".csv", dfs[i])
    end
end


# cd("C:\\Users\\pi96doc\\Documents\\Programming\\Julia\\OrthoView.jl\\")
# ]activate .
## interactive example:
module OrthoView
export ortho!, test_ortho_view

using GLMakie
using GLMakie.FileIO
using Printf
using ColorTypes  # for RGB type
using ColorSchemes
using PerceptualColourMaps

function get_crosshair_xs(px, sx, gap)
    xs = [0f0,px-gap,px+gap,sx,px,px,px,px]
    return xs
end
function get_crosshair_ys(py, sy, gap)
    gap = Float32(to_value(gap))
    ys = [py,py,py,py,0f0,py-gap,py+gap,sy]
    return ys
end

function crosshair(sl_x,sl_y, sz; markersize=0.7, color=:red)
    if isa(markersize, Slider)
        cross_xs = @lift(get_crosshair_xs($(sl_x.value), sz[1], Float32($(markersize.value))))
        cross_ys = @lift(get_crosshair_ys($(sl_y.value), sz[2], Float32($(markersize.value))))
    else
        gap = Float32(to_value(markersize))
        cross_xs = @lift(get_crosshair_xs($(sl_x.value), sz[1], gap))
        cross_ys = @lift(get_crosshair_ys($(sl_y.value), sz[2], gap))
    end
    linesegments!(cross_xs, cross_ys, color=color, linewidth=3)
end

function fit_into(start, stop, sz, do_limit=false)
    if start<0
        stop -= start; start=0;
    end
    if start>sz+1
        stop -= start-(sz+1); start = sz+1;
    end
    if stop<0
        start -= stop; stop=0;
    end
    if stop>sz+1
        start -= stop-(sz+1); stop=sz+1;
    end
    if abs(stop-start)+ 1 >sz
        delta = sign(stop-start)*(abs(stop-start) - (sz))
        start += delta/2
        stop += delta/2
    end
    if do_limit
        return clamp(start,-0.5,sz), clamp(stop,-0.5,sz)
    else
        return start, stop
    end
end

function do_zoom(ax_xy, ctr, sz, abs_zoom, aspects=(1,1,1), indices=(1,2))
    if isnothing(ax_xy)
        return
    end
    ix,iy = abs.(indices)
    sz2d = (sz[ix],sz[iy])
    lx,ly = to_value(ax_xy.limits)
    sx,sy = ax_xy.layoutobservables.computedbbox.val.widths  # The size in pixels that is available
    if isnothing(lx)
        return Consume(true)
    end
    c = (to_value(ctr) .- 0.5) # .* aspects
    c = (c[ix], c[iy])
    half_width = sign(indices[1])*abs_zoom .* sx / aspects[ix] / 2
    nx_start, nx_stop = fit_into(c[1] - half_width, c[1] + half_width, sz2d[1], false)
    xlims!(ax_xy,nx_start,nx_stop)
    half_width = sign(indices[2])*abs_zoom .* sy / aspects[iy] / 2
    ny_start, ny_stop = fit_into(c[2] - half_width, c[2] + half_width, sz2d[2], false)
    ylims!(ax_xy,ny_start,ny_stop)
end

function get_max_zoom(sz, ax, ax_zy, ax_xz)
    abs_zoom_x = sz[1] / ax.layoutobservables.computedbbox.val.widths[1]
    abs_zoom_y = sz[2] / ax.layoutobservables.computedbbox.val.widths[2]
    if length(sz) > 2 && !isnothing(ax_zy) && !isnothing(ax_xz)
        abs_zoom_z = sz[3] / ax_zy.layoutobservables.computedbbox.val.widths[1]
        abs_zoom_z2 = sz[3] / ax_xz.layoutobservables.computedbbox.val.widths[2]
        return max(abs_zoom_x,abs_zoom_y, abs_zoom_z, abs_zoom_z2)
    else
        return max(abs_zoom_x,abs_zoom_y)
    end
end

function zoom_all(abs_zoom, event, ax, ax_xz, ax_zy, aspects, ctr, sz)
    if !isnothing(event)
        zoom_dir = to_value(event.y)
        zoom_max = get_max_zoom(sz .* aspects, ax, ax_zy, ax_xz)
        abs_zoom = min(abs_zoom * 1.1 ^ -zoom_dir, zoom_max)
    end
    do_zoom(ax, ctr, sz, abs_zoom, aspects, (1,-2))
    do_zoom(ax_xz, ctr, sz, abs_zoom, aspects, (1,-3))
    do_zoom(ax_zy, ctr, sz, abs_zoom, aspects, (3,-2))
    return abs_zoom
end

function on_color_triangle(cb)
    scene = cb.parent.scene
    mouse = scene.events.mouseposition[]
    c_start = cb.layoutobservables.suggestedbbox.val.origin
    c_stop = cb.layoutobservables.suggestedbbox.val.origin .+ cb.layoutobservables.suggestedbbox.val.widths
    d_top = abs(mouse[2] .- c_start[2])
    d_buttom = abs(mouse[2] .- c_stop[2])
    if !(all(mouse.>c_start) && all(mouse .<c_stop))
        return 0;
    elseif (d_top < 20)
        return -1;
    elseif (d_buttom < 20)
        return 1;
    else
        return 0;
    end
end

function in_colorbar(cb)
    scene = cb.parent.scene
    mouse = scene.events.mouseposition[]
    c_start = cb.layoutobservables.suggestedbbox.val.origin
    c_stop = cb.layoutobservables.suggestedbbox.val.origin .+ cb.layoutobservables.suggestedbbox.val.widths
    return (all(mouse.>c_start) && all(mouse .<c_stop))
end

function register_colorbar_scroll!(cb, colorrange, default_range=(0.0,1.0), high_col=:red, low_col=:blue)
    scene = cb.parent.scene
    on(events(scene).scroll, priority = 2) do event
        if ! (in_colorbar(cb))
            return Consume(false)
        end
        c_min,c_max = to_value(colorrange)
        zoom_dir = to_value(event[2]) # event.y
        c_rng = (c_max - c_min) * 1.1 ^ -zoom_dir
        if ispressed(scene,Mouse.middle) || ispressed(scene, Keyboard.left_shift) || ispressed(scene, Keyboard.right_shift) || (on_color_triangle(cb) == -1)
            c_min = c_max - c_rng
        else
            c_max = c_min + c_rng
        end
        colorrange[] = (c_min,c_max)
        return Consume(true) # for now prevent the zoom
    end
    on(events(scene).mousebutton, priority = 2) do event
        if ! (in_colorbar(cb))
            return Consume(false)
        end
        if event.button == Mouse.left && event.action == Mouse.release
            if on_color_triangle(cb) == 1
                if isnothing(to_value(cb.highclip))
                    cb.highclip[] = high_col
                else
                    cb.highclip[] = nothing
                end
                return Consume(true) # for now prevent the zoom
            end
            if on_color_triangle(cb) == -1
                if isnothing(to_value(cb.lowclip))
                    cb.lowclip[] = low_col
                else
                    cb.lowclip[] = nothing
                end
                return Consume(true) # for now prevent the zoom
            end
            if ispressed(scene, Keyboard.left_shift) || ispressed(scene, Keyboard.right_shift)
                max_r = max(default_range...)
                colorrange[] = (-max_r,max_r)
            elseif ispressed(scene, Keyboard.left_control) || ispressed(scene, Keyboard.left_control)
                colorrange[] = (0.0,default_range[2])
            else
                colorrange[] = default_range
            end
        end
        return Consume(true) # for now prevent the zoom
    end
end

function register_panel_zoom_link!(ax, ctr, sz, ax_xz=nothing, ax_zy=nothing; aspects=ones(length(sz)))

    abs_zoom = get_max_zoom(sz.*aspects, ax, ax_zy, ax_xz)
    zoom_all(abs_zoom, nothing, ax, ax_xz, ax_zy, aspects, ctr, sz)
    register_interaction!(ax, :my_scroll_interaction) do event::ScrollEvent, axis
        abs_zoom = zoom_all(abs_zoom, event, ax, ax_xz, ax_zy, aspects, ctr, sz)
        return Consume(true) # for now prevent the zoom
    end
    if !isnothing(ax_xz)
        register_interaction!(ax_xz, :my_scroll_interaction) do event::ScrollEvent, axis
            abs_zoom = zoom_all(abs_zoom, event, ax, ax_xz, ax_zy, aspects, ctr, sz)
            return Consume(true) # for now prevent the zoom
        end
    end
    if !isnothing(ax_zy)
        register_interaction!(ax_zy, :my_scroll_interaction) do event::ScrollEvent, axis
            abs_zoom = zoom_all(abs_zoom, event, ax, ax_xz, ax_zy, aspects, ctr, sz)
            return Consume(true) # for now prevent the zoom
        end
    end
end

function get_text(name, pos, data, aspects)
    d = data[pos...];
    sz = size(data)
    rel_pos = pos .- (sz .÷2 .+1)
    str = "$(name): "
    if isa(d, Complex)
        str *= "$(@sprintf("%.3f + %.3fi",real(d),imag(d)))\n"
    else
        if isa(d, Real)
            str *= "$(@sprintf("%.3f",d)),\n"
        else
            if isa(d, Tuple)
                d = round.(data[pos...]; digits=3)
                str *= "$(d),\n"
            elseif isa(d,RGB)
                str *= "RGB: $(@sprintf("%.2f",d.r)), $(@sprintf("%.2f",d.g)), $(@sprintf("%.2f",d.b)),\n"
            else
                str *= "$(d),\n"
            end
        end
    end
    str *= "pos: $(pos)\nctr: $(rel_pos)\nsc. ctr: $(rel_pos .* aspects)\nsz:$(size(data))"

end

function register_panel_interactions!(ax, sl_x, sl_y, sl_z, ref_ax; key_buffer="")
    deregister_interaction!(ax, :rectanglezoom)
    sz = (max(to_value(sl_x.range)[1],to_value(sl_x.range)[end]), max(to_value(sl_y.range)[1],to_value(sl_y.range)[end])) # (to_value(ax.limits)[1][2] , to_value(ax.limits)[2][2])
    ctr = sz .÷ 2 .+ 1
    # pos = Node([Point2f0(0)])

    register_interaction!(ax, :my_mouse_interaction) do event::MouseEvent, axis
        if event.type === MouseEventTypes.leftclick || event.type === MouseEventTypes.leftdragstart ||
            event.type === MouseEventTypes.leftdragstop || event.type === MouseEventTypes.leftdrag ||
            event.type === MouseEventTypes.middledragstart || event.type === MouseEventTypes.middledragstop || event.type === MouseEventTypes.middledrag
            set_close_to!(sl_x, event.data[1] )
            set_close_to!(sl_y, event.data[2] )
            ref_ax[1] = ax
        end
    end

    register_interaction!(ax, :my_key_interaction) do event::KeysEvent, axis
        if ref_ax[1] != ax
            return
        end
        if Keyboard.left in event.keys
            set_close_to!(sl_x, to_value(sl_x.value) - 1f0)
        end
        if Keyboard.right in event.keys
            set_close_to!(sl_x, to_value(sl_x.value) + 1f0)
        end
        if Keyboard.up in event.keys
            set_close_to!(sl_y, to_value(sl_y.value) - 1f0)
        end
        if Keyboard.down in event.keys
            set_close_to!(sl_y, to_value(sl_y.value) + 1f0)
        end
        if Keyboard.home in event.keys
            set_close_to!(sl_x, ctr[1])
            set_close_to!(sl_y, ctr[2])
        end
        if !isnothing(sl_z)
            if Keyboard.page_up in event.keys
                set_close_to!(sl_z, to_value(sl_z.value) - 1f0)
            end
            if Keyboard.page_down in event.keys
                set_close_to!(sl_z, to_value(sl_z.value) + 1f0)
            end
            if Keyboard.enter in event.keys
                try
                    set_close_to!(sl_z, parse(Float32,key_buffer))
                    println(key_buffer)
                catch ev
                end
                key_buffer = ""
            end
            Keyboard._0 in event.keys && (key_buffer *= '0')
            Keyboard._1 in event.keys && (key_buffer *= '1')
            Keyboard._2 in event.keys && (key_buffer *= '2')
            Keyboard._3 in event.keys && (key_buffer *= '3')
            Keyboard._4 in event.keys && (key_buffer *= '4')
            Keyboard._5 in event.keys && (key_buffer *= '5')
            Keyboard._6 in event.keys && (key_buffer *= '6')
            Keyboard._7 in event.keys && (key_buffer *= '7')
            Keyboard._8 in event.keys && (key_buffer *= '8')
            Keyboard._9 in event.keys && (key_buffer *= '9')
        end
    end
end

function get_pos(pos)
        Tuple(round(Int,p) for p in pos)
end

function apply_min_max_gamma(min, max, gamma)
    (dat) -> ((dat - min) / (max-min))^gamma
end

function get_slice(data, pos, dims=(1,2), min_max_val=extrema(data))
    idx = Tuple((d in dims) ? Colon() : pos[d] for d = 1:length(pos))
    if dims[1] > dims[2]
        res = transpose(@view data[idx...])
    else
        res = @view data[idx...]
    end
    if isa(res[1],Complex)
        res = abs2.(res)
    end
    # res = map(apply_min_max_gamma(min_max_val[1], min_max_val[2], 1.0f0), res)
    return res
end

function obs_from_sliders(sls)
    sz = length(sls)
    if sz == 0
        return nothing
    elseif sz == 1
        position = @lift(get_pos(($(sls[1].value))))
    elseif sz == 2
        position = @lift(get_pos(($(sls[1].value), $(sls[2].value))))
    elseif sz == 3
        position = @lift(get_pos(($(sls[1].value), $(sls[2].value), $(sls[3].value))))
    elseif sz == 4
        position = @lift(get_pos(($(sls[1].value), $(sls[2].value), $(sls[3].value), $(sls[4].value))))
    elseif sz == 5
        position = @lift(get_pos(($(sls[1].value), $(sls[2].value), $(sls[3].value), $(sls[4].value), $(sls[5].value))))
    else
        error("a maximum of 5 extra dimensions is allowed")
    end
    position
end

function apply_gamma(rgba, gamma)
    ColorTypes.RGBA(rgba.r^gamma, rgba.g^gamma, rgba.b^gamma, rgba.alpha)
end

function colormap_line(subfig)
    col_options = ["L1","L2","L3","L4","C1","C2","C3","R1","R2","R3"]
    N_cmap = 2048
    my_rawmap = Node(RGBA{Float32}.(cmap("L1", N=N_cmap)))

    lp_menu = subfig[] = GridLayout()
    ## fill the lower menu bar with content:
    @time menu = Menu(lp_menu[1,1], options = col_options, label="Color", prompt="Colormap") # 380 MiB just the menu ...
    on(menu.selection) do s
        my_rawmap[] = RGBA{Float32}.(cmap(s, N=N_cmap))
    end
    # Gamma slider
    Label(lp_menu[1,2],"Gamma:", halign = :left, valign = :center) # 5 Mb
    sg = Slider(lp_menu[1,3], range = -2:0.1:2, horizontal = true, startvalue = 0.0, align=Inside(), label="Gamma")
    gamma_txt = @lift( "$(@sprintf("%.2f",10.0^$(sg.value)))")
    Label(lp_menu[1,4],gamma_txt, halign = :left, valign = :center)
    gamma = @lift(Float32(10f0 ^ $(sg.value)))
    return @lift(apply_gamma.(to_value($(my_rawmap)), to_value($(gamma))))
end

# Memory usage:  Menu: 380 Mb
# Slider, laber, lift: each 5Mb
# hidedecorations: 132 Mb
# axis: 27 Mb
# heatmap: 2 Mb
# crosshair: 0.26 Mb
# Colorbar: 48 Mb
# colsize!: 8.6 Mb
# rowsize!: 8.6 Mb
# xlims!: 1 Mb
# ylims!: 2 Mb

function ortho!(fig, myim; title = "Image", color=:red, markersize = 40.0, aspects=ones(ndims(myim)), colorbar=true)
    sz = size(myim)

    # arguments to hide axes and disable zooming
    hideaxisargs = Dict(:xrectzoom => false, :yrectzoom=>false, :titlevisible=>false,
    :xgridvisible=>false, :ygridvisible=>false,
    :xlabelvisible=>false, :ylabelvisible=>false,
    :bottomspinevisible=>false, :leftspinevisible=>false, :topspinevisible=>false ,:rightspinevisible=>false,
    :xticklabelsvisible=>false, :yticklabelsvisible=>false,
    :xticksvisible=>false, :yticksvisible=>false)

    fig = if isa(fig, Figure)
        my_layout = fig[1,1] = GridLayout()
    else
        my_layout = fig[] = GridLayout()
    end

    show_cbar = colorbar

    ax_im = ax_xz = ax_zy = nothing
    sl_z = nothing

    if ndims(myim) > 2 && sz[3] > 1
        my_cmap = colormap_line(fig[3,1:3]) # 554 Mb
    else
        my_cmap = colormap_line(fig[3,1:2])
    end
    colorrange = Node((0.0,1.0))

    if ndims(myim) > 2 && sz[3] > 1
        # grid_size = 3
        sl_x = Slider(fig[1,1,Bottom()], range = 1:sz[1], horizontal = true, startvalue = sz[1]/2+1)
        sl_y = Slider(fig[1,1,Right()], range = sz[2]:-1:1, horizontal = false, startvalue = sz[2]/2+1)
        sl_z = Slider(fig[1,2,Bottom()], range = 1:sz[3], horizontal = true, startvalue = sz[3]/2+1)

        if ndims(myim) > 8
            error("Maximal number of dimensions exceeded.")
        end

        sl_o = Tuple(Slider(fig[1:2,2+show_cbar+d], range = sz[3+d]:-1:1, horizontal = false, startvalue = 1) for d=1:ndims(myim)-3)
        pos_o = obs_from_sliders(sl_o)
        ax_im_xz = Axis(fig[2,1], yreversed = true, alignmode = Inside(); hideaxisargs...) #

        if isnothing(pos_o)
            myim_xz = @lift(collect(get_slice(myim, (sl_x.value, $(sl_y.value), sl_z.value), (1,3))))
            myim_zy = @lift(collect(get_slice(myim, ( $(sl_x.value), sl_y.value, sl_z.value), (3,2))))
            myim_xy = @lift(collect(get_slice(myim, (sl_x.value, sl_y.value, $(sl_z.value)), (1,2))))
            position = @lift(get_pos(($(sl_x.value), $(sl_y.value), $(sl_z.value))))
        else
            myim_xz = @lift(collect(get_slice(myim, (sl_x.value, $(sl_y.value), sl_z.value, $(pos_o)...), (1,3))))
            myim_zy = @lift(collect(get_slice(myim, ( $(sl_x.value), sl_y.value, sl_z.value, $(pos_o)...), (3,2))))
            myim_xy = @lift(collect(get_slice(myim, (sl_x.value, sl_y.value, $(sl_z.value), $(pos_o)...), (1,2))))
            position = @lift(get_pos(($(sl_x.value), $(sl_y.value), $(sl_z.value), $(pos_o)...)))
        end
        im_xz = heatmap!(myim_xz, interpolate=false, highclip=:red, lowclip=:blue, colormap=my_cmap)
        xlims!(0,sz[1])
        ylims!(sz[3],0) # no reverse
        crosshair(sl_x,sl_z, (sz[1],sz[3]), color=color)

        ax_im_zy = Axis(fig[1,2], yreversed = true, alignmode = Inside(); hideaxisargs...) #
        im_zy = heatmap!(myim_zy, interpolate=false, highclip=:red, lowclip=:blue, colormap=my_cmap)
        xlims!(0,sz[3])
        ylims!(sz[1],0) # no reverse
        crosshair(sl_z,sl_y, (sz[3],sz[2]), color=color)

        ax_im = Axis(fig[1,1], yreversed = true, alignmode = Inside(); hideaxisargs...)
        im = heatmap!(myim_xy, interpolate=false, highclip=:red, lowclip=:blue, colormap=my_cmap)
        xlims!(0,sz[1])
        ylims!(sz[2],0) # reverse y !
        crosshair(sl_x,sl_y, (sz[1],sz[2]), color=color)

        colorrange[]=(
            min(to_value(im.attributes.colorrange)[1],to_value(im_zy.attributes.colorrange)[1],to_value(im_xz.attributes.colorrange)[1]),
            max.(to_value(im.attributes.colorrange)[2],to_value(im_zy.attributes.colorrange)[2],to_value(im_xz.attributes.colorrange)[2]))

        im.attributes.colorrange=colorrange
        im_xz.attributes.colorrange=colorrange
        im_zy.attributes.colorrange=colorrange

        ref_ax = [ax_im]

        txt = @lift(get_text(title, to_value($(position)), myim, aspects))
        my_label = Label(fig[2,2:2+show_cbar],txt, halign = :left, valign = :top, tellheight=false) #

        if show_cbar
            # label = "Brightness",
            cbar = Colorbar(fig[1,3], im, alignmode = Outside(), width=15, ticklabelspace=25f0)
            register_colorbar_scroll!(cbar, colorrange, to_value(colorrange))
        end
        r_ratio = Auto(aspects[3]*sz[3]/(aspects[2]*sz[2]))
        c_ratio = Auto(aspects[3]*sz[3]/(aspects[1]*sz[1]))

        colsize!(fig, 2, c_ratio)
        rowsize!(my_layout, 2, r_ratio)
        rowsize!(my_layout, 3, 30)

        register_panel_interactions!(ax_im_xz, sl_x, sl_z, sl_y, ref_ax)
        register_panel_interactions!(ax_im_zy, sl_z, sl_y, sl_x, ref_ax)
        register_panel_zoom_link!(ax_im, position, sz, ax_im_xz, ax_im_zy, aspects=aspects)
    else
        ax_im = Axis(fig[1,1], title=title, yreversed = true, alignmode = Inside(); hideaxisargs...)
        sl_x = Slider(fig[1,1, Bottom()], range = 1:sz[1], horizontal = true, startvalue = sz[1]/2+1)
        sl_y = Slider(fig[1,1, Right()], range = sz[2]:-1:1, horizontal = false, startvalue = sz[2]/2+1)
        sl_o = Tuple(Slider(fig[1:2,1+d+show_cbar], range = sz[3+d]:-1:1, horizontal = false, startvalue = 1) for d=1:ndims(myim)-3)
        pos_o = obs_from_sliders(sl_o)
        # @time hidedecorations!(ax_im, grid = false);
        if isnothing(pos_o)
            myim_xy = collect(get_slice(myim, (sl_x.value, sl_y.value, 1), (1,2)))
            if ndims(myim) == 3
                position = @lift(get_pos(($(sl_x.value), $(sl_y.value),1)))
            else
                position = @lift(get_pos(($(sl_x.value), $(sl_y.value))))
            end
        else
            myim_xy = @lift(collect(get_slice(myim, (sl_x.value, sl_y.value, 1, $(pos_o)...), (1,2)))) # is needed due to other positions
            position = @lift(get_pos(($(sl_x.value), $(sl_y.value), 1, $(pos_o)...)))
        end
        im = heatmap!(myim_xy, interpolate=false, highclip=:red, lowclip=:blue, colormap=my_cmap)
        xlims!(0,sz[1])
        ylims!(sz[2],0) # reverse y !
        crosshair(sl_x,sl_y, sz[1:2], color=color)

        colorrange[] = to_value(im.attributes.colorrange)
        im.attributes.colorrange=colorrange

        ref_ax = [ax_im]
        txt = @lift(get_text(title, to_value($(position)), myim, aspects))
        my_label = Label(fig[2,1:1+show_cbar],txt, halign = :left, valign = :top)

        if show_cbar
            # label = "Brightness",
            cbar = Colorbar(fig[1,2], im, alignmode = Outside(), width=20, ticklabelspace=30f0)
            register_colorbar_scroll!(cbar, colorrange, to_value(colorrange))
        end

        register_panel_zoom_link!(ax_im, position, sz, nothing, nothing, aspects=aspects)

    end

    register_panel_interactions!(ax_im, sl_x, sl_y, sl_z, ref_ax)

    abs_zoom = get_max_zoom(sz.*aspects, ax_im, ax_zy, ax_xz)
    zoom_all(abs_zoom, nothing, ax_im, ax_xz, ax_zy, aspects, sz .÷ 2 .+1, sz)

    return fig # , grid_size
end

function ortho!(myim; preferred_size = 600, title = "Image", color=:red, markersize = 40.0, aspects=ones(ndims(myim)), colorbar=true)
    sz = size(myim) .* aspects
    if length(sz) > 2 && sz[3] != 1
        r_ratio = sz[3]/sz[2] # these ratios will be used in the ortho! function to specify the relative size
        c_ratio = sz[3]/sz[1]
        sz = sz[1:3]
        fak = preferred_size ./ max(sz...)
        sz3 = max(sz[3],160/fak)
        res = fak .* (sz[1]+sz3, sz[2]+sz[3]) ./ 2 .+ (80*colorbar,70)
    else
        fak = preferred_size ./ max(sz...)
        res = fak .* (sz[1], sz[2])  .+ (30*colorbar,160) # additional space for cbar and text
    end
    fig = Figure(resolution=res)
    ortho!(fig, myim; title = title,  color=color, markersize = markersize, aspects=aspects, colorbar=colorbar)
    return fig
end

function test_ortho_view(;aspects=(1,1,1,1))
    set_theme!(theme_black())
    obj = rand(10,20,30,40) # testimage("simple_3d_ball.tif")
    ortho!(obj, aspects=aspects)
end

end


module contactForceLaw_ballWithBall

using Modia3D
using Modia3D.StaticArrays
import Modia3D.ModiaMath

vmat1 = Modia3D.Material(color="LightBlue" , transparency=0.5)    # material of SolidFileMesh
vmat11 = Modia3D.Material(color="Red" , transparency=0.5)
vmat2 = deepcopy(vmat1)                                           # material of convex decomposition of SolidFileMesh
vmat2.transparency = 0.7

cmat = "Steel"

@assembly BallWithBall begin
  world = Modia3D.Object3D(visualizeFrame=true)

  sphereMoving     = Modia3D.Object3D(world, Modia3D.Solid(Modia3D.SolidSphere(0.5) , "Steel", vmat11; contactMaterial = cmat); r=[0.0, 0.0, 0.0],  fixed=false ) # , R=ModiaMath.rot2(-pi/2) )

  sphere     = Modia3D.Object3D(world, Modia3D.Solid(Modia3D.SolidSphere(1.0) , "Steel", vmat1; contactMaterial = cmat); r=[-3.0, 0.0, 0.0], fixed=true) #, R=ModiaMath.rot2(-pi/3) )
end

gravField = Modia3D.UniformGravityField(g=9.81, n=[-1,0,0])
threeD = BallWithBall(sceneOptions=Modia3D.SceneOptions(gravityField=gravField,visualizeFrames=true, defaultFrameLength=0.7,nz_max = 100, enableContactDetection=true, visualizeContactPoints=true, visualizeSupportPoints=true))

model = Modia3D.SimulationModel( threeD )
result = ModiaMath.simulate!(model; stopTime=10.0, tolerance=1e-8,interval=0.001, log=false)

println("... success of contactForceLaw_ballWithBall.jl!")
end


# Check that we can read the official HDF5 example files
urlbase = "http://www.hdfgroup.org/ftp/HDF5/examples/files/exbyapi/"

using HDF5

fcmp = [0 1 2 3 4 5 6;
    2 1.66667 2.4 3.28571 4.22222 5.18182 6.15385;
    4 2.33333 2.8 3.57143 4.44444 5.36364 6.30769;
    6 3 3.2 3.85714 4.66667 5.54545 6.46154]'
icmp = [0 -1 -2 -3 -4 -5 -6;
    0 0 0 0 0 0 0;
    0 1 2 3 4 5 6;
    0 2 4 6 8 10 12]'
scmp = ["Parting", "is such", "sweet", "sorrow."]
vicmp = Array{Int32}[[3,2,1],[1,1,2,3,5,8,13,21,34,55,89,144]]
opq = Array{Uint8}[[0x4f, 0x50, 0x41, 0x51, 0x55, 0x45, 0x30],
                   [0x4f, 0x50, 0x41, 0x51, 0x55, 0x45, 0x31],
                   [0x4f, 0x50, 0x41, 0x51, 0x55, 0x45, 0x32],
                   [0x4f, 0x50, 0x41, 0x51, 0x55, 0x45, 0x33]]

const savedir = joinpath(tempdir(), "h5")
if !isdir(savedir)
    mkdir(savedir)
end
function getfile(name)
    file = joinpath(savedir, name)
    if !isfile(file)
        file = download(urlbase*name, file)
    end
    file
end

file = getfile("h5ex_t_floatatt.h5")
fid = h5open(file, "r")
dset = fid["DS1"]
a = a_read(dset, "A1")
@assert norm(a - fcmp) < 1e-5
close(fid)

file = getfile("h5ex_t_float.h5")
fid = h5open(file, "r")
d = read(fid, "DS1")
@assert norm(d - fcmp) < 1e-5
close(fid)

file = getfile("h5ex_t_intatt.h5")
fid = h5open(file, "r")
dset = fid["DS1"]
a = a_read(dset, "A1")
@assert a == icmp
close(fid)

file = getfile("h5ex_t_int.h5")
fid = h5open(file, "r")
d = read(fid, "DS1")
@assert d == icmp
close(fid)

file = getfile("h5ex_t_objrefatt.h5")
fid = h5open(file, "r")
dset = fid["DS1"]
a = a_read(dset, "A1")
g = fid[a[1]]
@assert isa(g, HDF5Group)
ds2 = fid[a[2]]
ds2v = read(ds2)
@assert isa(ds2v, Array{Int32})
@assert isempty(ds2v)
close(fid)

file = getfile("h5ex_t_objref.h5")
fid = h5open(file, "r")
d = read(fid, "DS1")
g = fid[d[1]]
@assert isa(g, HDF5Group)
ds2 = fid[d[2]]
ds2v = read(ds2)
@assert isa(ds2v, Array{Int32})
@assert isempty(ds2v)
close(fid)

file = getfile("h5ex_t_stringatt.h5")
fid = h5open(file, "r")
dset = fid["DS1"]
a = a_read(dset, "A1")
@assert a == scmp
close(fid)

file = getfile("h5ex_t_string.h5")
fid = h5open(file, "r")
d = read(fid, "DS1")
@assert d == scmp
close(fid)

file = getfile("h5ex_t_vlenatt.h5")
fid = h5open(file, "r")
dset = fid["DS1"]
a = a_read(dset, "A1")
@assert a == vicmp
close(fid)

file = getfile("h5ex_t_vlen.h5")
fid = h5open(file, "r")
d = read(fid, "DS1")
@assert d == vicmp
close(fid)

file = getfile("h5ex_t_vlstringatt.h5")
fid = h5open(file, "r")
dset = fid["DS1"]
a = a_read(dset, "A1")
@assert a == scmp
close(fid)

file = getfile("h5ex_t_vlstring.h5")
fid = h5open(file, "r")
d = read(fid, "DS1")
@assert d == scmp
close(fid)

file = getfile("h5ex_t_opaqueatt.h5")
fid = h5open(file, "r")
dset = fid["DS1"]
a = a_read(dset, "A1")
@assert a.tag == "Character array"
@assert a.data == opq
close(fid)

file = getfile("h5ex_t_opaque.h5")
fid = h5open(file, "r")
d = read(fid, "DS1")
@assert d.tag == "Character array"
@assert d.data == opq
close(fid)


"""
An aggregator interface for SSA-like algorithms.

### Required Fields
- `next_jump`
- `next_jump_time`
- `end_time`
- `cur_rates`
- `sum_rate`
- `ma_jumps`
- `rates`
- `affects!`
- `save_positions`
- `rng`
"""
abstract type AbstractSSAJumpAggregator <: AbstractJumpAggregator end

DiscreteCallback(c::AbstractSSAJumpAggregator) = DiscreteCallback(c, c, initialize = c, save_positions = c.save_positions)

########### The following routines should be templates for all SSAs ###########

# # condition for jump to occur
# @inline function (p::SSAJumpAggregator)(u, t, integrator)
#   p.next_jump_time == t
# end

# # executing jump at the next jump time
# function (p::SSAJumpAggregator)(integrator)
#   execute_jumps!(p, integrator, integrator.u, integrator.p, integrator.t)
#   generate_jumps!(p, integrator, integrator.u, integrator.p, integrator.t)
#   register_next_jump_time!(integrator, p, integrator.t)
#   nothing
# end

# # setting up a new simulation
# function (p::SSAJumpAggregator)(dj, u, t, integrator) # initialize
#   initialize!(p, integrator, u, integrator.p, t)
#   register_next_jump_time!(integrator, p, integrator.t)
#   nothing
# end

############################## Generic Routines ###############################

@inline function register_next_jump_time!(integrator, p::AbstractSSAJumpAggregator, t)
    if p.next_jump_time < p.end_time
        add_tstop!(integrator, p.next_jump_time)
    end
    nothing
end

# helper routine for setting up standard fields of SSA jump aggregations
function build_jump_aggregation(jump_agg_type, u, p, t, end_time, ma_jumps, rates,
                                affects!, save_positions, rng; kwargs...)

  # mass action jumps
    majumps = ma_jumps
    if majumps === nothing
        majumps = MassActionJump(Vector{typeof(t)}(),
                             Vector{Vector{Pair{Int,eltype(u)}}}(),
                             Vector{Vector{Pair{Int,eltype(u)}}}())
    end

  # current jump rates, allows mass action rates and constant jumps
    cur_rates = Vector{typeof(t)}(undef, get_num_majumps(majumps) + length(rates))

    sum_rate = zero(typeof(t))
    next_jump = 0
    next_jump_time = typemax(typeof(t))
    jump_agg_type(next_jump, next_jump_time, end_time, cur_rates, sum_rate,
                majumps, rates, affects!, save_positions, rng; kwargs...)
end

# reevaluate all rates and total rate
function fill_rates_and_sum!(p::AbstractSSAJumpAggregator, u, params, t)
    sum_rate = zero(typeof(p.sum_rate))

  # mass action jumps
    majumps   = p.ma_jumps
    cur_rates = p.cur_rates
    @inbounds for i in 1:get_num_majumps(majumps)
        cur_rates[i] = evalrxrate(u, i, majumps)
        sum_rate    += cur_rates[i]
    end

  # constant rates
    rates = p.rates
    idx   = get_num_majumps(majumps) + 1
    @inbounds for rate in rates
        cur_rates[idx] = rate(u, params, t)
        sum_rate += cur_rates[idx]
        idx += 1
    end

    p.sum_rate = sum_rate
    nothing
end

# recalculate jump rates for jumps that depend on the just executed jump
# requires dependency graph
function update_dependent_rates!(p::AbstractSSAJumpAggregator, u, params, t)
    @inbounds dep_rxs = p.dep_gr[p.next_jump]
    cur_rates   = p.cur_rates
    sum_rate    = p.sum_rate
    @inbounds for rx in dep_rxs
        sum_rate -= cur_rates[rx]
        @inbounds cur_rates[rx] = calculate_jump_rate(p.ma_jumps, p.rates, u,params,t,rx)
        sum_rate += cur_rates[rx]
    end

    p.sum_rate = sum_rate
    nothing
end

# Update state based on the p.next_jump
@inline function update_state!(p::AbstractSSAJumpAggregator, integrator, u)
    @unpack ma_jumps, next_jump = p
    num_ma_rates = get_num_majumps(ma_jumps)
    if next_jump <= num_ma_rates # is next jump a mass action jump
        if u isa SVector
            integrator.u = executerx(u, next_jump, ma_jumps)
        else
            @inbounds executerx!(u, next_jump, ma_jumps)
        end
    else
        idx = next_jump - num_ma_rates
        @inbounds p.affects![idx](integrator)
    end
    return integrator.u
end

"check if the total rate is 0 and if it is, make the next jump time Inf"
@inline function nomorejumps!(p, sum_rate) :: Bool
    if sum_rate < eps(typeof(sum_rate))
        p.next_jump = 0
        p.next_jump_time = convert(typeof(sum_rate), Inf)
        return true
    end
    return false
end

"perform linear search of r over array. Output element j s.t. array[j-1] < r <= array[j]. Will error if no such r exists"
@inline function linear_search(array, r)
    jidx = 1
    @inbounds parsum = array[jidx]
    while parsum < r
        jidx   += 1
        @inbounds parsum += array[jidx]
    end
    return jidx
end

"perform rejection sampling test"
@inline function rejectrx(ma_jumps, rates, cur_rate_high, cur_rate_low, rng, u, jidx, params, t)
    # rejection test
    @inbounds r2     = rand(rng) * cur_rate_high[jidx]
    @inbounds crlow  = cur_rate_low[jidx]

    if crlow > zero(crlow) && r2 <= crlow
        return false
    else
        # calculate actual propensity, split up for type stability
        crate = calculate_jump_rate(ma_jumps, rates, u, params, t, jidx)
        if crate > zero(crate) && r2 <= crate
            return false
        end
    end
    return true
end

"update the jump rate, assuming p.rates is a vector of functions"
@inline function calculate_jump_rate(ma_jumps, rates, u, params, t, rx)
    num_majumps = get_num_majumps(ma_jumps)
    if rx <= num_majumps
        return evalrxrate(u, rx, ma_jumps)
    else
        @inbounds return rates[rx - num_majumps](u, params, t)
    end
end


function parse_registry(
    registry_dir = joinpath(homedir(), ".julia", "registries", "General")
)
    registry_dict = TOML.parsefile(joinpath(registry_dir, "Registry.toml"))
    pkgs = Dict{String, Package}()
    for (uuid_str, pkg_lookup) in registry_dict["packages"]
        pkg_name = pkg_lookup["name"]::String
        rel_path = pkg_lookup["path"]::String
        abs_path = joinpath(registry_dir, rel_path)
        pkg = parse_registry_package_dir(abs_path)
        pkgs[pkg_name] = pkg
    end
    pkgs
end

function parse_registry_package_dir(path::AbstractString)
    info = TOML.parsefile(joinpath(path, "Package.toml"))
    deps_data = Operations.load_package_data_raw(
        UUID, joinpath(path, "Deps.toml")
    )
    compat_data = Operations.load_package_data_raw(
        VersionSpec, joinpath(path, "Compat.toml")
    )
    toml_versions = Operations.load_versions(path)
    versions = Dict{VersionNumber, Version}()
    for v in keys(toml_versions)
        reqdict = Dict{String, Require}()
        ver = Version(nothing, reqdict)
        for (vr, dd) in compat_data
            if v in vr
                for (reqname, vs) in dd
                    reqdict[reqname] = Require(convert(VersionSet, vs), Symbol[])
                end
            end
        end
        for (vr, dd) in deps_data
            if v in vr
                for (reqname, uuid) in dd
                    if ! haskey(reqdict, reqname)
                        reqdict[reqname] = Require(VersionSet(), Symbol[])
                    end
                end
            end
        end
        versions[v] = ver
    end
    return Package(
        UUID(info["uuid"]),
        info["repo"],
        versions
    )
end

convert(::Type{VersionNumber}, v::VersionBound) = VersionNumber(v.t...)
function convert(::Type{VersionSet}, vs::VersionSpec)
    VersionSet(
        convert(VersionNumber, vs.ranges[1].lower),
        next_semver(convert(VersionNumber, vs.ranges[1].upper))
    )
end

function next_semver(v::VersionNumber)
    if v.major == 0
        return VersionNumber(0, v.minor + 1, 0)
    else
        return VersionNumber(v.major + 1, 0, 0)
    end
end


@testset "testing constructor" begin

    @test Δ_max(test_options) ≡ Δₘ

    @test δ_tol(test_options) ≡ δ

    @test samples(test_options) ≡ 2w

    @test max_iterations(test_options) ≡ N

    @test weave_level(test_options) ≡ WEAVE_LEVEL

    @test log_level(test_options) ≡ LOG_LEVEL

    restricted_type_test(test_options)
end

@testset "testing behavior" begin

    options = DriverOptions()

    # Mutating option fields only if positive
    @test !(Δ_max!(options, 0.0) ≡ 0.0)

    @test !(δ_tol!(options, 0.0) ≡ 0.0)

    @test !(ϵ_tol!(options, 0.0) ≡ 0.0)

    @test !(samples!(options, 0) ≡ 0)

    @test !(samples!(options, 1) ≡ 1)

    @test Δ_max!(options, 1.0) ≡ 1.0

    @test δ_tol!(options, 1.0) ≡ 1.0

    @test ϵ_tol!(options, 1.0) ≡ 1.0

    @test samples!(options, 2) ≡ 2

    @test weave_level!(options, NONE) ≡ NONE

    @test log_level!(options, ERROR) ≡ ERROR
end


#Cubic
doubleRotatedDensity = rotateStructure(rotateStructure(densityCube, 1.5, 1.5, 1.5), -1.5, -1.5, -1.5)
@test similarity(densityCube, doubleRotatedDensity) > 0.85