module QuadratureRules

    import FastGaussQuadrature
    import GenericLinearAlgebra
    import Polynomials
    import Polynomials: Polynomial

    include("utils.jl")

    include("quadrature_rule.jl")

    export QuadratureRule
    export nnodes, nodes, order, weights

    include("tabulated_quadratures.jl")

    export RiemannQuadratureLeft,
           RiemannQuadratureRight,
           MidpointQuadrature,
           TrapezoidalQuadrature

    include("chebyshev.jl")
    include("clenshaw_curtis.jl")
    include("gauss_legendre.jl")
    include("lobatto_legendre.jl")

    export ChebyshevQuadrature,
           GaussChebyshevQuadrature,
           LobattoChebyshevQuadrature,
           ClenshawCurtisQuadrature,
           GaussLegendreQuadrature,
           LobattoLegendreQuadrature

end


using Nystrom
using StaticArrays
using LinearAlgebra
using IterativeSolvers
using Plots

##
k = 3.3
pde = Maxwell(;dim=3,k)

# exact solution
G    = SingleLayerKernel(pde)
xs   = SVector(0.1,0.2,0.3)
x0   = SVector(5,5,5)
pol    = SVector(1+im,-2.,3.)
E    = (dof) -> G(dof,xs)*pol
exa  = E(x0)

# geometry and parameters
sph_radius = 2
geo = ParametricSurfaces.Sphere(;radius=sph_radius)
Ω = Geometry.Domain(geo)
Γ = boundary(Ω)

## CFIE
α = 1
β = im*k/sph_radius
order = 5         # quadrature order 1D
ndofs = Float64[]
errs = Float64[]
iterative = true;
#compress = Matrix
compress = Nystrom.IFGFCompressor(;p=(3,5,5),nmax=100);
##
for n in [4]
    M     = meshgen(Γ,(n,n))
    nmesh = NystromMesh(view(M,Γ);order)
    J     = Nystrom.diagonal_jacobian_matrix(nmesh)
    dualJ = Nystrom.diagonal_dualjacobian_matrix(nmesh)
    γ₀E   = γ₀(E,nmesh)
    rhs   = dualJ*ncross(γ₀E)

    @info "Computing operators..."
    T,K = Nystrom.maxwell_dim(pde,nmesh;compress)   # EFIE, MFIE
    @info "Assembling system matrix..."
    L   = Nystrom.assemble_maxwell_indirect_cfie(nmesh, α, β, K, T)
    @info "Solving..."
    if iterative
        ϕ_coeff = gmres(L,rhs; verbose=true,maxiter=600,restart=600,abstol=1e-6)
    else
        ϕ_coeff = L\rhs
    end
    ϕ     = J*ϕ_coeff
    Spot  = Nystrom.MaxwellEFIEPotential(pde,nmesh)
    Dpot  = Nystrom.MaxwellMFIEPotential(pde,nmesh)
    Eₐ    = (x) -> α*Dpot[ϕ](x) + β*Spot[ncross(ϕ)](x)
    er    = (Eₐ(x0) - exa)/norm(exa)

    ndof = length(Nystrom.dofs(nmesh))
    err = norm(er,Inf)

    push!(ndofs, ndof)
    push!(errs, err)
    @info "" ndof err
end

## Plot
sqrt_ndofs = sqrt.(ndofs)
fig = plot(sqrt_ndofs,errs,xscale=:log10,yscale=:log10,m=:o,label="error",lc=:black)
title = "k=$k, α=$α, β=$β, qorder=$order"
plot!(xlabel="√ndofs",ylabel="error",title=title)
for p in 1:5
    cc = errs[end]*sqrt_ndofs[end]^p
    plot!(fig,sqrt_ndofs,cc./sqrt_ndofs.^p,xscale=:log10,yscale=:log10,label="h^$p",ls=:dash)
end
display(fig)


module Check
using StatsBase
using Distributions
using ValidatedNumerics

abstract BaseCheck

function isDistribution(values :: Vector, check::BaseCheck)
    error("Unsupported check $check")
end

abstract NormalCheck <: BaseCheck

abstract ChiSquareCheck <: BaseCheck

abstract ChiSquareCheckByIntervalCount <: ChiSquareCheck

abstract ChiSquareCheckByIntervalStep <: ChiSquareCheck

immutable LargeNChiSquareCheck <: ChiSquareCheckByIntervalCount
    # Mann H.B., Wald A., On the choice of number of intervals in the applciation of the chi-square test – 1942
    # Hollander M., Proshan F., Testing whether new is better than used – 1972
    error_probability :: Real
    distribution
end

function calculateBinCount(values :: Vector, check :: LargeNChiSquareCheck)
    n = length(values)
    if n >= 500
        result = 4 * 0.75^0.2 * ( n - 1 )^0.4
        round(Int, result)
    else
        error("$n show be greater then or equal 500.")
    end
end

immutable SturgesChiSquareCheck <: ChiSquareCheckByIntervalCount
    # Herbert A. Sturges, The Choice of a Class Interval – 1926
    error_probability :: Real
    distribution
end

function calculateBinCount(values :: Vector, check :: SturgesChiSquareCheck)
    n = length(values)
    round(Int, 1 + log2(n))
end

immutable ScottChiSquareCheck <: ChiSquareCheckByIntervalStep
    # David W. Scott, On optimal and data-based histograms –  1979
    error_probability :: Real
    distribution
end

function calculateBinStep(values :: Vector, check :: ScottChiSquareCheck)
    n = length(values)
    s = std(check.distribution)
    3.49 * s * n^(-1/3)
end

immutable TaylorChiSquareCheck <: ChiSquareCheckByIntervalStep
    # Charles C. Taylor, Akaike's information criterion and the histogram – 1987
    error_probability :: Real
    distribution
end

function calculateBinStep(values :: Vector, check :: TaylorChiSquareCheck)
    n = length(values)
    s = std(check.distribution)
    2.72 * s * n^(-1/3)
end

immutable FreedmanDiaconisChiSquareCheck <: ChiSquareCheckByIntervalStep
    # David Freedman, Persi Diaconis, On the Histogram as a Density Estimator: L2 Theory – 1981
    error_probability :: Real
    distribution
end

function calculateBinStep(values :: Vector, check :: FreedmanDiaconisChiSquareCheck)
    n = length(values)
    q1 = quantile(check.distribution, 0.25)
    q3 = quantile(check.distribution, 0.75)
    iqr = q3 - q1
    2 * iqr * n^(-1/3)
end

immutable DoaneChiSquareCheck <: ChiSquareCheckByIntervalCount
    # David P. Doane, Aesthetic Frequance Classifications – 2012
    error_probability :: Real
    distribution
end

function calculateBinCount(values :: Vector, check :: DoaneChiSquareCheck)
    average = mean(values)
    s = std(values)
    n = length(values)

    reduced_per_pow = (p) -> mapreduce(x -> (x - average)^p, +, values)
    sqrt_q = reduced_per_pow(3) / reduced_per_pow(2)^(3/2)
    sigma_sqrt_q = sqrt((6 * (n - 2)) / ((n + 1) * (n + 3)))

    result = 1 + log2(n) + log2(1 + sqrt_q/sigma_sqrt_q)
    round(Int, result)
end

immutable WichardChiSquareCheck <: ChiSquareCheckByIntervalCount
    # Jorg D. Wichard, Ronald Kuhne, Binding site detection via mutual information - 2008
    error_probability :: Real
    distribution
end

function calculateBinCount(values :: Vector, check :: WichardChiSquareCheck)
    average = mean(values)
    variance = var(values)
    n = length(values)

    reduced_per_pow = (p) -> mapreduce(x -> (x - average)^p, +, values)
    kurtosis = reduced_per_pow(4) / ((n - 1) * variance^2)
    result = 1 + log2(n) + log2(1 + kurtosis / sqrt(n / 6))
    round(Int, result)
end

immutable ChiSquareNormalCheck <: ChiSquareCheck
    # Dahiya R.C., Gurland J., How many classes in the Pearson chi-square test? – 1973
    error_probability :: Real
    bin_count :: Int
    distribution
end

function ChiSquareNormalCheck(error_probability :: Real, bin_count :: Int, mu :: Real, sigma :: Real)
    ChiSquareNormalCheck(error_probability, bin_count, Normal(mu, sigma))
end

function fitHistogram(values :: Vector, check :: ChiSquareNormalCheck)
    mu, sigma = params(check.distribution)
    edges = histEdgesForNormalDistribution(check.bin_count, mu, sigma)
    hist = fit(Histogram, values, edges)
end

function histEdgesForNormalDistribution(bin_count, mu, sigma)
    const general_edges_per_bin_count = Dict(
        3=> [-0.4307],
        4=> [-0.6745, 0],
        5=> [-0.8416, -0.2533],
        6=> [-0.9674, -0.4307, 0],
        7=> [-1.0676, -0.5659, -0.1800],
        8=> [-1.1503, -0.6745, -0.3186, 0],
        9=> [-1.2206, -0.7647, -0.4307, -0.1397],
        10=> [-1.2816, -0.8416, -0.5244, -0.2533, 0],
        11=> [-1.3352, -0.9085, -0.6046, -0.3488, -0.1142],
        12=> [-1.3830, -0.9674, -0.6745, -0.4307, -0.2194, 0],
        13=> [-1.4261, -1.0201, -0.7363, -0.5024, -0.2934, -0.0966],
        14=> [-1.4652, -1.0676, -0.7916, -0.5660, -0.3661, -0.1800, 0],
        15=> [-1.5011, -1.1108, -0.8416, -0.6229, -0.4307, -0.2533, -0.0837]
    )

    semi_edges = general_edges_per_bin_count[bin_count]
    semi_edges_length = length(semi_edges)

    edge_symenty = semi_edges_length + (iseven(bin_count) ? 0 : 1)
    calculate_edge = (semi_edges, i) -> begin
        i <= semi_edges_length ? semi_edges[i] : -semi_edges[end - (i - edge_symenty)]
    end

    edges = [calculate_edge(semi_edges, i) for i in 1:(bin_count - 1)]
    far_left_edge, far_right_edge = -5, 5
    edges = vcat([far_left_edge], edges, [far_right_edge])
    mu + sigma*edges
end

function chiSquareCriticalValue(bin_count, check :: ChiSquareNormalCheck)
    critical_values_per_bin_count_and_probability = Dict(
        (3, 0.10)=> 2.371, (3, 0.05)=> 3.248, (3, 0.01)=> 5.418,
        (4, 0.10)=> 3.928, (4, 0.05)=> 5.107, (4, 0.01)=> 7.917,
        (5, 0.10)=> 5.442, (5, 0.05)=> 6.844, (5, 0.01)=> 10.075,
        (6, 0.10)=> 6.905, (6, 0.05)=> 8.479, (6, 0.01)=> 12.021,
        (7, 0.10)=> 8.322, (7, 0.05)=> 10.083, (7, 0.01)=> 13.837,
        (8, 0.10)=> 9.703, (8, 0.05)=> 11.543, (8, 0.01)=> 15.567,
        (9, 0.10)=> 11.055, (9, 0.05)=> 13.007, (9, 0.01)=> 17.234,
        (10, 0.10)=> 12.384, (10, 0.05)=> 14.438, (10, 0.01)=> 18.852,
        (11, 0.10)=> 13.694, (11, 0.05)=> 15.843, (11, 0.01)=> 20.431,
        (12, 0.10)=> 14.688, (12, 0.05)=> 17.226, (12, 0.01)=> 21.977,
        (13, 0.10)=> 16.267, (13, 0.05)=> 19.589, (13, 0.01)=> 23.495,
        (14, 0.10)=> 17.535, (14, 0.05)=> 19.937, (14, 0.01)=> 24.990,
        (15, 0.10)=> 18.792, (15, 0.05)=> 21.270, (15, 0.01)=> 26.464
    )
    bin_coint_and_probability = (bin_count, check.error_probability)
    critical_values_per_bin_count_and_probability[bin_coint_and_probability]
end

function fitHistogram(values :: Vector, check :: ChiSquareCheck)
    values_count = length(values)
    bin_count = calculateBinCount(values, check)
    hist = fit(Histogram, values, nbins=bin_count)
end

function calculateBinCount(values :: Vector, check :: WichardChiSquareCheck)
    average = mean(values)
    variance = var(values)
    n = length(values)

    reduced_per_pow = (p) -> mapreduce(x -> (x - average)^p, +, values)
    kurtosis = reduced_per_pow(4) / ((n - 1) * variance^2)
    result = 1 + log2(n) + log2(1 + kurtosis / sqrt(n / 6))
    round(Int, result)
end

function countsAndHistogram(values :: Vector, check :: ChiSquareCheck)
    values_count = length(values)
    hist = fitHistogram(values, check)
    bin_count = length(hist.weights)
    intervals_and_weights = zip(edgesToIntervals(hist.edges[1]), hist.weights)
    (values_count, bin_count, intervals_and_weights)
end

function fitHistogram(values :: Vector, check :: ChiSquareCheck)
    values_count = length(values)
    bin_count = calculateBinCount(values, check)
    hist = fit(Histogram, values, nbins=bin_count)
end

function calculateBinCount(values :: Vector, check :: ChiSquareCheckByIntervalStep)
    values_count = length(values)
    max, min = maximum(values), minimum(values)
    intervals_step = calculateBinStep(values, check)
    round(Int, (max - min) / intervals_step)
end

function histogram(values :: Vector, check :: ChiSquareCheck)
    _, _, hist = countsAndHistogram(values, check)
    hist
end

function isDistribution(values :: Vector, check :: ChiSquareCheck)
    values_count, bin_count, hist = countsAndHistogram(values, check)

    chi_square = mapreduce(
        (x) -> chiSquareEstimate(x, values_count, check), +,
        hist
    )

    chi_square_quantile = chiSquareCriticalValue(bin_count, check)
    chi_square <= chi_square_quantile
end

function chiSquareCriticalValue(bin_count, check :: ChiSquareCheck)
    freedom_degree_number = bin_count - 1
    getChiSquareQuantile(check.error_probability, freedom_degree_number)
end

function getChiSquareQuantile(value, freedom_degree_number)
    d = Chisq(freedom_degree_number)
    quantile(d, value)
end

function edgesToIntervals(edges)
    collected = collect(edges)
    downs = collected[1:end-1]
    ups = collected[2:end]
    [@interval(i[1], i[2]) for i in zip(downs, ups)]
end

function chiSquareEstimate(edgeAndValue, total_count, check :: ChiSquareCheck)
    interval = edgeAndValue[1]
    count_in_interval = edgeAndValue[2]
    d = check.distribution
    theoretical_probability = cdf(d, interval.hi) - cdf(d, interval.lo)
    theoretical_hist = total_count*theoretical_probability
    ((count_in_interval - theoretical_hist)^2)/theoretical_hist
end

end


function internal_wave_solution(; L, background_stratification=false)
    # Internal wave parameters
     ν = κ = 1e-9
    z₀ = -L/3
     δ = L/20
    a₀ = 1e-3
     m = 16
     k = 1
     f = 0.2
     ℕ = 1.0
     σ = sqrt( (ℕ^2*k^2 + f^2*m^2) / (k^2 + m^2) )

    # Numerical parameters
    Δt = 0.01 * 1/σ

    cᵍ = m * σ / (k^2 + m^2) * (f^2/σ^2 - 1)
     U = a₀ * k * σ   / (σ^2 - f^2)
     V = a₀ * k * f   / (σ^2 - f^2)
     W = a₀ * m * σ   / (σ^2 - ℕ^2)
     B = a₀ * m * ℕ^2 / (σ^2 - ℕ^2)

    a(x, y, z, t) = exp( -(z - cᵍ*t - z₀)^2 / (2*δ)^2 )

    u(x, y, z, t) = a(x, y, z, t) * U * cos(k*x + m*z - σ*t)
    v(x, y, z, t) = a(x, y, z, t) * V * sin(k*x + m*z - σ*t)
    w(x, y, z, t) = a(x, y, z, t) * W * cos(k*x + m*z - σ*t)

    # Buoyancy field depends on whether we use background fields or not
    wavy_b(x, y, z, t) = a(x, y, z, t) * B * sin(k*x + m*z - σ*t)
    background_b(x, y, z, t) = ℕ^2 * z

    if background_stratification # Move stratification to a background field
        background_fields = (; b=background_b)
        b = wavy_b
    else
        background_fields = NamedTuple()
        b(x, y, z, t) = wavy_b(x, y, z, t) + background_b(x, y, z, t)
    end

    u₀(x, y, z) = u(x, y, z, 0)
    v₀(x, y, z) = v(x, y, z, 0)
    w₀(x, y, z) = w(x, y, z, 0)
    b₀(x, y, z) = b(x, y, z, 0)

    solution = (; u, v, w, b)

    # Form model keyword arguments
    closure = IsotropicDiffusivity(ν=ν, κ=κ)
    buoyancy = BuoyancyTracer()
    tracers = :b
    coriolis = FPlane(f=f)
    model_kwargs = (; closure, buoyancy, tracers, coriolis)

    return solution, model_kwargs, background_fields, Δt, σ
end

function internal_wave_dynamics_test(model, solution, Δt)
    # Make initial conditions
    u₀(x, y, z) = solution.u(x, y, z, 0)
    v₀(x, y, z) = solution.v(x, y, z, 0)
    w₀(x, y, z) = solution.w(x, y, z, 0)
    b₀(x, y, z) = solution.b(x, y, z, 0)

    set!(model, u=u₀, v=v₀, w=w₀, b=b₀)

    simulation = Simulation(model, stop_iteration=10, Δt=Δt)
    try run!(simulation); catch; end # so the test continues to execute if there's a NaN

    # Tolerance was found by trial and error...
    @test relative_error(model.velocities.u, solution.u, model.clock.time) < 1e-4

    return nothing
end


#---------------------------------------------------------------------#
#This function warps a 2D grid
#Written by F.X. Giraldo on 5/2019
#           Department of Applied Mathematics
#           Naval Postgraduate School
#           Monterey; CA 93943-5216
#---------------------------------------------------------------------#
function warp_grid!(coord,Npoin,DFloat)

    for i=1:Npoin
        x, y = coord[1,i], coord[2,i]
        coord[1,i]=x + sin(π * x) * sin(2 * π * y) / 15
        coord[2,i]=y + sin(2 * π * x) * sin(π * y) / 15
    end

end


@doc raw"""
The Newton form for an interpolating polynomial represents ``f(x)`` at
points ``\tau``` by a sum.

``\sum_{i=0}^n f[x_0,\ldots,x_i]\prod_{j=0}^{i-1}(x-x_j)

Here, the `a` are the divided differences and ``\tau`` are the `x[j]`.
"""
struct NewtonForm{T}
    τ::Vector
    a::Vector{T}
end

"""
Construct a NewtonForm. From Conte and deBoor 1980, Eqn. 4.8.
"""
function NewtonForm(τ::Vector, f::Vector{T}) where {T <: Real}
    k = len(τ)
    a = zeros(T)
    dd = DividedDifference{T}()
    for j = 1:k
        a[j] = divided_difference(1, j, τ, f, dd)
    end
    NewtonForm{T}(τ, a)
end

function (nf::NewtonForm{T})(x) where {T}
    k = len(nf.a)
    total = zero(T)
    for j = k:-1:2
        total += a[j]
        total *= (x - τ[j - 1])
    end
    total += a[1]
    total
end

"""
Construct a polynomial that interpolates a function, using polynomials with zeroes.
This should match the constructor above. It comes from deBoor equation 4.2.
"""
function single_polynomial(τ::Vector{X}, f::Vector{T}) where {T <: Real, X<: Real}
    a = zeros(T, length(f))
    for i = 1:length(f)
        total = one(T)
        for l1 = 1:(i - 1)
            total *= 1 / (τ[l1] - τ[i])
        end
        for l2 = (i + 2):length(f)
            total *= 1 / (τ[l2] - τ[i])
        end
        a[i] = f[i] * total
    end
    NewtonForm{T}(τ, a)
end


include(joinpath("..","..","src","MaxwellSolver","PoissonFourier1D.jl"))
import .PoissonFourier1D

@testset "Test 1D Poisson solvers" begin
    Δx = 0.01
    ρ = [sin(2π*i*Δx) for i = 0:1000]
    e = similar(ρ)
    solve! = PoissonFourier1D.generate_solver(ρ, Δx)
    solve!(e, ρ)
    @test sum(@. (e - [-cos(2π*i*Δx)/2π for i = 0:1000])^2) ≈ 0 atol=1e-3
end


# %% value of τ demonstration
using DrWatson
@quickactivate "NonlinearDynamicsTextbook"
include(srcdir("style.jl"))
using DynamicalSystems, PyPlot, Random

d = 3
τ1 = 1
τ2 = 17
τ3 = 3τ2
θ = -30
e = 10
lo = Systems.lorenz([0, 10, 0.0])
A = trajectory(lo, 1000; Ttr=1000, reltol = 1e-12, abstol = 1e-12)
w = A[:, 1]
close("all")
N = 5000
fig = figure(figsize = (figx, 0.75figy))

for (i, τ) in enumerate((0, τ1, τ2, τ3))
    R = τ == 0 ? A : embed(w, d, τ)
    ax = subplot(1, 4, i, projection = "3d")
    ax.plot(R[1:N, 1], R[1:N, 2], R[1:N, 3],
    color = i == 1 ? "C0" : "C1", lw = 1,
    zorder = 1)
    ax.view_init(elev=e, azim=θ)
    ax.set_title(i == 1 ? "original" : "\$R:\\,\\tau=$(τ)\$", pad = -40)
    for a in (ax.xaxis, ax.yaxis, ax.zaxis); a.set_ticklabels([]); end
    dj = 167
    for j in (827, )
        χ, ψ, ζ = R[j:dj:j+dj, 1], R[j:dj:j+dj, 2], R[j:dj:j+dj, 3]
        ax.scatter3D(χ, ψ, ζ, depthshade = false, c = "C2", zorder = 99, s = 50)
    end
    ax.dist = 8
end

# ax = subplot(1, 4, 1, projection = "3d")
# ax.plot3D(x[1:N], y[1:N], z[1:N], lw = 1.0, color = "C1")
# ax.view_init(elev=e, azim=θ)
# for a in (ax.xaxis, ax.yaxis, ax.zaxis); a.set_ticklabels([]); end

fig.tight_layout(pad=0.1)
fig.subplots_adjust(wspace = 0.0001)#, bottom = -0.1, top = 1, left = 0.0, right = 1)
# wsave(plotsdir("6", "taudemonstration"), fig)


## runtests.jl : run all test files

using GaussianRandomFields
using Suppressor
using Plots
using Test

# test indexsets
include("test_covariance_functions.jl")
include("test_cholesky.jl")
include("test_spectral.jl")
include("test_karhunen_loeve.jl")
include("test_circulant_embedding.jl")
include("test_gaussian_random_fields.jl")
include("test_separable.jl")
include("test_fem.jl")
include("test_show.jl")
include("test_plot.jl")


@testset "Base Extension of Grid-search Struct" begin
    ranges = [[1.,2.,10], [3.,4.,10], [5.,6.,10]]
    param_ranges = ParameterGrid(ranges)

    @testset "Constuctor of ParameterGrid" begin
        @test param_ranges.len == FindSteadyStates.mul(Integer,[i[end] for i in ranges])
        @test length(param_ranges.indexes) == param_ranges.len
    end

    @testset "Length & size" begin
        #Base.length
        @test Base.length(param_ranges) == (FindSteadyStates.mul(Integer,[i[end] for i in ranges]))
        #Base.size
        @test Base.size(param_ranges) == ((FindSteadyStates.mul(Integer,[i[end] for i in ranges])),)
    end
end

@testset "Recursive index" begin

    ranges = [ (1,4,4), (2,8,4), (1,2,2) ]
    ind_max = FindSteadyStates.mul([i[end] for i in ranges])

    # first index vector
    vec_one = [1 for i in ranges]
    # last index vector
    vec_end = [i[end] for i in ranges]

    param_ranges = ParameterGrid(ranges)

    @test vec_one == recursive_index(param_ranges, 1)
    @test vec_end == recursive_index(param_ranges, ind_max)
    @test [4,1,1] == recursive_index(param_ranges, 4)
    @test [1,2,1] == recursive_index(param_ranges, 5)

end

## Indexing
@testset "get index for parameter generation" begin
    # Ranges
    ranges = [ (1.,4.,4), (2.,8.,4), (1.,2.,2) ]
    total_is = FindSteadyStates.mul([i[end] for i in ranges])

    # Grid search samplers
    para1 = ParameterGrid(ranges; method=UniformGrid())
    para2 = ParameterGrid(ranges; method=LogGrid(decay=10.))

    # number of index should equal to total grids.
    @test length(para1) == total_is
    @test length(para2) == total_is

    # Sampling: para 1
    @info "Sample the range: $ranges"
    @info "Print grid vecs: uniform distributed"
    for i in 1:1:length(para1)
        println(para1[i])
    end

    # Sampling Para 2
    @info "Print grid vecs: log distributed"
    for i in 1:1:length(para2)
        println(para2[i])
    end
    @info "DONE"
end

@testset "Iterator of ParameterGrid" begin

    ranges = [ (1.,4.,4), (2.,8.,4), (1.,2.,2) ]
    param = ParameterGrid(ranges)
    @info "Display ranges ($ranges)"

    @testset "iteration" begin

    for (i,j) in enumerate(param )
        println(j)
        @test param[i] == j
    end
    end

    @info "Done"
end

@testset "Range input" begin
    g1 = ParameterGrid([
        1.0:1.0:10.0,
        1:1.0:10.0
    ])

    g2 = ParameterGrid([
        range(0,10;length=10),
        range(0,10;length=10)
    ])

    r1 = ParameterRandom([
        1.0:1.0:10.0,
        1:1.0:10.0
    ])

    r2 = ParameterRandom([
        range(0,10;length=10),
        range(0,10;length=10)
    ])

    # test
    @test length(g2) == 10*10
    @test length(g1) == 10*10
    @test length(r1) == 10*10
    @test length(r2) == 10*10

end


dropmissing(x) = collect(skipmissing(x))

"""
    smallest(da::AbstractArray, n::Int) or smallest(df::DataFrame, varname::Symbol, n::Int)

List the `n` smallest values in `da` in a descending order. The default `n` is 5.
"""
function smallest(da::AbstractArray; n::Int = 5)
	return sort(collect(skipmissing(da)))[1:n]
end
smallest(df::DataFrame,varname::Symbol; n = 5) = smallest(df[!,varname], n = n)

"""
    largest(da::AbstractArray, n::Int) or largest(df::DataFrame, varname::Symbol, n::Int)

List the `n` largest values in `da` in an ascending order. The default `n` is 5.
"""
function largest(da::AbstractArray; n::Int = 5)
	return sort(collect(skipmissing(da)))[end-n+1:end]
end
largest(df::DataFrame,varname::Symbol; n = 5) = largest(df[!,varname], n = n)

"""
    univariate(da::AbstractArray) or univariate(df::DataFrame,varname::Symbol)

Produce univariate statistics from the `da` and return a DataFrame with two columns,
:Statistic and :Value. Computed statistics include `N_Total` (number of rows
in the input DataArray), `N_Missing` (number of rows with missing values), `N_Used` (number of non-missing rows),
`Sum` (sum), `Mean` (mean), `SD` (standard deviation), `Var` (variance), `Min` (minimum),
`P25` (25th percentile), `Median` (median), `P75` (75th percentile), `Max` (maximum),
`Skewness` (skewness), and `Kurtosis` (kurtosis).
"""
function univariate(da::AbstractVector)

    da2 = dropmissing(da)

	return DataFrame(
		Statistic = [:N_Total,
			:N_Missing,
			:N_Used,
			:Sum,
			:Mean,
			:SD,
			:Var,
			:Min,
			:P25,
			:Median,
			:P75,
			:Max,
			:Skewness,
			:Kurtosis],
		Value = [size(da,1),
			size(da,1) - size(da2,1),
			size(da2,1),
			sum(da2),
			mean(da2),
			sqrt(var(da2)),
			var(da2),
			minimum(da2),
			quantile(da2,0.25),
			quantile(da2,0.5),
			quantile(da2,0.75),
			maximum(da2),
			skewness(da2),
			kurtosis(da2)]
	)
end
univariate(df::DataFrame,var::Symbol) = univariate(df[!,var])

function strval(val::AbstractFloat)
  return @sprintf("%.2f",val)
end
function strval(val::AbstractFloat, decimals::Int)
    if decimals == 1
        return @sprintf("%.1f",val)
    elseif decimals == 2
        return @sprintf("%.2f",val)
    elseif decimals == 3
        return @sprintf("%.3f",val)
    elseif decimals == 4
        return @sprintf("%.4f",val)
    elseif decimals == 5
        return @sprintf("%.5f",val)
    elseif decimals == 6
        return @sprintf("%.6f",val)
    else
        return @sprintf("%f",val)
    end
end
function strval(val::Integer)
  return @sprintf("%.0d",val)
end

function getdictval(dt::Dict,val)
    return haskey(dt,val) ? dt[val] : val
end

"""
    tabstat(df::DataFrame,varname::Symbol,groupvar::Symbol;s::Vector{Function} = [N,mean,sd,minimum,p25,median,p75,maximum ],wt::Union{Nothing,Symbol}=nothing)

Produce a DataFrame that contains summary statistics for each `groupvar` subgroup
of the `varname` column in the `df`. The following are computed: `n` (total non-missing rows),
`mean` (mean), `sd` (standard deviation), `min` (minimum), `p25` (25th percentile),
`median` (median), `p75` (75th percentile), and `max` (maximum).
"""
function tabstat(indf::DataFrame, var1::Symbol, groupvar::Symbol; s::Vector{Function} = [N,mean,sd,minimum,p25,median,p75,maximum ],wt::Union{Nothing,Symbol}=nothing)

    if length(s) == 0
        error("No statistic functions were specified.")
    end

    # strip prepended "Stella." from the function list
    namevec = [Symbol(replace(string(x),r"(Stella|Statistics)\." => "")) for x in s]

    # number of levels in the groupvar
    lev = DataFrames.levels(indf[!,groupvar])
    gvnum = length(lev)

    # output DataFrame
    outdf = DataFrame()

    # groupvar
    outdf[!,groupvar] = Vector{Union{Missing,eltype(groupvar)}}(undef, gvnum)

    # stats as variables
    for j = 1:length(namevec)

        # for N, create a vector of integers
        if namevec[j] == :N
            outdf[!,namevec[j]] = Vector{Union{Missing,Int}}(undef, gvnum)
        # for all other stats, create float64 vectors
        else
            outdf[!,namevec[j]] = Vector{Union{Missing,Float64}}(undef, gvnum)
        end
    end

    # get stat for each level
    for subdf in groupby(indf, groupvar)

        # level index to be used as the row number in outdf
        gidx = findfirst(x->x==subdf[1,groupvar],lev)

        # non-missing values only
        da = collect(skipmissing(subdf[var1]))

        # if array is empty, there is nothing to process
        if length(da) == 0
            continue
        end

        # groupvar value
        outdf[gidx,groupvar] = subdf[1,groupvar]

        # obtain stats
        for j = 1:length(namevec)
            outdf[gidx,namevec[j]] = s[j](da)
        end
    end

    sort!(outdf,groupvar)

    return outdf
end

#----------------------------------------------------------------------------
# stats by subdataframe - begin
#----------------------------------------------------------------------------

"""
    p5(x::AbstractVector)
    p10(x::AbstractVector)
    p25(x::AbstractVector)
    p50(x::AbstractVector)
    p75(x::AbstractVector)
    p90(x::AbstractVector)
    p95(x::AbstractVector)

Produce percentile values (e.g., p5() produces 5% percentile, p10 10% percentile, etc)
from `x` AbstractVector. If `x` is a DataArray, it must be stripped of NA's before used in these functions.
Other percentile functions not shown here can easily be created by, for example,
`p15(x) = quantile(x,.15)`.
"""
p5(x) = quantile(x,.5)
p10(x) = quantile(x,.1)
p25(x) = quantile(x,.25)
p50(x) = quantile(x,.5)
p75(x) = quantile(x,.75)
p90(x) = quantile(x,.9)
p95(x) = quantile(x,.95)

"""
    cv(x::AbstractVector)

Is an alias of `variation()`.
"""
cv(x) = variation(x)

"""
    se(x::AbstractVector)

Is an alias of `sem(x)`.
"""
se(x) = sem(x)

"""
    sd(x::AbstractVector)

Is an alias of `std(x)`.
"""
sd(x) = std(x)

"""
    N(x::AbstractVector)

Is an alias of `size(x,1)`.
"""
N(x) = size(x,1)
# substat - pass functions to produce the stats by subframe
# functions: mean, var, std, sum, count, kurtosis, skewness, median, percentiles, maximum, minimum
#            cov, se

"""
    substat(df::DataFrame, varname::Symbol, groupvars::Array{Symbol,1}, func::Function)

Produces a vector of the same length as the `df` that contains summary statistics of
the `varname` column computed by `func` for each subgroup stratified by `groupvars`.
Multiple columns can be used as `groupvar`'s if provided in an array of Symbols.
Any function that can take one numeric vector as input can be used. Percentile functions
such as those that take two arguments (e.g., `quantile(x,.25)` for a 25th percentile of x)
should first be converted to a function that takes one argument (e.g, `p25(x) = quantile(x,.25)`).
The following percentile functions are already defined: `p5`, `p10`, `p25`, 'p50', `p75`, and `p90`.
This function emulates the Stata's `egen functions` such as `egen testmean = mean(test), by(groupvar)`.

```
julia> df[:testmean] = substat(df,:test, :class, mean)
```
"""
function substat(df::DataFrame, varname::Symbol, groupvars::Vector{Symbol}, func::Function)

    if (Missings.T(eltype(df[varname])) <: Number) == false
        error("Only numeric variables are allowed.")
    end

    df2 = by(df,groupvars) do subdf
        da = Vector(collect(skipmissing(subdf[varname])))
        if length(da) == 0
            DataFrame(x1 = missing)
        else
            DataFrame(x1 = func(da))
        end
    end

    df4 = DataFrame()
    df4[groupvars] = df[groupvars]
    df4[:___obs___] = collect(1:size(df4,1))

    df5 = join(df4, df2, on = groupvars, kind = :left)
    sort!(df5,cols=[:___obs___])
    return df5[:x1]
end
substat(df::DataFrame, varname::Symbol, groupvar::Symbol, func::Function) = substat(df,varname,[groupvar],func)

#----------------------------------------------------------------------------
# eform
#----------------------------------------------------------------------------
function eform(glmout::StatsModels.RegressionModel)

    # family and link function
    if isa(glmout.model,GeneralizedLinearModel)
        distrib = glmout.model.rr.d
        linkfun = GLM.Link(glmout.model.rr)
    else
        error("GLM model is required as the argument")
    end

	coeftable2 = coeftable(glmout)

	# estimates
	coeftable2.cols[1] = exp.(coeftable2.cols[1])

	# standard errors
	coeftable2.cols[2] = coeftable2.cols[1] .* coeftable2.cols[2]

	# rename column1 to OR
    coeftable2.colnms[1] = coeflab(distrib,linkfun)

	return coeftable2
end
function eform(glmout::StatsModels.RegressionModel, labels::Label)

    # family and link function
    if isa(glmout.model,GeneralizedLinearModel)
        distrib = glmout.model.rr.d
        linkfun = GLM.Link(glmout.model.rr)
    else
        error("GLM model is required as the argument")
    end

	coeftable2 = coeftable(glmout)

	# estimates
	coeftable2.cols[1] = exp.(coeftable2.cols[1])

	# standard errors
	coeftable2.cols[2] = coeftable2.cols[1] .* coeftable2.cols[2]

	# rename column1 to OR
	coeftable2.colnms[1] = coeflab(distrib,linkfun)

	# parse the row names and change variable names and values
	for i in 2:length(coeftable2.rownms)
		# parse row name and split into a tuple (varname, value)
		if occursin(": ",coeftable2.rownms[i])
			(varname,value) = split(coeftable2.rownms[i],": ")
		else
			(varname,value) = (coeftable2.rownms[i],"")
		end

        # get variable label from the label dictionary
		varlabel = varlab(labels,Symbol(varname))
        if varlabel == ""
            varlabel = varname
        end
        if value != ""
		    vlabel = vallab(labels,Symbol(varname),parse(Int,value))
        end

        # If value is 1 and value label is Yes, it is a binary variable
		# do not print
		if value == 1 && ismatch(r"^ *yes *$"i,vlabel)
			coeftable2.rownms[i] = varlabel
		elseif value != ""
			coeftable2.rownms[i] = string(varlabel, ": ", vlabel)
        else
            coeftable2.rownms[i] = varlabel
		end
	end

	return coeftable2
end

function coeflab(d::UnivariateDistribution,l::Link)
    if (isa(d,Bernoulli) || isa(d,Binomial)) && isa(l,LogitLink)
        return "OR"
    elseif (isa(d,Binomial) || isa(d,NegativeBinomial)) && isa(l,LogLink)
        return "RR"
    elseif isa(d,Poisson) && isa(l,LogLink)
        return "IRR"
    else
        return "exp(Est)"
    end
end

import GLM.coeftable
function GLM.coeftable(r::StatsModels.RegressionModel,labels::Label)
    coeftable2 = coeftable(r)

    # parse the row names and change variable names and values
	for i in 2:length(coeftable2.rownms)
		# parse row name and split into a tuple (varname, value)
		if occursin(": ",coeftable2.rownms[i])
			(varname,value) = split(coeftable2.rownms[i],": ")
		else
			(varname,value) = (coeftable2.rownms[i],"")
		end

        # get variable label from the label dictionary
		varlabel = varlab(labels,Symbol(varname))
        if varlabel == ""
            varlabel = varname
        end
        if value != ""
		    vlabel = vallab(labels,Symbol(varname),parse(Int,value))
        end

        # If value is 1 and value label is Yes, it is a binary variable
		# do not print
		if value == 1 && ismatch(r"^ *yes *$"i,vlabel)
			coeftable2.rownms[i] = varlabel
		elseif value != ""
			coeftable2.rownms[i] = string(varlabel, ": ", vlabel)
        else
            coeftable2.rownms[i] = varlabel
		end
	end

	return coeftable2
end

#--------------------------------------------------------------------------
# pairwise correlations
#--------------------------------------------------------------------------
struct pwcorr_return
    r::AbstractArray
    pval::AbstractArray
    N::AbstractArray
    colnms::Vector
end

"""
    pwcorr(a::AbstractArray)
    pwcorr(a::AbstractVector...)
    pwcorr(df::DataFrame, args::Symbol...)

Produces a n x n matrix that contain correlation coefficients between all `n` columns.
When the option `p = true` is specified, it produces a second n x n matrix containing
p-values which is calculated using the Fisher transformation. When the option `out = true`
is set, pwcorr prints the correlation coeffients and p-values (if specified) to the console
in addition to returning them in arrays.
"""
function pwcorr(a::AbstractArray)

    cols = size(a,2)
    N = zeros(Int64,cols,cols)
    r = zeros(Float64,cols,cols)
    pval = zeros(Float64,cols,cols)
    if typeof(a) == DataFrame
        colnames = names(a)
    else
        colnames = collect(1:size(a,2))
    end

    for j = 1:cols
        for i = j:cols
            if i != j
                if typeof(a) == DataFrame
                    x = Array(a[completecases(a[[i,j]]),[i,j]])
                else
                    x = a[:,[i,j]]
                end
                r[i,j] = r[j,i] = cor(x)[1,2]
                rows = N[i,j] = N[j,i] = size(x,1)
                z = (sqrt(rows - 3) / 2)*log((1+r[i,j])/(1-r[i,j]))
                pval[i,j] = pval[j,i] = Distributions.ccdf(Distributions.Normal(),z) / 2.0
            end
        end
    end
    return pwcorr_return(r, pval, N, colnames)
end
pwcorr(a::AbstractArray...) = pwcorr(hcat(a...))
pwcorr(a::DataFrame, args::Vector{Symbol}; out=true) = pwcorr(df[args], out = out)

#--------------------------------------------------------------------------
# ranksum(), signrank(), signtest()
#--------------------------------------------------------------------------
#
# struct ranksum_return
#     df::DataFrame
#     varname::Symbol
#     t::Float64
#     U::Float64
#     s²::Float64
#     var_t::Float64
#     z::Float64
#     pvalue::Float64
#     porder::Float64
# end
#
# function ranksum(df::DataFrame,varname::Symbol; group::Symbol = nothing)
#     if group == nothing
#         error("`group` variable is required")
#     end
#
#     df2 = df[completecases(df[[varname,group]]),[varname,group]]
#     df2[:__ranking] = tiedrank(df[varname])
#
#     df3 = by(df2,group) do subdf
#         DataFrame(
#             obs = size(subdf,1),
#             ranksum = sum(subdf[:__ranking]),
#             expected = size(subdf,1)*(size(df2,1)+1) / 2
#         )
#     end
#
#     # test statistic
#     t = df3[1,:ranksum]
#     n1 = df3[1,:obs]
#     U = t - n1*(n1+1) / 2
#     meanrank = mean(df2[:__ranking])
#     s² = var(df2[:__ranking])
#     vart = df3[1,:obs]*df3[2,:obs]*s² / size(df2,1)
#     z = (t - df3[1,:expected]) / sqrt(vart)
#     pval = ccdf()
#     porder = U / (df3[1,:obs]*df3[2,:obs])
#     pval = 2*Distributions.cdf(Distributions.Normal(), z)
#     return ranksum_return(df3,varname,t,U,s²,vart,z,pval,porder)
# end
# #
# # function print(r::ranksum_return)
# #     group = names(r.df)[1]
# #     print(r.df,"\n\n")
# #     @printf("         z = %.3f\n",r.z)
# #     @printf("Prob > |z| = %.3f\n",r.pvalue)
# #     @printf("P{%s(%s == %s)} >  P{%s(%s == %s)} = %.3f\n",
# #         string(r.varname),
# #         string(group),
# #         string(r.df[1,1]),
# #         string(r.varname),
# #         string(group),
# #         string(r.df[2,1]),
# #         r.porder
# #         )
# # end
#
# struct signrank_return
#     df::DataFrame
#     t::Float64
#     var_t::Float64
#     z::Float64
#     p::Float64
# end
#
# function signrank(df::DataFrame,var1::Symbol,var2::Symbol)
#
#     # complete cases only
#     df2 = df[completecases(df[[var1,var2]]),[var1,var2]]
#
#     # difference
#     d = df[var1] .- df[var2]
#
#     # sign
#     s = sign.(d)
#
#     # rank the absolute difference
#     r = tiedrank(abs(d)).*s
#
#     # test statistic
#     t = sum(r)
#
#     # dataframe output
#     n = size(df2,1)
#     n0 = sum(s .== 0)
#     et = (n*(n + 1)/2 - sum(s .== 0))/2
#     df = DataFrame(
#         sign = ["positive","negative","zero"],
#         obs = [
#             sum(s .== 1),
#             sum(s .== -1),
#             n0
#         ],
#         sum_ranks = [
#             sum((s .== 1) .* r),
#             abs(sum((s .== -1) .* r)),
#             n0
#         ],
#         expected = [et,et,n0]
#     )
#
#     varadjt = sum(r.^2) / 4
#     varunadjt = (n*(n + 1)*(2n + 1))/24
#     Δvarzeroadj = -1 * n0*(n0 + 1)*(2*n0 + 1) / 24
#     Δvartiesadj = varadjt - varunadjt - Δvarzeroadj
#
#     # z-value
#     z = (df[1,:sum_ranks] - et) / sqrt(varadjt)
#
#     # p-value
#     pval = 2*Distributions.cdf(Distributions.Normal(), z)
#
#     return signrank_return(df, et, varadjt, z, pval)
# end
# #
# # function print(sr::signrank_return)
# #     print(sr.df,"\n\n")
# #     @printf("Adj variance = %.3f\n",sr.var_t)
# #     @printf("           z = %.3f\n",sr.z)
# #     @printf("  Prob > |z| = %.3f\n",sr.p)
# # end
#
#
# struct signtest_return
#     df::DataFrame
#     p_left::Float64
#     p_right::Float64
#     p_both::Float64
# end
#
# function signtest(df::DataFrame,var1::Symbol,var2::Symbol)
#
#     # complete cases only
#     df2 = df[completecases(df[[var1,var2]]),[var1,var2]]
#
#     # difference
#     d = df[var1] .- df[var2]
#
#     # sign
#     s = sign.(d)
#
#     # test statistic
#     nplus = sum(s .> 0)
#     n0 = sum(s .== 0)
#     n_nonzero = size(df2,1) - n0
#     et = n_nonzero / 2
#
#     # output dataframe
#     df3 = DataFrame(
#         sign = ["positive","negative","zero"],
#         observed = [
#             sum(s .== 1),
#             sum(s .== -1),
#             n0
#         ],
#         expected = [et,et,n0]
#     )
#
#     # p1 - Ha: median of var1 - var2 > 0
#     p1 = Distributions.cdf(Distributions.Binomial(n_nonzero,.5),df3[2,:observed])
#
#     # p2 - Ha: median of var1 - var2 < 0
#     p2 = Distributions.cdf(Distributions.Binomial(n_nonzero,.5),df3[1,:observed])
#
#     # p2 - Ha: median of var1 - var2 != 0
#     p3 = min(1.0, 2.0*Distributions.cdf(Distributions.Binomial(n_nonzero,.5),df3[1,:observed]))
#
#     return signtest_return(df3, p1, p2, p3)
# end
# #
# # function print(sr::signtest_return)
# #     print(sr.df,"\n\n")
# #     @printf("Pr(#positive ≥ %d) = %.3f\n",sr.df[1,:observed],sr.p_left)
# #     @printf("Pr(#negative ≥ %d) = %.3f\n",sr.df[2,:observed],sr.p_right)
# #     @printf("Pr(#positive ≥ %d or #negative ≥ %d) = %.3f\n",sr.df[1,:observed],sr.df[2,:observed],sr.p_both)
# # end
# #

function dir(str::String)
  if occursin("*",str) || occursin("?",str)
    printdir(glob(str))
  elseif str == "." || str == ".."
    printdir(glob(string(str,"/*")))
  elseif str == ""
    printdir(readdir())
  else
    printdir(readdir(str))
  end
end
dir() = dir("")

function printdir(vstr::Vector{String})

  for i=1:length(vstr)
    vstr[i] = isdir(vstr[i]) ? string(vstr[i],"/") : vstr[i]
  end
  maxlen = maximum(length.(vstr))

  for i=1:length(vstr)
    println(rpad(vstr[i],maxlen),"  ", humanReadableByteCountBin(stat(vstr[i]).size))
  end
end

units   = ["K",  "M",  "G",  "T",  "P"]
bin_div = [0x1p10, 0x1p20, 0x1p30, 0x1p40, 0x1p40]

function humanReadableByteCountBin(bytes::Int64)
    b = bytes == typemin(Int64) ? typemax(Int64) : abs(bytes)
    if b < 1024
        return @sprintf("%d B", b)
    end
    sf = 40
    for i in 1:5
        if b < 0xfffcccccccccccc >> sf
            if i == 5
                b >>= 10
            end
            return @sprintf("%.1f %sB", b / bin_div[i], units[i])
        end
        sf -= 10
    end
    b = (b >> 20) / 0x1p40
    return @sprintf("%.1f EB", b)
end


# ------------------------------------------------------------------------------------------
# # OnlineStats Integration
#
# - OnlineStats is a Julia package for running statistical algorithms both online (one
# observation at a time) and in parallel.
# - In this notebook we will go over how to run these calculations through JuliaDB.
#
# First we'll load OnlineStats, JuliaDB and Plots (with GR backend)
# ------------------------------------------------------------------------------------------

using OnlineStats, JuliaDB, Plots
gr()

# print table rather than column summary
IndexedTables.set_show_compact!(false);

# ------------------------------------------------------------------------------------------
# Now we'll see:
# 1. An Intro to OnlineStats
# 1. An Example Using OnlineStats with `partitionplot`, `reduce`, and `groupreduce` (all
# available for out-of-core processing!)
# 1. Mosaic Plots
# 1. Linear Regression
# 1. Approximate Solutions to Statistical Learning Models with `StatLearn`
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# # Intro to OnlineStats
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ### Every stat is a type
# ------------------------------------------------------------------------------------------

m = Mean()

# ------------------------------------------------------------------------------------------
# ### Stats can be `fit!`-ted with more data
# ------------------------------------------------------------------------------------------

fit!(m, randn(100))

# ------------------------------------------------------------------------------------------
# ### Stats can be `merge!`-ed together
# ------------------------------------------------------------------------------------------

m2 = fit!(Mean(), randn(100))

merge!(m, m2)

# ------------------------------------------------------------------------------------------
# ### `fit!`-ting and `merge!`-ing works quite well with JuliaDB
#
# - JuliaDB can send stats to the worker processes and then merge them at the end.
#
# <img src="https://user-
# images.githubusercontent.com/8075494/32748459-519986e8-c88a-11e7-89b3-80dedf7f261b.png"
# width=400>
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# # Example: Diamonds Dataset
#
# - OnlineStats operations are available through `partitionplot`, `reduce` and
# `groupreduce`.
# - Here we will take a look at a dataset about diamond quality.
#
#
# - First let's examine the first few rows of the data:
# ------------------------------------------------------------------------------------------

;head diamonds.csv

# ------------------------------------------------------------------------------------------
# - The first column is the row number, so we'll only load the other columns.  Here we sort
# the data by column 2, `:carat`.
# ------------------------------------------------------------------------------------------

using JuliaDB
t = loadtable("diamonds.csv", indexcols = [2], datacols = 3:11)

# ------------------------------------------------------------------------------------------
# # `partitionplot`
#
# `partitionplot(table[, x], y; by, stat, nparts, dropmissing)`
#
# - We introduced `partitionplot` in the previous notebook.
# - Here we'll go through some more advanced examples.
# - If only one variable is provided, the x-axis will be the number of observations.
# - Note that `:carat` is our index variable and thus it is sorted:
# ------------------------------------------------------------------------------------------

partitionplot(t, :carat)

# ------------------------------------------------------------------------------------------
# - When we provide two variables, the summary of the `y` variable is plotted in each
# "section" of the `x` variable.
#
# - In this example, we'll use the Interact package to choose which statistic to summarize
# `y` with in the of plot `y = :price` vs. `x = :carat`:
# ------------------------------------------------------------------------------------------

import Interact

Interact.@manipulate for summarizer in [Mean(), Extrema(), Hist(10)]
    partitionplot(t, :carat, :price, stat = summarizer)
end

# ------------------------------------------------------------------------------------------
# - Any selector is a valid input to the `x` and `y` arguments of `partitionplot`:
# ------------------------------------------------------------------------------------------

partitionplot(t, :carat => x -> -x, :price, stat = Hist(10))

# ------------------------------------------------------------------------------------------
# - Any valid Plots keyword argument can also be included, such as changing the type of plot
# series to a barplot.
#
# - Here we are essentially plotting `groupby(mean, t, :cut, select = :carat)`:
# ------------------------------------------------------------------------------------------

partitionplot(t, :cut, :carat; stat = Mean(), seriestype = :bar)

# ------------------------------------------------------------------------------------------
# # `reduce` and `groupreduce`
#
# - In the following examples, we'll see that the reducer for `reduce` and `groupreduce` can
# come from OnlineStats.  The OnlineStats reducers can be:
#   1. A `Series`
#   1. An `OnlineStat`
#   1. A tuple of `OnlineStat`s
#
#
# - Here we get the mean of `:carat` for each level of `:cut`:
# ------------------------------------------------------------------------------------------

groupreduce(Mean(), t, :cut; select = :carat)

# ------------------------------------------------------------------------------------------
# - We could also calculate the same information with `groupby`
# - However `[group]reduce` with an OnlineStat is often more efficient.
# ------------------------------------------------------------------------------------------

@time groupreduce(Mean(), t, :cut; select = :carat)
@time groupby(mean, t, :cut; select = :carat)

# ------------------------------------------------------------------------------------------
# ## Size of Selections
#
# - One thing to note is that the selection in `reduce`/`groupreduce` must match the "input
# size" of the `OnlineStat` object passed as an argument.
# - Different stats can be applied to different columns with the `Group` type.
#     - Multiple stats of the same type can be created via integer multiplication (see
# example below)
# - For example, if you want to calculate the means for 5 different columns, you would use
# as reducer:
# ------------------------------------------------------------------------------------------

5 * Mean()

# ------------------------------------------------------------------------------------------
# - In this example we create 3 histograms of 50 bins each to use the columns `:x`, `:y`,
# and `:z` as input:
# ------------------------------------------------------------------------------------------

hists = reduce(3Hist(50), t; select = (:x, :y, :z))

# ------------------------------------------------------------------------------------------
# - The printed histograms do not provide much information.
# - Fortunately, OnlineStats implements many Plots recipes.
# - Plotting a `Group` will then plot the stats it contains:
# ------------------------------------------------------------------------------------------

plot(hists, layout = 3)

# ------------------------------------------------------------------------------------------
# - You may also want to calculate different statistics for different columns.
# ------------------------------------------------------------------------------------------

reducer = Group(Mean(), CountMap(String))

plot(reduce(reducer, t; select = (:carat, :cut)), layout = 2)

# ------------------------------------------------------------------------------------------
# ## Visualizing Continuous Distributions
#
# - The `Hist` type is particularly useful for visualizing continuous distributions, as it
# adaptively finds the "best" bin locations.
#
# - In the following cell, try increasing the number of bins and see how the histogram
# changes:
# ------------------------------------------------------------------------------------------

# try increasing the number of bins
plot(reduce(Hist(50), t; select = :carat))

# ------------------------------------------------------------------------------------------
# ## Visualizing Categorical Distributions
#
# - The `CountMap` type tracks the number of occurrences for each unique value in a column.
# - `CountMap` accepts the column type as its argument.
# - The `:cut` column is of type `String`, so here we use `CountMap(String)`:
# ------------------------------------------------------------------------------------------

plot(reduce(CountMap(String), t; select = :cut))

# ------------------------------------------------------------------------------------------
# ## Mosaic Plot
#
# - Mosaic plots are extremely useful in visualizing the association between two categorical
# variables, as it shows class probabilities of the `x` variables and conditional class
# probabilities of the `y` variable.
# - `Mosaic` accepts the two column types as its input.
#
#
# - The `:cut` and `:color` variables are both of type `String`, so here we use
# `Mosiac(String, String)`.
# - We can see most common `:cut` is `"Ideal"` and the least common is `"Fair"` (which we
# already know from above).
# ------------------------------------------------------------------------------------------

plot(reduce(Mosaic(String, String), t; select = (:cut, :color)))

# ------------------------------------------------------------------------------------------
# # Statistical Models
#
# - We can build linear (and ridge) regression models using the `LinRegBuilder` and `LinReg`
# types.
#
# ## `LinRegBuilder(p)`
#
# - `LinRegBuilder` builds a data structure that allows you to regress any of the observed
# columns on any subset of other columns.
# ------------------------------------------------------------------------------------------

x = (:carat,:depth,:table,:price,:x,:y,:z)

o = reduce(LinRegBuilder(7), t; select = x)

# ------------------------------------------------------------------------------------------
# - If we wish to fit a model with formula:
# ```
# price ~ carat + depth + table + x + y + z + 1
# ```
#   We can specify the `y` variable to be the 4-th column, which was `:price`.  By default,
# an intercept (bias) term is included as the last coefficient.
# ------------------------------------------------------------------------------------------

coef(o; y=4, verbose=true)

# ------------------------------------------------------------------------------------------
# - We can then create a different regression (without revisiting data) based on the formula
#
#   ```
#   carat ~ x + y + z
#   ```
#
#   by specifying the `y` and `x` variables and removing the `bias`:
# ------------------------------------------------------------------------------------------

coef(o; y=1, x = 5:7, bias = false)

# ------------------------------------------------------------------------------------------
# ## `LinReg()`
#
# - The `LinReg` type allows you to fit linear regression with an optional L2 (ridge)
# penalty.
# - One thing to note is that besides `LinRegBuilder`, models in OnlineStats expect data as
# a tuple: `(x, y)`.
#   - This requires selections to take the form: `((xvars...), yvar)`.
#
#
# - In this example, we'll create the model:
# ```
# carat ~ x + y + z
# ```
# - Note that we found the same result as we did with `LinRegBuilder` above.
# ------------------------------------------------------------------------------------------

o = reduce(LinReg(), t; select = ((:x, :y, :z), :carat))

coef(o, 0.1)

# ------------------------------------------------------------------------------------------
# ## Other Models
#
# - **OnlineStats** has a variety of methods that are more advanced than linear regression.
# - They're beyond the scope of this introduction, but interested readers can investigate
# the following:
#
# ### `StatLearn` (Stochastic approximation for linear statistical learning models)
#
# ### `NBClassifier` (Naive Bayes Classifier)
#
# ### `FastTree`/ `FastForest` (experimental online decision trees/random forests)
#
# ------------------------------------------------------------------------------------------


using StructuralCausalModels

ProjDir = @__DIR__
cd(ProjDir) #do

fname = scm_path("..", "examples", "Shipley", "fig2.6", "fig2.6.a.dot")
Sys.isapple() && run(`open -a GraphViz.app $(fname)`)

# Read `=>` as `~` in regression models, or `<-` in causal models.
d = OrderedDict(
  :u => [:x, :v, :s1],
  :w => [:v, :y, :s2]
);

dag = DAG("fig2_6_a", d);
show(dag)

f = [:x]; s = [:v]
g1 = d_separation(dag, f, s)
println("d_separation($(dag.name), $f, $s) == $g1")
g1 = d_separation(dag, f, s; c=[:u])
println("d_separation($(dag.name), $f, $s; c=[:u) == $g1")
g1 = d_separation(dag, f, s; c=[:s1])
println("d_separation($(dag.name), $f, $s; c=[:s1]) == $g1\n")

f = [:x]; s = [:w]
g2 = d_separation(dag, f, s)
println("d_separation($(dag.name), $f, $s) == $g2")
g2 = d_separation(dag, f, s; c=[:u])
println("d_separation($(dag.name), $f, $s; c=[:u]) == $g2")
g2 = d_separation(dag, f, s; c=[:v])
println("d_separation($(dag.name), $f, $s; c=[:v]) == $g2")
g2 = d_separation(dag, f, s; c=[:u, :v])
println("d_separation($(dag.name), $f, $s; c=[:u, :v]) == $g2\n")

f = [:x]; s = [:y]
g3 = d_separation(dag, f, s)
println("d_separation($(dag.name), $f, $s) == $g3")
g3 = d_separation(dag, f, s; c=[:u, :w])
println("d_separation($(dag.name), $f, $s; c=[:u, :w]) == $g3")
g3 = d_separation(dag, f, s; c=[:s1, :s2])
println("d_separation($(dag.name), $f, $s; c=[:s1, :s2]) == $g3")
g3 = d_separation(dag, f, s; c=[:u, :v, :w])
println("d_separation($(dag.name), $f, $s; c=[:u, :v, :w]) == $g3")
g3 = d_separation(dag, f, s; c=[:s1, :s2, :u])
println("d_separation($(dag.name), $f, $s; c=[:s1, :s2, :u]) == $g3")
g3 = d_separation(dag, f, s; c=[:s1])
println("d_separation($(dag.name), $f, $s; c=[:s1]) == $g3\n")

f = [:u]; s = [:w]
g4 = d_separation(dag, f, s)
println("d_separation($(dag.name), $f, $s) == $g4"), [:s1]
g4 = d_separation(dag, f, s; c=[:v])
println("d_separation($(dag.name), $f, $s; c=[:v]) == $g4\n")

bs = basis_set(dag)
bs |> display

adjustment_sets(dag, :u, :w) |> display

#end


using DSP, DSP.Periodograms
using FFTW

export tfd, Wigner, Spectrogram

abstract type TFKernel end

Base.@kwdef struct Spectrogram <: TFKernel
  nfft = 256
  noverlap = 0
  window = nothing
end

Base.@kwdef struct Wigner <: TFKernel
  nfft = 0
  window = nothing
  smooth = 0
  method = :CMP2005     # alternatives: CM1980
end

struct TFD{T} <: Periodograms.TFR{T}
  power::Matrix{T}
  freq::AbstractVector
  time::AbstractVector
end

DSP.Periodograms.time(tfr::TFD) = tfr.time

"""
$(TYPEDSIGNATURES)
Computes a spectrogram time-frequency distribution of signal `s` sampled as sampling rate `fs`.

# Examples:
```julia-repl
julia> x = real.(chirp(1kHz, 10kHz, 1s, 44.1kHz));
julia> y = tfd(x, Spectrogram());
julia> plot(y)
julia> y = tfd(x, Spectrogram(nfft=512, noverlap=256, window=hamming));
julia> plot(y)
```
"""
function tfd(s, kernel::Spectrogram; onesided=eltype(s)<:Real, fs=framerate(s))
  onesided && ((eltype(s) <: Real) || (s = real.(s)))
  tfr = spectrogram(samples(s), kernel.nfft, kernel.noverlap; onesided=onesided, fs=fs, window=kernel.window)
  w = power(tfr)
  f = freq(tfr)
  if !onesided
    f = fftshift(f)
    w = fftshift(w, 1)
  end
  TFD(w, f, time(tfr))
end

"""
$(TYPEDSIGNATURES)
Computes a Wigner-Ville time-frequency distribution of signal `s` sampled as sampling rate `fs`.

# Examples:
```julia-repl
julia> x = real.(chirp(1kHz, 10kHz, 0.01s, 44.1kHz));
julia> y = tfd(x, Wigner());
julia> plot(y; clim=(0,20))
julia> y = tfd(x, Wigner(nfft=512, smooth=10, method=:CM1980, window=hamming));
julia> plot(y; clim=(0,20))
```
"""
function tfd(s, kernel::Wigner; onesided=eltype(s)<:Real, fs=framerate(s))
  N = nframes(s)
  T = eltype(s)
  nfft = kernel.nfft <= 0 ? 2N : min(kernel.nfft, 2N)
  win = kernel.window == nothing ? nothing : kernel.window isa Function ? kernel.window(nfft) : kernel.window
  x = samples(s)
  if kernel.method === :CMP2005
    # Chassande-Mottin & Pai, IEEE Sig. Proc. Letters, 12(7), 2005.
    x̂ = repeat(s; inner=2)
  elseif kernel.method === :CM1980
    # Claasen & Mecklenbräuker, Philips J. Res., 35(4/5), p276–300, 1980.
    x̂ = resample(s, 2)
    length(x̂) < 2N && (x̂ = vcat(x̂, zeros(2N-length(x̂))))
  end
  X = zeros(T, nfft, N)
  @views for n ∈ 0:N-1
    kn = min(2n, 2N-1-2n, nfft÷2-1)
    pad = nfft - length(-kn:kn)
    if pad > 0
      xx = X[floor(Int, pad/2)+2 : end-(ceil(Int, pad/2)-1), n+1] # view
    else
      xx = X[:, n+1] # view
    end
    xx .= x̂[1 .+ 2n .+ (-kn:kn)] .* conj.(x̂[1 .+ 2n .- (-kn:kn)])
  end
  if kernel.smooth > 0
    X = copy(filt(ones(kernel.smooth)./kernel.smooth, copy(X'))') # The copying helps with memory access performance
  end
  win === nothing || (X .*= win)
  w = Array{T,2}(undef, onesided ? nfft÷2+1 : nfft, N)
  P = onesided ? plan_rfft(zeros(T, nfft)) : plan_fft(zeros(complex(T), nfft))
  wo = Array{complex(T),1}(undef, onesided ? nfft÷2+1 : nfft)
  @views for j ∈ 1:N
    mul!(wo, P, X[:,j])
    w[:,j] .= real.(wo) ./ √(2π)
  end
  if onesided
    f = FFTW.rfftfreq(nfft, fs)
  else
    f = fftshift(FFTW.fftfreq(nfft, fs))
    w = fftshift(w, 1)
  end
  TFD(w, f, (0:N-1)./fs)
end


println("===============================================")
println("Testing base.jl")
println("===============================================")

function _trapz(tol)
    x = [i for i in -5.0:0.01:5.0]
    y = 3*x.^2

    isapprox(RHEOS.trapz(y, x), 250.0, atol=tol)
end
@test _trapz(tol)

function _derivCD(tol)
    x = Vector(0.0:0.001:1.5)
    y = x.^2
    dy = 2*x
    dy_numeric = RHEOS.derivCD(y, x)

    isapprox(dy_numeric, dy, atol=tol)
end
@test _derivCD(tol)

function _derivBD(tol)
    x = collect(0.0:0.001:1.5)
    y = x.^2
    dy = 2*x
    dy_numeric = RHEOS.derivBD(y, x)
    # note that the backwards difference method has a weaker
    # numerical test than derivCD as it is less accurate
    all(i -> isapprox(dy[i], dy_numeric[i], atol=tol), eachindex(dy_numeric))
end
@test _derivBD(tol)

function _doublederivCD_quadratic(tol)
    x = Vector(0.0:0.001:1.5)
    y = x.^2
    dy = [2.0 for xi in x]
    dy_numeric = RHEOS.doublederivCD(y, x)

    isapprox(dy_numeric, dy, atol=tol)
end
@test _doublederivCD_quadratic(tol)

function _doublederivCD_cubic(tol)
    x = Vector(0.0:0.001:1.5)
    y = 50*x.^2 .- 2*x.^3 .+ x .- 100.0
    dy = [100.0 - 12*xi for xi in x]
    dy_numeric = RHEOS.doublederivCD(y, x)

    N = length(x)
    test1 = all(i -> isapprox(dy[i], dy_numeric[i], atol=tol), 2:(N-1))
    test2 = isapprox(dy[1], dy_numeric[1], atol=1.1*tol)
    test3 = isapprox(dy[N], dy_numeric[N], atol=1.1*tol)

    test1 && test2 && test3
end
@test _doublederivCD_cubic(tol)

function _doublederivCD_cubic_oscillatory(tol)
    x = Vector(0.0:0.001:20.0)
    y = 50*x.^2 .- 2*x.^3 .+ x .- 100.0 .+ 50.0*sin.(x)
    dy = [100.0 - 12*xi - 50*sin(xi) for xi in x]
    dy_numeric = RHEOS.doublederivCD(y, x)

    N = length(x)
    test1 = all(i -> isapprox(dy[i], dy_numeric[i], atol=tol), 2:(N-1))
    test2 = isapprox(dy[1], dy_numeric[1], atol=10*tol)
    test3 = isapprox(dy[N], dy_numeric[N], atol=10*tol)

    test1 && test2 && test3
end
@test _doublederivCD_cubic_oscillatory(tol)

@test RHEOS.constantcheck([1.0, 2.0, 3.0])==true
@test RHEOS.constantcheck([1.0, 2.0, 4.0])==false

@test RHEOS.closestindex([1.0, 2.0, 3.0], 1.7)==2

@test RHEOS.closestindices([1.0, 2.0, 3.0], [1.7, 3.4])==[2, 3]

@test RHEOS.getsigma(10.0, 1000.0)≈1873.9062512927758

# function _downsample_1region()
#     x0 = collect(0.0:0.01:1.0)
#     y0 = x0.^2
#
#     x1 = collect(0.0:0.1:1.0)
#     y1 = x1.^2
#
#     xout, yout = RHEOS.fixed_resample(x0, y0, [1, length(x0)], [-10])
#
#     xout==x1 && yout==y1
# end
# @test _downsample_1region()
#
# function _upsample_1region()
#     # must be linear as interpolation for upsampling is linear
#     x0 = collect(0.0:0.1:10.0)
#     y0 = 2*x0
#
#     x1 = collect(0.0:0.01:10.0)
#     y1 = 2*x1
#
#     xout, yout = RHEOS.fixed_resample(x0, y0, [1, length(x0)], [10])
#
#     # note that we expect x (generated from range) to be exactly
#     # the same as target higher-sampled data. y will only be the
#     # same within floating point error, hence the approximate
#     # comparison
#     xout==x1 && yout≈y1
# end
# @test _upsample_1region()
#
# function _upanddown_multipleregions()
#     # data to be resampled
#     x0 = collect(-10.0:0.5:10.0)
#     y0 = 2*x0
#
#     # data for comparison composed
#     # of multiple sections
#     x1a = collect(-10.0:0.1:-5.1)
#     x1b = collect(-5.0:1.0:0.0)
#     x1c = collect(0.05:0.05:4.95)
#     x1d = collect(5.0:2.0:10.0)
#     x1e = [x0[end]]
#
#     x1 = vcat(x1a, x1b, x1c, x1d, x1e)
#     y1 = 2*x1
#
#     indices = [1, RHEOS.closestindex(x0, -5.0), RHEOS.closestindex(x0, 0.0), RHEOS.closestindex(x0, 5.0), length(x0)]
#
#     xout, yout = RHEOS.fixed_resample(x0, y0, indices, [5, -2, 10, -4])
#
#     xout==x1 && yout≈y1
# end
# @test _upanddown_multipleregions()

@test RHEOS.singularitytest(x->1/x)
@test RHEOS.singularitytest(x->1/(x-5.0), t1 = 5.0)
@test RHEOS.singularitytest(x->NaN)

function _boltzintegral_nonsing_ramp(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    exact_response = 1 .- exp.(-t)
    ramp_loading = t
    ramp_loading_derivative = RHEOS.derivBD(ramp_loading, t)
    ramp_response = RHEOS.boltzintegral_nonsing(x->exp.(-x), t, ramp_loading_derivative)

    all(i -> isapprox(exact_response[i], ramp_response[i], atol=tol), eachindex(exact_response))
end
@test _boltzintegral_nonsing_ramp(tol)

function _boltzintegral_nonsing_ramp_2sections(tol)
    dt = 0.01
    t = [Vector{RheoFloat}(0.0:dt:(15.0-dt)); Vector{RheoFloat}(15.0:10*dt:20.0)]
    exact_response = 1 .- exp.(-t)
    ramp_loading = t
    ramp_loading_derivative = RHEOS.derivBD(ramp_loading, t)
    ramp_response = RHEOS.boltzintegral_nonsing(x->exp.(-x), t, ramp_loading_derivative)

    all(i -> isapprox(exact_response[i], ramp_response[i], atol=tol), eachindex(exact_response))
end
@test _boltzintegral_nonsing_ramp_2sections(tol)

function _boltzintegral_nonsing_linear_3sections(tol)
    t1 = Vector{RheoFloat}(0.0:0.01:9.99)
    t2 = Vector{RheoFloat}(10.0:0.1:16.9)
    t3 = Vector{RheoFloat}(17.0:0.001:20.0)
    t = [t1;t2;t3]
    exact_response = 1 .- exp.(-t)
    ramp_loading = t
    ramp_loading_derivative = RHEOS.derivBD(ramp_loading, t)
    ramp_response = RHEOS.boltzintegral_nonsing(x->exp.(-x), t, ramp_loading_derivative)

    all(i -> isapprox(exact_response[i], ramp_response[i], atol=tol), eachindex(exact_response))
end
@test _boltzintegral_nonsing_linear_3sections(tol)

function _boltzintegral_step(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    exact_response = exp.(-t)
    step_loading = ones(length(t))
    step_loading_deriv = RHEOS.derivBD(step_loading,t)
    step_response = RHEOS.boltzintegral_nonsing(x->exp.(-x), t, step_loading_deriv)

    all(i -> isapprox(exact_response[i], step_response[i], atol=tol), eachindex(exact_response))
end
@test _boltzintegral_step(tol)

function _boltzintegral_linearcombo(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    step_loading = ones(length(t))
    step_loading_deriv = RHEOS.derivBD(step_loading,t)
    step_response = RHEOS.boltzintegral_nonsing(x->exp.(-x), t, step_loading_deriv)

    ramp_loading = t
    ramp_loading_deriv = RHEOS.derivBD(ramp_loading, t)
    ramp_response = RHEOS.boltzintegral_nonsing(x->exp.(-x), t, ramp_loading_deriv)

    combined_loading = t .+ ones(length(t))
    combined_loading_deriv = RHEOS.derivBD(combined_loading, t)
    combined_response = RHEOS.boltzintegral_nonsing(x->exp.(-x), t, combined_loading_deriv)

    all(i -> isapprox(combined_response[i], (step_response[i] + ramp_response[i]), atol=tol), eachindex(combined_response))
end
@test _boltzintegral_linearcombo(tol)

function _boltzintegral_nonsing_parabolic(tol)
    # response of Maxwell model to
    # a parabola: 2500 - (t-50)^2
    t = Vector{RheoFloat}(0.0:0.001:20.0)
    exact_response = 102 .- 102*exp.(-t) .- 2t

    loading = 2500.0 .- (t .- 50).^2
    loading_derivative = RHEOS.derivBD(loading, t)

    integration_response = RHEOS.boltzintegral_nonsing(x->exp.(-x), t, loading_derivative)

    # note that tol is 5x higher here (and sample rate is higher)
    # as trapezoidal method is relatively innacurate.
    all(i -> isapprox(exact_response[i], integration_response[i], atol=5*tol), eachindex(exact_response))
end
@test _boltzintegral_nonsing_parabolic(tol)

function _boltzintegral_sing_linear(tol)
    # response of a power-law model
    # to a linear loading: t
    t = Vector{RheoFloat}(0.0:0.01:20.0)
    β = 0.5
    exact_response = t.^(1.0 - 0.5) / (1.0 - 0.5)

    loading = t
    loading_derivative = RHEOS.derivBD(loading, t)

    integration_response = RHEOS.boltzintegral_sing(x->x.^(-β), t, loading_derivative)

    all(i -> isapprox(exact_response[i], integration_response[i], atol=4*tol), eachindex(exact_response))
end
@test _boltzintegral_sing_linear(tol)

function _boltzintegral_sing_linear_2sections(tol)
    dt = 0.01
    t = [Vector{RheoFloat}(0.0:dt:(15.0-dt)); Vector{RheoFloat}(15.0:10*dt:20.0)]
    β = 0.5
    exact_response = t.^(1.0 - 0.5) / (1.0 - 0.5)

    loading = t
    loading_derivative = RHEOS.derivBD(loading, t)

    integration_response = RHEOS.boltzintegral_sing(x->x.^(-β), t, loading_derivative)

    all(i -> isapprox(exact_response[i], integration_response[i], atol=4*tol), eachindex(exact_response))
end
@test _boltzintegral_sing_linear_2sections(tol)

# function _boltzintegral_sing_linear_3sections(tol)
#     # note this test identified that going from higher sample period to lower sample period
#       could cause innacuracies due to singularity approximation
#     t1 = Vector{RheoFloat}(0.0:0.1:4.9)
#     t2 = Vector{RheoFloat}(5.0:0.1:9.9)
#     t3 = Vector{RheoFloat}(10.0:0.01:15.0)
#     t = [t1; t2; t3]
#     β = 0.5
#     exact_response = t.^(1.0 - 0.5) / (1.0 - 0.5)

#     loading = t
#     loading_derivative = RHEOS.derivBD(loading, t)

#     integration_response = RHEOS.boltzintegral_sing(x->x.^(-β), t, loading_derivative)

#     all(i -> isapprox(exact_response[i], integration_response[i], atol=4*tol), eachindex(exact_response))
# end
# @test _boltzintegral_sing_linear_3sections(tol)

function _boltzintegral_sing_step(tol)
    # response of a power-law model
    # to a step loading
    t = Vector{RheoFloat}(0.0:0.01:20.0)
    β = 0.5
    exact_response = t.^(-0.5)

    loading = ones(length(t))
    loading_derivative = RHEOS.derivBD(loading, t)

    integration_response = RHEOS.boltzintegral_sing(x->x.^(-β), t, loading_derivative)

    # note that first element is skipped due to singularity
    all(i -> isapprox(exact_response[i], integration_response[i], atol=tol), 2:length(t))
end
@test _boltzintegral_sing_step(tol)

function _boltzintegral_sing_parabolic(tol)
    # response of power-law model to
    # a parabola: 2500 - (t-50)^2
    t = Vector{RheoFloat}(0.0:0.001:20.0)
    β = 0.5
    exact_response = (100/(1-β))*t.^(1-β) .- (2/((1-β)*(2-β)))*t.^(2-β)

    loading = 2500.0 .- (t .- 50).^2
    loading_derivative = RHEOS.derivBD(loading, t)

    integration_response = RHEOS.boltzintegral_sing(x->x.^(-β), t, loading_derivative)
    # note that first element is skipped due to singularity
    # and the higher tolerance and skipping of many elements
    # this is one of the hardest cases for the trapezoidal
    # method of hereditary integration to handle when then there
    # is a singularity.
    all(i -> isapprox(exact_response[i], integration_response[i], atol=1.0), 250:length(t))
end
@test _boltzintegral_sing_parabolic(tol)

function _obj_var_nonsing_ramp(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    exact_response = 1 .- exp.(-t)
    ramp_loading = t
    ramp_loading_derivative = RHEOS.derivBD(ramp_loading, t)

    cost = RHEOS.obj_var_nonsing(nothing, nothing, (x, params)->exp.(-x), t, ramp_loading_derivative, exact_response)

    cost < length(t)*tol^2
end
@test _obj_var_nonsing_ramp(tol)

function _obj_var_nonsing_step(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    exact_response = exp.(-t)
    step_loading = ones(length(t))
    step_loading_deriv = RHEOS.derivBD(step_loading,t)

    cost = RHEOS.obj_var_nonsing(nothing, nothing, (x, params)->exp.(-x), t, step_loading_deriv, exact_response)

    cost < length(t)*tol^2
end
@test _obj_var_nonsing_step(tol)

function _obj_var_nonsing_parabolic()
    # response of Maxwell model to
    # a parabola: 2500 - (t-50)^2
    t = Vector{RheoFloat}(0.0:0.001:20.0)
    exact_response = 102 .- 102*exp.(-t) .- 2t

    loading = 2500.0 .- (t .- 50).^2
    loading_derivative = RHEOS.derivBD(loading, t)

    cost = RHEOS.obj_var_nonsing(nothing, nothing, (x, params)->exp.(-x), t, loading_derivative, exact_response)
    # note that cost is very high for parabolic as
    # hereditary integral approximation is not
    # good for this case.
    cost < 15*length(t)*tol
end
@test _obj_var_nonsing_parabolic()

function _obj_var_sing_linear(tol)
    # response of a power-law model
    # to a linear loading: t
    t = Vector{RheoFloat}(0.0:0.01:20.0)
    β = 0.5
    exact_response = t.^(1.0 - 0.5) / (1.0 - 0.5)

    loading = t
    loading_derivative = RHEOS.derivBD(loading, t)

    cost = RHEOS.obj_var_sing(nothing, nothing, (x, params)->x.^(-β), t, loading_derivative, exact_response)

    cost < 3*length(t)*tol
end
@test _obj_var_sing_linear(tol)

function _boltzconvolve_nonsing_ramp(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    exact_response = 1 .- exp.(-t)
    ramp_loading = t
    ramp_loading_derivative = RHEOS.derivBD(ramp_loading, t)
    ramp_response = RHEOS.boltzconvolve(x->exp.(-x), t, dt, ramp_loading_derivative)

    all(i -> isapprox(exact_response[i], ramp_response[i], atol=tol), eachindex(exact_response))
end
@test _boltzconvolve_nonsing_ramp(tol)

function _boltzconvolve_step(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    exact_response = exp.(-t)
    step_loading = ones(length(t))
    step_loading_deriv = RHEOS.derivBD(step_loading,t)
    step_response = RHEOS.boltzconvolve(x->exp.(-x), t, dt, step_loading_deriv)

    all(i -> isapprox(exact_response[i], step_response[i], atol=tol), eachindex(exact_response))
end
@test _boltzconvolve_step(tol)

function _boltzconvolve_linearcombo(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    step_loading = ones(length(t))
    step_loading_deriv = RHEOS.derivBD(step_loading,t)
    step_response = RHEOS.boltzconvolve(x->exp.(-x), t, dt, step_loading_deriv)

    ramp_loading = t
    ramp_loading_deriv = RHEOS.derivBD(ramp_loading, t)
    ramp_response = RHEOS.boltzconvolve(x->exp.(-x), t, dt, ramp_loading_deriv)

    combined_loading = t .+ ones(length(t))
    combined_loading_deriv = RHEOS.derivBD(combined_loading, t)
    combined_response = RHEOS.boltzconvolve(x->exp.(-x), t, dt, combined_loading_deriv)

    all(i -> isapprox(combined_response[i], (step_response[i] + ramp_response[i]), atol=tol), eachindex(combined_response))
end
@test _boltzconvolve_linearcombo(tol)

function _boltzconvolve_nonsing_parabolic(tol)
    # response of Maxwell model to
    # a parabola: 2500 - (t-50)^2
    dt = 0.001
    t = Vector{RheoFloat}(0.0:dt:20.0)
    exact_response = 102 .- 102*exp.(-t) .- 2t

    loading = 2500.0 .- (t .- 50).^2
    loading_derivative = RHEOS.derivBD(loading, t)

    integration_response = RHEOS.boltzconvolve(x->exp.(-x), t, dt, loading_derivative)

    # note that tol is 5x higher here (and sample rate is higher)
    # as trapezoidal method is relatively innacurate.
    all(i -> isapprox(exact_response[i], integration_response[i], atol=5*tol), eachindex(exact_response))
end
@test _boltzconvolve_nonsing_parabolic(tol)

function _boltzconvolve_sing_linear(tol)
    # response of a power-law model
    # to a linear loading:
    dt = 0.001
    t = Vector{RheoFloat}(0.0:dt:20.0)
    β = 0.5
    exact_response = t.^(1.0 - 0.5) / (1.0 - 0.5)

    loading = t
    loading_derivative = RHEOS.derivBD(loading, t)

    t[1] = dt/10.0
    integration_response = RHEOS.boltzconvolve(x->x.^(-β), t, dt, loading_derivative)

    all(i -> isapprox(exact_response[i], integration_response[i], atol=5*tol), eachindex(exact_response))
end
@test _boltzconvolve_sing_linear(tol)

function _boltzconvolve_sing_step(tol)
    # response of a power-law model
    # to a step loading
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    β = 0.5
    exact_response = t.^(-0.5)

    loading = ones(length(t))
    loading_derivative = RHEOS.derivBD(loading, t)

    t[1] = dt/10.0
    integration_response = RHEOS.boltzconvolve(x->x.^(-β), t, dt, loading_derivative)

    # note that first element is skipped due to singularity
    all(i -> isapprox(exact_response[i], integration_response[i], atol=tol), 2:length(t))
end
@test _boltzconvolve_sing_step(tol)

function _boltzconvolve_sing_parabolic(tol)
    # response of power-law model to
    # a parabola: 2500 - (t-50)^2
    dt = 0.001
    t = Vector{RheoFloat}(0.0:dt:20.0)
    β = 0.5
    exact_response = (100/(1-β))*t.^(1-β) .- (2/((1-β)*(2-β)))*t.^(2-β)

    loading = 2500.0 .- (t .- 50).^2
    loading_derivative = RHEOS.derivBD(loading, t)

    t[1] = dt/10.0
    integration_response = RHEOS.boltzconvolve(x->x.^(-β), t, dt, loading_derivative)
    # note that first element is skipped due to singularity
    # and the higher tolerance and skipping of many elements
    # this is one of the hardest cases for the trapezoidal
    # method of hereditary integration to handle when then there
    # is a singularity.
    all(i -> isapprox(exact_response[i], integration_response[i], atol=7.0), 250:length(t))
end
@test _boltzconvolve_sing_parabolic(tol)

function _obj_const_nonsing_ramp(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    exact_response = 1 .- exp.(-t)
    ramp_loading = t
    ramp_loading_derivative = RHEOS.derivBD(ramp_loading, t)

    cost = RHEOS.obj_const_nonsing(nothing, nothing, (x, params)->exp.(-x), t, dt, ramp_loading_derivative, exact_response)

    cost < length(t)*tol^2
end
@test _obj_const_nonsing_ramp(tol)

function _obj_const_nonsing_step(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    exact_response = exp.(-t)
    step_loading = ones(length(t))
    step_loading_deriv = RHEOS.derivBD(step_loading,t)

    cost = RHEOS.obj_const_nonsing(nothing, nothing, (x, params)->exp.(-x), t, dt, step_loading_deriv, exact_response)

    cost < length(t)*tol^2
end
@test _obj_const_nonsing_step(tol)

function _obj_const_nonsing_parabolic()
    # response of Maxwell model to
    # a parabola: 2500 - (t-50)^2
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    exact_response = 102 .- 102*exp.(-t) .- 2t

    loading = 2500.0 .- (t .- 50).^2
    loading_derivative = RHEOS.derivBD(loading, t)

    cost = RHEOS.obj_const_nonsing(nothing, nothing, (x, params)->exp.(-x), t, dt, loading_derivative, exact_response)
    # note that cost is very high for parabolic as
    # hereditary integral approximation is not
    # good for this case.
    cost < 15*length(t)*tol
end
@test _obj_const_nonsing_parabolic()

function _obj_const_sing_linear(tol)
    # response of a power-law model
    # to a linear loading: t
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    β = 0.5
    exact_response = t.^(1.0 - 0.5) / (1.0 - 0.5)

    loading = t
    loading_derivative = RHEOS.derivBD(loading, t)

    t[1] = dt/10.0
    cost = RHEOS.obj_const_sing(nothing, nothing, (x, params)->x.^(-β), t, dt, loading_derivative, exact_response)

    cost < 3*length(t)*tol
end
@test _obj_const_sing_linear(tol)

function _leastsquares_init_const_nonsing_ramp(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    exact_response = 1.0 .- exp.(-t)
    ramp_loading = t
    ramp_loading_derivative = RHEOS.derivBD(ramp_loading, t)
    #ramp_response = RHEOS.boltzconvolve(x->exp.(-x), t, dt, ramp_loading_derivative)
    modulus = (t, params)->(params[1]*exp.(-t/params[2]))

    init_params = [1.0, 1.0]
    results = RHEOS.leastsquares_init(init_params, [0.90, 0.90], [1.1, 1.1], modulus, t, dt, ramp_loading_derivative, exact_response; constant_sampling = true)

    found_params = results[2]

    isapprox(found_params, init_params, atol=tol)
end
@test _leastsquares_init_const_nonsing_ramp(tol)

function _leastsquares_init_var_nonsing_ramp(tol)
    dt = 0.01
    t = [Vector{RheoFloat}(0.0:dt:(15.0-dt)); Vector{RheoFloat}(15.0:10*dt:20.0)]
    exact_response = 1.0 .- exp.(-t)
    ramp_loading = t
    ramp_loading_derivative = RHEOS.derivBD(ramp_loading, t)
    #ramp_response = RHEOS.boltzconvolve(x->exp.(-x), t, dt, ramp_loading_derivative)
    modulus = (t, params)->(params[1]*exp.(-t/params[2]))

    init_params = [1.0, 1.0]
    results = RHEOS.leastsquares_init(init_params, [0.90, 0.90], [1.1, 1.1], modulus, t, dt, ramp_loading_derivative, exact_response; constant_sampling = false)

    found_params = results[2]

    isapprox(found_params, init_params, atol=tol)
end
@test _leastsquares_init_var_nonsing_ramp(tol)

function _leastsquares_init_const_sing_ramp(tol)
    # response of a power-law model
    # to a linear loading:
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    β = 0.5
    exact_response = t.^(1.0 - β) / (1.0 - β)

    loading = t
    loading_derivative = RHEOS.derivBD(loading, t)

    modulus = (t, params)->t.^(-params[1])
    init_params = [0.5]
    results = RHEOS.leastsquares_init(init_params, [0.3], [0.7], modulus, t, dt, loading_derivative, exact_response; constant_sampling = true, singularity=true)
    found_params = results[2]

    isapprox(found_params, init_params, atol=tol)
end
@test _leastsquares_init_const_sing_ramp(tol)

function _leastsquares_init_var_sing_ramp(tol)
    # response of a power-law model
    # to a linear loading:
    dt = 0.01
    t = [Vector{RheoFloat}(0.0:dt:(15.0-dt)); Vector{RheoFloat}(15.0:10*dt:20.0)]
    β = 0.5
    exact_response = t.^(1.0 - β) / (1.0 - β)

    loading = t
    loading_derivative = RHEOS.derivBD(loading, t)

    modulus = (t, params)->t.^(-params[1])
    init_params = [0.5]
    results = RHEOS.leastsquares_init(init_params, [0.3], [0.7], modulus, t, dt, loading_derivative, exact_response; constant_sampling = false, singularity=true)
    found_params = results[2]

    isapprox(found_params, init_params, atol=tol)
end
@test _leastsquares_init_var_sing_ramp(tol)

function _obj_step_nonsing()
    t = Vector{RheoFloat}(0.0:0.001:20.0)
    params = [2.0, 0.5]
    modulus = (x, p)->p[1]*exp.(-x/p[2])
    loading = 1.0
    expected_response = params[1]*exp.(-t/params[2])
    cost = RHEOS.obj_step_nonsing(params, nothing, modulus, t, loading, expected_response)

    cost < tol
end
@test _obj_step_nonsing()

function _obj_step_weighted()
    t = Vector{RheoFloat}(0.0:0.5:20.0)
    params = [2.0, 0.5]
    modulus = (x, p)->p[1]*x.^(-p[2])
    loading = 1.0
    expected_response = loading*params[1]*t.^(-params[2])
    cost = RHEOS.obj_step_weighted(params, nothing, modulus, t, loading, expected_response[2:end], collect(Integer, 2:length(t)))

    cost < tol
end
@test _obj_step_weighted()

function _leastsquares_stepinit_nonsing_ramp(tol)
    dt = 0.01
    t = Vector{RheoFloat}(0.0:dt:20.0)
    params = [2.0, 0.5]
    loading = 1.0
    expected_response = loading*params[1]*exp.(-t/params[2])

    modulus = (x, p)->p[1]*exp.(-x/p[2])
    init_params = [1.0, 1.0]
    results = RHEOS.leastsquares_stepinit(init_params, [0.70, 0.1], [3.0, 1.5], modulus, t, loading, expected_response)

    found_params = results[2]

    isapprox(params, found_params, atol=tol)
end
@test _leastsquares_stepinit_nonsing_ramp(tol)

function _leastsquares_stepinit_sing_ramp(tol)
    dt = 0.1
    t = Vector{RheoFloat}(0.0:dt:20.0)
    params = [2.0, 0.5]
    loading = 1.0
    expected_response = loading*params[1]*t.^(-params[2])

    modulus = (x, p) -> p[1]*x.^(-p[2])
    init_params = [1.0, 1.0]
    results = RHEOS.leastsquares_stepinit(init_params, [0.70, 0.1], [3.0, 1.5], modulus, t, loading, expected_response; singularity=true)

    found_params = results[2]

    isapprox(params, found_params, atol=tol)
end
@test _leastsquares_stepinit_sing_ramp(tol)


#=
2D advection using higher order FV or structured or unstructured mesh
=#

### If the Finch package has already been added, use this line #########
using Finch # Note: to add the package, first do: ]add "https://github.com/paralab/Finch.git"

### If not, use these four lines (working from the examples directory) ###
# if !@isdefined(Finch)
#     include("../Finch.jl");
#     using .Finch
# end
##########################################################################

init_finch("FVadvection2d");

useLog("FVadvection2dlog", level=3)

# Configuration setup
domain(2)
solverType(FV)

use_tri=false;
if use_tri
    timeStepper(RK4)
    # n = 30
    # mesh(QUADMESH, elsperdim=n, bids=4)

    mesh("src/examples/utriangle.msh")  # Using triangles

    add_boundary_ID(2, (x,y) -> (x >= 0.1));
    add_boundary_ID(3, (x,y) -> (y <= 0));
    add_boundary_ID(4, (x,y) -> (y >= 0.3));
    finiteVolumeOrder(2);
else
    timeStepper(RK4)
    n = 10
    mesh(QUADMESH, elsperdim=n, bids=4)
    finiteVolumeOrder(2);
end

# Variables and BCs
u = variable("u", location=CELL)
# boundary(u, 1, FLUX, "(abs(y-0.2) < 0.11) ? sin(2*pi*t)^2 : 0") # x=0
if use_tri
    boundary(u, 1, FLUX, "(abs(y-0.06) < 0.033 && sin(6*pi*t)>0) ? 1 : 0") # x=0
else
    boundary(u, 1, FLUX, "(abs(y-0.2) < 0.11 && sin(6*pi*t)>0) ? 1 : 0") # x=0
end
boundary(u, 2, NO_BC) # x=1
boundary(u, 3, NO_BC) # y=0
boundary(u, 4, NO_BC) # y=1

# Time interval and initial condition
T = 1;
timeInterval(T)
initial(u, "0")

# The flux and source terms of the conservation equation
# F and S in the following equation:
# Dt(int(u dx)) = int(S dx) - int(F.n ds)
if use_tri
    coefficient("a", ["0.1*cos(pi*x/2/0.1)","0.3*sin(pi*x/2/0.1)"], type=VECTOR) # advection velocity
else
    coefficient("a", ["cos(pi*x/2)","sin(pi*x/2)"], type=VECTOR) # advection velocity
end
coefficient("s", ["sin(pi*x)^4 * sin(pi*y)^4"]) # source
# The "upwind" function applies upwinding to the term (a.n)*u with flow velocity a.
# The optional third parameter is for tuning. Default upwind = 0, central = 1. Choose something between these.
flux(u, "upwind(a,u)")
# source(u, "0.1 * s")

# exportCode("fvad2dcode") # uncomment to export generated code to a file
# importCode("fvad2dcode") # uncomment to import code from a file

solve(u)

finalize_finch()

# output to file
# output_values(u, "fvad2d", format="csv");
#output_values(u, "fvad2d", format="vtk");

##### Uncomment below to plot

# xy = Finch.fv_info.cellCenters

# using Plots
# pyplot();
# display(plot(xy[1,:], xy[2,:], u.values[:], st=:surface))


module Schauder
export level, levelK, vectoroflevels, number, finger_pm, finger_permute, permutationmatrix, pickup!, pickup, drop!, drop, dropB1, dropB2, hat, fe_transf, fe_transfB2, fe_transfB1

#%  .. currentmodule:: Schauder
#%
#%  .. _modschauder:
#%
#%  Module Schauder
#%  ---------------
#%
#%  Introduction
#%  ~~~~~~~~~~~~
#%
#%  In the following ``hat(x)`` is the piecewise linear function taking values
#%  values ``(0,0), (0.5,1), (1,0)`` on the interval ``[0,1]`` and ``0`` elsewhere.
#%
#%  The Schauder basis of level ``L > 0`` in the interval ``[a,b]`` can be defined recursively
#%  from ``n = 2^L-1`` classical finite elements :math:`\psi_i(x)` on the grid
#%
#%  	``a + (1:n)/(n+1)*(b-a)``.
#%
#%
#%  Assume that f is expressed as linear combination
#%
#%  	:math:`f(x) = \sum_{i =1}^n c_i \psi_i(x)`
#%
#%  with
#%
#%  	:math:`\psi_{2j-1}(x) = hat(nx-j + 1)` 	for :math:`j = 1 \dots 2^{L-1}`
#%
#%  and
#%
#%  	:math:`\psi_{2j}(x) = hat(nx-j + 1/2)` 	for :math:`j = 1 \dots 2^{L-1}-1`
#%
#%  Note that these coefficients are easy to find for the finite element basis, just
#%
#%  .. code-block:: matlab
#%
#%  	function fe_transf(f, a,b, L)
#%  	    n = 2^L-1
#%  	    return map(f, a + (1:n)/(n+1)*(b-a))
#%  	end
#%

#%
#%  Then the coefficients of the same function with respect to the Schauder basis
#%
#%  	:math:`f(x) = \sum_{i = 1}^n c_i \phi_i(x)`
#%
#%  where for ``L = 2``
#%
#%  	:math:`\phi_2(x) = 2 hat(x)`
#%
#%  	:math:`\phi_1(x) = \psi_1(x) = hat(2x)`
#%
#%  	:math:`\phi_3(x) = \psi_3(x) = hat(2x-1)`
#%
#%  can be computed directly, but also using the recursion
#%
#%  	:math:`\phi_2(x) = \psi_1(x) + 2\psi_2(x) + \psi_2(x)`
#%
#%  This can be implemented inplace (see ``pickup()``), and is used throughout.
#%

#%
#% .. code-block:: matlab
#%
#%  	for l in 1:L-1
#%  		b = sub(c, 2^l:2^l:n)
#%  		b[:] *= 0.5
#%  		a = sub(c, 2^(l-1):2^l:n-2^(l-1))
#%  		a[:] -= b[:]
#%  		a = sub(c, 2^l+2^(l-1):2^l:n)
#%  		a[:] -= b[1:length(a)]
#%  	end
#%

#%  Reference
#%  ~~~~~~~~~
#%
#%
#%  .. function:: pickup!(x)
#%
#%  	Inplace computation of 2^L-1 Schauder-Faber coefficients from
#%  	``2^L-1`` overlapping finite-element coefficients ``x``.
#%
#%  	-- inverse of ``Schauder.drop``
#%
#%  	-- L = level(xj)
#%
#%  .. function:: drop!(x)
#%
#%  	Inplace computation of 2^L-1 finite element coefficients from
#%  	2^L-1 Faber schauder coefficients ``x``.
#%
#%  	-- inverse of ``Schauder.pickup``
#%
#%  .. function:: finger_permute(x)
#%
#%  	Reorders vector ``x`` or matrix ``A`` according to the reordering
#%  	of the elements of a Faber-Schauder-basis from
#%  	left to right, from bottom to top.
#%
#%  .. function:: finger_pm(L, K)
#%
#%  	Returns the permuation used in ``finger_permute``.
#%  	Memoized reordering of faber schauder elements from low level to high level. The last K elements/rows are left untouched.
#%

order(a,b) = a > b ? (b,a): (a,b)

function ilogbi(n::Integer)
	Base.exponent(float(n))
end

#%  .. function:: level(x)
#%
#%  	Gives the no. of levels of the biggest Schauder basis with less then length(x) elements.
#%  		level(x) = ilogb(size(x,1)+1)
#%
level(x) = ilogbi(size(x,1)+1)

#%  .. function:: level(x, K)
#%
#%  	Gives the no. of levels ``l`` of the biggest Schauder basis with less then length(x) elements
#%  	and the number of additional elements ``n-2^l+1``.
#%
function levelK(x)
	n = size(x,1)
	l = ilogbi(n+1)
	(l, n-2^l+1)
end

#%  .. function:: vectoroflevels(L, K)
#%
#%  	Gives a vector with the level of the hierarchical elements.
#%

function vectoroflevels(L, K)
	n = 2^L
	o = zeros(Int32, n-1+K)
	i = 0
	for l in 0:L-1
	   for j in 2^l:2^l:n-1
		o[j] = l
	   end
	end
	# additional elements (levels) at the end
	for l in n^2:n^2-1+K
		o[l] = l
	end
	o
end

function number(L, K)
	n = 2^L
	o = zeros(Int32, n-1+K)
	i = 0
	for l in 0:L-1
	   for j in 2^l:2^(l+1):n-1
		i+=1
		o[j] = i
	   end
	end
	for l in n:n-1+K
		i+=1
		o[l] = i
	end
	o
end
order_tbl = nothing

#memoized reordering of faber schauder elements from low level to high level
function finger_pm(L, K)
	global order_tbl
	if order_tbl == nothing
		order_tbl = Dict()
	end

	if haskey(order_tbl, (L,K))
               pm = order_tbl[(L,K)]
	else
		pm = sortperm(number(L,K))
		assign(order_tbl,pm, (L,K))

	end
	pm
end

# permutationmatrix(sortperm(number(L))
#  pm * pickup_Sigma!(fe_sigma(rand(100), 0, 1, 3),3) * pm'
function permutationmatrix(p)
	d = length(p)
	P = zeros(eltype(p), d,d)
	for i in 1:d
		P[i, p[i]] = 1
	end
	P
end

function finger_permute(x::Vector)
	(L,K) = levelK(x)
	x[finger_pm(L,K)] #undo with ipermute!(x, pm)
end

function finger_permute(Sigma::Matrix)
	(L,K) = levelK(Sigma)
	pm = permutationmatrix(finger_pm(L,K))
	pm * Sigma * pm'
end

#leaves K elements untouched
function pickup!(xj)
	L = level(xj)
	n = 2^L-1

	for l in 1:L-1
		b = sub(xj, 2^l:2^l:n)
		b[:] *= 0.5
		a = sub(xj, 2^(l-1):2^l:n-2^(l-1))
		a[:] -= b[:]
		a = sub(xj, 2^l+2^(l-1):2^l:n)
		a[:] -= b[1:length(a)]
	end
	xj
end

#leaves K elements untouched
function drop!(xj)
	L = level(xj)
	n = 2^L - 1

	for l in L-1:-1:1
		b = sub(xj, 2^l:2^l:n)
		a = sub(xj, 2^(l-1):2^l:n-2^(l-1))
		a[:] += b[:]
		a = sub(xj, 2^l+2^(l-1):2^l:n)
		a[:] += b[1:length(a)]
		b[:] *= 2

	end
	xj
end

compose(f, g) = x -> f(g(x))
drop = compose(drop!,copy)
pickup = compose(pickup!,copy)

function dropB1(yj)
	xj = copy(yj)
	(L, K) = levelK(xj)
	assert(K==1)
	n = 2^L-1
	xj[1:n] .+= xj[end]
	xj
end

function dropB2(yj)
	xj = copy(yj)
	(L, K) = levelK(xj)
	assert(K==2)
	n = 2^L-1

	for i in 1:n
		xj[i] += xj[end]*i/(n+1) + xj[end-1]*(n+1-i)/(n+1)
	end
	xj = xj[[n+1, 1:n, n+2]] #permute
end

#%  .. function:: hat(x)
#%
#%  	Hat function. Piecewise linear functions with values (-inf,0), (0,0),(0.5,1), (1,0), (inf,0).
#%  	-- ``x`` vector or number
function hat(x)
	max(1. .- abs(2.*x .- 1.),0.)
end

function fe_transf(f, a,b, L)
	n::Float64 = 2^L-1
	return map!(f, a .+ [1.:n]/(n+1.)*(b-a))
end

# versions for different basis elements

# phi1(x) = x
# phi2(x) = 1-x
function fe_transfB2(f, a,b, L)
	n::Float64 = 2^L-1
		return	[map!(f, a .+ [1.:n]/(n+1.)*(b-a)) .- [1.:n]/(n+1.)*f(a) .- [n:-1.:1.]/(n+1.)*f(b) ,f(a) , f(b)]
end
# phi1(x) = c
# periodic boundary condition
function fe_transfB1(f, a,b, L)
	n::Float64 = 2^L-1
	return	[map!(f, a .+ [1:n]/(n+1)*(b-a)) .- 0.5(f(a) + f(b)), 0.5(f(a) + f(b))]
end

include("npbayes.jl")

end


function MH(data::Data, prior::Prior, candiate::Candidate, draws = 1_000)

    x, n = data.x, data.n
    J = length(x)

    # Prepare posterior
    post = Chain(zeros(draws), zeros(draws), zeros(draws, N), zeros(draws, 2))

    # Starting values (random in the unit area)
    post.μ[1] = rand()
    post.σ[1] = rand()

    post.acceptance[1, :] = 1

    # Old posterior
    lp = logposterior(prior::Prior, post.μ[1], post.σ[1], x, n)

    for i in 2:draws

        ### MU ###

        # Candidate value for μ
        μ = rand(Normal(post.μ[i - 1], 0.5))

        # Candidate log-posterior
        lpc = logposterior(prior::Prior, μ, post.σ[i - 1], x, n)

        if (lpc - lp) > log(rand())
            post.μ[i] = μ
            post.acceptance[:, 1] = 1
        else
            post.μ[i] = post.μ[i - 1]
        end

        ### SIGMA ###

        # Candidate value for σ
        σ = rand(Normal(post.σ[i - 1], 0.5))

        # Candidate log-posterior
        lpc = logposterior(prior::Prior, post.μ[i - 1], σ, x, n)

        if (lpc - lp) > log(rand())
            post.σ[i] = σ
            post.acceptance[:, 2] = 1
        else
            post.σ[i] = post.σ[i - 1]
        end

        ### THETA ###
        for j in 1:J
            θ[i, j] = rand(Beta((1 - post.σ[i])/post.σ[i] * post.μ[i] + x[j], (1 - post.σ[i])/post.σ[i] * (1 - post.μ[i]) + n[j] - x[j]))
        end

    end
    return post
end

function logprior(prior::Beta, value::AbstractFloat)
    μ, ϕ = mu_and_phi(prior.a, prior.b)
    (μ - 1)*log(value) + (μ - 1)*log(1 - value)
end

function loglikelihood(μ, σ, x, n)
    l = logpdf(Beta(μ * (1 - σ)/σ + x), (1 - μ)*(1 - σ)/σ + n - x)
    l = l - logpdf(Beta(μ * (1 - σ)/σ, (1 - μ)*(1 - σ)/σ)) * length(x)
    return l
end

function logposterior(prior::Prior, μ, σ, x, n)
    l = loglikelihood(μ, σ, x, n)
    p = logprior(prior.μ, μ) + logprior(prior.σ, σ)
    return l+p
end

function alpha_and_beta(μ, ϕ)
    α = μ * (1 - ϕ)/ϕ
    β = (1 - μ) * (1 - ϕ)/ϕ
    return (α = α, β = β)
end

function mu_and_phi(α, β)
    μ = α / (α + β)
    ϕ = 1 / (1 + α + β)
end


# The 6D simplex
for N in 1:3:51
    facts("Simplex in $N dimensions") do
        N = 6
        m = Model()
        @variable(m, x[1:N] >= 0)
        @constraint(m, sum{x[i], i=1:N} == 1)

        v, r = get_extrema(m)

        @fact length(v) --> N
        @fact length(r) --> 0

        for k in 1:N
            ex = zeros(N)
            ex[k] = 1
            @fact ex in v --> true
        end
    end
end

# The 6D simplex, plus the origin
for N in 1:3:39
    facts("Simplex with the origin in $N dimensions") do
        m = Model()
        @variable(m, x[1:N] >= 0)
        @constraint(m, sum{x[i], i=1:N} ≤ 1)

        v, r = get_extrema(m)

        @fact length(v) --> N+1
        @fact length(r) --> 0

        @fact zeros(N) in v --> true
        for k in 1:N
            ex = zeros(N)
            ex[k] = 1
            @fact ex in v --> true
        end
    end
end


# ---
# title: 790. Domino and Tromino Tiling
# id: problem790
# author: Indigo
# date: 2021-01-29
# difficulty: Medium
# categories: Dynamic Programming
# link: <https://leetcode.com/problems/domino-and-tromino-tiling/description/>
# hidden: true
# ---
#
# We have two types of tiles: a 2x1 domino shape, and an "L" tromino shape.
# These shapes may be rotated.
#
#
#
#     XX  <- domino
#
#     XX  <- "L" tromino
#     X
#
#
# Given N, how many ways are there to tile a 2 x N board? **Return your answer
# modulo 10^9 + 7**.
#
# (In a tiling, every square must be covered by a tile. Two tilings are
# different if and only if there are two 4-directionally adjacent cells on the
# board such that exactly one of the tilings has both squares occupied by a
# tile.)
#
#
#
#     **Example:**
#     Input: 3
#     Output: 5
#     Explanation:
#     The five different ways are listed below, different letters indicates different tiles:
#     XYZ XXZ XYY XXY XYY
#     XYZ YYZ XZZ XYY XXY
#
# **Note:**
#
#   * N  will be in range `[1, 1000]`.
#
#
#
#
## @lc code=start
using LeetCode

function num_tilings(N::Int)
    f0, f1= 1, 1
    g0, g1 = 0, 1
    for i in 1:N-1
        f0, f1, g0, g1 = f1, f0 + f1 + 2 * g0, g1, g1 + f1
    end
    f1
end
## @lc code=end


export FDTDEnv

abstract Acoustics
abstract AcousticEnvironment <:Acoustics
abstract FDTDAcousticEnvironment <: AcousticEnvironment

immutable FDTDEnv <: FDTDAcousticEnvironment
	Fs::Float64        # sampling frequency
	c::Float64         # speed of sound
	X::Float64         # spatial step
	T::Float64         # temp. step
        scheme::FDTDScheme #FDTD scheme
end

"""
Create acoustic environment for FDTD simulation

## Usage

* 'FDTD(X::Float64, scheme::FDTDScheme)'
   * Create an acoustic env. for a given spatial step `X`
* 'FDTD(scheme::FDTDScheme, Fs::Float64)':
   * Create an acoustic env. for a given sampling frequency `Fs`
"""
function FDTDEnv(X::Float64, scheme::FDTDScheme; c = 343.)

	Fs = 343/(X*scheme.λ)   # Sampling Frequency
	return FDTDEnv(Fs,c,X,1/Fs,scheme)

end

function FDTDEnv(scheme::FDTDScheme, Fs::Float64; c = 343.)

	X = 343/(Fs*scheme.λ)   # Sampling Frequency
	return FDTDEnv(Fs,c,X,1/Fs,scheme)

end

function Base.show(io::IO, env::FDTDEnv)
	println(io, "FDTD Acoustic Env.")
	println(io, "Sampling freq. : $(round(env.Fs/1000,1))   kHz")
	println(io, "Spatial step   : $(round(env.X*100,2)) cm")
	println(io, "Speed of sound : $(round(env.c,1)) m/s")
	println(io, "FDTD Scheme    : $(env.scheme.name)")
end


"""
    ronkincoefficient(f, cc::ComponentComplement, N)

Compute the the Ronkin coefficient for the `ComponentComplement` `cc` using the trapezoidal rule
with `N` samples (which results in a steplength of ``2π/N``).
"""
function ronkincoefficient(f, cc::ComponentComplement, N)
    n_f = ronkinfunction(f, point(cc), N)

    n_f - order(cc)[1] * point(cc)[1] - order(cc)[2] * point(cc)[2]
end
function ronkincoefficients(f, ccs, nsamples=1024)
    map(cc -> ronkincoefficient(f, cc, nsamples), ccs)
end

"""
    ronkinfunction(f, z, N)

Evaluate the Ronkin function for `f` at `z` using the trapezoidal rule with `N` samples.

The Ronkin function is defined as
```math
x ⟼ \\frac{1}{(2πi)^2}∫_{\\text{Log}^{-1}|x|}\\log|f(z_1,z_2)| \\frac{dz_1dz_2}{z_1z_2}
```
where ``\\text{Log}^{-1}|x|`` is the fiber w.r.t. the log absolute map ``\\text{Log}|⋅|``.
Therefore
```math
\\text{Log}|(w_1,w_2)| = \\{ (exp(w_1+iθ_1), exp(w_2+iθ_2)) | θ_1, θ_2 ∈ [0,2π]\\}

Wit a change of variables ``(θ_1, ϴ_2) ⟼ (exp(w_1+iθ_1), exp(w_2+iθ_2))`` we get
```math
-\\frac{1}{(2πi)^2}∫_0^{2π}∫_0^{2π}\\log|f(exp(w_1+iθ_1), exp(w_2+iθ_2))|
    exp(w_1+iθ_1)exp(w_2+iθ_2) \\frac{dθ_1dθ_2}{exp(w_1+iθ_1)exp(w_2+iθ_2)}
```
which simplifies to
```math
\\frac{1}{2π}∫_0^{2π}\\frac{1}{2π}∫_0^{2π}\\log|f(exp(w_1+iθ_1), exp(w_2+iθ_2))| dθ_1dθ_2
```

Using the trapezoidal rule with `N` samples we can approximate the integral with
```math
1/N^2 ∑_{l=0}^{N-1}∑_{k=0}^{N-1} \\log|f(exp(w_1+iθ_1), exp(w_2+iθ_2))|
```
"""
function ronkinfunction(f, z, N)
    e_x, e_y = exp.(z)
    h = 2π/N
    # sum = 0.0
    partition = partition_work(N)
    inner_range = 0:N-1
    sums = Vector{Float64}(undef, Threads.nthreads())
    Threads.@threads for tid = 1:Threads.nthreads()
        r = shift_range(partition[tid], -1)
        partial_sum = ronkin_kernel(r, inner_range, f, h, e_x, e_y)
        sums[tid] = partial_sum
    end
    out = sum(sums) / N^2
    out
end

shift_range(r, n) = (r.start + n):(r.stop + n)

function ronkin_kernel(partial_outer_range, inner_range, f, h, e_x, e_y)
    partial_sum = 0.0
    for l in partial_outer_range, k in inner_range
        z1 = e_x * Base.FastMath.cis_fast(h*k)
        z2 = e_y * Base.FastMath.cis_fast(h*l)
        partial_sum += log(Base.FastMath.abs_fast(f(SVector(z1, z2))))
    end
    partial_sum
end

function ronkinpolynomial(p::MP.AbstractPolynomial, ronkincoeffs::Vector{Float64}, ccs)
    x, y = MP.variables(p)
    terms = map(ronkincoeffs, ccs) do r, cc
        i, j = order(cc)
        # We truncate the coefficients after 12 digits to avoid numerical problems
        r = trunc(r, digits=12)
        PolynomialAmoebas.Tropical(r) * x^i * y^j
    end
    MP.polynomial(terms)
end


"""
    quadratic_matrix_form(f, vars)

Extract the linear and quadratic matrices of a system of polynomial equations.

### Input

- `f`    -- vector of polynomials
- `vars` -- vector of variables

### Output

Matrices `F1` and `F2` of size `n × n` and `n × n^2` respectively,
such that `F1` contains the linear coefficients of the polynomial vector
field `f` and `F2` the quadratic coefficients, which indexing that respects
the Kronecker power `vars ⊗ vars`.

### Examples

TO-DO
"""
function quadratic_matrix_form(f, vars)
    n = length(vars)
    @assert length(f) == n

    # get the linear part
    F1 = zeros(n, n)
    update!(F1, vars, f)

    # get the quadratic part
    F2 = zeros(n, n^2)
    update!(F2, kron_pow(vars, 2), f)

    return F1, F2
end

function update!(Fk, xk, f)
    n = size(Fk, 1)
    for i in 1:n
        monom = monomials(f[i])
        S = Set()
        for (j, x) in enumerate(xk)
            # skip monomials already seen
            x ∈ S ? continue : push!(S, x)
            idx = findfirst(==(x), monom)
            if !isnothing(idx)
                Fk[i, j] = coefficient(f[i], x)
            end
        end
    end
    return Fk
end