abstract type AxesLayout end

struct HasFast{M} <: AxesLayout
    fast::Int
    slows::NTuple{M, Int}
end

struct NoFast{N} <: AxesLayout end
struct NoAxes <: AxesLayout end

NoFast(N::Int) = NoFast{N}()

NoSlow(_N::Int) = HasFast(1, ())  # maybe create a new type?

# Base.ndims(::T) where {T <: AxesLayout} = ndims(T)
# Base.ndims(::Type{HasFast{M}}) where M = M + 1
# Base.ndims(::Type{NoFast{N}}) where N = N
# Base.ndims(::Type{NoAxes}) = 0

# fastaxis(al::HasFast) = al.fast
# fastaxis(::AxesLayout) = nothing
slowaxes(al::HasFast) = al.slows
slowaxes(::NoFast{N}) where N = ntuple(identity, N)
slowaxes(::NoAxes) = ()

slowaxes(::T) where {T} = slowaxes(AxesLayout(T))

#=
AxesLayout(::Nothing, slow::NTuple) = NoFast(slow)
AxesLayout(fast::Int, slow::NTuple) = HasFast(fast, slow)
=#

const ColumnMajor{N} = Union{
    # List taken from `strides`:
    DenseArray{<:Any, N},
    StridedReshapedArray{<:Any, N},
    StridedReinterpretArray{<:Any, N},
}

AxesLayout(::T) where T = AxesLayout(T)
AxesLayout(T::Type) = error("AxesLayout is unknown for: ", T)
AxesLayout(::Type{<:ColumnMajor{N}}) where N =
    HasFast(1, Base.tail(ntuple(identity, N)))
AxesLayout(::Type{<:Union{
    <:Adjoint{<:Any, <:ColumnMajor{1}},
    <:Adjoint{<:Any, <:ColumnMajor{2}},
    <:Transpose{<:Any, <:ColumnMajor{1}},
    <:Transpose{<:Any, <:ColumnMajor{2}},
}}) = HasFast(2, (1,))
AxesLayout(::Type{A}) where {A <: ReferenceableArray} = AxesLayout(parenttype(A))

AxesLayout(::Type{<:AbstractArray{<:Any, N}}) where N = NoFast{N}()
AxesLayout(::Type{<:Ref}) = NoAxes()

AxesLayout(::Type{BC}) where {BC <: Broadcasted} = axeslayoutargs(argstype(BC))
argstype(::Type{<:Broadcasted{<:Any, <:Any, <:Any, Args}}) where Args = Args
axeslayoutargs(::Type{Tuple{}}) = NoAxes()
axeslayoutargs(::Type{Args}) where {Args <: Tuple} =
    bcaxeslayout(AxesLayout(tuple_type_head(Args)),
                 axeslayoutargs(tuple_type_tail(Args)))

# "Computed/virtual" arrays has no slow axes.
AxesLayout(::Type{<:AbstractRange}) = NoSlow(1)  # is it?
AxesLayout(::Type{<:Union{
    CartesianIndices{N},
    LinearIndices{N},
}}) where N =
    NoSlow(N)

# Merge two `AxesLayout` in a way compatible with broadcasting
bcaxeslayout(x::NoFast, ::AxesLayout) = x
bcaxeslayout(::NoAxes, y::AxesLayout) = y
bcaxeslayout(::HasFast, y::NoFast) = y
bcaxeslayout(x::HasFast, ::NoAxes) = x
bcaxeslayout(x::HasFast{N}, y::HasFast{M}) where {N, M} =
    if x.fast !== y.fast
        NoFast{N}()
    else
        slows = foldlargs((), ntuple(identity, max(N, M))...) do slows, i
            if i === x.fast
                slows
            else
                (slows..., i)
            end
        end
        return HasFast(x.fast, slows)
    end

struct Index{x} end
const Indices{N} = NTuple{N, Index}

Index(x::Index) = x
Index(x) = Index{x}()

indexname(::Index{x}) where x = x

struct AccessPattern{A, T <: Indices}
    indexable::A
    indices::T
end

Base.ndims(p::AccessPattern) = ndims(p.indexable)

AccessPattern(pair::Pair) = AccessPattern(pair[1], map(Index, pair[2]))

struct AccessPlan{T <: Indices}
    indices::T
end

"""
    plan(args::AccessPattern...) :: AccessPlan
    plan(pairs::Pair...) :: AccessPlan

Try to find an index that is categorized as "fast" for all `args` with
such index.

# Examples
```jldoctest
julia> using NDReducibles: plan

julia> A1 = ones(0)
       A2 = ones(0, 0)
       A3 = ones(0, 0, 0)
       A4 = ones(0, 0, 0, 0);

julia> plan(
           A1 => (:i,),
           A2 => (:i, :j),
       )
AccessPlan: j → i

julia> plan(
           A1 => (:i,),
           A2 => (:j, :i),
           A2 => (:j, :k),
       )
AccessPlan: i → k → j

julia> plan(
           A1 => (:i,),
           A2' => (:j, :i),
           A2 => (:j, :k),
       )
AccessPlan: j → k → i

julia> plan(
           A1 => (:i,),
           A2' => (:i, :j),
           A2 => (:i, :j),
           A3 => (:k, :i, :j),
           A4 => (:k, :j, :l, :m),
       )
AccessPlan: i → j → l → m → k
```
"""
plan(pairs::Pair...) = plan(map(AccessPattern, pairs)...)

function plan(args::AccessPattern...)
    allindices = foldlargs((), args...) do indices, pattern
        tupleunion(indices, pattern.indices)
    end :: Indices
    fastindices = foldlargs(allindices, args...) do indices, pattern
        slows = map(slowaxes(pattern.indexable)) do a
            pattern.indices[a]
        end
        tuplediff(indices::Indices, slows::Indices)
    end :: Indices
    # TODO: Sort `fastindices` by the number of arrays using it?
    return AccessPlan((tuplediff(allindices, fastindices)..., fastindices...))
end


const Point{T} = NTuple{2, T}

# ------------------------------------------------------------------------------------------
# Pedestrian
# ------------------------------------------------------------------------------------------
Base.@kwdef mutable struct Pedestrian{T<:Real}
    # mandatory properties
    id::Int
    pos::Point{T}
    vel::Point{T} = (0., 0.)
    target::Point{T} = pos

    # additional properties
    acc::Float64 = 0.5            # acceleration
    φ::Float64 = 3π/4             # maximum change of a pedestrian course
    radius::Float64 = 0.25        # social pedestrian size
    rlims::Point{T} = (0.1, 0.25) # physical pedestrian size

    # flags
    finished::Bool = false
    isexit::Bool = false

    # history
    position::Matrix{T} = zeros(0, 2)
    velocity::Matrix{T} = zeros(0, 2)
end

Pedestrian(id, pos; kwargs...) = Pedestrian(; id, pos, kwargs...)

"""
    isvalid(p::Pedestrian, pos, radius)

Checks wheter give position `pos` is valid with respect to pedestrian `p`.
"""
function isvalid(p::Pedestrian, pos::Point, radius::Real; social::Bool = false)
    radius_p = social ? p.rlims[2] : p.radius
    return distance(p.pos, pos) >= radius_p + radius
end

"""
    isvalid(p1::Pedestrian, p2::Pedestrian)

Checks wheter pedestrian `p2` has valid position with respect to pedestrian `p1`.
"""
function isvalid(p1::Pedestrian, p2::Pedestrian; kwargs...)
    return isvalid(p1, p2.pos, p2.radius; kwargs...)
end

# ------------------------------------------------------------------------------------------
# Room specification
# ------------------------------------------------------------------------------------------
abstract type RoomShape end
abstract type Obstacle end
abstract type Target end
abstract type Door <: Target end

struct Room
    shape::RoomShape
    obstacles::Vector{<:Obstacle}
    entrances::Vector{<:Door}
    exits::Vector{<:Door}
    checkpoints::Vector{<:Target}
end

"""
    isvalid(r::Room, pos::Point, r = 0)

Checks wheter give point `pos` is valid for given room configuration.
"""
function isvalid(room::Room, pos::Point, r = 0)
    # checks wheter given position lies inside the room (with zero radius)
    isvalid(room.shape, pos, 0) || return false

    # checks wheter given position is valid with respect to obstacles
    for o in room.obstacles
        isvalid(o, pos, r) || return false
    end

    # checks wheter given position lies inside the door area
    for d in vcat(room.entrances, room.exits)
        isvalid(d, pos, r) || return false
        isinside(d, pos, r) && return true
    end
    return isvalid(room.shape, pos, r)
end

# ------------------------------------------------------------------------------------------
# Model specification
# ------------------------------------------------------------------------------------------
abstract type TimeStrategy end
abstract type ScheduleStrategy end
abstract type TargetStrategy end
abstract type MoveStrategy end

Base.@kwdef struct Model{T<:Real}
    # room specification
    room::Room

    # strategies
    time_strategy::TimeStrategy
    schedule_strategy::ScheduleStrategy
    target_strategy::TargetStrategy
    move_strategy::MoveStrategy

    # pedestrians
    pedestrians::Dict{Int, Pedestrian} = Dict{Int, Pedestrian}()
    history::Dict{Int, Pedestrian} = Dict{Int, Pedestrian}()

    # additional parameters
    Δt::T = 0.05
    maxid::Ref{Int} = Ref(0)
    iter::Ref{Int} = Ref(0)
    safe_add::Bool = true
    stop::Ref{Bool} = Ref(false)
end

function Model(
    room,
    time_strategy,
    schedule_strategy,
    target_strategy,
    move_strategy,
    kwargs...
)
    return Model(;
        room,
        time_strategy,
        schedule_strategy,
        target_strategy,
        move_strategy,
        kwargs...
    )
end

function Base.show(io::IO, m::Model)
    println(io, "Pedestrian model:")
    println(io, " ⋅ room: ", m.room.shape)
    println(io, "   - checkpoints: ", length(m.room.checkpoints))
    println(io, "   - obstacles: ", length(m.room.obstacles))
    println(io, "   - entrances: ", length(m.room.entrances))
    println(io, "   - exits: ", length(m.room.exits))
    println(io, " ⋅ pedestrians: ", length(m.pedestrians))
    println(io, " ⋅ time strategy: ", typeof(m.time_strategy))
    println(io, " ⋅ schedule strategy: ", typeof(m.schedule_strategy))
    println(io, " ⋅ target strategy: ", typeof(m.target_strategy))
    println(io, " ⋅ move strategy: ", typeof(m.move_strategy))
    return
end

set_stop!(m::Model) = m.stop[] = true
maxid(m::Model) = m.maxid[]
npads(m::Model) = isempty(m.pedestrians)
iter(m::Model) = m.iter[]
allids(m::Model) = collect(keys(m.pedestrians))
allpedestrians(m::Model) = collect(values(m.pedestrians))

"""
    add_pedestrian!(m::Model, p::Pedestrian)

If the position of the pedestrian `p` is valid, adds `p` to the model `m` with smallest available `id`.
"""
function add_pedestrian!(m::Model, p::Pedestrian)
    if m.safe_add
        if !isvalid(m.room, p.pos, p.radius)
            @warn "invalid pedestrian with respect to room specification"
            return
        end
        for (~, p2) in m.pedestrians
            if !isvalid(p, p2)
                @warn "invalid pedestrian with respect to other pedestrians"
                return
            end
        end
    end

    m.maxid[] += 1
    p.id = m.maxid[]
    m.pedestrians[p.id] = p
    return
end

"""
    remove_pedestrian!(m::Model, p::Pedestrian)

Removes pedestrian `p` from the model `m`.
"""
function remove_pedestrian!(m::Model, p::Pedestrian)
    m.history[p.id] = deepcopy(p)
    delete!(m.pedestrians, p.id)
end

"""
    move_pedestrian!(m::Model, p::Pedestrian)

Move pedestrian `p` based on its position and velocity or remove `p` from model if in exit.
"""
function move_pedestrian!(m::Model, p::Pedestrian)
    p.velocity = vcat(p.velocity, [p.vel[1] p.vel[2]])
    p.position = vcat(p.position, [p.pos[1] p.pos[2]])
    p.finished && remove_pedestrian!(m, p)

    if p.isexit && norm(p.vel, 2)*m.Δt > distance(p.pos, p.target)
        p.finished = true
        remove_pedestrian!(m, p)
    else
        p.pos = p.pos .+ p.vel .* m.Δt
    end
end

function step!(m::Model)
    # add new pedestrians
    add_pedestrian!(m.time_strategy, m)

    # move all pedestrians
    for id in scheduler(m.schedule_strategy, m)
        pedestrian_step!(m.move_strategy, m, m.pedestrians[id])
    end

    # update counter
    m.iter[] += 1
    return
end

function run!(m::Model, iter)

    bar = Progress(iter; showspeed = true)
    for i in 1:iter
        step!(m)

        # progress bar
        k = length(keys(m.pedestrians))
        next!(bar; showvalues = [
                (:iter, i),
                (:pedestrians, k)
        ])
        if m.stop[]
            finish!(bar)
            break
        end
    end
    return
end


module EMLpump
	using DanaTypes
	using DotPlusInheritance
	using Reexport
	@reexport using ...streams.EMLstreams
	import EMLtypes.length
	include("pump/centrifugal_pump.jl")
	include("pump/pump.jl")
	include("pump/pump2.jl")
end


@libcall arb_mul_2exp_si 2
@libcall arb_one 1
@libcall arb_pow_ui 2 prec=true
@libcall arb_fac_ui 1 prec=true
@libcall arb_div 3 prec=true
@libcall arb_abs 2 prec=true
@libcall arb_add_error 2
@libcall arb_add 3 prec=true
@libcall arb_sub 3 prec=true


#
# This file is a part of MolecularGraph.jl
# Licensed under the MIT License http://opensource.org/licenses/MIT
#

export
    mcismol, mcismolsize,
    mcesmol, mcesmolsize

"""
    mcismol(mol1::UndirectedGraph, mol2::UndirectedGraph; kwargs...
        ) -> Tuple{Dict{Int,Int},Symbol}

Compute maximum common induced substructure (MCIS) of mol1 and mol2.

## Keyword arguments

- connected(Bool): if true, apply connected MCS constraint.
- topological(Bool): if true, apply topological constraint.
- diameter(Int): distance cutoff for topological constraint.
- tolerance(Int): distance mismatch tolerance for topological constraint.
- timeout(Int): abort calculation and return suboptimal results if the execution
time has reached the given value (default=60, in seconds).
- targetsize(Int): abort calculation and return suboptimal result so far if the
given mcs size achieved.
"""
function mcismol(mol1::UndirectedGraph, mol2::UndirectedGraph; kwargs...)
    afunc = atommatch(mol1, mol2)
    bfunc = bondmatch(mol1, mol2)
    return findmcis(
        mol1, mol2, nodematcher=afunc, edgematcher=bfunc; kwargs...)
end

mcismolsize(mol1, mol2; kwargs...) = length(mcismol(mol1, mol2; kwargs...)[1])

"""
    mcesmol(mol1::UndirectedGraph, mol2::UndirectedGraph; kwargs...
        ) -> Tuple{Dict{Int,Int},Symbol}

Compute maximum common edge induced substructure (MCES) of mol1 and mol2.
"""
function mcesmol(mol1::UndirectedGraph, mol2::UndirectedGraph; kwargs...)
    afunc = atommatch(mol1, mol2)
    bfunc = bondmatch(mol1, mol2)
    return findmces(
        mol1, mol2, nodematcher=afunc, edgematcher=bfunc; kwargs...)
end

mcesmolsize(mol1, mol2; kwargs...) = length(mcesmol(mol1, mol2; kwargs...)[1])

# TODO: subgraphview


using GLMakie, GeometryBasics, RPRMakie, RadeonProRender
using Colors, FileIO
using Colors: N0f8
RPR = RadeonProRender

context = RPR.Context()
matsys = RPR.MaterialSystem(context, 0)

materials = [
    RPR.DiffuseMaterial(matsys) RPR.MicrofacetMaterial(matsys);
    RPR.ReflectionMaterial(matsys) RPR.RefractionMaterial(matsys);
    RPR.EmissiveMaterial(matsys) RPR.UberMaterial(matsys);
]

cat = load(GLMakie.assetpath("cat.obj"))

fig = Figure(resolution=(1000, 1000))
ax = LScene(fig[1, 1], scenekw=(show_axis=false,))
palette = reshape(Makie.default_palettes.color[][1:6], size(materials))
for i in CartesianIndices(materials)
    x, y = Tuple(i)
    catmesh = mesh!(ax, cat, material=materials[i], color=palette[i])
    translate!(catmesh, x, y, 0)
end
# materials[3, 1].color = Vec4(200)
display(fig)
context, task = RPRMakie.replace_scene_rpr!(ax.scene, context, matsys)
# fetch(task)

volmat = materials[end, end]

volmat.scattering = Vec3(0, 0, 0)
volmat.absorption = RGB(0.01, 0.01, 0.01)
volmat.multiscatter = true
# volmat.emission = RPR.RPR_MATERIAL_INPUT_EMISSION,
# volmat.scatter_direction = RPR.RPR_MATERIAL_INPUT_G,


module CoreEcoliModel

# internal includes -
include("Include.jl")

#
# maximize_specific_growth_rate(path_to_measurements_file::String; number_of_samples::Int64 = 100)
export maximize_specific_growth_rate
export maximize_flux_at_index
export moma_calculation
export flux_variabilty_analysis

# Data dictionary methods -
export generate_default_data_dictionary
export constrain_measured_fluxes
export constrain_measured_metabolites
export optimize_specific_growth_rate

# Export utility methods -
export write_model_reactions_to_file

# export types -
export VLOptimalFluxResult

end # module


using ReinforcementLearningBase
using ReinforcementLearningEnvironments
using PyCall
using UnicodePlots

@testset "d4rl_policies" begin
    model = d4rl_policy("ant", "online", 10)

    @test typeof(model) <: D4RLGaussianNetwork

    env = GymEnv("ant-medium-v0")

    a = state(env) |> model

    @test action_space(env) |> size == size(a[1])
end

@testset "d4rl_policy_evaluate" begin
    plt = deep_ope_d4rl_evaluate("halfcheetah", "online", 10; num_evaluations=100)
    @test typeof(plt) <: UnicodePlots.Plot
end


module StructJuMP

export StructuredModel

using MathOptInterface
const MOI = MathOptInterface

using JuMP # To reexport, should be using (not import)
# Macro to exportall
macro exportall(pkg)
    Expr(:export, names(JuMP)...)
end
@exportall JuMP

# The following is largely inspired from JuMP/test/JuMPExtension.jl

mutable struct StructuredModel <: JuMP.AbstractModel
    # Structured data
    parent::Union{StructuredModel, Nothing}
    children::Dict{Int, StructuredModel}
    probability::Dict{Int, Float64}
    num_scen::Int

    # Model data
    nextvaridx::Int                                 # Next variable index is nextvaridx+1
    variables::Dict{Int, JuMP.AbstractVariable}     # Map varidx -> variable
    varnames::Dict{Int, String}                     # Map varidx -> name
    nextconidx::Int                                 # Next constraint index is nextconidx+1
    constraints::Dict{Int, JuMP.AbstractConstraint} # Map conidx -> variable
    connames::Dict{Int, String}                     # Map varidx -> name
    objective_sense::MOI.OptimizationSense
    objective_function::JuMP.AbstractJuMPScalar
    objdict::Dict{Symbol, Any}                      # Same that JuMP.Model's field `objdict`
    function StructuredModel(; parent=nothing, same_children_as=nothing, id=0,
                             num_scenarios::Int=0,
                             prob::Float64=default_probability(parent))
        if same_children_as !== nothing
            if !isa(same_children_as, StructuredModel)
                error("The JuMP model given for the argument `same_children_as' is not valid. Please create it using the `StructuredModel' function.")
            end
            probability = same_children_as.probability
            children = same_children_as.children
        else
            probability = Dict{Int, Float64}()
            children = Dict{Int, JuMP.Model}()
        end

        model = new(parent, children, probability, num_scenarios,                    # Structured
                    0, Dict{Int, JuMP.AbstractVariable}(),   Dict{Int, String}(),    # Model Variables
                    0, Dict{Int, JuMP.AbstractConstraint}(), Dict{Int, String}(),    # Model Constraints
                    MOI.FEASIBILITY_SENSE, zero(JuMP.GenericAffExpr{Float64, StructuredVariableRef}), # Model objective
                    Dict{Symbol, Any}())                                             # Model objects

        if parent === nothing
            id = 0
        else
            @assert id != 0
            parent.children[id] = model
            parent.probability[id] = prob
        end

        return model
    end
end

#### Structured ####

getparent(model::StructuredModel)      = model.parent
getchildren(model::StructuredModel)    = model.children
getprobability(model::StructuredModel) = model.probability
num_scenarios(model::StructuredModel)  = model.num_scen

default_probability(model::StructuredModel) = 1 / num_scenarios(model)
default_probability(::Nothing) = 1.0

#### Model ####

JuMP.object_dictionary(m::StructuredModel) = m.objdict

# Variables
struct StructuredVariableRef <: JuMP.AbstractVariableRef
    model::StructuredModel # `model` owning the variable
    idx::Int       # Index in `model.variables`
end
Base.broadcastable(v::StructuredVariableRef) = Ref(v)
Base.copy(v::StructuredVariableRef) = v
Base.:(==)(v::StructuredVariableRef, w::StructuredVariableRef) = v.model === w.model && v.idx == w.idx
JuMP.owner_model(v::StructuredVariableRef) = v.model
JuMP.isequal_canonical(v::StructuredVariableRef, w::StructuredVariableRef) = v == w
JuMP.variable_type(::StructuredModel) = StructuredVariableRef
function JuMP.add_variable(m::StructuredModel, v::JuMP.AbstractVariable, name::String="")
    m.nextvaridx += 1
    vref = StructuredVariableRef(m, m.nextvaridx)
    m.variables[vref.idx] = v
    JuMP.set_name(vref, name)
    vref
end
function MOI.delete!(m::StructuredModel, vref::StructuredVariableRef)
    delete!(m.variables, vref.idx)
    delete!(m.varnames, vref.idx)
end
MOI.is_valid(m::StructuredModel, vref::StructuredVariableRef) = vref.idx in keys(m.variables)
JuMP.num_variables(m::StructuredModel) = length(m.variables)

# Internal function
variable_info(vref::StructuredVariableRef) = vref.model.variables[vref.idx].info
function update_variable_info(vref::StructuredVariableRef, info::JuMP.VariableInfo)
    vref.model.variables[vref.idx] = JuMP.ScalarVariable(info)
end

JuMP.has_lower_bound(vref::StructuredVariableRef) = variable_info(vref).has_lb
function JuMP.lower_bound(vref::StructuredVariableRef)
    @assert !JuMP.is_fixed(vref)
    variable_info(vref).lower_bound
end
function JuMP.set_lower_bound(vref::StructuredVariableRef, lower)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(true, lower,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
function JuMP.delete_lower_bound(vref::StructuredVariableRef)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(false, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
JuMP.has_upper_bound(vref::StructuredVariableRef) = variable_info(vref).has_ub
function JuMP.upper_bound(vref::StructuredVariableRef)
    @assert !JuMP.is_fixed(vref)
    variable_info(vref).upper_bound
end
function JuMP.set_upper_bound(vref::StructuredVariableRef, upper)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           true, upper,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
function JuMP.delete_upper_bound(vref::StructuredVariableRef)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           false, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
JuMP.is_fixed(vref::StructuredVariableRef) = variable_info(vref).has_fix
JuMP.fix_value(vref::StructuredVariableRef) = variable_info(vref).fixed_value
function JuMP.fix(vref::StructuredVariableRef, value)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           true, value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
function JuMP.unfix(vref::StructuredVariableRef)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           false, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, info.integer))
end
JuMP.start_value(vref::StructuredVariableRef) = variable_info(vref).start
function JuMP.set_start_value(vref::StructuredVariableRef, start)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           true, start,
                                           info.binary, info.integer))
end
JuMP.is_binary(vref::StructuredVariableRef) = variable_info(vref).binary
function JuMP.set_binary(vref::StructuredVariableRef)
    @assert !JuMP.is_integer(vref)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           true, info.integer))
end
function JuMP.unset_binary(vref::StructuredVariableRef)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           false, info.integer))
end
JuMP.is_integer(vref::StructuredVariableRef) = variable_info(vref).integer
function JuMP.set_integer(vref::StructuredVariableRef)
    @assert !JuMP.is_binary(vref)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, true))
end
function JuMP.unset_integer(vref::StructuredVariableRef)
    info = variable_info(vref)
    update_variable_info(vref,
                         JuMP.VariableInfo(info.has_lb, info.lower_bound,
                                           info.has_ub, info.upper_bound,
                                           info.has_fix, info.fixed_value,
                                           info.has_start, info.start,
                                           info.binary, false))
end

# Constraints
struct StructuredConstraintRef
    model::StructuredModel # `model` owning the constraint
    idx::Int       # Index in `model.constraints`
end
JuMP.constraint_type(::StructuredModel) = StructuredConstraintRef
function JuMP.add_constraint(m::StructuredModel, c::JuMP.AbstractConstraint, name::String="")
    m.nextconidx += 1
    cref = StructuredConstraintRef(m, m.nextconidx)
    m.constraints[cref.idx] = c
    JuMP.set_name(cref, name)
    cref
end
function MOI.delete!(m::StructuredModel, cref::StructuredConstraintRef)
    delete!(m.constraints, cref.idx)
    delete!(m.connames, cref.idx)
end
MOI.is_valid(m::StructuredModel, cref::StructuredConstraintRef) = cref.idx in keys(m.constraints)
function JuMP.constraint_object(cref::StructuredConstraintRef, F::Type, S::Type)
    c = cref.model.constraints[cref.idx]
    # `TypeError` should be thrown is `F` and `S` are not correct
    # This is needed for the tests in `constraints.jl`
    c.func::F
    c.set::S
    c
end

# Objective
function JuMP.set_objective(m::StructuredModel, sense::MOI.OptimizationSense,
                            f::JuMP.AbstractJuMPScalar)
    m.objective_sense = sense
    m.objective_function = f
end
JuMP.objective_sense(m::StructuredModel) = m.objective_sense
function JuMP.objective_function(m::StructuredModel, FT::Type)
    m.objective_function isa FT || error("The objective function is not of type $FT")
    m.objective_function
end

# Names
JuMP.name(vref::StructuredVariableRef) = vref.model.varnames[vref.idx]
function JuMP.set_name(vref::StructuredVariableRef, name::String)
    vref.model.varnames[vref.idx] = name
end
JuMP.name(cref::StructuredConstraintRef) = cref.model.connames[cref.idx]
function JuMP.set_name(cref::StructuredConstraintRef, name::String)
    cref.model.connames[cref.idx] = name
end

include("BendersBridge.jl")

end


# Base.eltype{T}(::Type{Polynomial{T}}) = T
# Base.eltype{T}(::Type{TransferFunction{T}}) = T
# Base.eltype{T}(::Type{SisoZpk{T}}) = T
# Base.eltype{T}(::Type{SisoRational{T}}) = T
# Base.eltype{T}(::Type{StateSpace{T}}) = T
#

# """
#     responsetype(T)
# Return the numerical type of the time-response for composite LTISystem type `T`, e.g., `responsetype(ss(1.)) == Float64`
# If `T` is not a type, return `responsetype(typeof(T))`
# """
# responsetype{T<:Real}(::Type{T}) = T
# responsetype{T<:Complex}(::Type{T}) = promote_type(T.types...)
# responsetype(x) = typeof(x) # Fallback method
# responsetype{T}(::Type{T}) = T # Catch all

#matrix_type{T}(::Type{StateSpace{T, M{T}}}) = T

# Abstract type pyramid =============================================================

# NOTE: a lot that doesnt seem to be used...
#Base.promote_rule(::Type{StateSpace{T1}}, ::Type{StateSpace{T2}}) where {T1,T2} = StateSpace{promote_type(T1, T2)}

# NOTE: Is the below thing correct always?
function Base.promote_rule(::Type{StateSpace{T1,MT1}}, ::Type{StateSpace{T2,MT2}}) where {T1,T2,MT1,MT2}
    MT = promote_type(MT1, MT2)
    if isconcretetype(MT) # This sometimes fails and produces a Unionall. This condition can be checked at compile time, checked by f(x,y) = (MT = promote_type(x, y); isconcretetype(MT)); @code_llvm f(Int,Float64)
        StateSpace{promote_type(T1, T2),MT}
    else # If fail, fall back to an ordinary matrix
        FT = promote_type(T1, T2)
        StateSpace{FT,Matrix{FT}}
    end
end

Base.promote_rule(::Type{StateSpace{T,MT}}, ::Type{HeteroStateSpace{AT,BT,CT,DT}}) where {T,MT,AT,BT,CT,DT} = HeteroStateSpace{promote_type(MT,AT),promote_type(MT,BT),promote_type(MT,CT),promote_type(MT,DT)}

function Base.promote_rule(::Type{TransferFunction{S1}}, ::Type{StateSpace{T2,MT}}) where {T1,S1<:SisoTf{T1},T2,MT}
    #promote_type(Base.promote_op(/, T1, T1), T2)
    StateSpace{promote_type(T1,T2), promote_type(Matrix{T1},MT)}
end
# NOTE: Perhaps should try to keep matrix structure?

function Base.promote_rule(::Type{TransferFunction{S1}}, ::Type{DelayLtiSystem{T2}}) where {T1,S1<:SisoTf{T1},T2}
    DelayLtiSystem{promote_type(T1,T2)}
end
function Base.promote_rule(::Type{StateSpace{T1,MT}}, ::Type{DelayLtiSystem{T2}}) where {T1,MT,T2}
    DelayLtiSystem{promote_type(T1,T2)}
end

Base.promote_rule(::Type{TransferFunction{S1}}, ::Type{TransferFunction{S2}}) where {S1, S2} = TransferFunction{promote_type(S1, S2)}
#Base.promote_rule(::Type{SisoTf}, ::Type{TransferFunction}) = TransferFunction
#Base.promote_rule(::Type{SisoZpk}, ::Type{TransferFunction}) = TransferFunction
#Base.promote_rule(::Type{SisoRational}, ::Type{TransferFunction}) = TransferFunction

#function Base.promote_rule{T<:StateSpace,P<:TransferFunction}(::Type{T}, ::Type{P})  #where T <: StateSpace where P <: TransferFunction
#    S = promote_type(primitivereal(P), primitivereal(T)) # TODO: this is not correct for P <: zpk, in which case the StateSpace system gets complex matrices
#    StateSpace{Matrix{S}}
#end

# Promotion of Number ==========================================================

#Base.promote_rule{T1<:Number,T2<:Number}(::Type{TransferFunction{S}}, ::T2) = promote_type(T1, T2)

Base.promote_rule(::Type{TransferFunction{S}}, ::Type{T2}) where {S<:SisoTf, T2<:Number} =
    TransferFunction{promote_type(S, T2)}

# TODO Figure out a better way
function Base.promote_rule(::Type{StateSpace{T1, MT}}, ::Type{T2}) where {T1, MT, T2<:Number}
    NewMT = Base.promote_op(*, MT, T2)
    StateSpace{eltype(NewMT), NewMT}
end

Base.promote_rule(::Type{TransferFunction{SisoZpk{T1,TR1}}}, ::Type{M2}) where {T1, TR1, T2, M2<:AbstractMatrix{T2}} = TransferFunction{SisoZpk{T1, promote_type(TR1, T2)}}

Base.promote_rule(::Type{TransferFunction{SisoRational{T1}}}, ::Type{M2}) where {T1, T2, M2<:AbstractMatrix{T2}} = TransferFunction{SisoRational{promote_type(T1, T2)}}

function Base.promote_rule(::Type{StateSpace{T1, MT1}}, ::Type{MT2}) where {T1, MT1, MT2<:AbstractMatrix}
    MT = promote_type(MT1, MT2)
    StateSpace{eltype(MT), MT}
end

Base.promote_rule(::Type{DelayLtiSystem{T1,S}}, ::Type{MT1}) where {T1, S, MT1<:AbstractMatrix} =
    DelayLtiSystem{promote_type(T1, eltype(MT1)),S}

#Base.promote_rule{S<:TransferFunction{<:SisoTf}}(::Type{S}, ::Type{<:Real}) = S

# We want this, but not possible, so hardcode for SisoTypes
#Base.promote_rule(::Type{S}, ::Type{T2}) where {T1, S<:SisoTf{T1}, T2<:Number} = S{promote_type(T1, T2)}

function Base.promote_rule(::Type{SisoZpk{T1,C2}}, ::Type{T2}) where {T1, C2, T2<:Number}
    GainType = promote_type(T1, T2)
    return SisoZpk{GainType, complex(GainType)}
end
Base.promote_rule(::Type{SisoRational{T1}}, ::Type{T2}) where {T1, T2<:Number} = SisoRational{promote_type(T1,T2)}
#Base.promote_rule(::Type{StateSpace{T1, MT}}, ::Type{T2}) where {T1, T2<:Number} = SisoRational{promote_type(T1,T2)}

#Base.promote_rule{T<:SisoZpk}(::Type{T}, ::Type{<:Real}) = T
#Base.promote_rule{T}(::Type{SisoRational{T}}, ::Type{<:Real}) = SisoRational{T}
#Base.promote_rule{T}(::Type{StateSpace{T}}, ::Type{<:Real}) = StateSpace{T}
#Base.promote_rule{S<:TransferFunction{<:SisoTf},T<:Real}(::Type{S}, ::Union{Type{Array{T,2}},Type{Array{T,1}}}) = S

# Less abstract promotions
#Base.promote_rule(::Type{TransferFunction{SisoRational}}, ::Type{TransferFunction{SisoZpk}}) = TransferFunction{SisoZpk} # NOTE: Is this what we want?

# function Base.promote_rule{T1c<:SisoZpk,T2c<:SisoRational}(::Type{T1c}, ::Type{T2c})
#     upper_type = SisoZpk
#     inner_type = promote_type(arraytype(T1c), arraytype(T2c))
#     upper_type{inner_type}
# end

# @show siso_type = promote_type(eltype.(systems)...)
# if isleaftype(siso_type)
#     siso_type = siso_type.name.wrapper
# end
# # array_type = promote_type(arraytype.(systems)...)
# # mat = hcat([(e->convert(siso_type{array_type}, e)).(s.matrix) for s in systems]...)
# @show promote_type([s.matrix for s in systems]...)

#

#
#
# # Promote_op types # NOTE: Needed?
# Base.promote_op{T<:SisoTf}(::Any, ::Type{T}, ::Type{T}) = T
#
# #Just default SisoTf to SisoRational
# SisoTf(args...) = SisoRational(args...)
#
# Base.zero(::Type{<:SisoTf}) = zero(SisoRational)
# Base.zero(::SisoTf) = zero(SisoRational)
# Base.zero(::Type{<:SisoZpk}) = SisoZpk(Float64[],Float64[],0.0)
# Base.zero(::SisoZpk) = Base.zero(SisoZpk)
# Base.zero{T}(::Type{SisoRational{T}}) = SisoRational(zero(Polynomial{T}), one(Polynomial{T})) # FIXME: Is the in analogy with how zero for SisoRational is createdzer?
# Base.zero{T}(::SisoRational{T}) = Base.zero(SisoRational{T})


#  Copyright 2022 California Institute of Technology
#
#  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.
#
# Author: Philip G. Brodrick, philip.g.brodrick@jpl.nasa.gov

using DataFrames
using CSV
using Interpolations
using Logging
using Statistics
using Plots
using ModularIndices

function nanargmax(input::Array)
    x = copy(input)
    x[isnan.(x)] .= -Inf;
    return argmax(x);
end

function nanargmin(input::Array)
    x = copy(input)
    x[isnan.(x)] .= Inf;
    return argmin(x);
end

function read_envi_wavelengths(filename::String)
    header_name = splitext(filename)[1] * ".hdr"
    header = readlines(header_name)
    found = false
    for line in header
        if occursin("wavelength = {", line) || occursin("wavelength= {", line) ||  occursin("wavelength={", line)
            header = line
            found = true
            break
        end
    end

    if !found
        @error "No wavelength found in " * header_name
        return nothing
    end

    wavelengths = [parse(Float64, strip(x)) for x in split(split(split(header, "{")[2], "}")[1],",")]
    return wavelengths
end

function get_good_bands_mask(wavelengths::Array{Float64}, wavelength_pairs)
    good_bands = ones(Bool, length(wavelengths))

    for wvp in wavelength_pairs
        wavelength_diff = wavelengths .- wvp[1]
        wavelength_diff[wavelength_diff .< 0] .= maximum(filter(!isnan, wavelength_diff))
        lower_index = nanargmin(wavelength_diff)

        wavelength_diff = wvp[2] .- wavelengths
        wavelength_diff[wavelength_diff .< 0] .= maximum(filter(!isnan, wavelength_diff))
        upper_index = nanargmin(wavelength_diff)
        good_bands[lower_index:upper_index] .= false
    end

    return good_bands
end

mutable struct SpectralLibrary
    file_name::String
    class_header_name::String
    spectral_starting_column::Int64
    truncate_end_columns::Int64
    class_valid_keys
    scale_factor::Float64
    wavelength_regions_ignore
    SpectralLibrary(file_name::String, class_header_name::String, spectral_starting_column::Int64, truncate_end_columns::Int64, class_valid_keys = nothing,
                    scale_factor = 1.0, wavelength_regions_ignore= [[0,440],[1310,1490],[1770,2050],[2440,2880]]) =
                    new(file_name, class_header_name, spectral_starting_column, truncate_end_columns, class_valid_keys, scale_factor, wavelength_regions_ignore)

    spectra
    classes
    good_bands
    wavelengths
end

function load_data!(library::SpectralLibrary)

    df = DataFrame(CSV.File(library.file_name))

    try
        library.spectra = Matrix(df[:,library.spectral_starting_column:end-library.truncate_end_columns])
    catch e
        throw(ArgumentError("Could not read spectra from endmember library.  Try adjusting spectral_starting_column or truncate_end_columns, or reworking the library."))
    end

    try
        library.classes = convert(Array{AbstractString}, df[!,library.class_header_name])
    catch e
        throw(ArgumentError("Could not read classes from endmember library.  Try adjusting class_header_name, or reworking the library."))
    end

    try
        library.wavelengths = parse.(Float64, names(df[:,library.spectral_starting_column:end-library.truncate_end_columns]))
    catch e
        throw(ArgumentError("Could not read wavelengths from endmember library.  Try adjusting class_header_name, or reworking the library."))
    end

    wl_order = sortperm(library.wavelengths)
    library.spectra = library.spectra[:,wl_order]
    library.wavelengths = library.wavelengths[wl_order]

    if isnothing(library.class_valid_keys)
        library.class_valid_keys = unique(library.classes)
    end

    good_bands = get_good_bands_mask(library.wavelengths, library.wavelength_regions_ignore)
    library.good_bands = good_bands

    return library
end

function filter_by_class!(library::SpectralLibrary)
    if isnothing(library.class_valid_keys)
        @info "No class valid keys provided, no filtering occuring"
        return
    end

    valid_classes = zeros(Bool, size(library.spectra)[1])
    for cla in library.class_valid_keys
        valid_classes[library.classes .== cla] .= true
    end

    library.spectra = library.spectra[valid_classes,:]
    library.classes = library.classes[valid_classes]
end

function remove_wavelength_region_inplace!(library::SpectralLibrary, set_as_nans::Bool=false)
    if set_as_nans
        library.spectra[:,.!library.good_bands] .= NaN
        library.wavelengths[.!library.good_bands] .= NaN
    else
        library.spectra = library.spectra[:, library.good_bands]
        library.wavelengths = library.wavelengths[library.good_bands]
    end
end

function interpolate_library_to_new_wavelengths!(library::SpectralLibrary, new_wavelengths::Array{Float64})
    old_spectra = copy(library.spectra)

    library.spectra = zeros((size(library.spectra)[1], length(new_wavelengths)))
    for _s in 1:size(old_spectra)[1]
        fit = LinearInterpolation(library.wavelengths, old_spectra[_s,:], extrapolation_bc=Flat());
        library.spectra[_s,:] = fit(new_wavelengths)
    end
    library.wavelengths = new_wavelengths

    good_bands = get_good_bands_mask(library.wavelengths, library.wavelength_regions_ignore)
    library.good_bands = good_bands
end

function scale_library!(library::SpectralLibrary, scaling_factor=nothing)
    if isnothing(scaling_factor)
        library.spectra =  library.spectra ./ library.scale_factor
    else
        library.spectra /= scaling_factor
    end
end

function brightness_normalize!(library::SpectralLibrary)
    library.spectra = library.spectra ./ sqrt.(mean(library.spectra[:,library.good_bands].^2, dims=2))
end

function plot_mean_endmembers(endmember_library::SpectralLibrary, output_name::String)
    plots = []
    for (_u, u) in enumerate(endmember_library.class_valid_keys)
        mean_spectra = mean(endmember_library.spectra[endmember_library.classes .== u,:],dims=1)[:]
        plot!(endmember_library.wavelengths, mean_spectra, label=u, xlim=[300,3200])
    end
    xlabel!("Wavelength [nm]")
    ylabel!("Reflectance")
    xticks!([500, 1000, 1500, 2000, 2500, 3000])
    plot!(dpi=400)
    savefig(output_name)
end

function plot_endmembers(endmember_library::SpectralLibrary, output_name::String)

    for (_u, u) in enumerate(endmember_library.class_valid_keys)
        if _u == 1
            plot(endmember_library.wavelengths, endmember_library.spectra[endmember_library.classes .== u,:]', lab="", xlim=[300,3200], color=palette(:tab10)[Mod(_u)],dpi=400)
        else
            plot!(endmember_library.wavelengths, endmember_library.spectra[endmember_library.classes .== u,:]', lab="",xlim=[300,3200], color=palette(:tab10)[Mod(_u)])
        end
    end
    xlabel!("Wavelenth [nm]")
    ylabel!("Reflectance")
    xticks!([500, 1000, 1500, 2000, 2500, 3000])
    for (_u, u) in enumerate(endmember_library.class_valid_keys)
        plot!([1:2],[0,0.3], color=palette(:tab10)[Mod(_u)], labels=u)
    end
    savefig(output_name)
end

function plot_endmembers_individually(endmember_library::SpectralLibrary, output_name::String)
    plots = []
    spectra = endmember_library.spectra
    classes = endmember_library.classes
    for (_u, u) in enumerate(endmember_library.class_valid_keys)
        sp = spectra[classes .== u,:]
        sp[broadcast(isnan,sp)] .= 0
        brightness = sum(sp, dims=2)
        toplot = spectra[classes .== u,:] ./ brightness
        #push!(plots, plot(endmember_library.wavelengths, toplot', title=u, color=palette(:tab10)[_u], xlabel="Wavelength [nm]", ylabel="Reflectance"))
        push!(plots, plot(endmember_library.wavelengths, toplot', title=u, xlabel="Wavelength [nm]", ylabel="Reflectance"))
        xticks!([500, 1000, 1500, 2000, 2500])
    end
    plot(plots...,size=(1000,600),dpi=400)
    savefig(output_name)
end


const echo2 =  raw"""
       /==!/======ECHO==,==.==#
       |   |
$==>==@/==@/==<==#"""

@enum Direction left up right down

function snusp(datalength, progstring)
    stack = Vector{Tuple{Int, Int, Direction}}()
    data = zeros(datalength)
    dp = ipx = ipy = 1
    direction = right    # default to go to right at beginning

    lines = split(progstring, "\n")
    lmax = maximum(map(length, lines))
    lines = map(x -> rpad(x, lmax), lines)
    for (y, li) in enumerate(lines)
        if (x = findfirst("\$", li)) != nothing
            (ipx, ipy) = (x[1], y)
        end
    end

    instruction = Dict([('>', ()-> dp += 1),
        ('<', ()-> (dp -= 1; if dp < 0 running = false end)), ('+', ()-> data[dp] += 1),
        ('-', ()-> data[dp] -= 1), (',', ()-> (data[dp] = read(stdin, UInt8))),
        ('.', ()->print(Char(data[dp]))),
        ('/', ()-> (d = Int(direction); d += (iseven(d) ? 3 : 5); direction = Direction(d % 4))),
        ('\\', ()-> (d = Int(direction); d += (iseven(d) ? 1 : -1); direction = Direction(d))),
        ('!', () -> ipnext()), ('?', ()-> if data[dp] == 0 ipnext() end),
        ('@', ()-> push!(stack, (ipx, ipy, direction))),
        ('#', ()-> if length(stack) > 0 (ipx, ipy, direction) = pop!(stack) end),
        ('\n', ()-> (running = false))])

    inboundsx(plus) = (plus ? (ipx < lmax) : (ipx > 1)) ? true : exit(data[dp])
    inboundsy(plus) = (plus ? (ipy < length(lines)) : (ipy > 1)) ? true : exit(data[dp])
    function ipnext()
        if direction == right && inboundsx(true)     ipx += 1
        elseif direction == left && inboundsx(false) ipx -= 1
        elseif direction == down && inboundsy(true)  ipy += 1
        elseif direction == up && inboundsy(false)   ipy -= 1
        end
    end

    running = true
    while running
        cmdcode = lines[ipy][ipx]
        if haskey(instruction, cmdcode)
            instruction[cmdcode]()
        end
        ipnext()
    end
    exit(data[dp])
end

snusp(100, echo2)


const DUMMY_PRECISION = 1e-12

Base.isfinite(a::AbstractVec) = all(isfinite.(a))
Base.isinf(a::AbstractVec) = any(isinf.(a))

function StaticArrays.similar_type(::Type{V}, ::Type{F}, size::Size) where {V<:AbstractVec, F<:AbstractFloat}
    # TODO: implement this for other types
    if size == Size(V) && eltype(V) == F
        return V
    else
        error("StaticArrays.similar_type is not yet implemented for Vec except for Float64 and matching size.")
    end
end

Base.abs(a::AbstractVec) = error("abs(v::AbstractVec) has been removed. Use norm(v) to get the norm; abs.(v) to get the element-wise absolute value.")


export matmul, matmulTT, matmulNT, matmulTN

immutable LinAlg <: AFImpl
    matmul::Ptr{Void}

    function LinAlg(ptr)
        new(
            Libdl.dlsym(ptr, :af_matmul)
        )
    end
end

function matmul{B}(
    lhs::AFArray{B},
    rhs::AFArray{B},
    optLhs = AF_MAT_NONE,
    optRhs = AF_MAT_NONE)
    verifyAccess(lhs)
    verifyAccess(rhs)
    af = lhs.af
    result = af.results.ptr
    err = ccall(af.linAlg.matmul,
        Cint, (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Void}, Int32, Int32),
        result, lhs.ptr, rhs.ptr, optLhs, optRhs)
    assertErr(err)
    array(af, result[])
end

matmulTT(lhs, rhs) = matmul(lhs, rhs, AF_MAT_TRANS, AF_MAT_TRANS)

matmulTN(lhs, rhs) = matmul(lhs, rhs, AF_MAT_TRANS, AF_MAT_NONE)

matmulNT(lhs, rhs) = matmul(lhs, rhs, AF_MAT_NONE, AF_MAT_TRANS)


using MathOptInterface
const MOI = MathOptInterface
const MOIT = MOI.Test
const MOIU = MOI.Utilities
const MOIB = MOI.Bridges

juniper = DefaultTestSolver(log_levels=[:Table])

const optimizer = Juniper.Optimizer(juniper)

@testset "SolverName" begin
    @test MOI.get(optimizer, MOI.SolverName()) == "Juniper"
end

const config = MOIT.TestConfig(atol=1e-4, rtol=1e-4, optimal_status=MOI.LOCALLY_SOLVED)

@testset "Unit" begin
    bridged = MOIB.full_bridge_optimizer(optimizer, Float64)
    # A number of test cases are excluded because loadfromstring! works only
    # if the solver supports variable and constraint names.
    exclude = [
                "delete_nonnegative_variables", # delete and ConstraintFunction not supported
                "delete_soc_variables", # Soc and delete not supported
                "delete_variable", # Deleting not supported.
                "delete_variables", # Deleting not supported.
                "getvariable", # Variable names not supported.
                "solve_zero_one_with_bounds_1", # Variable names not supported.
                "solve_zero_one_with_bounds_2", # Variable names not supported.
                "solve_zero_one_with_bounds_3", # Variable names not supported.
                "getconstraint", # Constraint names not suported.
                "variablenames", # Variable names not supported.
                "solve_with_upperbound", # loadfromstring!
                "solve_with_lowerbound", # loadfromstring!
                "solve_integer_edge_cases", # loadfromstring!
                "solve_affine_lessthan", # loadfromstring!
                "solve_affine_greaterthan", # loadfromstring!
                "solve_affine_equalto", # loadfromstring!
                "solve_affine_interval", # loadfromstring!
                "get_objective_function", # Function getters not supported.
                "solve_constant_obj",  # loadfromstring!
                "solve_blank_obj", # loadfromstring!
                "solve_singlevariable_obj", # loadfromstring!
                "solve_objbound_edge_cases", # ObjectiveBound not supported.
                "solve_affine_deletion_edge_cases", # Deleting not supported.
                "solve_unbounded_model", # `NORM_LIMIT`
                "solve_duplicate_terms_obj", # duplicate terms objective
                "solve_duplicate_terms_vector_affine",
                "solve_duplicate_terms_scalar_affine",
                "solve_single_variable_dual_max", # no support for dual atm
                "solve_single_variable_dual_min", # no support for dual atm
                "solve_result_index", # no support for dual atm
                "update_dimension_nonnegative_variables", # currently no support for ConstraintFunction
               ]
    MOIT.unittest(bridged, config, exclude)
end


# misc functions
function epoch(; data, tbl, τ, sfreq, kwargs...)
    epoch(data, tbl, τ, sfreq; kwargs...)
end

function epoch(
    data::Array{T,1},
    tbl::DataFrame,
    τ::Tuple{Number,Number},
    sfreq;
    kwargs...,
) where {T<:Union{Missing,Number}}
    data_r = reshape(data, (1, :))
    epoch(data_r, tbl, τ, sfreq; kwargs...)
end

epoch(data::Matrix, tbl::DataFrame, τ::Vector, sfreq; eventtime) =
    epoch(data, tbl, Tuple(τ...), sfreq; eventtime = eventtime)
epoch(
    data::Matrix,
    tbl::DataFrame,
    τ::Tuple{Number,Number},
    sfreq;
    eventtime::String = "latency",
) = epoch(data, tbl, τ, sfreq; eventtime = Symbol(eventtime))

function epoch(
    data::Array{T,2},
    tbl::DataFrame,
    τ::Tuple{Number,Number},
    sfreq;
    eventtime::Symbol = :latency,
) where {T<:Union{Missing,Number}}
    # data: channels x times

    # partial taken from EEG.jl

    numEpochs = size(tbl, 1)
    τ = round_times(τ, sfreq)

    times = range(τ[1], stop = τ[2], step = 1 ./ sfreq)
    lenEpochs = length(times)
    numChans = size(data, 1)
    epochs = Array{Union{Missing,Float64}}(
        missing,
        Int(numChans),
        Int(lenEpochs),
        Int(numEpochs),
    )

    # User feedback
    @debug "Creating epochs: $numChans x $lenEpochs x $numEpochs"

    for si = 1:size(tbl, 1)
        #eventonset = tbl[si,eventtime] # in samples
        #d_start = eventonset
        d_start = Int(round(tbl[si, eventtime] - sum(times .< 0)))
        d_end = Int(round(tbl[si, eventtime] + lenEpochs - 1 - sum(times .< 0)))
        e_start = 1
        e_end = lenEpochs
        #println("d: $(size(data)),e: $(size(epochs)) | $d_start,$d_end,$e_start,$e_end | $(tbl[si,eventtime])")
        if d_start < 1
            e_start = e_start + (-d_start + 1)
            d_start = 1
        end
        if d_end > size(data, 2)
            e_end = e_end - (d_end - size(data, 2))
            d_end = size(data, 2)
        end
        #println("d: $(size(data)),e: $(size(epochs)) | $d_start,$d_end,$e_start,$e_end | $(tbl[si,eventtime])")
        epochs[:, e_start:e_end, si] = data[:, d_start:d_end]
    end
    return (epochs, times)
end

function round_times(τ, sfreq)
    # function to round τ to sfreq samples. This specifies the epoch length.
    # its a function to be the same for epoch & timeexpanded analyses
    return round.(τ .* sfreq) ./ sfreq

end
function dropMissingEpochs(X, y)
    missingIx = .!any(ismissing.(y), dims = (1, 2))
    print(size(missingIx))
    goodIx = dropdims(missingIx, dims = (1, 2))
    return X[goodIx, :], Array{Float64}(y[:, :, goodIx])
end

"""
$(SIGNATURES)
Flatten a 1D array from of a 2D/3D array. Also drops the empty dimension
"""
function linearize(x::AbstractArray)
    # used in condense_long to generate the long format
    return dropdims(reshape(x, :, 1), dims = 2)
end
function linearize(x::String)
    return x
end

"""
$(SIGNATURES)
Equates the length of data and designmatrix by cutting the shorter one

The reason we need this is because when generating the designmatrix, we do not know how long the data actually are. We only assume that event-latencies are synchronized with the data
"""
function zeropad(X, data::AbstractArray{T,2}) where {T<:Union{Missing,<:Number}}
    @debug("2d zeropad")
    if size(X, 1) > size(data, 2)
        X = X[1:size(data, 2), :]
    else
        data = data[:, 1:size(X, 1)]
    end
    return X, data
end
function zeropad(X, data::AbstractVector{T}) where {T<:Union{Missing,<:Number}}
    @debug("1d zeropad")
    if size(X, 1) > length(data)
        X = X[1:length(data)]
    else
        data = data[1:size(X, 1)]
    end
    return X, data
end

function zeropad(X, data::AbstractArray{T,3}) where {T<:Union{Missing,<:Number}}
    @debug("3d zeropad")
    if size(X, 1) > size(data, 3)
        X = X[1:size(data, 3), :]
    else
        data = data[:, :, 1:size(X, 1)]
    end
    return X, data
end

function clean_data(
    data::AbstractArray{T,2},
    winrej::AbstractArray{<:Number,2},
) where {T<:Union{Float64,Missing}}
    data = Array{Union{Float64,Missing}}(data)
    for row = 1:size(winrej, 1)
        data[:, Int.(winrej[row, 1]:winrej[row, 2])] .= missing
    end
    return data
end


module NSum

using Grid
#using Cubature
#using Base.Collections
#using Distributions
import Base: ndims, show, length, collect

export nsum

typealias Location Vector{Int}
typealias Dimension Vector{Int}
#mins :: NTuple{K,Int}

type EvalPoint
    location::Location
    vals::Vector{Float64}
end

abstract Region{K}

type RegionTree{K} <: Region{K}
    root::Location
    sub1::Region{K}
    sub2::Region{K}
    level::Int
    dims::Dimension
    cutdim::Int
end

type RegionLeaf{K} <: Region{K}
    root::Location
    evals::Vector{EvalPoint}
    level::Int
    dims::Dimension
end

function _split(root, dims, level, cut_plane=1)
    newdims = copy(dims)
    K = length(dims)
    newdims[cut_plane]/=2
    newroot = copy(root)
    newroot[cut_plane]+=newdims[cut_plane]
    #@show cut_plane, root, newroot, dims, newdims
    leaf1 = RegionLeaf{K}(root, [], level+1, newdims)
    leaf2 = RegionLeaf{K}(newroot, [], level+1, newdims)
    # This is already handled by tree_from_leaf. FIXME Delete
    #for (i,c) in enumerate(Counter(zeros(K).+2))
        ## Now generate all points for leaf1 and leaf2
        #newpt = (c .- 1) .* div(newdims,2)
        #p1 = newpt .+ leaf1.root
        #p2 = newpt .+ leaf2.root

        #push!(leaf1.evals, EvalPoint(p1,[]))
        #push!(leaf2.evals, EvalPoint(p2,[]))
    #end
    return (leaf1, leaf2)
end
split_empty_tree(r::Region, cut_plane=1) = _split(r.root, r.dims, r.level, cut_plane)

function RegionTree(dims::Dimension)
    k = length(dims)
    leaf1, leaf2 = _split(zeros(k), dims, 1)
    RegionTree{k}(zeros(k), leaf1, leaf2, 1, dims, 1)
end

ndims{K}(::Type{RegionLeaf{K}}) = K
ndims{K}(::Type{RegionTree{K}}) = K
ndims(r::Region) = ndims(typeof(r))

graball(curr,state) = push!(state, curr)
collect(r::Region) = (allnodes = Region[]; dfs(graball, allnodes, r); allnodes)

function show(io::IO, r::Region)
    if isa(r, RegionTree)
        allnodes = collect(r)
    else
        allnodes = [r]
    end

    for i=r.level:maxdepth(r)
        print(io, "Level $i: ")
        levelnodes = filter(allnodes) do x
            x.level == i
        end
        for n in sort(levelnodes, by=x->x.root, lt=lexless)
            print(io, n.root)
            if isa(n, RegionLeaf) && !isempty(n.evals[1].vals)
                print(io, n.evals[1].vals)
            end
        end
        println(io, "")
    end
end

function tree_from_leaf{K}(r::RegionLeaf{K}, cut_plane)
    leaf1, leaf2 = _split(r.root, r.dims, r.level, cut_plane)
    # Copy evals from r into leaf1 and leaf2
    #leaf1_buf = EvalPoints[]
    #leaf2_buf = EvalPoints[]
    #for (i,c) in enumerate(Counter(zeros(K).+2))
        ## first save all pre-computed points
        #if c[cut_plane] == 1 # r.evals[i] point goes to leaf1
            #push!(leaf1_buf, r.evals[i])
        #else
            #push!(leaf2_buf, r.evals[i])
        #end
    #end
    newdims = leaf1.dims
    for (i,c) in enumerate(Counter(zeros(K).+2))
        # Now generate all points for leaf1 and leaf2
        newpt = (c .- 1) .* div(newdims,2)
        p1 = newpt .+ leaf1.root
        p2 = newpt .+ leaf2.root

        p1prev = p2prev = false
        # Have we already computed p1?
        for pt in r.evals
            if p1 == pt.location
                p1prev = true
                push!(leaf1.evals, pt)
            elseif p2 == pt.location
                p2prev = true
                push!(leaf2.evals, pt)
            end
        end
        p1prev || push!(leaf1.evals, EvalPoint(p1,[]))
        p2prev || push!(leaf2.evals, EvalPoint(p2,[]))
    end
    RegionTree{K}(r.root, leaf1, leaf2, r.level, r.dims, cut_plane)
end

function pick_cut_plane(r::RegionLeaf)
    K = ndims(r)
    assert(any(r.dims .> 2))
    candidates = find(x->x>2, r.dims) # only consider directions with enough space
    if rand()<0.2 || length(r.evals) == 0 || any(x->isempty(x.vals), r.evals) # no choice but to pick randomly
        return -Inf, candidates[rand(1:length(candidates))]
    else
        discrep = -Inf
        direc = -1
        #@show r.root, length(r.evals), r.evals
        for eval in r.evals[2:end-1]
            poss_direc = findfirst(eval.location .- r.evals[1].location)
            #@show poss_direc, eval
            r.dims[poss_direc] == 2 && continue
            grad = r.evals[1].vals[end] - eval.vals[end]
            if grad > discrep
                discrep = grad
                direc = poss_direc
            end
        end
        return discrep, direc
    end
end

function cut!(r::RegionTree)
    if isa(r.sub1, RegionTree)
        cut!(r.sub1)
    elseif any(r.sub1.dims .> 2) #RegionLeaf, so we have to cut from above
        _, cut_plane = pick_cut_plane(r.sub1)
        r.sub1 = tree_from_leaf(r.sub1, cut_plane)
    end

    # argh.. I know about DRY, but how?
    if isa(r.sub2, RegionTree)
        cut!(r.sub2)
    elseif any(r.sub2.dims .> 2) #RegionLeaf, so we have to cut from above
        _, cut_plane = pick_cut_plane(r.sub2)
        r.sub2 = tree_from_leaf(r.sub2, cut_plane)
    end
    nothing
end

function dfs(f::Function, state, r::RegionLeaf, onlyleaves=false)
    f(r, state)
end

function dfs(f::Function, state, r::RegionTree, onlyleaves=false)
    !onlyleaves && f(r, state)
    tovisit = Region[r.sub1, r.sub2]
    while length(tovisit) > 0
        curr = pop!(tovisit)
        if !onlyleaves # run on all
            f(curr, state)
        elseif isa(curr,RegionLeaf) # or only on leaves
            f(curr, state)
        end
        isa(curr, RegionTree) && push!(tovisit, curr.sub1, curr.sub2)
    end
end

function recursive_dfs(f::Function, state, r::RegionTree, onlyleaves=false)
    assert(isa(state, Dict))
    if !haskey(state, :history)
        state[:history] = Region[r]
    else
        push!(state[:history],r)
    end
    !onlyleaves && f(r, state)
    recursive_dfs(f, state, r.sub1, onlyleaves)
    recursive_dfs(f, state, r.sub2, onlyleaves)
    pop!(state[:history]) # clean up after ourselves
end

function recursive_dfs(f::Function, state, r::RegionLeaf, onlyleaves=false)
    push!(state[:history],r)
    f(r, state)
    pop!(state[:history]) # clean up after ourselves
end

function maxdepth_(r::Region, state)
    if r.level > state[1]
        state[1] = r.level
    end
end

function minlen_(r::Region, state)
    if r.len < state[1]
        state[1] = r.len
    end
end

length_(r::Region, state) = state[1] += 1
length(r::Region) = (state = {0}; dfs(length_, state, r); state[1])

maxdepth(r::Region) = (state = {0}; dfs(maxdepth_, state, r); state[1])
minlen(r::Region) = (state = {typemax(Int)}; dfs(minlen_, state, r); state[1])

function treesum(r::Region, fdim)
    s = zeros(fdim)
    dfs(treesum_, s, r, true)
    return s
end

treesum_(r::RegionTree, state) = nothing
function treesum_(r::RegionLeaf, state)
    cellvolume = prod(r.dims) / 2^ndims(r)
    for eval in r.evals
        state[:] += eval.vals * cellvolume
    end
end

addifdirty(r::RegionTree, state) = nothing
function addifdirty(r::RegionLeaf, state)
    assert(length(r.evals) > 0)
    for eval in r.evals
        if length(eval.vals)==0 # Don't want to add already computed ones
            push!(state, eval)
        end
    end
end

function max_(r::RegionLeaf, state)
    if all(r.dims .== 2) # Ignore fully explored regions
        return
    end
    k = ndims(r)
    buf = zeros(state[:fdim])
    treesum_(r, buf)
    if !haskey(state,:maxhistory) || buf[end] > state[:maxvalue]
        state[:maxhistory] = copy(state[:history])
        state[:maxvalue] = buf[end]
    end
end

function find_max(r::Region, fdim)
    s = {:fdim=>fdim}
    recursive_dfs(max_, s, r, true)
    !haskey(s,:maxhistory) && return r # We've evaluated everything
    return s[:maxhistory]
end

function find_min_containing(r::Region, pt)
    @assert all(0 .<= pt .- r.root .< r.dims)
    while true
        r = pt[r.cutdim] .< r.dims[r.cutdim] ? r.sub1 : r.sub2
        isa(r, RegionLeaf) && return r
    end
end

function find_uneven_branch(r::RegionTree)
    # we are still high, so we need to recurse downwards
    # return maximum discrepancy branch:
    path1 = find_uneven_branch(r.sub1)
    path2 = find_uneven_branch(r.sub2)
    maxpath = path1[1] > path2[1] ? path1 : path2
    push!(maxpath[2], r)
    return maxpath
end

function find_uneven_branch(r::RegionLeaf)
    if all(r.dims .== 2) # but we can't go lower
        return(-Inf, Region[r])
    else
        # Get a measure of discrepancy:
        discrep,direc = pick_cut_plane(r)
        return (discrep, Region[r])
    end
end

function nsum(fdim::Integer, f::Function, maxs; abstol=0.01, maxevals=30)
    #pseudocode:
    #create octree using closest larger point to maxs
    #subdivide a few times, adding points to evaluate to dirty list
    #evaluate in batch
    #update region values
    lens = map(nextpow2,iround(maxs))
    r = RegionTree(lens)
    for i=1:3
        cut!(r)
    end

    dirtylist = {}
    dfs(addifdirty, dirtylist, r, true)
    f(dirtylist)
    tots = zeros(fdim)
    dfs(treesum_, tots, r, true)
    #println("current estimate: $tots")

    count = 0
    findtype = 1
    totnum = fdim==1 ? 1 : fdim - 1
    while any(abs(tots[1:totnum].-1) .> abstol) && count < maxevals
        count += 1

        if findtype == 1
            maxr = find_max(r, fdim)
            r === maxr[end] && break # Exhausted everything
            maxr = maxr[end-1]
            #println("found max: $(maxr.root) w/ dims: $(maxr.dims)")

            #get regions in other 2^D-1 directions from max
            #others = Array(Any, 2^D-1)
            #resize!(others, 0)
            #for c in Counter(zeros(D).+2)
                #newloc = (c .- 1) .* 2 .-1 .+ maxr.mins
                #any(newloc .< 0) && continue
                #push!(others, newloc)
            #end
            ##println(others)

            ##subdivide these other regions
            #rs = map(others) do x
                #find_min_containing(r, x)
            #end
            cut!(maxr)
            cut!(maxr)
            findtype $= 1
        else
            dis, maxrs = find_uneven_branch(r)
            dis === -Inf && break # Exhausted everything
            maxr = maxrs[2] # 1 is the leaf
            #println("found uneven point: $(maxr.root) w/ dims: $(maxr.dims)")
            cut!(maxr)
            cut!(maxr)
            findtype $= 1
        end

        dirtylist = {}
        dfs(addifdirty, dirtylist, r, true)

        if length(dirtylist)>0
            f(dirtylist)
        end
        fill!(tots, 0.0)
        dfs(treesum_, tots, r, true)
        #println("Tots estimate: $tots")
    end
    #println("Finished $count iters")
    tots, r
end

end


module TestEnvironmentSampler

include("preamble.jl")
include("toyenv.jl")
#using LyceumBase: asvec, ntimesteps, collate

function extract_tid(batch)
    u = unique(batch.actions)
    @assert length(u) == 1
    @assert length(u[1]) == 1
    tid = u[1][1]
    return tid
end

unwrap(x) = (@assert length(x) == 1; x[])
function unwraptraj(τ::Trajectory)
    Trajectory(unwrap.(τ.S), unwrap.(τ.O), unwrap.(τ.A), τ.R, τ.sT, τ.oT, τ.done)
end

@testset "single threaded" begin
    ntimesteps = 30
    env_kwargs = (max_length = 10, reward_scale = 5)
    sampler = EnvironmentSampler(n -> ntuple(i -> ToyEnv(; env_kwargs...), n))

    i = 1
    B = rollout(sampler, ntimesteps, reset! = reset!, nthreads = 1) do a, o
        a .= i
        i += 1
    end
    A = StructArray(B)

    @test length(A) == ntimesteps / env_kwargs.max_length
    @test all(τ -> length(τ) == env_kwargs.max_length, A)

    U = map(unwraptraj, A)

    @test all(U) do τ
        τ.S == [0, cumsum(τ.A[1:end-1])...]
    end
    @test all(U) do τ
        τ.O == 0:length(τ)-1
    end
    @test let start = 1
        all(U) do τ
            pass = τ.A == start:(start+length(τ)-1)
            start += length(τ)
            pass
        end
    end
    @test all(U) do τ
        τ.R == env_kwargs.reward_scale .* τ.S
    end
    @test all(U) do τ
        τ.sT[] == sum(τ.A)
    end
    @test all(U) do τ
        τ.oT[] == length(τ)
    end
    @test all(τ -> τ.done, U)
end

@testset "multi-threaded (nthreads = $nthreads)" for nthreads in (
    Threads.nthreads(),
    div(Threads.nthreads(), 2),
)
    if Threads.nthreads() < 2
        @warn "Cannot test EnvironmentSampler multi-threading with Threads.nthreads() < 2"
    end

    env_kwargs = (
        max_length = 50,
        reward_scale = 5,
        step_hook = _ -> isodd(Threads.threadid()) && busyloop(0.001),
    )
    ntimesteps = 5 * Threads.nthreads() * env_kwargs.max_length
    sampler = EnvironmentSampler(n -> ntuple(i -> ToyEnv(; env_kwargs...), n))

    tcounts = zeros(Int, Threads.nthreads())
    B = rollout(sampler, ntimesteps, reset! = reset!, nthreads = nthreads) do a, o
        a .= Threads.threadid()
        tcounts[Threads.threadid()] += 1
    end
    A = StructArray(B)

    @test length(A) == ntimesteps / env_kwargs.max_length
    @test all(τ -> length(τ) == env_kwargs.max_length, A)

    U = map(unwraptraj, A)

    @test all(U) do τ
        τ.S == [0, cumsum(τ.A[1:end-1])...]
    end
    @test all(U) do τ
        τ.O == 0:length(τ)-1
    end
    @test all(τ -> length(unique(τ.A)) == 1, U)
    @test all(U) do τ
        τ.R == env_kwargs.reward_scale .* τ.S
    end
    @test all(U) do τ
        τ.sT[] == sum(τ.A)
    end
    @test all(U) do τ
        τ.oT[] == length(τ)
    end
    @test all(τ -> τ.done, U)

    nonempty = filter(B -> ntrajectories(B) > 0, sampler.buffers)
    # test that all threads were utilized
    @test length(nonempty) == nthreads
    @test begin
        minn, maxx = extrema(map(ntrajectories, nonempty))
        # test that the number of trajectories collected by each thread differs by at most 2
        maxx - minn <= 2
    end
end

end # module


function Grid=DGNodes(Grid,Trans,DG)
for iE=1:Grid.NumEdges
  Grid.Edges(iE).DGPoints=Trans(DG.xw,P1,P2,Grid);
end
end


module HMDB

using LightXML
using Printf
import NMR
import Dates
import LinearAlgebra

global HMDB_dir
global hmdb_root

mutable struct HMDBpeaks
    spectID::String
    accession::String
    name::String
    nucleus::String
    frequency::String
    solvent::String
    pH::String
    pks::Array{Float64,1}
    ints::Array{Float64,1}
end

function HMDBpeaks(pks,ints; spectID="", accession="", name="", nucleus="",
    frequency="",solvent="",pH="")

    return HMDBpeaks(spectID,accession,name,nucleus,frequency,
      solvent,pH,pks,ints)
end

function HMDBpeaks(spectrum::XMLElement; accession="", name="", normalisation=1)
        peaks=find_element(spectrum,"nmr-one-d-peaks");
        snucleus=content(find_element(spectrum,"nucleus"));
        ssolvent=content(find_element(spectrum,"solvent"));
        sfreq=content(find_element(spectrum,"frequency"));
        spH=content(find_element(spectrum,"sample-ph"));
        sid=content(find_element(spectrum,"id"));
        pks=[parse(Float64,content(find_element(x,"chemical-shift"))) for x in child_elements(peaks)];
        ints=[parse(Float64,content(find_element(x,"intensity"))) for x in child_elements(peaks)];
        ints .*= normalisation/sum(ints)
	return HMDBpeaks(pks,ints,name=name,accession=accession,nucleus=snucleus,
                spectID=sid,frequency=sfreq,solvent=ssolvent,pH=spH)
end

global refPeaks

function __init__()
	global hmdb_root=root(parse_file("$(@__DIR__)/HMDB_subset_with_NMR.xml"))
	global refPeaks=Dict{String,HMDBpeaks}()
	name=""
	for m in hmdb_root["metabolite"]
		name = content(find_element(m,"name"))
		acc = content(find_element(m,"accession"))
		refspect = find_element(m,"nmr-ref-1D1H")
		spectProtons = parse(Int64, attribute(refspect,"spectProtons"))
		peaks = HMDBpeaks(find_element(refspect,"nmr-one-d"),accession=acc,normalisation=spectProtons,name=name)
		push!(refPeaks, name => peaks)
	end

	println("HMDB initialised from $(@__DIR__)");
end

"""
	(score,iscore,alpha,std) = matchPeaks(p::NMR.PeakStruct,ref::HMDBpeaks;tol=0.001)

match the peaks in `p` to the reference `ref`. A tuple of four values is returned:
- `score`: sum of the intensities of the peaks found, normalised by the height of the largest reference peak.  Missing peaks are counted as negative
- `iscore`: score normalised by the total intensity of the reference peaks (range -1...1)
- `alpha`: fitted normalised concentration
- `std`: mean error of fitted normalised concentration
"""
function matchPeaks(p::NMR.PeakStruct,ref::HMDBpeaks;tol=0.001)
    d=ref.pks
    i=ref.ints
    length(d) == length(i) || throw("lengths if peak position and intensity lists must match")

    npeaks=length(p.positions)
    score=0.0;

    q2=0.0; # cumulative normalisation
    p2=0.0;
    Pk=0.0; # cumulative projection

    for k=1:length(d)
        closest=p.positions[1];
        cInt=p.intensities[1];

        r=searchsorted(p.positions,d[k])
        if first(r) > npeaks
            closest=p.positions[npeaks]
            cInt=p.intensities[npeaks];

        elseif first(r)==1 && isempty(r)
            closest=p.positions[1]
            cInt=p.intensities[1]

        elseif !isempty(r)
            closest=p.positions[first(r)]
            cInt=p.intensities[first(r)]
        else
            closest= abs(p.positions[first(r)]-d[k])<abs(p.positions[first(r)-1]-d[k]) ? p.positions[first(r)] : p.positions[first(r)-1]
            cInt= abs(p.positions[first(r)]-d[k])<abs(p.positions[first(r)-1]-d[k]) ? p.intensities[first(r)] : p.intensities[first(r)-1]

        end

        if abs(closest - d[k]) < tol
          score += i[k]
            q2 += i[k]*i[k]
            Pk += i[k]*cInt
            p2 += cInt*cInt
        else
            score -= i[k]
        end
    end
    return (score,score/sum(i),Pk/q2, (p2-Pk*Pk/q2)/q2/(length(d)-1),ref.name)
end

function matchPeaks(p::NMR.PeakStruct,name::String; opts...)
	return matchPeaks(p, refPeaks[name]; opts...)
end

"""
	xml=matchReport(p::NMR.PeakStruct,tol=0.01,AutoShift=true,iscoreCutoff=0,Id="",verbose=true)

match peaks in `p` against all metabolites in `HMDB.refPeaks`, and create a table of the results.
If `AutoShift` is set to `true` (default), the `p` is automatically shfited for the greatest overlap
with D-Glucose. `tol` is the matching tolerance for peak identification. Metabolites
with iscores below `iscoreCutoff` are not reported. Metabolites are listed in decreasing
order of concentration. `matchReport()` returns an XML object containing the listed data. `Id` is incorporated
into the XML object as an attribute, to facilitate later identification of the match report.
"""
function matchReport(pinput::NMR.PeakStruct;tol=0.01,AutoShift=true,iscoreCutoff=0,verbose=true,Id="")
	xml=new_element("HMDBMatchReport");
	set_attribute(xml,"Id",Id);
	set_attribute(xml,"Date","$(Dates.now())")
	if AutoShift
		δ=-0.02:0.000125:0.02
		m=[HMDB.matchPeaks(NMR.shift(pinput,x),HMDB.refPeaks["D-Glucose"],tol=0.01)[2] for x in δ]
		p=NMR.shift(pinput,δ[argmax(m)])
	else
		p=pinput
	end

	matches=[HMDB.matchPeaks(p,ref,tol=tol) for (names,ref) in HMDB.refPeaks];
	filter!(x->x[2]>0,matches);         # remove all metabolites for which iscore<0
	sort!(matches,by=x->x[3],rev=true); # sort by descending concentration

	if verbose
		@printf("Metabolite Matches\n")
		@printf("==================\n")
		@printf("%-30s % 10s % 10s % 10s\n\n","Name","Score","iScore","alpha");
	end

	for (score,iscore,alpha,delta,name) in matches
   	 	verbose && @printf("%-30s % 10.2f% 10.2f% 14.3f ±%10.3f\n",name,score,iscore,alpha,sqrt(abs(delta)))
		c=new_child(xml,"Match")
		set_attributes(c,Name=name,score="$(score)", iscore="$(iscore)", alpha="$(alpha)",stderr="$(sqrt(abs(delta)))")
	end
	return xml
end

"""
    s::Data1D = refSpectrum(name::String,range;lw=0.01)

compute a reference spectrum over `range` using the peak positions in
reference spectrum `name`. A Lorentzian line with half width `lw` is used. The
reference spectrum is returned as a real `Data1D` object, unless the option `complex=true` is given.
"""
function refSpectrum(name::String,range::Array{Float64,1};lw=0.005,excl=x->false, complex=false)
	p=refPeaks[name]
  ints=[]
  pks=[]
  # filter out excluded peaks
  npeaks=0;
  for k=1:length(p.pks)
    if(!excl(p.pks[k]))
        push!(pks,p.pks[k])
        push!(ints,p.ints[k])
        npeaks+=1
      end
  end
    if (complex)
        vals = sum(k -> ints[k]*[NMR.clorentzian(pks[k],1.0/lw^2,x) for x in range], 1:npeaks)
        d=NMR.Data1D{Complex{Float64},Float64}(vals,minimum(range),maximum(range))
   else
        vals = sum(k -> ints[k]*[NMR.lorentzian(pks[k],1.0/lw^2,x) for x in range], 1:npeaks)
        d=NMR.Data1D{Float64,Float64}(vals,minimum(range),maximum(range))
   end
	return d
end

function refSpectrum(name::String,d::NMR.Data1D;opts...)
	return refSpectrum(name,collect(NMR.ind(d));opts...)
end

"""
	decompositionMatrix(names,range)

compute a decomposition matrix B such that B s  is the vector of concentrations
of the metabolites listed in `names`. The pseudo-inverse is used for this purpose.
"""
function decompositionMatrix(mlist,range;opts...)
	B=zeros(length(range),length(mlist))
	k=1
	for m in mlist
		r=HMDB.refSpectrum(m,range;opts...)
  	 	B[:,k]=NMR.val(r)
		k=k+1
	end
	return LinearAlgebra.pinv(B)
end

end


using ..ChemistryFeaturization.AbstractType:
    AbstractFeatureDescriptor, AbstractFeaturization
using ..ChemistryFeaturization.FeatureDescriptor
import ..ChemistryFeaturization.FeatureDescriptor: output_shape
using ..ChemistryFeaturization.Atoms: AtomGraph
using ..ChemistryFeaturization.Utils.ElementFeatureUtils

export GraphNodeFeaturization, encode
export encodable_elements, decode, chunk_vec, output_shape

using DataFrames

"""
    GraphNodeFeaturization(encoded_atom_features::Vector{AtomFeatureDescriptor})
    GraphNodeFeaturization(feature_names, lookup_table, nbins, logspaced, categorical)

A featurization for AtomGraph objects that encodes features associated with each node. Contains a collection of `AbstractAtomFeatureDescriptor` objects, and can be initialized by passing those, or by passing parameters for constructing them.

## Required Arguments
- `feature_names::Vector{String}`: Names of each feature
- `lookup_table::DataFrame` (optional): source of data for features, if not included in built-in `atom_data_df`

## Keyword Arguments
- `nbins::Union{Vector{Integer},Integer}`: Number of bins to use for one-hot encoding of continuous-valued features. Will be ignored for categorical features.
- `logspaced::Union{Vector{Bool},Bool}`: Whether to logarithmically space the bins
- `categorical::Union{Vector{Bool},Bool}`: Whether each feature is categorical or continous-valued.
"""
struct GraphNodeFeaturization <: AbstractFeaturization
    features::Vector{<:AbstractAtomFeatureDescriptor} # TODO: this might need another name
end

# NB: this constructor only works if every feature is an ElementFeature
function GraphNodeFeaturization(
    feature_names::Vector{String},
    lookup_table::Union{DataFrame,Nothing} = nothing;
    nbins::Union{Vector{<:Integer},Integer,Nothing} = nothing,
    logspaced::Union{Vector{Bool},Bool,Nothing} = nothing,
    categorical::Union{Vector{Bool},Bool,Nothing} = nothing,
)
    num_features = length(feature_names)
    local lookup_table_here, logspaced_here, categorical_here, nbins_here
    if isnothing(lookup_table)
        lookup_table_here = atom_data_df
    else
        # need to merge them in case some data is in one place and some the other
        lookup_table_here = outerjoin(atom_data_df, lookup_table, on = :Symbol)
    end

    if isnothing(logspaced)
        logspaced_here = map(fn -> default_log(fn, lookup_table_here), feature_names)
    else
        logspaced_here = get_param_vec(logspaced, num_features)
    end

    if isnothing(categorical)
        categorical_here =
            map(fn -> default_categorical(fn, lookup_table_here), feature_names)
    else
        categorical_here = get_param_vec(categorical, num_features)
    end

    if isnothing(nbins)
        nbins_here = [default_nbins for i = 1:num_features]
    else
        nbins_here = get_param_vec(nbins, num_features, pad_val = default_nbins)
    end

    afs = map(zip(feature_names, nbins_here, logspaced_here, categorical_here)) do args
        ElementFeatureDescriptor(
            args[1],
            lookup_table_here,
            nbins = args[2],
            logspaced = args[3],
            categorical = args[4],
        )
    end
    GraphNodeFeaturization(afs)
end

# pretty printing, short
function Base.show(io::IO, fzn::GraphNodeFeaturization)
    st = "GraphNodeFeaturization encoding $(length(fzn.features)) features"
    print(io, st)
end

# pretty printing, long
function Base.show(io::IO, ::MIME"text/plain", fzn::GraphNodeFeaturization)
    st = "GraphNodeFeaturization encoding $(length(fzn.features)) features:\n"
    for feature in fzn.features
        st = string(st, "\t", feature, "\n")
    end
    st = st[1:end-1]
    print(io, st)
end

output_shape(fzn::GraphNodeFeaturization) = sum(output_shape.(fzn.features))

encodable_elements(fzn::GraphNodeFeaturization) =
    intersect([encodable_elements(f) for f in fzn.features]...)

"""
    chunk_vec(vec, nbins)

Helper function that divides up an already-constructed feature vector into "chunks" (one for each feature) of lengths specified by the vector nbins.

Sum of nbins should be equal to the length of vec.

# Examples
```jldoctest
julia> chunk_vec([1,0,0,1,0], [3,2])
2-element Array{Array{Bool,1},1}:
 [1, 0, 0]
 [1, 0]
 ```
"""
function chunk_vec(vec::Vector{<:Real}, nbins::Vector{<:Integer})
    chunks = fill(Bool[], size(nbins, 1))
    @assert length(vec) == sum(nbins) "Total number of bins doesn't match length of feature vector."
    for i = 1:size(nbins, 1)
        if i == 1
            start_ind = 1
        else
            start_ind = sum(nbins[1:i-1]) + 1
        end
        chunks[i] = vec[start_ind:start_ind+nbins[i]-1]
    end
    return chunks
end

function encode(fzn::GraphNodeFeaturization, ag::AtomGraph)
    encoded = reduce(vcat, map((x) -> encode(x, ag), fzn.features))
    return encoded
end

function decode(fzn::GraphNodeFeaturization, encoded::Matrix{<:Real})
    num_atoms = size(encoded, 2)
    nbins = [output_shape(f) for f in fzn.features]
    local decoded = Dict{Integer,Dict{String,Any}}()
    for i = 1:num_atoms
        #println("atom $i")
        chunks = chunk_vec(encoded[:, i], nbins)
        decoded[i] = Dict{String,Any}()
        for (chunk, f) in zip(chunks, fzn.features)
            #println("    $(f.name): $(decode(f, chunk))")
            decoded[i][f.name] = decode(f, chunk)
        end
    end
    return decoded
end


function snap_1_d(filename::String, data::Dict{Any,Any})

    f = open(filename)

    seek(f, 264)

    N = sum(data["Header"]["npart"])
    skipsize = read(f, Int32)
    bit_size = skipsize/(3*N)

    if Int(bit_size) == 4

    elseif Int(bit_size) == 8

    else
        println("read error! neither 32 nor 64 bits data!")
        return -1
    end

    # read positions
    for i in 1:length(data["Header"]["PartTypes"])

        if data["Header"]["npart"][i] != 0

            data[data["Header"]["PartTypes"][i]] = Dict()
            N = Int64(data["Header"]["npart"][i])
            #dummy = read(f, Float32, (3,N))

            if Int(bit_size) == 4
                data[data["Header"]["PartTypes"][i]]["POS"] = copy(transpose(read!(f, Array{Float32,2}(undef,(3,N)))))
            else
                data[data["Header"]["PartTypes"][i]]["POS"] = copy(transpose(read!(f, Array{Float64,2}(undef,(3,N)))))
            end

        end

    end

    p = position(f)

    # skip identifiers
    seek(f, p+8)

    # read Velocities
    for i in 1:length(data["Header"]["PartTypes"])

        if data["Header"]["npart"][i] != 0

            N = Int64(data["Header"]["npart"][i])

            if Int(bit_size) == 4
                data[data["Header"]["PartTypes"][i]]["VEL"] = copy(transpose(read!(f, Array{Float32,2}(undef,(3,N)))))
            else
                data[data["Header"]["PartTypes"][i]]["VEL"] = copy(transpose(read!(f, Array{Float64,2}(undef,(3,N)))))
            end

        end

    end

    #dummy = 0
    #gc()

    p = position(f)

    # skip identifiers
    seek(f, p+8)

    # Read IDs
    for i in 1:length(data["Header"]["PartTypes"])

        if data["Header"]["npart"][i] != 0

            N = Int64(data["Header"]["npart"][i])
            data[data["Header"]["PartTypes"][i]]["ID"] = copy(transpose(read!(f, Array{UInt32,2}(undef,(1,N)))))

        end

    end

    p = position(f)

    # skip identifiers
    seek(f, p+8)

    # Read IDs
    for i in 1:length(data["Header"]["PartTypes"])

        if data["Header"]["npart"][i] != 0

            if data["Header"]["massarr"][i] == 0

                N = Int64(data["Header"]["npart"][i])

                if Int(bit_size) == 4
                    data[data["Header"]["PartTypes"][i]]["MASS"] = copy(transpose(read!(f, Array{Float32,2}(undef,(1,N)))))
                else
                    data[data["Header"]["PartTypes"][i]]["MASS"] = copy(transpose(read!(f, Array{Float64,2}(undef,(1,N)))))
                end

            else

                N = Int64(data["Header"]["npart"][i])
                data[data["Header"]["PartTypes"][i]]["MASS"] = zeros(N)
                data[data["Header"]["PartTypes"][i]]["MASS"] .= data["Header"]["massarr"][i]

            end

        end

    end

    if data["Header"]["npart"][1] != 0

        # skip identifiers
        p = position(f)
        seek(f, p+8)

        N = Int64(data["Header"]["npart"][1])

        # read U
        if Int(bit_size) == 4
            data["PartType0"]["InternalEnergy"] = copy(transpose(read!(f, Array{Float32,2}(undef,(1,N)))))
        else
            data["PartType0"]["InternalEnergy"] = copy(transpose(read!(f, Array{Float64,2}(undef,(1,N)))))
        end

        # skip identifiers
        p = position(f)
        seek(f, p+8)

        # Read Density
        if Int(bit_size) == 4
            data["PartType0"]["Density"] = copy(transpose(read!(f, Array{Float32,2}(undef,(1,N)))))
        else
            data["PartType0"]["Density"] = copy(transpose(read!(f, Array{Float64,2}(undef,(1,N)))))
        end

        # skip identifiers
        p = position(f)
        seek(f, p+8)

        # Read SmoothingLength
        if Int(bit_size) == 4
            data["PartType0"]["SmoothingLength"] = copy(transpose(read!(f, Array{Float32,2}(undef,(1,N)))))
        else
            data["PartType0"]["SmoothingLength"] = copy(transpose(read!(f, Array{Float64,2}(undef,(1,N)))))
        end

    end

    close(f)

    return data

end


module C
const NAMES = [
    "kCp0"
    "kCpB0"
    "kCpM0"
    "kCpBM0"
    "kCpS"
    "kCpBS"
    "kCpMS"
    "kCpBMS"
    "kmCpS"
    "kmCpBS"
    "kmCpMS"
    "kmCpBMS"
    "kCpu"
    "kCpBu"
    "kCpMu"
    "kCpBMu"
    "kmCpu"
    "kmCpBu"
    "kmCpMu"
    "kmCpBMu"

    "kBaM"
    "kCaB"
    "kCaM"
    "kCBaM"
    "kCMaB"
    "kCaBM"
    "kCpaBM"
    "kCpaB"
    "kCpaM"
    "kCpBaM"
    "kCpMaB"
    "kBdM"
    "kCdB"
    "kCdM"
    "kCBdM"
    "kCMdB"
    "kCpdB"
    "kCpdM"
    "kCpBdM"
    "kCpMdB"
    "kCdBM"
    "kCpdBM"

    "kTp0"
    "kCTpS"
    "kCBTpS"
    "kCMTpS"
    "kCBMTpS"
    "kmCTpS"
    "kmCBTpS"
    "kmCMTpS"
    "kmCBMTpS"
    "kTpIKK1"
    "kmTpIKK1"
    "kTpu"
    "kmTpu"
    "kTpC0"
    "kCTpCS"
    "kCBTpCS"
    "kCMTpCS"
    "kCBMTpCS"
    "kmCTpCS"
    "kmCBTpCS"
    "kmCMTpCS"
    "kmCBMTpCS"
    "kTpCIKK1"
    "kmTpCIKK1"
    "kTpCIKK2"
    "kmTpCIKK2"
    "kTpCIKK3"
    "kmTpCIKK3"
    "kTpCu"
    "kmTpCu"
    "kTaB"
    "kTaM"
    "kTaBM"
    "kTpaB"
    "kTpaM"
    "kTpaBM"
    "kTCd"
    "kTpCd"

    "kIp0"
    "kIpTAKp"
    "kmIpTAKp"
    "kIpTAKpC"
    "kmIpTAKpC"
    "kIpu"
    "kmIpu"
    "kICp0"
    "kICpTAKp"
    "kmICpTAKp"
    "kICpTAKpC"
    "kmICpTAKpC"
    "kIpCu"
    "kmIpCu"
    "kIaB"
    "kIaM"
    "kIaBM"
    "kIpaB"
    "kIpaM"
    "kIpaBM"
    "kICd"
    "kIpCd"
    "kIpCfaIKKpC"
    "kmIpCfaIKKpC"
    "kIpCfaIKKppC"
    "kmIpCfaIKKppC"
    "kIppCu"
    "kmIppCu"
    "kIppCd"
    "kIppChf"
    "kmIppChf"
    "kIpphf"
    "kmIpphf"
    "kIir"
    "kmIir"
    "kIpA20"
    "kmIpA20"
    "kIpCA20"
    "kmIpCA20"

    "kassaikk"
    "kassaikknfkb"
    "kassanfkbikk"
    "kdisaikk"
    "kdisaikknfkb"
    "kdisanfkbikk"
    "kassa"
    "kdisa"
    "kdegbounda"
    "kdegboundaIKK"
    "kdegfreea"
    "kdegfreeaIKK"
    "kshutboundikbain"
    "kshutboundikbaout"
    "kshutfreeikbain"
    "kshutfreeikbaout"
    "k0mrnaikba"
    "kdegmrnaikba"
    "khillprodmrnaikba"
    "kpikba"
    "kprodmrnaikba"

    "kassbikk"
    "kassbikknfkb"
    "kassbnfkbikk"
    "kdisbikk"
    "kdisbikknfkb"
    "kdisbnfkbikk"
    "kassb"
    "kdegboundb"
    "kdegboundbIKK"
    "kdegfreeb"
    "kdegfreebIKK"
    "kdisb"
    "kshutboundikbbin"
    "kshutboundikbbout"
    "kshutfreeikbbin"
    "kshutfreeikbbout"
    "k0mrnaikbb"
    "kdegmrnaikbb"
    "khillprodmrnaikbb"
    "kpikbb"
    "kprodmrnaikbb"
    "kasseikk"
    "kasseikknfkb"
    "kassenfkbikk"
    "kdiseikk"
    "kdiseikknfkb"
    "kdisenfkbikk"

    "kasse"
    "kdegbounde"
    "kdegboundeIKK"
    "kdegfreee"
    "kdegfreeeIKK"
    "kdise"
    "kshutboundikbein"
    "kshutboundikbeout"
    "kshutfreeikbein"
    "kshutfreeikbeout"
    "k0mrnaikbe"
    "kdegmrnaikbe"
    "khillprodmrnaikbe"
    "kpikbe"
    "kprodmrnaikbe"

    "kshutnfkbin"
    "kshutnfkbout"

    "k0mrnaa20"
    "kdegmrnaa20"
    "kprodmrnaa20"
    "kpa20"
    "kdega20"
    "khillprodmrnaa20"
    "delayrnae"
    "X"
    "term"
]

for (idx,name) in enumerate(NAMES)
    eval(Meta.parse("const $name = $idx"))
end

const NUM = length(NAMES)

end  # module


# This is where most of the code from PeaceVote will go

# abstract type AbstractPort end

### Could have additional information on regulation
struct BraiderConfig
    N::UInt8
    M::UInt8
    gk::GateKeeper
end

# DHasym, DHsym

struct BraidOfficer <: Officer
    sb::BraiderConfig
    bc::BraidChain
end

### This module actually specifies seal method with fold and unfold. I could use thoose for unfolding data to a Dictionary. It should be arguable whether it would not be better to type each signature

function SynchronicBallot.regulation(bo::BraidOfficer)

    N, M = bo.sb.N, bo.sb.M
    mix = bo.sb.gk.mix.id
    gk = bo.sb.gk.id
    hash = Hash(bo.bc)

    metadata = MetaData(mix, gk, hash)
    mbytes = fold(Dict(metadata))

    return Regulation(N, M, mbytes)
end

Base.in(id, bo::BraidOfficer) = id in pseudonyms(bo.bc)

function SynchronicBallot.audit!(bo::BraidOfficer, ballot::Ballot, signatures)

    braid = SynchronicBraid(ballot)
    contract = Contract(Dict[unfold(i) for i in signatures])
    transaction = Transaction(braid, contract)
    push!(bo.bc, transaction)

    return nothing
end

function validate(sb::SynchronicBraid, bo::BraidOfficer, id)
    bc, tlog = bo.sb, bo.bc

    metadata = sb.metadata

    validate(tlog, metadata.hash) || return false
    bc.gk.id == metadata.gk || return false
    bc.gk.mix.id == metadata.mix || return false

    id in sb.pseudonyms || return false

    return true
end

serve!(bo::BraidOfficer, signer::Signer) = SynchronicBallot.serve!(bo.sb.gk, bo, signer)

function braid(oldsigner::Signer, newsigner::Signer, bo::BraidOfficer)

    newid = id(newsigner)
    idbytes = Vector{UInt8}(newid, base=16, length=bo.sb.M)

    signbraid = (ballot) -> begin
        braid = SynchronicBraid(ballot)
        @assert validate(braid, bo, newid)
        signature = sign(braid, oldsigner)
        fold(Dict(signature))
    end

    vote(idbytes, bo.sb.gk, oldsigner, signbraid)

    return nothing
end


struct Sample{A<:Random.AbstractRNG,M<:AbstractModel,S<:AbstractSampler,K} <: Transducers.Transducer
    rng::A
    model::M
    sampler::S
    kwargs::K
end

function Sample(model::AbstractModel, sampler::AbstractSampler; kwargs...)
    return Sample(Random.GLOBAL_RNG, model, sampler; kwargs...)
end

"""
    Sample([rng, ]model, sampler; kwargs...)

Create a transducer that returns samples from the `model` with the Markov chain Monte Carlo
`sampler`.

# Examples

```jldoctest; setup=:(using AbstractMCMC: Sample)
julia> struct MyModel <: AbstractMCMC.AbstractModel end

julia> struct MySampler <: AbstractMCMC.AbstractSampler end

julia> function AbstractMCMC.step(rng, ::MyModel, ::MySampler, state=nothing; kwargs...)
           # all samples are zero
           return 0.0, state
       end

julia> transducer = Sample(MyModel(), MySampler());

julia> collect(transducer(1:10)) == zeros(10)
true
```
"""
function Sample(
    rng::Random.AbstractRNG,
    model::AbstractModel,
    sampler::AbstractSampler;
    kwargs...
)
    return Sample(rng, model, sampler, kwargs)
end

function Transducers.start(rf::Transducers.R_{<:Sample}, result)
    sampler = Transducers.xform(rf)
    return Transducers.wrap(
        rf,
        step(sampler.rng, sampler.model, sampler.sampler; sampler.kwargs...),
        Transducers.start(Transducers.inner(rf), result),
    )
end

function Transducers.next(rf::Transducers.R_{<:Sample}, result, input)
    t = Transducers.xform(rf)
    Transducers.wrapping(rf, result) do (sample, state), iresult
        iresult2 = Transducers.next(Transducers.inner(rf), iresult, sample)
        return step(t.rng, t.model, t.sampler, state; t.kwargs...), iresult2
    end
end

function Transducers.complete(rf::Transducers.R_{Sample}, result)
    _private_state, inner_result = Transducers.unwrap(rf, result)
    return Transducers.complete(Transducers.inner(rf), inner_result)
end


function choice_gradients(trace::VectorTrace{UnfoldType,T,U}, selection::Selection,
                        retval_grad) where {T,U}
    error("Not implemented")
end

function accumulate_param_gradients!(trace::VectorTrace{UnfoldType,T,U}, retval_grad) where {T,U}
    args = get_args(trace)
    (len, init_state, params) = unpack_args(args)

    kernel_has_grads = has_argument_grads(trace.gen_fn.kernel)
    if kernel_has_grads[1]
        error("Cannot differentiate with respect to index in unfold")
    end
    if kernel_has_grads[2]
        # backpropagation through the state not yet implemented
        error("Not implemented")
    end
    params_has_grad = kernel_has_grads[3:end]

    params_grad = Vector(undef, length(params_has_grad))
    for (i, has_grad) in enumerate(params_has_grad)
        if has_grad
            params_grad[i] = Vector(undef, len)
        else
            params_grad[i] = nothing
        end
    end

    for key=1:len
        subtrace = trace.subtraces[key]
        kernel_retval_grad = (retval_grad == nothing) ? nothing : retval_grad[key]
        kernel_arg_grad::Tuple = accumulate_param_gradients!(subtrace, kernel_retval_grad)
        @assert kernel_arg_grad[1] == nothing
        @assert kernel_arg_grad[2] == nothing
        for (i, (grad, has_grad)) in enumerate(zip(kernel_arg_grad[3:end], params_has_grad))
            if has_grad
                params_grad[i][key] = grad
            end
        end
    end
    (nothing, nothing, params_grad...)
end