## This file contains the exchange and exch_UV functions
# along with grid-specific methods (exch_T_N.jl, etc.)

## User Front Ends

"""
    exchange(fld::MeshArray)

Exchange / transfer data between neighboring arrays. Other methods are

    exchange(fld::MeshArray,N::Integer)
    exchange(u::MeshArray,v::MeshArray)
    exchange(u::MeshArray,v::MeshArray,N::Integer)
"""
function exchange(fld::MeshArray)
  FLD=exch_T_N(fld,1);
end

function exchange(fld::MeshArray,N::Integer)
  FLD=exch_T_N(fld,N);
end

function exchange(u::MeshArray,v::MeshArray)
  (uex,vex)=exch_UV_N(u,v,1);
end

function exchange(u::MeshArray,v::MeshArray,N::Integer)
  (uex,vex)=exch_UV_N(u,v,N);
end

## dispatch over grid types

#note: the "cs" implementation covers both cs and llc

function exch_T_N(fld,N)

if fld.grid.class=="llc"
  FLD=exch_T_N_cs(fld,N)
elseif fld.grid.class=="cs"
  FLD=exch_T_N_cs(fld,N)
elseif fld.grid.class=="ll"
  FLD=exch_T_N_ll(fld,N)
else
  error("unknown grTopo case")
end

return FLD

end

function exch_UV_N(u,v,N)

if u.grid.class=="llc"
  (uex,vex)=exch_UV_N_cs(u,v,N)
elseif u.grid.class=="cs"
  (uex,vex)=exch_UV_N_cs(u,v,N)
elseif u.grid.class=="ll"
  (uex,vex)=exch_UV_N_ll(u,v,N)
else
  error("unknown grTopo case")
end

return uex,vex

end

function exch_UV(u,v)

if u.grid.class=="llc"
  (uex,vex)=exch_UV_cs(u,v)
elseif u.grid.class=="cs"
  (uex,vex)=exch_UV_cs(u,v)
elseif u.grid.class=="ll"
  (uex,vex)=exch_UV_ll(u,v)
else
  error("unknown grTopo case")
end

return uex,vex

end

## Grid-specific implementations: ll grid case

function exch_T_N_ll(fld::MeshArray,N::Integer)

fillval=0.0

#step 1

s=size.(fld.f);
FLD=similar(fld);
FLD.f[1]=fill(fillval,s[1].+2N);
@views FLD.f[1][N+1:N+s[1][1],N+1:N+s[1][2]]=fld.f[1];

#step 2

iW=(s[1][1]-N+1:s[1][1],1:s[1][2]);
iE=(1:N,1:s[1][2]);
jW=(1:N,N+1:N+s[1][2]);
jE=(N+1+s[1][1]:2N+s[1][1],N+1:N+s[1][2]);
FLD.f[1][jW[1],jW[2]]=view(fld.f[1],iW[1],iW[2])
FLD.f[1][jE[1],jE[2]]=view(fld.f[1],iE[1],iE[2])

return FLD

end

##

function exch_UV_N_ll(fldU,fldV,N);

FLDU=exch_T_N_ll(fldU,N);
FLDV=exch_T_N_ll(fldV,N);

return FLDU,FLDV

end

##

function exch_UV_ll(fldU,fldV);

fillval=0.0

#step 1

s=size.(fldU.f);
FLDU=similar(fldU);
FLDV=similar(fldV);

FLDU.f[1]=fill(fillval,s[1][1]+1,s[1][2]);
FLDV.f[1]=fill(fillval,s[1][1],s[1][2]+1);
@views FLDU.f[1][1:s[1][1],1:s[1][2]]=fldU.f[1];
@views FLDV.f[1][1:s[1][1],1:s[1][2]]=fldV.f[1];

#step 2

FLDU.f[1][s[1][1]+1,1:s[1][2]]=view(fldU.f[1],1,1:s[1][2])

return FLDU,FLDV

end

## Grid-specific implementations: cs & llc grid case

#note: the "cs" implementation covers both cs and llc

function exch_T_N_cs(fld::MeshArray,N::Integer)

fillval=0.0

#step 1

s=size.(fld.f)
nf=fld.grid.nFaces
nf==5 ? s=vcat(s,s[3]) : nothing
tp=fld.grid.class
FLD=similar(fld)

for i=1:nf; FLD.f[i]=fill(fillval,s[i].+2N); end;
#code below yields strange, seemingly incorrect results:
#for i=1:nf; FLD.f[i]=Array{eltype(fld.f[i])}(undef,s[i].+2N); end;

#all versions below yield same @time and memory (despite diff in allocs)
for i=1:nf;
# FLD.f[i][N+1:end-N,N+1:end-N]=fld.f[i];
 @views FLD.f[i][N+1:N+s[i][1],N+1:N+s[i][2]]=fld.f[i];
end;

#step 2

(ovfW,ovfE,ovfS,ovfN,evfW,evfE,evfS,evfN)=exch_cs_viewfunctions();

for a=1:nf
(jW, jE, jS, jN)=exch_cs_target(s[a],N)
(aW,aE,aS,aN,iW,iE,iS,iN)=exch_cs_sources(a,s,N)
if !iseven(a)
 aW <= nf ? FLD.f[a][jW[1],jW[2]]=ovfW(fld.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLD.f[a][jE[1],jE[2]]=ovfE(fld.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLD.f[a][jS[1],jS[2]]=ovfS(fld.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLD.f[a][jN[1],jN[2]]=ovfN(fld.f[aN],iN[1],iN[2]) : nothing
else
 aW <= nf ? FLD.f[a][jW[1],jW[2]]=evfW(fld.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLD.f[a][jE[1],jE[2]]=evfE(fld.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLD.f[a][jS[1],jS[2]]=evfS(fld.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLD.f[a][jN[1],jN[2]]=evfN(fld.f[aN],iN[1],iN[2]) : nothing
end
end

return FLD

end

##

function exch_UV_N_cs(fldU::MeshArray,fldV::MeshArray,N::Integer)

fillval=0.0

#step 1

s=size.(fldU.f)
nf=fldU.grid.nFaces
nf==5 ? s=vcat(s,s[3]) : nothing
tp=fldU.grid.class
FLDU=similar(fldU)
FLDV=similar(fldV)

for i=1:nf;
 FLDU.f[i]=fill(fillval,s[i].+2N);
 FLDV.f[i]=fill(fillval,s[i].+2N);
 @views FLDU.f[i][N+1:N+s[i][1],N+1:N+s[i][2]]=fldU.f[i];
 @views FLDV.f[i][N+1:N+s[i][1],N+1:N+s[i][2]]=fldV.f[i];
end;

#step 2

(ovfW,ovfE,ovfS,ovfN,evfW,evfE,evfS,evfN)=exch_cs_viewfunctions();

for a=1:nf
(jW, jE, jS, jN)=exch_cs_target(s[a],N)
(aW,aE,aS,aN,iW,iE,iS,iN)=exch_cs_sources(a,s,N)
if !iseven(a)
 aW <= nf ? FLDU.f[a][jW[1],jW[2]]=ovfW(fldV.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLDU.f[a][jE[1],jE[2]]=ovfE(fldU.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLDU.f[a][jS[1],jS[2]]=ovfS(fldU.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLDU.f[a][jN[1].+1,jN[2]]=-ovfN(fldV.f[aN],iN[1],iN[2]) : nothing
 aW <= nf ? FLDV.f[a][jW[1],jW[2].+1]=-ovfW(fldU.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLDV.f[a][jE[1],jE[2]]=ovfE(fldV.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLDV.f[a][jS[1],jS[2]]=ovfS(fldV.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLDV.f[a][jN[1],jN[2]]=ovfN(fldU.f[aN],iN[1],iN[2]) : nothing
else
 aW <= nf ? FLDU.f[a][jW[1],jW[2]]=evfW(fldU.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLDU.f[a][jE[1],jE[2]]=evfE(fldV.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLDU.f[a][jS[1].+1,jS[2]]=-evfS(fldV.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLDU.f[a][jN[1],jN[2]]=evfN(fldU.f[aN],iN[1],iN[2]) : nothing
 aW <= nf ? FLDV.f[a][jW[1],jW[2]]=evfW(fldV.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLDV.f[a][jE[1],jE[2].+1]=-evfE(fldU.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLDV.f[a][jS[1],jS[2]]=evfS(fldU.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLDV.f[a][jN[1],jN[2]]=evfN(fldV.f[aN],iN[1],iN[2]) : nothing
end
end

return FLDU,FLDV

end

##

function exch_UV_cs(fldU::MeshArray,fldV::MeshArray)

fillval=0.0

#step 1

s=size.(fldU.f)
nf=fldU.grid.nFaces
nf==5 ? s=vcat(s,s[3]) : nothing
tp=fldU.grid.class
FLDU=similar(fldU)
FLDV=similar(fldV)

for i=1:nf
  FLDU.f[i]=fill(fillval,s[i][1]+1,s[i][2]);
  FLDV.f[i]=fill(fillval,s[i][1],s[i][2]+1);
  @views FLDU.f[i][1:s[i][1],1:s[i][2]]=fldU.f[i];
  @views FLDV.f[i][1:s[i][1],1:s[i][2]]=fldV.f[i];
end

 #step 2

(ovfW,ovfE,ovfS,ovfN,evfW,evfE,evfS,evfN)=exch_cs_viewfunctions();

for a=1:nf
(jW, jE, jS, jN)=exch_cs_target(s[a],1)
(aW,aE,aS,aN,iW,iE,iS,iN)=exch_cs_sources(a,s,1)
if !iseven(a)
 aE <= nf ? FLDU.f[a][jE[1].-1,jE[2].-1]=ovfE(fldU.f[aE],iE[1],iE[2]) : nothing
 aN <= nf ? FLDV.f[a][jN[1].-1,jN[2].-1]=ovfN(fldU.f[aN],iN[1],iN[2]) : nothing
else
 aE <= nf ? FLDU.f[a][jE[1].-1,jE[2].-1]=evfE(fldV.f[aE],iE[1],iE[2]) : nothing
 aN <= nf ? FLDV.f[a][jN[1].-1,jN[2].-1]=evfN(fldV.f[aN],iN[1],iN[2]) : nothing
end
end

return FLDU,FLDV

end

## Convenience functions used in the cs & llc case

function exch_cs_target(sa::Tuple{Int64,Int64},N::Integer)

    #target array indices
    jW=(1:N,N+1:N+sa[2]);
    jE=(N+1+sa[1]:2N+sa[1],N+1:N+sa[2]);
    jS=(N+1:N+sa[1],1:N);
    jN=(N+1:N+sa[1],N+1+sa[2]:2N+sa[2]);

    return jW, jE, jS, jN

end

function exch_cs_sources(a::Integer,s::Array{Tuple{Int64,Int64},1},N::Integer)

#source array IDs
aW=0; aE=0; aS=0; aN=0;
if a==1;     aW=5; aE=2; aS=6; aN=3;
elseif a==2; aW=1; aE=4; aS=6; aN=3;
elseif a==3; aW=1; aE=4; aS=2; aN=5;
elseif a==4; aW=3; aE=6; aS=2; aN=5;
elseif a==5; aW=3; aE=6; aS=4; aN=1;
elseif a==6; aW=5; aE=2; aS=4; aN=1;
else; error("Array index is out of bounds.");
end;

if !iseven(a)
    #source array indices
    iW=(1:s[aW][1],s[aW][2]-N+1:s[aW][2]);
    iE=(1:N,1:s[aE][2]);
    iS=(1:s[aS][1],s[aS][2]-N+1:s[aS][2]);
    iN=(1:N,1:s[aN][2]);
else
    #source array indices
    iW=(s[aW][1]-N+1:s[aW][1],1:s[aW][2]);
    iE=(1:s[aE][1],1:N);
    iS=(s[aS][1]-N+1:s[aS][1],1:s[aS][2]);
    iN=(1:s[aN][1],1:N);
end

return aW,aE,aS,aN,iW,iE,iS,iN
end

function exch_cs_viewfunctions()
#view functions for odd numbered arrays
ovfW(x,i,j)=PermutedDimsArray(view(x,reverse(i),j),(2,1))
ovfE(x,i,j)=view(x,i,j)
ovfS(x,i,j)=view(x,i,j)
ovfN(x,i,j)=PermutedDimsArray(view(x,i,reverse(j)),(2,1))
#view functions for even numbered arrays
evfW(x,i,j)=view(x,i,j)
evfE(x,i,j)=PermutedDimsArray(view(x,reverse(i),j),(2,1))
evfS(x,i,j)=PermutedDimsArray(view(x,i,reverse(j)),(2,1))
evfN(x,i,j)=view(x,i,j)
#
return ovfW,ovfE,ovfS,ovfN,evfW,evfE,evfS,evfN
end

##


using IProfile

@iprofile begin
function f1(x::Int)
    k = 5
    l = 12
    y = sum(1:1000)
    for j = 1:100000
        z = j^2
    end
    if z > 0
        z = z-1
    elseif z == 0
        z = 1000
    else
        z = z+1
    end
    return x+1
end
function f1(x::Float64)
    return x+2
end

function f1{T}(x::T)
    return x+5
end

unused_top_level_var = 2

# Test ternary operators with trivial return values. See issue #17.
function f2()
    rand()<0.5 ? 1 : 0
end

end #@iprofile begin

f1(7)
f1(7.3)
println("Less than 0.5: ", f2())

for i = 1:200
    f1(Int16(i))
end

@iprofile report
@iprofile clear


# Contains the configuration module, holding global settings for the package

"Configuration options"
module config
# Data directories
"The directory in which .jld data files are stored"
const datadir = normpath(joinpath(dirname(@__FILE__), "..", "data"))
"The directory in which raw data are stored in text format"
const rawdata = joinpath(datadir, "raw")
"The directory in which test data are stored"
const testdata = normpath(joinpath(dirname(@__FILE__), "..", "test", "data"))

# Gridded EOS parameters
"Grid resolution for making gridded equations of state"
const grid_resolution = 256
"Minimum pressure used in constructing the full EOS [Pa]"
const Pmin = 10^(4.5)
"Maximum pressure used in constructing the full EOS [Pa]"
const Pmax = 1e14
"Minimum temperature used in constructing the full EOS [K]"
const Tmin = 275
"Maximum temperature used in constructing the full EOS [K]"
const Tmax = 25000

end # module config


##########
## join ##
##########

function joinSortedIdx_inner(td1::AbstractTimedata,
                             td2::AbstractTimedata)
    ## only works for sorted and increasing index!

    if typeof(idx(td1)) != typeof(idx(td2))
        error("both indices must be of same type to join")
    end

    ## find common dates
    equalIds = findCommonDates(idx(td1), idx(td2))

    df = [td1.vals[equalIds[:, 1], :] td2.vals[equalIds[:, 2], :]]
    td = Timedata(df, idx(td1)[equalIds[:, 1]])
end

function joinSortedIdx_outer(td1::AbstractTimedata,
                             td2::AbstractTimedata)
    ## only works for sorted and increasing index!

    if typeof(idx(td1)) != typeof(idx(td2))
        error("both indices must be of same type to join")
    end

    equalIds = findAllOccurringDates(idx(td1), idx(td2))
    nObs = size(equalIds, 1)
    nVars1 = size(td1, 2)
    nVars2 = size(td2, 2)

    ## init output DataFrame with NAs
    df = DataFrame([eltypes(td1.vals), eltypes(td2.vals)],
                   [names(td1.vals), names(td2.vals)],
                   nObs)
    idxs = Array(eltype(idx(td1)), nObs)

    for ii=1:nObs
        if isna(equalIds[ii, 1]) & !isna(equalIds[ii, 2])
            rowInd = equalIds[ii, 2]
            idxs[ii] = idx(td2)[rowInd]
            for jj=1:nVars1
                df[ii, jj] = NA
            end
            for jj=(nVars1+1):(nVars1+nVars2)
                df[ii, jj] = td2.vals[rowInd, (jj-nVars1)]
            end
        elseif !isna(equalIds[ii, 1]) & isna(equalIds[ii, 2])
            rowInd = equalIds[ii, 1]
            idxs[ii] = idx(td1)[rowInd]
            for jj=1:nVars1
                df[ii, jj] = td1.vals[rowInd, jj]
            end
            for jj=(nVars1+1):(nVars1+nVars2)
                df[ii, jj] = NA
            end
        elseif !isna(equalIds[ii, 1]) & !isna(equalIds[ii, 2])
            rowInd1 = equalIds[ii, 1]
            rowInd2 = equalIds[ii, 2]
            idxs[ii] = idx(td1)[rowInd1]
            for jj=1:nVars1
                df[ii, jj] = td1.vals[rowInd1, jj]
            end
            for jj=(nVars1+1):(nVars1+nVars2)
                df[ii, jj] = td2.vals[rowInd2, (jj-nVars1)]
            end
        end
    end

    td = Timedata(df, idxs)
end

function joinSortedIdx_right(td1::AbstractTimedata,
                             td2::AbstractTimedata)

    ## only works for sorted and increasing index!
    td = joinSortedIdx_left(td2, td1)
    nVars2 = size(td2, 2)
    td2 = [td[:, (nVars2+1):end] td[:, 1:nVars2]]
end

function joinSortedIdx_left(td1::AbstractTimedata,
                            td2::AbstractTimedata)
    ## only works for sorted and increasing index!

    if typeof(idx(td1)) != typeof(idx(td2))
        error("both indices must be of same type to join")
    end

    nIds1 = size(td1, 1)

    ## find dates of 1 in 2
    equalIds = findDatesOfLeftInRight(idx(td1), idx(td2))

    ## get number of variables
    (nVars1, nVars2) = (size(td1, 2), size(td2, 2))

    ## init output DataFrame with NAs
    df = DataFrame([eltypes(td1.vals), eltypes(td2.vals)],
                   [names(td1.vals), names(td2.vals)],
                   nIds1)

    ## successively fill values: run through rows and columns
    for ii=1:nIds1
        for jj=1:nVars1
            df[ii, jj] = td1.vals[ii, jj] # values of 1 must be filled
                                        # always
        end

        if !isna(equalIds[ii])
            ## append values of 2
            for jj=1:nVars2
                df[ii, (nVars1 + jj)] = td2.vals[equalIds[ii], jj]
            end
        end
    end
    tdJoint = Timedata(df, idx(td1))        # contains NAs
end

##################################
## sorted join helper functions ##
##################################

function findAllOccurringDates(idx1, idx2)
    (nIds1, nIds2) = (length(idx1), length(idx2))

    ## some preallocation
    equalIds = DataArray(Int, max(nIds1, nIds2), 2)
    ii = 1; jj = 1; nDatesFound = 1

    ## find none of both indices is completely used
    while (ii <= nIds1) & (jj <= nIds2)
        ## test for equality
        if idx1[ii] == idx2[jj]
            ## note down respective index
            equalIds = addValueToDataArray!(equalIds, @data([ii jj]),
                                 nDatesFound)
            ii += 1; jj += 1; nDatesFound += 1

            ## if no equality: increase smaller index
        elseif idx1[ii] < idx2[jj]
            equalIds = addValueToDataArray!(equalIds, @data([ii NA]),
                                 nDatesFound)
            ii += 1; nDatesFound += 1
        elseif idx1[ii] > idx2[jj]
            equalIds = addValueToDataArray!(equalIds, @data([NA jj]),
                                nDatesFound)
            jj += 1; nDatesFound += 1

        end
    end

    ## append rest
    if jj > nIds2
        while ii <= nIds1
            equalIds = addValueToDataArray!(equalIds, @data([ii NA]),
                                nDatesFound)
            ii += 1; nDatesFound += 1
        end
    elseif ii > nIds1
        while jj <= nIds2
            equalIds = addValueToDataArray!(equalIds, @data([NA jj]),
                                nDatesFound)
            jj += 1; nDatesFound += 1
        end
    end
    return equalIds
end

function addValueToDataArray!(equalIds, DArow, nDatesFound)
    if nDatesFound > size(equalIds, 1)
        equalIds = vcat(equalIds, DArow)
    else
        equalIds[nDatesFound, :] = DArow
    end
    equalIds
end

function getIndices(equalIds, idx1, idx2)
    nObs = size(equalIds, 1)
    ids = Array(eltype(idx1), nObs)

    for ii=1:nObs
        if !isna(equalIds[ii, 1])
            ids[ii] = idx1[equalIds[ii, 1]]
        else
            ids[ii] = idx2[equalIds[ii, 2]]
        end
    end
    ids
end

function getIndices(equalIds, idx)
    nObs = size(equalIds, 1)
    ids = DataArray(eltype(idx), nObs)

    for ii=1:nObs
        if !isna(equalIds[ii])
            ids[ii] = idx[equalIds[ii]]
        end
    end
    ids
end

function findDatesOfLeftInRight(idx1, idx2)
    ## for each date in idx1, find its entry in idx2. If date does not
    ## exist, mark it with NA.

    nIds1 = length(idx1)
    nIds2 = length(idx2)

    ## running variables
    ii = 1
    jj = 1

    ## preallocation with NAs
    equalIds = DataArray(typeof(ii), nIds1)

    ## find common dates
    while ii <= nIds1
        ## test for equality
        if idx1[ii] == idx2[jj]
            ## note down respective index
            equalIds[ii] = jj
            ii += 1
            jj += 1
            if jj > nIds2
                break
            end
            ## if no equality: increase smaller index
        elseif idx1[ii] < idx2[jj]
            equalIds[ii] = NA
            ii += 1
        elseif idx1[ii] > idx2[jj]
            jj += 1
            if jj > nIds2
                break
            end
        end
    end
    return equalIds
end

function findCommonDates(idx1, idx2)

    nIds1 = length(idx1)
    nIds2 = length(idx2)

    ## running variables
    ii = 1
    jj = 1

    ## preallocation with NAs
    equalIds = [[] []]

    ## find common dates
    while ii <= nIds1
        ## test for equality
        if idx1[ii] == idx2[jj]
            ## note down respective index
            equalIds = vcat(equalIds, [ii jj])
            ii += 1
            jj += 1
            if jj > nIds2
                break
            end
            ## if no equality: increase smaller index
        elseif idx1[ii] < idx2[jj]
            ii += 1
        elseif idx1[ii] > idx2[jj]
            jj += 1
            if jj > nIds2
                break
            end
        end
    end
    return equalIds
end


using PyCall
using PyPlot
@pyimport matplotlib.animation as anim

#surf(disc.disc_data.flow.xi,t,yout[:,1:N])

plt = PyPlot
# First set up the figure, the axis, and the plot element we want to animate
fig = plt.figure()
#ax = plt.axes(xlim=(0, 0.5), ylim=(0.00323439, 0.00323441))
ax = plt.axes(xlim=(0, 0.5), ylim=(0.000, 0.005))
global lines = [ax[:plot]([], [], lw=2)[1];ax[:plot]([], [], lw=2)[1]]

# initialization function: plot the background of each frame
function init()
    global lines
	for line = lines
		line[:set_data]([], [])
	end
	plt.legend(["Nonlinear", "Linear"])
    return (lines,None)
end

# animation function.  This is called sequentially
function animate(i)
	k = 1
	global lines
	for line = lines
		x = [xi]
		if k == 1
			y = [yout[i*4,N+1:end-1][:]]
		else
			y = [youtlin[i*4,N+1:end-1][:]]
		end
		line[:set_data](x, y)
		#line = plt.plot(x,y)
		k += 1
	end
    return (lines,None)
end

# call the animator.  blit=True means only re-draw the parts that have changed.
myanim = anim.FuncAnimation(fig, animate, init_func=init,
                               frames=2500, interval=1);

#myanim[:save]("PyPlots-sinplot.mp4", extra_args=["-vcodec", "libx264", "-pix_fmt", "yuv420p"])


using MVApp

data_folder = "data/0429"
main(data_folder,97,1,10)
main(data_folder,97,-1,-1)

# main(data_folder,95,1,10)
push!(ARGS, data_folder, "1","10", "97", "56")
MVApp.julia_main()


@testset "schemas" begin

    using StatsModels: schema, apply_schema, FullRank

    @testset "no-op apply_schema" begin
        f = @formula(y ~ 1 + a + b + c + b&c)
        df = (y = rand(9), a = 1:9, b = rand(9), c = repeat(["d","e","f"], 3))
        f = apply_schema(f, schema(f, df))
        @test f == apply_schema(f, schema(f, df))
    end

    @testset "lonely term in a tuple" begin
        d = (a = [1,1],)
        @test apply_schema(ConstantTerm(1), schema(d)) == apply_schema((ConstantTerm(1),), schema(d))
        @test apply_schema(Term(:a), schema(d)) == apply_schema((Term(:a),), schema(d))
    end

    @testset "hints" begin
        f = @formula(y ~ 1 + a)
        d = (y = rand(10), a = repeat([1,2], outer=2))

        sch = schema(f, d)
        @test sch[term(:a)] isa ContinuousTerm

        sch1 = schema(f, d, Dict(:a => CategoricalTerm))
        @test sch1[term(:a)] isa CategoricalTerm{DummyCoding}
        f1 = apply_schema(f, sch1)
        @test f1.rhs.terms[end] == sch1[term(:a)]

        sch2 = schema(f, d, Dict(:a => DummyCoding()))
        @test sch2[term(:a)] isa CategoricalTerm{DummyCoding}
        f2 = apply_schema(f, sch2)
        @test f2.rhs.terms[end] == sch2[term(:a)]

        hint = deepcopy(sch2[term(:a)])
        sch3 = schema(f, d, Dict(:a => hint))
        # if an <:AbstractTerm is supplied as hint, it's included as is
        @test sch3[term(:a)] === hint !== sch2[term(:a)]
        f3 = apply_schema(f, sch3)
        @test f3.rhs.terms[end] === hint

    end

    @testset "has_schema" begin
        using StatsModels: has_schema

        d = (y = rand(10), a = rand(10), b = repeat([:a, :b], 5))

        f = @formula(y ~ a*b)
        @test !has_schema(f)
        @test !has_schema(f.rhs)
        @test !has_schema(StatsModels.collect_matrix_terms(f.rhs))

        ff = apply_schema(f, schema(d))
        @test has_schema(ff)
        @test has_schema(ff.rhs)
        @test has_schema(StatsModels.collect_matrix_terms(ff.rhs))

        sch = schema(d)
        a, b = term.((:a, :b))
        @test !has_schema(a)
        @test has_schema(sch[a])
        @test !has_schema(b)
        @test has_schema(sch[b])

        @test !has_schema(a & b)
        @test !has_schema(a & sch[b])
        @test !has_schema(sch[a] & a)
        @test has_schema(sch[a] & sch[b])

    end

    @testset "nice errors" begin
        d = (yyy = rand(10), aaa = rand(10), bbb = repeat([:a, :b], 5))
        @test_throws ArgumentError("There isn't a variable called 'aa' in your data; the nearest names appear to be: aaa") concrete_term(Term(:aa), d, nothing)
        @test_throws ArgumentError("There isn't a variable called 'aab' in your data; the nearest names appear to be: aaa, bbb") concrete_term(Term(:aab), d, nothing)

        @test_throws ArgumentError("Column 'a' is empty.") concrete_term(Term(:a), (; a=[]), nothing)
        @test_throws ArgumentError("Column 'a' is empty.") schema((; b=[1,2], a=[]))

    end

end


mutable struct MapGenerateState{T,U}
    score::Float64
    noise::Float64
    weight::Float64
    subtraces::Vector{U}
    retval::Vector{T}
    num_nonempty::Int
end

function process!(gen_fn::Map{T,U}, args::Tuple, choices::ChoiceMap,
                  key::Int, state::MapGenerateState{T,U}) where {T,U}
    local subtrace::U
    local retval::T
    kernel_args = get_args_for_key(args, key)
    submap = get_submap(choices, key)
    (subtrace, weight) = generate(gen_fn.kernel, kernel_args, submap)
    state.weight += weight
    state.noise += project(subtrace, EmptySelection())
    state.num_nonempty += (isempty(get_choices(subtrace)) ? 0 : 1)
    state.score += get_score(subtrace)
    state.subtraces[key] = subtrace
    retval = get_retval(subtrace)
    state.retval[key] = retval
end

function generate(gen_fn::Map{T,U}, args::Tuple, choices::ChoiceMap) where {T,U}
    len = length(args[1])
    state = MapGenerateState{T,U}(0., 0., 0., Vector{U}(undef,len), Vector{T}(undef,len), 0)
    # TODO check for keys that aren't valid constraints
    for key=1:len
        process!(gen_fn, args, choices, key, state)
    end
    trace = VectorTrace{MapType,T,U}(gen_fn,
        PersistentVector{U}(state.subtraces), PersistentVector{T}(state.retval),
        args, state.score, state.noise, len, state.num_nonempty)
    (trace, state.weight)
end


using TestImages, ImageCore, ImageShow
using Test, Suppressor
using FileIO
using ImageDistances

@testset "GIF" begin
    toucan = testimage("toucan") # 150×162 RGBA image
    moon = testimage("moon") # 256×256 Gray image

    # This might be a non-error in future
    @test_throws ArgumentError ImageShow.gif(toucan, moon)
    warning_msg = Suppressor.@capture_err ImageShow.gif([toucan, moon]; fps=0.5)
    @test occursin("FPS should be larger than 1", warning_msg)

    @testset "show" begin
        # TODO: the `fps` keyword is unable to test

        img = RGB.(testimage("mri-stack")) # ImageMagick saves GIF as RGB/ARGB array
        ref_gif = ImageShow.gif(img)

        @test !showable(MIME("image/gif"), img)
        @test showable(MIME("image/gif"), ref_gif)

        workdir = "tmp"
        mktempdir() do workdir
            fn = joinpath(workdir, "writemime.gif")
            open(fn, "w") do file
                show(file, MIME("image/gif"), ImageShow.gif(img))
            end
            @test load(fn) == ref_gif

            framestack = [img[:, :, i] for i in axes(img, 3)]
            fn = joinpath(workdir, "writemime.gif")
            open(fn, "w") do file
                show(file, MIME("image/gif"), ImageShow.gif(framestack))
            end
            @test load(fn) == ref_gif

            framestack = [toucan, moon]
            fn = joinpath(workdir, "writemime.gif")
            open(fn, "w") do file
                show(file, MIME("image/gif"), ImageShow.gif(framestack))
            end
            @test euclidean(RGB.(load(fn)), RGB.(ImageShow.gif(framestack))) <= 5.5

            sizes = 16:4:64
            values = range(0, stop=1, length=length(sizes))
            gif = ImageShow.gif(values, sizes) do v, x
                fill(v, ntuple(_->x, 2)...)
            end
            fn = joinpath(workdir, "writemime.gif")
            open(fn, "w") do file
                show(file, MIME("image/gif"), gif)
            end
            @test euclidean(RGB.(load(fn)), RGB.(gif)) <= 0.5
        end
    end

    @testset "gif as a readonly array" begin
        A = fill(1.0, 4, 8)
        B = fill(2.0, 2, 2)
        gif = ImageShow.gif([A, B])
        @test axes(gif) == (1:4, 1:8, 1:2)
        @test size(gif) == (4, 8, 2)
        cv = channelview(gif)
        @test all(cv[:, :, 1] .== 1.0) && all(cv[2:3, 4:5, 2] .== 2.0)
    end
end


module TestSetters

include("preamble.jl")

@testset "nullsetter" begin
    @test set(1, nullsetter, 2) === 1
    @test string(nullsetter) === "nullsetter"
end

@testset "ToField" begin
    setter = (@lens _.x) ∘ ToField(@lens _.a)
    @test set((x = 1, y = 2), setter, (a = 10, b = 20)) === (x = 10, y = 2)
end

end  # module


# A default choice model which can be used mostly for testing purposes. It just falls back on the
# default implementations of the interface.
# In practice the choice model will typically/always need to be tuned to the specific generator
# for it to be useful.
type SimpleChoiceModel <: ChoiceModel; end

function setsimplechoicemodel!(g::Generator)
	Base.depwarn("Use setchoicemodel!(g, SimpleChoiceModel()) in place of setsimplechoicemodel!(g, ...)", :setsimplechoicemodel!)
	setchoicemodel!(g, SimpleChoiceModel())
end

#
# The godelnumber function is the interface to all subtypes of ChoiceModel. Override to implement more specific
# behavior.
#
# Guarentees made by DataGenerators to the choice model:
#
#	 (1) ChoiceContext.lowerbound <= ChoiceContext.upperbound
#	 (2) ChoiceContext.lowerbound and upperbound have the type ChoiceContext.datatype (for integer datatypes this means
#      that full range is indicated by bounds of typemin and typemax rather than -Inf and Inf)
#
# Assumptions made by DataGenerators on value return by the choice model:
#
#  (1) must be between ChoiceContext.lowerbound and ChoiceContext.upperbound (inclusive)
#  (2) must be convertible without loss of precision to ChoiceContext.datatype (i.e. does not cause convert to raise an
#      InexactError), but need not be of the specified datatype
#
function godelnumber(cm::SimpleChoiceModel, cc::ChoiceContext)
	# finitise infinities to maxintfloat()/10 (approx 9e14 for Float64)
	# using a range that has a size that is less than maxintfloat ensures that the range is small enough that some of the Godel numbers
	# are, after conversion back to the datatype, have a non-zero floating point part
	lowerbound = isfinite(cc.lowerbound) ? cc.lowerbound : sign(cc.lowerbound) * maxintfloat(cc.datatype) / 10
	upperbound = isfinite(cc.upperbound) ? cc.upperbound : sign(cc.upperbound) * maxintfloat(cc.datatype) / 10
	rangelen = convert(Float64,upperbound) - convert(Float64,lowerbound)
	if cc.datatype <: Integer
		rangelen += 1.0 # since the random value will be later floor'ed
	end
	if cc.cptype == :sequence
		# Default is to use maxReps of max 2 regardless of what the actual max is
		# This is to limit the size of generated data for recursively defined generators.
		rangelen = min(rangelen, 3.0) # 3 because 1 is added to the rangelen above
	end
	gn = lowerbound + rand() * rangelen # note rand() returns a value in [0,1)
	if cc.datatype <: Integer
		gn = floor(gn)
		# note that query_choice_model will convert to the appropriate type, so need to use int() nor convert() here
	end
	gn, Dict()
end

#
# The following functions are used to set and get the parameters to the model (e.g. to enable optimisation)
# Currently the parameters are assumed to be Real numbers
#

# valid ranges for each parameter, return as a vector; each range is a tuple (min,max) where (ironically, given the notation)
# values are inclusive
paramranges(cm::SimpleChoiceModel) = Tuple{Float64,Float64}[]

# set parameters using the passed vector of values
setparams!(cm::SimpleChoiceModel, params) = nothing

# get parameters as a vector of values
getparams(cm::SimpleChoiceModel) = Float64[]

show(io::IO, cm::SimpleChoiceModel) = print(io, "simple choice model")


module globals

# Free space impedance
imp0 = 377.0

# Courant number
cdtds = 1.0

end


#################################################################################################
# Functions needed for the manipulation of datacell labels
#
# Desired functionality:
#     - adding labels to existing DataCell
#     - removing labels from existing DataCell
#     - changing some/all labels from existing DataCell
#     - remap labels to something else : e.g. from "apple" to "bear" etc using regexpz
#     - extract column(s) from data and transform them into labels
#
# Observation: All functions return new objects (they do not modify the existing ones)
#################################################################################################

# Remove labels from DataCells
# - Can be done by creating a new datacell(old_datacell.x) or datacell(old_datacell.x, old_datacell.y[:,...])

# Change labels from existing datacell
# - Extract current labels, modify accordingly and create new datacell re-using the data and new labels.

"""
	addlabels(x, labels)

Creates a new datacell by adding `labels<:AbstractArray` to the existing labels
of data cell `x`.
"""
addlabels(x::T where T<:CellData, labels::S where S<:AbstractArray) = datacell(getx!(x), dcat(gety!(x), labels), name=x.tinfo)

"""
	unlabel(data)

Removes labels (if any) from the data cell and returns a new datacell with the
data contents only.
"""
unlabel(x::T where T<:CellData) = datacell(getx(x); name = x.tinfo)

"""
	labelize(x, f [,idx ,remove])
	labelize(x, idx [,remove])
	labelize(x, v [,remove])

Adds labels (also known as 'targets') to `x::DataCell`. If `x` is not provided, returns a fixed `Function cell` that when piped a DataCell into,
it will add labels/targets and return it.

# Arguments
  * `f` is a `targets`-related function (e.g. `targets(f,...)` ) that it is applied to the existing labels of `x` if `idx` is not specified or to the
  variables of `x` indicated by `idx`
  * `idx` specifies variables in `x`; can be anything that can be used as a variable index in a `DataCell`
  * `v` a vector that will become the new targets of `x`
  * `remove` defaults to `true` and specifies whether any existing labels/targets are to be kept or, if `idx` is present, whether to remove the
  variables from `x` from which the new labels/targets were obtained.
"""
labelize(f::T where T<:Function, remove::Bool=true) = FunctionCell(labelize, (f,remove), "Data labeler: f=$f, remove=$remove")
labelize(v::T where T<:AbstractArray, remove::Bool=true) = FunctionCell(labelize, (v,remove), "Data labeler: preloaded targets, remove=$remove")
labelize(idx::T where T, remove::Bool=true) = FunctionCell(labelize, (idx,remove), "Data labeler: idx=$idx remove=$remove")
labelize(f::T where T<:Function, idx::S where S, remove::Bool=true) = FunctionCell(labelize, (f,idx,remove), "Data labeler: f=$f idx=$idx remove=$remove")

labelize(x::T where T<:CellDataU, f::Function, remove::Bool=true) =
	error("[labelize] Targets or the indices of variables from which to create targets required.")

labelize(x::T where T<:CellData, f::Function, remove::Bool=true) = begin
	if remove
		datacell(getx(x), targets(f, gety(x)), name=x.tinfo) 			# replace labels
	else
		datacell(getx(x), dcat(gety(x), targets(f, gety(x))), name=x.tinfo)	# add to existing labels
	end
end

labelize(x::T where T<:CellData, v::S where S<:AbstractArray, remove::Bool=true) = begin
	if remove
		return datacell( getx(x), getobs(v), name=x.tinfo) 			# replace labels
	else
		return datacell( getx(x), dcat(gety(x), getobs(v)), name=x.tinfo ) 	# add to existing labels
	end
end

labelize(x::T where T<:CellData, idx::S where S, remove::Bool=true) = labelize(x, identity, idx, remove)

labelize(x::T where T<:CellData, f::Function, idx::S where S, remove::Bool=true) = begin
	labels = targets(f, getx!(varsubset(x,idx)))
	if remove
		return datacell( getobs(_variable_(x, setdiff(1:nvars(x),idx))), labels, name=x.tinfo)
	else
		return datacell( getx(x), labels, name=x.tinfo )
	end
end


# ------------------------------------------------------------------
# Licensed under the ISC License. See LICENSE in the project root.
# ------------------------------------------------------------------

# connected component of adjacency matrix containing vertex
function component(adjacency::AbstractMatrix{Int}, vertex::Int)
  frontier = [vertex]
  visited  = Int[]
  # breadth-first search
  while !isempty(frontier)
    u = pop!(frontier)
    push!(visited, u)
    for v in findall(!iszero, adjacency[u,:])
      if v ∉ visited
        push!(frontier, v)
      end
    end
  end
  visited
end

"""
    @metasolver solver solvertype body

A helper macro to create a solver named `solver` of type `solvertype`
with parameters specified in `body`.

## Examples

Create a solver with parameters `mean` and `variogram` for each variable
of the problem, and a global parameter that specifies whether or not
to use the GPU:

```julia
julia> @metasolver MySolver AbstractSimulationSolver begin
  @param mean = 0.0
  @param variogram = GaussianVariogram()
  @jparam rho = 0.7
  @global gpu = false
end
```

### Notes

This macro is not intended to be used directly, see other macros defined
below for estimation and simulation solvers.
"""
macro metasolver(solver, solvertype, body)
  # discard any content that doesn't start with @param or @global
  content = filter(arg -> arg isa Expr, body.args)

  # lines starting with @param refer to single variable parameters
  vparams = filter(p -> p.args[1] == Symbol("@param"), content)
  vparams = map(p -> p.args[3], vparams)

  # lines starting with @jparam refer to joint variable parameters
  jparams = filter(p -> p.args[1] == Symbol("@jparam"), content)
  jparams = map(p -> p.args[3], jparams)

  # lines starting with @global refer to global solver parameters
  gparams = filter(p -> p.args[1] == Symbol("@global"), content)
  gparams = map(p -> p.args[3], gparams)

  # add default value of `nothing` if necessary
  gparams = map(p -> p isa Symbol ? :($p = nothing) : p, gparams)

  # replace Expr(:=, a, 2) by Expr(:kw, a, 2) for valid kw args
  gparams = map(p -> Expr(:kw, p.args...), gparams)

  # keyword names
  gkeys = map(p -> p.args[1], gparams)

  # solver parameter type for single variable
  solvervparam = Symbol(solver,"Param")

  # solver parameter type for joint variables
  solverjparam = Symbol(solver,"JointParam")

  # variables are symbols or tuples of symbols
  vtype = Symbol
  jtype = NTuple{<:Any,Symbol}

  esc(quote
    $Parameters.@with_kw_noshow struct $solvervparam
      __dummy__ = nothing
      $(vparams...)
    end

    $Parameters.@with_kw_noshow struct $solverjparam
      __dummy__ = nothing
      $(jparams...)
    end

    @doc (@doc $solvervparam) (
    struct $solver <: $solvertype
      vparams::Dict{$vtype,$solvervparam}
      jparams::Dict{$jtype,$solverjparam}
      $(gkeys...)

      # auxiliary fields
      varnames::Vector{Symbol}
      adjacency::Matrix{Int}

      function $solver(vparams::Dict{$vtype,$solvervparam},
                       jparams::Dict{$jtype,$solverjparam},
                       $(gkeys...))
        svars = collect(keys(vparams))
        jvars = collect(keys(jparams))
        lens₁ = length.(jvars)
        lens₂ = length.(unique.(jvars))

        @assert all(lens₁ .== lens₂ .> 1) "invalid joint variable specification"

        varnames = svars ∪ Iterators.flatten(jvars)

        adjacency = zeros(Int, length(varnames), length(varnames))
        for (i, u) in enumerate(varnames)
          for vtuple in jvars
            if u ∈ vtuple
              for v in vtuple
                j = indexin([v], varnames)[1]
                i == j || (adjacency[i,j] = 1)
              end
            end
          end
        end

        new(vparams, jparams, $(gkeys...), varnames, adjacency)
      end
    end)

    function $solver(params...; $(gparams...))
      # build dictionaries for inner constructor
      vdict = Dict{$vtype,$solvervparam}()
      jdict = Dict{$jtype,$solverjparam}()

      # convert named tuples to solver parameters
      for (varname, varparams) in params
        kwargs = [k => v for (k,v) in zip(keys(varparams), varparams)]
        if varname isa Symbol
          push!(vdict, varname => $solvervparam(; kwargs...))
        else
          push!(jdict, varname => $solverjparam(; kwargs...))
        end
      end

      $solver(vdict, jdict, $(gkeys...))
    end

    function GeoStatsBase.covariables(var::Symbol, solver::$solver)
      vind = indexin([var], solver.varnames)[1]
      if vind ≠ nothing
        comp = GeoStatsBase.component(solver.adjacency, vind)
        vars = Tuple(solver.varnames[sort(comp)])
        params = []
        for v in vars
          push!(params, (v,) => solver.vparams[v])
        end
        for vtuple in keys(solver.jparams)
          if any(v ∈ vars for v in vtuple)
            push!(params, vtuple => solver.jparams[vtuple])
          end
        end
      else
        # default parameter for single variable
        vars = (var,)
        params = [(var,) => $solvervparam()]
      end

      (names=vars, params=Dict(params))
    end

    GeoStatsBase.variables(solver::$solver) = solver.varnames

    # ------------
    # IO methods
    # ------------
    function Base.show(io::IO, solver::$solver)
      print(io, $solver)
    end

    function Base.show(io::IO, ::MIME"text/plain", solver::$solver)
      println(io, solver)
      for (var, varparams) in merge(solver.vparams, solver.jparams)
        if var isa Symbol
          println(io, "  └─", var)
        else
          println(io, "  └─", join(var, "—"))
        end
        pnames = setdiff(fieldnames(typeof(varparams)), [:__dummy__])
        for pname in pnames
          pval = getfield(varparams, pname)
          if pval ≠ nothing
            print(io, "    └─", pname, " ⇨ ")
            show(IOContext(io, :compact => true), pval)
            println(io, "")
          end
        end
      end
    end
  end)
end

"""
    @estimsolver solver body

A helper macro to create a estimation solver named `solver` with parameters
specified in `body`. For examples, please check the documentation for
`@metasolver`.
"""
macro estimsolver(solver, body)
  esc(quote
    GeoStatsBase.@metasolver $solver GeoStatsBase.AbstractEstimationSolver $body
  end)
end

"""
    @estimsolver solver body

A helper macro to create a simulation solver named `solver` with parameters
specified in `body`. For examples, please check the documentation for
`@metasolver`.
"""
macro simsolver(solver, body)
  esc(quote
    GeoStatsBase.@metasolver $solver GeoStatsBase.AbstractSimulationSolver $body
  end)
end


module TestConstructors
    using Base.Test
    using DataTables, DataTables.Index

    #
    # DataTable
    #

    dt = DataTable()
    @test isequal(dt.columns, Any[])
    @test isequal(dt.colindex, Index())

    dt = DataTable(Any[NullableCategoricalVector(zeros(3)),
                       NullableCategoricalVector(ones(3))],
                   Index([:x1, :x2]))
    @test size(dt, 1) == 3
    @test size(dt, 2) == 2

    @test isequal(dt, DataTable(Any[NullableCategoricalVector(zeros(3)),
                                    NullableCategoricalVector(ones(3))]))
    @test isequal(dt, DataTable(x1 = NullableArray([0.0, 0.0, 0.0]),
                                x2 = NullableArray([1.0, 1.0, 1.0])))

    dt2 = convert(DataTable, NullableArray([0.0 1.0;
                                            0.0 1.0;
                                            0.0 1.0]))
    names!(dt2, [:x1, :x2])
    @test isequal(dt[:x1], NullableArray(dt2[:x1]))
    @test isequal(dt[:x2], NullableArray(dt2[:x2]))

    @test isequal(dt, DataTable(x1 = NullableArray([0.0, 0.0, 0.0]),
                                x2 = NullableArray([1.0, 1.0, 1.0])))
    @test isequal(dt, DataTable(x1 = NullableArray([0.0, 0.0, 0.0]),
                                x2 = NullableArray([1.0, 1.0, 1.0]),
                                x3 = NullableArray([2.0, 2.0, 2.0]))[[:x1, :x2]])

    dt = DataTable(Nullable{Int}, 2, 2)
    @test size(dt) == (2, 2)
    @test eltypes(dt) == [Nullable{Int}, Nullable{Int}]

    dt = DataTable([Nullable{Int}, Nullable{Float64}], [:x1, :x2], 2)
    @test size(dt) == (2, 2)
    @test eltypes(dt) == [Nullable{Int}, Nullable{Float64}]

    @test isequal(dt, DataTable([Nullable{Int}, Nullable{Float64}], 2))

    @test_throws BoundsError SubDataTable(DataTable(A=1), 0)
    @test_throws BoundsError SubDataTable(DataTable(A=1), 0)
    @test isequal(SubDataTable(DataTable(A=1), 1), DataTable(A=1))
    @test isequal(SubDataTable(DataTable(A=1:10), 1:4), DataTable(A=1:4))
    @test isequal(view(SubDataTable(DataTable(A=1:10), 1:4), 2), DataTable(A=2))
    @test isequal(view(SubDataTable(DataTable(A=1:10), 1:4), [true, true, false, false]), DataTable(A=1:2))

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

    @testset "associative" begin
        dt = DataTable(Dict(:A => 1:3, :B => 4:6))
        @test dt == DataTable(A = 1:3, B = 4:6)
        @test eltypes(dt) == [Int, Int]
    end

    @testset "recyclers" begin
        @test DataTable(a = 1:5, b = 1) == DataTable(a = collect(1:5), b = fill(1, 5))
        @test DataTable(a = 1, b = 1:5) == DataTable(a = fill(1, 5), b = collect(1:5))
    end

    @testset "constructor errors" begin
        @test_throws DimensionMismatch DataTable(a=1, b=[])
        @test_throws DimensionMismatch DataTable(Any[collect(1:10)], DataTables.Index([:A, :B]))
        @test_throws DimensionMismatch DataTable(A = rand(2,2))
        @test_throws DimensionMismatch DataTable(A = rand(2,1))
    end

    @testset "column types" begin
        dt = DataTable(A = 1:3, B = 2:4, C = 3:5)
        answer = [Array{Int,1}, Array{Int,1}, Array{Int,1}]
        @test map(typeof, dt.columns) == answer
        dt[:D] = NullableArray([4, 5, Nullable()])
        push!(answer, NullableArray{Int,1})
        @test map(typeof, dt.columns) == answer
        dt[:E] = 'c'
        push!(answer, Array{Char,1})
        @test map(typeof, dt.columns) == answer
    end
end


function add(::Type{FunctionUtils}, arg0::Vector{UnivariateDifferentiableFunction})
    return jcall(FunctionUtils, "add", UnivariateDifferentiableFunction, (Vector{UnivariateDifferentiableFunction},), arg0)
end

function add(::Type{FunctionUtils}, arg0::Vector{UnivariateFunction})
    return jcall(FunctionUtils, "add", UnivariateFunction, (Vector{UnivariateFunction},), arg0)
end

function collector(::Type{FunctionUtils}, arg0::BivariateFunction, arg1::UnivariateFunction, arg2::jdouble)
    return jcall(FunctionUtils, "collector", MultivariateFunction, (BivariateFunction, UnivariateFunction, jdouble), arg0, arg1, arg2)
end

function collector(::Type{FunctionUtils}, arg0::BivariateFunction, arg1::jdouble)
    return jcall(FunctionUtils, "collector", MultivariateFunction, (BivariateFunction, jdouble), arg0, arg1)
end

function combine(::Type{FunctionUtils}, arg0::BivariateFunction, arg1::UnivariateFunction, arg2::UnivariateFunction)
    return jcall(FunctionUtils, "combine", UnivariateFunction, (BivariateFunction, UnivariateFunction, UnivariateFunction), arg0, arg1, arg2)
end

function compose(::Type{FunctionUtils}, arg0::Vector{UnivariateDifferentiableFunction})
    return jcall(FunctionUtils, "compose", UnivariateDifferentiableFunction, (Vector{UnivariateDifferentiableFunction},), arg0)
end

function compose(::Type{FunctionUtils}, arg0::Vector{UnivariateFunction})
    return jcall(FunctionUtils, "compose", UnivariateFunction, (Vector{UnivariateFunction},), arg0)
end

function derivative(::Type{FunctionUtils}, arg0::MultivariateDifferentiableFunction, arg1::Vector{jint})
    return jcall(FunctionUtils, "derivative", MultivariateFunction, (MultivariateDifferentiableFunction, Vector{jint}), arg0, arg1)
end

function derivative(::Type{FunctionUtils}, arg0::UnivariateDifferentiableFunction, arg1::jint)
    return jcall(FunctionUtils, "derivative", UnivariateFunction, (UnivariateDifferentiableFunction, jint), arg0, arg1)
end

function equals(obj::Object, arg0::Object)
    return jcall(obj, "equals", jboolean, (Object,), arg0)
end

function fix1st_argument(::Type{FunctionUtils}, arg0::BivariateFunction, arg1::jdouble)
    return jcall(FunctionUtils, "fix1stArgument", UnivariateFunction, (BivariateFunction, jdouble), arg0, arg1)
end

function fix2nd_argument(::Type{FunctionUtils}, arg0::BivariateFunction, arg1::jdouble)
    return jcall(FunctionUtils, "fix2ndArgument", UnivariateFunction, (BivariateFunction, jdouble), arg0, arg1)
end

function get_class(obj::Object)
    return jcall(obj, "getClass", Class, ())
end

function hash_code(obj::Object)
    return jcall(obj, "hashCode", jint, ())
end

function multiply(::Type{FunctionUtils}, arg0::Vector{UnivariateDifferentiableFunction})
    return jcall(FunctionUtils, "multiply", UnivariateDifferentiableFunction, (Vector{UnivariateDifferentiableFunction},), arg0)
end

function multiply(::Type{FunctionUtils}, arg0::Vector{UnivariateFunction})
    return jcall(FunctionUtils, "multiply", UnivariateFunction, (Vector{UnivariateFunction},), arg0)
end

function notify(obj::Object)
    return jcall(obj, "notify", void, ())
end

function notify_all(obj::Object)
    return jcall(obj, "notifyAll", void, ())
end

function sample(::Type{FunctionUtils}, arg0::UnivariateFunction, arg1::jdouble, arg2::jdouble, arg3::jint)
    return jcall(FunctionUtils, "sample", Vector{jdouble}, (UnivariateFunction, jdouble, jdouble, jint), arg0, arg1, arg2, arg3)
end

function to_differentiable(::Type{FunctionUtils}, arg0::MultivariateFunction, arg1::MultivariateVectorFunction)
    return jcall(FunctionUtils, "toDifferentiable", MultivariateDifferentiableFunction, (MultivariateFunction, MultivariateVectorFunction), arg0, arg1)
end

function to_differentiable(::Type{FunctionUtils}, arg0::UnivariateFunction, arg1::Vector{UnivariateFunction})
    return jcall(FunctionUtils, "toDifferentiable", UnivariateDifferentiableFunction, (UnivariateFunction, Vector{UnivariateFunction}), arg0, arg1)
end

function to_string(obj::Object)
    return jcall(obj, "toString", JString, ())
end

function wait(obj::Object)
    return jcall(obj, "wait", void, ())
end

function wait(obj::Object, arg0::jlong)
    return jcall(obj, "wait", void, (jlong,), arg0)
end

function wait(obj::Object, arg0::jlong, arg1::jint)
    return jcall(obj, "wait", void, (jlong, jint), arg0, arg1)
end


import DataFrames
import Dates
import TimeSeries

function verify_forecasts(sys::System, num_initial_times, num_forecasts, horizon)
    initial_times = get_forecast_initial_times(sys)
    if length(initial_times) != num_initial_times
        @error "count of initial_times doesn't match" num_initial_times initial_times
        return false
    end

    total_forecasts = 0
    for it in initial_times
        forecasts = get_forecasts(Forecast, sys, it)
        for forecast in forecasts
            if IS.get_horizon(forecast) != horizon
                @error "horizon doesn't match" IS.get_horizon(forecast) horizon
                return false
            end
        end
        total_forecasts += length(forecasts)
    end

    if num_forecasts != total_forecasts
        @error "num_forecasts doesn't match" num_forecasts total_forecasts
        return false
    end

    return true
end

@testset "Test read_timeseries_metadata" begin
    forecasts = IS.read_timeseries_metadata(joinpath(RTS_GMLC_DIR,
                                                     "timeseries_pointers.json"))
    @test length(forecasts) == 282

    for forecast in forecasts
        @test isfile(forecast.data_file)
    end
end

@testset "Test forecast normalization" begin
    component_name = "122_HYDRO_1"
    timeseries_file = joinpath(DATA_DIR, "forecasts", "RTS_GMLC_forecasts", "gen", "Hydro",
                               "DAY_AHEAD_hydro.csv")
    timeseries = IS.read_timeseries(timeseries_file)[Symbol(component_name)]
    max_value = maximum(TimeSeries.values(timeseries))

    metadata = IS.TimeseriesFileMetadata(
        "DAY_AHEAD",
        "Generator",
        "122_HYDRO_1",
        "PMax MW",
        1.0,
        timeseries_file,
        [],
        "Deterministic",
    )

    # Test code path where no normalization occurs.
    sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m"))
    add_forecasts!(sys, [metadata])
    verify_forecasts(sys, 1, 1, 24)
    forecast = collect(PSY.iterate_forecasts(sys))[1]
    @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries)

    # Test code path where timeseries is normalized by dividing by the max value.
    metadata.scaling_factor = "Max"
    sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m"))
    add_forecasts!(sys, [metadata])
    verify_forecasts(sys, 1, 1, 24)
    forecast = collect(PSY.iterate_forecasts(sys))[1]
    @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries ./ max_value)

    # Test code path where timeseries is normalized by dividing by a custom value.
    sf = 95.0
    metadata.scaling_factor = sf
    sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m"))
    add_forecasts!(sys, [metadata])
    verify_forecasts(sys, 1, 1, 24)
    forecast = collect(PSY.iterate_forecasts(sys))[1]
    @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries ./ sf)
end

@testset "Test single forecast addition" begin
    component_name = "122_HYDRO_1"
    timeseries_file = joinpath(DATA_DIR, "forecasts", "RTS_GMLC_forecasts", "gen", "Hydro",
                               "DAY_AHEAD_hydro.csv")
    timeseries = IS.read_timeseries(timeseries_file)[Symbol(component_name)]

    # Test with a filename.
    sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m"))
    component = get_component(HydroDispatch, sys, component_name)
    add_forecast!(sys, timeseries_file, component, "PMax MW", 1.0)
    verify_forecasts(sys, 1, 1, 24)
    forecast = collect(PSY.iterate_forecasts(sys))[1]
    @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries)
    @test IS.get_timeseries(forecast) == timeseries

    # Test with TimeSeries.TimeArray.
    sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m"))
    component = get_component(HydroDispatch, sys, component_name)
    add_forecast!(sys, timeseries, component, "PMax MW", 1.0)
    verify_forecasts(sys, 1, 1, 24)
    forecast = collect(PSY.iterate_forecasts(sys))[1]
    @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries)
    @test IS.get_timeseries(forecast) == timeseries

    # Test with DataFrames.DataFrame.
    sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m"))
    component = get_component(HydroDispatch, sys, component_name)
    df = DataFrames.DataFrame(timeseries)
    add_forecast!(sys, df, component, "PMax MW", 1.0)
    verify_forecasts(sys, 1, 1, 24)
    forecast = collect(PSY.iterate_forecasts(sys))[1]
end

@testset "Forecast data matpower" begin
    sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "case5_re.m"))
    forecasts_metadata = joinpath(FORECASTS_DIR, "5bus_ts", "timeseries_pointers_da.json")
    add_forecasts!(sys, forecasts_metadata)
    @test verify_forecasts(sys, 1, 5, 24)

    # Add the same files.
    # This will fail because the component-label pairs will be duplicated.
    @test_throws IS.DataFormatError add_forecasts!(sys, forecasts_metadata)

    forecasts_metadata = joinpath(FORECASTS_DIR, "5bus_ts", "timeseries_pointers_rt.json")

    ## This will fail because the resolutions are different.
    @test_throws IS.DataFormatError add_forecasts!(sys, forecasts_metadata)

    ## TODO: need a dataset with same resolution but different horizon.

    sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "case5_re.m"))
    add_forecasts!(sys, forecasts_metadata)
    @test verify_forecasts(sys, 1, 5, 288)
end

@testset "Test forecast splitting" begin
    component_name = "122_HYDRO_1"
    timeseries_file = joinpath(DATA_DIR, "forecasts", "RTS_GMLC_forecasts", "gen", "Hydro",
                               "DAY_AHEAD_hydro.csv")
    timeseries = IS.read_timeseries(timeseries_file)[Symbol(component_name)]

    # Test with a filename.
    sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m"))
    component = get_component(HydroDispatch, sys, component_name)
    add_forecast!(sys, timeseries_file, component, "PMax MW", 1.0)
    verify_forecasts(sys, 1, 1, 24)
    forecast = collect(PSY.iterate_forecasts(sys))[1]
    @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries)
    @test IS.get_timeseries(forecast) == timeseries
    @test IS.get_resolution(forecast) == Dates.Hour(1)

    interval = Dates.Hour(1)
    horizon = 12
    forecasts = IS.make_forecasts(forecast, interval, horizon)
    @test length(forecasts) == 13
    compare_initial_time = IS.get_initial_time(forecast)
    for forecast_ in forecasts
        @test IS.get_horizon(forecast_) == horizon
        @test IS.get_initial_time(forecast_) == compare_initial_time
        compare_initial_time += interval
    end

    # Interval is smaller than resolution.
    @test_throws(IS.ArgumentError,
                 IS.make_forecasts(forecast, Dates.Minute(1), horizon))
    # Interval is not multiple of resolution.
    @test_throws(IS.ArgumentError,
                 IS.make_forecasts(forecast, Dates.Minute(13), horizon))
    # Horizon is larger than forecast horizon.
    @test_throws(IS.ArgumentError,
                 IS.make_forecasts(forecast, interval, 25))

    # making a series of forecasts from a list of forecasts
    forecasts = get_forecasts(IS.Deterministic, sys, IS.get_initial_time(forecast))
    forecasts_ = IS.make_forecasts(forecasts, Hour(1), 2)
    @test length(forecasts_) == 23

    # removal of all forecasts
    clear_forecasts!(sys)
    forecasts = get_forecasts(IS.Deterministic, sys, IS.get_initial_time(forecast))
    @test length(forecasts) == 0

    # adding series of forecasts
    add_forecasts!(sys, forecasts_)
    ts = get_forecast_initial_times(sys)
    @test length(ts) == 23

    # replace a long forecast with a series of forecasts
    clear_forecasts!(sys)
    add_forecast!(sys, forecast)
    forecasts = get_forecasts(IS.Deterministic, sys, IS.get_initial_time(forecast))
    split_forecasts!(sys, forecasts, Hour(6), 12)
    ts = get_forecast_initial_times(sys)
    @test length(ts) == 3

    # TODO: need to cover serialization.
end


facts("KDTrees.build") do

# Issue # 15
context("KDTrees.build.15") do
    # Just check if it can be created
    @fact typeof(KDTree(ones(1, 11))) --> KDTree{Float64}
end

end


module ParameterTest

using Test
using Parametron

import Parametron: setdirty!, mock_model

@testset "Parameter" begin
    # out-of-place parameter
    val = Ref(true)
    model = mock_model()
    p1 = Parameter{Bool}(() -> val[], model)
    @test p1()

    # test caching
    val[] = false
    @test p1()
    setdirty!(p1)
    @test !p1()

    # in-place parameter
    A = zeros(3, 4)
    Aval = Ref(1.0)
    f = let Aval = Aval # https://github.com/JuliaLang/julia/issues/15276
        m -> m .= Aval[]
    end
    p2 = Parameter(f, A, model)
    @test p2() === A
    @test all(p2() .== 1)

    # caching
    Aval[] = 2.0
    @test p2() === A
    @test all(p2() .== 1.0)
    setdirty!(p2)
    @test all(p2() .== 2.0)
    setdirty!(p2)
    allocs = @allocated p2()
    @test allocs == 0
end

end


# :LN_BOBYQA
@testset "f_3p_1im restricted :LN_BOBYQA" begin
    res0 = [get_right_endpoint(
        [3., 8., 2.1],
        i,
        (x::Vector{Float64}) -> f_3p_1im(x) - 9.,
        Val(:CICO_ONE_PASS);
        scan_bound = 4.,
        local_alg = :LN_BOBYQA
    ) for i in 1:3]
    @test res0[1][3] == :SCAN_BOUND_REACHED
    @test res0[2][3] == :SCAN_BOUND_REACHED
    @test res0[3][3] == :SCAN_BOUND_REACHED
end

@testset "f_3p_1im_dep :LN_BOBYQA" begin
    res0 = [get_right_endpoint(
        [3., 2., 2.1],
        i,
        (x::Vector{Float64}) -> f_3p_1im_dep(x) - 9.,
        Val(:CICO_ONE_PASS);
        local_alg = :LN_BOBYQA,
        loss_tol = 1e-3
    ) for i in 1:3]
    @test isapprox(res0[1][1], 5., atol=1e-2) # XXX: bad tolerance
    @test res0[1][3] == :BORDER_FOUND_BY_SCAN_TOL
    @test isapprox(res0[2][1], 2.0+2.0*sqrt(2.), atol=1e-2) # XXX: bad tolerance
    @test res0[2][3] == :BORDER_FOUND_BY_SCAN_TOL
    @test res0[3][3] == :SCAN_BOUND_REACHED
end

@testset "f_3p_1im_dep scan_bound :LN_BOBYQA" begin
    res0 = [get_right_endpoint(
        [3., 2., 2.1],
        i,
        (x::Vector{Float64}) -> f_3p_1im_dep(x) - 9.,
        Val(:CICO_ONE_PASS);
        scan_bound = [4.,10.,10.][i],
        local_alg = :LN_BOBYQA
    ) for i in 1:3]
    @test res0[1][3] == :SCAN_BOUND_REACHED
    @test isapprox(res0[2][1], 2.0+2.0*sqrt(2.), atol=1e-2) # XXX: bad tolerance
    @test res0[2][3] == :BORDER_FOUND_BY_SCAN_TOL
    @test res0[3][3] == :SCAN_BOUND_REACHED
end

# :LN_NEWUOA
@testset "f_3p_1im restricted :LN_NEWUOA" begin
    res0 = [get_right_endpoint(
        [3., 8., 2.1],
        i,
        (x::Vector{Float64}) -> f_3p_1im(x) - 9.,
        Val(:CICO_ONE_PASS);
        scan_bound = 4.,
        local_alg = :LN_NEWUOA
    ) for i in 1:3]
    @test res0[1][3] == :SCAN_BOUND_REACHED # XXX: fail
    @test res0[2][3] == :SCAN_BOUND_REACHED
    @test res0[3][3] == :SCAN_BOUND_REACHED # XXX: fail
end

@testset "f_3p_1im_dep :LN_NEWUOA" begin
    res0 = [get_right_endpoint(
        [3., 2., 2.1],
        i,
        (x::Vector{Float64}) -> f_3p_1im_dep(x) - 9.,
        Val(:CICO_ONE_PASS);
        local_alg = :LN_NEWUOA
    ) for i in 1:3]
    @test isapprox(res0[1][1], 5., atol=1e-2)
    @test res0[1][3] == :BORDER_FOUND_BY_SCAN_TOL
    @test isapprox(res0[2][1], 2.0+2.0*sqrt(2.), atol=1e-2)
    @test res0[2][3] == :BORDER_FOUND_BY_SCAN_TOL
    @test res0[3][3] == :SCAN_BOUND_REACHED
end

@testset "f_3p_1im_dep scan_bound :LN_NEWUOA" begin
    res0 = [get_right_endpoint(
        [3., 2., 2.1],
        i,
        (x::Vector{Float64}) -> f_3p_1im_dep(x) - 9.,
        Val(:CICO_ONE_PASS);
        scan_bound = [4.,10.,10.][i],
        local_alg = :LN_NEWUOA
    ) for i in 1:3]
    @test res0[1][3] == :SCAN_BOUND_REACHED
    @test isapprox(res0[2][1], 2.0+2.0*sqrt(2.), atol=1e-2)
    @test res0[2][3] == :BORDER_FOUND_BY_SCAN_TOL
    @test res0[3][3] == :SCAN_BOUND_REACHED
end

# :LN_SBPLX
@testset "f_3p_1im restricted :LN_SBPLX" begin
    res0 = [get_right_endpoint(
        [3., 8., 2.1],
        i,
        (x::Vector{Float64}) -> f_3p_1im(x) - 9.,
        Val(:CICO_ONE_PASS);
        scan_bound = 4.,
        local_alg = :LN_SBPLX
    ) for i in 1:3]
    @test res0[1][3] == :SCAN_BOUND_REACHED
    @test res0[2][3] == :SCAN_BOUND_REACHED
    @test res0[3][3] == :SCAN_BOUND_REACHED
end

@testset "f_3p_1im_dep :LN_SBPLX" begin
    res0 = [get_right_endpoint(
        [3., 2., 2.1],
        i,
        (x::Vector{Float64}) -> f_3p_1im_dep(x) - 9.,
        Val(:CICO_ONE_PASS);
        local_alg = :LN_SBPLX
    ) for i in 1:3]
    @test isapprox(res0[1][1], 5., atol=1e-2)
    @test res0[1][3] == :BORDER_FOUND_BY_SCAN_TOL
    @test isapprox(res0[2][1], 2.0+2.0*sqrt(2.), atol=1e-2) # XXX: bad tolerance
    @test res0[2][3] == :BORDER_FOUND_BY_SCAN_TOL
    @test res0[3][3] == :SCAN_BOUND_REACHED
end

@testset "f_3p_1im_dep scan_bound :LN_SBPLX" begin
    res0 = [get_right_endpoint(
        [3., 2., 2.1],
        i,
        (x::Vector{Float64}) -> f_3p_1im_dep(x) - 9.,
        Val(:CICO_ONE_PASS);
        scan_bound = [4.,10.,10.][i],
        local_alg = :LN_SBPLX
    ) for i in 1:3]
    @test res0[1][3] == :SCAN_BOUND_REACHED
    @test isapprox(res0[2][1], 2.0+2.0*sqrt(2.), atol=1e-2) # XXX: bad tolerance
    @test res0[2][3] == :BORDER_FOUND_BY_SCAN_TOL
    @test res0[3][3] == :SCAN_BOUND_REACHED
end


module Sample

using ..Space, ..Util
import Random: AbstractRNG
import Random

export defrandalg, condomegasample, randsample, omegarandsample

"Default sampling algorithm"
function defrandalg end

function omegarandsample end
function omegarandsample1 end

"""
`n` random samples from `x` such that `yₓ(ω)` is true where `yₓ` are conditions of `x
using `alg` algorithm

```
x = 1 ~ StdNormal()
randsample(x |ᶜ x >ₚ 2.0, 3; alg = RejectionSample)
'''
"""
randsample(rng::AbstractRNG, x, n; alg = defrandalg(x), ΩT = defΩ(), kwargs...) =
  randsample(rng, ΩT, x, n, alg; kwargs...)

"""
Sample `n` `ω::AbstractΩ` according to function `logenergy(ω)` using inference algorithm `alg`

# Arguments
`rng` : random number generator
`logenergy` :

Returns
Vector of `n` samples
"""
omegarandsample(rng::AbstractRNG, logenergy, n; ΩT = defΩ(), alg, kwargs...) =
  omegarandsample(rng, ΩT, logenergy, n, alg; kwargs...)

omegarandsample(logenergy, n; ΩT = defΩ(), alg, kwargs...) =
  omegarandsample(Random.GLOBAL_RNG, logenergy, n; ΩT, alg, kwargs...)

# Convenience methods

"`randsample` using GLOBAL_RNG"
randsample(x, n::Integer; kwargs...) =
  randsample(Random.GLOBAL_RNG, x, n; kwargs...)

randsample(x; kwargs...) =
  first(randsample(x, 1, kwargs...))

randsample(rng::AbstractRNG, xs::Tuple, args...; kwargs...) =
  randsample(rng, ω -> mapf(ω, xs), args...; kwargs...)

end


module ClineSimulation

export binomial_migration,
       migration,
       correction,
       initialfreqs

using Distributions: Binomial,  pdf

include("migration.jl")
include("initialfrequencies.jl")

end # module


#####
##### Table
#####

export Dealer, SmallBlind, BigBlind, FirstToAct
export Table
export move_buttons!

Base.@kwdef mutable struct Winners
    declared::Bool = false
    players::Union{Nothing,Tuple,Player} = nothing
end

function Base.show(io::IO, winners::Winners, include_type = true)
    include_type && println(io, typeof(winners))
    println(io, "Winners declared = $(winners.declared)")
    println(io, "Winners          = $(winners.players)")
end

struct Blinds{S,B}
    small::S
    big::B
end

Blinds() = Blinds(1,2) # default
default_dealer_id() = 1 # default

function Base.show(io::IO, blinds::Blinds, include_type = true)
    include_type && println(io, typeof(blinds))
    println(io, "Blinds           = (small=$(blinds.small),big=$(blinds.big))")
end

struct Buttons
    dealer::Int
    small_blind::Int
    big_blind::Int
    first_to_act::Int
end

dealer_id(b::Buttons) = b.dealer
small_blind_id(b::Buttons) = b.small_blind
big_blind_id(b::Buttons) = b.big_blind
first_to_act_id(b::Buttons) = b.first_to_act

buttons(b::Buttons) = (
    b.dealer,
    b.small_blind,
    b.big_blind,
    b.first_to_act,
)

mutable struct Table
    deck::PlayingCards.Deck
    players::Tuple
    cards::Union{Nothing,Tuple{<:Card,<:Card,<:Card,<:Card,<:Card}}
    blinds::Blinds
    pot::Float64
    state::AbstractGameState
    buttons::Buttons
    current_raise_amt::Float64
    initial_round_raise_amt::Float64
    transactions::TransactionManager
    winners::Winners
    play_out_game::Bool
    n_max_actions::Int
end

buttons(table::Table) = table.buttons
n_max_actions(table::Table) = table.n_max_actions

"""
    compute_n_max_actions(players, bb)

The total possible number of actions,
which happens when
 - pre-flop: all players check/call
 - flop: all players check
 - turn: all players check
 - river: 1) all but one player checks (n_players-1 actions)
          2) the last player raises (1 action)
          3) everyone, except the last player, calls, (n_players-1 actions)
          4) the last player re-raises (1 action)
          ...
          until all players are all-in.
"""
function compute_n_max_actions(players, bb)
    maxbr = maximum(bank_roll.(players))
    n_players = length(players)
    n_check_call_rounds = n_players*3 # PreFlop, Flop, Turn
    n_raise_rounds = findfirst(1:typemax(Int)) do i
        maxbr < bb^n_raises(i, n_players)
    end
    return n_check_call_rounds + n_raise_rounds
end
n_raises(i, n_players) = Int(floor(i/n_players))

function Table(;
    players::Tuple,
    deck = ordered_deck(),
    cards = nothing,
    blinds = Blinds(),
    pot = Float64(0),
    state = PreFlop(),
    dealer_id = default_dealer_id(),
    current_raise_amt = Float64(0),
    initial_round_raise_amt = blinds.small,
    transactions = nothing,
    winners = Winners(),
    play_out_game = false,
)
    if transactions == nothing
        transactions = TransactionManager(players)
    end
    buttons = Buttons(dealer_id, players)
    n_max_actions = compute_n_max_actions(players, blinds.big)
    @debug "n_max_actions = $n_max_actions"
    return Table(deck,
        players,
        cards,
        blinds,
        pot,
        state,
        buttons,
        current_raise_amt,
        initial_round_raise_amt,
        transactions,
        winners,
        play_out_game,
        n_max_actions)
end

function Buttons(dealer_id, players::Tuple)
    dealer_id       = this_or_next_valid_id(dealer_id       , players)
    small_blind_id  = this_or_next_valid_id(dealer_id     +1, players)
    big_blind_id    = this_or_next_valid_id(small_blind_id+1, players)
    first_to_act_id = this_or_next_valid_id(big_blind_id  +1, players)
    return Buttons(dealer_id, small_blind_id, big_blind_id, first_to_act_id)
end

valid_button(player::Player) = !zero_bank_roll(player) && still_playing(player)

"""
    this_or_next_valid_id(id, players::Tuple)

Given an index `id`, return this index, if valid,
otherwise find the next valid index.
"""
function this_or_next_valid_id(id, players::Tuple)
    n_players = length(players)
    id = circle_index(n_players, id) # so that we can pass in id+1
    if valid_button(players[id])
        return id
    else
        state = id
        id_need_defined = true
        while id_need_defined
            state > 2*n_players && error("Broken logic in this_or_next_valid_id")
            id = circle_index(n_players, state)
            if valid_button(players[id])
                return id
            end
            state+=1
        end
    end
end

function Base.show(io::IO, table::Table, include_type = true)
    include_type && println(io, typeof(table))
    show(io, blinds(table), false)
    show(io, table.winners, false)
    println(io, "Dealer           = $(dealer_id(table))")
    println(io, "Pot              = $(table.transactions)")
    println(io, "All cards        = $(table.cards)")
    println(io, "Observed cards   = $(observed_cards(table))")
end

get_table_cards!(deck::PlayingCards.Deck) =
    Iterators.flatten(ntuple(i->pop!(deck, 1), 5)) |> collect |> Tuple
cards(table::Table) = table.cards

observed_cards(table::Table) = observed_cards(table, table.state)
observed_cards(table::Table, ::PreFlop) = ()
observed_cards(table::Table, ::Flop) = table.cards[1:3]
observed_cards(table::Table, ::Turn) = table.cards[1:4]
observed_cards(table::Table, ::River) = table.cards
current_raise_amt(table::Table) = table.current_raise_amt
initial_round_raise_amt(table::Table) = table.initial_round_raise_amt
minimum_raise_amt(table::Table) = blinds(table).small

state(table::Table) = table.state

play_out_game(table::Table) = table.play_out_game

dealer_id(table::Table) = dealer_id(table.buttons)
small_blind_id(table::Table) = small_blind_id(table.buttons)
big_blind_id(table::Table) = big_blind_id(table.buttons)
first_to_act_id(table::Table) = first_to_act_id(table.buttons)

dealer(table::Table) = players_at_table(table)[dealer_id(table)]
small_blind(table::Table) = players_at_table(table)[small_blind_id(table)]
big_blind(table::Table) = players_at_table(table)[big_blind_id(table)]
first_to_act(table::Table) = players_at_table(table)[first_to_act_id(table)]

is_dealer(table::Table, player::Player) = seat_number(player) == seat_number(dealer(table))
is_small_blind(table::Table, player::Player) = seat_number(player) == seat_number(small_blind(table))
is_big_blind(table::Table, player::Player) = seat_number(player) == seat_number(big_blind(table))
is_first_to_act(table::Table, player::Player) = seat_number(player) == seat_number(first_to_act(table))

players_at_table(table::Table) = table.players
all_playing_checked(table::Table) = all(map(x -> not_playing(x) || checked(x), players_at_table(table)))
all_playing_all_in(table::Table) = all(map(x -> not_playing(x) || all_in(x), players_at_table(table)))
all_all_in_or_checked(table::Table) = all(map(x -> checked(x) || all_in(x), players_at_table(table)))

"""
    bank_roll_leader(table::Table)

Return the player who
 - Is still playing and
 - Has the highest bank roll
and a `Bool` indicating if there
are multiple players with the largest
bank roll.
"""
function bank_roll_leader(table::Table)
    max_rbr = 0
    players = players_at_table(table)
    br_leader = first(players)
    for player in players
        still_playing(player) || continue # only consider players still playing
        pbr = round_bank_roll(player)
        if pbr > max_rbr
            br_leader = player
            max_rbr = pbr
        end
    end
    multiple_leaders = count(map(players) do player
        round_bank_roll(player) ≈ max_rbr && still_playing(player)
    end) > 1
    return br_leader, multiple_leaders
end

paid_current_raise_amount(table::Table, player::Player) =
    round_contribution(player) ≈ current_raise_amt(table)

# Can be true in exactly 2 cases:
#  1) Everyone (still playing) is all-in.
#  2) Everyone (still playing), except `player`, is all-in.
function all_oppononents_all_in(table::Table, player::Player)
    all_opp_all_in = true
    for opponent in players_at_table(table)
        seat_number(opponent) == seat_number(player) && continue
        not_playing(opponent) && continue
        if action_required(opponent)
            all_opp_all_in = false
        else
            all_opp_all_in = all_opp_all_in && all_in(opponent)
        end
    end
    return all_opp_all_in
end

# One case that we need to catch is when everyone, except
# the bank roll leader, has folded or gone all-in. In this
# case, while the bank roll leader may have required actions
# everyone else does not, so nobody can respond to their raise
# (if they chose to do so). Therefore, we must "play out" the
# entire game with no further actions.
# Note that this method is intended to be used _during_ a round.
function all_all_in_except_bank_roll_leader(table::Table)
    br_leader, multiple_leaders = bank_roll_leader(table)
    players = players_at_table(table)
    multiple_leaders && return false # the bank roll leader can go all-in

    @assert !multiple_leaders # We have a single bank roll leader

    return all(map(players) do player
        cond1 = not_playing(player)
        cond2 = all_in(player)
        cond3 = seat_number(player) == seat_number(br_leader) && !action_required(br_leader)
        any((cond1, cond2, cond3))
    end)
end

# Note that this method is only valid before or after a round has ended.
function set_play_out_game!(table::Table)
    br_leader, multiple_leaders = bank_roll_leader(table)
    players = players_at_table(table)
    if multiple_leaders
        return all(map(player -> not_playing(player) || all_in(player), players))
    end

    @assert !multiple_leaders # We have a single bank roll leader

    table.play_out_game = all(map(players) do player
        cond1 = not_playing(player)
        cond2 = all_in(player)
        cond3 = seat_number(player) == seat_number(br_leader)
        any((cond1, cond2, cond3))
    end)
end

blinds(table::Table) = table.blinds

function is_blind_call(table::Table, player::Player, amt = call_amount(table, player))
    pot_inv = pot_investment(player)
    @debug "amt = $amt, pot_investment(player) = $pot_inv"
    bb = blinds(table).big
    sb = blinds(table).small
    if is_small_blind(table, player)
        return amt ≈ sb && pot_inv ≈ sb
    elseif is_big_blind(table, player)
        return amt ≈ 0 && pot_inv ≈ bb
    else
        return amt ≈ bb && pot_inv ≈ 0
    end
end

function reset_round_bank_rolls!(table::Table)
    players = players_at_table(table)
    for player in players
        player.round_bank_roll = bank_roll(player)
    end
end

function reset_round!(table::Table)
    players = players_at_table(table)
    for player in players
        not_playing(player) && continue
        all_in(player) && continue
        player.checked = false
        player.action_required = true
        player.last_to_raise = false
        player.round_contribution = 0
    end
    table.initial_round_raise_amt = blinds(table).small
    table.current_raise_amt = 0
end

function set_state!(table::Table, state::AbstractGameState)
    table.state = state
end

# Check for winner, in case when only a single player remains
# playing
function check_for_and_declare_winner!(table::Table)
    players = players_at_table(table)
    n_players = length(players)
    table.winners.declared = count(not_playing.(players)) == n_players-1
    if table.winners.declared
        for player in players
            not_playing(player) && continue
            table.winners.players = player
        end
    end
end

#####
##### Circling the table
#####

"""
    move_buttons!(table::Table)

Move the dealer, small blind, big blind,
and first-to-act buttons to the next set
of players.
"""
function move_buttons!(table::Table)
    players = players_at_table(table)
    dealer = table.buttons.dealer
    dealer       = this_or_next_valid_id(dealer      + 1, players)
    small_blind  = this_or_next_valid_id(dealer      + 1, players)
    big_blind    = this_or_next_valid_id(small_blind + 1, players)
    first_to_act = this_or_next_valid_id(big_blind   + 1, players)
    table.buttons = Buttons(dealer, small_blind, big_blind, first_to_act)
end

"""
    circle_index(n_players, state)

A circular index for indexing into `players`.
`state = 1` corresponds to `player[1]`.
"""
circle_index(n_players, i) = mod(i-1, n_players)+1

any_actions_required(table::Table) = any(action_required.(players_at_table(table)))

abstract type TablePosition end
struct Dealer <: TablePosition end
struct SmallBlind <: TablePosition end
struct BigBlind <: TablePosition end
struct FirstToAct <: TablePosition end # (after BigBlind)

struct CircleTable{CircType,P, NITER}
    players::Tuple
    buttons::Buttons
    n_players::Int
    player::P
end

n_iterations(::CircleTable{CircType,P,NITER}) where {CircType,P,NITER} = NITER

function Base.length(ct::CircleTable)
    n_iter = n_iterations(ct)
    if !(n_iter === Inf)
        return n_iter
    else
        error("CircleTable has an infinite length")
    end
end

circle(table::Table, tp::TablePosition, n_iter = Inf) =
    CircleTable{typeof(tp),Nothing, n_iter}(table.players, buttons(table), length(table.players), nothing)

circle(table::Table, player::Player, n_iter = Inf) =
    CircleTable{typeof(player),typeof(player), n_iter}(table.players, buttons(table), length(table.players), player)

function Base.iterate(ct::CircleTable{Dealer}, state = dealer_id(ct.buttons))
    state > n_iterations(ct)+dealer_id(ct.buttons)-1 && return nothing
    i_circ = circle_index(ct.n_players, state)
    player = ct.players[i_circ]
    state == dealer_id(ct.buttons) && @assert active(player)
    return (player, state+1)
end

function Base.iterate(ct::CircleTable{SmallBlind}, state = small_blind_id(ct.buttons))
    state > n_iterations(ct)+small_blind_id(ct.buttons)-1 && return nothing
    i_circ = circle_index(ct.n_players, state)
    player = ct.players[i_circ]
    state == small_blind_id(ct.buttons) && @assert active(player)
    return (player, state+1)
end

function Base.iterate(ct::CircleTable{BigBlind}, state = big_blind_id(ct.buttons))
    state > n_iterations(ct)+big_blind_id(ct.buttons)-1 && return nothing
    i_circ = circle_index(ct.n_players, state)
    player = ct.players[i_circ]
    state == big_blind_id(ct.buttons) && @assert active(player)
    return (player, state+1)
end

function Base.iterate(ct::CircleTable{FirstToAct}, state = first_to_act_id(ct.buttons))
    state > n_iterations(ct)+first_to_act_id(ct.buttons)-1 && return nothing
    i_circ = circle_index(ct.n_players, state)
    player = ct.players[i_circ]
    state == first_to_act_id(ct.buttons) && @assert active(player)
    return (player, state+1)
end

function Base.iterate(ct::CircleTable{P},
        state = circle_index(ct.n_players, seat_number(ct.player))
    ) where {P <: Player}
    state > n_iterations(ct)+circle_index(ct.n_players, seat_number(ct.player))-1 && return nothing
    i_circ = circle_index(ct.n_players, state)
    player = ct.players[i_circ]
    state == circle_index(ct.n_players, seat_number(ct.player)) && @assert active(player)
    return (player, state+1)
end

show_cards(table::Table, player::Player{Human}) = player.cards
show_cards(table::Table, player::Player{LF}) where {LF <: AbstractAI} = "(??,??)"

#####
##### Deal
#####

function deal!(table::Table, blinds::Blinds)
    players = players_at_table(table)
    shuffle!(table.deck)
    call_blinds = true
    for (i, player) in enumerate(circle(table, SmallBlind()))

        i>length(players) && break # deal cards to each player once

        not_playing(player) && continue

        player.cards = pop!(table.deck, 2)

        if is_small_blind(table, player) && bank_roll(player) ≤ blinds.small
            contribute!(table, player, bank_roll(player), call_blinds)
            @info "$(name(player)) paid the small blind (all-in) and dealt cards: $(show_cards(table, player))"
        elseif is_big_blind(table, player) && bank_roll(player) ≤ blinds.big
            contribute!(table, player, bank_roll(player), call_blinds)
            @info "$(name(player)) paid the  big  blind (all-in) and dealt cards: $(show_cards(table, player))"
        else
            if is_small_blind(table, player)
                contribute!(table, player, blinds.small, call_blinds)
                @info "$(name(player)) paid the small blind and dealt cards: $(show_cards(table, player))"
            elseif is_big_blind(table, player)
                contribute!(table, player, blinds.big, call_blinds)
                @info "$(name(player)) paid the  big  blind and dealt cards: $(show_cards(table, player))"
            else
                @info "$(name(player)) dealt (free) cards:                   $(show_cards(table, player))"
            end
        end
    end

    table.cards = get_table_cards!(table.deck)
    @info "Table cards dealt (face-down)."
end


using HDF5, DataFrames, CSV, JLD
using StatsBase, Polynomials, CurveFit

kep_filename = joinpath(Pkg.dir("ExoplanetsSysSim"), "data", "q1_q17_dr25_stellar.csv")
gaia_filename = joinpath(Pkg.dir("ExoplanetsSysSim"), "data", "gaiadr2_keplerdr25_crossref.csv")
mast_filename = joinpath(Pkg.dir("ExoplanetsSysSim"), "data", "KeplerMAST_TargetProperties.csv")
stellar_catalog_file_out = joinpath(Pkg.dir("ExoplanetsSysSim"), "data", "q1q17_dr25_gaia_fgk.jld")

kep_df = CSV.read(kep_filename)
gaia_df = CSV.read(gaia_filename, rows_for_type_detect=30000)
mast_df = CSV.read(mast_filename)

dup_gaiaid = find(nonunique(DataFrame(x = gaia_df[:source_id])))
deleterows!(gaia_df, dup_gaiaid)

println("Total crossref target stars = ", length(gaia_df[:kepid]))

mag_diff = gaia_df[:phot_g_mean_mag].-gaia_df[:kepmag]
quant_arr = quantile(mag_diff, [0.067,0.933])   # 1.5-sigma cut
mag_match = find(x->quant_arr[1]<=x<=quant_arr[2], mag_diff)
gaia_df = gaia_df[mag_match,:]

gaia_col = [:kepid,:source_id,:parallax,:parallax_error,:astrometric_gof_al,:astrometric_excess_noise_sig,:bp_rp,:priam_flags,:teff_val,:teff_percentile_lower,:teff_percentile_upper,:radius_val,:radius_percentile_lower,:radius_percentile_upper,:lum_val,:lum_percentile_lower,:lum_percentile_upper]
df = join(kep_df, gaia_df[:,gaia_col], on=:kepid)
df = join(df, mast_df, on=:kepid)
kep_df = nothing
gaia_df = nothing

df = join(df, mast_df, on=:kepid)
kep_df = nothing
gaia_df = nothing

println("Total target stars (KOIs) matching magnitude = ", length(df[:kepid]), " (", sum(df[:nkoi]),")")

df[:teff] = df[:teff_val]
df[:teff_err1] = df[:teff_percentile_upper].-df[:teff_val]
df[:teff_err2] = df[:teff_percentile_lower].-df[:teff_val]
delete!(df, :teff_val)
delete!(df, :teff_percentile_upper)
delete!(df, :teff_percentile_lower)
df[:radius_err1] = df[:radius_percentile_upper].-df[:radius_val]
df[:radius_err2] = df[:radius_percentile_lower].-df[:radius_val]
df[:radius] = df[:radius_val]
delete!(df, :radius_val)
delete!(df, :radius_percentile_upper)
delete!(df, :radius_percentile_lower)

not_binary = (df[:astrometric_gof_al] .<= 20) .& (df[:astrometric_excess_noise_sig] .<= 5)
astro_good = []
for x in 1:length(df[:kepid])
    if !(ismissing(df[x,:priam_flags]))
        pflag = string(df[x,:priam_flags])
         if (pflag[2] == '0') & (pflag[3] == '0')
             push!(astro_good, true)
         else
             push!(astro_good, false)
         end
     else
         push!(astro_good, false)
     end
end
#astro_good = astro_good .& (df[:parallax_error] .< 0.05*df[:parallax])
astro_good = astro_good .& (df[:parallax_error] .< 0.1*df[:parallax])
planet_search = df[:kepmag] .<= 16.

has_mass = .! (ismissing.(df[:mass]) .| ismissing.(df[:mass_err1]) .| ismissing.(df[:mass_err2]))
has_radius = .! (ismissing.(df[:radius]) .| ismissing.(df[:radius_err1]) .| ismissing.(df[:radius_err2]))
has_dens = .! (ismissing.(df[:dens]) .| ismissing.(df[:dens_err1]) .| ismissing.(df[:dens_err2]))

has_cdpp = .! (ismissing.(df[:rrmscdpp01p5]) .| ismissing.(df[:rrmscdpp02p0]) .| ismissing.(df[:rrmscdpp02p5]) .| ismissing.(df[:rrmscdpp03p0]) .| ismissing.(df[:rrmscdpp03p5]) .| ismissing.(df[:rrmscdpp04p5]) .| ismissing.(df[:rrmscdpp05p0]) .| ismissing.(df[:rrmscdpp06p0]) .| ismissing.(df[:rrmscdpp07p5]) .| ismissing.(df[:rrmscdpp09p0]) .| ismissing.(df[:rrmscdpp10p5]) .| ismissing.(df[:rrmscdpp12p0]) .| ismissing.(df[:rrmscdpp12p5]) .| ismissing.(df[:rrmscdpp15p0]))
has_rest = .! (ismissing.(df[:dataspan]) .| ismissing.(df[:dutycycle]))
#has_limbd = .! (ismissing.(df[:limbdark_coeff1]) .| ismissing.(df[:limbdark_coeff2]) .| ismissing.(df[:limbdark_coeff3]) .| ismissing.(df[:limbdark_coeff4]))

mast_cut =.&(df[:numLCEXqtrs].>0,df[:numLCqtrs].>4)

is_usable = .&(has_radius, has_mass, has_rest, has_cdpp, has_dens, astro_good, not_binary, planet_search, mast_cut)

df = df[find(is_usable),:]
println("Total stars (KOIs) with valid parameters = ", length(df[:kepid]), " (", sum(df[:nkoi]),")")

fgk_color = (0.5 .<= df[:bp_rp] .<= 1.7)
df = df[find(fgk_color),:]
is_FGK = []
coeff = [2.5,-3.6,0.9]
for i in 1:6
    is_FGK = (log10.(df[:lum_val]).< map(x->polyval(Poly(coeff),x),df[:bp_rp]) + log10(1.75))
    coeff = poly_fit(df[is_FGK,:bp_rp],log10.(df[is_FGK,:lum_val]),2)
end
FGK = find(is_FGK)

println("Total FGK stars (KOIs) with valid parameters = ", length(FGK), " (", sum(df[:nkoi]),")")

# See options at: http://exoplanetarchive.ipac.caltech.edu/docs/API_keplerstellar_columns.html
# TODO SCI DETAIL or IMPORTANT?: Read in all CDPP's, so can interpolate?
symbols_to_keep = [ :kepid, :source_id, :mass, :mass_err1, :mass_err2, :radius, :radius_err1, :radius_err2, :dens, :dens_err1, :dens_err2, :rrmscdpp01p5, :rrmscdpp02p0, :rrmscdpp02p5, :rrmscdpp03p0, :rrmscdpp03p5, :rrmscdpp04p5, :rrmscdpp05p0, :rrmscdpp06p0, :rrmscdpp07p5, :rrmscdpp09p0, :rrmscdpp10p5, :rrmscdpp12p0, :rrmscdpp12p5, :rrmscdpp15p0, :dataspan, :dutycycle ]
delete!(df, [~(x in symbols_to_keep) for x in names(df)])    # delete columns that we won't be using anyway
df = df[FGK, symbols_to_keep]
tmp_df = DataFrame()
for col in names(df)
    tmp_df[col] = collect(skipmissing(df[col]))
end
df = tmp_df

save(stellar_catalog_file_out,"stellar_catalog", df)


type ScatterPlot <: Graph
	dataGroups::Vector{DataGroup}
end

ScatterPlot(dg::DataGroup) = ScatterPlot([dg])

function plotData(p::PlotFrame, g::ScatterPlot, showLegend::Bool)
	global dgCount

	for x in 1:length(g.dataGroups)
		dg = g.dataGroups[x]
		if dg.lineColor == "blank"
			dg.lineColor = goodColors[mod(x, length(goodColors))]
		end

		if dg.markerColor == "blank"
			dg.markerColor = goodColors[mod(x, length(goodColors))]
		end

		if dg.markerType == "blank"
			dg.markerType = goodSymbols[mod(x, length(goodSymbols))]
		end

		plotPoints(p, dg)

		if showLegend
			dgCount += 1
			if dg.legend.label == "Data Group "
				dg.legend.label = dg.legend.label*string(dgCount)
			end
		end
	end
end

function scatterplot(dgs::Vector{DataGroup})
	plot(PlotFrame(ScatterPlot(dgs)))
end

function scatterplot(dg::DataGroup)
	plot(PlotFrame(ScatterPlot(dg)))
end

function scatterplot(dfs::Vector{DataFrame})
	dgs = DataGroup[]
	for df in dfs
		push!(dgs, DataGroup(df))
	end
	scatterplot(dgs)
end

function scatterplot(df::DataFrame)
	scatterplot(DataGroup(df))
end

function scatterplot(ys::Array{Float64, 2})
	dfs = DataFrame[]
	for r = 1:size(y, 1)
		push!(dfs, DataFrame(x = collect(1:length(y)), y = y[r,:]))
	end
	scatterplot(dfs)
end

function scatterplot(xs::Array{Float64, 2}, ys::Array{Float64, 2})
	dfs = DataFrame[]
	for r = 1:size(y, 1)
		push!(dfs, DataFrame(x = x[r,:], y = y[r,:]))
	end
	scatterplot(dfs)
end

function scatterplot(xs::Array{Float64, 2}, ys::Array{Float64, 2}, yerrs::Array{Float64, 2})
	dfs = DataFrame[]
	for r = 1:size(ys, 1)
		push!(dfs, DataFrame(x = vec(xs[r,:]), y = vec(ys[r,:]), yerr = vec(yerrs[r,:])))
	end
	scatterplot(dfs)
end


@time begin

include("./Initial.jl")
using .Initial
include("./Frame.jl")
using .Frame
include("./DB.jl")
using .DB

time = tick
const frames = [frame(1, deepcopy(nodes))]

function startCalculations()
    insertToDB(deepcopy(nodes), 1)
    for i in 2:totalTicks
        calculateFrame(nodes)
        # push!(frames, frame(i, deepcopy(nodes)))
        insertToDB(deepcopy(nodes), i)

        println("$i frames calculated")
    end
end

connectToDB()
startCalculations()

end


# fbp/z-test.jl

using Test: @testset

list = [
"image_geom.jl"
"sino_geom.jl"
#
"cuboid_im.jl"
"disk-phantom.jl"
"ellipse_im.jl"
"ellipsoid_im.jl"
"ellipse_sino.jl"
"rect_im.jl"
"rect_sino.jl"
"rotate2d.jl"
]

for file in list
	@testset "$file" begin
		include(file)
	end
end