using FileIO, CSV, ImageAxes, Compose

"""
    readSEManticsImage(fn::AbstractString)

Read a SEMantics PNG image and the create an `ImageAxes` (`AxisArray`) which associates
`:x`, `:y` coordinates with the image axes according to the X, Y and FOV in the
images.txt file in the same directory.
"""
function readSEManticsImage(fn::AbstractString)
    img, sp = load(fn), splitpath(fn)
    img_txt = joinpath(sp[1:end-1]..., "images.txt")
    if isfile(img_txt)
        imgs = CSV.File(img_txt, delim = "\t") |> DataFrame
        (nm, _) = splitext(sp[end])
        rg = Regex(replace(nm[1:end-3], "_" => "[.\\\\:]", "["=>"\\[", "]"=>"\\]"))
        mimgs = filter(r -> !isnothing(match(rg, r[:Name])), imgs)
        if nrow(mimgs) > 0
            # Since an image can be written more than once, take the last...
            x, y, fov = mimgs[end, :X], mimgs[end, :Y], mimgs[end, :FOV]
            pix = fov / size(img, 2) # square pixels determined by horizontal FOV
            img = AxisArray(
                img, #
                Axis{:y}(StepRangeLen(y, pix, size(img,1), size(img,1)÷2)),
                Axis{:x}(StepRangeLen(x, -pix, size(img,2), size(img,2)÷2))
            )

        else
            @info "There is no meta-data in image.txt for images matching $rg."
        end
    else
        @info "There is no meta-data file images.txt for images matching $rg."
    end
    return img
end

function _ns_closest(srl::AbstractRange, val)
    idx = searchsortedfirst(srl, val, rev = step(srl) < 0.0)
    ip = idx + (step(srl) < 0.0 ? -1 : 1)
    return (val < min(srl[1], srl[end])) || (val > max(srl[1], srl[end])) ? nothing : #
           (abs(srl[idx] - val) < abs(srl[ip] - val) ? idx : ip)
end

function _ns_closest(img::AxisArray, coord)
    return (
        _ns_closest(ImageAxes.axisvalues(img)[2], coord[1]),
        _ns_closest(ImageAxes.axisvalues(img)[1], coord[2]),
    )
end

function build_scale_bar(img::AxisArray; marker = HSVA(60, 1, 1, 0.3))
    ax = AxisArrays.axisvalues(img)[1]
    fov = abs(ax[end] - ax[1]) / 2.0
    sc = 10.0^floor(log10(fov))
    l = (fov / sc >= 5.0 ? 5.0 : (fov / sc >= 2.5 ? 2.5 : 1.0)) * sc
    dp = l / abs(ax[end] - ax[1])
    xo, yo = 0.02, 0.02
    return Tuple[
        (context(), Compose.line([(1.0 - 2.0 * xo - dp, yo + 0.07), (1.0 - 2.0 * xo, yo + 0.07)]), stroke(marker), linewidth(1.0)),
        (context(), Compose.text(1.0 - 2.0 * xo - dp / 2.0, yo + 0.045, "$(round(Int,1000.0*l)) μm", hcenter, vbottom), fill(marker), stroke("transparent"), fontsize(8.0)),
        (context(), Compose.rectangle(1.0 - 3.0 * xo - dp, yo, dp + 2.0 * xo, yo + 0.07), stroke("transparent"), fill(RGBA(0.1, 0.1, 0.1, 0.3)))
    ]
end

function build_acquisition_points(
    img::AxisArray,
    coords::AbstractArray{<:AbstractDict{Symbol, <:AbstractFloat}};
    label = true,
    marker = HSVA(60, 1, 1, 0.3)
)
    x, y = Float64[], Float64[]
    for coord in coords
        pt = _ns_closest(img, (coord[:X], coord[:Y]))
        if !any(isnothing.(pt))
            push!(x, pt[1] / size(img, 1))
            push!(y, pt[2] / size(img, 2))
        end
    end
    #  Place circles at acquisition point
    items = Tuple[(context(), Compose.circle(x, y, [0.005]), fill(marker), stroke("transparent"))]
    if label
        fx(x) = x < 0.9 ? x + 0.01 : x - 0.01
        fh(x) = x < 0.9 ? hleft : hright
        push!(items, (context(), Compose.text(fx.(x), y, repr.(eachindex(coords)), fh.(x), [ vcenter]), fill(marker), stroke("transparent")))
    end
    return items
end

function build_thumbnails(
    img::AxisArray,
    thumbnails::Union{Nothing, AbstractArray{<:AxisArray}}=nothing;
    label = true,
    marker = HSVA(60, 1, 1, 0.3)
)
    ya, xa = axisvalues(img)[1:2]
    rx, ry, rw, rh = Float64[], Float64[], Float64[], Float64[]
    for thumb in thumbnails
        yt, xt = axisvalues(thumb)[1:2]
        y1o = (yt[1]-ya[1])/(ya[end]-ya[1])
        yeo = (yt[end]-ya[1])/(ya[end]-ya[1])
        x1o = (xt[1]-xa[1])/(xa[end]-xa[1])
        xeo = (xt[end]-xa[1])/(xa[end]-xa[1])
        if (max(x1o,xeo)>0.0) && (min(x1o,xeo)<1.0) &&
            (max(y1o,yeo)>0.0) && (min(y1o,yeo)<1.0)
            push!(rx, x1o)
            push!(ry, y1o)
            push!(rw, xeo-x1o)
            push!(rh, yeo-y1o)
        end
    end
    #  Place rectangles at image points
    items = Tuple[ (context(), Compose.rectangle(rx, ry, rw, rh), fill(RGBA(0.1, 0.1, 0.1, 0.3)), stroke(marker)) ]
    if label
        push!(items, (context(), Compose.text(rx .+ 0.001, ry .+ 0.005, repr.(eachindex(thumbnails)), [ hleft ], [ vtop ]), fill(marker), stroke("transparent")))
    end
    return items
end

"""
    annotate(
        img::AxisArray;
        scale::Bool = true,
        coords::Union{Nothing, AbstractArray{<:AbstractDict{Symbol, <:AbstractFloat}}} = nothing,
        spectra::Union{Nothing, <:AbstractArray{<:Spectrum}} = nothing,
        thumbnails::Union{Nothing, AbstractArray{<:AxisArray}}=nothing,
        labelcoords::Bool = true,
        labelthumbnails::Bool = true,
        marker::Colorant = HSVA(60, 1, 1, 0.3)
    )

Add annotations like scale-bars, acquisition points, image areas to an image
and display the result.

  * `coords` and `spectra` display as circles (with/without numeric labels).
  * `thumbnails` display as rectangles overlaying the image
  * `scale` displays as a scale-bar in the upper-right corner
  * `marker` is the color of the annotations which is by-default slightly transparent
"""
function annotate(
    img::AxisArray;
    scale::Bool = true,
    coords::Union{Nothing, AbstractArray{<:AbstractDict{Symbol, <:AbstractFloat}}} = nothing,
    spectra::Union{Nothing, <:AbstractArray{<:Spectrum}} = nothing,
    thumbnails::Union{Nothing, AbstractArray{<:AxisArray}}=nothing,
    labelcoords::Bool = true,
    labelthumbnails::Bool = true,
    marker::Colorant = HSVA(60, 1, 1, 0.3)
)
    items = Tuple[]
    if scale
        append!(items, build_scale_bar(img, marker=marker))
    end
    allcoords = AbstractDict{Symbol, <:AbstractFloat}[]
    if !isnothing(coords)
        append!(allcoords, coords)
    end
    if !isnothing(spectra)
        foreach(sp->push!(allcoords, sp[:StagePosition]), spectra)
    end
    if length(allcoords)>0
        append!(items, build_acquisition_points(img, allcoords; label=labelcoords, marker=marker))
    end
    if !isnothing(thumbnails)
        append!(items, build_thumbnails(img, thumbnails; label=labelthumbnails, marker=marker))
    end
    io = IOBuffer()
    ImageIO.save(Stream{format"PNG"}(io), img)
    return compose(
        context(0.0, 0.0, 1.0, 1.0),
        items...,
        ( context(), Compose.bitmap("image/png", take!(io), 0.0, 0.0, 1.0, 1.0) ),
    )
end

"""
    shannon_entropy(img::AbstractArray{Gray{N0f8}})

Computes the log2-entropy of the data in the image.
The entropy(...) in Images.jl 24.1 is buggy and is removed in 25.0
"""
function shannon_entropy(img::AbstractArray{Gray{N0f8}})
    # c = @MVector zeros(Int(typemax(UInt8))+1) # Reduces allocations to zero..
    c = zeros(Int(typemax(UInt8))+1)
    for v in img
        @inbounds c[reinterpret(UInt8, v.val)+1] += 1.0/length(img)
    end
    l2(v) = v==0 ? 0.0 : log2(v)
    return -sum(v->v*l2(v), c)
end


# Dense Bitmapped Tries
# =====================

abstract DenseBitmappedTrie{T} <: BitmappedTrie{T}

# Why is the shift value of a DenseLeaf 5 instead of 0, and why does
# the shift value of a DenseNode start at 10?
#
# The PersistentVector implements a "tail" optimization, where it
# inserts appended elements into a tail array until that array is 32
# elements long, only then inserting it into the actual bitmapped
# vector trie. This significantly increases the performance of
# operations that touch the very end of the vector (last, append, pop,
# etc.) because you don't have to traverse the trie.
#
# However, it adds a small amount of complexity to the implementation;
# when you query the trie, you now get back a length-32 array instead
# of the actual element. This is why the DenseLeaf has a shift value
# of 5: it leaves an "extra" 5 bits for the PersistentVector to use to
# index into the array returned from the trie. (This also means that a
# DenseNode has to start at shiftby*2.)

immutable DenseNode{T} <: DenseBitmappedTrie{T}
    arr::Vector{DenseBitmappedTrie{T}}
    shift::Int
    length::Int
    maxlength::Int
end

immutable DenseLeaf{T} <: DenseBitmappedTrie{T}
    arr::Vector{T}

    DenseLeaf(arr::Vector) = new(arr)
    DenseLeaf() = new(T[])
end

arrayof(    node::DenseNode) = node.arr
shift(      node::DenseNode) = node.shift
maxlength(  node::DenseNode) = node.maxlength
Base.length(node::DenseNode) = node.length

arrayof(    leaf::DenseLeaf) = leaf.arr
shift(          ::DenseLeaf) = shiftby
maxlength(  leaf::DenseLeaf) = trielen
Base.length(leaf::DenseLeaf) = length(arrayof(leaf))

function promoted{T}(node::DenseBitmappedTrie{T})
    DenseNode{T}(DenseBitmappedTrie{T}[node],
                 shift(node) + shiftby,
                 length(node),
                 maxlength(node) * trielen)
end

function demoted{T}(node::DenseNode{T})
    if shift(node) == shiftby * 2
        DenseLeaf{T}(T[])
    else
        DenseNode{T}(DenseBitmappedTrie{T}[],
                     shift(node) - shiftby,
                     0,
                     round(Int, maxlength(node) / trielen))
    end
end

function witharr{T}(node::DenseNode{T}, arr::Array, lenshift::Int=0)
    DenseNode{T}(arr, shift(node), length(node) + lenshift, maxlength(node))
end
witharr{T}(leaf::DenseLeaf{T}, arr::Array) = DenseLeaf{T}(arr)

function append(leaf::DenseLeaf, el)
    if length(leaf) < maxlength(leaf)
        newarr = copy_to_len(arrayof(leaf), 1 + length(leaf))
        newarr[end] = el
        witharr(leaf, newarr)
    else
        append(promoted(leaf), el)
    end
end
function append{T}(node::DenseNode{T}, el)
    if length(node) == 0
        child = append(demoted(node), el)
        witharr(node, DenseBitmappedTrie{T}[child], 1)
    elseif length(node) < maxlength(node)
        if length(arrayof(node)[end]) == maxlength(arrayof(node)[end])
            newarr = copy_to_len(arrayof(node), 1 + length(arrayof(node)))
            newarr[end] = append(demoted(node), el)
            witharr(node, newarr, 1)
        else
            newarr = arrayof(node)[:]
            newarr[end] = append(newarr[end], el)
            witharr(node, newarr, 1)
        end
    else
        append(promoted(node), el)
    end
end
push(leaf::DenseLeaf, el) = append(leaf, el)
push(node::DenseNode, el) = append(node, el)

Base.getindex(leaf::DenseLeaf, i::Int) = arrayof(leaf)[mask(leaf, i)]
Base.getindex(node::DenseNode, i::Int) = arrayof(node)[mask(node, i)][i]

function assoc{T}(leaf::DenseLeaf{T}, i::Int, el)
    newarr = arrayof(leaf)[:]
    newarr[mask(leaf, i)] = el
    DenseLeaf{T}(newarr)
end
function assoc(node::DenseNode, i::Int, el)
    newarr = arrayof(node)[:]
    idx = mask(node, i)
    newarr[idx] = assoc(newarr[idx], i, el)
    witharr(node, newarr)
end

peek(bt::DenseBitmappedTrie) = bt[end]

# Pop is usually destructive, but that doesn't make sense for an immutable
# structure, so `pop` is defined to return a Trie without its last
# element. Use `peek` to access the last element.
#
pop(leaf::DenseLeaf) = witharr(leaf, arrayof(leaf)[1:end-1])
function pop(node::DenseNode)
    newarr = arrayof(node)[:]
    newarr[end] = pop(newarr[end])
    witharr(node, newarr, -1)
end

# Persistent Vectors
# ==================

immutable PersistentVector{T} <: AbstractArray{T,1}
    trie::DenseBitmappedTrie{Vector{T}}
    tail::Vector{T}
    length::Int

    PersistentVector(trie::DenseBitmappedTrie, tail, length::Int) =
        new(trie, tail, length)
    PersistentVector() = new(DenseLeaf{Vector{T}}(), T[], 0)
    function PersistentVector(arr)
        if length(arr) <= trielen
            new(DenseLeaf{Vector{T}}(), arr, length(arr))
        else
            append(new(DenseLeaf{Vector{T}}(), T[], 0), arr)
        end
    end
end
PersistentVector() = PersistentVector{Any}()
PersistentVector(itr) = PersistentVector{eltype(itr)}(itr)

mask(i::Int) = ((i - 1) & (trielen - 1)) + 1

function boundscheck!(v::PersistentVector, i::Int)
    0 < i <= v.length || error(BoundsError(), " :: Index $i out of bounds ($(v.length))")
end

Base.size(   v::PersistentVector) = v.length
Base.length( v::PersistentVector) = v.length
Base.isempty(v::PersistentVector) = length(v) == 0
Base.endof(  v::PersistentVector) = length(v)

Base.isequal(v1::PersistentVector, v2::PersistentVector) =
    isequal(v1.tail, v2.tail) && isequal(v1.trie, v2.trie)
==(v1::PersistentVector, v2::PersistentVector) =
    v1.tail == v2.tail && v1.trie == v2.trie

function Base.getindex(v::PersistentVector, i::Int)
    boundscheck!(v, i)
    if i > v.length - length(v.tail)
        v.tail[mask(i)]
    else
        v.trie[i][mask(i)]
    end
end

peek(v::PersistentVector) = v[end]

function push{T}(v::PersistentVector{T}, el)
    if length(v.tail) < trielen
        newtail = copy_to_len(v.tail, 1 + length(v.tail))
        newtail[end] = el
        PersistentVector{T}(v.trie, newtail, 1 + v.length)
    else
        # T[el] will give an error when T is an tuple type in v0.3
        # workaround:
        arr = Array(T, 1)
        arr[1] = convert(T, el)
        PersistentVector{T}(append(v.trie, v.tail), arr, 1 + v.length)
    end
end
append{T}(v::PersistentVector{T}, itr) = foldl(push, v, itr)

function assoc{T}(v::PersistentVector{T}, i::Int, el)
    boundscheck!(v, i)
    if i > v.length - length(v.tail)
        newtail = v.tail[1:end]
        newtail[mask(i)] = el
        PersistentVector{T}(v.trie, newtail, v.length)
    else
        newnode = v.trie[i][1:end]
        newnode[mask(i)] = el
        PersistentVector{T}(assoc(v.trie, i, newnode), v.tail, v.length)
    end
end

function pop{T}(v::PersistentVector{T})
    if isempty(v.tail)
        newtail = peek(v.trie)[1:end-1]
        PersistentVector{T}(pop(v.trie), newtail, v.length - 1)
    else
        newtail = v.tail[1:end-1]
        PersistentVector{T}(v.trie, newtail, v.length - 1)
    end
end

immutable ItrState{T}
    index::Int
    leaf::Vector{T}
end

Base.start{T}(v::PersistentVector{T}) = ItrState(1, v.length <= 32 ? v.tail : v.trie[1])
Base.done{T}(v::PersistentVector{T}, state::ItrState{T}) = state.index > v.length

function Base.next{T}(v::PersistentVector{T}, state::ItrState{T})
    i, leaf = state.index, state.leaf
    m = mask(i)
    value = leaf[m]
    i += 1
    if m == 32
        leaf = i > v.length - length(v.tail) ? v.tail : v.trie[i]
    end
    return value, ItrState(i, leaf)
end

function Base.map{T}(f::Function, pv::PersistentVector{T})
    if length(pv) == 0 return PersistentVector{T}() end

    first = f(pv[1])
    v = PersistentVector{typeof(first)}([first])
    for i = 2:length(pv)
        v = append(v, f(pv[i]))
    end
    v
end

function Base.filter{T}(f::Function, pv::PersistentVector{T})
    v = PersistentVector{T}()
    for el in pv
        if f(el)
            v = append(v, el)
        end
    end
    v
end

function Base.hash{T}(pv::PersistentVector{T})
    h = hash(length(pv))
    for el in pv
        h = Base.hash(el, h)
    end
    @compat UInt(h)
end

function print_elements(io, pv, range)
    for i=range
        print(io, "$(pv[i]), ")
    end
end

function print_vec(io::IO, t, head::AbstractString)
    print(io, "$head[")
    if length(t) < 50
        print_elements(io, t, 1:length(t)-1)
    else
        print_elements(io, t, 1:25)
        print(io, "..., ")
        print_elements(io, t, length(t)-25:length(t)-1)
    end
    if length(t) >= 1
        print(io, "$(t[end])]")
    else
        print(io, "]")
    end
end

@compat Base.show{T}(io::IO, ::MIME"text/plain", pv::PersistentVector{T}) =
    print_vec(io, pv, "Persistent{$T}")


function gcam_go_techspecific(x, y)
    x * y
end


using Random

function count_elt(map_el::MapElites)
    return length(collect(skipmissing(map_el.solutions)))
end

function add_to_map(map_el::MapElites,coordinates,solution::Individual,performance::Array{Float64})
    if (!haskey(map_el.solutions.data,Tuple(coordinates)))
        map_el.solutions.data[Tuple(coordinates)] = solution
        map_el.performances.data[Tuple(coordinates)] = performance
    elseif (map_el.performances.data[Tuple(coordinates)] < performance)
        map_el.solutions.data[Tuple(coordinates)] = solution
        map_el.performances.data[Tuple(coordinates)] = performance
    end
end

function select_random(map_el::MapElites)
    available_ind = collect(skipmissing(map_el.solutions))
    nb_ind = length(available_ind)
    rand_idx = Random.rand(1:nb_ind)
    return available_ind[rand_idx]
end

function select_most_promising(map_el::MapElites)
    available_ind = collect(skipmissing(map_el.solutions))
    available_perf = collect(skipmissing(map_el.performances))
    idx_best = argmax(available_perf)
    return available_ind[idx_best]
end


module MultiAssemblers

using Gridap
using Gridap.Helpers
using Gridap.MultiFESpaces: _compute_offsets

using SparseArrays
using SparseMatricesCSR

export MultiAssembler
export MultiSparseMatrixAssembler

import Gridap.Assemblers: assemble
import Gridap.Assemblers: assemble!
import Gridap.Assemblers: SparseMatrixAssembler
import Gridap.Assemblers: SparseMatrixAssembler
using  Gridap.Assemblers: sparse_from_coo

abstract type MultiAssembler{M<:AbstractMatrix,V<:AbstractVector} end

function assemble(
  ::MultiAssembler{M,V},
  ::Vararg{Tuple{<:MultiCellVector,<:CellNumber}})::V where {M,V}
  @abstractmethod
end

function assemble(
  ::MultiAssembler{M,V},
  ::Vararg{Tuple{<:MultiCellMatrix,<:CellNumber,<:CellNumber}})::M where {M,V}
  @abstractmethod
end

function assemble!(
  ::V,
  ::MultiAssembler{M,V},
  ::Vararg{Tuple{<:MultiCellVector,<:CellNumber}}) where {M,V}
  @abstractmethod
end

function assemble!(
  ::M,
  ::MultiAssembler{M,V},
  ::Vararg{Tuple{<:MultiCellMatrix,<:CellNumber,<:CellNumber}}) where {M,V}
  @abstractmethod
end

function assemble(
  a::MultiAssembler,cv::MultiCellVector)
  l = length(cv)
  ide = IdentityCellNumber(Int,l)
  assemble(a,(cv,ide))
end

function assemble(
  a::MultiAssembler,cv::MultiCellMatrix)
  l = length(cv)
  ide = IdentityCellNumber(Int,l)
  assemble(a,(cv,ide,ide))
end

function assemble!(r,a::MultiAssembler,cv::MultiCellVector)
  l = length(cv)
  ide = IdentityCellNumber(Int,l)
  assemble!(r,a,(cv,ide))
end

function assemble!(r,a::MultiAssembler,cv::MultiCellMatrix)
  l = length(cv)
  ide = IdentityCellNumber(Int,l)
  assemble!(r,a,(cv,ide,ide))
end

"""
MultiAssembler that produces SparseMatrices from the SparseArrays package
"""
struct MultiSparseMatrixAssembler{E,M} <: MultiAssembler{M,Vector{E}}
  testfesps::MultiFESpace{E}
  trialfesps::MultiFESpace{E}

  function MultiSparseMatrixAssembler(
    ::Type{M},
    testfesp::MultiFESpace{E},
    trialfesp::MultiFESpace{E}) where {M<:AbstractSparseMatrix,E}
    new{E,M}(testfesp,trialfesp)
  end
end

function MultiSparseMatrixAssembler(
  testfesp::MultiFESpace{E},
  trialfesp::MultiFESpace{E}) where {E}
  MultiSparseMatrixAssembler(SparseMatrixCSC, testfesps, trialfesp)
end

function MultiSparseMatrixAssembler(
  ::Type{M},
  testfesps::Vector{<:FESpaceWithDirichletData},
  trialfesps::Vector{<:FESpaceWithDirichletData}) where {M<:AbstractSparseMatrix}
  V = MultiFESpace(testfesps)
  U = MultiFESpace(trialfesps)
  MultiSparseMatrixAssembler(M,V,U)
end

function MultiSparseMatrixAssembler(
  testfesps::Vector{<:FESpaceWithDirichletData},
  trialfesps::Vector{<:FESpaceWithDirichletData})
  MultiSparseMatrixAssembler(SparseMatrixCSC,V,U)
end

function SparseMatrixAssembler(
  ::Type{M},
  testfesps::Vector{<:FESpaceWithDirichletData},
  trialfesps::Vector{<:FESpaceWithDirichletData}) where {M<:AbstractSparseMatrix}
  MultiSparseMatrixAssembler(M,testfesps,trialfesps)
end

function SparseMatrixAssembler(
  testfesps::Vector{<:FESpaceWithDirichletData},
  trialfesps::Vector{<:FESpaceWithDirichletData})
  MultiSparseMatrixAssembler(SparseMatrixCSC,testfesps,trialfesps)
end

function MultiSparseMatrixAssembler(
  ::Type{M},
  testfesps::MultiFESpace{E,S},
  trialfesps::MultiFESpace{E,S}) where {M<:AbstractSparseMatrix,E,S}
  @notimplementedif S != ConsequtiveMultiFieldStyle
  MultiSparseMatrixAssembler{E,M}(M,testfesps,trialfesps)
end

function MultiSparseMatrixAssembler(
  testfesps::MultiFESpace{E,S},
  trialfesps::MultiFESpace{E,S}) where {E,S}
  @notimplementedif S != ConsequtiveMultiFieldStyle
  MultiSparseMatrixAssembler(SparseMatrixCSC,testfesps,trialfesps)
end

function assemble(
  this::MultiSparseMatrixAssembler{E},
  allvals::Vararg{Tuple{<:MultiCellVector,<:CellNumber}}) where {E}

  n = num_free_dofs(this.testfesps)
  vec = zeros(E,n)
  assemble!(vec,this,allvals...)
  vec
end

function assemble!(
  vec::Vector{E},
  this::MultiSparseMatrixAssembler{E},
  allmcv::Vararg{Tuple{<:MultiCellVector,<:CellNumber}}) where {E}

  vec .= zero(E)
  V = this.testfesps
  offsets = _compute_offsets(V)
  _mf_rows = celldofids(V)
  for (mcv,cellids) in allmcv
    mf_vals = apply_constraints(V,mcv,cellids)
    i_to_fieldid = mf_vals.fieldids
    mf_rows = reindex(_mf_rows,cellids)
    _assemble_vec!(vec,mf_rows,mf_vals,i_to_fieldid,offsets)
  end
end

function assemble(
  this::MultiSparseMatrixAssembler{E,M},
  allmcm::Vararg{Tuple{<:MultiCellMatrix,<:CellNumber,<:CellNumber}}) where {E,M}

  V = this.testfesps
  U = this.trialfesps
  offsets_row = _compute_offsets(V)
  offsets_col = _compute_offsets(U)
  _mf_rows = celldofids(V)
  _mf_cols = celldofids(U)
  aux_row = Int[]; aux_col = Int[]; aux_val = E[]

  for (mcm,cellids_row,cellids_col) in allmcm
    mf_vals = apply_constraints_rows(V, mcm, cellids_row)
    mf_rows = reindex(_mf_rows, cellids_row)
    mf_vals = apply_constraints_cols(U,mf_vals,cellids_col)
    mf_cols = reindex(_mf_cols, cellids_col)
    i_to_fieldid = mf_vals.fieldids
    _assemble_mat!(M,
      aux_row,aux_col,aux_val,mf_rows,mf_cols,mf_vals,
      i_to_fieldid,offsets_row,offsets_col)
  end
  num_rows = num_free_dofs(this.testfesps)
  num_cols = num_free_dofs(this.trialfesps)
  finalize_coo!(M,aux_row,aux_col,aux_val,num_rows,num_cols)
  sparse_from_coo(M,aux_row, aux_col, aux_val)
end

function assemble!(
  mat::AbstractSparseMatrix{E},
  this::MultiSparseMatrixAssembler{E},
  allvals::Vararg{Tuple{<:MultiCellMatrix,<:CellNumber,<:CellNumber}}) where E
  # This routine can be optimized a lot taking into a count the sparsity graph of mat
  # For the moment we create an intermediate matrix and then transfer the nz values
  m = assemble(this,allvals...)
  nonzeros(mat) .= nonzeros(m)
end

function _assemble_vec!(vec,mf_rows,mf_vals,i_to_fieldid,offsets)
  for (mf_rows_c,mf_vals_c) in zip(mf_rows,mf_vals)
    for (i,vals_c) in enumerate(mf_vals_c)
      ifield, = i_to_fieldid[i]
      rows_c = mf_rows_c[ifield]
      offset = offsets[ifield]
      for (lid,gid) in enumerate(rows_c)
        if gid > 0
          vec[gid+offset] += vals_c[lid]
        end
      end
    end
  end
end

function _assemble_mat!(::Type{M},
  aux_row,aux_col,aux_val,mf_rows,mf_cols,mf_vals,
  i_to_fieldid,offsets_row,offsets_col) where {M<:AbstractSparseMatrix}

  for (mf_rows_c,mf_cols_c,mf_vals_c) in zip(mf_rows,mf_cols,mf_vals)
    for (i,vals_c) in enumerate(mf_vals_c)
      ifield,jfield = i_to_fieldid[i]
      rows_c = mf_rows_c[ifield]
      cols_c = mf_cols_c[jfield]
      offset_row = offsets_row[ifield]
      offset_col = offsets_col[jfield]
      for (lidcol,gidcol) in enumerate(cols_c)
        if gidcol > 0
          for (lidrow,gidrow) in enumerate(rows_c)
            if gidrow > 0
              push_coo!(M,aux_row, aux_col, aux_val,
                    gidrow+offset_row, gidcol+offset_col, vals_c[lidrow,lidcol])
            end
          end
        end
      end
    end
  end

end

end # module MultiAssemblers


export Equation

mutable struct Equation
    lhs::Expression
    rhs::Expression
end
Base.broadcastable(eq::Equation) = Ref(eq)

Base.:~(lhs::Expression, rhs::Expression) = Equation(lhs, rhs)
Base.:~(lhs::Expression, rhs::Number    ) = Equation(lhs, rhs)
Base.:~(lhs::Number    , rhs::Expression) = Equation(lhs, rhs)

_is_derivative(x::Variable) = x.diff !== nothing
_is_dependent(x::Variable) = x.subtype === :Unknown && !isempty(x.dependents)
_is_parameter(ivs) = x -> x.subtype === :Parameter && x ∉ ivs
_subtype(subtype::Symbol) = x -> x.subtype === subtype

function extract_elements(eqs, predicates)
    result = [Variable[] for p ∈ predicates]
    vars = foldl(vars!, eqs; init=Set{Variable}())

    for var ∈ vars
        for (i, p) ∈ enumerate(predicates)
            p(var) && (push!(result[i], var); break)
        end
    end

    return result
end

function vars!(vars, op)
    args = isa(op, Equation) ? Expression[op.lhs, op.rhs] : op.args

    for arg ∈ args
        if isa(arg, Operation)
            vars!(vars, arg)
        elseif isa(arg, Variable)
            push!(vars, arg)
            for dep ∈ arg.dependents
                push!(vars, dep)
            end
        end
    end

    return vars
end


# nufft/z-list.jl

include("dtft.jl")
include("nufft.jl")


# Sequence

for _Seq in [:HSeq, :VSeq]
  @eval begin
    # constructor
    @test length($_Seq()) == 0
    @test length($_Seq(Event())) == 1
    @test length($_Seq(HSeq())) == 1
    @test length($_Seq(VSeq())) == 1
    @test length($_Seq(Event(), HSeq(), VSeq())) == 3
    @test length($_Seq([Event(), HSeq(), VSeq()])) == 3

    # conversion
    @test convert($_Seq, Event(:foo => 1)) == $_Seq(Event(:foo => 1))
    @test convert($_Seq, [Event(:foo => 1)]) == $_Seq(Event(:foo => 1))

    # show
    @test length("$($_Seq())") > 0
    @test length("$($_Seq(Event()))") > 0

    # merge
    @test merge($_Seq(), $_Seq()) == $_Seq()
    @test merge($_Seq(Event(:foo => 1)), $_Seq(Event(:bar => 1))) ==
      $_Seq(Event(:foo => 1, :bar => 1))
    @test merge(Event(), $_Seq()) == $_Seq()
    @test merge(Event(), $_Seq(Event())) == $_Seq(Event())
    @test merge($_Seq(), Event()) == $_Seq()
    @test merge($_Seq(Event()), Event()) == $_Seq(Event())
    @test merge($_Seq(Event()), [Dict(:foo => 1)]) == $_Seq(Event(:foo => 1))
    @test merge($_Seq(Event()), Dict(:foo => 1)) == $_Seq(Event(:foo => 1))
    @test merge($_Seq(Event()), :foo => 1) == $_Seq(Event(:foo => 1))

    # repeat
    @test repeat($_Seq(Event(:foo => 1), Event(:bar => 1)), inner=2) ==
      $_Seq(Event(:foo => 1), Event(:foo => 1), Event(:bar => 1), Event(:bar => 1))
    @test repeat($_Seq(Event(:foo => 1), Event(:bar => 1)), outer=2) ==
      $_Seq(Event(:foo => 1), Event(:bar => 1), Event(:foo => 1), Event(:bar => 1))

    # collection
    @test collect($_Seq, []) == $_Seq()
    @test collect($_Seq, [Event(), VSeq(), HSeq()]) ==
      $_Seq(Event(), VSeq(), HSeq())

    @test circshift($_Seq(Event(:i => 1), Event(:i => 2), Event(:i => 3)), 1) ==
      $_Seq(Event(:i => 3), Event(:i => 1), Event(:i => 2))
    @test circshift($_Seq(Event(:i => 1), Event(:i => 2), Event(:i => 3)), -1) ==
      $_Seq(Event(:i => 2), Event(:i => 3), Event(:i => 1))

    @test reverse($_Seq(Event(:i => 1), Event(:i => 2), Event(:i => 3))) ==
      $_Seq(Event(:i => 3), Event(:i => 2), Event(:i => 1))

    @test rot180($_Seq(Event(:i => 1), Event(:i => 2), Event(:i => 3))) ==
      $_Seq(Event(:i => 3), Event(:i => 2), Event(:i => 1))
  end
end

# specific conversion
@test convert(HSeq, VSeq(Event())) == HSeq(Event())
@test convert(VSeq, HSeq(Event())) == VSeq(Event())

# matrix
@test HSeq([Event(:i => 0) Event(:i => 1); Event(:i => 2) Event(:i => 3)]) ==
  HSeq(VSeq(Event(:i => 0),Event(:i => 1)), VSeq(Event(:i => 2),Event(:i => 3)))
@test VSeq([Event(:i => 0) Event(:i => 1); Event(:i => 2) Event(:i => 3)]) ==
  VSeq(HSeq(Event(:i => 0),Event(:i => 1)), HSeq(Event(:i => 2),Event(:i => 3)))

m =
  VSeq([Event(:i => 0) Event(:i => 1) Event(:i => 2);
        Event(:i => 3) Event(:i => 4) Event(:i => 5)])

r1 = rotr90(m)
r2 = rotr90(r1)
r3 = rotr90(r2)
r4 = rotr90(r3)
l1 = rotl90(m)
l2 = rotl90(l1)
l3 = rotl90(l2)
l4 = rotl90(l3)

@test r1 == l3 ==
  HSeq([Event(:i => 3) Event(:i => 4) Event(:i => 5);
        Event(:i => 0) Event(:i => 1) Event(:i => 2)])

@test r2 == l2 ==
  VSeq([Event(:i => 5) Event(:i => 4) Event(:i => 3);
        Event(:i => 2) Event(:i => 1) Event(:i => 0)])

@test r3 == l1 ==
  HSeq([Event(:i => 2) Event(:i => 1) Event(:i => 0);
        Event(:i => 5) Event(:i => 4) Event(:i => 3)])

@test r4 == l4 == m


struct SMaxHandler{N} end

gethandler(::Bool,
           ::Type{typeof(max)},
           x::Type{<:SBitstreamLike},
           y::Type{<:SBitstreamLike},
           zs::Type{<:SBitstreamLike}...) =
    SMaxHandler{length(zs) + 2}()
init_state(::SMaxHandler) = (id = 0,)

function (handler::SMaxHandler{N})(buffer, netlist, state, inputs, outputs) where N
    @assert length(inputs) == N "Cannot apply SMaxHandler{$N} to length(inputs) == $(length(inputs))"

    # update netlist with inputs
    foreach(inputs) do input
        setsigned!(netlist, input, true)
    end

    # compute broadcast naming
    names = handle_broadcast_name(name.(inputs), netsize.(inputs), netsize(outputs[1]))

    # update netlist with output
    setsigned!(netlist, outputs[1], true)

    if handler.broadcasted
        num_elements = join(netsize(outputs[1]), "*")
        push!(netlist, Net(name = "max_$(N)_$(state.id)_inputs", size = (N, netsize(outputs[1])...), signed = true))
        write(buffer, """
            $stdcomment
            // BEGIN max_$(N)_$(state.id)
            """)
        write_bcast_instantiation(buffer, "max_$(N)_$(state.id)", netsize(outputs[1]), """
            assign max_$(N)_$(state.id)_inputs_p[((max_$(N)_$(state.id)_i + 1)*$N - 1) -: $N] = {$(join(map(fname -> fname("_p"), names), "[max_$(N)_$(state.id)_i], "))[max_$(N)_$(state.id)_i]};
            assign max_$(N)_$(state.id)_inputs_m[((max_$(N)_$(state.id)_i + 1)*$N - 1) -: $N] = {$(join(map(fname -> fname("_m"), names), "[max_$(N)_$(state.id)_i], "))[max_$(N)_$(state.id)_i]};

            stoch_signed_nmax #(
                    .NUM_INPUTS($N)
                ) max_$(N)_$(state.id) (
                    .CLK(CLK),
                    .nRST(nRST),
                    .as_p(max_$(N)_$(state.id)_inputs_p[((max_$(N)_$(state.id)_i + 1)*$N - 1) -: $N]),
                    .as_m(max_$(N)_$(state.id)_inputs_m[((max_$(N)_$(state.id)_i + 1)*$N - 1) -: $N]),
                    .y_p($(name(outputs[1]))_p[max_$(N)_$(state.id)_i]),
                    .y_m($(name(outputs[1]))_m[max_$(N)_$(state.id)_i])
                );""")
        write(buffer, """
            // END max_$(N)_$(state.id)
            \n""")
    else
        push!(netlist, Net(name = "max_$(N)_$(state.id)_inputs", size = (N, 1), signed = true))
        write(buffer, """
            $stdcomment
            // BEGIN max_$(N)_$(state.id)
            """)
        write(buffer, "assign max_$(N)_$(state.id)_inputs_p = {")
        write(buffer, join(map(fname -> fname("_p"), names), ", "))
        write(buffer, "};\n")
        write(buffer, "assign max_$(N)_$(state.id)_inputs_m = {")
        write(buffer, join(map(fname -> fname("_m"), names), ", "))
        write(buffer, "};\n")
        write(buffer, """
            stoch_signed_nmax #(
                    .NUM_INPUTS($N)
                ) max_$(N)_$(state.id) (
                    .CLK(CLK),
                    .nRST(nRST),
                    .as_p(max_$(N)_$(state.id)_inputs_p),
                    .as_m(max_$(N)_$(state.id)_inputs_m),
                    .y_p($(name(outputs[1]))_p),
                    .y_m($(name(outputs[1]))_m)
                );
            // END max_$(N)_$(state.id)
            \n""")
    end

    return buffer, (id = state.id + 1,)
end


# Run the benchmarks for Julia (run.jl) and Python (run.py). Copy the results in the vectors below and plot

using VegaLite
using DataFrames

# Agents.jl benchmark results
jlresults = [
0.124292399,
0.261890796,
0.382272875,
0.526156528,
0.643057459,
0.795138978,
1.012378349,
1.180964479,
1.248486157,
1.369958317]

# Mesa benchmark results
pyresults = [
0.9389688929659314,
1.9137998179649003,
2.9129630780080333,
4.070444974990096,
5.039975131978281,
6.156719513994176,
7.4113737950101495,
8.584513186011463,
10.217067367048003,
11.377028140006587
]

size_range = (100:100:1000) .* 100

dd = DataFrame(runtime=vcat(jlresults, pyresults), lang=vcat(["Agents.jl" for i in 1:10], ["Mesa" for i in 1:10]), gridsize=vcat(size_range, size_range));

@vlplot(data=dd,
  mark={:line,
    size=4,
    point={filled=false, fill="white", size=60}
  },
  x={:gridsize,
    type="ordinal",
    title="Grid size",
    axis={titleFontSize=16, labelFontSize=14},
    grid=true,
  },
  y={:runtime,
    type="quantitative",
    title="Run time (seconds)",
    axis={titleFontSize=13,
    labelFontSize=14},
    grid=true
  },
  color={:lang,
    type="nominal",
    title="Framework",
    legend={titleFontSize=15, labelFontSize=14},
    scale={range=["#EA98D2", "#659CCA"]},
  },
  height=400,
  width=500,
)

dd2 = DataFrame(ratio=pyresults./jlresults, gridsize=size_range);

@vlplot(data=dd2,
  mark={:bar},
  x={:gridsize,
    type="ordinal",
    title="Grid size",
    axis={titleFontSize=16, labelFontSize=14},
  },
  y={:ratio,
    type="quantitative",
    title="Mesa/Agents run time",
    axis={titleFontSize=13,
    labelFontSize=14}
  },
  height=400,
  width=500,
)


for (op, fun) in ((:maximum, :max), (:minimum, :min), (:prod, :prod), (:all, :all))
    isdefined(Base, op) || continue
    _op = Symbol("_", op)
    __op = Symbol("__", op)

    @eval begin
        Base.$op(x::AbstractJaxArray; dims=:, keepdims=_autokeep(dims)) = $_op(nothing, x, keepdims, dims)
        Base.$op(T::Type, x::AbstractJaxArray; dims=:, keepdims=_autokeep(dims)) = $_op(T, x, keepdims, dims)

        $_op(T, x, keepdims, dims...) = $__op(T, x, keepdims, _convert_dims(size(x), dims...))
        $__op(T, x, keepdims, dims) = numpy.$fun(x, axis=dims, dtype=T, keepdims=keepdims)
    end
end


    columnspair(a::AbstractVector{S}, b::AbstractVector{T}) where {S, T} = Columns{Pair{S, T}}((a, b))
    c = Columns(([1,1,1,2,2], [1,2,4,3,5]))
    d = Columns(([1,1,2,2,2], [1,3,1,4,5]))
    e = Columns(([1,1,1], sort([rand(),0.5,rand()])))
    f = Columns(([1,1,1], sort([rand(),0.5,rand()])))
    @test map(+,NDSparse(c,ones(5)),NDSparse(d,ones(5))).index == Columns(([1,2],[1,5]))
    @test length(map(+,NDSparse(e,ones(3)),NDSparse(f,ones(3)))) == 1
    @test eltype(c) == Tuple{Int,Int}
    @test map_rows(i -> (exp = exp(i), log = log(i)), 1:5) == Columns((exp = exp.(1:5), log = log.(1:5)))
    @test map_rows(tuple, 1:3, ["a","b","c"]) == Columns(([1,2,3], ["a","b","c"]))

 c = columnspair(Columns((a=[1,2,3],)), Columns((b=["a","b","c"],)))
    @test columns(c).first == Columns((a=[1,2,3],))
    @test columns(c).second == Columns((b=["a","b","c"],))
    @test colnames(c) == ((:a,) => (:b,))
    @test length(c) == 3
    @test ncols(c) == (1 => 1)
    @test eltype(c) == typeof((a=1,)=>(b="a",))
    @test c[1] == ((a=1,) => (b="a",))
    @test c[1:2] ==  columnspair(Columns((a=[1,2],)), Columns((b=["a","b"],)))
    @test view(c, 1:2) == columnspair(Columns((a=view([1,2,3],1:2),)), Columns((b=view(["a","b","c"],1:2),)))
    d = deepcopy(c)
    d[1] = (a=2,) => (b="aa",)
    @test d[1] == ((a=2,) => (b="aa",))
    d = deepcopy(c)
    push!(d, (a=4,) => (b="d",))
    @test d == columnspair(Columns((a=[1,2,3,4],)), Columns((b=["a","b","c","d"],)))
    e = vcat(d, d)
    append!(d, d)
    @test d == columnspair(Columns((a=[1,2,3,4,1,2,3,4],)), Columns((b=["a","b","c","d","a","b","c","d"],)))
    @test d == e
    empty!(d)
    @test d == c[Int[]]
    @test c != Columns((a=[1,2,3], b=["a","b","c"]))
    x = Columns(([1], [1.0], WeakRefStrings.StringArray(["a"])))
    @test IndexedTables.arrayof(eltype(x)) == typeof(x)
    @test IndexedTables.arrayof(WeakRefString{UInt8}) == WeakRefStrings.StringArray{WeakRefString{UInt8},1}
    @test typeof(similar(c, 10)) == typeof(similar(typeof(c), 10)) == typeof(c)
    @test length(similar(c, 10)) == 10
    @test issorted(c)
    @test sortperm(c) == [1,2,3]
    permute!(c, [2,3, 1])
    @test c == columnspair(Columns((a=[2,3,1],)), Columns((b=["b","c","a"],)))
    f = columnspair(Columns(([1, 1, 2, 2],)), ["b", "a", "c", "d"])
    @test IndexedTables._strip_pair(f) == Columns(([1, 1, 2, 2], ["b", "a", "c", "d"]))
    @test sortperm(f) == [2, 1, 3, 4]
    @test sort(f) == columnspair(Columns(([1, 1, 2, 2],)), ["a", "b", "c", "d"])
    @test !issorted(f)
#end

Random.seed!(123)
A = NDSparse(rand(1:3,10), rand('A':'F',10), map(UInt8,rand(1:3,10)), collect(1:10), randn(10))
B = NDSparse(map(UInt8,rand(1:3,10)), rand('A':'F',10), rand(1:3,10), randn(10))
C = NDSparse(map(UInt8,rand(1:3,10)), rand(1:3,10), rand(1:3,10), randn(10))

let a = NDSparse([12,21,32], [52,41,34], [11,53,150]), b = NDSparse([12,23,32], [52,43,34], [56,13,10])
    @test eltype(a) == Int
    @test sum(a) == 214

    c = similar(a)
    @test typeof(c) == typeof(a)
    @test length(c.index) == 0

    c = copy(a)
    @test typeof(c) == typeof(a)
    @test length(c.index) == length(a.index)
    empty!(c)
    @test length(c.index) == 0

    c = convertdim(convertdim(a, 1, Dict(12=>10, 21=>20, 32=>20)), 2, Dict(52=>50, 34=>20, 41=>20), -)
    @test c[20,20] == 97

    c = map(+, a, b)
    @test length(c.index) == 2
    @test sum(map(-, c, c)) == 0

    @test map(iseven, a) == NDSparse([12,21,32], [52,41,34], [false,false,true])

    # 97
    x = ndsparse((t=[0.01, 0.05],), (x=[1,2], y=[3,4],))
    @test map(p->(r = sum(p),), x).data == Columns(([4,6],), names=[:r])
end

    idx = Columns(p=[1,2], q=[3,4])
    t = NDSparse(idx, Columns(a=[5,6],b=[7,8]))
    t1 = NDSparse(Columns(p=[1,2,3]), Columns(c=[4,5,6]))
    t2 = NDSparse(Columns(q=[2,3]), Columns(c=[4,5]))

    # scalar output
    @test broadcast(==, t, t) == NDSparse(idx, Bool[1,1])
    @test broadcast((x,y)->x.a+y.c, t, t1) == NDSparse(idx, [9,11])
    @test broadcast((x,y)->y.a+x.c, t1, t) == NDSparse(idx, [9,11])
    @test broadcast((x,y)->x.a+y.c, t, t2) == NDSparse(idx[1:1], [10])

    # Tuple output
    b1 = broadcast((x,y)->(x.a, y.c), t, t1)
    @test isa(b1.data, Columns)
    @test b1 == NDSparse(idx, Columns(([5,6], [4,5])))

    b2 = broadcast((x,y)->(m=x.a, n=y.c), t, t1)
    @test b2 == NDSparse(idx, Columns(m=[5,6], n=[4,5]))
    @test isa(b2.data, Columns)
    @test fieldnames(eltype(b2.data)) == (:m, :n)

    S = sprand(10,10,.1)
    v = rand(10)
    nd = convert(NDSparse, S)
    ndv = convert(NDSparse,v)
    @test broadcast(*, nd, ndv) == convert(NDSparse, S .* v)
    # test matching dimensions by name
    ndt0 = convert(NDSparse, sparse(S .* (v')))
    ndt = NDSparse(Columns(a=columns(ndt0.index)[1], b=columns(ndt0.index)[2]), ndt0.data, presorted=true)
    @test broadcast(*,
                    NDSparse(Columns(a=columns(nd.index)[1], b=columns(nd.index)[2]), nd.data),
                    NDSparse(Columns(b=columns(ndv.index)[1]), ndv.data)) == ndt

let a = rand(10), b = rand(10), c = rand(10)
    @test NDSparse(a, b, c) == NDSparse(a, b, c)
    c2 = copy(c)
    c2[1] += 1
    @test NDSparse(a, b, c) != NDSparse(a, b, c2)
    b2 = copy(b)
    b2[1] += 1
    @test NDSparse(a, b, c) != NDSparse(a, b2, c)
end

let a = rand(10), b = rand(10), c = rand(10), d = rand(10)
    local nd = NDSparse(a,b,c,d)
    @test permutedims(nd,[3,1,2]) == NDSparse(c,a,b,d)
    @test_throws ArgumentError permutedims(nd, [1,2])
    @test_throws ArgumentError permutedims(nd, [1,3])
    @test_throws ArgumentError permutedims(nd, [1,2,2])
end

let r=1:5, s=1:2:5
    A = NDSparse([r;], [r;], [r;])
    @test A[s, :] == NDSparse([s;], [s;], [s;])
    @test_throws ErrorException A[s, :, :]
end

 a = NDSparse([1,2,2,2], [1,2,3,4], [10,9,8,7])
    @test a[1,1] == 10
    @test a[2,3] == 8
    #@test_throws ErrorException a[2]
    @test a[2,:] == NDSparse([2,3,4], [9,8,7])
    @test a[:,1] == NDSparse([1], [10])
    @test collect(where(a, 2, :)) == [9,8,7]
    @test collect(Base.pairs(a)) == [(1,1)=>10, (2,2)=>9, (2,3)=>8, (2,4)=>7]
    @test first(Base.pairs(a[:, 3])) == ((2,)=>8)

    IndexedTables.update!(x->x+10, a, 2, :)
    @test a == NDSparse([1,2,2,2], [1,2,3,4], [10,19,18,17])

    a[2,2:3] = 77
    @test a == NDSparse([1,2,2,2], [1,2,3,4], [10,77,77,17])

let a = NDSparse([1,2,2,2], [1,2,3,4], zeros(4))
    a2 = copy(a); a3 = copy(a)
    #a[2,:] = 1
    #@test a == NDSparse([1,2,2,2], [1,2,3,4], Float64[0,1,1,1])
    a2[2,[2,3]] = 1
    @test a2 == NDSparse([1,2,2,2], [1,2,3,4], Float64[0,1,1,0])
    a3[2,[2,3]] = [8,9]
    @test a3 == NDSparse([1,2,2,2], [1,2,3,4], Float64[0,8,9,0])
end

# issue #15
let a = NDSparse([1,2,3,4], [1,2,3,4], [1,2,3,4])
    a[5,5] = 5
    a[5,5] = 6
    @test a[5,5] == 6
end

let a = NDSparse([1,2,2,3,4,5], [1,2,2,3,4,5], [1,2,20,3,4,5], agg=+)
    @test a == NDSparse([1,2,3,4,5], [1,2,3,4,5], [1,22,3,4,5])
    @test a == aggregate!(+, NDSparse([1,2,2,3,4,5], [1,2,2,3,4,5], [1,2,20,3,4,5]))
end

let a = rand(5,5,5)
    for dims in ([2,3], [1], [2])
        r = dropdims(reduce(+, a; dims=dims), dims=(dims...,))
        asnd = convert(NDSparse,a)
        b = reduce(+, asnd, dims=dims)
        bv = reducedim_vec(sum, asnd, dims)
        c = convert(NDSparse, r)
        @test b.index == c.index == bv.index
        @test b.data ≈ c.data
        @test bv.data ≈ c.data
    end
    @test_throws ArgumentError reduce(+, convert(NDSparse,a), dims=[1,2,3])
end

for a in (rand(2,2), rand(3,5))
    nd = convert(NDSparse, a)
    @test nd == convert(NDSparse, sparse(a))
    for (I,d) in zip(nd.index, nd.data)
        @test a[I...] == d
    end
end

_colnames(x::NDSparse) = keys(columns(x.index))

@test _colnames(NDSparse(ones(2),ones(2),ones(2),names=[:a,:b])) == (:a, :b)
@test _colnames(NDSparse(Columns(x=ones(2),y=ones(2)), ones(2))) == (:x, :y)

x = NDSparse(Columns(x = [1,2,3], y = [4,5,6], z = [7,8,9]), [10,11,12])
    names = (:x, :y, :z)
    @test _colnames(x) == names
    @test _colnames(filter(a->a==11, x)) == names
    @test _colnames(selectkeys(x, (:z, :x))) == (:z, :x)
    @test _colnames(selectkeys(x, (:y,))) == (:y,)
    @test _colnames(filter((:x=>a->a>1, :z=>a->a>7), x, )) == names
    @test _colnames(x[1:2, 4:5, 8:9]) == names
    @test convertdim(x, :y, a->0) == NDSparse(Columns(x=[1,2,3], y=[0,0,0], z=[7,8,9]), [10,11,12])
    @test convertdim(x, :y, a->0, name=:yy) == NDSparse(Columns(x=[1,2,3], yy=[0,0,0], z=[7,8,9]), [10,11,12])

# test showing

@test repr(ndsparse(Columns(([1],)), Columns(()))) == """
1-d NDSparse with 1 values (0-tuples):
1 │
──┼
1 │ """

@test repr(table()) == """
Table with 0 rows, 0 columns:

"""

@test repr(NDSparse([1,2,3],[3,2,1],Float64[4,5,6])) == """
2-d NDSparse with 3 values (Float64):
1  2 │
─────┼────
1  3 │ 4.0
2  2 │ 5.0
3  1 │ 6.0"""

@test repr(NDSparse(Columns(a=[1,2,3],test=[3,2,1]),Float64[4,5,6])) == """
2-d NDSparse with 3 values (Float64):
a  test │
────────┼────
1  3    │ 4.0
2  2    │ 5.0
3  1    │ 6.0"""

@test repr(NDSparse(Columns(a=[1,2,3],test=[3,2,1]),Columns(x=Float64[4,5,6],y=[9,8,7]))) == """
2-d NDSparse with 3 values (2 field named tuples):
a  test │ x    y
────────┼───────
1  3    │ 4.0  9
2  2    │ 5.0  8
3  1    │ 6.0  7"""

@test repr(NDSparse([1,2,3],[3,2,1],Columns(x=Float64[4,5,6],y=[9,8,7]))) == """
2-d NDSparse with 3 values (2 field named tuples):
1  2 │ x    y
─────┼───────
1  3 │ 4.0  9
2  2 │ 5.0  8
3  1 │ 6.0  7"""

@test repr(NDSparse([1:19;],ones(Int,19))) == """
1-d NDSparse with 19 values ($Int):
1  │
───┼──
1  │ 1
2  │ 1
3  │ 1
4  │ 1
5  │ 1
6  │ 1
7  │ 1
8  │ 1
9  │ 1
   ⋮
12 │ 1
13 │ 1
14 │ 1
15 │ 1
16 │ 1
17 │ 1
18 │ 1
19 │ 1"""

function foo(n, data=ones(Int, 1))
    t=IndexedTables.namedtuple((Symbol("x$i") for i=1:n)...)
    NDSparse(Columns(t([ones(Int, 1) for i=1:n]...)), data)
end

let x = Columns(([6,5,4,3,2,2,1],[4,4,4,4,4,4,4],[1,2,3,4,5,6,7]))
    @test issorted(x[sortperm(x)])
end

let x = NDSparse([1,2],[3,4],[:a,:b],[3,5])
    @test x[1,:,:a] == NDSparse([3],[3])
end

# issue #42
let hitemps = NDSparse([fill("New York",3); fill("Boston",3)],
                           repeat(Date(2016,7,6):Dates.Day(1):Date(2016,7,8), 2),
                           [91,89,91,95,83,76])
    @test hitemps[:, Date(2016,7,8)] == NDSparse(["New York", "Boston"],
                                                     [91,76])
end

    cs = Columns(([1], [2]))
    t = table(cs)
    @test t.pkey == Int[]
    @test t.columns == [(1,2)]
    @test column(t.columns,1) !== columns(cs)[1]
    t = table(cs, copy=false)
    @test column(t.columns,1) === columns(cs)[1]
    t = table(cs, copy=false, pkey=[1])
    @test column(t.columns,1) === columns(cs)[1]
    cs = Columns(([2, 1], [3,4]))
    t = table(cs, copy=false, pkey=[1])
    @test t.pkey == Int[1]
    cs = Columns(([2, 1], [3,4]))
    t = table(cs, copy=false, pkey=[1])
    @test column(t.columns,1) === columns(cs)[1]
    @test t.pkey == Int[1]
    @test t.columns == [(1,4), (2,3)]

    cs = Columns((x=[2, 1], y=[3,4]))
    t = table(cs, copy=false, pkey=:x)
    @test column(t.columns,1) === columns(cs).x
    @test t.pkey == Int[1]
    @test t.columns == [(x=1,y=4), (x=2,y=3)]

    cs = Columns(([2, 1], [3,4]))
    t = table(cs, presorted=true, pkey=[1])
    @test t.pkey == Int[1]
    @test t.columns == [(2,3), (1,4)]

    a = table([1, 2, 3], [4, 5, 6])
    b = table([1, 2, 3], [4, 5, 6], names=[:x, :y])
    @test table(([1, 2, 3], [4, 5, 6])) == a
    @test table((x = [1, 2, 3], y = [4, 5, 6])) == b
    @test table(Columns(([1, 2, 3], [4, 5, 6]))) == a
    @test table(Columns(x=[1, 2, 3], y=[4, 5, 6])) == b
    @test b == table(b)
    b = table([2, 3, 1], [4, 5, 6], names=[:x, :y], pkey=:x)
    b = table([2, 1, 2, 1], [2, 3, 1, 3], [4, 5, 6, 7], names=[:x, :y, :z], pkey=(:x, :y))
    t = table([1, 2], [3, 4])
    @test pkeynames(t) == ()
    t = table([1, 2], [3, 4], pkey=1)
    @test pkeynames(t) == (1,)
    t = table([2, 1], [1, 3], [4, 5], names=[:x, :y, :z], pkey=(1, 2))
    @test pkeys(t) == Columns((x = [1, 2], y = [3, 1]))
    @test pkeys(a) == Columns((Base.OneTo(3),))
    a = table(["a", "b"], [3, 4], pkey=1)
    @test pkeys(a) == Columns((["a", "b"],))
    t = table([2, 1], [1, 3], [4, 5], names=[:x, :y, :z], pkey=(1, 2))
    @test excludecols(t, (:x,)) == (2, 3)
    @test excludecols(t, Not((2, 3))) == (2, 3)
    @test excludecols(t, (2,)) == (1, 3)
    @test excludecols(t, pkeynames(t)) == (3,)
    @test excludecols([1, 2, 3], (1,)) == ()
    @test convert(IndexedTable, Columns(x=[1, 2], y=[3, 4]), Columns(z=[1, 2]), presorted=true) == table([1, 2], [3, 4], [1, 2], names=Symbol[:x, :y, :z])
    @test colnames([1, 2, 3]) == (1,)
    @test colnames(Columns(([1, 2, 3], [3, 4, 5]))) == (1, 2)
    @test colnames(table([1, 2, 3], [3, 4, 5])) == (1, 2)
    @test colnames(Columns(x=[1, 2, 3], y=[3, 4, 5])) == (:x, :y)
    @test colnames(table([1, 2, 3], [3, 4, 5], names=[:x, :y])) == (:x, :y)
    @test colnames(ndsparse(Columns(x=[1, 2, 3]), Columns(y=[3, 4, 5]))) == (:x, :y)
    @test colnames(ndsparse(Columns(x=[1, 2, 3]), [3, 4, 5])) == (:x, 2)
    @test colnames(ndsparse(Columns(x=[1, 2, 3]), [3, 4, 5])) == (:x, 2)
    @test colnames(ndsparse(Columns(([1, 2, 3], [4, 5, 6])), Columns(x=[6, 7, 8]))) == (1, 2, :x)
    @test colnames(ndsparse(Columns(x=[1, 2, 3]), Columns(([3, 4, 5], [6, 7, 8])))) == (:x, 2, 3)

    x = ndsparse(["a", "b"], [3, 4])
    @test (keytype(x), eltype(x)) == (Tuple{String}, Int)
    x = ndsparse((date = Date.(2014:2017),), [4:7;])
    @test x[Date("2015-01-01")] == 5
    @test (keytype(x), eltype(x)) == (Tuple{Date}, Int)
    x = ndsparse((["a", "b"], [3, 4]), [5, 6])
    @test (keytype(x), eltype(x)) == (Tuple{String,Int}, Int)
    @test x["a", 3] == 5
    x = ndsparse((["a", "b"], [3, 4]), ([5, 6], [7.0, 8.0]))
    x = ndsparse((x = ["a", "a", "b"], y = [3, 4, 4]), (p = [5, 6, 7], q = [8.0, 9.0, 10.0]))
    @test (keytype(x), eltype(x)) == (Tuple{String,Int}, NamedTuple{(:p,:q), Tuple{Int,Float64}})
    @test x["a", :] == ndsparse((y = [3, 4],), Columns((p = [5, 6], q = [8.0, 9.0])))

    x = ndsparse([1, 2], [3, 4])
    @test pkeynames(x) == (1,)

    a = Columns(([1,2,1],["foo","bar","baz"]))
    b = Columns(([2,1,1],["bar","baz","foo"]))
    c = Columns(([1,1,2],["foo","baz","bar"]))
    @test a != b
    @test a != c
    @test b != c
    sort!(a)
    @test sort(b) == a
    sort!(b); sort!(c)
    @test a == b == c
    @test size(a) == size(b) == size(c) == (3,)
    @test eltype(a) == Tuple{Int,String}
    @test length(similar(a)) == 3
    aa = map(tuple, columns(a)...)
    @test isa(convert(Columns, aa), Columns)
    @test convert(Columns, aa) == a
    bb = map((x,y)->(x=x,y=y), columns(a)...)
    @test isa(convert(Columns, bb), Columns)
    @test convert(Columns, bb) == Columns(x=column(a,1), y=column(a, 2))

    #78
    @test_throws ArgumentError map(x->throw(ArgumentError("x")), a)
    @inferred Columns((c=[1],))
    @inferred Columns(([1],))
    @inferred Columns(c=[1])
    #@inferred NDSparse(Columns(c=[1]), [1])
    #@inferred NDSparse(Columns([1]), [1])
    c = Columns(([1,1,1,2,2], [1,2,4,3,5]))
    d = Columns(([1,1,2,2,2], [1,3,1,4,5]))
    e = Columns(([1,1,1], sort([rand(),0.5,rand()])))
    f = Columns(([1,1,1], sort([rand(),0.5,rand()])))
    @test merge(NDSparse(c,ones(5)),NDSparse(d,ones(5))).index == Columns(([1,1,1,1,2,2,2,2],[1,2,3,4,1,3,4,5]))
    @test eltype(merge(NDSparse(c,Columns((ones(Int, 5),))),NDSparse(d,Columns((ones(Float64, 5),)))).data) == Tuple{Float64}
    @test eltype(merge(NDSparse(c,Columns(x=ones(Int, 5))),NDSparse(d,Columns(x=ones(Float64, 5)))).data) == typeof((x=0.,))
    @test length(merge(NDSparse(e,ones(3)),NDSparse(f,ones(3)))) == 5
    @test vcat(Columns(x=[1]), Columns(x=[1.0])) == Columns(x=[1,1.0])
    @test vcat(Columns(x=PooledArray(["x"])), Columns(x=["y"])) == Columns(x=["x", "y"])

@testset "Getindex" begin
    cs = Columns(x=[1.2, 3.4], y=[3,4])
    t = table(cs, copy=false, pkey=:x)

    @test t[1] == (x=1.2, y=3)
    @test t[[true, false]] == t[[1]]
    @test t[[1,2]].columns == t.columns
    @test_throws ArgumentError t[[2,1]]

    s = table(cs)
    @test s[[2,1]] == table(rows(s)[[2,1]])

    ## Also test getindex, haskey, and get for NDSparse
    ts = ndsparse(t)
    @test ts[1.2] == (y = 3,)
    @test_throws KeyError ts[1.3]
    @test haskey(ts, (1.2,)) == true
    @test haskey(ts, (1.3,)) == false
    @test get(ts, (1.2,), missing) == (y = 3,)
    @test get(ts, (1.3,), missing) === missing
    @test get(ts, (1.2,)) do
        missing
    end == (y = 3,)
    @test get(ts, (1.3,)) do
        missing
    end === missing
end

@testset "view & range" begin
    @test table(1:10) == table(collect(1:10))
    t = table(1:10, copy=false)
    @test columns(t, 1) == 1:10
    v = collect(1:10)
    t = view(table(v), 1:2)
    @test columns(t, 1) == view(v, 1:2)
    @test view(table(1:2), [true, false]) == view(table(1:2), [1])
    @test_throws ArgumentError view(table(1:2, pkey = 1), [2,1])
end

@testset "sortpermby" begin
    cs = Columns(x=[1,1,2,1,2], y=[1,2,2,1,2], z=[7,6,5,4,3])
    t = table(cs, copy=false, pkey=[:x, :y])
    # x=[1,1,1,2,2], y=[1,1,2,2,2], z=[7,4,6,5,3]
    @test column(t, :z) == [7,4,6,5,3]
    @test issorted(rows(t, (:x,:y)))
    @test sortpermby(t, (:y, :z), cache=true) == [2,1,5,4,3]
    @test t.perms[1].perm == [2,1,5,4,3]
    perms = [primaryperm(t); t.perms]

    @test sortpermby(t, (:y, :x)) == [2,1,3,5,4]
    @test length(t.perms) == 1

    # fully known
    @test best_perm_estimate(perms, [1,2]) == (2, Base.OneTo(5))
    @test best_perm_estimate(perms, [2,3]) == (2, [2, 1, 5, 4, 3])

    # first column known
    @test best_perm_estimate(perms, [1,3]) == (1, Base.OneTo(5))
    @test best_perm_estimate(perms, [2,1]) == (1, [2, 1, 5, 4, 3])

    # nothing known
    @test best_perm_estimate(perms, [3,1]) == (0, nothing)
end

@testset "sort" begin
    t = table(collect(1:10), collect(10:-1:1), names = [:x, :y])
    @test columns(sort(t, rev = true), :x) == collect(10:-1:1)
    @test columns(sort(t, :y), :y) == collect(1:10)
    sort!(t)
    @test columns(t, :x) == collect(1:10)
    sort!(t, :y)
    @test columns(t, :y) == collect(1:10)
end

    t = table([2, 1], [1, 3], [4, 5], names=[:x, :y, :z], pkey=(1, 2))
    @test reindex(t, (:y, :z)) == table([1, 3], [4, 5], [2, 1], names=Symbol[:y, :z, :x])
    @test reindex(t, Not(:x)) == reindex(t, (:y, :z))
    @test reindex(t, Not(:x), r"x") == reindex(t, (:y, :z), (:x,))
    @test pkeynames(t) == (:x, :y)
    @test reindex(t, (:w => [4, 5], :z)) == table([4, 5], [5, 4], [1, 2], [3, 1], names=Symbol[:w, :z, :x, :y])
    @test pkeynames(t) == (:x, :y)

    t = table([1, 2], [3, 4], names=[:x, :y])
    @test columns(t) == (x = [1, 2], y = [3, 4])
    @test columns(t, :x) == [1, 2]
    @test columns(t, (:x,)) == (x = [1, 2],)
    @test columns(t, (:y, :x => (-))) == (y = [3, 4], x = [-1, -2])
    t = table([1, 2], [3, 4], names=[:x, :y])
    @test rows(t) == Columns((x = [1, 2], y = [3, 4]))
    @test rows(t, :x) == [1, 2]
    @test rows(t, (:x,)) == Columns((x = [1, 2],))
    @test rows(t, (:y, :x => (-))) == Columns((y = [3, 4], x = [-1, -2]))

    x = NDSparse(Columns(a=[1,1], b=[1,2]), Columns(c=[3,4]))
    y = NDSparse(Columns(a=[1,1], b=[1,2]), [3,4])

    @test column(x, :a) == [1,1]
    @test column(x, [5,6]) == [5,6]
    @test column(x, :b) == [1,2]
    @test column(x, :c) == [3,4]
    @test column(x, 3) == [3,4]
    @test column(y, 3) == [3,4]

    @test columns(x, :a) == [1,1]
    @test columns(x, (:a,:c)) == (a=[1,1], c=[3,4])
    @test columns(y, (1, 3)) == ([1,1], [3,4])

    @test rows(x) == [(a=1,b=1,c=3), (a=1,b=2,c=4)]
    @test rows(x, :b) == [1, 2]
    @test rows(x, (:b, :c)) == [(b=1,c=3), (b=2,c=4)]
    @test rows(x, (:c, :b => -)) == [(c=3, b=-1),(c=4, b=-2)]
    @test rows(x, (:c, :x => [1,2])) == [(c=3, x=1),(c=4, x=2)]
    @test rows(x, (:c, [1,2])) == [(3,1), (4,2)]

    @test keys(x) == [(a=1,b=1), (a=1,b=2)]
    @test keys(x, :a) == [1, 1]

    @test values(x) == [(c=3,), (c=4,)]
    @test values(x,1) == [3,4]
    @test values(y) == [3, 4]
    @test values(y,1) == [3,4]

    @test collect(Base.pairs(x)) == [(a=1,b=1)=>(c=3,), (a=1,b=2)=>(c=4,)]
    @test collect(Base.pairs(y)) == [(a=1,b=1)=>3, (a=1,b=2)=>4]

@testset "column manipulation" begin
    t = table([1, 2], [3, 4], names=[:x, :y])
    @test transform(t, 2 => [5, 6]) == table([1, 2], [5, 6], names=Symbol[:x, :y])
    @test transform(t, 2 => [5, 6], 1 => [7, 12]) == table([7, 12], [5, 6], names=Symbol[:x, :y]) ==
        transform(t, (2 => [5, 6], 1 => [7, 12])) == transform(t, [2 => [5, 6], 1 => [7, 12]])
    @test transform(t, :x => :x => (x->1 / x)) == table([1.0, 0.5], [3, 4], names=Symbol[:x, :y])
    t = table([0.01, 0.05], [1, 2], [3, 4], names=[:t, :x, :y], pkey=:t)
    t2 = transform(t, :t => [0.1, 0.05])
    @test t2 == table([0.05, 0.1], [2,1], [4,3], names=[:t,:x,:y])
    t = table([0.01, 0.05], [2, 1], [3, 4], names=[:t, :x, :y], pkey=:t)
    @test transform(t, :z => [1 // 2, 3 // 4]) == table([0.01, 0.05], [2, 1], [3, 4], [1//2, 3//4], names=Symbol[:t, :x, :y, :z])
    @test transform(t, :z => [1 // 2, 3 // 4], :w => [0, 1]) ==
        table([0.01, 0.05], [2, 1], [3, 4], [1//2, 3//4], [0, 1], names=Symbol[:t, :x, :y, :z, :w]) ==
        transform(t, (:z => [1 // 2, 3 // 4], :w => [0, 1])) == transform(t, [:z => [1 // 2, 3 // 4], :w => [0, 1]])
    t = table([0.01, 0.05], [2, 1], [3, 4], names=[:t, :x, :y], pkey=(:t,:x))
    @test select(t, Not(:t)) == table([1, 2], [4,3], names=Symbol[:x, :y])
    @test select(t, Not(ncols(t))) == table([0.01, 0.05], [2, 1], names=Symbol[:t, :x])
    @test select(t, Not(All(:x, :y))) == table([0.01, 0.05], names=Symbol[:t]) == select(t, Not((:x, :y)))
    @test transform(t, :z => [1 // 2, 3 // 4]) == table([0.01, 0.05], [2, 1], [3, 4], [1//2, 3//4], names=Symbol[:t, :x, :y, :z])

    # 99
    @test select(t, Not(:x)).pkey == [1]

    # "Copy-on write"
    t = table([1,2,3], [4,5,6], names=[:x,:y], pkey=:x)
    tcopy = copy(t)
    @test column(transform(t, :z => [7,8,9]), :x) === column(t, :x)
    @test t == tcopy

    @test column(transform(t, :y => [7,8,9]), :x) === column(t, :x)
    @test t == tcopy

    # seting or popping an index column causes copy
    t2 = transform(t, :x => [9,8,7])
    @test column(t2, :y) !== column(t, :y)
    @test t == tcopy
    @test t2 == table([7,8,9], [6,5,4], names=[:x, :y])

    t2 = select(t, Not(:x))
    @test column(t2, :y) !== column(t, :y)
    @test t == tcopy
    @test t2 == table([4,5,6], names=[:y])

    t = table([0.01, 0.05], [2, 1], [3, 4], names=[:t, :x, :y], pkey=:t)
    @test insertcols(t, 2, :w => [0, 1]) == table([0.01, 0.05], [0, 1], [2, 1], [3, 4], names=Symbol[:t, :w, :x, :y])
    t = table([0.01, 0.05], [2, 1], [3, 4], names=[:t, :x, :y], pkey=:t)
    @test insertcolsafter(t, :t, :w => [0, 1]) == table([0.01, 0.05], [0, 1], [2, 1], [3, 4], names=Symbol[:t, :w, :x, :y])
    t = table([0.01, 0.05], [2, 1], [3, 4], names=[:t, :x, :y], pkey=:t)
    @test insertcolsbefore(t, :x, :w => [0, 1]) == table([0.01, 0.05], [0, 1], [2, 1], [3, 4], names=Symbol[:t, :w, :x, :y])

    t = table([0.01, 0.05], [2, 1], [3, 4], names=[:t, :x, :y], pkey=:t)
    @test insertcols(t, 2, :w => [0, 1], :z => [2, 3]) ==
        table([0.01, 0.05], [0, 1], [2, 3], [2, 1], [3, 4], names=Symbol[:t, :w, :z, :x, :y]) ==
        insertcols(t, 2, :w => [0, 1], :z => [2, 3])
    t = table([0.01, 0.05], [2, 1], [3, 4], names=[:t, :x, :y], pkey=:t)
    @test insertcolsafter(t, :t, :w => [0, 1], :z => [2, 3]) ==
        table([0.01, 0.05], [0, 1], [2, 3], [2, 1], [3, 4], names=Symbol[:t, :w, :z, :x, :y]) ==
        insertcolsafter(t, :t, (:w => [0, 1], :z => [2, 3]))
    t = table([0.01, 0.05], [2, 1], [3, 4], names=[:t, :x, :y], pkey=:t)
    @test insertcolsbefore(t, :x, :w => [0, 1], :z => [2, 3]) ==
        table([0.01, 0.05], [0, 1], [2, 3], [2, 1], [3, 4], names=Symbol[:t, :w, :z, :x, :y]) ==
        insertcolsbefore(t, :x, (:w => [0, 1], :z => [2, 3]))

    t = table([0.01, 0.05], [2, 1], names=[:t, :x])
    @test rename(t, :t => :time) == table([0.01, 0.05], [2, 1], names=Symbol[:time, :x])
    @test_throws ErrorException rename(t, :tt => :time)
    @test rename(t, :t => :time) == rename(t, :t => :time)
    @test rename(t, :t => :time, :x => :position) ==
        table([0.01, 0.05], [2, 1], names=Symbol[:time, :position]) ==
        rename(t, (:t => :time, :x => :position)) == rename(t, [:t => :time, :x => :position])
end

@testset "map" begin
    x = ndsparse((t = [0.01, 0.05],), (x = [1, 2], y = [3, 4]))
    manh = map((row->row.x + row.y), x)
    vx = map((row->row.x / row.t), x, select=(:t, :x))
    polar = map((p->(r = hypot(p.x + p.y), θ = atan(p.y, p.x))), x)
    @test map(sin, polar, select=:θ) == ndsparse((t = [0.01, 0.05],), [0.9486832980505138, 0.8944271909999159])

    t = table([0.01, 0.05], [1, 2], [3, 4], names=[:t, :x, :y])
    manh = map((row->row.x + row.y), t)
    polar = map((p->(r = hypot(p.x + p.y), θ = atan(p.y, p.x))), t)
    vx = map((row->row.x / row.t), t, select=(:t, :x))
    @test map(sin, polar, select=:θ) == sin.(column(polar, :θ))
    t = NDSparse([1,2,3], Columns(x=[4,5,6]))
    @test isa(map(x->x.x, t).data, Vector)
    @test map(x->x.x, t).data == [4,5,6]

    t1 = map(x->(x=x.x,y=x.x^2), t)
    @test isa(t1.data, Columns)
    @test fieldnames(eltype(t1.data)) == (:x,:y)

    t2 = map(x->(x.x,x.x^2), t)
    @test isa(t2.data, Columns)
    @test isa(columns(t2.data), Tuple{Vector{Int}, Vector{Int}})

    t3 = map(x->ntuple(identity, x.x), t)
    @test isa(t3.data, Vector)
    @test eltype(t3.data) == Tuple{Int,Int,Int,Int,Vararg{Int,N} where N}

    y = [1, 1//2, "x"]
    function foo(x)
        tuple(x.x, y[x.x-3])
    end
    t4 = map(foo, t)
    @test isa(t4.data, Columns)
    @test eltype(t4.data) <: Tuple{Int, Any}

    t5 = table([1,2], ["a", "b"], names = [:x, :y])
    s = [:x, :y]
    @test map(i -> Tuple(getfield(i, j) for j in s), t5) == table([1,2], ["a", "b"])
    @test map(i -> (i.x => i.y), t5) == table([1,2], ["a","b"], pkey=1)
    @test map(i -> ((a = i.x-1,)=>(b=i.y,)), t5) == table([0,1], ["a","b"], pkey=:a, names=[:a,:b])

    @test map(t -> (1,2), table(Int[])) == table(Int[], Int[])
end

@testset "Original Join Testset" begin
    l = table([1, 1, 2, 2], [1, 2, 1, 2], [1, 2, 3, 4], names=[:a, :b, :c], pkey=(:a, :b))
    r = table([0, 1, 1, 3], [1, 1, 2, 2], [1, 2, 3, 4], names=[:a, :b, :d], pkey=(:a, :b))
    @test join(l, r) == table([1, 1], [1, 2], [1, 2], [2, 3], names=Symbol[:a, :b, :c, :d])
    @test isequal(
        join(l, r, how=:left),
        table((a=[1, 1, 2, 2], b=[1, 2, 1, 2], c=[1, 2, 3, 4], d=[2, 3, missing, missing]))
    )
    @test isequal(
        join(l, r, how=:outer),
        table((a=[0, 1, 1, 2, 2, 3], b=[1, 1, 2, 1, 2, 2], c=[missing, 1, 2, 3, 4, missing], d=[1, 2, 3, missing, missing, 4]))
    )
    a = table([1],[2], names=[:x,:y])
    b = table([1],[3], names=[:a,:b])
    @test join(a, b, lkey=:x,rkey=:a) == table([1],[2],[3], names=[:x,:y,:b]) # issue JuliaDB.jl#105
    @test join(a, b, lkey=Not(:y), rkey = Not(:b)) == join(a, b, lkey=:x,rkey=:a)
    @test join(l, r, how=:anti) == table([2, 2], [1, 2], [3, 4], names=Symbol[:a, :b, :c])
    l1 = table([1, 2, 2, 3], [1, 2, 3, 4], names=[:x, :y])
    r1 = table([2, 2, 3, 3], [5, 6, 7, 8], names=[:x, :z])
    @test join(l1, r1, lkey=:x, rkey=:x) == table([2, 2, 2, 2, 3, 3], [2, 2, 3, 3, 4, 4], [5, 6, 5, 6, 7, 8], names=Symbol[:x, :y, :z])
    @test isequal(join(l, r, lkey=:a, rkey=:a, lselect=:b, rselect=:d, how=:outer), table([0, 1, 1, 1, 1, 2, 2, 3], [missing, 1, 1, 2, 2, 1, 2, missing], [1, 2, 3, 2, 3, missing, missing, 4], names=Symbol[:a, :b, :d]))

    t = table(["a","b","c","a"], [1,2,3,4]); t1 = table(["a","b"], [1,2])
    @test isequal(leftjoin(t,t1,lkey=1,rkey=1), table(["a","a","b","c"], [1,4,2,3], [1,1,2,missing]))

    t1 = table([1,2,3,4], [5,6,7,8], pkey=[1])
    t2 = table([0,3,4,5], [5,6,7,8], pkey=[1])
    t3 = table([0,3,4,4], [5,6,7,8], pkey=[1])
    t4 = table([1,3,4,4], [5,6,7,8], pkey=[1])
    @test naturaljoin(+, t1, t2, lselect=2, rselect=2) == table([3,4], [13, 15])
    @test naturaljoin(t1, t2, lselect=2, rselect=2) == table([3,4],[7,8],[6,7])
    @test naturaljoin(t1, t2) == table([3,4],[7,8],[6,7])
    @test naturaljoin(+, t1, t3, lselect=2, rselect=2) == table([3,4,4], [13, 15, 16])
    @test naturaljoin(+, t3, t4, lselect=2, rselect=2) == table([3,4,4,4,4], [12, 14,15,15,16])

    a = NDSparse([12,21,32], [52,41,34], [11,53,150])
    b = NDSparse([12,23,32], [52,43,34], [56,13,10])
    c = naturaljoin(+, a, b)
    @test c[12,52] == 67
    @test c[32,34] == 160
    @test length(c.index) == 2
    @test naturaljoin(a, b) == NDSparse([12,32], [52,34], Columns(([11,150], [56,10])))

    c = NDSparse([12,32], [52,34], Columns(([0,1], [2,3])))
    @test naturaljoin(a, c) == NDSparse([12,32], [52,34], Columns(([11,150], [0,1], [2,3])))
    @test naturaljoin(c, a) == NDSparse([12,32], [52,34], Columns(([0,1], [2,3], [11,150])))

    @test isequal(
        leftjoin(t1, t2, lselect=2, rselect=2),
        table([1,2,3,4], [5,6,7,8], [missing, missing, 6, 7])
    )

    # null instead of missing row
    @test isequal(
        leftjoin(+, t1, t2, lselect=2, rselect=2),
        table([1,2,3,4], [missing, missing, 13, 15])
    )

    @test isequal(leftjoin(t1, t2), table([1,2,3,4], [5,6,7,8], [missing, missing, 6,7]))
    @test isequal(leftjoin(+, t1, t3, lselect=2, rselect=2), table([1,2,3,4,4],[missing,missing,13,15,16]))
    @test isequal(leftjoin(+, t3, t4, lselect=2, rselect=2), table([0,3,4,4,4,4], [missing, 12, 14,15,15,16]))

    @test isequal(leftjoin(NDSparse([1,1,1,2], [2,3,4,4], [5,6,7,8]),
                   NDSparse([1,1,3],   [2,4,4],   [9,10,12])),
                  NDSparse([1,1,1,2], [2,3,4,4], Columns(([5, 6, 7, 8], [9, missing, 10, missing]))))

    @test isequal(
                  leftjoin(NDSparse([1,1,1,2], [2,3,4,4], [5,6,7,8]),
                   NDSparse([1,1,2],   [2,4,4],   [9,10,12])),
                  NDSparse([1,1,1,2], [2,3,4,4], Columns(([5, 6, 7, 8], [9, missing, 10, 12]))))

    @test isequal(outerjoin(t1, t2, lselect=2, rselect=2), table([0,1,2,3,4,5], [missing, 5,6,7,8,missing], [5,missing,missing,6,7,8]))

    #showl instead of missing row
    @test isequal(outerjoin(+, t1, t2, lselect=2, rselect=2), table([0,1,2,3,4,5], [missing, missing, missing, 13, 15, missing]))

    @test isequal(outerjoin(t1, t2), table([0,1,2,3,4,5], [missing, 5,6,7,8,missing], [5,missing,missing,6,7,8]))
    @test isequal(outerjoin(+, t1, t3, lselect=2, rselect=2), table([0,1,2,3,4,4],[missing,missing,missing,13,15,16]))
    @test isequal(outerjoin(+, t3, t4, lselect=2, rselect=2), table([0,1,3,4,4,4,4], [missing, missing, 12,14,15,15,16]))
end
@testset "groupjoin" begin
    l = table([1, 1, 1, 2], [1, 2, 2, 1], [1, 2, 3, 4], names=[:a, :b, :c], pkey=(:a, :b))
    r = table([0, 1, 1, 2], [1, 2, 2, 1], [1, 2, 3, 4], names=[:a, :b, :d], pkey=(:a, :b))
    @test groupjoin(l, r) == table([1, 2], [2, 1], [Columns((c = [2, 2, 3, 3], d = [2, 3, 2, 3])), Columns((c = [4], d = [4]))], names=Symbol[:a, :b, :groups])
    @test groupjoin(l, r, how=:left) == table([1, 1, 2], [1, 2, 1], [Columns((c = [], d = [])), Columns((c = [2, 2, 3, 3], d = [2, 3, 2, 3])), Columns((c = [4], d = [4]))], names=Symbol[:a, :b, :groups])
    @test groupjoin(l, r, how=:outer) == table([0, 1, 1, 2], [1, 1, 2, 1], [Columns((c = [], d = [])), Columns((c = [], d = [])), Columns((c = [2, 2, 3, 3], d = [2, 3, 2, 3])), Columns((c = [4], d = [4]))], names=Symbol[:a, :b, :groups])
    @test groupjoin(l, r, lkey=:a, rkey=:a, lselect=:c, rselect=:d, how=:outer) == table([0, 1, 2], [Columns((c = [], d = [])), Columns((c = [1, 1, 2, 2, 3, 3], d = [2, 3, 2, 3, 2, 3])), Columns((c = [4], d = [4]))], names=Symbol[:a, :groups])
    t = table([0,1,2,2], [0,1,2,3])
    t2 = table([1,2,2,3],[4,5,6,7])
    @test outergroupjoin(t, t2, lkey=1, rkey=1) == table([0,1,2,3], [[],[(1,4)], [(2,5), (2,6), (3,5), (3,6)], []])
    @test outergroupjoin(-,t, t2, lkey=1, rkey=1, lselect=2,rselect=2, init_group=()->0, accumulate=min) == table([0,1,2,3], [0, -3, -4, 0])
end

@testset "reducedim" begin
    x = ndsparse((x = [1, 1, 1, 2, 2, 2], y = [1, 2, 2, 1, 2, 2], z = [1, 1, 2, 1, 1, 2]), [1, 2, 3, 4, 5, 6])
    @test reduce(+, x, dims=1) == ndsparse((y = [1, 2, 2], z = [1, 1, 2]), [5, 7, 9])
    @test reduce(+, x, dims=(1, 3)) == ndsparse((y = [1, 2],), [5, 16])
end

@testset "select" begin
    tbl = table([0.01, 0.05], [2, 1], [3, 4], names=[:t, :x, :y], pkey=:t)
    @test select(tbl, 2) == [2, 1]
    @test select(tbl, :t) == [0.01, 0.05]
    @test select(tbl, :t => (t->1 / t)) == [100.0, 20.0]
    @test select(tbl, [3, 4]) == [3, 4]
    @test select(tbl, (2, 1)) == table([2, 1], [0.01, 0.05], names=Symbol[:x, :t])
    vx = select(tbl, (:x, :t) => (p->p.x / p.t))
    @test select(tbl, (:x, :t => (-))) == table([1, 2], [-0.05, -0.01], names=Symbol[:x, :t])
    @test select(tbl, (:x, :t, [3, 4])) == table([2, 1], [0.01, 0.05], [3, 4], names=[1, 2, 3])
    @test select(tbl, (:x, :t, :z => [3, 4])) == table([2, 1], [0.01, 0.05], [3, 4], names=Symbol[:x, :t, :z])
    @test select(tbl, (:x, :t, :minust => (:t => (-)))) == table([2, 1], [0.01, 0.05], [-0.01, -0.05], names=Symbol[:x, :t, :minust])
    @test select(tbl, (:x, :t, :vx => ((:x, :t) => (p->p.x / p.t)))) == table([2, 1], [0.01, 0.05], [200.0, 20.0], names=Symbol[:x, :t, :vx])
    @test select(tbl, (:x, :yy => :y)) == table([2, 1], [3, 4], names=Symbol[:x, :yy])
    @test select(tbl, (:x, :yy => :y => (p->p.+1))) == table([2, 1], [4, 5], names=Symbol[:x, :yy])

    a = ndsparse(([1,1,2,2], [1,2,1,2]), [6,7,8,9])
    @test selectkeys(a, 1, agg=+) == ndsparse([1,2], [13,17])
    @test selectkeys(a, 2, agg=+) == ndsparse([1,2], [14,16])
end

@testset "specialselector" begin
    t = table([1, 2, 3], ["a", "b", "c"], [4, 5, 6], names = [:x, :y, :z], pkey = :x)

    @test select(t, Keys()) == select(t, (:x,))
    @test select(t, (Keys(), :y)) == select(t, ((:x,), :y))
    @test select(t, Not(Keys())) == select(t, Not(:x)) == select(t, (:y, :z))
    @test select(t, Not(All(Keys(), :y))) == select(t, Not(All(:x, :y))) == select(t, (:z,))
    @test select(t, All(Keys(), :y)) == select(t, (:x, :y))
    @test select(t, All()) == t
    @test select(t, Between(:x, :z)) == select(t, (:x, :y, :z))
    @test select(t, i -> i == :y) == select(t, (:y,))
    @test select(t, r"x|z") == select(t, (:x, :z))
    @test select(t, Int) == select(t, (:x, :z))
    @test select(t, String) == select(t, (:y,))

    @test rows(t, Keys()) == rows(t, (:x,))
    @test rows(t, (Keys(), :y)) == rows(t, ((:x,), :y))
    @test rows(t, Not(Keys())) == rows(t, Not(:x)) == rows(t, (:y, :z))
    @test rows(t, Not(All(Keys(), :y))) == rows(t, Not(All(:x, :y))) == rows(t, (:z,))
    @test rows(t, All(Keys(), :y)) == rows(t, (:x, :y))
    @test rows(t, All()) == rows(t)
    @test rows(t, Between(:x, :z)) == rows(t, (:x, :y, :z))
    @test rows(t, i -> i == :y) == rows(t, (:y,))
    @test rows(t, r"x|z") == rows(t, (:x, :z))

    @test columns(t, Keys()) == columns(t, (:x,))
    @test columns(t, (Keys(), :y)) == columns(t, ((:x,), :y))
    @test columns(t, Not(Keys())) == columns(t, Not(:x)) == columns(t, (:y, :z))
    @test columns(t, Not(All(Keys(), :y))) == columns(t, Not(All(:x, :y))) == columns(t, (:z,))
    @test columns(t, All(Keys(), :y)) == columns(t, (:x, :y))
    @test columns(t, All()) == columns(t)
    @test columns(t, Between(:x, :z)) == columns(t, (:x, :y, :z))
    @test columns(t, i -> i == :y) == columns(t, (:y,))
    @test columns(t, r"x|z") == columns(t, (:x, :z))

    @test hascolumns(t, 1)
    @test !hascolumns(t, 10)
    @test hascolumns(t, :x)
    @test !hascolumns(t, :xx)
    @test !hascolumns(t, (:xx, :y))
    @test hascolumns(t, Keys())
    @test hascolumns(t, (Keys(), :y))
    @test hascolumns(t, Not(Keys()))
    @test !hascolumns(t, Not(All(Keys(), :xx)))
    @test hascolumns(t, All(Keys(), :y))
    @test hascolumns(t, All())
    @test hascolumns(t, Between(:x, :z))
    @test !hascolumns(t, Between(:x, :xx))
    @test hascolumns(t, i -> i == :y)
    @test hascolumns(t, r"x|z")
end
@testset "dropmissing" begin
    t = table([0.1, 0.5, missing, 0.7], [2, missing, 4, 5], [missing, 6, missing, 7], names=[:t, :x, :y])
    @test dropmissing(t) == table([0.7], [5], [7], names=Symbol[:t, :x, :y])
    @test isequal(dropmissing(t, :y), table([0.5, 0.7], [missing, 5], [6, 7], names=Symbol[:t, :x, :y]))
    @test typeof(column(dropmissing(t, :x), :x)) == Array{Int,1}
end
@testset "filter" begin
    t = table(["a", "b", "c"], [0.01, 0.05, 0.07], [2, 1, 0], names=[:n, :t, :x])
    @test filter((p->p.x / p.t < 100), t) == table(["b", "c"], [0.05, 0.07], [1, 0], names=Symbol[:n, :t, :x])
    x = ndsparse((n = ["a", "b", "c"], t = [0.01, 0.05, 0.07]), [2, 1, 0])
    @test filter((y->y < 2), x) == ndsparse((n = ["b", "c"], t = [0.05, 0.07]), [1, 0])
    @test filter(iseven, t, select=:x) == table(["a", "c"], [0.01, 0.07], [2, 0], names=Symbol[:n, :t, :x])
    @test filter((p->p.x / p.t < 100), t, select=(:x, :t)) == table(["b", "c"], [0.05, 0.07], [1, 0], names=Symbol[:n, :t, :x])
    @test filter((p->p[2] / p[1] < 100), x, select=(:t, 3)) == ndsparse((n = ["b", "c"], t = [0.05, 0.07]), [1, 0])
    @test filter((:x => iseven, :t => (a->a > 0.01)), t) == table(["c"], [0.07], [0], names=Symbol[:n, :t, :x])
    @test filter((3 => iseven, :t => (a->a > 0.01)), x) == ndsparse((n = ["c"], t = [0.07]), [0])

end

@testset "asofjoin" begin
    x = ndsparse((["ko", "ko", "xrx", "xrx"], Date.(["2017-11-11", "2017-11-12", "2017-11-11", "2017-11-12"])), [1, 2, 3, 4])
    y = ndsparse((["ko", "ko", "xrx", "xrx"], Date.(["2017-11-12", "2017-11-13", "2017-11-10", "2017-11-13"])), [5, 6, 7, 8])
    @test asofjoin(x, y) == ndsparse((String["ko", "ko", "xrx", "xrx"], Date.(["2017-11-11", "2017-11-12", "2017-11-11", "2017-11-12"])), [1, 5, 7, 7])
    @test asofjoin(NDSparse([:msft,:ibm,:ge], [1,3,4], [100,200,150]),
                   NDSparse([:ibm,:msft,:msft,:ibm], [0,0,0,2], [100,99,101,98])) ==
                       NDSparse([:msft,:ibm,:ge], [1,3,4], [101, 98, 150])

    @test asofjoin(NDSparse([:AAPL, :IBM, :MSFT], [45, 512, 454], [63, 93, 54]),
                   NDSparse([:AAPL, :MSFT, :AAPL], [547,250,34], [88,77,30])) ==
                       NDSparse([:AAPL, :MSFT, :IBM], [45, 454, 512], [30, 77, 93])

    @test asofjoin(NDSparse([:aapl,:ibm,:msft,:msft],[1,1,1,3],[4,5,6,7]),
                   NDSparse([:aapl,:ibm,:msft],[0,0,0],[8,9,10])) ==
                       NDSparse([:aapl,:ibm,:msft,:msft],[1,1,1,3],[8,9,10,10])

end

@testset "merge" begin
    a = table([1, 3, 5], [1, 2, 3], names=[:x, :y], pkey=:x)
    b = table([2, 3, 4], [1, 2, 3], names=[:x, :y], pkey=:x)
    @test merge(a, b) == table([1, 2, 3, 3, 4, 5], [1, 1, 2, 2, 3, 3], names=Symbol[:x, :y])
    c = merge(a, select(b, (:y, :x)))
    @test merge(a, b) == c
    @test c.pkey == [1]
    d = reindex(b, :y)
    e = merge(a, d)
    @test Set(e) == Set(c)
    @test e.pkey == []
    a = ndsparse([1, 3, 5], [1, 2, 3])
    b = ndsparse([2, 3, 4], [1, 2, 3])
    @test merge(a, b) == ndsparse(([1, 2, 3, 4, 5],), [1, 1, 2, 3, 3])
    @test merge(a, b, agg=+) == ndsparse(([1, 2, 3, 4, 5],), [1, 1, 4, 3, 3])

    # merge optimization for pooled arrays
    x = begin
        x = [randstring(5) for idx in 1:128];
        for idx in 1:5
            x = vcat(x, x)
        end
        PooledArray(x)
    end;
    a = table(x, names=[:x]);
    b = table([randstring(5) for idx in 1:64], names=[:x]);
    c = merge(a, b);
    d = merge(b, a);

    @test isa(select(a, :x), PooledArray)
    @test !isa(select(b, :x), PooledArray)
    @test isa(select(c, :x), PooledArray)
    @test isa(select(d, :x), PooledArray)
end

@testset "broadcast" begin
    a = ndsparse(([1, 1, 2, 2], [1, 2, 1, 2]), [1, 2, 3, 4])
    b = ndsparse([1, 2], [1 / 1, 1 / 2])
    @test broadcast(identity, a) == a
    @test broadcast(sqrt, a) == ndsparse(([1, 1, 2, 2], [1, 2, 1, 2]), [1.0, √2, √3, √4])
    @test broadcast(sqrt, b) == ndsparse([1, 2], [1.0, √(1/2)])
    @test broadcast(*, a, b) == ndsparse(([1, 1, 2, 2], [1, 2, 1, 2]), [1.0, 2.0, 1.5, 2.0])
    @test a .* b == ndsparse(([1, 1, 2, 2], [1, 2, 1, 2]), [1.0, 2.0, 1.5, 2.0])
    @test broadcast(*, a, b, dimmap=(0, 1)) == ndsparse(([1, 1, 2, 2], [1, 2, 1, 2]), [1.0, 1.0, 3.0, 2.0])
end
using OnlineStats

    t = table([0.1, 0.5, 0.75], [0, 1, 2], names=[:t, :x])
    @test reduce(+, t, select=:t) == 1.35
    @test reduce(+, t, init = 1.0, select = :t) == 2.35
    @test reduce(((a, b)->(t = a.t + b.t, x = a.x + b.x)), t) == (t = 1.35, x = 3)
    @test value(reduce(Mean(), t, select=:t)) == 0.45
    y = reduce((min, max), t, select=:x)
    @test y.max == 2
    @test y.min == 0
    y = reduce((sum = (+), prod = (*)), t, select=:x)
    x = select(t, :x)
    @test y == (sum = sum(x), prod = prod(x))
    y = reduce((Mean(), Variance()), t, select=:t)
    @test value(y.Mean) == 0.45
    @test value(y.Variance) == 0.10749999999999998
    @test reduce((xsum = (:x => (+)), negtsum = ((:t => (-)) => (+))), t) == (xsum = 3, negtsum = -1.35)

@testset "groupreduce" begin
    a = table([1, 1, 2], [2, 3, 3], [4, 5, 2], pkey=[1,2])
    b = table(Columns(a=[1, 1, 2], b=[3, 2, 2], c=[4, 5, 2]), pkey=(1,2))

    @test groupreduce(min, a, select=3) == a
    @test groupreduce(min, b, select=3) == rename(b, :c => :min)
    @test_throws ArgumentError groupreduce(+, b, [:x, :y]) # issue JuliaDB.jl#100
    t = table([1, 1, 1, 2, 2, 2], [1, 1, 2, 2, 1, 1], [1, 2, 3, 4, 5, 6], names=[:x, :y, :z], pkey=(:x, :y))
    @test groupreduce(+, t, :x, select=:z) == table([1, 2], [6, 15], names=Symbol[:x, :+])
    @test groupreduce(((x, y)->if x isa Int
                           (y = x + y,)
                    else
                           (y = x.y + y,)
                    end), t, :x, select=:z) == table([1, 2], [6, 15], names=Symbol[:x, :y])
    @test groupreduce(:y => (+), t, :x, select=:z) == table([1, 2], [6, 15], names=Symbol[:x, :y])
    t = table([1, 1, 1, 2, 2, 2], [1, 1, 2, 2, 1, 1], [1, 2, 3, 4, 5, 6], names=[:x, :y, :z])
    @test groupreduce(+, t, :x, select=:z) == table([1, 2], [6, 15], names=Symbol[:x, :+])
    @test groupreduce(+, t, (:x, :y), select=:z) == table([1, 1, 2, 2], [1, 2, 1, 2], [3, 3, 11, 4], names=Symbol[:x, :y, :+])
    @test groupreduce((+, min, max), t, (:x, :y), select=:z) == table([1, 1, 2, 2], [1, 2, 1, 2], [3, 3, 11, 4], [1, 3, 5, 4], [2, 3, 6, 4], names=Symbol[:x, :y, :+, :min, :max])
    @test groupreduce((+, min, max), t, All(:x, :y), select=:z) == groupreduce((+, min, max), t, (:x, :y), select=:z)
    @test groupreduce((zsum = (+), zmin = min, zmax = max), t, (:x, :y), select=:z) == table([1, 1, 2, 2], [1, 2, 1, 2], [3, 3, 11, 4], [1, 3, 5, 4], [2, 3, 6, 4], names=Symbol[:x, :y, :zsum, :zmin, :zmax])
    @test groupreduce((xsum = :z => +, negysum = (:y => -) => +), t, :x) == table([1, 2], [6, 15], [-4, -4], names=Symbol[:x, :xsum, :negysum])
    t = NDSparse([1, 1, 1, 1, 2, 2],
                     [2, 2, 2, 3, 3, 3],
                     [1, 4, 3, 5, 2, 0], presorted=true)
end

@testset "groupby" begin
    x = Columns(a=[1, 1, 1, 1, 1, 1],
                b=[2, 2, 2, 3, 3, 3],
                c=[1, 4, 3, 5, 2, 0])

    a = table(x, pkey=[1,2], presorted=true)
    @test groupby(maximum, a, select=3) == table(Columns(a=[1, 1], b=[2, 3], maximum=[4, 5]))
    @test groupby(identity, a, Keys()) == groupby(identity, a)

    @test groupby((maximum, minimum), a, select=3) ==
                table(Columns(a=[1, 1], b=[2, 3],
                                  maximum=[4, 5], minimum=[1, 0]))

    @test groupby((max=maximum, min=minimum), a, select=3) ==
                table(Columns(a=[1, 1], b=[2, 3],
                                  max=[4, 5], min=[1, 0]))
    t = table([1, 1, 1, 2, 2, 2], [1, 1, 2, 2, 1, 1], [1, 2, 3, 4, 5, 6], names=[:x, :y, :z])
    @test groupby(mean, t, :x, select=:z) == table([1, 2], [2.0, 5.0], names=Symbol[:x, :mean])
    @test groupby(identity, t, (:x, :y), select=:z) == table([1, 1, 2, 2], [1, 2, 1, 2], [[1, 2], [3], [5, 6], [4]], names=Symbol[:x, :y, :identity])
    @test groupby(mean, t, (:x, :y), select=:z) == table([1, 1, 2, 2], [1, 2, 1, 2], [1.5, 3.0, 5.5, 4.0], names=Symbol[:x, :y, :mean])
    @test groupby((mean, std, var), t, :y, select=:z) == table([1, 2], [3.5, 3.5], [2.3804761428476167, 0.7071067811865476], [5.666666666666667, 0.5], names=Symbol[:y, :mean, :std, :var])
    @test groupby((q25 = (z->quantile(z, 0.25)), q50 = median, q75 = (z->quantile(z, 0.75))), t, :y, select=:z) == table([1, 2], [1.75, 3.25], [3.5, 3.5], [5.25, 3.75], names=Symbol[:y, :q25, :q50, :q75])
    @test groupby((xmean = (:z => mean), ystd = ((:y => (-)) => std)), t, :x) == table([1, 2], [2.0, 5.0], [0.5773502691896257, 0.5773502691896257], names=Symbol[:x, :xmean, :ystd])
    @test groupby((ncols = length∘colnames,), t, :x) == table([1, 2], [2, 2], names = [:x, :ncols], pkey = :x)
    func1 = (key, dd) -> key.x + length(dd)
    @test groupby((:s => func1, ), t, :x, usekey = true) == table([1, 2], [4, 5], names = [:x, :s], pkey = :x)
    func2 = (key, dd) -> key.x - length(dd)
    @test groupby((:s => func1, :d => func2), t, :x, usekey = true) == table([1, 2], [4, 5], [-2, -1], names = [:x, :s, :d], pkey = :x)
    @test groupby(:s => func1, t, :x, usekey = true) == table([1, 2], [4, 5], names = [:x, :s], pkey = :x)
    s(key, dd) = func1(key, dd)
    @test groupby(s, t, :x, usekey = true) == groupby((:s => func1, ), t, :x, usekey = true)
    s2(key, dd) = length(dd)
    @test groupby(s2, t, usekey = true) == (s2 = 6,)

    @test groupby(maximum,
                  NDSparse([1, 1, 1, 1, 1, 1],
                               [2, 2, 2, 3, 3, 3],
                               [1, 4, 3, 5, 2, 0], presorted=true)) ==
                  NDSparse([1, 1], [2, 3], [4, 5])

    @test groupby(maximum,
                  NDSparse([1, 1, 1, 1, 1, 1],
                               [2, 2, 2, 3, 3, 3],
                               [1, 4, 3, 5, 2, 0], presorted=true), select=(2,3)) ==
                  NDSparse([1, 1], [2, 3], [(2,4), (3,5)])

    @test groupby((maximum, minimum),
                  NDSparse([1, 1, 1, 1, 1, 1],
                               [2, 2, 2, 3, 3, 3],
                               [1, 4, 3, 5, 2, 0], presorted=true)) ==
                  NDSparse([1, 1], [2, 3], Columns(maximum=[4, 5], minimum=[1, 0]))

    @test groupby((maxv = maximum, minv = minimum), NDSparse([1, 1, 1, 1, 1, 1],
                                     [2, 2, 2, 3, 3, 3],
                                     [1, 4, 3, 5, 2, 0], presorted=true),) ==
                        NDSparse([1, 1], [2, 3], Columns(maxv=[4, 5], minv=[1, 0]))
end

    a = table([1,3,5], [2,2,2], names = [:x, :y])
    @test summarize((mean, std), a) ==
        (x_mean = 3.0, y_mean = 2.0, x_std = 2.0, y_std = 0.0)
    @test summarize((mean, std), a, select = :x) == (mean = 3.0, std = 2.0)
    @test summarize((m = mean, s = std), a) ==
        (x_m = 3.0, y_m = 2.0, x_s = 2.0, y_s = 0.0)
    b = table(["a","a","b","b"], [1,3,5,7], [2,2,2,2], names = [:x, :y, :z], pkey = :x)
    @test summarize(mean, b) ==
        table(["a","b"], [2.0,6.0], [2.0,2.0], names = [:x, :y, :z], pkey = :x)
    @test summarize((mean, std), a, stack = true) ==
        table([:x, :y], [3.0, 2.0], [2.0, 0.0], names = [:variable, :mean, :std])
    @test summarize(mean, a, stack = true) ==
        table([:x, :y], [3.0, 2.0], names = [:variable, :mean])
    @test summarize(mean, b, stack = true) ==
        table(["a","a","b","b"], [:y,:z,:y,:z], [2.0,2.0,6.0,2.0], names = [:x, :variable, :mean], pkey = :x)
    @test summarize((mean, sum), b, stack = true) ==
        table(["a","a","b","b"], [:y,:z,:y,:z], [2.0,2.0,6.0,2.0], [4,4,12,4],
        names = [:x, :variable, :mean, :sum], pkey = :x)

@testset "reshape" begin
    t = table(1:4, [1, 4, 9, 16], [1, 8, 27, 64], names = [:x, :xsquare, :xcube], pkey = :x)
    tsparse = IndexedTables._convert(NDSparse, t)
    long = stack(t; variable = :var, value = :val)
    longsparse = stack(tsparse; variable = :var, value = :val)
    @test long == table([1, 1, 2, 2, 3, 3, 4, 4],
                        [:xsquare, :xcube, :xsquare, :xcube, :xsquare, :xcube, :xsquare, :xcube],
                        [1, 1, 4, 8, 9, 27, 16, 64];
                        names = [:x, :var, :val], pkey = :x)
    @test longsparse == IndexedTables._convert(NDSparse, long)
    @test unstack(long; variable = :var, value = :val) == t
    @test unstack(longsparse; variable = :var, value = :val) == tsparse
    t1 = table([1, 1, 1, 2, 2], [:x, :x, :y, :x, :y], [1, 2, 3, 4, 5], names = [:x, :variable, :value])
    @test_throws Exception unstack(t1, :x)
    long2 = table([1, 1, 2, 2, 3, 3, 4, 4],
                  [2, 3, 2, 3, 2, 3, 2, 3],
                  [1, 1, 4, 8, 9, 27, 16, 64];
                  names = [:x, :var, :val], pkey = :x)
    res = table(1:4, [1, 4, 9, 16], [1, 8, 27, 64], names = [:x, Symbol(2), Symbol(3)], pkey = :x)
    @test unstack(long2; variable = :var, value = :val) == res
    long3 = table([1, 1, 2, 2, 3, 3, 4, 4],
                  [2, 3, 2, 3, 2, 3, 2, 3],
                  string.([1, 1, 4, 8, 9, 27, 16, 64]);
                  names = [:x, :var, :val], pkey = :x)
    res = table(1:4, string.([1, 4, 9, 16]), string.([1, 8, 27, 64]), names = [:x, Symbol(2), Symbol(3)], pkey = :x)
    @test unstack(long3; variable = :var, value = :val) == res
end

@testset "select" begin
    a = table([12,21,32], [52,41,34], [11,53,150], pkey=[1,2])
    b = table([12,23,32], [52,43,34], [56,13,10], pkey=[1,2])

    c = filter((1=>x->x<30, 2=>x->x>40), a)
    @test rows(c) == [(12,52,11), (21,41,53)]
    @test c.pkey == [1,2]

    c = select(a, (1, 2))
    @test c == table(column(a, 1), column(a, 2))
    @test c.pkey == [1,2]
    @test convertdim(NDSparse([1, 1, 1, 1, 1, 1],
                                  [0, 1, 2, 3, 4, 5],
                                  [1, 4, 3, 5, 2, 0], presorted=true), 2, x->div(x,3), vecagg=maximum) ==
                        NDSparse([1, 1], [0, 1], [4, 5])
end

@testset "conversions" begin
    A = rand(3,3)
    B = rand(3,3)
    C = rand(3,3)
    nA = convert(NDSparse, A)
    nB = convert(NDSparse, B)
    columns(nB.index)[1][:] .+= 3
    @test merge(nA,nB) == convert(NDSparse, vcat(A,B))
    nC = convert(NDSparse, C)
    columns(nC.index)[1][:] .+= 6
    @test merge(nA,nB,nC) == merge(nA,nC,nB) == convert(NDSparse, vcat(A,B,C))
    merge!(nA,nB)
    @test nA == convert(NDSparse, vcat(A,B))

    t1 = NDSparse(Columns(a=[1,1,2,2], b=[1,2,1,2]), [1,2,3,4])
    t2 = NDSparse(Columns(a=[0,1,2,3], b=[1,2,1,2]), [1,2,3,4])
    @test merge(t1, t2, agg=+) == NDSparse(Columns(a=[0,1,1,2,2,3], b=[1,1,2,1,2,2]), [1,1,4,6,4,4])
    @test merge(t1, t2, agg=nothing) == NDSparse(Columns(a=[0,1,1,1,2,2,2,3], b=[1,1,2,2,1,1,2,2]), [1,1,2,2,3,3,4,4])

    S = sparse(Diagonal(1:5))
    nd = convert(NDSparse, S)
    @test sum(S) == sum(nd) == sum(convert(NDSparse, Matrix(S)))

    @test sum(broadcast(+, 10, nd)) == (sum(nd) + 10*nnz(S))
    @test sum(broadcast(+, nd, 10)) == (sum(nd) + 10*nnz(S))
    @test sum(broadcast(+, nd, nd)) == 2*(sum(nd))

    nd[1:5,1:5] = 2
    @test nd == convert(NDSparse, sparse(Diagonal(fill(2, 5))))

    a = [1,2,3]
    b = ["a","b","c"]
    v = columnspair(a, b)
    @test convert(NDSparse, a, b) == convert(NDSparse, v) == ndsparse(v) == ndsparse(a, b)
end

@testset "mapslices" begin
    # scalar
    x=NDSparse(Columns(a=[1,1,1,2,2],b=PooledArray(["a","b","c","a","b"])),[1,2,3,4,5])
    t = mapslices(y->sum(y), x, (1,))
    @test t == NDSparse(Columns(b=["a","b","c"]), [5,7,3])

    A = [1]
    # shouldn't mutate input
    mapslices(x, [:a]) do slice
        NDSparse(Columns((A,)), A)
    end
    @test A == [1]

    # scalar
    r = Ref(0)
    t = mapslices(x, [:a]) do slice
        r[] += 1
        n = length(slice)
        NDSparse(Columns(c=[1:n;]), [r[] for i=1:n])
    end
    @test t == NDSparse(Columns(b=["a","a","b","b","c"], c=[1,2,1,2,1]), [1,1,2,2,3])

    # dedup names
    x=NDSparse(Columns(a=[1],b=[1]),Columns(c=[1]))
    t = mapslices(x,[:b]) do slice
            NDSparse(Columns(a=[2], c=[2]),
                         Columns(d=[1]))
    end
    @test t==NDSparse(Columns(a_1=[1], a_2=[2], c=[2]), Columns(d=[1]))

    # signleton slices
    x=NDSparse(Columns(([1,2],)),Columns(([1,2],)))
    @test_throws ErrorException mapslices(x,()) do slice
        true
    end
    t = mapslices(x,()) do slice
        @test slice == NDSparse(Columns(([1],)), Columns(([1],))) || slice == NDSparse(Columns(([2],)), Columns(([2],)))
        NDSparse(Columns(([1],)), ([1]))
    end
    @test t == NDSparse(Columns(([1,2], [1,1])), [1,1])

    x = NDSparse([1,1,1,2,2,2,3,3],[1,2,3,4,5,6,7,8],rand(8));
    y = mapslices(t -> (1, 2), x, 2)
    @test isa(y.data, Columns)
end

@testset "flatten" begin
    x = table([1,2], [[3,4], [5,6]], names=[:x, :y])
    @test flatten(x, 2) == table([1,1,2,2], [3,4,5,6], names=[:x,:y])
    @test flatten(x, 2) == flatten(x)

    x = table([1,2], [table([3,4],[5,6], names=[:a,:b]), table([7,8], [9,10], names=[:a,:b])], names=[:x, :y]);
    @test flatten(x, :y) == table([1,1,2,2], [3,4,7,8], [5,6,9,10], names=[:x,:a, :b])
    x = table([1,2], [(2i for i in 1:3 if isodd(i)), (5, nothing)])
    @test flatten(x) == table(([1,1,2,2], [2,6,5,nothing]))

    # test that isiterable_val output is known statically
    f(x) = IndexedTables.isiterable_val(x) ? x : nothing
    val1 = @inferred f([1, 2])
    @test val1 == [1, 2]
    val2 = @inferred f(:a)
    @test val2 === nothing

    t = table([1,1,2,2], [3,4,5,6], names=[:x,:y])
    @test groupby((:normy => x->Iterators.repeated(mean(x), length(x)),),
                  t, :x, select=:y, flatten=true) == table([1,1,2,2], [3.5,3.5,5.5,5.5], names=[:x, :normy])
    t=table([1,1,1,2,2,2], [1,1,2,1,1,2], [1,2,3,4,5,6], names=[:x,:y,:z], pkey=[1,2]);
    @test groupby(identity, t, (:x, :y), select=:z, flatten = true) == rename(t, :z => :identity)
    @test groupby(identity, t, (:x, :y), select=:z, flatten = true).pkey == [1,2]
    # If return type is non iterable, return the same as non flattened
    @test groupby(i -> (y = :y,), t, :x, flatten=true) == groupby(i -> (y = :y,), t, :x, flatten=false)
end

@testset "ColDict" begin
    t = table([1], names=[:x1])
    d = ColDict(t)
    d[:x2] = [2]
    d[:x3] = :x2 => -
    @test d[] == table([1], [2], [-2], names=[:x1,:x2,:x3])
end

@testset "shared data setindex!" begin
    x = ndsparse(([1,2], [3,4]), [5,6])
    reinterpret(UInt8, columns(x)[1]) # set isshared flag
    x[3,4] = 7
    flush!(x)
    @test x == ndsparse(([1,2,3],[3,4,4]), [5,6,7])
end


# A Using Case:
"""
yao> nbits(4)

yao> cc = begin
    2 => X
    3 => C, 1=>C, 4=>X
end

yao> nbits(6)

yao> dc = begin
    subroutine(1,5,2,3) cc
    3=>Rx(:θ)   # symbolic input
end

yao> reg = bit"000000"

yao> reg |> d

yao> p(rintln) reg

yao> p(rintln) c  # print summary

yao> s(how) reg

yao> c(ode) s reg

yao> m(easure!) [op] reg

yao> reset!(reg, 0)

yao> e(xpect) dc reg

yao> e(xpect)' dc reg

yao> f(idelity)' reg reg

yao> i(nspect) c     # show matrix

yao> l(istvars)    # list symbol table

yao> q(uit)
"""

CmdTable = Dict(
    # query
    'p'=>"print",
    'i'=>"inspect",
    # compute
    'e'=>"expect",
    'f'=>"fidelity",
    'r'=>"reset",
    'm'=>"measure",
    # interactive command
    'l'=>"listvars",
    'h'=>"help_repl",
)

struct YaoREPLState
    regs::Dict{Symbol,Any}
    blocks::Dict{Symbol,Any}
    parseinfo::YaoBlocks.ParseInfo
end
YaoREPLState() = YaoREPLState(Dict{Symbol,Any}(), Dict{Symbol,Any}(), YaoBlocks.ParseInfo(-1, ""))

function _checkfunc(f)
    sf = string(f)
    if sf in values(CmdTable)
        return f
    elseif sf[end] == '!' && sf[1:end-1] in values(CmdTable)
        return f
    else
        error("Expression `$f` can not be parsed to a known function!")
    end
end

"""
expand shortcuts.
"""
function yaorepl_parse(str)
    length(str) == 0 && return nothing
    str = strip(str, [' ', '\n'])
    if haskey(CmdTable, str[1]) && (length(str) == 1 || str[2] in [' ', '!', '\''])
        args = filter(x-> x!="", split(str, ' '))
        func = get(CmdTable, args[1][1], "noshortcut")
        str = func*args[1][2:end]*'('*join(args[2:end], ", ")*')'
    end
    Meta.parse(str)
end

yaorepl_trans(ex, state) = @match ex begin
    :($x = zero_state($nbit, nbatch=$nbatch)) => :($x = zero_state($(_checkbit(nbit)), nbatch=$(_checkbatch(nbatch))))
    :($x = zero_state($nbit)) => :($x = zero_state($(_checkbit(nbit))))
    :($x = rand_state($nbit, nbatch=$nbatch)) => :($x = rand_state($(_checkbit(nbit)), nbatch=$(_checkbatch(nbatch))))
    :($x = rand_state($nbit)) => :($x = rand_state($(_checkbit(nbit))))
    :($x = [$(args...)]) => :($x = ArrayReg(ComplexF64[$(Float64.(args)...)]))
    :($x = @bit_str $line $val) => :($x = ArrayReg(@bit_str $(_checkbit(val))))
    :($x = $ex) => begin
        blk = YaoBlocks.parse_ex(ex, state.parseinfo)
        :($x = $blk)
    end
    :(expect($a, $b=>$c)) => :(expect($(_checkop(a)), $(_checkreg(b))=>$(_checkop(c))))
    :(expect'($a, $b=>$c)) => :(expect'($(_checkop(a)), $(_checkreg(b))=>$(_checkop(c))))
    :(expect($a, $b)) => :(expect($(_checkop(a)), $(_checkreg(b))))
    :(expect'($a, $b)) => :(expect'($(_checkop(a)), $(_checkreg(b))))
    :(fidelity($a=>$oa, $b=>$ob)) => :(fidelity($(_checkreg(a))=>$(_checkop(oa)), $(_checkreg(b))=>$(_checkop(ob))))
    :(fidelity'($a=>$oa, $b=>$ob)) => :(fidelity'($(_checkreg(a))=>$(_checkop(oa)), $(_checkreg(b))=>$(_checkop(ob))))
    :(fidelity($a, $b)) => :(fidelity($(_checkreg(a)), $(_checkreg(b))))
    :(fidelity'($a, $b)) => :(fidelity'($(_checkreg(a)), $(_checkreg(b))))
    :(measure($a)) => :(measure($(_checkreg(a))))
    :(measure($a, nshots=$nshots)) => :(measure($(_checkreg(a)), nshots=$(_checkshots(nshots))))
    :(measure($op, $a)) => :(measure($(_checkop(op)), $(_checkreg(a))))
    :(measure($op, $a, nshots=$nshots)) => :(measure($(_checkop(op)), $(_checkreg(a)), nshots=$(_checkshots(nshots))))
    :(measure!($a)) => :(measure!($(_checkreg(a))))
    :(measure!($op, $a)) => :(measure!($(_checkop(op)), $(_checkreg(a))))
    :(print($a)) => :(print2str($(_checkreg(a))))
    :(inspect($a)) => :(inspect2str($(_checkreg(a))))
    :($x |> $y) => :(apply!($x, $y))

    :(help_repl()) => :(help_repl())
    :(listvars()) => :(listvars($state))
    :(nbits($n)) => :(setnbits!($n, $state))
    :(nbits()) => :(getnbits($state))
    _ => error("Expression `$ex` can not be translated!")
end

function yaorepl_execute(ex, state)
    @match ex begin
        :($x = $expr) => begin
            var = eval(expr)
            if var isa AbstractRegister
                state.regs[Symbol(x)] = var
            elseif var isa AbstractBlock
                state.blocks[Symbol(x)] = var
            else
                error("incorrect assignment with return value $var.")
            end
            "done"
        end
        :($f($(args...))) => begin
            nargs = [query_state(state, arg) for arg in args]
            res = eval(:($(_checkfunc(f))($(nargs...))))
            if res isa AbstractString
                res
            elseif res isa Nothing
                "done"
            else
                io = IOBuffer()
                Base.show(io, "text/plain", res)
                String(take!(io))
            end
        end
    end
end

function query_state(state::YaoREPLState, arg)
    @match arg begin
        :($a => $b) => :($(query_state(state, a)) => $(query_state(state, b)))
        ::Symbol => begin
            if haskey(state.regs, arg)
                state.regs[arg]
            elseif haskey(state.blocks, arg)
                state.blocks[arg]
            else
                arg
            end
        end
        _ => arg
    end
end

function listvars(state)
    regstr = ["$k ($(nqubits(v)))" for (k,v) in state.regs]
    blkstr = ["$k ($(nqubits(v)))" for (k,v) in state.blocks]
    join(["current nbits = $(state.parseinfo.nbit)\n", "=== registers ===", regstr..., "\n=== blocks ===", blkstr...], "\n")
end

function help_repl()
"""Guide:
=== Basic ===
q: quit
help_repl() | h: print this manual
c cmd: show the julia code of an expression
listvars() | l: list defined register/block table
nbits(n): set the number of qubits to `n` (used in defining a block)
nbits(): show the number of qubits

=== Define a Register ===
reg = rand_state(n): random `n`-qubit state
reg = zero_state(n): zero state
reg = bit"01011": product state

=== Operations ===
expect(op, reg): compute <reg|op|reg>
expect'(op, reg[=>c]): compute ∂<reg|c'*op*c|reg>/∂reg and ∂<reg|c'*op*c|reg>/∂c
fidelity(reg1, reg2): compute <reg1|reg2>
fidelity'(reg1[=>c1], reg2[=>c2]): compute ∂<reg1|c1'*c2|reg2>/∂reg1, ∂<reg1|c1*c2|reg2>/∂c1 ...
reg |> op: apply `op` on `reg`
measure!([op, ]reg) | m! [op] reg: measure `op` inplace.
measure([op, ]reg, [nshots=n]) | m! [op] reg [nshots=n]: measure `op` without collapsing `reg`.

* Note: `n` is integer, `cmd` is command, `reg` is register, `op` is operator or gate (block)."""
end

function yaorepl_handler(str::AbstractString, state)
    if length(strip(str)) >=2 && strip(str)[1:2] == "c "
        string(yaorepl_trans(yaorepl_parse(str[3:end]), state))
    else
        yaorepl_execute(yaorepl_trans(yaorepl_parse(str), state), state)
    end
end


push!(LOAD_PATH, dirname(@__FILE__))
@everywhere log(1.0) # Workaround for https://github.com/JuliaLang/julia/issues/18465
reload("Analysis")
Analysis.compute_permpval()
Analysis.compute_permpval_eachisi()
Analysis.compute_latency()
Analysis.compute_alignedinfoisi()
Analysis.compute_permpval_isiwin()
Analysis.compute_permpval_maintenance()
Analysis.compute_modulation_maintenance()
Analysis.compute_modulation_presentation()
Analysis.compute_maintenance_topstim_holdout()
Analysis.compute_maintenance_topstim_correct()
Analysis.compute_alignedinfoisi(pos1=true)