"""
Code get from github:
https://github.com/JuliaGPU/CUDA.jl/issues/177#issuecomment-838311843
"""

import Zygote

import Base: _RepeatInnerOuter

@kernel function repeat_inner_kernel!(a::AbstractArray{<:Any, N}, inner::NTuple{N}, out) where {N}
    inds = @index(Global, NTuple)
    inds_a = ntuple(i -> (inds[i] - 1) ÷ inner[i] + 1, N)

    @inbounds out[inds...] = a[inds_a...]
end

function repeat_inner(a::TV, inner) where {TV<:AbstractArray}
    out = TV(undef, inner .* size(a))

    kernel! = if out isa CuArray
        repeat_inner_kernel!(CUDADevice(), 512)
    else
        repeat_inner_kernel!(CPU(), Threads.nthreads())
    end

    ev = kernel!(a, inner, out, ndrange=size(out))
    wait(ev)
    return out
end
# Non-cached coherent loads
@kernel function repeat_outer_kernel!(a::AbstractArray{<:Any, N}, sa::NTuple{N}, outer::NTuple{N}, out) where {N}
    inds = @index(Global, NTuple)
    inds_a = ntuple(i -> (inds[i] - 1) % sa[i] + 1, N)

    @inbounds out[inds...] = a[inds_a...]
end

function repeat_outer(a::TV, outer) where {TV<:AbstractArray}
    out = TV(undef, outer .* size(a))

    kernel! = if out isa CuArray
        repeat_outer_kernel!(CUDADevice(), 512)
    else
        repeat_outer_kernel!(CPU(), Threads.nthreads())
    end

    ev = kernel!(a, size(a), outer, out, ndrange=size(out))
    wait(ev)
    return out
end

# Overload methods used by `Base.repeat`.
# No need to implement `repeat_inner_outer` since this is implemented in `Base` as
# `repeat_outer(repeat_inner(arr, inner), outer)`.
function _RepeatInnerOuter.repeat_inner(a::CuArray{<:Any, N}, dims::NTuple{N}) where {N}
    return repeat_inner(a, dims)
end

function _RepeatInnerOuter.repeat_outer(a::CuArray{<:Any, N}, dims::NTuple{N}) where {N}
    return repeat_outer(a, dims)
end

function _RepeatInnerOuter.repeat_outer(a::CuVector, dims::Tuple{Any})
    return repeat_outer(a, dims)
end

function _RepeatInnerOuter.repeat_outer(a::CuMatrix, dims::NTuple{2, Any})
    return repeat_outer(a, dims)
end

### Adjoint implementation for `repeat`
@kernel function repeat_adjoint_gpu_kernel!(
    Δ::AbstractArray{T},
    inner::NTuple,
    outer::NTuple,
    out::AbstractArray{T},
    outsize::NTuple{N}
) where {N, T<:Real}
    dest_inds = @index(Global, NTuple)
    src_inds = ntuple(i -> mod1((dest_inds[i] - 1) ÷ inner[i] + 1, outsize[i]), N)
    CUDA.@atomic out[src_inds...] += Δ[dest_inds...]
end;

# FIXME: not threadsafe atm!!! And therefore not used anywhere (we only overload) the
# adjoint for `CuArray`. But if we have  something like `@atomic_addindex!`
# from https://github.com/JuliaLang/julia/pull/37683, we'd be golden.
@kernel function repeat_adjoint_cpu_kernel!(
    Δ::AbstractArray,
    inner::NTuple,
    outer::NTuple,
    out::AbstractArray,
    outsize::NTuple{N}
) where {N}
    dest_inds = @index(Global, NTuple)
    src_inds = ntuple(i -> mod1((dest_inds[i] - 1) ÷ inner[i] + 1, outsize[i]), N)
    # FIXME: make threadsafe
    out[src_inds...] += Δ[dest_inds...]
end;

function repeat_adjoint(
    x,
    Δ::AbstractArray{<:Any, N},
    inner::NTuple{N},
    outer::NTuple{N}
) where {N}
    out = zero(x)

    kernel! = if out isa CuArray
        repeat_adjoint_gpu_kernel!(CUDADevice(), 512)
    else
        repeat_adjoint_cpu_kernel!(CPU(), Threads.nthreads())
    end

    ev = kernel!(Δ, inner, outer, out, size(out), ndrange=size(Δ))
    wait(ev)

    return out
end;

Zygote.@adjoint function _RepeatInnerOuter.repeat_inner_outer(xs::AbstractArray, inner::Nothing, outer::Nothing)
    return xs, Δ -> (Δ, )
end
Zygote.@adjoint function _RepeatInnerOuter.repeat_inner_outer(
    xs::AbstractArray,
    inner::Nothing,
    outer::NTuple{N}
) where {N}
    inner_new = ntuple(_ -> 1, N)
    return (
        _RepeatInnerOuter.repeat_outer(xs, outer),
        Δ -> (repeat_adjoint(xs, Δ, inner_new, outer), )
    )
end
Zygote.@adjoint function _RepeatInnerOuter.repeat_inner_outer(
    xs::AbstractArray,
    inner::NTuple{N},
    outer::Nothing
) where {N}
    outer_new = ntuple(_ -> 1, N)
    return (
        _RepeatInnerOuter.repeat_inner(xs, inner),
        Δ -> (repeat_adjoint(xs, Δ, inner, outer_new), )
    )
end
Zygote.@adjoint function _RepeatInnerOuter.repeat_inner_outer(
    xs::AbstractArray,
    inner::NTuple{N},
    outer::NTuple{N}
) where {N}
    return (
        _RepeatInnerOuter.repeat_outer(_RepeatInnerOuter.repeat_inner(xs, inner), outer),
        Δ -> (repeat_adjoint(xs, Δ, inner, outer), )
    )
end

# We need to stop Zygote from using the rule implemented in
# https://github.com/FluxML/Zygote.jl/blob/d5be4d5ca80e79278d714eaac15ca71904a262e3/src/lib/array.jl#L149-L163
# We stop this adjoint from triggering, and then call the underlying `repeat_inner_outer`.
# Unfortunately we need to replicate the body of `_RepeatInnerOuter.repeat` since we can't do `Zygote.pullback`
# for kwargs.
Zygote.@adjoint function Base.repeat(arr::CuArray; inner = nothing, outer = nothing)
    _RepeatInnerOuter.check(arr, inner, outer)
    arr, inner, outer = _RepeatInnerOuter.resolve(arr, inner, outer)
    return Zygote.pullback(_RepeatInnerOuter.repeat_inner_outer, arr, inner, outer)
end


"""
     AbstractMutableComp

Abstract base type for computation units (read layers) which may mutate.
"""
abstract type AbstractMutableComp end
Base.Broadcast.broadcastable(m::AbstractMutableComp) = Ref(m)

# Generic forwarding methods. Just implement layer(t::Type) and wrapped(t::Type) to enable

# Not possible in julia <= 1.1. See #14919
# (m::AbstractMutableComp)(x...) = layer(m)(x...)
layertype(m::AbstractMutableComp) = layertype(layer(m))

NaiveNASlib.nin(m::AbstractMutableComp) = nin(layer(m))
NaiveNASlib.nout(m::AbstractMutableComp) = nout(layer(m))

# Leave some room to override clone
NaiveNASlib.clone(m::AbstractMutableComp;cf=clone) = typeof(m)(map(cf, getfield.(m, fieldnames(typeof(m))))...)

NaiveNASlib.mutate_inputs(m::AbstractMutableComp, inputs::AbstractArray{<:Integer,1}...) = mutate_inputs(wrapped(m), inputs...)
NaiveNASlib.mutate_outputs(m::AbstractMutableComp, outputs) = mutate_outputs(wrapped(m), outputs)

mutate_weights(m::AbstractMutableComp, w) = mutate_weights(wrapped(m), w)

NaiveNASlib.minΔninfactor(m::AbstractMutableComp) = minΔninfactor(layertype(m), layer(m))
NaiveNASlib.minΔnoutfactor(m::AbstractMutableComp) = minΔnoutfactor(layertype(m), layer(m))

NaiveNASlib.compconstraint!(s, m::AbstractMutableComp, data) = NaiveNASlib.compconstraint!(s, layertype(m), data)

"""
    MutableLayer

Wraps a layer in order to allow for mutation as layer structures are typically immutable
"""
mutable struct MutableLayer <: AbstractMutableComp
    layer
end
(m::MutableLayer)(x...) = layer(m)(x...)
wrapped(m::MutableLayer) = m.layer
layer(m::MutableLayer) = wrapped(m)
layertype(m::MutableLayer) = layertype(layer(m))

function NaiveNASlib.mutate_inputs(m::MutableLayer, inputs::AbstractArray{<:Integer,1}...)
    @assert length(inputs) == 1 "Only one input per layer!"
    mutate(layertype(m), m, inputs=inputs[1])
end

NaiveNASlib.mutate_outputs(m::MutableLayer, outputs) = mutate(layertype(m), m, outputs=outputs)

mutate_weights(m::MutableLayer, w) = mutate(layertype(m), m, other=w)

mutate(m::MutableLayer; inputs, outputs, other = l -> ()) = mutate(layertype(m), m, inputs=inputs, outputs=outputs, other=other)

function mutate(::FluxParLayer, m::MutableLayer; inputs=1:nin(m), outputs=1:nout(m), other= l -> ())
    l = layer(m)
    otherdims = other(l)
    w = select(weights(l), outdim(l) => outputs, indim(l) => inputs, otherdims...)
    b = select(bias(l), 1 => outputs)
    newlayer(m, w, b, otherpars(other, l))
end
otherpars(o, l) = ()

function mutate(::FluxDepthwiseConv, m::MutableLayer; inputs=1:nin(m), outputs=1:nout(m), other= l -> ())
    l = layer(m)
    otherdims = other(l)
    weightouts = map(Iterators.partition(outputs, length(inputs))) do group
        all(group .< 0) && return group[1]
        return (maximum(group) - 1) ÷ length(inputs) + 1
    end

    w = select(weights(l), outdim(l) => weightouts, indim(l) => inputs, otherdims...)
    b = select(bias(l), 1 => outputs)
    newlayer(m, w, b, otherpars(other, l))
end

function mutate(t::FluxRecurrent, m::MutableLayer; inputs=1:nin(m), outputs=1:nout(m), other=missing)
    l = layer(m)
    outputs_scaled = mapfoldl(vcat, 1:outscale(l)) do i
        offs = (i-1) * nout(l)
        return map(x -> x > 0 ? x + offs : x, outputs)
    end

    wi = select(weights(l), outdim(l) => outputs_scaled, indim(l) => inputs)
    wh = select(hiddenweights(l), 1 => outputs_scaled, 2 => outputs)
    b = select(bias(l), 1 => outputs_scaled)
    mutate_recurrent_state(t, m, outputs, wi, wh, b)
end

function mutate_recurrent_state(::FluxRecurrent, m::MutableLayer, outputs, wi, wh, b)
    l = layer(m)
    h = select(hiddenstate(l), 1 => outputs)
    s = select(state(l), 1 => outputs)

    cellnew = setproperties(layer(m).cell, (Wi=param(wi), Wh=param(wh), b = param(b), h = param(h)))
    lnew = setproperties(layer(m), (cell=cellnew, state = param(s)))
    m.layer = lnew
end

function mutate_recurrent_state(::FluxLstm, m::MutableLayer, outputs, wi, wh, b)
    l = layer(m)
    hcurr, scurr = hiddenstate(l), state(l)
    hc = select.(hcurr, repeat([1 => outputs], length(hcurr)))
    s = select.(scurr, repeat([1 => outputs], length(scurr)))

    cellnew = setproperties(layer(m).cell, (Wi=param(wi), Wh=param(wh), b = param(b), h = param(hc[1]), c = param(hc[2])))
    lnew = setproperties(layer(m), (cell=cellnew, state = tuple(param.(s)...)))
    m.layer = lnew

    return (h = hc[1], c = hc[2]), tuple(param.(s))
end

function mutate(t::FluxParInvLayer, m::MutableLayer; inputs=missing, outputs=missing, other=missing)
    @assert any(ismissing.((inputs, outputs))) || inputs == outputs "Try to mutate $inputs and $outputs for invariant layer $(m)!"
    ismissing(inputs) || return mutate(t, m, inputs)
    ismissing(outputs) || return mutate(t, m, outputs)
end

function mutate(::FluxDiagonal, m::MutableLayer, inds)
    l = layer(m)
    w = select(weights(l), 1 => inds)
    b = select(bias(l), 1 => inds)
    newlayer(m, w, b)
end

function mutate(::FluxLayerNorm, m::MutableLayer, inds)
    # LayerNorm is only a wrapped Diagonal. Just mutate the Diagonal and make a new LayerNorm of it
    proxy = MutableLayer(layer(m).diag)
    mutate(proxy, inputs=inds, outputs=inds, other=l->())
    m.layer = LayerNorm(layer(proxy))
end

function mutate(::FluxParNorm, m::MutableLayer, inds)
    # Good? bad? I'm the guy who assumes mean and std type parameters will be visited in a certain order and uses a closure for that assumption
    ismean = false
    parselect = function(x)
        ismean = !ismean
        return select(x, 1 => inds; insval = (ismean ? 0 : 1))
    end
    m.layer = Flux.mapchildren(parselect, m.layer)
end

function mutate(::FluxGroupNorm, m::MutableLayer, inds)

    l = m.layer
    ngroups = l.G
    nchannels = length(inds)
    # Make a "best fit" to new group size, but don't allow 1 as group size
    g_alts = filter(ga -> ga > 1, findall(r -> r == 0, nchannels .% (1:nchannels)))
    ngroups = g_alts[argmin(abs.(g_alts .- ngroups))]

    nchannels_per_group = div(nchannels, ngroups)
    # Map indices to (new) groups
    inds_groups = ceil.(Int, map(ind -> ind > 0 ? ind / nchannels_per_group : ind, inds))
    # TODO: Select the most commonly occuring index in each column (except -1)
    inds_groups = reshape(inds_groups, nchannels_per_group, ngroups)[1,:]

    sizetoinds = Dict(nin(l) => inds, l.G => inds_groups)

    ismean = false
    parselect = function(x)
        ismean = !ismean
        return select(x, 1 => sizetoinds[length(x)]; insval = (ismean ? 0 : 1))
    end
    m.layer = Flux.mapchildren(parselect, m.layer)
    m.layer.G = ngroups
end

newlayer(m::MutableLayer, w, b, other=nothing) = m.layer = newlayer(layertype(m), m, w, b, other)

newlayer(::FluxDense, m::MutableLayer, w, b, other) = Dense(param(w), param(b), deepcopy(layer(m).σ))
newlayer(::FluxConvolutional, m::MutableLayer, w, b, other) = setproperties(layer(m), (weight=param(w), bias=param(b), σ=deepcopy(layer(m).σ), other...))
newlayer(::FluxDiagonal, m::MutableLayer, w, b, other) = Flux.Diagonal(param(w), param(b))

"""
    LazyMutable

Lazy version of MutableLayer in the sense that it does not perform any mutations
until invoked to perform a computation.

This is done by calling the method trigger_mutation on the mutable, meaning that
there must exist a method for the the mutable type in order for something to
happen.

This reduces the need to garbage collect intermediate parameters when both inputs
and outputs are mutated.

Also useable for factory-like designs where the actual layers of a computation graph
are not instantiated until the graph is used.

# Examples
```julia-repl
julia> using NaiveNASflux

julia> struct DenseConfig end

julia> lazy = LazyMutable(DenseConfig(), 2,3)
LazyMutable(DenseConfig(), 1:2, 1:3)

julia> function NaiveNASflux.dispatch(m::LazyMutable, ::DenseConfig, x)
       m.mutable = MutableLayer(Dense(nin(m), nout(m), relu))
       return m.mutable(x)
       end

julia> lazy(Float32[1,2])
Tracked 3-element Array{Float32,1}:
 1.568902f0
 0.556749f0
 2.0417972f0

julia> lazy
LazyMutable(MutableLayer(Dense(2, 3, NNlib.relu)), 1:2, 1:3)
```
"""
mutable struct LazyMutable <: AbstractMutableComp
    mutable
    inputs::AbstractVector{<:Integer}
    outputs::AbstractVector{<:Integer}
    other
end
LazyMutable(m::AbstractMutableComp) = LazyMutable(m, nin(m), nout(m))
LazyMutable(m, nin::Integer, nout::Integer) = LazyMutable(m, 1:nin, 1:nout, m -> ())

wrapped(m::LazyMutable) = m.mutable
layer(m::LazyMutable) = layer(wrapped(m))

treelike_fields(T::Type{LazyMutable}) = (:mutable,)

(m::LazyMutable)(x...) = dispatch!(m, m.mutable, x...)
dispatch!(m::LazyMutable, mutable::AbstractMutableComp, x...) = mutable(x...)

NaiveNASlib.nin(m::LazyMutable) = length(m.inputs)
NaiveNASlib.nout(m::LazyMutable) = length(m.outputs)

function NaiveNASlib.mutate_inputs(m::LazyMutable, inputs::AbstractArray{<:Integer,1}...)
    @assert length(inputs) == 1 "Only one input per layer!"
    m.inputs == inputs[1] && return

    m.mutable = ResetLazyMutable(trigger_mutation(m.mutable))
    m.inputs = select(m.inputs, 1 => inputs[1], insval=-1)
end

function NaiveNASlib.mutate_outputs(m::LazyMutable, outputs::AbstractArray{<:Integer,1})
    outputs == m.outputs && return

    m.mutable = ResetLazyMutable(trigger_mutation(m.mutable))
    m.outputs = select(m.outputs, 1=>outputs, insval = -1)
end

function mutate_weights(m::LazyMutable, w)
    m.other == w && return

    m.mutable = ResetLazyMutable(trigger_mutation(m.mutable))
    m.other = w
end

NaiveNASlib.mutate_inputs(m::LazyMutable, nin::Integer...) = mutate_inputs(m, trunc_or_pad.(length(m.inputs), nin)...)

NaiveNASlib.mutate_outputs(m::LazyMutable, nout::Integer) = mutate_outputs(m, trunc_or_pad(length(m.outputs), nout))

function trunc_or_pad(maxselect, size)
    res = -ones(Int, size)
    lastselected = min(size, maxselect)
    res[1:lastselected] = 1:lastselected
    return res
end

"""
    MutationTriggered

Dispatches mutation for LazyMutable.
"""
struct MutationTriggered
    wrapped
end
# Functionality is opt-in
trigger_mutation(m) = m
trigger_mutation(m::AbstractMutableComp) = MutationTriggered(m)

function dispatch!(lm::LazyMutable, m::MutationTriggered, x...)
    mutate(m.wrapped; inputs=lm.inputs, outputs=lm.outputs, other=lm.other)
    lm.mutable = m.wrapped
    return lm(x...)
end

layer(m::MutationTriggered) = layer(m.wrapped)
layertype(m::MutationTriggered) = layertype(layer(m))

Flux.@treelike MutationTriggered

"""
    ResetLazyMutable

Reset a `LazyMutable` when dispatching.
"""
struct ResetLazyMutable
    wrapped
end
ResetLazyMutable(r::ResetLazyMutable) = r

function dispatch!(lm::LazyMutable, m::ResetLazyMutable, x...)
    lm.mutable = m.wrapped
    output = lm(x...)
    lm.inputs = 1:nin(lm)
    lm.outputs = 1:nout(lm)
    lm.other = m -> ()
    return output
end

layer(m::ResetLazyMutable) = layer(m.wrapped)
layertype(m::ResetLazyMutable) = layertype(layer(m))

Flux.@treelike ResetLazyMutable

"""
    NoParams

Ignores size mutation.

Useful for layers which don't have parameters.
"""
struct NoParams
    layer
end

(i::NoParams)(x...) = layer(i)(x...)
layer(i::NoParams) = i.layer
layertype(i::NoParams) = layertype(layer(i))

LazyMutable(m::NoParams) = m

function NaiveNASlib.mutate_inputs(::NoParams, inputs) end
function NaiveNASlib.mutate_outputs(::NoParams, outputs) end
function mutate_weights(::NoParams, w) end
NaiveNASlib.minΔninfactor(m::NoParams) = minΔninfactor(layertype(m), layer(m))
NaiveNASlib.minΔnoutfactor(m::NoParams) = minΔnoutfactor(layertype(m), layer(m))


### Start 31.07.2017

# "Check if the condition holds"
# function condForReduce2(subsig, delta)
#     n = length(subsig)
#     @assert n > 1
#     for i = 1 : n-1
#         s0 = subsig[1:i]
#         s1 = subsig[(i+1):n]
#         ecut = calcEcut(length(s0), length(s1), delta)
#         #ecut = calcEcut(subsig[1:n], length(s0), length(s1), delta)
#         m0 = mean(s0)
#         m1 = mean(s1)
#         if abs(m1-m0) >= ecut
#             return true
#         end
#     end
#     return false
# end
#
# function condForReduce(subsig, ecut_custom)
#     n = length(subsig)
#     @assert n > 1
#     for i = 1 : n-1
#         s0 = subsig[1:i]
#         s1 = subsig[(i+1):n]
#         m0 = mean(s0)
#         m1 = mean(s1)
#         if abs(m1-m0) >= ecut_custom
#             return true
#         end
#     end
#     return false
# end

# function test_cond()
#     println( cond([1, 2;], 1))
#     println( cond([1, 2;], 0.9))
# end
# test_cond()
#
# function recReduce!(w, ecut_custom)
#     """OLD"""
#     if length(w) > 1
#         if condForReduce(w, ecut_custom)
#             recReduce!(w[1:(end-1)], ecut_custom)
#         end
#     else
#         return w
#     end
# end
#
# function test_recReduce()
#     function loctest(_v; e = 1.0)
#         recReduce!(_v, e)
#         println(_v)
#     end
#     loctest([1.0])
#     loctest([1.0, 3.0, 3.1])
#     loctest([1.0, 3.0, 3.1, 3.1, 3.1])
#     loctest([1.0, 1.0, 1.0, 1.0, 1.0])
#     loctest([1.0, 1.0, 1.0, 4.0, 4.0])
# end
# #test_recReduce()

### End 31.07.2017

include("./ListItem.jl")
include("./List.jl")

mutable struct AdwinObject
    DELTA                   :: Float64
    mintMinimLongitudWindow :: Int
    mdbldelta               :: Float64
    mintTime                :: Int
    mintClock               :: Int
    mdblWidth               :: Float64  # Mean of Width = mdblWidth/Number of items
    #
    #BUCKET
    MAXBUCKETS              :: Int
    lastBucketRow           :: Int
    TOTAL                   :: Float64
    VARIANCE                :: Float64
    WIDTH                   :: Int
    BucketNumber            :: Int
    #
    Detect                  :: Int
    numberDetections        :: Int
    DetectTwice             :: Int
    blnBucketDeleted        :: Bool
    BucketNumberMAX         :: Int
    mintMinWinLength        :: Int

    listRowBuckets          :: List
end

function initAdwinObject()
    AO = AdwinObject()

end

function adwin(d::Float64)
    DELTA::Float64 = 0.002
    mintMinimLongitudWindow::Int = 10
    mdbldelta::Float64 = 0.002
    mintTime::Int = 0
    mintClock::Int = 32
    mdblWidth::Float64 = 0 # Mean of Width = mdblWidth/Number of items

    #BUCKET
    MAXBUCKETS::Int = 5
    lastBucketRow::Int = 0
    TOTAL::Float64 = 0
    VARIANCE::Float64 = 0
    WIDTH::Int = 0
    BucketNumber::Int = 0

    Detect::Int = 0
    numberDetections::Int = 0
    DetectTwice::Int  = 0
    blnBucketDeleted::Bool = false
    BucketNumberMAX::Int = 0
    mintMinWinLength::Int = 5

    listRowBuckets::List = List()

    function insertElement!(value::Float64)
        WIDTH +=1
    end

    #function setInput!(intEntrada::Float64)
    #    setInput!(intEntrada, mdbldelta)
    #end

    function setInput!(intEntrada::Float64, delta::Float64 = mdbldelta)
        blnChange::Bool = false
        blnExit::Bool = false
        cursor::ListIt
        #mintTime += 1

        # 1,2)Increment window in one element
        #insertElement!(intEntrada)
    end

    for p in 0:999
        x = p < 500 ? 1000.0 : 500.0
        setInput!(x)
    end

end

adwin(0.01)

function a()
    a = 0
    function setInput(b)
        a = a+1
        c = b
    end
    println(a)
    setInput(5)
    println(a)
end
a()


using NeXLUncertainties

"""
    StandardLabel

A ReferenceLabel that represents a reference spectrum or reference properties associated with a set of
characteristic x-rays (CharXRay) objects over a contiguous range of spectrum channels that is to be
used as a standard.
"""
struct StandardLabel <: ReferenceLabel
    spectrum::SpectrumOrProperties # The spectrum used as the reference for fitting...
    # roi::UnitRange{Int} # Not available in k-ratios
    xrays::Vector{CharXRay}
    standard::Material
    hash::UInt
    function StandardLabel(cxrl::CharXRayLabel, stdMat::Material)
        new(cxrl.spectrum, cxrl.xrays, stdMat, hash(cxrl.spectrum, hash(cxrl.xrays, hash(stdMat, UInt(0xBADF00D5)))))
    end
    function StandardLabel(props::Dict{Symbol,Any}, xrays::Vector{CharXRay}, stdMat::Material)
        new(props, xrays, stdMat, hash(props, hash(xrays, hash(stdMat, UInt(0xBADF00D5)))))
    end

end

"""
    matches(cxrl::CharXRayLabel, std::KRatio)
    matches(cxrl::CharXRayLabel, std::StandardLabel)

Does the measured `CharXRayLabel` match the standard?
"""
function matches(cxrl::CharXRayLabel, std::KRatio)::Bool
    cp, sp = properties(cxrl), std.stdProps
    return issetequal(cxrl.xrays, std.xrays) && # Same X-rays
        isequal(cp[:Composition], sp[:Composition]) && # Same reference material
        isapprox(cp[:BeamEnergy], sp[:BeamEnergy], rtol=0.001) && # Same beam energy
        isapprox(cp[:TakeOffAngle], sp[:TakeOffAngle], atol=deg2rad(0.1)) # Same take-off-angle
end
function matches(cxrl::CharXRayLabel, std::StandardLabel)::Bool
    cp, sp = properties(cxrl), properties(std)
    return issetequal(cxrl.xrays, std.xrays) && # Same X-rays
        isequal(cp[:Composition], sp[:Composition]) && # Same reference material
        isapprox(cp[:BeamEnergy], sp[:BeamEnergy], rtol=0.001) && # Same beam energy
        isapprox(cp[:TakeOffAngle], sp[:TakeOffAngle], atol=deg2rad(0.1)) # Same take-off-angle
end

function Base.show(io::IO, cl::StandardLabel)
    comp = composition(cl)
    compname = isnothing(comp) ? "Unspecified" : name(comp)
    print(io,"k_std[$(name(cl.xrays)), $(name(cl.standard)) fit by $compname]")
end

"""
`StandardizeModel` is designed to combine k-ratio measurements relative to common reference spectra
to re-standardize a microanalysis measurement.  As a `MeasurementModel` it combines k-ratio data
while maintaining covariance relationships between the measured values.
"""
struct StandardizeModel <: MeasurementModel
    measured::Vector{CharXRayLabel}
    standards::Vector{StandardLabel}

    StandardizeModel(measured::AbstractVector{CharXRayLabel}, standards::AbstractVector{StandardLabel}) = new(measured, standards)
end

function NeXLUncertainties.compute(
    sm::StandardizeModel,
    inputs::LabeledValues,
    withJac::Bool,
)::MMResult
    outputs, results = Array{CharXRayLabel}(undef, length(sm.measured)), zeros(Float64, length(sm.measured))
    for (i, meas) in enumerate(sm.measured)
        j = findfirst(std->matches(meas, std), sm.standards)
        if (!isnothing(j)) && (inputs[sm.standards[j]] > 0.0) # Re-standardized
            std = sm.standards[j]
            sp = copy(properties(std))
            sp[:Composition] = std.standard
            outputs[i] = CharXRayLabel(sp, meas.roi, meas.xrays) # restandardized label
            results[i] = inputs[meas] / inputs[std]
        else
            outputs[i] = meas
            results[i] = inputs[meas]
        end
    end
    if withJac
        jac = zeros(Float64, length(sm.measured), length(sm.measured)+length(sm.standards))
        for (i, meas) in enumerate(sm.measured)
            j = findfirst(std->matches(meas, std), sm.standards)
            if (!isnothing(j)) &&  (inputs[sm.standards[j]] > 0.0) # Re-standardized
                std = sm.standards[j]
                jac[i, indexin(inputs, meas)] = 1.0 / inputs[std]
                jac[i, indexin(inputs, std)] = -inputs[meas]/(inputs[std]^2)
            else
                jac[i, indexin(inputs, meas)] = 1.0
            end
        end
    end
    return (LabeledValues(outputs, results), jac)
end

function __remap_peaktoback(pbs::Dict{<:ReferenceLabel,NTuple{2,Float64}}, sm::StandardizeModel)::Dict{ReferenceLabel,NTuple{2,Float64}}
    res = Dict{ReferenceLabel,NTuple{2,Float64}}()
    for (meas, pb) in pbs
        i = findfirst(std->matches(meas, std), sm.standards)
        if !isnothing(i)
            std = sm.standards[i]
            sp = copy(properties(std))
            sp[:Composition] = std.standard
            res[CharXRayLabel(sp, meas.roi, meas.xrays)] = pb
        else
            res[meas] = pb
        end
    end
    return res
end

"""
    NeXLCore.standardize(ffr::FilterFitResult, standard::FilterFitResult, material::Material, els=elms(material))::FilterFitResult
    NeXLCore.standardize(ffrs::Vector{FilterFitResult}, standard::FilterFitResult, material::Material, els=elms(material))::Vector{FilterFitResult}
    NeXLCore.standardize(ffr::FilterFitResult, standards::AbstractArray{KRatio})::FilterFitResult
    NeXLCore.standardize(ffrs::Vector{FilterFitResult}, standards::AbstractArray{KRatio})::FilterFitResult

Apply the standard `KRatio`s to the `FilterFitResult` producing a re-standardized `FilterFitResult`.
"""
function NeXLCore.standardize(ffr::FilterFitResult, standard::FilterFitResult, material::Material, els=elms(material))::FilterFitResult
    stds = filter(labels(standard.kratios)) do lbl
        (lbl isa CharXRayLabel) && (element(lbl) in els)
    end
    ext = extract(standard.kratios, stds)
    rext = uvs([ StandardLabel(l, material) for l in labels(ext)], ext.values, ext.covariance)
    # Problem here with equivalent CharXRayLabel(s) for unknown and standard...
    inp = cat(ffr.kratios, rext)
    stdize = StandardizeModel(Vector{CharXRayLabel}(labels(ffr.kratios)), Vector{StandardLabel}(labels(rext)))
    return FilterFitResult(
            ffr.label, # Same
            stdize(inp), # remapped
            ffr.roi, # Same
            ffr.raw, # Same
            ffr.residual, # Same - residual is due to references
            __remap_peaktoback(ffr.peakback, stdize) # Remapped to standard labels
        )
end

function NeXLCore.standardize(ffrs::AbstractVector{FilterFitResult}, standard::FilterFitResult, material::Material, els=elms(material))::Vector{FilterFitResult}
    map(ffrs) do ffr
        standardize(ffr, standard, material, els)
    end
end

function NeXLCore.standardize(ffr::FilterFitResult, standards::AbstractArray{KRatio})::FilterFitResult
    @assert all(std->isstandard(std), standards) "One or more required property is missing from a standard (for details, see the above warnings.)"
    stdis = map(cxrl->findfirst(std->matches(cxrl, std), standards), labels(ffr.kratios))
    stds = uvs(map(filter(i->!isnothing(i), stdis)) do i
        std = standards[i]
        StandardLabel(std.stdProps, std.xrays, std.unkProps[:Composition])=>uv(std.kratio)
    end...)
    return if length(stds)>0
        stdize = StandardizeModel(Vector{CharXRayLabel}(labels(ffr.kratios)), Vector{StandardLabel}(labels(stds)))
        inp = cat(ffr.kratios, stds)
        FilterFitResult(
            ffr.label,
            stdize(inp),
            ffr.roi,
            ffr.raw,
            ffr.residual,
            __remap_peaktoback(ffr.peakback, stdize)
        )
    else
        @warn "No suitable standards were provided to standardize this measurement."
        ffr
    end
end

function NeXLCore.standardize(ffrs::AbstractVector{FilterFitResult}, standards::AbstractArray{KRatio})::Vector{FilterFitResult}
    map(ffrs) do ffr
        standardize(ffr, standards)
    end
end


@testset "fsm_retx_relaxed.jl" begin
    DEFAULT_CAPACITY = 65000
    TIMEOUT_DFLT = 1000
    NREPS = 64
    MAX_PAYLOAD_SIZE = 1460
    MAX_RETX_ATTEMPTS = 8
    @testset "single segment re-transmit" begin
        tx_ackno = WrappingInt32(rand(UInt32))
        conn = fsm_in_established(tx_ackno - 1, tx_ackno - 1; cap=DEFAULT_CAPACITY)
        data = "asdf"
        write!(conn, data)
        tick!(conn, 1)
        expect_one_seg(conn; ack=true, data, payload_size=length(data))
        tick!(conn, TIMEOUT_DFLT - 2)

        expect_no_seg(conn)
        tick!(conn, 2)
        expect_one_seg(conn; ack=true, data, payload_size=length(data))

        tick!(conn, 10TIMEOUT_DFLT + 100)
        expect_one_seg(conn; ack=true, data, payload_size=length(data))

        for i in 2:MAX_RETX_ATTEMPTS-1
            tick!(conn, (TIMEOUT_DFLT << i) - i)
            expect_no_seg(conn)
            tick!(conn, i)
            expect_one_seg(conn; ack=true, data, payload_size=length(data))
        end
        expect_state(conn, JLSponge.ESTABLISHED)
        tick!(conn, (TIMEOUT_DFLT << MAX_RETX_ATTEMPTS) + 1)
        expect_state(conn, JLSponge.RESET)
        expect_one_seg(conn; rst=true)
    end
end


############################################################
# judiPDE ####################################################
############################################################

# Authors: Philipp Witte (pwitte@eos.ubc.ca), Henryk Modzelewski (hmodzelewski@eos.ubc.ca)
# Date: January 2017

export judiPDE, judiPDEexception, judiPDEadjoint, judiPDEadjointException, subsample

############################################################

# Type for linear operator representing  Pr*A(m)^-1*Ps,
# i.e. it includes source and receiver projections

# Forward
struct judiPDE{DDT<:Number,RDT<:Number} <: joAbstractLinearOperator{DDT,RDT}
    name::String
    m::Integer
    n::Integer
    info::Info
    model::Model
    geometry::Geometry
    options::Options
    fop::Function              # forward
    fop_T::Union{Function, Nothing}  # transpose
end

mutable struct judiPDEexception <: Exception
    msg :: String
end

# Adjoint
struct judiPDEadjoint{DDT<:Number,RDT<:Number} <: joAbstractLinearOperator{DDT,RDT}
    name::String
    m::Integer
    n::Integer
    info::Info
    model::Model
    geometry::Geometry
    options::Options
    fop::Function              # forward
    fop_T::Union{Function, Nothing}  # transpose
end

mutable struct judiPDEadjointException <: Exception
    msg :: String
end

############################################################
## outer constructors

function judiPDE(name::String,info::Info,model::Model, geometry::Geometry; options=Options(), DDT::DataType=Float32, RDT::DataType=DDT)
# JOLI wrapper for nonlinear forward modeling
    (DDT == Float32 && RDT == Float32) || throw(judiPDEexception("Domain and range types not supported"))
    m = n_samples(geometry, info)
    n = info.n * sum(info.nt)
    srcnum = 1:info.nsrc

    return F = judiPDE{Float32,Float32}(name, m, n, info, model, geometry, options,
                              args -> time_modeling(model, args[1], args[2], args[3], args[4], args[5], srcnum, 'F', 1, options),
                              args_T -> time_modeling(model, args_T[1], args_T[2], args_T[3], args_T[4], args_T[5], srcnum, 'F', -1, options)
                              )
end

function judiPDEadjoint(name::String,info::Info,model::Model, geometry::Geometry; options=Options(), DDT::DataType=Float32, RDT::DataType=DDT)
# JOLI wrapper for nonlinear forward modeling
    (DDT == Float32 && RDT == Float32) || throw(judiPDEadjointException("Domain and range types not supported"))
    m = n_samples(geometry, info)
    n = info.n * sum(info.nt)

    srcnum = 1:info.nsrc

    return F = judiPDEadjoint{Float32,Float32}(name, m, n, info, model, geometry, options,
                                args_T -> time_modeling(model, args_T[1], args_T[2], args_T[3], args_T[4], args_T[5], srcnum, 'F', -1, options),
                                args -> time_modeling(model, args[1], args[2], args[3], args[4], args[5], srcnum, 'F', 1, options),
                                )
end

############################################################
## overloaded Base functions

# conj(judiPDE)
conj(A::judiPDE{DDT,RDT}) where {DDT,RDT} =
    judiPDE{DDT,RDT}("conj("*A.name*")",A.m,A.n,A.info,A.model,A.geometry,A.options,
        A.fop,
        A.fop_T
        )

# transpose(jo)
transpose(A::judiPDE{DDT,RDT}) where {DDT,RDT} =
    judiPDEadjoint{DDT,RDT}(A.name,A.n,A.m,A.info,A.model,A.geometry,A.options,
        A.fop_T,
        A.fop
        )

# adjoint(jo)
adjoint(A::judiPDE{DDT,RDT}) where {DDT,RDT} =
	judiPDEadjoint{DDT,RDT}(A.name,A.n,A.m,A.info,A.model,A.geometry,A.options,
		A.fop_T,
		A.fop
		)

# conj(jo)
conj(A::judiPDEadjoint{DDT,RDT}) where {DDT,RDT}=
    judiPDEadjoint{DDT,RDT}("conj("*A.name*")",A.m,A.n,A.info,A.model,A.geometry,A.options,
        A.fop,
        A.fop_T
        )

# transpose(jo)
transpose(A::judiPDEadjoint{DDT,RDT}) where {DDT,RDT} =
    judiPDE{DDT,RDT}(A.name,A.n,A.m,A.info,A.model,A.geometry,A.options,
        A.fop_T,
        A.fop
        )
# adjoint(jo)
adjoint(A::judiPDEadjoint{DDT,RDT}) where {DDT,RDT} =
    judiPDE{DDT,RDT}(A.name,A.n,A.m,A.info,A.model,A.geometry,A.options,
        A.fop_T,
        A.fop
        )

############################################################
## overloaded Base *(...judi...)

# *(judiPDE,judiRHS)
function *(A::judiPDE{ADDT,ARDT},v::judiRHS{vDT}) where {ADDT,ARDT,vDT}
    A.n == size(v,1) || throw(judiPDEexception("Shape mismatch: A:$(size(A)), v: $(size(v))"))
    jo_check_type_match(ADDT,vDT,join(["DDT for *(judiPDE,judiRHS):",A.name,typeof(A),vDT]," / "))
	args = (v.geometry, v.data, A.geometry, nothing, nothing)
    V = A.fop(args)
    jo_check_type_match(ARDT,eltype(V),join(["RDT from *(judiPDE,judiVector):",A.name,typeof(A),eltype(V)]," / "))
    return V
end

function *(A::judiPDEadjoint{ADDT,ARDT},v::judiRHS{vDT}) where {ADDT,ARDT,vDT}
    A.n == size(v,1) || throw(judiPDEadjointException("Shape mismatch: A:$(size(A)), v: $(size(v))"))
    jo_check_type_match(ADDT,vDT,join(["DDT for *(judiPDE,judiRHS):",A.name,typeof(A),vDT]," / "))
	args = (A.geometry,nothing,v.geometry,v.data,nothing)
    V = A.fop(args)
    jo_check_type_match(ARDT,eltype(V),join(["RDT from *(judiPDE,judiVector):",A.name,typeof(A),eltype(V)]," / "))
    return V
end

# *(judiPDE,judiWavefield)
function *(A::judiPDE{ADDT,ARDT},v::judiWavefield{vDT}) where {ADDT,ARDT,vDT}
	A.n == size(v,1) || throw(judiPDEexception("Shape mismatch: A:$(size(A)), v: $(size(v))"))
	jo_check_type_match(ADDT,vDT,join(["DDT for *(judiPDE,judiWavefield):",A.name,typeof(A),vDT]," / "))
	args = (nothing,v.data,A.geometry,nothing,nothing)
	V = A.fop(args)
	jo_check_type_match(ARDT,eltype(V),join(["RDT from *(judiPDE,judiVector):",A.name,typeof(A),eltype(V)]," / "))
	return V
end

function *(A::judiPDEadjoint{ADDT,ARDT},v::judiWavefield{vDT}) where {ADDT,ARDT,vDT}
	A.n == size(v,1) || throw(judiPDEadjointException("Shape mismatch: A:$(size(A)), v: $(size(v))"))
	jo_check_type_match(ADDT,vDT,join(["DDT for *(judiPDE,judiWavefield):",A.name,typeof(A),vDT]," / "))
	args = (A.geometry,nothing,nothing,v.data,nothing)
	V = A.fop(args)
	jo_check_type_match(ARDT,eltype(V),join(["RDT from *(judiPDE,judiVector):",A.name,typeof(A),eltype(V)]," / "))
	return V
end

# *(judiPDE,judiProjection)
function *(A::judiPDE{CDT,ARDT},B::judiProjection{BDDT,CDT}) where {ARDT,BDDT,CDT}
    A.n == size(B,1) || throw(judiPDEexception("Shape mismatch: A:$(size(A)), B: $(size(B))"))
    return judiModeling(A.info,A.model,B.geometry,A.geometry;options=A.options,DDT=CDT,RDT=ARDT)
end

function *(A::judiPDEadjoint{CDT,ARDT},B::judiProjection{BDDT,CDT}) where {ARDT,BDDT,CDT}
    A.n == size(B,1) || throw(judiPDEadjointException("Shape mismatch: A:$(size(A)), B: $(size(B))"))
    return adjoint(judiModeling(A.info,A.model,A.geometry,B.geometry,options=A.options,DDT=CDT,RDT=ARDT))
end

# *(judiPDE,judiLRWF)
function *(A::judiPDE{CDT,ARDT},B::judiLRWF{BDDT,CDT}) where {ARDT,BDDT,CDT}
    A.n == size(B,1) || throw(judiPDEexception("Shape mismatch: A:$(size(A)), B: $(size(B))"))
    return judiPDEextended(A.info,A.model,B.wavelet,A.geometry;options=A.options,DDT=CDT,RDT=ARDT)
end

function *(A::judiPDEadjoint{CDT,ARDT},B::judiLRWF{BDDT,CDT}) where {ARDT,BDDT,CDT}
    A.n == size(B,1) || throw(judiPDEadjointException("Shape mismatch: A:$(size(A)), B: $(size(B))"))
    return adjoint(judiPDEextended(A.info,A.model,B.wavelet,A.geometry,options=A.options,DDT=CDT,RDT=ARDT))
end

#multiplications with judiExtendedSource
# *(judiPDE,judiExtendedSource)
function *(A::judiPDE{ADDT,ARDT}, B::judiExtendedSource{vDT}) where {ADDT, ARDT,vDT}
    A.n == size(B,1) || throw(judiPDEexception("Shape mismatch: A:$(size(A)), B: $(size(B))"))
    jo_check_type_match(ADDT, vDT, join(["DDT for *(judiPDE,judiExtendedSource):",A.name,typeof(A),vDT]," / "))
    F = judiPDEextended(A.info, A.model, B.wavelet, A.geometry; options=A.options,DDT=vDT, RDT=ARDT)
    V = F*B.weights
    jo_check_type_match(ARDT, eltype(V),join(["RDT from *(judiPDE, judiExtendedSource):",A.name,typeof(A), eltype(V)]," / "))
    return V
end

# *(judiPDEadjoint, judiExtendedSource)
function *(A::judiPDEadjoint{ADDT,ARDT}, B::judiExtendedSource{vDT}) where {ADDT,ARDT,vDT}
    A.n == size(B, 1) || throw(judiPDEadjointException("Shape mismatch: A:$(size(A)), B: $(size(B))"))
    jo_check_type_match(ADDT, vDT, join(["DDT for *(judiPDEadjoint,judiExtendedSource):",A.name,typeof(A),vDT]," / "))
    F = adjoint(judiPDEextended(A.info, A.model, B.wavelet, A.geometry; options=A.options,DDT=vDT,RDT=ARDT))
    V = F*B.weights
    jo_check_type_match(ARDT,eltype(V),join(["RDT from *(judiPDE,judiExtendedSource):",A.name,typeof(A),eltype(V)]," / "))
    return V
end

# *(num,judiPDE)
function *(a::Number,A::judiPDE{ADDT,ARDT}) where {ADDT,ARDT}
    return judiPDE{ADDT,ARDT}("(N*"*A.name*")",A.m,A.n,A.info,A.model,A.geometry,A.options,
        v1 -> jo_convert(ARDT,a, false)*A.fop(v1),
        v2 -> jo_convert(ARDT,a, false)*A.fop_T(v2)
        )
end

function *(a::Number,A::judiPDEadjoint{ADDT,ARDT}) where {ADDT,ARDT}
    return judiPDEadjoint{ADDT,ARDT}("(N*"*A.name*")",A.m,A.n,A.info,A.model,A.geometry,A.options,
        v1 -> jo_convert(ARDT,a, false)*A.fop(v1),
        v2 -> jo_convert(ADDT,a, false)*A.fop_T(v2)
        )
end

############################################################
## overloaded Basees +(...judiPDE...), -(...judiPDE...)

# -(judiPDE)
-(A::judiPDE{DDT,RDT}) where {DDT,RDT} =
    judiPDE{DDT,RDT}("(-"*A.name*")",A.m,A.n,A.info,A.model,A.geometry,A.options,
        v1->-A.fop(v1),
        v2->-A.fop_T(v2)
        )

-(A::judiPDEadjoint{DDT,RDT}) where {DDT,RDT} =
    judiPDEadjoint{DDT,RDT}("(-"*A.name*")",A.m,A.n,A.info,A.model,A.geometry,A.options,
        v1->-A.fop(v1),
        v2->-A.fop_T(v2)
        )

############################################################
## Additional overloaded functions

# Subsample Modeling operator
function subsample(F::judiPDE{ADDT,ARDT}, srcnum) where {ADDT,ARDT}
    geometry = subsample(F.geometry,srcnum)     # Geometry of subsampled data container
    info = Info(F.info.n, length(srcnum), F.info.nt[srcnum])
    return judiPDE(F.name, info, F.model, geometry; options=F.options, DDT=ADDT, RDT=ARDT)
end

function subsample(F::judiPDEadjoint{ADDT,ARDT}, srcnum) where {ADDT,ARDT}
    geometry = subsample(F.geometry,srcnum)     # Geometry of subsampled data container
    info = Info(F.info.n, length(srcnum), F.info.nt[srcnum])
    return judiPDEadjoint(F.name, info, F.model, geometry; options=F.options, DDT=ADDT, RDT=ARDT)
end

getindex(F::judiPDE,a) = subsample(F,a)
getindex(F::judiPDEadjoint,a) = subsample(F,a)


using TensorOperations
using LinearAlgebra

function RMP2(Alg::RMP2a)
    aoints = IntegralHelper{Float64}()
    rhf = RHF(aoints)
    moints = IntegralHelper(orbitals=rhf.orbitals)
    RMP2(moints, aoints, Alg)
end

function RMP2(aoints::IntegralHelper{Float64,E,AtomicOrbitals}, Alg::RMP2a) where E <: AbstractERI
    rhf = RHF(aoints)
    moints = IntegralHelper(orbitals=rhf.orbitals)
    RMP2(moints, aoints, Alg)
end

function RMP2(O::AbstractRestrictedOrbitals, Alg::RMP2a)
    aoints = IntegralHelper()
    moints = IntegralHelper(orbitals=O)
    RMP2(moints, aoints, Alg)
end

function RMP2(rhf::RHF, Alg::RMP2a)
    aoints = IntegralHelper()
    moints = IntegralHelper(orbitals=rhf.orbitals)
    RMP2(moints, aoints, Alg)
end

function RMP2(M::Molecule, Alg::RMP2a)
    aoints = IntegralHelper{Float64}(molecule=M, eri_type=SparseERI)
    rhf = RHF(M,aoints)
    moints = IntegralHelper(molecule = M, orbitals=rhf.orbitals)
    RMP2(moints, aoints, Alg)
end

function RMP2(moints::IntegralHelper{T1,Chonky,O}, aoints::IntegralHelper{T2,E,AtomicOrbitals}, Alg::RMP2a) where {T1<:AbstractFloat,
                                                                                T2<:AbstractFloat,O<:RHFOrbitals,E<:AbstractERI}
    mp_header()
    mo_from_ao!(moints, aoints, "OVOV")
    RMP2(moints, Alg)
end

function RMP2(moints::IntegralHelper{T1,E1,O}, aoints::IntegralHelper{T2,E2,AtomicOrbitals}, Alg::RMP2a) where {T1<:AbstractFloat,T2<:AbstractFloat,
                                                                                E1<:AbstractDFERI,E2<:AbstractDFERI,O<:RHFOrbitals}
    mp_header()
    mo_from_ao!(moints, aoints, "BOV")
    RMP2(moints, Alg)
end

function RMP2(moints::IntegralHelper{T1,Chonky,O}, aoints::IntegralHelper{T2,E,AtomicOrbitals}, Alg::RMP2a) where {T1<:AbstractFloat,
                                                                                T2<:AbstractFloat,O<:AbstractRestrictedOrbitals,E<:AbstractERI}
    mp_header()
    mo_from_ao!(moints, aoints, "Fia","OVOV")
    RMP2(moints, Alg)
end

function RMP2(moints::IntegralHelper{T1,E1,O}, aoints::IntegralHelper{T2,E2,AtomicOrbitals}, Alg::RMP2a) where {T1<:AbstractFloat,T2<:AbstractFloat,
                                                                                E1<:AbstractDFERI,E2<:AbstractDFERI,O<:AbstractRestrictedOrbitals}
    mp_header()
    mo_from_ao!(moints, aoints, "Fia","BOV")
    RMP2(moints, Alg)
end

function RMP2(ints::IntegralHelper{<:AbstractFloat,<:AbstractERI,<:AbstractRestrictedOrbitals}, Alg::RMP2a)

    # Check frozen core and inactive virtual
    ndocc = ints.molecule.Nα
    nvir = size(ints.orbitals.C,1) - ndocc
    core = Options.get("drop_occ")
    inac = Options.get("drop_vir")

    if core ≥ ndocc
        throw(FermiException("invalid number of frozen orbitals ($core) for $ndocc doubly occupied orbitals"))
    end
    if inac ≥ nvir
        throw(FermiException("invalid number of inactive virtual orbitals ($inac) for $nvir total virtual orbitals"))
    end

    # Collect RHF information

    output("  Starting MP2 computation")
    output(" Number of frozen orbitals:             {:d}" , core)
    output(" Number of inactive orbitals:           {:d}" , inac)
    output(" Number of correlated electron pairs:   {:d}", ndocc-core)
    output(" Number of correlated virtual orbitals: {:d}", nvir-inac)
    output(" ⇒ Total number of MP2 amplitudes:      {:d}", (ndocc-core)^2*(nvir-inac)^2)

    output(repeat("-",80))

    # Compute MP2 energy
    t = @elapsed Emp2 = RMP2_energy(ints, Alg)
    Eref = ints.orbitals.sd_energy

    output("   @Final RMP2 Correlation Energy {:>20.12f} Eₕ", Emp2)
    output("   Reference Energy               {:>20.12f} Eₕ", Eref)
    output("   @Final RMP2 Total Energy       {:>20.12f} Eₕ", Emp2+Eref)
    output(repeat("-",80))

    RMP2(Emp2, Emp2+Eref)
end

function RMP2_energy(ints::IntegralHelper{T,E,O}, Alg::RMP2a) where {T<:AbstractFloat, E<:AbstractERI,O<:AbstractRestrictedOrbitals}
    inac = Options.get("drop_vir")
    core = Options.get("drop_occ")
    ndocc = ints.molecule.Nα
    nbf = size(ints.orbitals.C,1)
    ϵo = ints["Fd"][(core+1):ndocc]
    ϵv = ints["Fd"][(ndocc+1):(nbf-inac)]

    Emp2 = RMP2_nonrhf_energy(ints, ϵo, ϵv)
    Emp2 += RMP2_rhf_energy(ints, ϵo, ϵv)

    return Emp2
end

function RMP2_energy(ints::IntegralHelper{T,E,RHFOrbitals}, Alg::RMP2a) where {T<:AbstractFloat, E<:AbstractERI}
    inac = Options.get("drop_vir")
    core = Options.get("drop_occ")
    ndocc = ints.molecule.Nα
    nbf = size(ints.orbitals.C,1)
    ϵo = ints["Fd"][(core+1):ndocc]
    ϵv = ints["Fd"][(ndocc+1):(nbf-inac)]

    Emp2 = RMP2_rhf_energy(ints, ϵo, ϵv)

    return Emp2
end

function RMP2_nonrhf_energy(ints::IntegralHelper{T,E,O}, ϵo::AbstractArray{T,1}, ϵv::AbstractArray{T,1}) where {T<:AbstractFloat,E<:AbstractERI, O<:AbstractRestrictedOrbitals}

    t = @elapsed begin
    output("Computing non-RHF contribution to the MP2 energy... ", ending="")

    Dia = [ϵo[i]-ϵv[a] for i = eachindex(ϵo), a = eachindex(ϵv)]
    Fia = ints["Fia"]
    Emp2 = sum(transpose(Fia)*(Fia ./ Dia))
    end
    output("Done in {:3.5f} seconds.", t)

    return Emp2
end

function RMP2_rhf_energy(ints::IntegralHelper{T,RIFIT,O}, ϵo::AbstractArray{T,1}, ϵv::AbstractArray{T,1}) where {T<:AbstractFloat, O<:AbstractRestrictedOrbitals}
    Bov = ints["BOV"].data

    output(" Computing DF-MP2 Energy... ", ending="")
    v_size = length(ϵv)
    o_size = length(ϵo)

    # Pre-allocating arrays for threads
    BABs = [zeros(T, v_size, v_size) for i=1:Threads.nthreads()]
    BBAs = [zeros(T, v_size, v_size) for i=1:Threads.nthreads()]

    # Vector containing the energy contribution computed by each thread
    ΔMP2s = zeros(T,Threads.nthreads())
    TWO = T(2.0)
    ONE = one(T)
    t = @elapsed begin
        @sync for i in 1:o_size
            Threads.@spawn begin
            id = Threads.threadid()
            @views Bi = Bov[:,i,:]
            BAB = BABs[id]
            BBA = BBAs[id]
            for j in i:o_size

                @views Bj = Bov[:,j,:]
                @tensor BAB[a,b] = Bi[Q,a]*Bj[Q,b]
                transpose!(BBA,BAB)

                eij = ϵo[i] + ϵo[j]
                dmp2_1 = zero(T)
                dmp2_2 = zero(T)
                @fastmath for b in 1:v_size
                    @inbounds eij_b = eij - ϵv[b]
                    for a in 1:v_size
                        @inbounds begin
                            iajb = BAB[a,b]
                            d = iajb/(eij_b - ϵv[a])
                            dmp2_1 += d*iajb
                            dmp2_2 += d*BBA[a,b]
                        end
                    end
                end
                fac = i != j ? TWO : ONE
                dmp2 = TWO*dmp2_1 - dmp2_2
                ΔMP2s[id] += fac*dmp2
            end
            end
        end
        Emp2 = sum(ΔMP2s)
    end
    output("Done in {:5.5f} seconds.", t)
    return Emp2
end

function RMP2_rhf_energy(ints::IntegralHelper{T,Chonky,O}, ϵo::AbstractArray{T,1}, ϵv::AbstractArray{T,1}) where {T<:AbstractFloat, O<:AbstractRestrictedOrbitals}
    output(" Computing MP2 Energy... ", ending="")
    ovov = ints["OVOV"]

    ΔMP2s = zeros(T,Threads.nthreads())
    TWO = T(2.0)
    t = @elapsed begin
        @sync for b in eachindex(ϵv)
            Threads.@spawn begin
            id = Threads.threadid()
            for a in eachindex(ϵv)
                @inbounds ϵ_ab = ϵv[a] + ϵv[b]
                for j in eachindex(ϵo)
                    @inbounds ϵ_abj = ϵo[j] - ϵ_ab
                    for i in eachindex(ϵo)
                        @fastmath @inbounds ΔMP2s[id] += ovov[i,a,j,b]*(TWO*ovov[i,a,j,b] - ovov[i,b,j,a]) / (ϵo[i]+ϵ_abj)
                    end
                end
            end
            end
        end
    end
    output("Done in {:5.5f} s\n", t)
    return sum(ΔMP2s)
end


module TcrCore

##############################################################################
##
## Dependencies
##
##############################################################################

using Distributions, HypothesisTests, StatsBase

##############################################################################
##
## Exported methods and types (in addition to everything reexported above)
##
##############################################################################

export
    #Agents:
    Agent,
    setupAgentsWithFixedAccuracy,
    setupRandomAgents,
    evaluateCandidateByAgent,
    evaluateCandidateByAgentPrecise,

    #Items:
    getCandidate,
    registryMean,

    #Actions:
    simpleVote,
    binaryTokenVote,
    proRataTokenVote,
    selectRandomAgent,
    selectRandomAgentWithMinBalance,
    noneRedistribution,
    onlyChallengerRewardRedistribution,
    fullRedistribution,
    challenge,
    application,

    #Simulations:
    simSimple,
    simChallenge,
    simToken

    #Research
    # tokensAndDiversifications,
    # tokensAndDiversificationsEffects

##############################################################################
##
## Load files
##
##############################################################################

include("environment/items.jl")
include("environment/agents.jl")
include("actions/vote.jl")
include("actions/select-agent.jl")
include("actions/redistribute.jl")
include("actions/composite-actions.jl")
include("simulations/simulations.jl")
include("experiments/benchmarks.jl")
include("experiments/sample-scenarios.jl")
include("experiments/research.jl")

end # module TcrSimulations


function OnBoardAntennaTurnAroundRangeModifier(arg0::Vector3D)
    return OnBoardAntennaTurnAroundRangeModifier((Vector3D,), arg0)
end

function get_parameters_drivers(obj::OnBoardAntennaTurnAroundRangeModifier)
    return jcall(obj, "getParametersDrivers", List, ())
end

function modify(obj::OnBoardAntennaTurnAroundRangeModifier, arg0::EstimatedMeasurement)
    return jcall(obj, "modify", void, (EstimatedMeasurement,), arg0)
end


"""
Table Schema generic data structure
https://github.com/frictionlessdata/TableSchema.jl#table
"""
mutable struct Table
    source
    headers::Array{String}
    schema::Schema
    errors::Array{ConstraintError}

    function Table(csvfilename::String, schema::Schema=Schema() ; encoding::Encoding=enc"UTF-8")
        if match(r"^https?://", csvfilename) !== nothing
            source = read_remote_csv(csvfilename)
        else
			if encoding === enc"UTF-8"
				source = readdlm(csvfilename, DELIMITER)
			else
				io = StringEncodings.open(csvfilename, encoding)
				source = readdlm(io, DELIMITER)
				close(io)
			end
        end
        headers, source = get_headers(source)
        new(source, headers, schema, [])
    end
    function Table(csvdata::Base.GenericIOBuffer, schema::Schema=Schema())
        source = readdlm(csvdata, DELIMITER)
        headers, source = get_headers(source)
        new(source, headers, schema, [])
    end
    function Table(source, headers::Array{String}, schema::Schema=Schema())
        new(source, headers, schema, [])
    end
    function Table()
        new(Nothing, [], Schema(), [])
    end
end

function read_remote_csv(url::String)
    req = request("GET", url)
    readdlm(req.body, DELIMITER)
end

function get_headers(source::Array)
    headers = [ String(s) for s in source[1,:] ]
    headers, source[2:end,:] # clear the headers
end

function read(t::Table ; data=nothing, keyed=false, extended=false, cast=true, relations=false, limit=nothing)
    (keyed == false && extended == false && relations == false && limit == nothing) || throw(ErrorException("Not implemented"))
    if data != nothing
        if typeof(data) == Array{Any, 2}
            t.headers, t.source = get_headers(data)
        else
            throw(ErrorException("Data must be a 2-dimensional array"))
        end
    end
    if cast
        if !is_valid(t.schema)
            throw(ErrorException("Schema must be valid to cast Table"))
        end
        if t.source == nothing
            throw(ErrorException("Data must be available to cast Table"))
        end
        newtable = Nothing
		# Iterate table rows
        for row in t
			# Apply cast to the row's values
            newrow = cast_row(t.schema, row, false, false)
			# Reshape back into a non-elemental array
			newrow = reshape(newrow, 1, length(newrow))
            if newtable == Nothing
                newtable = newrow
            else
                newtable = vcat(newtable, newrow)
            end
        end
        t.source = newtable
    end
    t.source
end

function infer(t::Table ; limit::Int64=-1)
	# what
	limit !== -1 && throw(ErrorException("limit parameter not implemented"))
	tr = read(t, cast=false)
	t.schema = Schema()
	infer(t.schema, tr, t.headers)
end

function save(t::Table, target::String)
	# TODO: should we check the schema too?
	# !valid(t.schema) &&
    #     throw(TableValidationException("Schema not valid"))
	@debug "Building table data"
	headers = reshape(t.headers, 1, length(t.headers))
	tabledata = vcat(headers, t.source)
	@debug "Saving table data to $target"
	delim = ','
	open(target, "w") do io
		writedlm(io, tabledata, delim)
	end
end

function validate(t::Table)
    is_empty(t.schema) &&
        throw(TableValidationException("No schema available"))
    # TODO: should we check the schema too?
    # !valid(t.schema) &&
    #     throw(TableValidationException("Schema not valid"))
    tr = t.source
    for fld in t.schema.fields
        ix = findall(in([fld.name]), t.headers)
        if length(ix) != 1
            # TODO: shouldn't this just cause a ConstraintError?
            throw(TableValidationException("Missing field defined in Schema: $(fld.name)"))
        end
        try
            column = tr[:,ix]
            for r = 1:size(tr, 2)
                row = tr[r,:]
                checkrow(fld, row[ix[1]], column)
            end
        catch ex
            if isa(ex, ConstraintError)
                push!(t.errors, ex)
            end
        end
    end
    return length(t.errors) == 0
end

Base.eltype(it::Table) = Table
Base.length(it::Table) = size(it.source, 1)
function Base.iterate(it::Table, (el, i)=(nothing, 1))
    if i > length(it); return nothing; end
    return (it.source[i,:], (nothing, i + 1))
end

# Base.start(t::Table) = 1
# Base.done(t::Table, i) = i > size(t.source, 1)
# Base.next(t::Table, i) = t.source[i,:], i+1


# cartpole_play.jl
using Gym
using Knet
using ArgParse

include(joinpath(@__DIR__, "dqn", "dqn.jl"))

main() = main(ARGS)
main(args::String) = main(split(args))
function main(args::Vector{<:AbstractString})
    s = ArgParseSettings()
    # s.description = "(c) Ozan Arkan Can, 2018. An implementation of the deep q network."
    @add_arg_table s begin
        ("model"; arg_type=String; help="model name"; required=true)
        ("--frames"; arg_type=Int; default=2000; help="number of frames")
        # ("--lr"; arg_type=Float64; default=0.001; help="learning rate")
        # ("--gamma"; arg_type=Float64; default=0.99; help="discount factor")
        # ("--hiddens"; arg_type=Int; nargs='+'; default=[32]; help="number of units in the hiddens for the mlp")
        ("--env_id"; default="CartPole-v0")
        ("--render"; action=:store_true)
        # ("--memory"; arg_type=Int; default=10000; help="memory size")
        # ("--bs"; arg_type=Int; default=32; help="batch size")
        ("--stack"; arg_type=Int; default=4; help="length of the frame history")
        # ("--save"; default=""; help="model name")
        # ("--load"; default=""; help="model name")
        # ("--play"; action=:store_true; help="only play")
        ("--printinfo"; action=:store_true; help="print the playing messages")
        # ("--tupdate"; arg_type=Int; default=500; help="update frequency for the target network")
    end
    if "--help" ∈ args || "-h" ∈ args
        ArgParse.show_help(s; exit_when_done=false)
        return
    end
    o = parse_args(args, s)
    o["atype"] = Array{Float32}
    # o["load"] = o["model"]
    o["play"] = true

    env = GymEnv(o["env_id"])
    cartpoleenv = env.gymenv
    cartpoleenv[:_max_episode_steps] = typemax(Int)
    cartpoleenv[:_max_episode_seconds] = typemax(Int)

    w = DQN.load_model(o["model"], o["atype"])
    o["n_hiddens"] = length(w) ÷ 2 - 1
    o["hiddens"] = [0]  # DUMMY
    opts = Dict{String, Array{Float32}}()   # DUMMY
    buffer = DQN.ReplayBuffer(1)    # DUMMY
    exploration = DQN.PiecewiseSchedule([(0, 1.0)]) # DUMMY

    _rewards, _frames = DQN.dqn_learn(w, opts, env, buffer, exploration, o)

    close!(env)
end

if abspath(PROGRAM_FILE) == @__FILE__
    main()
end