Base.@kwdef mutable struct SARSA{O, T<:AbstractTraceUpdate} <: LearningUpdate
    lambda::Float64
    opt::O
    trace::T
    e::IdDict = IdDict()
    prev_discounts::IdDict = IdDict()
end

function update!(lu::SARSA,
                 learner::QLearner{M, LU},
                 demons,
                 obs,
                 next_obs,
                 state,
                 action,
                 next_state,
                 next_action,
                 is_terminal,
                 behaviour_pi_func,
                 reward) where {M<:AbstractMatrix, LU<:SARSA}

     weights = learner.model
     λ = lu.lambda
     e = get!(()->zero(weights), lu.e, weights)::typeof(weights)

     C, next_discounts, target_pis, next_target_pis = get_demon_parameters(lu, learner, demons, obs, state, action, next_obs, next_state, next_action, reward)
     b_πs = behaviour_pi_func(state, obs)
     discounts = get!(()->zero(next_discounts), lu.prev_discounts, learner)::typeof(next_discounts)

     #NOTE: Off policy SARSA(λ) I think is the same as EASRSA except the backup is sample and not expected.
     ρ = if b_πs[action] == 0
         zeros(length(target_pis[:,action]))
     else
         target_pis[:,action] ./ b_πs[action]
     end

     inds = get_action_inds(action, learner.num_actions, learner.num_demons)
     state_action_row_ind = inds

     update_trace!(lu.trace,
                   e,
                   state,
                   λ,
                   repeat(discounts, inner = learner.num_actions),
                   repeat(ρ, inner = learner.num_actions),
                   inds)

     next_preds = learner(next_state, next_action)
     pred = learner(state, action)

     td_err = C .+ next_discounts .* next_preds .- pred
     td_err_across_demons = repeat(vec(td_err), inner=learner.num_actions)

     if lu.opt isa Auto
         next_state_action_row_ind = get_action_inds(next_action, learner.num_actions, learner.num_demons)
         state_discount = zero(e)
         state_discount[state_action_row_ind,:] .+= state'
         state_discount[next_state_action_row_ind,:] .-= next_discounts * next_state'
         abs_phi = abs.(e)
         update!(lu.opt, weights, e, td_err_across_demons, abs_phi .* max.(state_discount, abs_phi))
     else
         Flux.Optimise.update!(lu.opt, weights,  -(e .* td_err_across_demons))
     end
     discounts .= next_discounts
end

function zero_all_nonfinites!(itm)
    if eltype(itm) <: Number
        itm[findall(.!isfinite.(itm))] .= 0.0

    elseif eltype(itm) <: Vector
        [zero_all_nonfinites(lst) for lst in itm]
    else
        throw(ArgumentError("Not Implemented"))
    end
end

function update!(lu::SARSA,
                 learner::LSTDLearner,
                 demons,
                 obs,
                 next_obs,
                 state,
                 action,
                 next_state,
                 next_action,
                 is_terminal,
                 behaviour_pi_func,
                 env_reward)

     C, γ_tp1, π_t, π_tp1 =
         get_demon_parameters(learner,
                              demons,
                              obs,
                              state,
                              action,
                              next_obs,
                              next_state,
                              next_action)

     na = learner.num_actions
     fs = learner.feature_size
     λ = lu.lambda
     t = learner.t
     μ_t = behaviour_pi_func(state, obs)[action]
     μ_tp1 = behaviour_pi_func(next_state, next_obs)

     ρ_t = π_t ./ μ_t
     ρ_tp1 = π_tp1 ./ μ_tp1'

     zero_all_nonfinites!(ρ_t)
     zero_all_nonfinites!(ρ_tp1)

     γ_t = get!(()->zero(γ_tp1), lu.prev_discounts, learner)::typeof(γ_tp1)

     ϕ_t = state
     ϕ_tp1 = next_state

     x_t = get_active_action_state_vector(
         ϕ_t, action, learner.feature_size, learner.num_actions)
     x_tp1 = get_active_action_state_vector(
         ϕ_tp1, next_action, learner.feature_size, learner.num_actions)

    all_gvf_e = get!(()->zero.(learner.w_real), lu.e, learner.w_real)::typeof(learner.w_real)

     for gvf ∈ 1:learner.num_demons
         A_inv = learner.A_inv[gvf]
         e = all_gvf_e[gvf]
         b = learner.b[gvf]
         c = C[gvf]

         # NOTE TD Update
         e .= γ_t[gvf]*λ*ρ_t[gvf] * e + x_t
         #NOTE TB Update
         # e .= γ_t[gvf]*λ*π_t[gvf] * e + x_t
         b .+= (c*e - b)/(t+1)

         # NOTE TD UPDATE
         u = sum(ρ_tp1[gvf, a] .* get_active_action_state_vector(ϕ_tp1, a, fs, na) for a ∈ 1:na)
         # NOTE TB Update
         # u = sum(π_tp1[gvf, a] .* get_active_action_state_vector(ϕ_tp1, a, fs, na) for a ∈ 1:na)
         v = transpose(transpose(x_t - γ_tp1[gvf]*u) * A_inv)

         if t > 0
             scale = (t+1)/t
             vz = dot(v, e)
             Ainv_zvt = (A_inv * e) * transpose(v)
             A_inv .= scale * (A_inv - Ainv_zvt./(t + vz))
         else
             Aev = A_inv*(e*v')
             ve = dot(v, e)
             A_inv .-= Aev/(1 + ve)
         end
         learner.w_real[gvf] .= A_inv * b
     end
     γ_t .= γ_tp1
     learner.t += 1
end

function zero_eligibility_traces!(lu::SARSA)
    for (k, v) ∈ lu.e
        if eltype(v) <: Integer
            lu.e[k] = Int[]
        elseif eltype(v) <: Vector #Used for LSTD
            lu.e[k] = zero.(v)
        else
            v .= 0
        end
    end
end


# This file is a part of BAT.jl, licensed under the MIT License (MIT).

# ToDo: This is type piracy, find a cleaner solution!
@recipe function f(h::Histogram,
                parsel::NTuple{2,Integer};
                intervals = standard_confidence_vals,
                colors = standard_colors,
                diagonal = Dict(),
                upper = Dict(),
                right = Dict())

    pi_x, pi_y = parsel

    seriestype = get(plotattributes, :seriestype, :histogram2d)

    if seriestype == :histogram2d || seriestype == :histogram || seriestype == :hist
        _plots_module() != nothing || throw(ErrorException("Package Plots not available, but required for this operation"))

        @series begin
            seriestype := :bins2d
            colorbar --> true
            h.edges[1], h.edges[2], _plots_module().Surface(h.weights)
        end

    elseif seriestype == :smallest_intervals_contour || seriestype == :smallest_intervals_contourf
        _plots_module() != nothing || throw(ErrorException("Package Plots not available, but required for this operation"))

        colors = colors[sortperm(intervals, rev=true)]

        if seriestype == :smallest_intervals_contour
            plotstyle = :contour
        else
            plotstyle = :contourf
        end

        lev = calculate_levels(h.weights, intervals)
        x, y, m = bin_centers(h)

        # quick fix: needed when plotting contour on top of histogram
        # otherwise scaling of histogram colorbar would change scaling
        lev = lev/10000
        m = m/10000

        colorbar --> false

       if _plots_module().backend() == _plots_module().PyPlotBackend()
            @series begin
                seriestype := plotstyle
                levels --> lev
                linewidth --> 2
                color --> colors # only works with pyplot
                (x, y, m')
            end
        else
             @series begin
                seriestype := plotstyle
                levels --> lev
                linewidth --> 2
                (x, y, m')
            end
        end

    elseif seriestype == :smallest_intervals
        _plots_module() != nothing || throw(ErrorException("Package Plots not available, but required for this operation"))

        colors = colors[sortperm(intervals, rev=true)]

        hists, orig_hist, realintervals = split_smallest(h, intervals)

        for (i, int) in enumerate(realintervals)
            @series begin
                seriestype := :bins2d
                color --> _plots_module().cgrad([colors[i], colors[i]])
                label --> "smallest $(@sprintf("%.2f", realintervals[i]*100))% interval(s)"
                hists[i].edges[1], hists[i].edges[2], _plots_module().Surface(hists[i].weights)
            end
            # fake a legend
            @series begin
                seriestype := :shape
                fillcolor --> colors[i]
                linewidth --> 0
                label --> "smallest $(@sprintf("%.2f", realintervals[i]*100))% interval(s)"
                colorbar --> false
                [hists[i].edges[1][1], hists[i].edges[1][1]], [hists[i].edges[2][1], hists[i].edges[2][1]]
            end
        end

    elseif seriestype == :marginal
        _plots_module() != nothing || throw(ErrorException("Package Plots not available, but required for this operation"))

        size --> (900, 600)

        layout --> _plots_module().grid(2,2, widths=(0.8, 0.2), heights=(0.2, 0.8))
        link --> :both

        if get(diagonal, "seriestype", :histogram) != :histogram
            colorbar --> false
        end

        @series begin
            subplot := 1
            xguide --> "\$p(\\theta_$(pi_x))\$"
            seriestype := get(upper, "seriestype", :histogram)
            bins --> get(upper, "nbins", 200)
            normalize --> get(upper, "normalize", true)
            colors --> get(upper, "colors", standard_colors)
            intervals --> get(upper, "intervals", standard_confidence_vals)
            #mean --> get(upper, "mean", false)
            #std_dev --> get(upper, "std_dev", false)
            #globalmode --> get(upper, "globalmode", false)
            #localmode --> get(upper, "localmode", false)
            h, pi_x
        end

        # empty plot (needed since @layout macro not available)
        @series begin
            seriestype := :scatter
            subplot := 2
            grid := false
            xaxis := false
            yaxis := false
            markersize := 0.1
            markerstrokewidth := 0
            markeralpha := 1
            markerstrokealpha := 1
            legend := false
            label := ""
            xguide := ""
            yguide := ""
            [(0,0)]
        end

        @series begin
            subplot := 3
            seriestype := get(diagonal, "seriestype", :histogram)
            legend --> false

            normalize --> get(diagonal, "normalize", true)
            bins --> get(diagonal, "nbins", 200)
            colors --> get(diagonal, "colors", standard_colors)
            intervals --> get(diagonal, "intervals", standard_confidence_vals)
            #mean --> get(diagonal, "mean", false)
            #std_dev --> get(diagonal, "std_dev", false)
            #globalmode --> get(diagonal, "globalmode", false)
            #localmode --> get(diagonal, "localmode", false)

            h, (pi_x, pi_y)
        end

        @series begin
            subplot := 4
            seriestype := get(right, "seriestype", :histogram)
            orientation := :horizontal
            xguide --> "\$p(\\theta_$(pi_y))\$"
            normalize --> get(right, "normalize", true)
            bins --> get(right, "nbins", 200)
            colors --> get(right, "colors", standard_colors)
            intervals --> get(right, "intervals", standard_confidence_vals)
            #mean --> get(right, "mean", false)
            #std_dev --> get(right, "std_dev", false)
            #globalmode --> get(right, "globalmode", false)
            #localmode --> get(right, "localmode", false)

            h, pi_y
        end

    else
        error("seriestype $seriestype not supported")
    end

end

#--- rectangle bounds ------------------------
@recipe function f(bounds::HyperRectBounds, parsel::NTuple{2,Integer})
    pi_x, pi_y = parsel

    vol = spatialvolume(bounds)
    vhi = vol.hi[[pi_x, pi_y]]; vlo = vol.lo[[pi_x, pi_y]]
    rect_xy = rectangle_path(vlo, vhi)
    bext = 0.1 * (vhi - vlo)
    xlims = (vlo[1] - bext[1], vhi[1] + bext[1])
    ylims = (vlo[2] - bext[2], vhi[2] + bext[2])

    @series begin
        seriestype := :path
        label --> "bounds"
        color --> :darkred
        alpha --> 0.75
        linewidth --> 2
        xlims --> xlims
        ylims --> ylims
        (rect_xy[:,1], rect_xy[:,2])
    end

    nothing
end


@testset "Async" begin

    @testset "async_map" begin
        x = Signal(1; name="x")
        t, y = async_map(-, 0, x)
        z = map(yv->2yv, y; name="z")

        @test (value(t)) == (nothing)
        @test (value(y)) == (0)
        @test (value(z)) == (0)

        push!(x, 2)
        step()
        step()

        @test (value(y)) == (-2)
        @test (value(z)) == (-4)
    end
end


using Modia
@static if VERSION < v"0.7.0-DEV.2005"
    using Base.Test
else
    using Test
end

@testset "RunSimulations" begin

    Modia.ModiaLogging.resetTestStatus()

    include("models/TestVariableTypes.jl")
    include("models/TestEquations.jl")

    include("models/TestElectrical.jl")
    include("models/TestFilter.jl")
    include("models/TestArrayOfComponents.jl")
    include("models/TestConditionalComponents.jl")
    include("models/TestConditionalEquations.jl")

    include("models/TestSpatialDiscretization.jl")

    #include("models/TestCoupledInertias.jl")
    #include("models/TestPendulum.jl")
    #include("models/TestStateSelection.jl")
    #include("models/TestRetranslationOmega.jl")

    Modia.ModiaLogging.printTestStatus()

end
nothing


@recipe function f(fCCF::CCF)

    # plot configuration
    legend:= false

    titlefont:= font(8, "Courier")
    xtickfont:= font(3, "Courier")
    ytickfont:= font(3, "Courier")
    yguidefont:= font(5, "Courier")
    xguidefont:= font(5, "Courier")

    N = fCCF.N
    if split(fCCF.type,'_')[1] == "pacf"
        type = (fCCF.type) == "pacf_step" ? "Stepwise PACF" : "Real PACF"
        type = (fCCF.type) == "pacf_stepwise-real" ? "Stepwise and Real PACF   |s|r|" : type
        auto_prefix = ""
    else
        type = (fCCF.type == "cor") ? "Correlation" : "Covariance"
        auto_prefix = fCCF.auto ? "Auto-" : "Cross-"
    end

    yguide := auto_prefix*type
    xguide := "Lag"

    # Center ticks
    lr = length(fCCF.ccf)
    gap = div(lr,11)
    mp = div(lr+1,2)
    rs = mp:gap:lr
    ls = mp-gap:-gap:1

    if fCCF.auto
        xticks := 1:gap:lr
    else
        xt = vcat(reverse(collect(ls)),collect(rs))
        xticks := (xt, xt .- xt[div(end+1,2)])
    end

    @series begin
        seriestype := :sticks
        ymirror := false
        guide_position := :left
        linewidth := 100/(fCCF.lag+1)

        collect(1:size(fCCF.ccf)[1]+1) .- ((ndims(fCCF.ccf) == 2) ? .1 : 0),
        ndims(fCCF.ccf) == 2 ? fCCF.ccf[:,1] : fCCF.ccf
    end

    if ndims(fCCF.ccf) == 2
        @series begin
            seriestype := :sticks
            ymirror := false
            guide_position := :left
            linewidth := 100/(fCCF.lag+1)

            collect(1:size(fCCF.ccf)[1]+1) .+ .1,
            fCCF.ccf[:,2]
        end
    end

    if (fCCF.type == "cor") | (split(fCCF.type,'_')[1] == "pacf")
        lg = fCCF.lag
        a1 = fCCF.alpha[1]; c1 = fCCF.ci[1]
        a2 = fCCF.alpha[2]; c2 = fCCF.ci[2]
        dc = c2-c1

        @series begin
            seriestype := :hline
            seriescolor := :black
            linealpha := 0.5
            linestyle := :dash
            [-c1,c1]
        end

        @series begin
            seriestype := :hline
            seriescolor := :black
            linealpha := 0.5
            linestyle := :dot
            ax = fCCF.auto ? lg : 2*lg + 1
            annotations :=
                [(ax,c1+dc/8,string("CI %",a1*100),font(3, "Courier")),
                 (ax,c2+dc/8,string("CI %",a2*100),font(3, "Courier"))]
            [-c2,c2]
        end
    end
end


# Custom function to allow for batch evaluation outside of julia
# or to exploit parallelized objective functions
# (included from "AbstractObjectiveInterface")
struct BatchObjectiveFunction{F<:Function} <: Function
    function_handle :: F
end

# evaluation of a BatchObjectiveFunction
function (objf::BatchObjectiveFunction)(x :: Vec)
    return ensure_vec(objf.function_handle(x))
end

# overload broadcasting for BatchObjectiveFunction's
# that are assumed to handle arrays themselves
function Broadcast.broadcasted( objf::BatchObjectiveFunction, X :: VecVec)
    ensure_vec.( objf.function_handle( X ) )
end

function _new_batch( func1 :: F, func2 :: T ) where{ F <:Function ,T <: Function }
    return BatchObjectiveFunction(
        function( x :: Union{ VecVec, Vec } )
            if isa( x, Vec )
                [ func1(x); func2(x) ]
            else
                f1 = func1.(x)
                f2 = func2.(x)
                [ [f1[i];f2[i] ] for i = eachindex(x) ]
            end
        end
    )
end

combine( func1 :: Function, func2 :: BatchObjectiveFunction ) = _new_batch( func1, func2)
combine( func1 :: BatchObjectiveFunction, func2 :: Function) = _new_batch( func1, func2 )
combine( func1 :: BatchObjectiveFunction, func2 :: BatchObjectiveFunction ) = _new_batch( func1, func2 )

"Get a new function function handle stacking the output of `func1` and `func2`."
function combine(func1 :: F, func2 :: T ) where{ F <:Function ,T <: Function }
    return function( x :: Vec )
        return vcat( func1(x), func2(x) )
    end
end


__precompile__()

module PisingerKnapsack

export bouknap, minknap, mulknap

const deps_file = joinpath(dirname(@__FILE__), "..", "deps", "deps.jl")
if isfile(deps_file)
    include(deps_file)
else
    error("PisingerKnapsack not properly installed. Please run import Pkg; Pkg.build(\"PisingerKnapsack\")")
end

include("Cwrapper.jl")
include("doubleknp.jl")

end # module


"""視覚化"""

module PlotTool

using Plots
using ProgressBars

include("utils.jl")
include("kinematics.jl")

using .Kinematics

function make_plot_basic(data)
    x, y, z = split_vec_of_arrays(data.q)
    fig = plot(xlim=(0, data.t[end]))
    plot!(fig, data.t, x, label="l1")
    plot!(fig, data.t, y, label="l2")
    plot!(fig, data.t, z, label="l3")

    x, y, z = split_vec_of_arrays(data.q_dot)
    fig2 = plot(xlim=(0, data.t[end]))
    plot!(fig2, data.t, x, label="l1_dot")
    plot!(fig2, data.t, y, label="l2_dot")
    plot!(fig2, data.t, z, label="l3_dot")

    x, y, z = split_vec_of_arrays(data.error)
    fig3 = plot(xlim=(0, data.t[end]))
    plot!(fig3, data.t, x, label="e1")
    plot!(fig3, data.t, y, label="e2")
    plot!(fig3, data.t, z, label="e3")

    x, y, z = split_vec_of_arrays(data.τ)
    fig4 = plot(xlim=(0, data.t[end]))
    plot!(fig4, data.t, x, label="τ1")
    plot!(fig4, data.t, y, label="τ2")
    plot!(fig4, data.t, z, label="τ3")

    fig_I = plot(
        fig, fig2, fig3, fig4,
        layout=(4, 1), size=(500, 800))
    savefig(fig_I, "julia.png")

end

"""1フレームを描写"""
function draw_frame(
    t::T, q::Vector{T}, xd::Union{Vector{T}, Nothing},
    fig_shape::Union{
        NamedTuple{(:xl, :xu, :yl, :yu, :zl, :zu), Tuple{T, T, T, T, T, T}},
        Nothing
    }
    ) where T

    arm0 = Arm(q, 0)
    arm1 = Arm(q, 1)
    arm2 = Arm(q, 2)

    x, y, z = split_vec_of_arrays(arm0)
    fig = plot(
        x, y, z,
        #marker=:circle,
        aspect_ratio = 1,
        #markersize=2,
        label="1",
        xlabel = "X[m]", ylabel = "Y[m]", zlabel = "Z[m]",
    )
    x, y, z = split_vec_of_arrays(arm1)
    plot!(
        fig,
        x, y, z,
        #marker=:circle,
        aspect_ratio = 1,
        #markersize=2,
        label="2",
        xlabel = "X[m]", ylabel = "Y[m]", zlabel = "Z[m]",
    )
    x, y, z = split_vec_of_arrays(arm2)
    plot!(
        fig,
        x, y, z,
        #marker=:circle,
        aspect_ratio = 1,
        #markersize=2,
        label="3",
        xlabel = "X[m]", ylabel = "Y[m]", zlabel = "Z[m]",
    )
    if !isnothing(xd)
        scatter!(
            fig,
            [xd[1]], [xd[2]], [xd[3]],
            label="xd",
            markershape=:star6,
        )
    end

    # scatter!(
    #     fig,
    #     [xd[1]], [xd[2]],
    #     label="xd_true",
    #     markershape=:star6,
    # )

    plot!(
        fig,
        xlims=(fig_shape.xl, fig_shape.xu),
        ylims=(fig_shape.yl, fig_shape.yu),
        zlims=(fig_shape.zl, fig_shape.zu),
        legend = true,
        size=(600, 600),
        title = string(round(t, digits=2)) * "[s]"
    )

    return fig
end

# fig = draw_frame(
#     0.0,
#     [
#         0.0, 0.0, 0.0,
#         0.0, 0.0, 0.00,
#         0.0, 0.15, 0.1
#     ], nothing, nothing)

"""アニメ作成"""
function make_animation(sol, xd_func)
    println("アニメ作成中...")
    # 枚数決める
    #println(data.t)
    epoch_max = 100
    epoch = length(sol.t)
    if epoch < epoch_max
        step = 1
    else
        step = div(epoch, epoch_max)
    end

    #println(step)

    x_max = 0.2
    x_min = -0.2
    y_max = 0.2
    y_min = -0.2
    z_max = 0.55
    z_min = 0.0
    max_range = max(x_max-x_min, y_max-y_min, z_max-z_min)*0.5
    x_mid = (x_max + x_min) / 2
    y_mid = (y_max + y_min) / 2
    z_mid = (z_max + z_min) / 2
    fig_shape = (
        xl = x_mid-max_range, xu = x_mid+max_range,
        yl = y_mid-max_range, yu = y_mid+max_range,
        zl = z_mid-max_range, zu = z_mid+max_range,
    )

    #xd_all = [P(q, 1.0) for q in data.qd]
    #x, y, z = split_vec_of_arrays(xd_all)

    anim = Animation()
    @gif for i in tqdm(1:step:length(sol.t))
        _fig = draw_frame(sol.t[i], sol.u[i], xd_func(sol.t[i]), fig_shape)
        #plot!(_fig, x, y, z, label="xd",)
        frame(anim, _fig)
    end

    gif(anim, "julia.gif", fps = 60)

    println("アニメ作成完了")
end

end


type VideoMode
    width::UInt32
    height::UInt32
    bits_per_pixel::UInt32

    function VideoMode(width::Integer, height::Integer, bits_per_pixel::Integer)
        return new(width, height, bits_per_pixel)
    end

    function VideoMode(width::Integer, height::Integer)
        return new(width, height, 32)
    end
end

function get_desktop_mode()
    return ccall((:sfVideoMode_getDesktopMode, libcsfml_window), VideoMode, ())
end

function is_valid(mode::VideoMode)
    return ccall((:sfVideoMode_isValid, libcsfml_window), Bool, (VideoMode,), mode)
end


### A Pluto.jl notebook ###
# v0.14.5

using Markdown
using InteractiveUtils

# ╔═╡ 565f87b8-b04e-11eb-38ac-bfec45a98555
using Revise, ReproducePlotUtils, Plots, FileIO, StatsPlots

# ╔═╡ 321dfa1c-7cec-465c-abeb-e8d518487863
using Reproduce

# ╔═╡ 0ce48396-7229-4801-ba66-d9e1d05563c6
ic, diff_dict =
		ReproducePlotUtils.load_data("example_data/dir_tmaze_er_rnn_rmsprop_10/")

# ╔═╡ ef1c7adc-dafe-4543-83b0-1295b31d2b10
FileIO.load(joinpath(ic[1].folder_str, "results.jld2"))["results"]

# ╔═╡ 51c42faa-c44f-47f9-b021-c4fbc3f001df
# data_col = ReproducePlotUtils.get_line_data_for(
#                ic,
#                ["numhidden", "truncation", "cell", "eta"],
#                [];
#                comp=:max,
#                get_comp_data=(x)->ReproducePlotUtils.get_AUE(x, :successes),
#                get_data=(x)->ReproducePlotUtils.get_AUE(x, :successes))

# ╔═╡ 17614876-687b-4b29-9bb2-65736f837ca3
data_col = ReproducePlotUtils.get_line_data_for(
               ic,
               ["numhidden", "truncation", "cell"],
               ["eta"];
               comp=:max,
               get_comp_data=(x)->ReproducePlotUtils.get_MUE(x, :successes),
               get_data=(x)->ReproducePlotUtils.get_rolling_mean_line(x, :successes, 300))

# ╔═╡ 4a520cfd-ed7d-4bf8-8c26-587e99e998e2
plot(data_col, Dict("numhidden"=>10, "truncation"=>12), palette=ReproducePlotUtils.custom_colorant, legend=:topleft)

# ╔═╡ 9bd94861-8f2a-4949-b5b8-14ffdc760341
data_col_sens = ReproducePlotUtils.get_line_data_for(
               ic,
               ["numhidden", "truncation", "cell", "eta"],
               [];
               comp=:max,
               get_comp_data=(x)->ReproducePlotUtils.get_MUE(x, :successes),
               get_data=(x)->ReproducePlotUtils.get_MUE(x, :successes))

# ╔═╡ e6762f85-8922-4b65-97a2-4e83b17eb89a
let
	plot(data_col_sens,
	 	 Dict("numhidden"=>10, "truncation"=>12, "cell"=>"MAGRU");
		 sort_idx="eta", z=1.97, lw=2, palette=ReproducePlotUtils.custom_colorant)
	plot!(data_col_sens,
	 	  Dict("numhidden"=>17, "truncation"=>20, "cell"=>"GRU");
	 	  sort_idx="eta", z=1.97, lw=2, palette=ReproducePlotUtils.custom_colorant)
    plot!(data_col_sens,
	 	  Dict("numhidden"=>20, "truncation"=>12, "cell"=>"AAGRU");
	 	  sort_idx="eta", z=1.97, lw=2, palette=ReproducePlotUtils.custom_colorant)
end

# ╔═╡ ff833f3f-7feb-4f6d-a5ae-0abcc3de4c5f
data_col_eps = ReproducePlotUtils.get_line_data_for(
               ic,
               ["numhidden", "truncation", "cell"],
               ["eta"];
               comp=:max,
               get_comp_data=(x)->ReproducePlotUtils.get_MUE(x, :successes),
               get_data=(x)->ReproducePlotUtils.get_extended_line(x, :successes, :total_steps, n=500))

# ╔═╡ e5331c45-6171-45a9-a537-9e3c64c17fd6
plot(data_col_eps, Dict("numhidden"=>15, "truncation"=>20, "cell"=>"MAGRU"), palette=ReproducePlotUtils.custom_colorant, legend=:topleft, z=1.0)

# ╔═╡ 3a23313d-ed57-4faa-b2b0-bec1d549c255
data_col_box = ReproducePlotUtils.get_line_data_for(
               ic,
               ["numhidden", "truncation", "cell"],
               ["eta"];
               comp=:max,
               get_comp_data=(x)->ReproducePlotUtils.get_MUE(x, :successes),
               get_data=(x)->ReproducePlotUtils.get_MUE(x, :successes))

# ╔═╡ 5e461d4b-98a5-4305-a579-78e59c4c62e7
let
	args = [
		Dict("numhidden"=>20, "truncation"=>20, "cell"=>"GRU"),
		Dict("numhidden"=>20, "truncation"=>12, "cell"=>"AAGRU"),
		Dict("numhidden"=>10, "truncation"=>12, "cell"=>"MAGRU")
		]
	boxplot(data_col_box, args;
			label_idx="cell",
			legend=false,
			palette=ReproducePlotUtils.custom_colorant)
end

# ╔═╡ 78a8f6b0-6425-434b-b2be-a30df9c2df39
let
	params = Dict("numhidden"=>20, "truncation"=>20, "cell"=>"GRU")
	idx = findfirst(data_col_box.data) do ld
		all(ld.line_params[i] == params[i] for i in keys(params))
	end
	data_col_box[idx].data, data_col_box[idx].data_pms
end

# ╔═╡ 85c8a929-5c1d-4375-b2a0-a5f6f1db4e4b
let
	args = [
		Dict("numhidden"=>20, "truncation"=>12, "cell"=>"RNN"),
		Dict("numhidden"=>20, "truncation"=>12, "cell"=>"AARNN"),
		Dict("numhidden"=>10, "truncation"=>12, "cell"=>"MARNN"),
		Dict("numhidden"=>20, "truncation"=>12, "cell"=>"GRU"),
		Dict("numhidden"=>20, "truncation"=>12, "cell"=>"AAGRU"),
		Dict("numhidden"=>10, "truncation"=>12, "cell"=>"MAGRU")
		]
	# args = Dict("numhidden"=>10, "truncation"=>12)
	plt = boxplot(data_col_box, args;
			label_idx="cell",
			legend=false,
			palette=ReproducePlotUtils.custom_colorant,
			trim=true)
	# boxplot!(data_col_box, args;
	# 		label_idx="cell",
	# 		legend=false,
	# 		palette=ReproducePlotUtils.custom_colorant,
	# 		show_median=true, lw=2, outliers=true, range=1.5)
	savefig("dir_tmaze_box_plot.pdf")
	plt
end

# ╔═╡ e564bfe9-7c26-49f8-a956-42b11a706ba1
let
	args = [
		Dict("numhidden"=>20, "truncation"=>12, "cell"=>"GRU"),
		Dict("numhidden"=>20, "truncation"=>12, "cell"=>"AAGRU"),
		Dict("numhidden"=>10, "truncation"=>12, "cell"=>"MAGRU")
		]
	violin(data_col_box, args;
			label_idx="cell",
			legend=false,
			palette=ReproducePlotUtils.custom_colorant)
end

# ╔═╡ eb41c814-0e4b-4087-b35c-1a5626c87167
let
	get_data = (x)->ReproducePlotUtils.get_MUE(x, :successes)
	sub_ic = search(ic, Dict("numhidden"=>20, "truncation"=>20, "cell"=>"GRU", "eta"=>7.8125e-5))
	diff_dict = diff(sub_ic)
	tmp = get_data(FileIO.load(joinpath(sub_ic[1].folder_str, "results.jld2")))
	d = typeof(tmp)[]
	pms_template = NamedTuple(Symbol(k)=>diff_dict[k][1] for k ∈ keys(diff_dict))
	pms = typeof(pms_template)[]
	for (idx, item) ∈ enumerate(sub_ic)
		push!(d, get_data(FileIO.load(joinpath(item.folder_str, "results.jld2"))))
		push!(pms, NamedTuple(Symbol(k)=>item.parsed_args[k] for k ∈ keys(diff_dict)))
	end
	d, pms
end

# ╔═╡ f5524caa-825e-444f-9f4a-627f8c7b67d3

# ╔═╡ Cell order:
# ╠═565f87b8-b04e-11eb-38ac-bfec45a98555
# ╠═321dfa1c-7cec-465c-abeb-e8d518487863
# ╠═0ce48396-7229-4801-ba66-d9e1d05563c6
# ╠═ef1c7adc-dafe-4543-83b0-1295b31d2b10
# ╠═51c42faa-c44f-47f9-b021-c4fbc3f001df
# ╠═17614876-687b-4b29-9bb2-65736f837ca3
# ╠═4a520cfd-ed7d-4bf8-8c26-587e99e998e2
# ╠═9bd94861-8f2a-4949-b5b8-14ffdc760341
# ╠═e6762f85-8922-4b65-97a2-4e83b17eb89a
# ╠═ff833f3f-7feb-4f6d-a5ae-0abcc3de4c5f
# ╠═e5331c45-6171-45a9-a537-9e3c64c17fd6
# ╠═3a23313d-ed57-4faa-b2b0-bec1d549c255
# ╠═5e461d4b-98a5-4305-a579-78e59c4c62e7
# ╠═78a8f6b0-6425-434b-b2be-a30df9c2df39
# ╠═85c8a929-5c1d-4375-b2a0-a5f6f1db4e4b
# ╠═e564bfe9-7c26-49f8-a956-42b11a706ba1
# ╠═eb41c814-0e4b-4087-b35c-1a5626c87167
# ╠═f5524caa-825e-444f-9f4a-627f8c7b67d3


include("dyna_agent.jl")
include("env_models/env_models.jl")


using BenchmarkTools
# using BenchmarkTools: @benchmarkable, BenchmarkGroup

using GenomicFeatures

include(joinpath(@__DIR__, "..", "test", "Utilities.jl"))

import ..Utilities: random_intervals

N = 1000
SEED = 1234
SEQNAMES = "chr" .* string.(1:3)

intervals = random_intervals(SEQNAMES, 1000, N, SEED)
intervals_sorted = sort(intervals)

SUITE = BenchmarkGroup()

let suite = SUITE["accessors"] = BenchmarkGroup()
    s0 = suite["$(typeof(intervals))"] = BenchmarkGroup()
    s0["seqname"] = @benchmarkable(seqname.($intervals))
    s0["leftposition"] = @benchmarkable(leftposition.($intervals))
    s0["rightposition"] = @benchmarkable(rightposition.($intervals))
    s0["strand"] = @benchmarkable(strand.($intervals))
    s0["metadata"] = @benchmarkable(metadata.($intervals))
end

let suite = SUITE["sort"] = BenchmarkGroup()
    suite["$(typeof(intervals))"] = @benchmarkable(sort(i), setup=(i = copy($intervals)))
end

let suite = SUITE["insert"] = BenchmarkGroup()
    suite["shorthand"] = @benchmarkable(IntervalCollection($intervals_sorted))
    suite["type"] = @benchmarkable(IntervalCollection{Int}($intervals_sorted))
end

let suite = SUITE["push"] = BenchmarkGroup()
    suite["$(typeof(intervals))"] = @benchmarkable([push!(col, i) for i in $intervals], setup=(col=IntervalCollection{Int}()))
end

let suite = SUITE["eachoverlap"] = BenchmarkGroup()
    intervals_a = intervals_sorted
    intervals_b = sort(random_intervals(SEQNAMES, 1000, N, SEED+1))

    col_a = IntervalCollection(intervals_a)
    col_b = IntervalCollection(intervals_b)

    As = [intervals_a, col_a]
    Bs = [intervals_b, col_b]

    for (A, B) in Iterators.product(As,Bs)
        str = "$(typeof(A)), $(typeof(B))"
        suite[str] = @benchmarkable(collect(eachoverlap($A,$B)))
    end
end


module AbstractType

export analyticalSolution

export GoverningDiffEqn, Analysis, Approach
export Dimensions, Material
export BoundaryCondition, Source
export ResultSpec

export Err_InputCombo

abstract type GoverningDiffEqn  end
abstract type Analysis          end
abstract type Approach          end
abstract type Dimensions        end
abstract type Material          end
abstract type BoundaryCondition end
abstract type Source            end
abstract type ResultSpec        end

const Err_InputCombo = "Analytical solution not yet implemented for requested input combination"

function analyticalSolution(g::GoverningDiffEqn, an::Analysis, ap::Approach,
                            d::Dimensions, m::Material,
                            b::BoundaryCondition, s::Source,
                            r::ResultSpec)
  error(Err_InputCombo)
end

end


# generic implementations

macro reroute(f, g)
    quote
        @inline function Cassette.overdub(ctx::HessianSparsityContext,
                                          f::typeof($(esc(f))),
                                          args...)
            println("rerouted")
            Cassette.overdub(
                ctx,
                invoke,
                $(esc(g.args[1])),
                $(esc(:(Tuple{$(g.args[2:end]...)}))),
                args...)
        end
    end
end

@reroute LinearAlgebra.BLAS.dot LinearAlgebra.dot(Any, Any)
@reroute LinearAlgebra.BLAS.axpy! LinearAlgebra.axpy!(Any,
                                                      AbstractArray,
                                                      AbstractArray)


using .InteractiveViz
using Statistics
using DSP.Periodograms

export iplot, iplot!, ispecgram

"""
$(SIGNATURES)
Plots interactive timeseries of the signal.
"""
function InteractiveViz.iplot(s::SampledSignal, args...; kwargs...)
  s1 = samples(s)
  if isanalytic(s1)
    @warn "Plotting only real part of complex signal"
    s1 = real.(s1)
  end
  t = domain(s)
  if maximum(t) <= 1.0
    t *= 1000.0
    xlabel = "Time (ms)"
  else
    xlabel = "Time (s)"
  end
  iplot(t, s1, args...; xlabel=xlabel, kwargs...)
end

"""
$(SIGNATURES)
Plots interactive timeseries of the signal over a previous plot.
"""
function InteractiveViz.iplot!(s::SampledSignal; kwargs...)
  s1 = samples(s)
  if isanalytic(s1)
    @warn "Plotting only real part of complex signal"
    s1 = real.(s1)
  end
  t = domain(s)
  maximum(t) <= 1.0 && (t *= 1000.0)
  iplot!(t, s1; kwargs...)
end

"""
$(SIGNATURES)
Plots interactive spectrogram of the signal.
"""
function ispecgram(s; fs=framerate(s), nfft=min(div(length(s),8),256), noverlap=div(nfft,2), window=nothing, pooling=mean, kwargs...)
  @assert Base.size(s,2) == 1 "ispecgram only works with vectors"
  s1 = samples(s)[:,1]
  if isanalytic(s1)
    @warn "Using only real part of complex signal"
    s1 = real.(s1)
  end
  p = spectrogram(s1, nfft, noverlap; fs=inHz(fs), window=window)
  t = time(p)
  f = freq(p)
  tunit = "s"
  funit = "Hz"
  if maximum(f) >= 10000
    f /= 1000.0
    funit = "kHz"
  end
  if maximum(t) <= 1.0
    t *= 1000.0
    tunit = "ms"
  end
  xlabel = "Time ("*tunit*")"
  ylabel = "Frequency ("*funit*")"
  z = pow2db.(power(p)')
  cmax = ceil(Int, maximum(z)/5)*5
  cmin = floor(Int, max(cmax-50, minimum(z))/5)*5
  iheatmap(z, t[1], t[end], f[1], f[end]; clim=(cmin, cmax), xlabel=xlabel, ylabel=ylabel, pooling=pooling, kwargs...)
end


using DataDeps

register(DataDep(
  "trains",
  "http://archive.ics.uci.edu/ml/datasets/Trains",
  "http://archive.ics.uci.edu/ml/machine-learning-databases/trains/trains-transformed.data",
  "d536c86b5d3b913e5a494dce1ea6f09a2b756b29831ce9156c02fe7ba25a556e",
  post_fetch_method=(path -> begin
    # The missing values are `-`. Replace with `?`
    write(path, replace(read(path, String), "-" => "?"))

    UCIData.process_dataset(path,
        target_index=33,
        feature_indices=1:32,
        categoric_indices=[4:5; 7; 9:10; 12; 14:15; 17; 19:20; 22:32],
        delim=' ',
        footerskip=1,
        ignorerepeated=true,
    )
  end),
))


module Languages

using Unicode
using Niancat.Gameface

struct Word
    w::String
end

isinalphabet(c::Char) :: Bool = occursin(c, "abcdefghijklmnopqrstuvwxyzåäö")
ismeaningfuldiacritic(c::Char) :: Bool = occursin(c, "åäö")

function normalizechar(c::AbstractChar) :: String
    stripmark = !ismeaningfuldiacritic(c)
    Unicode.normalize(string(c), stripmark=stripmark)
end

function normalize(s::String) :: String
    lower = Unicode.normalize(s, casefold=true)
    normalizedchars = [normalizechar(c) for c in lower]

    normalizedword = join(normalizedchars)
    filter(isinalphabet, normalizedword)
end

function sortword(s::String) :: String
    ns = normalize(s)
    String(sort([c for c in ns]))
end

isanagram(s1::String, s2::String) = sortword(s1) == sortword(s2)

struct SwedishDictionary <: LanguageDictionary
    words::Set{Word}
    anagrams::Dict{String, Vector{String}}

    function SwedishDictionary(words::AbstractVector{<:String})
        normalizedwords = Word[]
        anagrams = Dict{String, Vector{String}}()
        for w in words
            nw = normalize(w)
            push!(normalizedwords, Word(nw))

            push!(get!(anagrams, sortword(nw), String[]), nw)
        end
        new(Set(normalizedwords), anagrams)
    end
end

Base.in(word::String, s::SwedishDictionary) = in(Word(normalize(word)), s.words)

findanagrams(dictionary::SwedishDictionary, s::String) = get(dictionary.anagrams, sortword(s), [])

normalizedword(::SwedishDictionary, word::String) :: String = normalize(word)

export SwedishDictionary, Word, isanagram, findanagrams, normalizedword

end


function scatter(data::CuMatrix, targets::CuVector, bins, ⊕)
    function kernel!(output, data, targets)
        thread = threadIdx().x + (blockIdx().x - 1) * blockDim().x - 1
        i = thread % size(output, 1) + 1
        j = thread ÷ size(output, 1) + 1

        @inbounds if i <= size(output, 1) && j <= length(targets)
            @atomic output[i, targets[j]] = ⊕(output[i, targets[j]], data[i, j])
        end
        return
    end

    # TODO: FIX TYPES
    output = CUDA.zeros(eltype(data), size(data, 1), bins)

    threads = 256
    blocks = ceil(Int, size(output, 1) * length(targets) / threads)
    @cuda blocks=blocks threads=threads kernel!(output, data, targets)

    return output
end

# Defining the fused message-aggregate operation
# function gather(X::CuMatrix, A::CuSparseMatrixHCOO, ⊕)
#     function kernel!(O, X, I, J, V)
#         thread = threadIdx().x + (blockIdx().x - 1) * blockDim().x - 1
#         i = thread % size(O, 1) + 1
#         j = thread ÷ size(O, 1) + 1

#         @inbounds if i <= size(O, 1) && j <= length(J)
#             @atomic O[i, J[j]] = ⊕(O[i, J[j]], X[i, I[j]] * V[j])
#         end
#         return
#     end

#     # TODO: FIX TYPES
#     O = CUDA.zeros(size(X, 1), size(A, 2))

#     threads = 256
#     blocks = ceil(Int, size(O, 1) * length(A.nzVal) / threads)
#     @cuda blocks=blocks threads=threads kernel!(O, X, A.rowInd, A.colInd, A.nzVal)

#     return O
# end


module Analysis

using Common
using SizeModel
using IntervalModel

function test_dist()
	X = Distribution(4, Array(Float64, 0), Array(Float64, 0), Array(Float64, 0), Array(Float64, 0))
	X.p = [0.1, 0.2, 0.3, 0.4]
	# X.p = [1. / float64(X.count) for i = 1:X.count]
	X.c = [1. for i = 1:X.count]
	check_validity(X)
	update_derived_values(X)

	# println(Common.unique(X, 5.77))
	println(unique_inv(X, 3.))
	println(ccp(X, 3))
	# println(ccp(X, 4))

	# println(unique_inv(X, 19.))
	# println(ccp(X, 19))
end

function test_basic()
	# n = unique key count
	# s = zipf skew
	n = 2 * 1000 * 1000
	# n = 10 * 1000 * 1000
	# n = 100 * 1000 * 1000
	# n = 1000 * 1000 * 1000
	# s = 0.    # uniform
	s = 0.99    # zipf
	# s = 1.		# zipf, fast generation
	println("n = ", n)
	println("s = ", s)

	# @time X = zipf(n, s)
	# println("X.c = ", length(X.c))
	# @time X = compress(X, 0.1)
	# println("X.c = ", length(X.c), " (after compression)")
	@time X = load_zipf_compressed(n, s, 0.1)
	# @time X = load_zipf_compressed(n, s, 0.001)
	println("X.c = ", length(X.c), " (after compression)")
	println()

	@time update_derived_values(X)

	# ## basic
	# println("== basic")
	# println("unique(X, 4000.) = ", Common.unique(X, 4000.))
	# println()

	# ## fill time
	# println("== fill time")
	# println("unique_inv(X, 10000.) = ", unique_inv(X, 10000.))
	# println("unique(unique_inv(X, 10000.)) = ", Common.unique(X, unique_inv(X, 10000.)))
	# println("unique_inv(X, 100000.) = ", unique_inv(X, 100000.))
	# println("unique(unique_inv(X, 100000.)) = ", Common.unique(X, unique_inv(X, 100000.)))
	# println("unique_inv(X, 1000000.) = ", unique_inv(X, 1000000.))
	# println("unique(unique_inv(X, 1000000.)) = ", Common.unique(X, unique_inv(X, 1000000.)))
	# println()
end

function test_wa_estimation()
	# #### WA estimation
	# println("== WA estimation")

	# # log size, level 0 table count
	# log_size = 4. * 1048576. / 1000
	# l0_count = 4.
	# println("log_size = ", log_size);
	# println("l0_count = ", l0_count);
	# println()

	# ## size-based compaction
	# println("= size-based")
	# sizes = [100000., 1000000., float64(n)]
	# # sizes = [30000., 200000., float64(n)]
	# println("sizes = ", sizes);
	# wa = LevelDB_WA(X, log_size, l0_count, sizes)
	# println("WA (log->0) = ", wa[1])
	# println("WA (0->1) = ", wa[2])
	# println("WA (1->2) = ", wa[3])
	# println("WA (2->3) = ", wa[4])
	# println("WA = ", sum(wa))

	# ## interval_based multi-level compaction
	# println("= interval_based multi-level")

	# # Level 1, 2, 3 compaction interval ratios (not normalized)
	# compact_interval_ratio = [18., 130., 1000.]
	# current_compact_interval = geom_mean(compact_interval_ratio)

	# compact_interval = compact_interval_ratio * (log_size * l0_count / current_compact_interval)
	# println("compact_interval = ", compact_interval)
	# println("avg_compact_interval = ", geom_mean(compact_interval))

	# wa = ProbMultiLevel_WA(X, log_size, l0_count, compact_interval)
	# println("WA (log->0) = ", wa[1])
	# println("WA (0->1) = ", wa[2])
	# println("WA (1->2) = ", wa[3])
	# println("WA (2->3) = ", wa[4])
	# println("WA = ", sum(wa))
end

function test_wa_optimization()
	#### WA optimization
	println("== WA optimization")

	# ftol = 1.e-8
	# max_time = 1.
	ftol = 1.e-16
	max_time = 5.
	#max_time = 10.

	# log size, level 0 table count, total level count (besides level-0)
	item_size = 1000.
	l1_size = 10. * 1048576. / item_size
	log_size = 4. * 1048576. / item_size
	l0_count = 4.
	# l0_count = 8.
	# l0_count = 12.
	println("log_size = ", log_size);
	println("l0_count = ", l0_count);
	println()

	# println("=========")
	# SizeModel.print(X, log_size, l0_count, [28500., 90856., 278944., 802296., 2000000.])
	# println()
	# IntervalModel.print_twolevel(X, log_size, l0_count, [log_size * l0_count, unique_inv(X, 28500.), unique_inv(X, 90856.), unique_inv(X, 278944.), unique_inv(X, 802296.)])
	# println("=========")
	# println()

	results_wa = []
	results_params = []

	# growth_factors = [3.]
	# level_counts = [5]
	growth_factors = [3., 4., 5., 10., 20.]
	level_counts = [3, 4, 5, 6, 7, 8]

	# ## size-based compaction - fixed (LevelDB)
	# println("= size-based - fixed")
	# for growth_factor in growth_factors
	#	sizes = SizeModel.init_sizes(X, l1_size, growth_factor)
	#	wa = SizeModel.calculate_wa(X, log_size, l0_count, sizes)
	#   wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
	#   wa_sum = get_wa(wa_r_factor, wa)
	#	push!(results_wa, ["size_fixed_wa", growth_factor, wa_sum, wa])
	#	push!(results_params, ["size_fixed_sizes", growth_factor, sizes])
	#	SizeModel.print(X, log_size, l0_count, sizes)
	# end
	# println()

	# ## size-based compaction - flexible
	# println("= size-based - flexible")
	# for level_count in level_counts
	#	sizes = SizeModel.init_sizes(X, l1_size, 0., level_count)
	#	sizes = SizeModel.optimize_wa(X, log_size, l0_count, sizes, wa_r_factor, ftol, max_time)
	#	wa = SizeModel.calculate_wa(X, log_size, l0_count, sizes)
	#   wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
	#   wa_sum = get_wa(wa_r_factor, wa)
	#	push!(results_wa, ["size_flexible_wa", level_count, wa_sum, wa])
	#	push!(results_params, ["size_flexible_sizes", level_count, sizes])
	#	SizeModel.print(X, log_size, l0_count, sizes)
	# end
	# println()

	# ## interval_based two-level compaction
	# println("= interval-based two-level")
	# for level_count in level_counts
	#	intervals = IntervalModel.init_intervals(log_size, l0_count, level_count)
	#	# intervals = [log_size * l0_count, unique_inv(X, sizes[1]), unique_inv(X, sizes[2]), unique_inv(X, sizes[3]), unique_inv(X, sizes[4])]
	#	intervals = IntervalModel.optimize_wa_twolevel(X, log_size, l0_count, intervals, wa_r_factor, ftol, max_time)
	#	wa = IntervalModel.calculate_wa_twolevel(X, log_size, l0_count, intervals)
	#   wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
	#   wa_sum = get_wa(wa_r_factor, wa)
	#	push!(results_wa, ["interval_two_wa", level_count, wa_sum, wa])
	#	push!(results_params, ["interval_two_intervals", level_count, intervals])
	#	IntervalModel.print_twolevel(X, log_size, l0_count, intervals)
	# end
	# println()

	# ## interval_based multi-level compaction
	# println("= interval-based multi-level")
	# for level_count in level_counts
	#	intervals = IntervalModel.init_intervals(log_size, l0_count, level_count)
	#	intervals = IntervalModel.optimize_wa_multilevel(X, log_size, l0_count, intervals, wa_r_factor, ftol, max_time)
	#	wa = IntervalModel.calculate_wa_multilevel(X, log_size, l0_count, intervals)
	#   wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
	#   wa_sum = get_wa(wa_r_factor, wa)
	#	push!(results_wa, ["interval_multi_wa", level_count, wa_sum, wa])
	#	push!(results_params, ["interval_multi_intervals", level_count, intervals])
	#	IntervalModel.print_multilevel(X, log_size, l0_count, intervals)
	# end
	# println()
end

#function sensitivity_1()
#	println("== sensitivity")
#
#	results_sensitivity = []
#
#	# wa_r_factor = 0.7
#	wa_r_factor = 0.0
#
#	println("= sensitivity - item size")
#	item_sizes = [10, 100, 1000, 10000, 100000]
#	for item_size in item_sizes
#		n = 2 * 1024 * 1024 * 1024 / item_size
#		s = 0.99    # zipf
#		X = load_zipf_compressed(iround(n), s, 0.1)
#		update_derived_values(X)
#
#		l1_size = 10. * 1048576. / item_size
#		log_size = 4. * 1048576. / item_size
#		l0_count = 4.
#
#		sizes = SizeModel.init_sizes(X, l1_size, 10.)
#		wa = SizeModel.calculate_wa(X, log_size, l0_count, sizes)
#		wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
#		wa_sum = get_wa(wa_r_factor, wa)
#		push!(results_sensitivity, ["sensitivity_item_size_wa", item_size, wa_sum, wa])
#	end
#	println()
#
#	n = 2 * 1000 * 1000
#	s = 0.99    # zipf
#	X = load_zipf_compressed(n, s, 0.1)
#	update_derived_values(X)
#
#	item_size = 1000.
#	l1_size = 10. * 1048576. / item_size
#	log_size = 4. * 1048576. / item_size
#
#	println("= sensitivity - l0 count")
#	l0_counts = [4., 8., 12.]
#	for l0_count in l0_counts
#		sizes = SizeModel.init_sizes(X, l1_size, 10.)
#		wa = SizeModel.calculate_wa(X, log_size, l0_count, sizes)
#		wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
#		wa_sum = get_wa(wa_r_factor, wa)
#		push!(results_sensitivity, ["sensitivity_l0_count_wa", l0_count, wa_sum, wa])
#	end
#	println()
#
#
#	l0_count = 4.
#
#	println("= sensitivity - level size")
#	level_count = 3
#	mod_level = 1	# level 1, 2
#    exps = [i for i = -0.8:0.08:0.8]
#
#	sizes = SizeModel.init_sizes(X, l1_size, 0., level_count)
#	sizes_opt = SizeModel.optimize_wa(X, log_size, l0_count, sizes, wa_r_factor, ftol, max_time)
#    for exp1 in exps
#        for exp2 in exps
#			sizes = copy(sizes_opt)
#			sizes[mod_level] *= 10. ^ exp1
#			sizes[mod_level + 1] *= 10. ^ exp2
#			if mod_level - 1 >= 1 && sizes[mod_level - 1] > sizes[mod_level]
#				continue
#            elseif sizes[mod_level] > sizes[mod_level + 1]
#                continue
#            elseif mod_level + 2 <= level_count && sizes[mod_level + 1] > sizes[mod_level + 2]
#                continue
#			end
#			wa = SizeModel.calculate_wa(X, log_size, l0_count, sizes)
#			wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
#			wa_sum = get_wa(wa_r_factor, wa)
#			#push!(results_sensitivity, ["sensitivity_level_size_wa", ratio1, ratio2, wa_sum, wa])
#			push!(results_sensitivity, ["sensitivity_level_size_wa", exp1, exp2, wa_sum, wa])
#		end
#	end
#
#	write_output("output.txt", cat(1, results_wa, results_params, results_sensitivity))
#end

function write_output(filename, rows)
	f = open(filename, "w")
	for row in rows
		for i = 1:length(row)
			if typeof(row[i]) == Float64
				@printf(f, "%5.2f", row[i])
			else
				print(f, row[i])
			end
			if i < length(row)
				print(f, "\t")
			end
		end
		println(f)
	end
	close(f)
end

function write_output_more_digits(filename, rows)
	f = open(filename, "w")
	for row in rows
		for i = 1:length(row)
			if typeof(row[i]) == Float64
				@printf(f, "%5.4f", row[i])
			else
				print(f, row[i])
			end
			if i < length(row)
				print(f, "\t")
			end
		end
		println(f)
	end
	close(f)
end

function unique_data()
	results = []

	eval_f = (results, prefix, n, s) -> begin
		X = load_zipf_compressed(n, s, 0.1)
		update_derived_values(X)

		c = 1.
		count = 0
		while true
			u = Common.unique(X, c)
			if c == Inf || count > 1000
				break
			end
			push!(results, ["$(prefix)_unique", n, s, c, u])
			c = min(c * 1.2, unique_inv(X, u + float64(n) / 100.))
			count += 1
		end
	end

	#for n in [1000000, 3300000, 10 * 1000000, 33 * 1000000, 100 * 1000000, 330 * 1000000, 1000 * 1000000]
	#	for s in [0.00, 0.99]
	#		eval_f(results, "unique_item_count", n, s)
	#	end
	#end

	for n in [100 * 1000000]
		#for s in [0.00, 0.20, 0.40, 0.60, 0.80, 0.99, 1.20, 1.40, 1.60, 1.80, 2.00]
		for s in [0.00, 0.20, 0.40, 0.60, 0.80, 0.99, 1.20, 1.40, 1.60]
			eval_f(results, "unique_skewness", n, s)
		end
	end

	write_output("output_unique.txt", results)
end

function sensitivity_2()
	results = []

	l0_count = 4.
	# l0_count = 8.
	# l0_count = 12.

	# wa_r_factor = 0.7
	wa_r_factor = 0.0

	ftol = 1.e-16
	max_time = 300.

	eval_f = (results, prefix, n, s, item_size, level_count, log_size) -> begin
		println(prefix, ", n=", n, ", s=", s, ", item_size=", item_size, ", level_count=", level_count, ", log_size=", log_size)
		l1_size = 10. * 1048576. / item_size
		# log_size = 4. * 1048576. / item_size

		X = load_zipf_compressed(n, s, 0.1)
		update_derived_values(X)

		println("  leveldb")
		if level_count == 0
			sizes0 = SizeModel.init_sizes(X, l1_size, 10.)
			level_count = length(sizes0)
		else
			sizes0 = SizeModel.init_sizes(X, l1_size, 0., level_count)
		end
		wa0 = SizeModel.calculate_wa(X, log_size, l0_count, sizes0)
		wa0 = ([round(v, 2) for v in wa0[1]], [round(v, 2) for v in wa0[2]])
		wa0_sum = get_wa(wa_r_factor, wa0)
		push!(results, ["$(prefix)_leveldb_wa_r", n, s, round(Int, log_size * item_size), level_count, sum(wa0[1]), wa0[1]])
		push!(results, ["$(prefix)_leveldb_wa_w", n, s, round(Int, log_size * item_size), level_count, sum(wa0[2]), wa0[2]])
		push!(results, ["$(prefix)_leveldb_wa", n, s, round(Int, log_size * item_size), level_count, wa0_sum])
		push!(results, ["$(prefix)_leveldb_sizes", n, s, round(Int, log_size * item_size), level_count, sizes0])

		wa0_rc = SizeModel.calculate_random_compaction_wa(X, log_size, l0_count, sizes0)
		wa0_rc = ([round(v, 2) for v in wa0_rc[1]], [round(v, 2) for v in wa0_rc[2]])
		wa0_rc_sum = get_wa(wa_r_factor, wa0_rc)
		push!(results, ["$(prefix)_leveldb_rc_wa_r", n, s, round(Int, log_size * item_size), level_count, sum(wa0_rc[1]), wa0_rc[1]])
		push!(results, ["$(prefix)_leveldb_rc_wa_w", n, s, round(Int, log_size * item_size), level_count, sum(wa0_rc[2]), wa0_rc[2]])
		push!(results, ["$(prefix)_leveldb_rc_wa", n, s, round(Int, log_size * item_size), level_count, wa0_rc_sum])

		println("  leveldb_opt")
		sizes1 = SizeModel.optimize_wa(X, log_size, l0_count, sizes0, wa_r_factor, ftol, max_time)
		wa1 = SizeModel.calculate_wa(X, log_size, l0_count, sizes1)
		wa1 = ([round(v, 2) for v in wa1[1]], [round(v, 2) for v in wa1[2]])
		wa1_sum = get_wa(wa_r_factor, wa1)
		push!(results, ["$(prefix)_leveldb_opt_wa_r", n, s, round(Int, log_size * item_size), level_count, sum(wa1[1]), wa1[1]])
		push!(results, ["$(prefix)_leveldb_opt_wa_w", n, s, round(Int, log_size * item_size), level_count, sum(wa1[2]), wa1[2]])
		push!(results, ["$(prefix)_leveldb_opt_wa", n, s, round(Int, log_size * item_size), level_count, wa1_sum])
		push!(results, ["$(prefix)_leveldb_opt_sizes", n, s, round(Int, log_size * item_size), level_count, sizes1])

		println("  twolevel_opt")
		intervals2 = IntervalModel.init_intervals(log_size, l0_count, level_count)
		intervals2 = IntervalModel.optimize_wa_twolevel(X, log_size, l0_count, intervals2, wa_r_factor, ftol, max_time)
		wa2 = IntervalModel.calculate_wa_twolevel(X, log_size, l0_count, intervals2)
		wa2 = ([round(v, 2) for v in wa2[1]], [round(v, 2) for v in wa2[2]])
		wa2_sum = get_wa(wa_r_factor, wa2)
		sizes2 = IntervalModel.calculate_sizes_twolevel(X, log_size, l0_count, intervals2)
		push!(results, ["$(prefix)_twolevel_opt_wa_r", n, s, round(Int, log_size * item_size), level_count, sum(wa2[1]), wa2[1]])
		push!(results, ["$(prefix)_twolevel_opt_wa_w", n, s, round(Int, log_size * item_size), level_count, sum(wa2[2]), wa2[2]])
		push!(results, ["$(prefix)_twolevel_opt_wa", n, s, round(Int, log_size * item_size), level_count, wa2_sum])
		push!(results, ["$(prefix)_twolevel_opt_sizes", n, s, round(Int, log_size * item_size), level_count, sizes2])

		wa_best_sum = wa0_sum
		wa_best = wa0
		sizes_best = sizes0
		if wa_best_sum > wa1_sum
			wa_best_sum = wa1_sum
			wa_best = wa1
			sizes_best = sizes1
		end
		if wa_best_sum > wa2_sum
			wa_best_sum = wa2_sum
			wa_best = wa2
			sizes_best = sizes2
		end
		push!(results, ["$(prefix)_leveldb_best_wa_r", n, s, round(Int, log_size * item_size), level_count, sum(wa_best[1]), wa_best[1]])
		push!(results, ["$(prefix)_leveldb_best_wa_w", n, s, round(Int, log_size * item_size), level_count, sum(wa_best[2]), wa_best[2]])
		push!(results, ["$(prefix)_leveldb_best_wa", n, s, round(Int, log_size * item_size), level_count, wa_best_sum])
		push!(results, ["$(prefix)_leveldb_best_sizes", n, s, round(Int, log_size * item_size), level_count, sizes_best])

		# println("  multilevel")
		# intervals3 = IntervalModel.init_intervals(log_size, l0_count, level_count)
		# intervals3 = IntervalModel.optimize_wa_multilevel(X, log_size, l0_count, intervals3, wa_r_factor, ftol, max_time)
		# wa3 = IntervalModel.calculate_wa_multilevel(X, log_size, l0_count, intervals3)
		# wa3 = ([round(v, 2) for v in wa3[1]], [round(v, 2) for v in wa3[2]])
		# wa3_sum = get_wa(wa_r_factor, wa3)
		# sizes3 = IntervalModel.calculate_sizes_multilevel(X, log_size, l0_count, intervals3)
		# push!(results, ["$(prefix)_multilevel_wa", n, s, round(Int, log_size * item_size), level_count, wa3_sum, wa3])
		# push!(results, ["$(prefix)_multilevel_sizes", n, s, round(Int, log_size * item_size), level_count, sizes3])

		# println("  rocksdb")
		# # https://github.com/facebook/rocksdb/blob/master/util/options.cc
		# # ColumnFamilyOptions::OptimizeLevelStyleCompaction()
		# rocksdb_memtable_memory_budget = log_size * 2.		# so that we use the same memory for mbuf
		# rocksdb_write_buffer_size = rocksdb_memtable_memory_budget / 4.
		# rocksdb_min_write_buffer_number_to_merge = 2.
		# rocksdb_level0_file_num_compaction_trigger = 2.
		# rocksdb_max_bytes_for_level_base = rocksdb_memtable_memory_budget

		# new_log_size = rocksdb_write_buffer_size * rocksdb_min_write_buffer_number_to_merge
		# @assert new_log_size == log_size
		# new_l0_count = rocksdb_level0_file_num_compaction_trigger
		# new_l1_size = rocksdb_max_bytes_for_level_base
		# sizes3 = SizeModel.init_sizes(X, new_l1_size, 10.)
		# new_level_count = length(sizes3)
		# wa3 = SizeModel.calculate_wa(X, new_log_size, new_l0_count, sizes3)
		# wa3 = ([round(v, 2) for v in wa3[1]], [round(v, 2) for v in wa3[2]])
		# wa3_sum = get_wa(wa_r_factor, wa3)
		# push!(results, ["$(prefix)_rocksdb_wa", n, s, round(Int, log_size * item_size), new_level_count, wa3_sum, wa3])
		# push!(results, ["$(prefix)_rocksdb_sizes", n, s, round(Int, log_size * item_size), new_level_count, sizes3])

		# println("  mbuf")
		# sizes5 = copy(sizes0)
		# for i in 1:level_count
		#	if sizes5[i] < log_size
		#		sizes5[i] = log_size
		#	end
		# end
		# wa5 = SizeModel.calculate_mbuf_wa(X, log_size, sizes5)
		# wa5 = ([round(v, 2) for v in wa5[1]], [round(v, 2) for v in wa5[2]])
		# wa5_sum = get_wa(wa_r_factor, wa5)
		# push!(results, ["$(prefix)_mbuf_wa", n, s, round(Int, log_size * item_size), level_count, wa5_sum, wa5])
		# push!(results, ["$(prefix)_mbuf_sizes", n, s, round(Int, log_size * item_size), level_count, sizes5])

		# println("  mbuf_opt")
		# sizes6 = SizeModel.optimize_mbuf_wa(X, log_size, sizes5, wa_r_factor, ftol, max_time)
		# wa6 = SizeModel.calculate_mbuf_wa(X, log_size, sizes6)
		# wa6 = ([round(v, 2) for v in wa6[1]], [round(v, 2) for v in wa6[2]])
		# wa6_sum = get_wa(wa_r_factor, wa6)
		# push!(results, ["$(prefix)_mbuf_opt_wa", n, s, round(Int, log_size * item_size), level_count, wa6_sum, wa6])
		# push!(results, ["$(prefix)_mbuf_opt_sizes", n, s, round(Int, log_size * item_size), level_count, sizes6])

		# println("  leveldb_corrected")
		# sizes7 = copy(sizes0)
		# for i in 1:level_count
		#	if sizes7[i] < log_size
		#		sizes7[i] = log_size
		#	end
		# end
		# wa7 = SizeModel.calculate_wa(X, log_size, l0_count, sizes7)
		# wa7 = ([round(v, 2) for v in wa7[1]], [round(v, 2) for v in wa7[2]])
		# wa7_sum = get_wa(wa_r_factor, wa7)
		# push!(results, ["$(prefix)_leveldb_corrected_wa", n, s, round(Int, log_size * item_size), level_count, wa7_sum, wa7])
		# push!(results, ["$(prefix)_leveldb_corrected_sizes", n, s, round(Int, log_size * item_size), level_count, sizes7])

		println()
	end

	item_size = 1000

	for n in [1000000, 3300000, 10 * 1000000, 33 * 1000000, 100 * 1000000, 330 * 1000000, 1000 * 1000000]
	# for n in [1000000]
	# for n in [1000000, 3300000, 10 * 1000000]
	# for n in [100 * 1000000]
		for s in [0.00, 0.99]
		# for s in [0.00]
			eval_f(results, "sensitivity_item_count", n, s, item_size, 0, 4 * 1048576 / item_size)
			write_output("output_sensitivity.txt", results)
		end
	end

	# for n in [100 * 1000000, 200 * 1000000, 300 * 1000000, 400 * 1000000, 500 * 1000000, 600 * 1000000, 700 * 1000000, 800 * 1000000, 900 * 1000000, 1000 * 1000000]
	#	for s in [0.00, 0.99]
	#		eval_f(results, "sensitivity_item_count2", n, s, item_size, 0, 4 * 1048576 / item_size)
	#		write_output("output_sensitivity.txt", results)
	#	end
	# end

	for n in [100 * 1000000]
	  for s in [0.00, 0.20, 0.40, 0.60, 0.80, 0.99, 1.20, 1.40, 1.60]
		eval_f(results, "sensitivity_skewness", n, s, item_size, 0, 4 * 1048576 / item_size)
		write_output("output_sensitivity.txt", results)
	  end
	end

	for n in [100 * 1000000]
	  for s in [0.00, 0.99]
		for level_count in [3, 4, 5, 6, 7, 8, 9, 10]
			# this should be adjusted as used n (100 M = 5)
			if level_count == 5
				eval_f(results, "sensitivity_level_count", n, s, item_size, 0, 4 * 1048576 / item_size)
			else
				eval_f(results, "sensitivity_level_count", n, s, item_size, level_count, 4 * 1048576 / item_size)
			end
			write_output("output_sensitivity.txt", results)
		end
	  end
	end

	# use 10 M that we can plug into the implementation
	for n in [10 * 1000000]
		for s in [0.00, 0.99]
		# for s in [0.00]
			for log_size_b in [4 * 1048576, 10000000, 100000000, 1000000000]
				eval_f(results, "sensitivity_log_size", n, s, item_size, 0, log_size_b / item_size)
				write_output("output_sensitivity.txt", results)
			end
		end
	end
end

function density_analysis()
	item_size = 1000.
	l1_size = 10. * 1048576. / item_size

	n = 100 * 1000000

	results = []

	eval_f = (results, prefix, n, s, size) -> begin
		X = load_zipf_compressed(n, s, 0.1)
		update_derived_values(X)

		dinterval = interval_from_density(X, size)

		d = 0.
		while true
			ds = density(X, dinterval, d)
			push!(results, ["$(prefix)_unique", n, s, size, d, ds])
			if d == n - 1.
				break
			end
			d = min(d + n * 0.01, n - 1.)
		end
	end

	for s in [0.00, 0.99]
		size = l1_size * 1000.	# level 4

		X = load_zipf_compressed(n, s, 0.1)
		update_derived_values(X)

		println("n = ", n)
		println("s = ", s)
		println("size = ", size)
		println("UniqueInv = ", unique_inv(X, size))

		@time dinterval = interval_from_density(X, size)
		println("interval_from_density = ", dinterval)
		println("density_sum = ", density_sum(X, dinterval))

		eval_f(results, "density", n, s, size)
	end

	write_output_more_digits("output_density.txt", results)
end

function cola_test()
	#n = 1000000
	n = 100 * 1000000
	s = 0.00
	#s = 0.99

	item_size = 1000.
	log_size = 4. * 1048576. / item_size

	# growth factor
	#r = 2
	r = 3
	#r = 4
	#r = 10
	# level count
	L = round(Int, ceil(log(n / log_size) / log(float(r))))

	# wa_r_factor = 0.7
	wa_r_factor = 0.0

	X = load_zipf_compressed(n, s, 0.1)
	update_derived_values(X)

	wa = SizeModel.calculate_wa_cola(X, log_size, r, L)

	wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
	wa_sum = get_wa(wa_r_factor, wa)
	println(wa_sum, " ", wa)

	wa = SizeModel.calculate_wa_samt(X, log_size, r, L)

	wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
	wa_sum = get_wa(wa_r_factor, wa)
	println(wa_sum, " ", wa)
end

function silt_test()
	#n = 1000000
	n = 100 * 1000000
	s = 0.00

	hash_occupancy = 0.93
	tag_size = 15

	# from Section 5 in the SILT paper
	partition_count = 4
	hash_size = 2 ^ (tag_size + 2)
	hash_count = 62

	# wa_r_factor = 0.7
	wa_r_factor = 0.0

	mem_use_func = (n, partition_count, hash_size, hash_count) -> begin
		((6 * hash_size) + (2 * hash_size * hash_count / 2) + (0.4 * round(n / partition_count))) * partition_count
	end

	X = load_zipf_compressed(round(Int, round(n / partition_count)), s, 0.1)
	update_derived_values(X)

	wa = SizeModel.calculate_wa_silt(X, float64(hash_size), hash_occupancy, hash_count)
	wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
	wa_sum = get_wa(wa_r_factor, wa)
	println(wa_sum, " ", wa)
	mem_use = mem_use_func(n, partition_count, hash_size, hash_count)
	println(mem_use / 1000000., " ", "MB")

	s = 0.99

	X = load_zipf_compressed(round(Int, round(n / partition_count)), s, 0.1)
	update_derived_values(X)

	wa = SizeModel.calculate_wa_silt(X, float64(hash_size), hash_occupancy, hash_count)
	wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
	wa_sum = get_wa(wa_r_factor, wa)
	println(wa_sum, " ", wa)
	mem_use = mem_use_func(n, partition_count, hash_size, hash_count)
	println(mem_use / 1000000., " ", "MB")

	hash_count = 25

	wa = SizeModel.calculate_wa_silt(X, float64(hash_size), hash_occupancy, hash_count)
	wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
	wa_sum = get_wa(wa_r_factor, wa)
	println(wa_sum, " ", wa)
	mem_use = mem_use_func(n, partition_count, hash_size, hash_count)
	println(mem_use / 1000000., " ", "MB")

	hash_threshold = 0.10
	hash_count = 4

	mem_use_func_multi = (n, partition_count, hash_size, hash_count, hash_threshold) -> begin
		((6 * hash_size) + (2 * hash_size * (hash_count - 1) / 2) + (2 * round(n / partition_count) * hash_threshold) + (0.4 * round(n / partition_count))) * partition_count
	end
	wa = SizeModel.calculate_wa_silt_multi(X, float64(hash_size), hash_occupancy, hash_count, hash_threshold)

	wa = ([round(v, 2) for v in wa[1]], [round(v, 2) for v in wa[2]])
	wa_sum = get_wa(wa_r_factor, wa)
	println(wa_sum, " ", wa)
	mem_use = mem_use_func_multi(n, partition_count, hash_size, hash_count, hash_threshold)
	println(mem_use / 1000000., " ", "MB")

end

function uc_test()
	# https://github.com/facebook/rocksdb/wiki/Universal-Compaction

	# level0_file_num_compaction_trigger = 4		# default
	# level0_file_num_compaction_trigger = 6
	# level0_file_num_compaction_trigger = 8		# used in the example: https://github.com/facebook/rocksdb/wiki/Universal-Style-Compaction-Example
	# level0_file_num_compaction_trigger = 9
	level0_file_num_compaction_trigger = 12

	size_ratio = 1                      	# default
	# size_ratio = 10                   	# for skewed workloads
	# size_ratio = 100                  	# for skewed workloads
	#min_merge_width = 2                	# not implemented
	#max_merge_width = 1000000          	# not implemented
	max_size_amplification_percent = 200	# default
	#compression_size_percent = -1      	# not implemented
	stop_style = 1                      	# default; CompactionStopStyle::kCompactionStopStyleTotalSize
	#allow_trivial_move = false         	# not implemented

	# verbose = true
	verbose = false
	# simple = true
	simple = false

	merge_all_f = (X, arr) -> begin
		if simple
			min(sum(arr), 1000.)
		else
			interval_sum = 0.
			for i in 1:length(arr)
				interval_sum += unique_inv(X, arr[i])
			end
			Common.unique(X, interval_sum)
		end
	end

	uc_merge_f = (X, tables) -> begin
		# precondition
	    len = length(tables)
		if len >= level0_file_num_compaction_trigger
			@assert len >= 2

			# condition 1
			numer = sum(tables[1:end - 1])
			denom = tables[end]
			if numer / denom > max_size_amplification_percent / 100.
				source_size = sum(tables)
				merged_size = merge_all_f(X, tables)
				tables = [merged_size]
				if verbose
					println("cond 1: ", tables)
				end
				return tables, source_size, merged_size
			end

			# condition 2
			# PickCompactionUniversalReadAmp()
		    for start_i in 1:len
				candidate_count = 1
			    candidate_size = tables[start_i]
			    for i in start_i + 1:len
					sz = candidate_size * (100. + size_ratio) / 100.
					if sz < tables[i]
						break
					end
				    if stop_style == 0
						# kCompactionStopStyleSimilarSize
				        sz = (tables[i] * (100. + size_ratio)) / 100.
				        if sz < candidate_size
							break
						end
						candidate_size = tables[i]
					else
						# kCompactionStopStyleTotalSize
						candidate_size += tables[i]
					end
					candidate_count += 1
				end
				if candidate_count >= 2
					last_i = start_i + candidate_count - 1
					source_size = sum(tables[start_i:last_i])
					merged_size = merge_all_f(X, tables[start_i:last_i])
					tables = vcat(tables[1:start_i - 1], [merged_size], tables[last_i + 1:end])
					if verbose
						println("cond 2: ", tables)
					end
					return tables, source_size, merged_size
				end
			end

			# condition 3
			# PickCompactionUniversalReadAmp() with large ratio, a limited mergeable file count
			start_i = 1
			candidate_count = len - level0_file_num_compaction_trigger
			if candidate_count >= 2
			    last_i = start_i + candidate_count - 1
				source_size = sum(tables[start_i:last_i])
				merged_size = merge_all_f(X, tables[start_i:last_i])
				tables = vcat(tables[1:start_i - 1], [merged_size], tables[last_i + 1:end])
				if verbose
					println("cond 3: ", tables)
				end
				return tables, source_size, merged_size
			end
		end

		return tables, 0., 0.
	end

	eval_f = (results, prefix, n, s, item_size, log_size) -> begin
		if simple
			item_size = 1.
			log_size = 1.
		end

		X_load = load_zipf_compressed(n, 0.00, 0.1)
		update_derived_values(X_load)

		X_trans = load_zipf_compressed(n, s, 0.1)
		update_derived_values(X_trans)

		steps = 0
		inserts = 0.
		reads = 0.
		writes = 0.

		if simple
			tables = [1000.]
		else
			tables = [float64(X_load.count)]
		end

		do_inserts_f = (X, max_inserts) -> begin
			while inserts < max_inserts
				if length(tables) < level0_file_num_compaction_trigger + 2
					if simple
						new_table_size = 1.
					else
						new_table_size = Common.unique(X, log_size)
					end

					writes += log_size
					writes += new_table_size
					inserts += log_size
					tables = vcat([new_table_size], tables)

					if verbose
						println("insert: ", tables)
					end
				end

				# while true
				 	tables, new_reads, new_writes = uc_merge_f(X, tables)
				 	reads += new_reads
				 	writes += new_writes
				#	if new_writes == 0.
				#		break
				#	end
				# end

				steps += 1
				if steps % 20000 == 0
					wa_r = reads / inserts
					wa_w = writes / inserts
					println("WA_r: ", wa_r)
					println("WA_w: ", wa_w)
					# inserts = 0.
					# reads = 0.
					# writes = 0.
				end
			end
		end

		do_inserts_f(X_load, n)

		if verbose
			println("initial:", tables)
		end

		steps = 0
		inserts = 0.
		reads = 0.
		writes = 0.

		do_inserts_f(X_trans, n * 10)

		if verbose
			println("final:  ", tables)
		end

		wa_r = reads / inserts
		wa_w = writes / inserts
		println("WA_r: ", wa_r)
		println("WA_w: ", wa_w)

		wa_r = round(wa_r, 2)
		wa_w = round(wa_w, 2)
		push!(results, ["$(prefix)_wa_r", n, s, round(Int, log_size * item_size), wa_r])
		push!(results, ["$(prefix)_wa_w", n, s, round(Int, log_size * item_size), wa_w])
	end

	item_size = 1000.
	log_size = 4. * 1048576. / item_size
	# log_size = 256. * 1048576. / item_size

	results = []

	for n in [1000000, 3300000, 10000000, 33000000]
	# for n in [33000000]
		for s in [0.00, 0.99]
		# for s in [0.99]
			println("n = ", n)
			println("s = ", s)
			eval_f(results, "universal_compaction", n, s, item_size, log_size)
		end
	end

	write_output("output_universal_compaction.txt", results)
end

function run()
	#unique_data()
	sensitivity_2()
	# density_analysis()
	#cola_test()
	#uc_test()
end

end


printstyled("  seismogram differentiation/integration\n", color=:light_green)

convert_file = path*"/SampleFiles/SEIS/2019_M7.1_Ridgecrest.seis"
S = randSeisData(3, s=1.0)
S.units = ["m/s2", "m", "m/s"]
U = deepcopy(S)
S1 = deepcopy(S)

printstyled("  conversion to m/s\n", color=:light_green)
redirect_stdout(out) do
  convert_seis!(S, v=3)
  @test S.units[1] == S.units[2] == S.units[3] == "m/s"
  @test S.x[3] == U.x[3]
end

printstyled("  conversion to m/s2\n", color=:light_green)
redirect_stdout(out) do
  convert_seis!(S, v=2, units_out="m/s2")
  @test S.units[1] == S.units[2] == S.units[3] == "m/s2"
  @test isapprox(S.x[1], U.x[1])
end

printstyled("  conversion to m\n", color=:light_green)
redirect_stdout(out) do
  convert_seis!(S1, v=2, units_out="m")
  @test S1.units[1] == S1.units[2] == S1.units[3] == "m"
  @test isapprox(S1.x[2], U.x[2])
end

printstyled("  conversion from m to m/s2 and back to m\n", color=:light_green)
redirect_stdout(out) do
  for j = 1:100
    S = randSeisData(3, s=1.0)
    S.units = ["m", "m", "m"]

    for i = 1:3
      Nx = min(length(S.x[i]), 20000)
      S.t[i] = [1 S.t[i][1,2]; Nx 0]
      S.x[i] = rand(Float64, Nx)
    end
    U = deepcopy(S)

    convert_seis!(S, units_out="m/s2")
    @test S.units[1] == S.units[2] == S.units[3] == "m/s2"
    convert_seis!(S, units_out="m")
    @test S.units[1] == S.units[2] == S.units[3] == "m"

    for i = 1:3
      @test isapprox(S.x[i], U.x[i])
    end
  end
end

printstyled("    at Float32 precision\n", color=:light_green)
redirect_stdout(out) do
  S = rseis(convert_file)[1]
  U = deepcopy(S)
  convert_seis!(S, units_out="m/s2", v=2)
  convert_seis!(S)
  for i = 1:16
    @test isapprox(S.x[i], U.x[i])
  end
end

printstyled("    at Float64 precision\n", color=:light_green)
redirect_stdout(out) do
  S = rseis(convert_file)[1]
  for i = 1:S.n
    S.x[i] = Float64.(S.x[i])
  end
  detrend!(S)

  U = deepcopy(S)
  convert_seis!(S, units_out="m/s2")
  for i = 17:19
     @test S.x[i] == U.x[i]
  end
  convert_seis!(S)
  for i = 1:16
    @test isapprox(S.x[i], U.x[i])
    @test isapprox(S.t[i], U.t[i])
  end

  convert_seis!(S, units_out="m")
  convert_seis!(S, units_out="m/s")
  convert_seis!(S, units_out="m")
  convert_seis!(S, units_out="m/s2")
  T = convert_seis(S, units_out="m")

  # Test on a SeisChannel
  C = deepcopy(U[1])
  convert_seis!(C, units_out="m", v=1)
  convert_seis!(C, units_out="m/s", v=1)
  convert_seis!(C, units_out="m/s2", v=1)
  convert_seis!(C, units_out="m", v=1)
  D = convert_seis(C, units_out="m/s2", v=1)
end


using DSGE, JLD2, Dates, ModelConstructors, DataFrames
using Test, FileIO

path = dirname(@__FILE__)

# Initialize models
function make_test_model(year::Int)
    m = Model990()
    vint = Dates.format(Dates.Date(year, 4, 10), "yymmdd")
    m <= Setting(:data_vintage, vint)
    m <= Setting(:cond_vintage, vint)
    m <= Setting(:date_forecast_start, DSGE.quartertodate("$year-Q1"))
    m <= Setting(:date_conditional_end, DSGE.quartertodate("$year-Q1"))
    m <= Setting(:forecast_horizons, 16)
    return m
end

m_new = make_test_model(2016)
m_old = make_test_model(2014)

# Read in data and parameters
@load "$path/../reference/decompose_forecast_args.jld2" df_new df_old params_new params_old

# Read in expected outputs
@load "$path/../reference/decompose_forecast_out.jld2" exp_decomps

cond_types = [(:none, :none), (:none, :full), (:full, :none), (:full, :full)]
for (cond_new, cond_old) in cond_types
    # Test inner method
    decomps = decompose_forecast(m_new, m_old,
                                 cond_new == :full ? df_new : df_new[1:end-1, :],
                                 cond_old == :full ? df_old : df_old[1:end-1, :],
                                 params_new, params_old, cond_new, cond_old,
                                 [:obs, :pseudo]; check = true)

    for k in keys(decomps)
        @test decomps[k] ≈ exp_decomps[(cond_new, cond_old)][k]
    end
end

# Test outer method
m_new = AnSchorfheide(testing = true)
m_new <= Setting(:saveroot, tempdir())
m_new <= Setting(:date_forecast_start, quartertodate("2015-Q4"))
m_new <= Setting(:date_conditional_end, quartertodate("2015-Q4"))
m_new <= Setting(:use_population_forecast, true)
m_new <= Setting(:forecast_horizons, 12)

estroot = normpath(joinpath(dirname(@__FILE__), "..", "reference"))
overrides = forecast_input_file_overrides(m_new)
overrides[:mode] = joinpath(estroot, "optimize.h5")
overrides[:full] = joinpath(estroot, "mhsave_test.h5")
m_new <= Setting(:forecast_block_size, 20)

m_old = deepcopy(m_new)
m_old <= Setting(:date_forecast_start, quartertodate("2014-Q4"))
m_old <= Setting(:date_conditional_end, quartertodate("2014-Q4"))

df_new = load_data(m_new)
df_old = df_new[1:end-4, :]

decompose_forecast(m_new, m_old, df_new, df_old, :mode, :none, :none, [:obs, :pseudo];
                         verbose = :none)
decomposition_means(m_new, m_old, :mode, :none, :none, [:obs, :pseudo]; verbose = :none)
decompose_forecast(m_new, m_old, df_new, df_old, :full, :none, :none, [:obs, :pseudo], verbose = :none)
decomposition_means(m_new, m_old, :full, :none, :none, [:obs, :pseudo], verbose = :none)

## Regime switching
custom_settings = Dict{Symbol, Setting}(
    :data_vintage             => Setting(:data_vintage, "160812"),
    :cond_vintage             => Setting(:cond_vintage, "160812"),
    :cond_id                  => Setting(:cond_id, 0),
    :use_population_forecast  => Setting(:use_population_forecast, true),
    :date_presample_start     => Setting(:date_presample_start, Date(1959, 9, 30)),
    :date_forecast_start      => Setting(:date_forecast_start, DSGE.quartertodate("2016-Q3")),
    :date_conditional_end     => Setting(:date_conditional_end, DSGE.quartertodate("2016-Q3")),
    :forecast_horizons        => Setting(:forecast_horizons, 16),
    :n_mon_anticipated_shocks => Setting(:n_mon_anticipated_shocks, 6))
m    = Model1002("ss10", testing = true, custom_settings = custom_settings)  # baseline model
m_rs = Model1002("ss51v", testing = true, custom_settings = custom_settings) # pseudo regime switching (identical values for standard deviations)
m_rs <= Setting(:rate_expectations_source, :ois)
m_rs.settings[:regime_switching] = Setting(:regime_switching, true)
m_rs.settings[:n_regimes] = Setting(:n_regimes, 3)
m_rs.settings[:regime_switching_ndraws] = Setting(:regime_switching_ndraws, 4)
m_rs.test_settings[:regime_switching] = Setting(:regime_switching, true)
m_rs.test_settings[:n_regimes] = Setting(:n_regimes, 3)
m_rs.test_settings[:regime_switching_ndraws] = Setting(:regime_switching_ndraws, 4)
m_rs.settings[:regime_dates] = Setting(:regime_dates,
                                       Dict{Int, Date}(1 => date_presample_start(m), 2 => Date(2010, 3, 31), 3 => Date(2012, 9, 30)))
m_rs.test_settings[:regime_dates] = Setting(:regime_dates,
                                            Dict{Int, Date}(1 => date_presample_start(m), 2 => Date(2010, 3, 31), 3 => Date(2012, 9, 30)))
df = load("$path/../reference/regime_switch_data.jld2", "none")

for i in 1:3
    adj = (i == 1) ? 1. : .95
    ModelConstructors.set_regime_val!(m_rs[:α], i, adj * m[:α].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_g], i, adj * m[:σ_g].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_b], i, adj * m[:σ_b].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_μ], i, adj * m[:σ_μ].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_ztil], i, adj * m[:σ_ztil].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_λ_f], i, adj * m[:σ_λ_f].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_λ_w], i, adj * m[:σ_λ_w].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_r_m], i, adj * m[:σ_r_m].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_σ_ω], i, adj * m[:σ_σ_ω].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_μ_e], i, adj * m[:σ_μ_e].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_γ], i, adj * m[:σ_γ].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_π_star], i, adj * m[:σ_π_star].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_lr], i, adj * m[:σ_lr].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_z_p], i, adj * m[:σ_z_p].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_tfp], i, adj * m[:σ_tfp].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_gdpdef], i, adj * m[:σ_gdpdef].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_corepce], i, adj * m[:σ_corepce].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_gdp], i, adj * m[:σ_gdp].value; override_bounds = true)
    ModelConstructors.set_regime_val!(m_rs[:σ_gdi], i, adj * m[:σ_gdi].value; override_bounds = true)

    for j = 1:DSGE.n_mon_anticipated_shocks(m_rs)
        ModelConstructors.set_regime_val!(m_rs[Symbol("σ_r_m$(j)")], i, adj * m[Symbol("σ_r_m$(j)")]; override_bounds = true)
    end
end

m_rs_old = deepcopy(m_rs)
m_rs_old <= Setting(:date_forecast_start, quartertodate("2015-Q3"))
m_rs_old <= Setting(:date_conditional_end, quartertodate("2015-Q3"))
m_rs_old.settings[:forecast_horizons] = Setting(:forecast_horizons, 12)
m_rs_old.test_settings[:forecast_horizons] = Setting(:forecast_horizons, 12)
df_old = df[1:end - 4, :]

decompose_forecast(m_rs, m_rs_old, df, df_old, map(x -> x.value, m_rs.parameters),
                   map(x -> x.value, m_rs_old.parameters), :none, :none, [:obs, :pseudo])
@test_broken decomposition_means(m_rs, m_rs_old, :mode, :none, :none, [:obs, :pseudo]; verbose = :none) # TODO: NEED METHOD THAT LOADS THE DRAWS FROM A SAVED FILE B/C THAT ONE WRITES TO STUFF TO A JLD2 FILE

nothing


"""
    read_fasta_alignment(filename::AbstractString, max_gap_fraction::Real)

Return a `L × M` matrix of integers (`L` is the sequence length, and `M` is the number of sequences) of the
multiple sequence alignment contained in the fasta file `filename` including all sequences with a fraction of gaps (`-`) ≤
`max_gap_fraction`.
"""
function read_fasta_alignment(filename::AbstractString, max_gap_fraction::Real)

    f = FastaReader(filename)

    max_gap_fraction = Float64(max_gap_fraction)

    # pass 1

    seqs = Int[]
    inds = Int[]
    fseqlen = 0

    for (name, seq) in f
        ngaps = 0
        if f.num_parsed == 1
            ls = length(seq)
            resize!(inds, ls)
            for i = 1:ls
                c = seq[i]
                if c != '.' && c == uppercase(c)
                    fseqlen += 1
                    inds[fseqlen] = i
                    c == '-' && (ngaps += 1)
                end
            end
        else
            ls = length(seq)
            ls == length(inds) || error("inputs are not aligned")
            tstfseqlen = 0
            for i = 1:ls
                c = seq[i]
                if c != '.' && c == uppercase(c)
                    tstfseqlen += 1
                    inds[tstfseqlen] == i || error("inconsistent inputs")
                    c == '-' && (ngaps += 1)
                end
            end
            tstfseqlen == fseqlen || error("inconsistent inputs")
        end
        ngaps / fseqlen <= max_gap_fraction && push!(seqs, f.num_parsed)
    end

    length(seqs) > 0 || error("Out of $(f.num_parsed) sequences, none passed the filter (max_gap_fraction=$max_gap_fraction)")

    # pass 2
    Z = Array{Int8}(undef, fseqlen, length(seqs))

    seqid = 1
    for (name, seq) in f
        seqs[end] < f.num_parsed && break
        seqs[seqid] == f.num_parsed || continue
        for i = 1:fseqlen
            c = seq[inds[i]]
            Z[i, seqid] = letter2num(c)
        end
        seqid += 1
    end
    @assert seqid == length(seqs) + 1

    close(f)

    return Z
end

let alphabet = [ 1,21, 2, 3, 4, 5, 6, 7, 8,21, 9,10,11,12,21,13,14,15,16,17,21,18,19,21,20]
               # A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y
    global letter2num
    function letter2num(c::Union{Char,UInt8})
        i = UInt8(c) - 0x40
        1 <= i <= 25 && return alphabet[i]
        return 21
    end
end

"""
    remove_duplicate_sequences(Z::Matrix{Ti}) where Ti<:Integer
Remove duplicate sequences (columns) in the alignment matrix `Z`

# Examples
```
julia> Z = [1 2 3 1;
            1 3 2 1;]
2×4 Array{Int64,2}:
 1  2  3  1
 1  3  2  1

julia> remove_duplicate_sequences(Z)
removing duplicate sequences... done: 4 -> 3
([1 2 3; 1 3 2], [1, 2, 3])
```
"""
function remove_duplicate_sequences(Z::Matrix{Ti}) where Ti<:Integer

    N, M = size(Z)
    hZ = Array{UInt}(undef, M)
    @inbounds for i = 1:M
        hZ[i] = hash(Z[:,i])
    end
    print("removing duplicate sequences... ")
    ref_seq_ind = Array{Int}(undef, M)
    ref_seq = Dict{UInt,Int}()
    @inbounds for i = 1:M
        ref_seq_ind[i] = get!(ref_seq, hZ[i], i)
    end
    uniqueseqs = collect(values(ref_seq))
    # Check for collisions
    collided = falses(M)
    @inbounds for i in 1:M
        k = ref_seq_ind[i]
        k == i && continue
        for j = 1:N
            Z[j,i] != Z[j,k] && (collided[i] = true; break)
        end
    end
     if any(collided)
        nowcollided = BitArray(undef, M)
        while any(collided)
            # Collect index of first row for each collided hash
            empty!(ref_seq)
            @inbounds for i = 1:M
                collided[i] || continue
                ref_seq_ind[i] = get!(ref_seq, hZ[i], i)
            end
            for v in values(ref_seq)
                push!(uniqueseqs, v)
            end

            # Check for collisions
            fill!(nowcollided, false)
            @inbounds for i = 1:M
                k = ref_seq_ind[i]
                (!collided[i] || k == i) && continue
                for j = 1:N
                    Z[j,i] != Z[j,k] && (nowcollided[i] = true; break)
                end
            end
            collided, nowcollided = nowcollided, collided
        end
    end

    sort!(uniqueseqs)

    newM = length(uniqueseqs)
    newZ = Z[:,uniqueseqs]
    println("done: $M -> $newM")
    return newZ,uniqueseqs
end


export Combiner

# TODO: Have combiner store the locations
# of the uncombined and combined indices
# This can generalize to a Combiner that combines
# multiple set of indices, e.g. (i,j),(k,l) -> (a,b)
struct Combiner <: TensorStorage{Number}
  perm::Vector{Int}
  comb::Vector{Int}
  cind::Vector{Int}
  isconj::Bool
  function Combiner(perm::Vector{Int}, comb::Vector{Int}, cind::Vector{Int}, isconj::Bool)
    return new(perm, comb, cind, isconj)
  end
end

Combiner() = Combiner(Int[], Int[], Int[1], false)

Combiner(perm::Vector{Int}, comb::Vector{Int}) = Combiner(perm, comb, Int[1], false)

data(::Combiner) = error("Combiner storage has no data")

blockperm(C::Combiner) = C.perm
blockcomb(C::Combiner) = C.comb
cinds(C::Combiner) = C.cind
isconj(C::Combiner) = C.isconj
setisconj(C::Combiner, isconj) = Combiner(blockperm(C), blockcomb(C), cinds(C), isconj)

function copy(C::Combiner)
  return Combiner(copy(blockperm(C)), copy(blockcomb(C)), copy(cinds(C)), isconj(C))
end

eltype(::Type{<:Combiner}) = Number

eltype(::Combiner) = eltype(Combiner)

promote_rule(::Type{<:Combiner}, StorageT::Type{<:Dense}) = StorageT

conj(::AllowAlias, C::Combiner) = setisconj(C, !isconj(C))
conj(::NeverAlias, C::Combiner) = conj(AllowAlias(), copy(C))

#
# CombinerTensor (Tensor using Combiner storage)
#

const CombinerTensor{ElT,N,StoreT,IndsT} =
  Tensor{ElT,N,StoreT,IndsT} where {StoreT<:Combiner}

combinedindex(T::CombinerTensor) = inds(T)[1]
uncombinedinds(T::CombinerTensor) = popfirst(inds(T))

blockperm(C::CombinerTensor) = blockperm(storage(C))
blockcomb(C::CombinerTensor) = blockcomb(storage(C))

function contraction_output(
  ::TensorT1, ::TensorT2, indsR::IndsR
) where {TensorT1<:CombinerTensor,TensorT2<:DenseTensor,IndsR}
  TensorR = contraction_output_type(TensorT1, TensorT2, IndsR)
  return similar(TensorR, indsR)
end

function contraction_output(
  T1::TensorT1, T2::TensorT2, indsR
) where {TensorT1<:DenseTensor,TensorT2<:CombinerTensor}
  return contraction_output(T2, T1, indsR)
end

function contract!!(R::Tensor, labelsR, T1::CombinerTensor, labelsT1, T2::Tensor, labelsT2)
  NR = ndims(R)
  N1 = ndims(T1)
  N2 = ndims(T2)
  if N1 ≤ 1
    # Empty combiner, acts as multiplying by 1
    R = permutedims!!(R, T2, getperm(labelsR, labelsT2))
    return R
  elseif N1 + N2 == NR
    error("Cannot perform outer product involving a combiner")
  elseif count_common(labelsT1, labelsT2) == 1 && N1 == 2
    # This is the case of index replacement
    ui = setdiff(labelsT1, labelsT2)[]
    newind = inds(T1)[findfirst(==(ui), labelsT1)]
    cpos1, cpos2 = intersect_positions(labelsT1, labelsT2)
    storeR = copy(storage(T2))
    indsR = setindex(inds(T2), newind, cpos2)
    return tensor(storeR, indsR)
  elseif count_common(labelsT1, labelsT2) == 1 && length(inds(T1)) != 2
    # This is the case of uncombining
    cpos1, cpos2 = intersect_positions(labelsT1, labelsT2)
    storeR = copy(storage(T2))
    indsC = deleteat(inds(T1), cpos1)
    indsR = insertat(inds(T2), indsC, cpos2)
    return tensor(storeR, indsR)
  elseif is_combiner(labelsT1, labelsT2)
    # This is the case of combining
    Alabels, Blabels = labelsT2, labelsT1
    final_labels = contract_labels(Blabels, Alabels)
    final_labels_n = contract_labels(labelsT1, labelsT2)
    indsR = inds(R)
    if final_labels != final_labels_n
      perm = getperm(final_labels_n, final_labels)
      indsR = permute(inds(R), perm)
      labelsR = permute(labelsR, perm)
    end
    cpos1, cposR = intersect_positions(labelsT1, labelsR)
    labels_comb = deleteat(labelsT1, cpos1)
    vlR = [labelsR...]
    for (ii, li) in enumerate(labels_comb)
      insert!(vlR, cposR + ii, li)
    end
    deleteat!(vlR, cposR)
    labels_perm = tuple(vlR...)
    perm = getperm(labels_perm, labelsT2)
    T2p = reshape(R, permute(inds(T2), perm))
    permutedims!(T2p, T2, perm)
    R = reshape(T2p, indsR)
  end
  return R
end

function contract!!(
  R::Tensor, labelsR, T1::Tensor, labelsT1, T2::CombinerTensor, labelsT2
) where {NR,N1,N2}
  return contract!!(R, labelsR, T2, labelsT2, T1, labelsT1)
end

function show(io::IO, mime::MIME"text/plain", S::Combiner)
  println(io, "Permutation of blocks: ", S.perm)
  return println(io, "Combination of blocks: ", S.comb)
end

function show(io::IO, mime::MIME"text/plain", T::CombinerTensor)
  summary(io, T)
  println(io)
  return show(io, mime, storage(T))
end


citation(year, vv) = @match my_varname(vv) begin
    "DSi" || "DIP" || "DIN" => citation_Nutrients(year)
    "Temp" => citation_Temperature(year)
    "Salt" => citation_Salinity(year)
    "O2" || "O2sat" || "AOU" => citation_Oxygen(year)
    "Dens" || "Cond" => citation(year)
    _ => error("Not sure what you are trying to cite.")
end

citation(year) = @match my_product_year(year) begin
    "13" => "Boyer, T. P., J. I. Antonov, O. K. Baranova, C. Coleman, H. E. Garcia, A. Grodsky, D. R. Johnson, R. A. Locarnini, A. V. Mishonov, T. D. O'Brien, C. R. Paver, J. R. Reagan, D. Seidov, I. V. Smolyar, and M. M. Zweng, 2013: World Ocean Database 2013, NOAA Atlas NESDIS 72, S. Levitus, Ed., A. Mishonov, Technical Ed.; Silver Spring, MD, 209 pp., doi:10.7289/V5NZ85MT"
    "18" => "Boyer, T. P., O. K. Baranova, C. Coleman, H. E. Garcia, A. Grodsky, R. A. Locarnini, A. V. Mishonov, C. R. Paver, J. R. Reagan, D. Seidov, I. V. Smolyar, K. Weathers, M. M. Zweng,(2018): World Ocean Database 2018. A. V. Mishonov, Technical Editor, NOAA Atlas NESDIS 87."
    _ => "I could not find a citation for WOD$(my_product_year(year)). Add it if you know where to find it!"
end

citation_Temperature(year) = @match my_product_year(year) begin
    "09" => "Locarnini, R. A., A. V. Mishonov, J. I. Antonov, T. P. Boyer, H. E. Garcia, O. K. Baranova, M. M. Zweng, and D. R. Johnson, 2010. World Ocean Atlas 2009, Volume 1: Temperature. S. Levitus, Ed. NOAA Atlas NESDIS 68, U.S. Government Printing Office, Washington, D.C., 184 pp."
    "13" => "Locarnini, R. A., A. V. Mishonov, J. I. Antonov, T. P. Boyer, H. E. Garcia, O. K. Baranova, M. M. Zweng, C. R. Paver, J. R. Reagan, D. R. Johnson, M. Hamilton, and D. Seidov, 2013. World Ocean Atlas 2013, Volume 1: Temperature. S. Levitus, Ed., A. Mishonov Technical Ed.; NOAA Atlas NESDIS 73, 40 pp."
    "18" => "Locarnini, R. A., A. V. Mishonov, O. K. Baranova, T. P. Boyer, M. M. Zweng, H. E. Garcia, J. R. Reagan, D. Seidov, K. Weathers, C. R. Paver, and I. Smolyar, 2018. World Ocean Atlas 2018, Volume 1: Temperature. A. Mishonov Technical Ed.; in preparation."
    _ => error("No citation for WOA$(my_product_year(year)) Temperature. Add it if it should be there!")
end

citation_Salinity(year) = @match my_product_year(year) begin
    "09" => "Antonov, J. I., D. Seidov, T. P. Boyer, R. A. Locarnini, A. V. Mishonov, H. E. Garcia, O. K. Baranova, M. M. Zweng, and D. R. Johnson, 2010. World Ocean Atlas 2009, Volume 2: Salinity. S. Levitus, Ed. NOAA Atlas NESDIS 69, U.S. Government Printing Office, Washington, D.C., 184 pp."
    "13" => "Zweng, M. M., J. R. Reagan, J. I. Antonov, R. A. Locarnini, A. V. Mishonov, T. P. Boyer, H. E. Garcia, O. K. Baranova, D. R. Johnson, D. Seidov, M. M. Biddle, 2013. World Ocean Atlas 2013, Volume 2: Salinity. S. Levitus, Ed., A. Mishonov Technical Ed.; NOAA Atlas NESDIS 74, 39 pp."
    "18" => "Zweng, M. M., J. R. Reagan, D. Seidov, T. P. Boyer, R. A. Locarnini, H. E. Garcia, A. V. Mishonov, O. K. Baranova, K. Weathers, C. R. Paver, and I. Smolyar, 2018. World Ocean Atlas 2018, Volume 2: Salinity. A. Mishonov Technical Ed.; in preparation."
    _ => error("No citation for WOA$(my_product_year(year)) Salinity. Add it if it should be there!")
end

citation_Oxygen(year) = @match my_product_year(year) begin
    "09" => "Garcia, H. E., R. A. Locarnini, T. P. Boyer, J. I. Antonov, O. K. Baranova, M. M. Zweng, and D. R. Johnson, 2010. World Ocean Atlas 2009, Volume 3: Dissolved Oxygen, Apparent Oxygen Utilization, and Oxygen Saturation. S. Levitus, Ed. NOAA Atlas NESDIS 70, U.S. Government Printing Office, Washington, D.C., 344 pp."
    "13" => "Garcia, H. E., R. A. Locarnini, T. P. Boyer, J. I. Antonov, O. K. Baranova, M. M. Zweng, J. R. Reagan, D. R. Johnson, 2014. World Ocean Atlas 2013, Volume 3: Dissolved Oxygen, Apparent Oxygen Utilization, and Oxygen Saturation. S. Levitus, Ed., A. Mishonov Technical Ed.; NOAA Atlas NESDIS 75, 27 pp."
    "18" => "Garcia, H. E., K. Weathers, C. R. Paver, I. Smolyar, T. P. Boyer, R. A. Locarnini, M. M. Zweng, A. V. Mishonov, O. K. Baranova, D. Seidov, and J. R. Reagan, 2018. World Ocean Atlas 2018, Volume 3: Dissolved Oxygen, Apparent Oxygen Utilization, and Oxygen Saturation. A. Mishonov Technical Ed.; in preparation."
    _ => error("No citation for WOA$(my_product_year(year)) AOU. Add it if it should be there!")
end

citation_Nutrients(year) = @match my_product_year(year) begin
    "09" => "Garcia, H. E., R. A. Locarnini, T. P. Boyer, J. I. Antonov, M. M. Zweng, O. K. Baranova, and D. R. Johnson, 2010. World Ocean Atlas 2009, Volume 4: Nutrients (phosphate, nitrate, silicate). S. Levitus, Ed. NOAA Atlas NESDIS 71, U.S. Government Printing Office, Washington, D.C., 398 pp."
    "13" => "Garcia, H. E., R. A. Locarnini, T. P. Boyer, J. I. Antonov, O. K. Baranova, M. M. Zweng, J. R. Reagan, D. R. Johnson, 2014. World Ocean Atlas 2013, Volume 4: Dissolved Inorganic Nutrients (phosphate, nitrate, silicate). S. Levitus, Ed., A. Mishonov Technical Ed.; NOAA Atlas NESDIS 76, 25 pp."
    "18" => "Garcia, H. E., K. Weathers, C. R. Paver, I. Smolyar, T. P. Boyer, R. A. Locarnini, M. M. Zweng, A. V. Mishonov, O. K. Baranova, D. Seidov, and J. R. Reagan, 2018. World Ocean Atlas 2018, Volume 4: Dissolved Inorganic Nutrients (phosphate, nitrate and nitrate+nitrite, silicate). A. Mishonov Technical Ed.; in preparation."
    _ => error("No citation for WOA$(my_product_year(year)) Nutrients. Add it if it should be there!")
end

# TODO Add other citations for other years!