# TODO: include matype options in macd, rsi, and adx functions

@doc doc"""
momentum{Float64}(x::Vector{Float64}, n::Int64=1)

Momentum indicator (price now vs price `n` periods back)
""" ->
function momentum{Float64}(x::Vector{Float64}, n::Int64=1)
    @assert n>0 "Argument n must be positive."
    return diffn(x, n)
end

@doc """
roc{Float64}(x::Vector{Float64}, n::Int64=1)

Rate of change indicator (percent change between i'th observation and (i-n)'th observation)
""" ->
function roc{Float64}(x::Vector{Float64}, n::Int64=1)
    @assert n<size(x,1) && n>0 "Argument n out of bounds."
    out = zeros(x)
    @inbounds for i = n:size(x,1)
        out[i] = x[i]/x[i-n] - 1.0
    end
    out[1:n] = NaN
    return out * 100.0
end

@doc doc"""
macd{Float64}(x::Vector{Float64}, nfast::Int64=12, nslow::Int64=26, nsig::Int64=9)

Moving average convergence-divergence

`Output:`

- Column 1: MACD
- Column 2: MACD Signal Line
- Column 3: MACD Histogram
""" ->
function macd{Float64}(x::Vector{Float64}, nfast::Int64=12, nslow::Int64=26, nsig::Int64=9;
                       fastMA::Function=ema, slowMA::Function=ema, signalMA::Function=sma)
    out = zeros(Float64, (length(x),3))
    out[:,1] = fastMA(x, nfast) - slowMA(x, nslow)
    out[:,2] = signalMA(out[:,1], nsig)
    out[:,3] = out[:,1] - out[:,2]
    return out
end

@doc doc"""
rsi{Float64}(x::Vector{Float64}, n::Int64=14; ma::Function=ema, args...)

Relative strength index
""" ->
function rsi{Float64}(x::Vector{Float64}, n::Int64=14; ma::Function=ema, args...)
    @assert n<size(x,1) && n>0 "Argument n is out of bounds."
    N = size(x,1)
    ups = zeros(N)
    dns = zeros(N)
    zro = 0.0
    dx = [NaN; diff(x)]
    @inbounds for i=2:N
        if dx[i] > zro
            ups[i] = dx[i]
        elseif dx[i] < zro
            dns[i] = -dx[i]
        end
    end
    rs = [NaN; ma(ups[2:end], n; args...) ./ ma(dns[2:end], n; args...)]
    return 100.0 - 100.0 ./ (1.0 + rs)
end

@doc doc"""
adx{Float64}(hlc::Array{Float64}, n::Int64=14; wilder=true)

Average directional index

`Output:`

- Column 1: DI+
- Column 2: DI-
- Column 3: ADX
""" ->
function adx{Float64}(hlc::Array{Float64}, n::Int64=14; ma::Function=ema, args...)
    @assert n<size(hlc,1) && n>0 "Argument n is out of bounds."
    if size(hlc,2) != 3
        error("HLC array must have three columns")
    end
    N = size(hlc,1)
    updm = zeros(N)
    dndm = zeros(N)
    updm[1] = dndm[1] = NaN
    @inbounds for i = 2:N
        upmove = hlc[i,1] - hlc[i-1,1]
        dnmove = hlc[i-1,2] - hlc[i,2]
        if upmove > dnmove && upmove > 0.0
            updm[i] = upmove
        elseif dnmove > upmove && dnmove > 0.0
            dndm[i] = dnmove
        end
    end
    dip = [NaN; ma(updm[2:N], n; args...)] ./ atr(hlc, n) * 100.0
    dim = [NaN; ma(dndm[2:N], n; args...)] ./ atr(hlc, n) * 100.0
    dmx = abs(dip-dim) ./ (dip+dim)
    adx = [fill(NaN,n); ma(dmx[n+1:N], n; args...)] * 100.0
    return [dip dim adx]
end

@doc doc"""
psar{Float64}(hl::Array{Float64,2}, af::Float64=0.02, af_max::Float64=0.2, af_min::Float64=0.02)

Parabolic stop and reverse (SAR)

- hl: 2D array of high and low prices in first and second columns respectively
- af_mi: starting/initial value for acceleration factor
- af_max: maximum acceleration factor (accel factor capped at this value)
- af_inc: increment to the acceleration factor (speed of increase in accel factor)
""" ->
function psar{Float64}(hl::Array{Float64}, af_min::Float64=0.02, af_max::Float64=0.2, af_inc::Float64=af_min)
    @assert af_min<1.0 && af_min>0.0 "Argument af_min must be in [0,1]."
    @assert af_max<1.0 && af_max>0.0 "Argument af_max must be in [0,1]."
    @assert af_inc<1.0 && af_inc>0.0 "Argument af_inc must be in [0,1]."
    @assert size(hl,2) == 2 "Argument hl must have 2 columns."
    ls0 = 1
    ls = 0
    af0 = af_min
    af = 0.0
    ep0 = hl[1,1]
    ep = 0.0
    maxi = 0.0
    mini = 0.0
    sar = zeros(Float64,size(hl,1))
    sar[1] = hl[1,2] - std(hl[:,1]-hl[:,2])
    @inbounds for i = 2:size(hl,1)
        ls = ls0
        ep = ep0
        af = af0
        mini = min(hl[i-1,2], hl[i,2])
        maxi = max(hl[i-1,1], hl[i,1])
        # Long/short signals and local extrema
        if (ls == 1)
            ls0 = hl[i,2] > sar[i-1] ? 1 : -1
            ep0 = max(maxi, ep)
        else
            ls0 = hl[i,1] < sar[i-1] ? -1 : 1
            ep0 = min(mini, ep)
        end
        # Acceleration vector
        if ls0 == ls  # no signal change
            sar[i] = sar[i-1] + af*(ep-sar[i-1])
            af0 = (af == af_max) ? af_max : (af + af_inc)
            if ls0 == 1  # current long signal
                af0 = (ep0 > ep) ? af0 : af
                sar[i] = min(sar[i], mini)
            else  # current short signal
                af0 = (ep0 < ep) ? af0 : af
                sar[i] = max(sar[i], maxi)
            end
        else  # new signal
            af0 = af_min
            sar[i] = ep0
        end
    end
    return sar
end

@doc doc"""
kst{Float64}(x::Vector{Float64},
                      nroc::Vector{Int64}=[10,15,20,30], navg::Vector{Int64}=[10,10,10,15],
                      wgts::Vector{Int64}=collect(1:length(nroc)); ma::Function=sma)

KST (Know Sure Thing) -- smoothed and summed rates of change
""" ->
function kst{Float64}(x::Vector{Float64}, nroc::Vector{Int64}=[10,15,20,30], navg::Vector{Int64}=[10,10,10,15];
                      wgts::Vector{Int64}=collect(1:length(nroc)), ma::Function=sma)
    @assert length(nroc) == length(navg)
    @assert all(nroc.>0) && all(nroc.<size(x,1))
    @assert all(navg.>0) && all(navg.<size(x,1))
    N = length(x)
    k = length(nroc)
    out = zeros(x)
    @inbounds for j = 1:k
        out += ma(roc(x, nroc[j]), navg[j]) * wgts[j]
    end
    return out
end

@doc doc"""
wpr{Float64}(hlc::Array{Float64,2}, n::Int64=14)

Williams %R
""" ->
function wpr{Float64}(hlc::Array{Float64,2}, n::Int64=14)
    hihi = runmax(hlc[:,1], n, false)
    lolo = runmin(hlc[:,2], n, false)
    return -100 * (hihi - hlc[:,3]) ./ (hihi - lolo)
end

@doc doc"""
cci{Float64}(hlc::Array{Float64,2}, n::Int64=20, c::Float64=0.015; ma::Function=sma)

Commodity channel index
""" ->
function cci{Float64}(hlc::Array{Float64,2}, n::Int64=20, c::Float64=0.015; ma::Function=sma, args...)
    tp = (hlc[:,1] + hlc[:,2] + hlc[:,3]) / 3.0
    dev = runmad(tp, n, false, fun=mean)
    avg = ma(tp, n; args...)
    return (tp - avg) ./ (c * dev)
end

@doc doc"""
stoch{Float64}(hlc::Array{Float64,2}, nK::Int64=14, nD::Int64=3;
               kind::ByteString="fast", ma::Function=sma, args...)

Stochastic oscillator (fast or slow)
""" ->
function stoch{Float64}(hlc::Array{Float64,2}, nK::Int64=14, nD::Int64=3;
                        kind::Symbol=:fast, ma::Function=sma, args...)
    @assert kind == :fast || kind == :slow "Argument `kind` must be either :fast or :slow"
    @assert nK<size(hlc,1) && nK>0 "Argument `nK` out of bounds."
    @assert nD<size(hlc,1) && nD>0 "Argument `nD` out of bounds."
    hihi = runmax(hlc[:,1], nK, false)
    lolo = runmin(hlc[:,2], nK, false)
    out = zeros(Float64, (size(hlc,1),2))
    out[:,1] = (hlc[:,3]-lolo) ./ (hihi-lolo) * 100.0
    out[:,2] = ma(out[:,1], nD; args...)
    if kind == "slow"
        out[:,1] = out[:,2]
        out[:,2] = ma(out[:,1], nD; args...)
    end
    return out
end

@doc doc"""
smi{Float64}(hlc::Array{Float64,2}, n::Int64=13, nFast::Int64=2, nSlow::Int64=25, nSig::Int64=9;
             maFast::Function=ema, maSlow::Function=ema, maSig::Function=sma)

SMI (stochastic momentum oscillator)
""" ->
function smi{Float64}(hlc::Array{Float64,2}, n::Int64=13, nFast::Int64=2, nSlow::Int64=25, nSig::Int64=9;
                      maFast::Function=ema, maSlow::Function=ema, maSig::Function=sma)
    hihi = runmax(hlc[:,1], n, false)
    lolo = runmin(hlc[:,2], n, false)
    hldif = hihi-lolo
    delta = hlc[:,3] - (hihi+lolo) / 2.0
    numer = maSlow(maFast(delta, nFast), nSlow)
    denom = maSlow(maFast(hldif, nFast), nSlow) / 2.0
    out = zeros(Float64, (size(hlc,1),2))
    out[:,1] = 100.0*(numer./denom)
    out[:,2] = maSig(out[:,1])
    return out
end


export zoom_Nwt
function zoom_Nwt(h :: LineModel,
                  t₀ :: Float64,
                  t₁ :: Float64;
                  c₁ :: Float64=0.01,
                  tol :: Float64=1e-7,
                  ϵ :: Float64=1e-10,
                  maxiter :: Int=100,
                  verbose :: Bool=false)

        if obj(h,t₀)<obj(h,t₁)
          tl=t₀
          th=t₁
        else
          tl=t₁
          th=t₀
        end

        hl=obj(h,tl)
        dhl=grad(h,tl)
        hh=obj(h,th)
        dhh=grad(h,th)

        h₀=obj(h,t₀)
        dh₀=grad(h,t₀)

        γ=0.8
        t=t₁
        tp=t₀
        tqnp=t₀
        hk=0
        hkm1=0
        gkm1=0
        hp=0
        i=0

        hk=obj(h,t)
        gk=grad(h,t)
        hkm1=obj(h,tqnp)
        gkm1=grad(h,tqnp)

        verbose && @printf(" iter        tₗ        tₕ         t        hₗ        hₕ         hk         gk\n")
        verbose && @printf(" %7.2e %7.2e  %7.2e  %7.2e  %7.2e %7.2e %7.2e %7.2e\n", i,tl,th,t,hl,hh,hk,gk)

        while ((i<maxiter) & (abs(gk)>tol)) || i==0

          if (hk>h₀+c₁*(t-t₀)*dh₀) || (hl<=hk)
            th=t
            tlast=th
            hlast=hh
            dhlast=dhh
            hh=hk
            dhh=gk
          else
            #gk=grad(h,t)
            if (abs(gk)<ϵ)
              topt=t
              iter=i
              return (topt,iter)
            elseif gk*(th-tl)>=0
              th=tl
              hh=hl
              dhh=dhl
            end
            tl=t
            tlast=tl
            hlast=hl
            dhlast=dhl
            hl=hk
            dhl=gk
          end

          kₖ=hess(h,t)
          dN=-gk/kₖ #direction de Newton

          if ((tp-t)*dN>0) & (dN/(tp-t)<γ)
            tplus = t + dN
            hplus = obj(h, tplus)
            gplus = grad(h,tplus)
            verbose && println("N")
          else
            tplus = (t+tp)/2
            hplus = obj(h, tplus)
            gplus = grad(h,tplus)
            verbose && println("B")
          end

          if t>tp
            if gplus<0
              tp=t
              tqnp=t
              t=tplus
            else
              tqnp=t
              t=tplus
            end
          else
            if gplus>0
              tp=t
              tqnp=t
              t=tplus
            else
              tqnp=t
              t=tplus
            end
          end

          #mise à jour des valeurs
          hkm1=hk
          gkm1=gk
          hk=hplus
          gk=gplus
          i=i+1
          #println("on continue d'itérer")
          verbose && @printf(" %7.2e %7.2e  %7.2e  %7.2e %7.2e %7.2e %7.2e %7.2e\n", i,tl,th,t,hl,hh,hk,gk)

        end
        topt=t
        iter=i

        return (topt,iter)
end


"""
Compute B-Spline basis
    bs(x, df, norder, nderiv=0)
    :param x: time samples
    :param df: degree of freedom
    :param norder: order of splines
    :param nderiv: derivative number
"""
function bs(x::Vector, df, norder, nderiv=0)
    n = length(x);

    nbreaks = df - norder + 2;
    breaks = LinRange(minimum(x), maximum(x), nbreaks);
    # Check Inputs
    if df <= 0
        error("Degrees of freedom <= 0.")
    end

    if norder <= 0
        error("order of splines <= 0.")
    end

    if minimum(diff(x)) < 0
        isrt = sortperm(x);
        sort!(x);
        sortwrd = true;
    else
        sortwrd = false;
    end

    if x[1] - breaks[1] < -1e-10 || x[n] - breaks[nbreaks] > 1e-10
        println([x[1], x[n]])
        error("Argument values out of range")
    end

    # set some abbreviations
    k = norder;  # order of splines
    km1 = k-1;
    nb = length(breaks);  # number of break points
    nx = length(x);  # number of argument values
    nd = nderiv+1;  # ND is order of derivative plus one
    ns = nb-2+k;  # number of splines to compute
    if ns < 1
        println("There are no B-splines for the given input")
        bsplinemat = [];
        return bsplinemat
    end
    onenx = ones(nx);
    onenb = ones(k);
    onens = ones(ns);

    # augment break sequence to get knots by adding a  K-fold knot at
    # each end.

    knots = [breaks[1].*ones(km1); breaks; breaks[nb].*ones(km1)];
    nbasis = length(knots) - k;

    # For each i, determine left(i) so that K<= left(i) < nbasis+1, and
    # within that restriction, knots(left(i)) <= pts(i) < knot(left(i)+1)

    knotslower = knots[1:nbasis];
    index = sortperm([knotslower; x]);
    point = findall(index .> nbasis) - collect(1:length(x));
    left = maximum(hcat(point, k*onenx),dims=2);

    # compute bspline values and derivatives, if needed:

    # initialize the b array.
    temp = transpose([1; zeros(km1)]);
    b = repeat(temp, nd*nx, 1);
    nxs = nd.*collect(1:nx);

    # run the recurrence simultaneously for all x(i)

    # First, bring it upt to the intended level:

    for j in 1:(k-nd)
        saved = zeros(nx);
        for r in 1:j
            leftpr = round.(Integer, left .+ r);
            tr = knots[leftpr] - x;
            tl = x .- knots[leftpr.-j];
            term = b[nxs,r]./(tr+tl);
            b[nxs,r] = saved + tr.*term;
            saved = tl.*term;
        end
        b[nxs,j+1] = saved;
    end

    # save the B-spline values in successive blocks in b.

    for jj in 1:(nd-1)
        j = k - nd + jj;
        saved = zeros(nx);
        nxn = nxs - 1;
        for r in 1:j
            leftpr = left+r;
            tr = knots[leftpr] - x;
            tl = x-knots[leftpr-j];
            term = b[nxs,r]./(tr+tl);
            b[nxn,r] = saved + tr.*term;
            saved = tl.*term;
        end
        b[nxn, j+1] = saved;
        nxs = nxn;
    end

    # now use the fact that derivative values can be obtained by differencing

    for jj in (nd-1):-1:1
        j = k - jj;
        temp = collect(jj:(nd-1)).*onenx + ones(nd-jj)*nxn;
        nxs = reshape(temp, (nd-1-jj+1)*nx, 1);
        for r in j:-1:1
            leftpr = left + r;
            temp = ones(nd-jj) * (knots[leftpr] - knots[leftpr-j])/j;
            b[nxs,r] = -1*b[nxs,r]./temp;
            b[nxs,r+1] = b[nxs,r+1] - b[nxs,r];
        end
    end

    # Finally, zero out all rows of b corresponding to x outside the basic
    # interval, [breaks[1] ... breaks[nb]]

    index = findall((x.<breaks[1]) .| (x .> breaks[nb]));
    if !isempty(index)
        temp = [(1-nd):0].*ones(length(index))+nd*ones(nd).*index;
        b[temp, :] = zeros(nd*length(index),k);
    end

    # set up output matrix bsplinemat
    width = maximum([ns, nbasis]) + km1 + km1;
    cc = zeros(nx*width);
    index = collect((1-nx):0)*transpose(onenb) + nx * (left*transpose(onenb) + onenx*transpose(collect(-km1:0)));
    index = round.(Integer, index);
    cc[index] = b[nd*collect(1:nx), :];
    # (This uses the fact that for a column vector v and a matrix A,
    #  v(A)(i,j) = v(A(i,j)), all i, j.)
    index2 = round.(Integer, collect((1-nx):0)*transpose(onens) + nx * onenx*transpose(collect(1:ns)));
    bsplinemat = reshape(cc[index2],nx,ns);

    if sortwrd
        temp = copy(bsplinemat);
        bsplinemat[isrt,:] = temp;
    end

    return bsplinemat
end


module Grid

include("grid_utils.jl")
include("nodes_wavenumbers.jl")
include("fourier_grid.jl")

end # module Grid


using Base: axes1, tail
using OffsetArrays
import Statistics
using Statistics: mean, var
import AxisArrays
using ImageMorphology: dilate!, erode!

# Compat.@dep_vectorize_2arg Gray atan2
# Compat.@dep_vectorize_2arg Gray hypot

"""
`M = meanfinite(img, region)` calculates the mean value along the dimensions listed in `region`, ignoring any non-finite values.
"""
meanfinite(A::AbstractArray{T}, region) where {T<:Real} = _meanfinite(A, T, region)
meanfinite(A::AbstractArray{CT}, region) where {CT<:Colorant} = _meanfinite(A, eltype(CT), region)
function _meanfinite(A::AbstractArray, ::Type{T}, region) where T<:AbstractFloat
    inds = Base.reduced_indices(A, region)
    K = similar(Array{Int}, inds)
    S = similar(Array{eltype(A)}, inds)
    fill!(K, 0)
    fill!(S, zero(eltype(A)))
    sumfinite!(S, K, A)
    S./K
end
_meanfinite(A::AbstractArray, ::Type, region) = mean(A, dims=region)  # non floating-point

using Base: check_reducedims, reducedim1, safe_tail
using Base.Broadcast: newindex

"""
    sumfinite!(S, K, A)

Compute the sum `S` and number of contributing pixels `K` for
reductions of the array `A` over dimensions. `S` and `K` must have
identical indices, and either match `A` or have singleton-dimensions for
the dimensions that are being summed over. Only pixels with finite
value are included in the tallies of `S` and `K`.

Note that the pixel mean is just S./K.
"""
function sumfinite!(S, K, A::AbstractArray{T,N}) where {T,N}
    check_reducedims(S, A)
    isempty(A) && return S, K
    axes(S) == axes(K) || throw(DimensionMismatch("S and K must have identical axes"))

    indsAt, indsSt = safe_tail(axes(A)), safe_tail(axes(S))
    keep, Idefault = _newindexer(indsSt)
    if reducedim1(S, A)
        # keep the accumulators as a local variable when reducing along the first dimension
        i1 = first(axes1(S))
        @inbounds for IA in CartesianIndices(indsAt)
            IS = newindex(IA, keep, Idefault)
            s, k = S[i1,IS], K[i1,IS]
            for i in axes(A, 1)
                tmp = A[i, IA]
                if isfinite(tmp)
                    s += tmp
                    k += 1
                end
            end
            S[i1,IS], K[i1,IS] = s, k
        end
    else
        @inbounds for IA in CartesianIndices(indsAt)
            IS = newindex(IA, keep, Idefault)
            for i in axes(A, 1)
                tmp = A[i, IA]
                if isfinite(tmp)
                    S[i, IS] += tmp
                    K[i, IS] += 1
                end
            end
        end
    end
    S, K
end
_newindexer(ax) = Base.Broadcast.shapeindexer(ax)

function Statistics.var(A::AbstractArray{C}; kwargs...) where C<:AbstractGray
    imgc = channelview(A)
    base_colorant_type(C)(var(imgc; kwargs...))
end

function Statistics.var(A::AbstractArray{C,N}; kwargs...) where {C<:Colorant,N}
    imgc = channelview(A)
    colons = ntuple(d->Colon(), Val(N))
    inds1 = axes(imgc, 1)
    val1 = Statistics.var(view(imgc, first(inds1), colons...); kwargs...)
    vals = similar(imgc, typeof(val1), inds1)
    vals[1] = val1
    for i in first(inds1)+1:last(inds1)
        vals[i] = Statistics.var(view(imgc, i, colons...); kwargs...)
    end
    base_colorant_type(C)(vals...)
end

Statistics.std(A::AbstractArray{C}; kwargs...) where {C<:Colorant} = mapc(sqrt, Statistics.var(A; kwargs...))

# Entropy for grayscale (intensity) images
function _log(kind::Symbol)
    if kind == :shannon
        return log2
    elseif kind == :nat
        return log
    elseif kind == :hartley
        return log10
    else
        throw(ArgumentError("Invalid entropy unit. (:shannon, :nat or :hartley)"))
    end
end

"""
    entropy(logᵦ, img)
    entropy(img; [kind=:shannon])

Compute the entropy of a grayscale image defined as `-sum(p.*logᵦ(p))`.
The base β of the logarithm (a.k.a. entropy unit) is one of the following:

- `:shannon ` (log base 2, default), or use logᵦ = log2
- `:nat` (log base e), or use logᵦ = log
- `:hartley` (log base 10), or use logᵦ = log10
"""
entropy(img::AbstractArray; kind=:shannon) = entropy(_log(kind), img)
function entropy(logᵦ::Log, img) where Log<:Function
    hist = StatsBase.fit(Histogram, vec(img), nbins=256, closed=:right)
    counts = hist.weights
    p = counts / length(img)
    logp = logᵦ.(p)

    # take care of empty bins
    logp[Bool[isinf(v) for v in logp]] .= 0

    -sum(p .* logp)
end

function entropy(img::AbstractArray{Bool}; kind=:shannon)
    logᵦ = _log(kind)

    p = sum(img) / length(img)

    (0 < p < 1) ? - p*logᵦ(p) - (1-p)*logᵦ(1-p) : zero(p)
end

entropy(img::AbstractArray{C}; kind=:shannon) where {C<:AbstractGray} = entropy(channelview(img), kind=kind)

"""
`m = minfinite(A)` calculates the minimum value in `A`, ignoring any values that are not finite (Inf or NaN).
"""
function minfinite(A::AbstractArray{T}) where T
    ret = sentinel_min(T)
    for a in A
        ret = minfinite_scalar(a, ret)
    end
    ret
end
function minfinite(f, A::AbstractArray)
    ret = sentinel_min(typeof(f(first(A))))
    for a in A
        ret = minfinite_scalar(f(a), ret)
    end
    ret
end

"""
`m = maxfinite(A)` calculates the maximum value in `A`, ignoring any values that are not finite (Inf or NaN).
"""
function maxfinite(A::AbstractArray{T}) where T
    ret = sentinel_max(T)
    for a in A
        ret = maxfinite_scalar(a, ret)
    end
    ret
end
function maxfinite(f, A::AbstractArray)
    ret = sentinel_max(typeof(f(first(A))))
    for a in A
        ret = maxfinite_scalar(f(a), ret)
    end
    ret
end

"""
`m = maxabsfinite(A)` calculates the maximum absolute value in `A`, ignoring any values that are not finite (Inf or NaN).
"""
function maxabsfinite(A::AbstractArray{T}) where T
    ret = sentinel_min(typeof(abs(A[1])))
    for a in A
        ret = maxfinite_scalar(abs(a), ret)
    end
    ret
end

minfinite_scalar(a::T, b::T) where {T} = isfinite(a) ? (b < a ? b : a) : b
maxfinite_scalar(a::T, b::T) where {T} = isfinite(a) ? (b > a ? b : a) : b
minfinite_scalar(a::T, b::T) where {T<:Union{Integer,FixedPoint}} = b < a ? b : a
maxfinite_scalar(a::T, b::T) where {T<:Union{Integer,FixedPoint}} = b > a ? b : a
minfinite_scalar(a, b) = minfinite_scalar(promote(a, b)...)
maxfinite_scalar(a, b) = maxfinite_scalar(promote(a, b)...)

function minfinite_scalar(c1::C, c2::C) where C<:AbstractRGB
    C(minfinite_scalar(c1.r, c2.r),
      minfinite_scalar(c1.g, c2.g),
      minfinite_scalar(c1.b, c2.b))
end
function maxfinite_scalar(c1::C, c2::C) where C<:AbstractRGB
    C(maxfinite_scalar(c1.r, c2.r),
      maxfinite_scalar(c1.g, c2.g),
      maxfinite_scalar(c1.b, c2.b))
end

sentinel_min(::Type{T}) where {T<:Union{Integer,FixedPoint}} = typemax(T)
sentinel_max(::Type{T}) where {T<:Union{Integer,FixedPoint}} = typemin(T)
sentinel_min(::Type{T}) where {T<:AbstractFloat} = convert(T, NaN)
sentinel_max(::Type{T}) where {T<:AbstractFloat} = convert(T, NaN)
sentinel_min(::Type{C}) where {C<:AbstractRGB} = _sentinel_min(C, eltype(C))
_sentinel_min(::Type{C},::Type{T}) where {C<:AbstractRGB,T} = (s = sentinel_min(T); C(s,s,s))
sentinel_max(::Type{C}) where {C<:AbstractRGB} = _sentinel_max(C, eltype(C))
_sentinel_max(::Type{C},::Type{T}) where {C<:AbstractRGB,T} = (s = sentinel_max(T); C(s,s,s))
sentinel_min(::Type{C}) where {C<:AbstractGray} = _sentinel_min(C, eltype(C))
_sentinel_min(::Type{C},::Type{T}) where {C<:AbstractGray,T} = C(sentinel_min(T))
sentinel_max(::Type{C}) where {C<:AbstractGray} = _sentinel_max(C, eltype(C))
_sentinel_max(::Type{C},::Type{T}) where {C<:AbstractGray,T} = C(sentinel_max(T))

# FIXME: replace with IntegralImage
# average filter
"""
`kern = imaverage(filtersize)` constructs a boxcar-filter of the specified size.
"""
function imaverage(filter_size=[3,3])
    if length(filter_size) != 2
        error("wrong filter size")
    end
    m, n = filter_size
    if mod(m, 2) != 1 || mod(n, 2) != 1
        error("filter dimensions must be odd")
    end
    f = ones(Float64, m, n)/(m*n)
end

# FIXME: do something about this
# more general version
function imlaplacian(alpha::Number)
    lc = alpha/(1 + alpha)
    lb = (1 - alpha)/(1 + alpha)
    lm = -4/(1 + alpha)
    return [lc lb lc; lb lm lb; lc lb lc]
end

accum(::Type{T}) where {T<:Integer} = Int
accum(::Type{Float32})    = Float32
accum(::Type{T}) where {T<:Real} = Float64
accum(::Type{C}) where {C<:Colorant} = base_colorant_type(C){accum(eltype(C))}

graytype(::Type{T}) where {T<:Number} = T
graytype(::Type{C}) where {C<:AbstractGray} = C
graytype(::Type{C}) where {C<:Colorant} = Gray{eltype(C)}

# Simple image difference testing
macro test_approx_eq_sigma_eps(A, B, sigma, eps)
    quote
        if size($(esc(A))) != size($(esc(B)))
            error("Sizes ", size($(esc(A))), " and ",
                  size($(esc(B))), " do not match")
        end
        kern = KernelFactors.IIRGaussian($(esc(sigma)))
        Af = imfilter($(esc(A)), kern, NA())
        Bf = imfilter($(esc(B)), kern, NA())
        diffscale = max(maxabsfinite($(esc(A))), maxabsfinite($(esc(B))))
        d = sad(Af, Bf)
        if d > length(Af)*diffscale*($(esc(eps)))
            error("Arrays A and B differ")
        end
    end
end

# image difference testing (@tbreloff's, based on the macro)
#   A/B: images/arrays to compare
#   sigma: tuple of ints... how many pixels to blur
#   eps: error allowance
# returns: percentage difference on match, error otherwise
function test_approx_eq_sigma_eps(A::AbstractArray, B::AbstractArray,
                         sigma::AbstractVector{T} = ones(ndims(A)),
                         eps::AbstractFloat = 1e-2,
                         expand_arrays::Bool = true) where T<:Real
    if size(A) != size(B)
        if expand_arrays
            newsize = map(max, size(A), size(B))
            if size(A) != newsize
                A = copyto!(zeros(eltype(A), newsize...), A)
            end
            if size(B) != newsize
                B = copyto!(zeros(eltype(B), newsize...), B)
            end
        else
            error("Arrays differ: size(A): $(size(A)) size(B): $(size(B))")
        end
    end
    if length(sigma) != ndims(A)
        error("Invalid sigma in test_approx_eq_sigma_eps. Should be ndims(A)-length vector of the number of pixels to blur.  Got: $sigma")
    end
    kern = KernelFactors.IIRGaussian(sigma)
    Af = imfilter(A, kern, NA())
    Bf = imfilter(B, kern, NA())
    diffscale = max(maxabsfinite(A), maxabsfinite(B))
    d = sad(Af, Bf)
    diffpct = d / (length(Af) * diffscale)
    if diffpct > eps
        error("Arrays differ.  Difference: $diffpct  eps: $eps")
    end
    diffpct
end

"""
BlobLoG stores information about the location of peaks as discovered by `blob_LoG`.
It has fields:

- location: the location of a peak in the filtered image (a CartesianIndex)
- σ: the value of σ which lead to the largest `-LoG`-filtered amplitude at this location
- amplitude: the value of the `-LoG(σ)`-filtered image at the peak

Note that the radius is equal to σ√2.

See also: [`blob_LoG`](@ref).
"""
struct BlobLoG{T,S,N}
    location::CartesianIndex{N}
    σ::S
    amplitude::T
end

"""
    blob_LoG(img, σscales, [edges], [σshape]) -> Vector{BlobLoG}

Find "blobs" in an N-D image using the negative Lapacian of Gaussians
with the specifed vector or tuple of σ values. The algorithm searches for places
where the filtered image (for a particular σ) is at a peak compared to all
spatially- and σ-adjacent voxels, where σ is `σscales[i] * σshape` for some i.
By default, `σshape` is an ntuple of 1s.

The optional `edges` argument controls whether peaks on the edges are
included. `edges` can be `true` or `false`, or a N+1-tuple in which
the first entry controls whether edge-σ values are eligible to serve
as peaks, and the remaining N entries control each of the N dimensions
of `img`.

# Citation:

Lindeberg T (1998), "Feature Detection with Automatic Scale Selection",
International Journal of Computer Vision, 30(2), 79–116.

See also: [`BlobLoG`](@ref).
"""
function blob_LoG(img::AbstractArray{T,N}, σscales::Union{AbstractVector,Tuple},
                  edges::Tuple{Vararg{Bool}}=(true, ntuple(d->false, Val(N))...), σshape=ntuple(d->1, Val(N))) where {T,N}
    sigmas = sort(σscales)
    img_LoG = Array{Float64}(undef, length(sigmas), size(img)...)
    colons = ntuple(d->Colon(), Val(N))
    @inbounds for isigma in eachindex(sigmas)
        img_LoG[isigma,colons...] = (-sigmas[isigma]) * imfilter(img, Kernel.LoG(ntuple(i->sigmas[isigma]*σshape[i],Val(N))))
    end
    maxima = findlocalmaxima(img_LoG, 1:ndims(img_LoG), edges)
    [BlobLoG(CartesianIndex(tail(x.I)), sigmas[x[1]], img_LoG[x]) for x in maxima]
end
blob_LoG(img::AbstractArray{T,N}, σscales, edges::Bool, σshape=ntuple(d->1, Val(N))) where {T,N} =
    blob_LoG(img, σscales, (edges, ntuple(d->edges,Val(N))...), σshape)

blob_LoG(img::AbstractArray{T,N}, σscales, σshape=ntuple(d->1, Val(N))) where {T,N} =
    blob_LoG(img, σscales, (true, ntuple(d->false,Val(N))...), σshape)

@inline function _clippedinds(Router,rstp)
    CartesianIndices(map((f,l)->f:l,
                         (first(Router)+rstp).I,(last(Router)-rstp).I))
end

findlocalextrema(img::AbstractArray{T,N}, region, edges::Bool, order) where {T,N} = findlocalextrema(img, region, ntuple(d->edges,Val(N)), order)

function findlocalextrema(img::AbstractArray{T,N}, region::Union{Tuple{Int,Vararg{Int}},Vector{Int},UnitRange{Int},Int}, edges::NTuple{N,Bool}, order::Base.Order.Ordering) where {T<:Union{Gray,Number},N}
    issubset(region,1:ndims(img)) || throw(ArgumentError("invalid region"))
    extrema = Array{CartesianIndex{N}}(undef, 0)
    edgeoffset = CartesianIndex(map(!, edges))
    R0 = CartesianIndices(axes(img))
    R = _clippedinds(R0,edgeoffset)
    rstp = one(first(R0))
    Rinterior = _clippedinds(R0,rstp)
    iregion = CartesianIndex(ntuple(d->d∈region, Val(N)))
    Rregion = CartesianIndices(map((f,l)->f:l,(-iregion).I, iregion.I))
    z = zero(iregion)
    for i in R
        isextrema = true
        img_i = img[i]
        if i ∈ Rinterior
            # If i is in the interior, we don't have to worry about i+j being out-of-bounds
            for j in Rregion
                j == z && continue
                if !Base.Order.lt(order, img[i+j], img_i)
                    isextrema = false
                    break
                end
            end
        else
            for j in Rregion
                (j == z || i+j ∉ R0) && continue
                if !Base.Order.lt(order, img[i+j], img_i)
                    isextrema = false
                    break
                end
            end
        end
        isextrema && push!(extrema, i)
    end
    extrema
end

"""
`findlocalmaxima(img, [region, edges]) -> Vector{CartesianIndex}`

Returns the coordinates of elements whose value is larger than all of
their immediate neighbors.  `region` is a list of dimensions to
consider.  `edges` is a boolean specifying whether to include the
first and last elements of each dimension, or a tuple-of-Bool
specifying edge behavior for each dimension separately.
"""
findlocalmaxima(img::AbstractArray, region=coords_spatial(img), edges=true) =
        findlocalextrema(img, region, edges, Base.Order.Forward)

"""
Like `findlocalmaxima`, but returns the coordinates of the smallest elements.
"""
findlocalminima(img::AbstractArray, region=coords_spatial(img), edges=true) =
        findlocalextrema(img, region, edges, Base.Order.Reverse)

# restrict for AxisArray and ImageMeta
restrict(img::AxisArray, ::Tuple{}) = img
restrict(img::ImageMeta, ::Tuple{}) = img

function restrict(img::ImageMeta, region::Dims)
    shareproperties(img, restrict(arraydata(img), region))
end

function restrict(img::AxisArray{T,N}, region::Dims) where {T,N}
    inregion = falses(ndims(img))
    inregion[[region...]] .= true
    inregiont = (inregion...,)::NTuple{N,Bool}
    rdata = restrict(img.data, region)
    AxisArray(rdata, map(modax, AxisArrays.axes(img), axes(rdata), inregiont))
end

# FIXME: this doesn't get inferred, but it should be (see issue #628)
function restrict(img::Union{AxisArray,ImageMetaAxis}, ::Type{Ax}) where Ax
    dim = axisdim(img, Ax)
    A = restrict(img.data, dim)
    AxisArray(A, replace_axis(modax(img[Ax], axes(A)[dim]), AxisArrays.axes(img)))
end

replace_axis(newax, axs) = _replace_axis(newax, axnametype(newax), axs...)
@inline _replace_axis(newax, ::Type{Ax}, ax::Ax, axs...) where {Ax} = (newax, _replace_axis(newax, Ax, axs...)...)
@inline _replace_axis(newax, ::Type{Ax}, ax, axs...) where {Ax} = (ax, _replace_axis(newax, Ax, axs...)...)
_replace_axis(newax, ::Type{Ax}) where {Ax} = ()

axnametype(ax::Axis{name}) where {name} = Axis{name}

ofaxes(::Base.OneTo, r) = r
ofaxes(axs::Any, r) = OffsetArray(r, axs)

function modax(ax, Aax)
    v = ax.val
    if iseven(length(v))
        return ax(ofaxes(Aax, range(first(v)-step(v)/2, stop=last(v)+step(v)/2, length=length(v)÷2 + 1)))
    else
        return ax(ofaxes(Aax, range(first(v)/1, stop=last(v)/1, length=length(v)÷2 + 1)))
    end
end

modax(ax, Aax, inregion::Bool) = inregion ? modax(ax, Aax) : ax

function imlineardiffusion(img::Array{T,2}, dt::AbstractFloat, iterations::Integer) where T
    u = img
    f = imlaplacian()
    for i = dt:dt:dt*iterations
        u = u + dt*imfilter(u, f, "replicate")
    end
    u
end

function imgaussiannoise(img::AbstractArray{T}, variance::Number, mean::Number) where T
    return img + sqrt(variance)*randn(size(img)) + mean
end

imgaussiannoise(img::AbstractArray{T}, variance::Number) where {T} = imgaussiannoise(img, variance, 0)
imgaussiannoise(img::AbstractArray{T}) where {T} = imgaussiannoise(img, 0.01, 0)

# image gradients

# forward and backward differences
# can be very helpful for discretized continuous models
forwarddiffy(u::AbstractMatrix) = [u[2:end,:]; u[end:end,:]] - u
forwarddiffx(u::AbstractMatrix) = [u[:,2:end] u[:,end:end]] - u
backdiffy(u::AbstractMatrix) = u - [u[1:1,:]; u[1:end-1,:]]
backdiffx(u::AbstractMatrix) = u - [u[:,1:1] u[:,1:end-1]]
function div(p::AbstractArray{T,3}) where T
    # Definition from the Chambolle citation below, between Eqs. 5 and 6
    # This is the adjoint of -forwarddiff
    inds = axes(p)[1:2]
    out = similar(p, inds)
    Router = CartesianIndices(inds)
    rstp = one(first(Router))
    Rinner = _clippedinds(Router,rstp)
    # Since most of the points are in the interior, compute them more quickly by avoiding branches
    for I in Rinner
        out[I] = p[I,1] - p[I[1]-1, I[2], 1] +
                 p[I,2] - p[I[1], I[2]-1, 2]
    end
    # Handle the edge points
    for I in EdgeIterator(Router, Rinner)
        out[I] = 0
        if I[1] == first(inds[1])
            out[I] += p[I, 1]
        elseif I[1] == last(inds[1])
            out[I] -= p[I[1]-1, I[2], 1]
        else
            out[I] += p[I,1] - p[I[1]-1, I[2], 1]
        end
        if I[2] == first(inds[2])
            out[I] += p[I, 2]
        elseif I[2] == last(inds[2])
            out[I] -= p[I[1], I[2]-1, 2]
        else
            out[I] += p[I,2] - p[I[1], I[2]-1, 2]
        end
    end
    out
end

"""
```
imgr = imROF(img, λ, iterations)
```

Perform Rudin-Osher-Fatemi (ROF) filtering, more commonly known as Total
Variation (TV) denoising or TV regularization. `λ` is the regularization
coefficient for the derivative, and `iterations` is the number of relaxation
iterations taken. 2d only.

See https://en.wikipedia.org/wiki/Total_variation_denoising and
Chambolle, A. (2004). "An algorithm for total variation minimization and applications".
    Journal of Mathematical Imaging and Vision. 20: 89–97
"""
function imROF(img::AbstractMatrix{T}, λ::Number, iterations::Integer) where T<:NumberLike
    # Total Variation regularized image denoising using the primal dual algorithm
    # Also called Rudin Osher Fatemi (ROF) model
    # λ: regularization parameter
    s1, s2 = size(img)
    p = zeros(T, s1, s2, 2)
    # This iterates Eq. (9) of the Chambolle citation
    local u
    τ = 1/4   # see 2nd remark after proof of Theorem 3.1.
    for i = 1:iterations
        div_p = div(p)
        u = img - λ*div_p # multiply term inside ∇ by -λ. Thm. 3.1 relates this to u via Eq. 7.
        grad_u = cat(forwarddiffy(u), forwarddiffx(u), dims=3)
        grad_u_mag = sqrt.(sum(abs2, grad_u, dims=3))
        p .= (p .- (τ/λ).*grad_u)./(1 .+ (τ/λ).*grad_u_mag)
    end
    return u
end

# ROF Model for color images
function imROF(img::AbstractMatrix{<:Color}, λ::Number, iterations::Integer)
    out = similar(img)
    imgc = channelview(img)
    outc = channelview(out)
    for chan = 1:size(imgc, 1)
        outc[chan, :, :] = imROF(imgc[chan, :, :], λ, iterations)
    end
    out
end

# morphological operations for ImageMeta
dilate(img::ImageMeta, region=coords_spatial(img)) = shareproperties(img, dilate!(copy(arraydata(img)), region))
erode(img::ImageMeta, region=coords_spatial(img)) = shareproperties(img, erode!(copy(arraydata(img)), region))

# phantom images

"""
```
phantom = shepp_logan(N,[M]; highContrast=true)
```

output the NxM Shepp-Logan phantom, which is a standard test image usually used
for comparing image reconstruction algorithms in the field of computed
tomography (CT) and magnetic resonance imaging (MRI). If the argument M is
omitted, the phantom is of size NxN. When setting the keyword argument
`highConstrast` to false, the CT version of the phantom is created. Otherwise,
the high contrast MRI version is calculated.
"""
function shepp_logan(M,N; highContrast=true)
  # Initially proposed in Shepp, Larry; B. F. Logan (1974).
  # "The Fourier Reconstruction of a Head Section". IEEE Transactions on Nuclear Science. NS-21.

  P = zeros(M,N)

  x = range(-1, stop=1, length=M)'
  y = range(1, stop=-1, length=N)

  centerX = [0, 0, 0.22, -0.22, 0, 0, 0, -0.08, 0, 0.06]
  centerY = [0, -0.0184, 0, 0, 0.35, 0.1, -0.1, -0.605, -0.605, -0.605]
  majorAxis = [0.69, 0.6624, 0.11, 0.16, 0.21, 0.046, 0.046, 0.046, 0.023, 0.023]
  minorAxis = [0.92, 0.874, 0.31, 0.41, 0.25, 0.046, 0.046, 0.023, 0.023, 0.046]
  theta = [0, 0, -18.0, 18.0, 0, 0, 0, 0, 0, 0]

  # original (CT) version of the phantom
  grayLevel = [2, -0.98, -0.02, -0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]

  if(highContrast)
    # high contrast (MRI) version of the phantom
    grayLevel = [1, -0.8, -0.2, -0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
  end

  for l=1:length(theta)
    P += grayLevel[l] * (
           ( (cos(theta[l] / 360*2*pi) * (x .- centerX[l]) .+
              sin(theta[l] / 360*2*pi) * (y .- centerY[l])) / majorAxis[l] ).^2 .+
           ( (sin(theta[l] / 360*2*pi) * (x .- centerX[l]) .-
              cos(theta[l] / 360*2*pi) * (y .- centerY[l])) / minorAxis[l] ).^2 .< 1 )
  end

  return P
end

shepp_logan(N;highContrast=true) = shepp_logan(N,N;highContrast=highContrast)

"""
```
integral_img = integral_image(img)
```

Returns the integral image of an image. The integral image is calculated by assigning
to each pixel the sum of all pixels above it and to its left, i.e. the rectangle from
(1, 1) to the pixel. An integral image is a data structure which helps in efficient
calculation of sum of pixels in a rectangular subset of an image. See `boxdiff` for more
information.
"""
function integral_image(img::AbstractArray)
    integral_img = Array{accum(eltype(img))}(undef, size(img))
    sd = coords_spatial(img)
    cumsum!(integral_img, img, dims=sd[1])
    for i = 2:length(sd)
        cumsum!(integral_img, integral_img, dims=sd[i])
    end
    integral_img
end

"""
```
sum = boxdiff(integral_image, ytop:ybot, xtop:xbot)
sum = boxdiff(integral_image, CartesianIndex(tl_y, tl_x), CartesianIndex(br_y, br_x))
sum = boxdiff(integral_image, tl_y, tl_x, br_y, br_x)
```

An integral image is a data structure which helps in efficient calculation of sum of pixels in
a rectangular subset of an image. It stores at each pixel the sum of all pixels above it and to
its left. The sum of a window in an image can be directly calculated using four array
references of the integral image, irrespective of the size of the window, given the `yrange` and
`xrange` of the window. Given an integral image -

        A - - - - - - B -
        - * * * * * * * -
        - * * * * * * * -
        - * * * * * * * -
        - * * * * * * * -
        - * * * * * * * -
        C * * * * * * D -
        - - - - - - - - -

The sum of pixels in the area denoted by * is given by S = D + A - B - C.
"""
boxdiff(int_img::AbstractArray{T, 2}, y::UnitRange, x::UnitRange) where {T} = boxdiff(int_img, y.start, x.start, y.stop, x.stop)
boxdiff(int_img::AbstractArray{T, 2}, tl::CartesianIndex, br::CartesianIndex) where {T} = boxdiff(int_img, tl[1], tl[2], br[1], br[2])

function boxdiff(int_img::AbstractArray{T, 2}, tl_y::Integer, tl_x::Integer, br_y::Integer, br_x::Integer) where T
    sum = int_img[br_y, br_x]
    sum -= tl_x > 1 ? int_img[br_y, tl_x - 1] : zero(T)
    sum -= tl_y > 1 ? int_img[tl_y - 1, br_x] : zero(T)
    sum += tl_y > 1 && tl_x > 1 ? int_img[tl_y - 1, tl_x - 1] : zero(T)
    sum
end

"""
```
P = bilinear_interpolation(img, r, c)
```

Bilinear Interpolation is used to interpolate functions of two variables
on a rectilinear 2D grid.

The interpolation is done in one direction first and then the values obtained
are used to do the interpolation in the second direction.

"""
function bilinear_interpolation(img::AbstractArray{T, 2}, y::Number, x::Number) where T
    y_min = floor(Int, y)
    x_min = floor(Int, x)
    y_max = ceil(Int, y)
    x_max = ceil(Int, x)

    topleft = chkbounds(Bool, img, y_min, x_min) ? img[y_min, x_min] : zero(T)
    bottomleft = chkbounds(Bool, img, y_max, x_min) ? img[y_max, x_min] : zero(T)
    topright = chkbounds(Bool, img, y_min, x_max) ? img[y_min, x_max] : zero(T)
    bottomright = chkbounds(Bool, img, y_max, x_max) ? img[y_max, x_max] : zero(T)

    if x_max == x_min
        if y_max == y_min
            return T(topleft)
        end
        return T(((y_max - y) * topleft + (y - y_min) * bottomleft) / (y_max - y_min))
    elseif y_max == y_min
        return T(((x_max - x) * topleft + (x - x_min) * topright) / (x_max - x_min))
    end

    r1 = ((x_max - x) * topleft + (x - x_min) * topright) / (x_max - x_min)
    r2 = ((x_max - x) * bottomleft + (x - x_min) * bottomright) / (x_max - x_min)

    T(((y_max - y) * r1 + (y - y_min) * r2) / (y_max - y_min))

end

chkbounds(::Type{Bool}, img, x, y) = checkbounds(Bool, img, x, y)

"""
```
pyramid = gaussian_pyramid(img, n_scales, downsample, sigma)
```

Returns a  gaussian pyramid of scales `n_scales`, each downsampled
by a factor `downsample` > 1 and `sigma` for the gaussian kernel.

"""
function gaussian_pyramid(img::AbstractArray{T,N}, n_scales::Int, downsample::Real, sigma::Real) where {T,N}
    kerng = KernelFactors.IIRGaussian(sigma)
    kern = ntuple(d->kerng, Val(N))
    gaussian_pyramid(img, n_scales, downsample, kern)
end

function gaussian_pyramid(img::AbstractArray{T,N}, n_scales::Int, downsample::Real, kern::NTuple{N,Any}) where {T,N}
    downsample > 1 || @warn("downsample factor should be greater than one")
    # To guarantee inferability, we make sure that we do at least one
    # round of smoothing and resizing
    img_smoothed_main = imfilter(img, kern, NA())
    img_scaled = pyramid_scale(img_smoothed_main, downsample)
    prev = convert(typeof(img_scaled), img)
    pyramid = typeof(img_scaled)[prev]
    if n_scales ≥ 1
        # Take advantage of the work we've already done
        push!(pyramid, img_scaled)
        prev = img_scaled
    end
    for i in 2:n_scales
        img_smoothed = imfilter(prev, kern, NA())
        img_scaled = pyramid_scale(img_smoothed, downsample)
        push!(pyramid, img_scaled)
        prev = img_scaled
    end
    pyramid
end

function pyramid_scale(img, downsample)
    sz_next = map(s->ceil(Int, s/downsample), size(img))
    imresize(img, sz_next)
end

function pyramid_scale(img::OffsetArray, downsample)
    sz_next = map(s->ceil(Int, s/downsample), length.(axes(img)))
#    off = (.-ceil.(Int,(.-iterate.(axes(img).-(1,1))[1])./downsample))
    off = (.-ceil.(Int,(.-iterate.(map(x->UnitRange(x).-1,axes(img)))[1])./downsample))
    OffsetArray(imresize(img, sz_next), off)
end

"""
```
thres = otsu_threshold(img)
thres = otsu_threshold(img, bins)
```

Computes threshold for grayscale image using Otsu's method.

Parameters:
-    img         = Grayscale input image
-    bins        = Number of bins used to compute the histogram. Needed for floating-point images.

"""
function otsu_threshold(img::AbstractArray{T, N}, bins::Int = 256) where {T<:Union{Gray,Real}, N}

    min, max = extrema(img)
    edges, counts = imhist(img, range(gray(min), stop=gray(max), length=bins))
    histogram = counts./sum(counts)

    ω0 = 0
    μ0 = 0
    μt = 0
    μT = sum((1:(bins+1)).*histogram)
    max_σb=0.0
    thres=1

    for t in 1:bins
        ω0 += histogram[t]
        ω1 = 1 - ω0
        μt += t*histogram[t]

        σb = (μT*ω0-μt)^2/(ω0*ω1)

        if(σb > max_σb)
            max_σb = σb
            thres = t
        end
    end

    return T((edges[thres-1]+edges[thres])/2)
end

"""
```
thres = yen_threshold(img)
thres = yen_threshold(img, bins)
```

Computes threshold for grayscale image using Yen's maximum correlation criterion for bilevel thresholding

Parameters:
-    img         = Grayscale input image
-    bins        = Number of bins used to compute the histogram. Needed for floating-point images.

#Citation
Yen J.C., Chang F.J., and Chang S. (1995) “A New Criterion for Automatic Multilevel Thresholding” IEEE Trans. on Image Processing, 4(3): 370-378. DOI:10.1109/83.366472
"""
function yen_threshold(img::AbstractArray{T, N}, bins::Int = 256) where {T<:Union{Gray, Real}, N}

    min, max = extrema(img)
    if(min == max)
        return T(min)
    end

    edges, counts = imhist(img, range(gray(min), stop=gray(max), length=bins))

    prob_mass_function = counts./sum(counts)
    clamp!(prob_mass_function,eps(),Inf)
    prob_mass_function_sq = prob_mass_function.^2
    cum_pmf = cumsum(prob_mass_function)
    cum_pmf_sq_1 = cumsum(prob_mass_function_sq)
    cum_pmf_sq_2 = reverse!(cumsum(reverse!(prob_mass_function_sq)))

    #Equation (4) cited in the paper.
    criterion = log.(((cum_pmf[1:end-1].*(1.0 .- cum_pmf[1:end-1])).^2) ./ (cum_pmf_sq_1[1:end-1].*cum_pmf_sq_2[2:end]))

    thres = edges[findmax(criterion)[2]]
    return T(thres)

end


using RainFARM
using Test
using Statistics

# write your own tests here

print("Testing main RainFARM function and aggregation:\n")
print("-----------------------------------------------\n")
nt = 2; nf = 8; ns = 64; nas = 8
prf = rand(ns, ns ,nt)
prf[2,2,1] = NaN
prl = agg(prf, nas, nt)
ww = ones(64, 64)
print("Testing fglob=false, fsmooth=false\n")
pr = rainfarm(prl, 1.7, nf, 1.; fglob=false, fsmooth=false, verbose=false)
pra = agg(pr, nas, nt)
eps1 = Statistics.mean((pra-prl).^2)
@test eps1 < 1e-20
print("Testing fglob=true, fsmooth=false, weights\n")
pr = rainfarm(prl, 1.7, nf, ww; fglob=true, fsmooth=false, verbose=false)
pra = agg(pr, nas, nt)
eps1 = (Statistics.mean(pra[:,:,1])-Statistics.mean(prl[:,:,1])).^2
@test eps1 < 1e-20
print("Testing fglob=false, fsmooth=true\n")
pr = rainfarm(prl, 1.7, nf, 1.; fglob=false, fsmooth=true, verbose=false)
pra = agg(pr, nas, nt)
eps1 = (Statistics.mean(pra[:,:,1])-Statistics.mean(prl[:,:,1])).^2
@test eps1 < 0.005
print("\nTesting auxiliary functions:\n")
print("----------------------------\n")
print("Testing fitslope\n")
fx = (1.:10.).^-2
sx = fitslope(fx)
eps1 = abs(sx-1)
@test eps1 < 1e-8
print("Testing lon_lat_fine\n")
lon = 0:7; lat = 0:7; nf = 2
(lon_f, lat_f) = lon_lat_fine(lon, lat, nf)
lon_c = collect(-0.25:0.5:7.25)
eps1 = sum((lon_c-lon_f).^2)
@test eps1 < 1e-20


using SubstitutionTilings
using SubstitutionTilings.Penrose
import SubstitutionTilings.Penrose: ψ
using Test

using Luxor

L = Penrose.Qζ

width = 1000*4
height = 1000*3
sc = 80
@draw begin
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    first_tile = hkite(0, false, L(0))
    tiling = substitute(penrose(), [first_tile], 12, Penrose.in_bounds, (w=width/sc, h=height/sc))
    #tiling  = pentagon
    println(typeof(tiling))
    setline(0.1)

    for tile in tiling
        origin()
        draw(tile, sc, colors[Penrose.color(tile)], :fill)
        draw(tile, sc, "black", :stroke)
    end
    origin()
    setline(10)
    draw(first_tile, sc, "black", :stroke)
end width height
@draw begin
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    first_tile = hkite(0, false, L(0))
    tiling = substitute(penrose(), [first_tile], 12, Penrose.in_bounds, (w=width/sc, h=height/sc))
    #tiling  = pentagon
    println(typeof(tiling))
    setline(0.1)

    for tile in tiling
        origin()
        draw(tile, sc, colors[Penrose.color(tile)], :fill)
        draw(tile, sc, "black", :stroke)
    end
    setline(10)
    origin()
    draw(first_tile, sc, "black", :stroke)
end width height# "canonical1.png"
@png begin
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    first_tile = hkite(0, false, L(0))
    tiling = substitute(penrose(), [first_tile], 12, Penrose.in_bounds, (w=width/sc, h=height/sc))
    #tiling  = pentagon
    println(typeof(tiling))
    setline(0.1)

    for tile in tiling
        origin()
        draw(tile, sc, colors[Penrose.color(tile)], :fill)
        draw(tile, sc, "black", :stroke)
    end
    first_tile = hkite(0, false, L(0))
    tiling = substitute(penrose(), [first_tile], 11, Penrose.in_bounds, (w=width/sc, h=height/sc))
    #tiling  = pentagon
    println(typeof(tiling))
    setline(2)

    for tile in tiling
        origin()
        draw(tile, sc*1.618, "black", :stroke)
    end
    setline(10)
    draw(first_tile, sc, "black", :stroke)
end width height "canonical2.png"
@png begin
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    first_tile = hkite(0, false, L(0))
    tiling = substitute(penrose(), [first_tile], 12, Penrose.in_bounds, (w=width/sc, h=height/sc))
    #tiling  = pentagon
    println(typeof(tiling))
    setline(0.1)

    for tile in tiling
        origin()
        draw(tile, sc, colors[Penrose.color(tile)], :fill)
        draw(tile, sc, "black", :stroke)
    end
    first_tile = hkite(0, false, L(0))
    tiling = substitute(penrose(), [first_tile], 10, Penrose.in_bounds, (w=width/sc, h=height/sc))
    #tiling  = pentagon
    println(typeof(tiling))
    setline(2)

    for tile in tiling
        origin()
        draw(tile, sc*1.618^2, "black", :stroke)
    end
    setline(10)
    draw(first_tile, sc, "black", :stroke)
end width height "canonical3.png"

width = 800
height = 600
@png begin
    sc = 80
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    quadrants = Tiler(width, height, 2, 2, margin=5)

    for (pos, n) in quadrants
        if n % 2 == 0
            first_tile = hkite(0, false, L(0))
        else
            first_tile = hdart(0, false, L(0))
        end
        tiling = substitute(penrose(), [first_tile], div(n-1,2),)
        for tile in tiling
            origin()
            translate(pos)
            scale(sc)
            sethue(colors[Penrose.color(tile)])
            transform(embed_aff(tile[1]))
            draw(tile[2], :fill)
        end
    end
end width height
@png begin
    sc = 120
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    first_tile = hkite(0, false, L(0))
    quadrants = Tiler(width, height, 2, 3, margin=5)

    for (pos, n) in quadrants
        tiling = substitute(penrose(), [first_tile], n,)
        for tile in tiling
            origin()
            translate(pos)
            scale(sc/1.618^n)
            sethue(colors[Penrose.color(tile)])
            transform(embed_aff(tile[1]))
            draw(tile[2], :fill)
        end
    end
end width height
@png begin
    sc = 120
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    first_tile = hdart(0, false, L(0))
    quadrants = Tiler(width, height, 2, 3, margin=5)

    for (pos, n) in quadrants
        tiling = substitute(penrose(), [first_tile], n,)
        for tile in tiling
            origin()
            translate(pos)
            scale(sc/1.618^n)
            sethue(colors[Penrose.color(tile)])
            transform(embed_aff(tile[1]))
            draw(tile[2], :fill)
        end
    end
end width height
@png begin
    sc = 80
    setline(1)
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    quadrants = Tiler(width, height, 1, 2, margin=5)

    for (pos, n) in quadrants
        if n % 2 == 0
            first_tile = hkite(0, false, L(0))
        else
            first_tile = hdart(0, false, L(0))
        end
        tiling = Penrose.force([first_tile])
        for tile in tiling
            origin()
            translate(pos)
            scale(sc)
            sethue(colors[Penrose.color(tile)])
            transform(embed_aff(tile[1]))
            draw(tile[2], :fill)
        end
        origin()
        translate(pos)
        scale(sc)
        sethue("black")
        transform(embed_aff(first_tile[1]))
        draw(first_tile[2], :stroke)
    end
end width height
@png begin
    sc = 18
    setline(1)
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    quadrants = Tiler(width, height, 1, 2, margin=80)

    for (pos, n) in quadrants
        if n % 2 == 0
            first_tile = hkite(0, false, L(0))
        else
            first_tile = hdart(0, false, L(0))
        end
        tiling = unique(substitute(forced_penrose(), ([first_tile]), 4))
        for tile in tiling
            origin()
            translate(pos)
            scale(sc)
            sethue(colors[Penrose.color(tile)])
            transform(embed_aff(tile[1]))
            draw(tile[2], :fill)
        end
        origin()
        translate(pos)
        scale(sc*1.618^4)
        sethue("black")
        transform(embed_aff(first_tile[1]))
        draw(first_tile[2], :stroke)
    end
end width height

width = 1000
height = 1000
sc = 80
@draw begin
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    first_tile = hkite(0, false, L(0))
    tiling = substitute(penrose(), [first_tile], 12, Penrose.in_bounds, (w=width/sc, h=height/sc))
    #tiling  = pentagon
    println(typeof(tiling))
    setline(1)

    for tile in tiling
        origin()
        draw(tile, sc, colors[Penrose.color(tile)], :fill)
        draw(tile, sc, "black", :stroke)
    end
    origin()
    setline(5)
    draw(first_tile, sc, "black", :stroke)
end width height
@draw begin
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    first_tile = hkite(0, false, L(0))
    tiling = substitute(penrose(), [first_tile], 11, Penrose.in_bounds, (w=width/sc, h=height/sc))
    #tiling  = pentagon
    println(typeof(tiling))
    setline(1)

    for tile in substitute(penrose(), tiling, 1)
        origin()
        draw(tile, sc, colors[Penrose.color(tile)], :fill)
        draw(tile, sc, "black", :stroke)
    end
    origin()
    setline(5)
    draw(collect(tiling)[80], sc*1.618, "black", :stroke)
    origin()
    setline(1)
    draw(first_tile, sc, "black", :stroke)
end width height
@draw begin
    colors = ["#DD93FC", "#E7977A", "#9B70AF", "#A0644F",]
    first_tile = hkite(0, false, L(0))
    preimg = substitute(penrose(), [first_tile], 7)
    tiling = substitute(penrose(), preimg, 1)
    g = inv(collect(tiling)[49][1])
    tiling = g*tiling
    preimg = dilate(ϕ, g)*preimg
    preimg_tile = collect(preimg)[600]
    setline(1)

    for tile in tiling
        origin()
        draw(tile, sc, colors[Penrose.color(tile)], :fill)
        draw(tile, sc, "black", :stroke)
    end
    origin()
    setline(1)
    draw(first_tile, sc, "black", :stroke)
    origin()
    setline(5)
    draw(preimg_tile, sc*1.618, "black", :stroke)
end width height


using ExploringOceanData
@boilerplate

local_directory = "/mnt/podaac_drive/Version4/Release4/interp_monthly/SSH/1992/"
filename = "SSH_1992_01.nc"
ds = Dataset(local_directory * filename,"r")

η = ds["SSH"]

# 2D Plot

latitude = ds["latitude"]
longitude = ds["longitude"]
skip = 2
η_plot = η[begin:skip:end, begin:skip:end,1]
lat = latitude[begin:skip:end, 1]
lon = longitude[begin:skip:end, 1]

missing_value = 0.0
bools = η_plot .== missing_value

η_max = quantile(abs.(η_plot[:]), 0.99)
clims = quantile.(Ref(η_plot[:]), [0.1,0.9])
clims = (-η_max, η_max)

# for plotting make it a NaN
η_plot[bools] .= NaN
plt = heatmap(lat, lon, η_plot, colormap = :balance, colorrange = clims)
plt.axis.xlabel = "latitude"
plt.axis.ylabel = "longitude"
Colorbar(plt.figure[1, 2], plt.plot)
display(plt)

## 3D
x = [cosd(λ) * cosd(ϕ) for λ in lat, ϕ in lon]
y = [cosd(λ) * sind(ϕ) for λ in lat, ϕ in lon]
z = [sind(λ)  for λ in lat, ϕ in lon]

cutoff(x) = x > 0 ? 0 : x
tmp = cutoff.(η_plot') .+ 5e4
# surface(xs, ys, zs, axis=(type=Axis3,))
splt = surface(x, y, z, color=η_plot', colormap=:balance, colorrange=clims, show_axis=false, shading = false)

continents = Float32.(bools)
tmpbool = .!bools
continents[tmpbool] .= NaN
surface!(splt.figure[1,1], x  , y  , z , color = continents', colormap = :greys)
display(splt)


"""
Apply fourier operator v ← op * u
"""
function apply_operator!(op::Matrix{CT}, u::Union{Matrix{CT},SharedArray{CT,2}}, v::Union{Matrix{CT},SharedArray{CT,2}}) where {CT}
    v .= op .* u
end

"""
Apply fourier operator v[i] ← op[i] * u for i in 1:length(op)
"""
function apply_operator!(op::Vector{Matrix{CT}}, u::Union{Matrix{CT},SharedArray{CT,2}}, v::Union{Vector{Matrix{CT}},Vector{SharedArray{CT,2}}}) where {CT}
    for k in 1:length(op)
        v[k] .= op[k] .* u
    end
end

"""
Apply fourier operator v ← sum_i op[i] * u[i]
"""
function apply_operator!(op::Vector{Matrix{CT}}, u::Union{Vector{Matrix{CT}},Vector{SharedArray{CT,2}}}, v::Union{Matrix{CT},SharedArray{CT,2}}) where {CT}
    @assert length(op) == length(u)
    fill!(v, 0)
    for k in 1:length(op)
        v .+= op[k] .* u[k]
    end
end

"""
Returns gradient operator
"""
function get_gradient(ft::FourierTransform{ℳ,𝒩,RT,CT}) where {ℳ,𝒩,RT,CT}
    D₁ = zeros(CT, ℳ, 𝒩)
    D₂ = zeros(CT, ℳ, 𝒩)

    for s in 1:𝒩
        for r in 1:ℳ
            D₁[r,s] = 1im * ft.m[r]
            D₂[r,s] = 1im * ft.n[s]
        end
    end

    [D₁, D₂]
end

"""
Returns inverse Laplacian operator
"""
function get_inverse_laplacian(ft::FourierTransform{ℳ,𝒩,RT,CT}; sign=+1) where {ℳ,𝒩,RT,CT}
    Δ⁻¹ = zeros(CT, ℳ, 𝒩)

    for s in 1:𝒩
        for r in 1:ℳ
            if !(r == 1 && s == 1)
                Δ⁻¹[r,s] = - sign / (ft.m[r]^2 + ft.n[s]^2)
            end
        end
    end

    Δ⁻¹
end


using Manifolds, LinearAlgebra, PGFPlotsX, Colors, Distributions, Contour, Random

#
# Settings
#
dark_mode = false

line_offset_brightness = 0.25
patch_opacity = 1.0
geo_opacity = dark_mode ? 0.66 : 0.5
geo_line_width = 35
mesh_line_width = 5
mesh_opacity = dark_mode ? 0.5 : 0.7
logo_colors = [(77, 100, 174), (57, 151, 79), (202, 60, 50), (146, 89, 163)] # Julia colors

rgb_logo_colors = map(x -> RGB(x ./ 255...), logo_colors)
rgb_logo_colors_bright = map(x -> RGB( (1 + line_offset_brightness) .* x ./ 255...), logo_colors)
rgb_logo_colors_dark = map(x -> RGB( (1 - line_offset_brightness) .* x ./ 255...), logo_colors)

out_file_prefix = dark_mode ? "logo-dark" : "logo"
out_file_ext = ".svg"

#
# Helping functions
#
polar_to_cart(r, θ) = (r * cos(θ), r * sin(θ))

cart_to_polar(x, y) = (hypot(x, y), atan(y, x))

normal_coord_to_vector(M, x, rθ, B) = get_vector(M, x, collect(polar_to_cart(rθ...)), B)

normal_coord_to_point(M, x, rθ, B) = exp(M, x, normal_coord_to_vector(M, x, rθ, B))

function plot_normal_coord!(
    ax,
    M,
    x,
    B,
    rs,
    θs;
    ncirc = 9,
    options = Dict(),
    kwargs...,
)
    for r in rs[2:end-1]
        push!(ax, Plot3(
            options,
            Coordinates(map(θ -> Tuple(normal_coord_to_point(M, x, [r, θ], B)), θs))
        ))
    end
    for θ in range(0, 2π; length = ncirc)
        push!(ax, Plot3(
            options,
            Coordinates(map(r -> Tuple(normal_coord_to_point(M, x, [r, θ], B)), rs))
        ))
    end
    return ax
end

function plot_patch!(ax, M, x, B, r, θs; options = Dict())
    push!(ax, Plot3(
        options,
        Coordinates(map(θ -> Tuple(normal_coord_to_point(M, x, [r, θ], B)), θs))
    ))
    return ax
end

function plot_geodesic!(ax, M, x, y; n = 100, options = Dict())
    γ = shortest_geodesic(M, x, y)
    T = range(0, 1; length = n)
    push!(ax, Plot3(options, Coordinates(Tuple.(γ(T)))))
    return ax
end

#
# Prepare document
#
resize!(PGFPlotsX.CUSTOM_PREAMBLE, 0)
push!(PGFPlotsX.CUSTOM_PREAMBLE, raw"\pgfplotsset{scale=6.0}")
push!(PGFPlotsX.CUSTOM_PREAMBLE, raw"\usetikzlibrary{arrows.meta}")
push!(PGFPlotsX.CUSTOM_PREAMBLE, raw"\pgfplotsset{roundcaps/.style={line cap=round}}")
push!(
    PGFPlotsX.CUSTOM_PREAMBLE,
    raw"\pgfplotsset{meshlinestyle/.style={dash pattern=on 0pt off 2.5\pgflinewidth, line cap=round}}",
)
if dark_mode
    push!(PGFPlotsX.CUSTOM_PREAMBLE, raw"\pagecolor{black}")
end
S = Sphere(2)

center = normalize([1, 1, 1])
x, y, z = eachrow(Matrix{Float64}(I, 3, 3))
γ1 = shortest_geodesic(S, center, z)
γ2 = shortest_geodesic(S, center, x)
γ3 = shortest_geodesic(S, center, y)
p1 = γ1(1)
p2 = γ2(1)
p3 = γ3(1)

#
# Setup Axes
if dark_mode
    tp = @pgf Axis({
        axis_lines = "none",
        axis_equal,
        view = "{135}{35}",
        zmin = -0.05, zmax = 1.0, xmin = 0.0, xmax = 1.0, ymin = 0.0, ymax = 1.0
    })
else
    tp = @pgf Axis({
        axis_lines = "none",
        axis_equal,
        view = "{135}{35}",
        zmin = -0.05, zmax = 1.0, xmin = 0.0, xmax = 1.0, ymin = 0.0, ymax = 1.0
    })
end
rs = range(0, π / 5; length = 6)
θs = range(0, 2π; length = 100)

#
# Plot manifold patches
patch_colors = rgb_logo_colors[2:end]
patch_colors_line = dark_mode ? rgb_logo_colors_bright[2:end] : rgb_logo_colors_dark[2:end]
base_points = [p1, p2, p3]
basis_vectors = [log(S, p1, p2), log(S, p2, p1), log(S, p3, p1)]
for i in eachindex(base_points)
    x = base_points[i]
    B = DiagonalizingOrthonormalBasis(basis_vectors[i])
    basis = get_basis(S, x, B)
    optionsP = @pgf {fill = patch_colors[i], draw = "none", opacity = patch_opacity}
    plot_patch!(tp, S, x, basis, π / 5, θs; options = optionsP)
    optionsL = @pgf {
        meshlinestyle,
        color = dark_mode ? "white" : "black",
        line_width = mesh_line_width,
        opacity = mesh_opacity,
    }
    plot_normal_coord!(tp, S, x, basis, rs, θs; options = optionsL)
end

#
# Plot geodesics
options = @pgf {
    opacity=geo_opacity,
    no_markers,
    roundcaps,
    line_width = geo_line_width,
    color = dark_mode ? "white" : "black"}
plot_geodesic!(tp, S, base_points[1], base_points[2]; options = options)
plot_geodesic!(tp, S, base_points[1], base_points[3]; options = options)
plot_geodesic!(tp, S, base_points[2], base_points[3]; options = options)

#
# Export Logo.
out_file = "$(out_file_prefix)$(out_file_ext)"
pgfsave(out_file, tp)


using LinearAlgebra
using CUDA

"""
	sim_activity_gpu(J::Matrix{Float32}, g::Float32, h0::Vector{Float32};
		dt=1f-1, t_bound=1f3, s=0f0, savestep=1, minimum_norm=0)

Integrates a tanh rate network with coupling matrix J and gain param g from
initial condition h0 and returns the matrix of single unit activities.
"""
function sim_activity_gpu(J::Matrix{Float32}, g::Float32, h0::Vector{Float32};
		dt=1f-1, t_bound=1f3, s=0f0, savestep=1, minimum_norm=0)

	N = size(J)[1]
	# Check dimensional consistency
	@assert(N==size(J)[2]==length(h0))

	maxstep = round(Int64, t_bound / dt)

	# Allocate memory for output
	out = CuArray{Float32,2}(undef, N, maxstep ÷ savestep)

	h = CuArray{Float32,1}(h0) # Set initial condition
	r = CuArray{Float32,1}(undef, N)

	J = CuArray{Float32,2}(g .* J + s * I)

	step = 0
	while step < maxstep
		step += 1
		map!(tanh, r, h)
		mul!(h, J, r, dt, 1 - dt)
		if step % savestep == 0 # Save every specified time interval
			savept = step ÷ savestep
			out[:,savept] .= h
		end
		if norm(h) < minimum_norm
			D = Matrix(-I + J .* ((sech.(h)).^2)')
			if all(real(eigvals(D)) .< 0)
				break
			end
		end
	end

	return Matrix{Float32}(out)
end

"""
	sim_lyapunov_gpu(J::Matrix{Float32}, g::Float32, h0::Vector{Float32},
		dh::Vector{Float32}, Q0::Matrix{Float32}; dt=1f-1, t_bound=1f3,
		t_ONS=1f0, wait=0f0, return_max_LE=false, s=0f0, printstuff=false)

Integrates a tanh network with a given weight matrix and gain parameter from a
specified initial condition, and returns a Lyapunov spectrum calculated from
Jacobians along the solution trajectory.

Can also return the max Lyapunov exponent calculated from a direct numerical
simulation of a perturbed trajectory.
"""
function sim_lyapunov_gpu(J::Matrix{Float32}, g::Float32, h0::Vector{Float32},
		dh::Vector{Float32}, Q0::Matrix{Float32}; dt=1f-1, t_bound=1f3,
		t_ONS=1f0, wait=0f0, return_max_LE=false, s=0f0, printstuff=false)

	# Check dimensional consistency
	N = size(J, 1)
	@assert size(J) == (length(h0), length(dh)) == size(Q0) == (N,N)

	# Discretization of time
	maxstep = round(Int64, t_bound / dt)
	startstep = round(Int64, wait / dt)
	tONSstep = round(Int64, t_ONS / dt)
	@assert(maxstep % tONSstep == 0 && startstep % tONSstep == 0,
			"The reorthonormalization interval must divide both the total " *
			"simulation time and wait period"
	)

	# Allocate memory for output
	spectrum = CUDA.zeros(N)

	# Allocate vector for activity and firing rate of current state
	h = CuArray{Float32,1}(h0)
	r = CuArray{Float32,1}(undef, N)

	# Load network structure and ICs to GPU
	J = CuArray{Float32,2}(g .* J + s * I)
	Q = CuArray{Float32,2}(Q0)

	# Allocate mem for calculating Jacobian and evolving the orthonormal basis
	sech2(x::Float32)::Float32 = 1f0 / (CUDA.cosh(x))^2
	sech2h = CuArray{Float32,2}(undef, 1, N)
	D = CuArray{Float32,2}(undef, N, N)
	dQ = CuArray{Float32,2}(undef, N, N)

	# Things for performing a direct numerical sim of a perturbation
	if return_max_LE
		max_LE = 0.0f0
		p_factor = 1f-6  # Order of the perturbation handled by the sim
		dh = CuArray{Float32,1}(dh ./ norm(dh))
		h2 = h .+ p_factor .* dh
		r2 = CuArray{Float32,1}(undef, N)
	end

	step = 0
	while step < maxstep
		step += 1

		#Evolve the trajectory and the orthonormal basis in the tangent space
		map!(tanh, r, h)
		mul!(h, J, r, dt, 1 - dt)

		map!(sech2, sech2h, h)
		copyto!(D, J)
		D .*= sech2h
		D[diagind(D)] .-= 1.0

		mul!(dQ, D, Q, dt, 0f0)
        Q .+= dQ

		#Evolve the perturbed trajectory and calculate how much it has grown
		if return_max_LE
			map!(tanh, r2, h2)
			mul!(h2, J, r2, dt, 1 - dt)
			copyto!(dh, h2)
			dh .-= h
			dh ./= p_factor
			scale = norm(dh)

			# If the perturbation scale has grown or shrunk too much, then
			# reset it to 1 and track the cumulative sum of log scales
			if scale > 100 || scale < 0.01 || step == startstep
				copyto!(h2, h)
				dh ./= (scale / p_factor)
				h2 .+= dh
				if step > startstep
					max_LE += log(scale)
				end
			end
		end

		# At each re-orthonormalization step, the log diagonal entries of R are
		# saved in a cumulative sum
		if step % tONSstep == 0
			printstuff && println("Time step: ", step)
			copyto!(dQ, Q)
			F = qr!(dQ)
			fill!(Q, 0)
			diags = view(F.factors, diagind(F.factors))
			map!(sign, view(Q, diagind(Q)), diags)
			if step > startstep  # forcing positive R-diagonal elements
				spectrum .+= log.(abs.(diags))
			end
            lmul!(F.Q, Q)
		end
	end

	# Update the maximum Lyapunov exponent after the simulation has ended so
	# that the full simulation time is used for the calculation
	actual_time = dt * (maxstep - startstep)
	if return_max_LE
		max_LE += log(norm((h2 .- h) ./ p_factor))
		spectrum ./= actual_time
		max_LE /= actual_time
		return (Vector{Float32}(spectrum), max_LE)
	else
		spectrum ./= actual_time
		return Vector{Float32}(spectrum)
	end
end


using Distributions, ZVSimulator

d = TDist(5.)

results = psim_rand_vrf(d, nsets=5)

# writedlm_vrf(results, "./", ' ')
# writedlm_reformat("_means.txt", "./_means_pprint.txt", fill("%.3e", 3))


### Compare LombScargle.jl with Astropy

using LombScargle, Base.Test, PyCall
PyCall.@pyimport astropy.stats as ast

ntimes = 401
t = linspace(0.01, 10pi, ntimes)
t += step(t)*rand(ntimes)
for f in (x -> sinpi(x), x -> sin(x) + 1.5*cospi(4*x) + 3)
    s = f(t)
    # "psd" normalization in LombScargle slightly differ from that of
    # Astropy and the test would fail if we includ it.
    for fitmean in (true, false), nrm in ("standard", "model", "log")
        f_jl, p_jl = freqpower(lombscargle(t, s, fit_mean = fitmean,
                                           normalization=nrm,
                                           maximum_frequency=20))
        f_py, p_py =
            ast.LombScargle(t, s,
                            fit_mean = fitmean)[:autopower](method="cython",
                                                            normalization=nrm,
                                                            maximum_frequency=20)
        @test_approx_eq f_jl f_py
        @test_approx_eq p_jl p_py
    end
end

# Test the fast method with evenly spaced data.
t = linspace(0.01, 10pi, ntimes)
for f in (x -> sinpi(x), x -> sin(x) + 1.5*cospi(4*x) + 3)
    s = f(t)
    # "psd" normalization in LombScargle slightly differ from that of
    # Astropy and the test would fail if we includ it.
    for fitmean in (true, false), nrm in ("standard", "model", "log")
        f_jl, p_jl = freqpower(lombscargle(t, s, fit_mean = fitmean,
                                           normalization=nrm,
                                           maximum_frequency=10,
                                           samples_per_peak=10))
        f_py, p_py =
            ast.LombScargle(t, s,
                            fit_mean = fitmean)[:autopower](method="fast",
                                                            normalization=nrm,
                                                            maximum_frequency=10,
                                                            samples_per_peak=10)
        @test_approx_eq f_jl f_py
        @test_approx_eq p_jl p_py
    end
end

# Test the model functions
for f in (x -> sinpi(x), x -> sin(x) + 1.5*cospi(4*x) + 3)
    s = f(t)
    for fm in (true, false), cd in (true, false)
        m_jl = LombScargle.model(t, s, 1/2pi, fit_mean=fm, center_data=cd)
        m_py = ast.LombScargle(t, s, center_data=cd, fit_mean=fm)[:model](t, 1/2pi)
        @test_approx_eq m_jl m_py
    end
end

info("LombScargle.jl is consistent with Astropy")


using IntelGEMM
using LinearAlgebra
using Test

for T in (Float32, Float64)
    A = T[2 1; 4 3]
    Bv = T[1; 2]
    B = reshape(Bv, :, 1)
    Cv = T[4; 10]
    C = reshape(Cv, :, 1)
    @test A * Bv  == Cv
    @test A * B == C
    eigen(Symmetric(A))
    eigen(A)
end


function luc_seeder{T<:Integer}(n::T)
    a = -ones(BigInt, n)
    a[end] = big(n)
    return a
end

function luc{T<:Integer}(n::T, k::T)
    NFib(n, k, luc_seeder)
end


using Test
using MechGluecode
using MechanicalUnits
using MechanicalUnits: Unitfu.promote_to_derived
@import_expand(m, s, N) # Takes long if defined late)
using DifferentialEquations

@testset "Unitful time with unitful function" begin
    u0 = 30.0N
    tspan = (0.0, 10.0)s
    prob1 = ODEProblem((du,u,t,p) -> (du[1] = -0.2∙s⁻¹ * u[1]), [u0], tspan)
    prob2 = ODEProblem((u,t,p)    ->         (-0.2∙s⁻¹ * u[1]), u0, tspan)
    prob3 = ODEProblem((u,t,p)    ->         [-0.2∙s⁻¹* u[1]], [u0],tspan)
    so1 =  solve(prob1, Euler(), dt = 1s)
    so2 =  solve(prob2, Midpoint())
    so3 =  solve(prob3, Tsit5())
    plot(so1, denseplot = false, marker = true)
    plot(so2, denseplot = false, marker = true)
    plot(so3, denseplot = false, marker = true)
    sf1(t) = so1(t)[1]
    sf2(t) = so2(t)[1]
    sf3(t) = so3(t)[1]
    plot([sf1, sf2, sf3], xlims =(0,10)s, ylims =(0,40)N)
    plot([t-> sf1(t) / sf3(t), t-> sf2(t) / sf3(t)], xlims =(0,10)s, ylims = (0.5,1.1), minorgrid=true, grid = (:y, :olivedrab, :dot, 1, 0.9))

end
@testset "Unitful time with unitless function" begin
    u0 = 30.0
    tspan = (0.0, 10.0)s
    prob1 = ODEProblem((du,u,t,p) -> (du[1] = -0.2∙s⁻¹ * u[1]), [u0], tspan)
    prob2 = ODEProblem((u,t,p)    ->         (-0.2∙s⁻¹ * u[1]), u0, tspan)
    prob3 = ODEProblem((u,t,p)    ->         [-0.2∙s⁻¹* u[1]], [u0],tspan)
    so1 =  solve(prob1, Euler(), dt = 1s)
    so2 =  solve(prob2, Midpoint())
    so3 =  solve(prob3, Tsit5())
    # don't work plot(so1, denseplot = false, marker = true)
    # don't work plot(so2, denseplot = false, marker = true)
    # don't work plot(so3, denseplot = false, marker = true)
    sf1(t) = so1(t)[1]
    sf2(t) = so2(t)[1]
    sf3(t) = so3(t)[1]
    plot([sf1, sf2, sf3], xlims =(0,10)s, ylims =(0,40))
    plot([t-> sf1(t) / sf3(t), t-> sf2(t) / sf3(t)], xlims =(0,10)s, ylims = (0.5,1.1), minorgrid=true, grid = (:y, :olivedrab, :dot, 1, 0.9))
end

@testset "Plot ODE quantity" begin
    # Don't give us kN from from solution in this case, just defaults.
    promote_to_derived()
    f1 = (y, p, t) -> -1.5y / 0.3s
    u0 = 1.5N
    tspan = (0.0s, 1.0s)
    prob = ODEProblem(f1, u0, tspan)
    sol = solve(prob)
    plot(sol)
end

@testset "Plot ODE system same quantities" begin
    # This is not a good example, as the units are incorrect and have no
    # known physical interpretation
    σ = 10/s
    ρ = 28m
    β = (8 / 3)m
    function lorenz!(du, u, p, t)
        du[1] = (u[2] - u[1])∙σ
        du[2] = (u[1] * (ρ - u[3])) / (m∙s) - u[2]/s
        du[3] =  u[1] * u[2] / (m∙s)    - β ∙ u[3]/(m∙s)
    end
    u0 = [1.0, 0.0, 0.0]m
    tspan = (0.0,100.0)s
    prob = ODEProblem(lorenz!,u0,tspan)
    sol = solve(prob)
    # Plot all variables vs time
    plot(sol)
    # Plot in phase space
    plot(sol, vars=(1,2,3))
end
#=
@testset "Plot ODE system single pendulum" begin
    # Second order non-linear, undamped pendulum:
    # mLθ'' + mg∙sin(θ)
    # where m is mass, unit kg
    #       L is length, unit m
    #       θ is angle to vertical, strictly no unit
    #       g is gravity's acceleration
    # Acceleration
    # θ'' + k * sin(θ) = 0
    #    where k = g / L , unit(k) = s⁻²
    # Reduced to first order non-linear system by defining:
    # θ = y[1]
    # θ' = y[2]
    # It follows that
    # θ'' = y'[2]
    # and
    # y'[1] = y[2]
    # y'[2] = -k * sin(y[1])
    # To solve numerically, we define the derivative.
    # For efficiency, it's evaluated in place, i.e. dy changes at every call.
    function f!(dy, y, k, t)
        dy[1] = y[2]
        dy[2] = -k ∙ sin(y[1])
    end
    # Fixed parameter
    k = upreferred(g / 1.0m)
    # Initial conditions
    θ0 = 0.1
    θ0_vel = 0.0s⁻¹
    θ0_acc = -k *sin(θ0)

    y0 = [θ0, θ0_vel]
    y0 = ArrayPartition([θ0_vel, θ0_acc])
    tspan = (0.0, 100.0)s
    prob = ODEProblem(f!, y0, tspan, k)
    sol = solve(prob)
    # Plot all variables vs time
    plot(sol)
    # Plot in phase space
    plot(solve(prob), vars=(1,2))
end
=#


#!/usr/bin/env julia

module EnsembleKalmanInversionTests

    #Start Test Script
    using EnsembleKalmanInversion
    using Test
    using Random
    using Statistics

    # Run tests

    @testset "EnsembleKalmanInversion Tests" begin

        @testset "EnsembleKalmanInversion Tests" begin
            @testset "Linear Test" begin
            #This should certainly be a set of "convergence" tests
                G = randn(50,100)
                x = randn(100)
                σ = 0.1
                yt = G*x
                y = yt .+ σ * randn(50)
                η = sqrt((y-yt)'*(y-yt)/σ^2)
                prior() = randn(100)
                gmap(q) = G*q
                J = 100
                N = 100
                xe = eki(y, σ, η, J, N, prior, gmap; verbosity=1, ρ=0.5, ζ=2.0)
                @test sqrt((yt-G*xe)'*((yt-G*xe))/σ^2) <= 2.0*η
            end

            @testset "Linear Batched Test" begin
            #This should certainly be a set of "convergence" tests
                G1 = randn(10,100)
                G2 = randn(15,100)
                G3 = randn(13,100)
                G4 = randn(11,100)

                x = randn(100)
                σ = 0.1
                yt = [G1*x,G2*x,G3*x,G4*x]
                y = [G1*x.+σ*randn(10),G2*x.+σ*randn(15),G3*x.+σ*randn(13),G4*x.+σ*randn(11)]
                η = sqrt((y-yt)'*(y-yt)/σ^2)
                prior() = randn(100)
                function gmap(q,b)
                    if b <=1
                        return G1*q
                    elseif b <= 2
                        return G2*q
                    elseif b <= 3
                        return G3*q
                    else
                        return G4*q
                    end
                end
                J = 100
                N = 100
                xe = eki(y, σ, η, J, N, prior, gmap; verbosity=1, ρ=0.5, ζ=2.0, batched=true, batches=3)
                ye = [G1*xe,G2*xe,G3*xe,G4*xe]
                @test sqrt((vcat(yt...)-vcat(ye...))'*((vcat(y...)-vcat(ye...)))/σ^2) <= 2.0*η
            end
        end
    end
end


module PharmValuation
# import mean and disallow method extension
using Statistics: mean
using Printf: @printf
using Plots

struct CashProcess{T}
    annual_cash_flow::T
    drift::T
    volatility::T
    terminal_multiplier::T
    risk_premium::T
end

struct CostProcess{T}
    investment::T
    total_cost::T
    volatility::T
    failure_rate::T
end

struct SimOptions{T,U}
    time_step::T
    term::U
    runs::U
    basis::Function
end

struct ProjectProcess{T}
    CashProcess
    CostProcess
    correlation::T
    risk_free_rate::T
    SimOptions
end

function main(;runs=100_000)
    basis(x,y) = [1, x, y, x*y, x^2, y^2, x^2 * y, x * y^2, (x*y)^2]
    cash = CashProcess(20e6,0.02,35/100,5.0,0.036)
    cost = CostProcess(10e6,100e6,0.5,0.06931)
    sim = SimOptions(0.25,20,runs,basis)
    project = ProjectProcess(cash,cost,-0.1,0.05,sim)
    project_value = lsm(project)
    @printf "The project value: \$%.0f" project_value
end

""" simulate returns the cash flow and cost matrices generated by their respective processes """
function simulate(pp::ProjectProcess)
    # Set the number of periods
    periods = Int(ceil(pp.SimOptions.term / pp.SimOptions.time_step))

    # Risk adjusted cash flow drift rate
    adj_cash_flow = pp.CashProcess.drift - pp.CashProcess.risk_premium

    # Set return matrices and correlated random errors.
    net_cash = randn(pp.SimOptions.runs, periods)
    cost = pp.correlation * net_cash .+ sqrt(1 - pp.correlation^2) * randn(pp.SimOptions.runs,periods)

    # simulate the investment costs and correlated cash flows
    @inbounds for run in 1:pp.SimOptions.runs
        @inbounds for period in 1:periods
            # cash flow simulation
            prev_cash = period != 1 ? net_cash[run,period-1] : pp.CashProcess.annual_cash_flow
            net_cash[run,period] = prev_cash * exp((adj_cash_flow-0.5 * pp.CashProcess.volatility^2)*pp.SimOptions.time_step +
                pp.CashProcess.volatility * sqrt(pp.SimOptions.time_step) * net_cash[run,period])

            # cost simulation
            prev_cost = period != 1 ? cost[run,period-1] : pp.CostProcess.total_cost
            current_cost = 0.0
            if prev_cost != 0
                current_cost = prev_cost - pp.CostProcess.investment * pp.SimOptions.time_step +
                    pp.CostProcess.volatility * sqrt(pp.CostProcess.investment*prev_cost*pp.SimOptions.time_step) * cost[run,period]
                current_cost = current_cost < 0 ? 0 : current_cost
            end
            cost[run,period] = current_cost
        end
    end
    return net_cash, cost
end

""" lsm returns the least squares monte carlo estimate for the optimal stopping problem"""
function lsm(pp::ProjectProcess)
    periods = Int(ceil(pp.SimOptions.term / pp.SimOptions.time_step))

    # local for clarity
    period_investmt = pp.CostProcess.investment * pp.SimOptions.time_step

    # Simulate the net cash flows and cost values
    cash_matrix, cost_matrix = simulate(pp)

    # Set value function array to terminal value where investment has stopped.
    value_func = zeros(size(cash_matrix))
    terminal_cash_flow = cash_matrix[:,end]
    value_func[:,end] = pp.CashProcess.terminal_multiplier * (cost_matrix[:,end] .== 0) .* terminal_cash_flow

    # Discount rates for investing phase and sales phase
    cash_discount_rate = exp(-1 * pp.risk_free_rate * pp.SimOptions.time_step)
    invest_discount_rate = exp(-1 * (pp.risk_free_rate + pp.CostProcess.failure_rate)*pp.SimOptions.time_step)

    # value function iteration
    @inbounds for period in (periods-1):-1:1
        # initialize next periods value
        next_val = invest_discount_rate * value_func[:,period+1]

        # Apply basis function to cost and cash flows to approximate the discounted
        # next period value.
        basis_vecs = pp.SimOptions.basis.(cash_matrix[:,period],cost_matrix[:,period])

        # Transform vector of vector bases into a basis matrix for regression analysis.
        basis_matrix = reduce(hcat,basis_vecs)'

        # Solve regression coefficients
        coefficients = basis_matrix \ next_val

        # Estimate continuation value of investment
        est_val = basis_matrix * coefficients

        # Update value function array
        @inbounds for run in 1:pp.SimOptions.runs
            # Investing phase
            if cost_matrix[run,period] != 0
                # Investment decision criterion
                invest_val = est_val[run] - period_investmt

                # Update value function with for the investment phase.
                value_func[run,period] = invest_val > 0 ? next_val[run] - period_investmt : 0.0
            else
                # Update value function for the post-investment sales phase.
                value_func[run, period] = cash_matrix[run,period] * pp.SimOptions.time_step +
                    cash_discount_rate * value_func[run,period+1]
            end
        end
    end
    retval = invest_discount_rate * value_func[:,1]
    return mean(retval)
end

end # module


addqueen(queens::Array{Vector{Int}}, queen::Vector{Int}) = push!(copy(queens), queen)

hitsany(queen::Vector{Int}, queens::Array{Vector{Int}}) = any(map((x) -> hits(queen, x), queens))
hits(a::Array{Int}, b::Array{Int}) = any(a .== b) || abs(a-b)[1] == abs(a-b)[2]

function solve(x::Int, y::Int, n::Int, d::Array{Vector{Int}})
  if n == 0
    return d
  end
  for px = 1:x
    for py = 1:y
      if !hitsany([px, py], d)
        s = solve(x, y, n-1, addqueen(d, [px, py]))
        if !isequal(s, None)
          return s
        end
      end
    end
  end
  return None
end

solve(x, y, n) = solve(x, y, n, Array(Vector{Int}, 0))

for i = 1:8
  print("Solve for $i\n")
  print(solve(8, 8, i))
  print("\n")
end


"""
    AbstractInterpolatingPolynomial{T,X}

Abstract type for interpolating polynomials.

These are polynomial representations of `p(x)` satisfying `p(x_i) =
y_i` for a specified set of `x` values and `y` values.

For a collection of points `(x_0,y_0), ..., (x_n, y_n)` there is a
*unique* polynomial of degree `n` or less satisfying
`p(x_i)=y_i`. This fact allows the specification of `p(x)` using a
vector of coefficients relative to some set of basis vectors.

The two main types, `Lagrange` and `Newton`, store the nodes within
the instance. In particular, the type does not contain all the
information needed to describe the family. So methods like
`convert(::Type, p)` will not work. Use `fit(Type, xs, p)`, as
appropriate, instead.

"""
abstract type AbstractInterpolatingPolynomial{T,X} <: AbstractSpecialPolynomial{T,X} end

function Polynomials.showterm(
    io::IO,
    ::Type{P},
    pj::T,
    var,
    j,
    first::Bool,
    mimetype,
) where {N,T,P<:AbstractInterpolatingPolynomial}
    iszero(pj) && return false
    !first && print(io, " ")
    print(io, Polynomials.hasneg(T) && Polynomials.isneg(pj) ? "- " : (!first ? "+ " : ""))
    print(io, "$(abs(pj))⋅$(basis_symbol(P))_$(j)($var)")
    return true
end

basis_symbol(::Type{<:AbstractInterpolatingPolynomial}) = "P"

function Polynomials.variable(
    ::Type{P},
    var::Polynomials.SymbolLike=:x,
) where {P<:AbstractInterpolatingPolynomial}
    @warn "No `variable` function is defined for a type, just an instance"
    throw(MethodError())
end

# pick new nodes for product
function _new_nodes(xs::Vector{S}, ys::Vector{T}) where {S,T}
    sort!(ys)
    n = length(ys)
    out = setdiff(ys, xs)
    length(out) >= n - 1 && return out[1:(n - 1)]
    out = out / 1
    tmp = intersect(xs, ys)
    sort!(tmp)
    for i in 2:length(tmp)
        a, b = tmp[i - 1], tmp[i]
        c = (a + b) / 2
        while c in xs
            c = (a + c) / 2
        end
        push!(out, c)
    end
    out
end


#=
This file attempts to implement the "imporoved dimension estimation" by Judd:

Judd, K. (1992). An improved estimator of dimension and some comments on providing
confidence intervals. Physica D: Nonlinear Phenomena, 56(2–3), 216–228. https://doi.org/10.1016/0167-2789(92)90025-I

Judd, K. (1994). Estimating dimension from small samples. Physica D: Nonlinear Phenomena,
71(4), 421–429. https://doi.org/10.1016/0167-2789(94)90008-6

So far this attempt has failed.
=#
using Distances, BigCombinatorics

"""
    interpoint_distances(X, N, norm = Euclidean()) -> ds

returns N interpoint distances of randomly chosen pairs of points from time series X.
N must be smaller than the free memory. N ≈ 5000 is actually already sufficient for the histograms. N ≈ 45000 sort of the upper limit of what you should use, this makes for a smoother hist later
"""
function interpoint_distances(X, N, norm = Euclidean())
    ds = eltype(X)[]

    @assert N < Int(Sys.free_memory()) ÷ 8

    # choosing N random points from X.
    sizehint!(ds, Int(N*(N-1)/2))
    while length(ds) < Int(N*(N-1)/2)
        i,j = rand(1:length(X),2)
        dist = evaluate(norm, X[i], X[j])
        (i != j && !any(ds .== dist)) && push!(ds, dist)
    end
    return filter(x-> x!= 0.0,ds)
end

"""
    logspace_histogram(distances::Vector, λ = exp(-w0), ϵ₀ = λ^2 * maximum(x)) -> bins, counts
Calculate a logarithmically spaced histogram of the values in `x` (must be positive
definite, i.e. distances) and return the bins and count of values in the bins.

The `i`-th bin is `[ϵ₀*λ^i, ϵ₀*λ^(i+1))`. Elements of `x` greater than `ϵ₀` are disregarded.
"""
function logspace_histogram(distances::AbstractVector, ε₀, λ = exp(-w0(x)))
    @assert λ < 1
    εs = [ε₀]
    λmin = minimum(distances)*λ
    εᵢ = ε₀
    while εᵢ > λmin
        εᵢ = εs[end]*λ
        push!(εs, εᵢ)
    end

    reverse!(εs)
    L = length(εs)
    bs = zeros(Int, L)
    # Fill bins with values
    for v in distances
        i = searchsortedfirst(εs, v)
        i == L + 1 && continue
        bs[i] += 1
    end
    return reverse!(εs), reverse!(bs)
end
# TODO: test the algorithm with including the bin [∞, ε0)
w0(x) = log(maximum(x)/minimum(x))/√length(x)

"""
    bin_judd(x::AbstractVector) -> εs, bs, ε₀

returns a logspace histogram with optimal bin width.
Estimation of the latter could probably be done more efficient

"""
function bin_judd(x::AbstractVector)

    opt_λ =  optimal_λ(x) # This is probably not very efficient
    εs, bs = logspace_histogram(x,maximum(x),opt_λ)

    ε₀ = εs[findmax(bs)[2]+1]
    new_εs = reverse(εs[findmax(bs)[2]+1:end])
    new_bs = reverse(bs[findmax(bs)[2]+1:end])
    push!(new_bs, sum(bs[1:findmax(bs)[2]]))
    push!(new_εs, εs[findmax(bs)[2]])
    reverse!(new_bs)
    reverse!(new_εs)
    return new_εs, new_bs, ε₀
end

function P(εᵢ, d,a,ε₀)
    polynomial = length(a) == 0 ? 1 : 0
    for t in 1:length(a)
        polynomial += a[t] * (εᵢ/ε₀)^(t-1)
    end
    return (εᵢ/ε₀)^d * polynomial
end

""" log_l(εs, bs, P, d, a,ε₀)

returns the log-likelihood function according to Judd
"""
log_l(εs, bs, P, d, a,ε₀) = -sum(bs[i]*log( P(εs[i],d,a,ε₀) - P(εs[i+1],d,a,ε₀) ) for i in 1:length(εs)-1 )

""" optimal_λ(dists)

searches for the optimal bin width w = log(1/λ) of the judd histogram.
The search is currently not how Judd proposed, because I didn't get anywhere with that. Right now, this is a brute force search, this should definitely be improved if this is actuallly going to be used.

"""
function optimal_λ(dists)
    n = length(dists)
    w₀ = w0(dists)
    ws = collect(w₀:0.001:.5)
    f_ws = []

    for w in ws
        εs, bs = logspace_histogram(dists, maximum(dists), exp(-w))
        m = findlast(x->x!=0,bs)
        f = log(w) + log(Multinomial(bs[1:m])) + log(Binomial(n+m+1,m))
        push!(f_ws,f)
    end

    w = ws[findmin(f_ws)[2]]
    λ = exp(-w)
    return λ
end

problargerzero(εs, bs, d₀,a₀,ε₀) = all(P(εs[i], d₀, a₀,ε₀)-P(εs[i+1],d₀,a₀,ε₀) > 0 for i in 1:length(εs)-1)

""" nested_optimizer(εs, bs; ε₀, d₀, a₀, stp = 1e-5, η = 1e-3, tol=1e-4, decay=1e-4, maxiter = 10000)

    uses numerical (center-diff) gradient descent with adaptive (decaying) learning rate to find
    a minimum of the log-liklelihood function.
    Seems to produce reasonable results if `deg(a) < 2`, but still sensitive to initial
    conditions.
    The key was to use the nested version that Judd suggested in a footnote of the 1994 paper:
    nest the optimizations so that first `d` is optimized with fixed `a`, then the coefficients
    of `a` are optimized with fixed `d`. Repeat until total step size is smaller than `tol`.

    There are a lot of parameters to this optimization problem that themselves can probably be
    optimized and/ or made more adaptive so that they fit the specific problem.

"""
function nested_optimizer(εs, bs; ε₀, d₀, a₀, stp = 1e-5, η = 1e-3, γ = 1e-3, tol=1e-4, decay=1e-4, maxiter = 10000)

    iter = 0

    deg_a = length(a₀) # Technically, this is not the *degree*, but deg(a)+1, but it's more convenient to write it like this

    cur_x = [d₀, a₀...]
    cur_d = cur_x[1]
    cur_a = cur_x[2:end]

    step_size = 10

    while iter < maxiter && step_size > tol

        prev_v = cur_x

        # First we will optimize d with fixed polynomial coefficients
        cur_d = cur_x[1]
        v = 0
        iter_d = 0
        d_step = 1
        while iter_d < maxiter÷10 && d_step > tol/10
            prev_d = cur_d
            g = (log_l(εs, bs, P, cur_d+stp, a₀,ε₀)-log_l(εs, bs, P, cur_d-stp, a₀,ε₀))/(2stp)
            v = η * g + γ * v
            cur_d -= v
            d_step = abs(cur_d-prev_d)
            iter_d += 1
        end

        # now we will optimize the coefficients of a with the previous best d
        grad = zeros(deg_a)
        cur_a = cur_x[2:end]
        v = zeros(deg_a)
        iter_a = 0
        a_step = 1
        while iter_a < maxiter÷10 && a_step > tol/10
            prev_a = cur_a
            for k in 1:deg_a
                a_stp = zeros(deg_a)
                a_stp[k] = stp
                grad[k] = (log_l(εs, bs, P, d₀, a₀ + a_stp,ε₀)-log_l(εs, bs, P, d₀, a₀-a_stp,ε₀))/(2stp)
            end
            v = γ * v + η * grad
            cur_a -= v
            a_step = evaluate(Euclidean(), cur_a, prev_a)
            iter_a += 1
        end

        if problargerzero(εs, bs, cur_d,cur_a,ε₀)
            cur_x = [cur_d, cur_a...]
        else
            break
        end

        # check total step size of parameter vector
        step_size = evaluate(Euclidean(), cur_x, prev_v)

        # update learning rate
        η /= (1+decay*iter)

        iter += 1
    end
    cur_d, cur_a
end


push!(LOAD_PATH, joinpath(pwd(), "src"))
using JuML
using Test
using MLJ
import MLJBase
import DataFrames
import CSV

# train_df = DataFrame(joinpath("data", "airlinetrain"))
# test_df = DataFrame(joinpath("data", "airlinetest"))

# distance = train_df[:Distance]
# deptime = train_df[:DepTime]

# deptime = factor(train_df[:DepTime])
# distance = factor(train_df[:Distance])

# factors = [train_df.factors; [deptime, distance]]

# label = train_df[:dep_delayed_15min]

# model = XGBClassifier(maxdepth = 2, nrounds = 5)

# mach = machine(model, factors, label)
# fit!(mach; force = true)

# yhat = MLJ.predict(mach, train_df)

df = CSV.read(abspath(joinpath("data", "airlinetrain1M.csv")))

DataFrames.names(df)
DataFrames.categorical!(df, [1, 2, 3, 5, 6, 7, 9])

train_df = JuML.DataFrame(df)

distance = train_df["Distance"]
deptime = train_df["DepTime"]
label = covariate(train_df["dep_delayed_15min"], level -> level == "Y" ? 1.0 : 0.0)
deptime = factor(train_df["DepTime"])
distance = factor(train_df["Distance"])

factors = [train_df.factors; [deptime, distance]]

@time model7 = xgblogit(label, factors; η = 0.1, λ = 1.0, γ = 0.0, minchildweight = 1.0, nrounds = 100, maxdepth = 8, ordstumps = false, pruning = false, caching = true, usefloat64 = false, singlethread = true, slicelength = 0);
auc = getauc(model7.pred, label)
model7.pred[1:5]


module day23

using LinkedLists

wrap(i, len) = i == 0 ? len : i

function playcups(cups, iterations)
    len = length(cups)
    circle = LinkedList{Int}()
    append!(circle, cups)
    lookups = repeat([lastindex(circle)], len)
    for index in keys(circle)
        lookups[getindex(circle, index)] = index
    end

    for i in 1:iterations
        # Put current back at the end of the list
        currentlabel = popfirst!(circle)
        push!(circle, currentlabel)
        lookups[currentlabel] = lastindex(circle)
        # Remove next three
        removed = [popfirst!(circle) for i in 1:3]
        # Find destination
        destlabel = wrap(currentlabel - 1, len)
        while destlabel ∈ removed destlabel = wrap(destlabel - 1, len) end
        destindex = lookups[destlabel]
        # Replace three removed after dest
        for r in removed
            destindex = last(iterate(circle, destindex))
            destindex = insert!(circle, destindex, r)
            lookups[r] = destindex
        end
    end
    cups = [i for i in circle]
end

cups = parse.(Int, collect("247819356"))
cups = playcups(cups, 10)
println("Part 1: remaining = $(join(circshift(cups, -(only(indexin(1, cups)) - 1))[2:end]))")

# Part 2
bigcups = collect(1:1000000)
bigcups[1:9] = parse.(Int, collect("247819356"))
bigcups = playcups(bigcups, 10000000)
println("Part 2: score = $(prod(circshift(bigcups, -(only(indexin(1, bigcups)) - 1))[2:3]))")

end


@testset "interval.jl" begin
    it = Interval(2.0, 5.0)
    @test minimum(it) == 2.0
    @test maximum(it) == 5.0

    @test in(3.0, it) == true
    @test in(10.0, it) == false
    @test in(5.0, it) == true

    @test isempty(it) == false
    @test isempty(Interval(2.0,1.0)) == true
    @test isempty(Interval(0.0, 0.0)) == true

    @test intersect(Interval(2.0, 5.0), Interval(3.0, 6.0)) == Interval(3.0, 5.0)
    @test intersect(Interval(2.0, 5.0), Interval(6.0, 9.0)) == Interval(0.0, 0.0)

    @test intersect(Interval(2.0, 5.0), Interval(0.0, 0.0)) == Interval(0.0, 0.0)
    @test intersect(Interval(5.0, 5.0), Interval(2.0, 5.0)) == Interval(0.0, 0.0)
end


"""
    rna(R::AbstractRecurrenceMatrix)
    rna(args...; kwargs...)

Calculate a set of Recurrence Network parameters.
The input `R` can be a symmetric recurrence matrix that is
interpreted as the adjacency matrix of an undirected complex network,
such that linked vertices are neighboring points in the phase space.

Alternatively, the inputs can be a graph object or any
valid inputs to the `SimpleGraph` constructor of the
[LightGraphs](https://github.com/JuliaGraphs/LightGraphs.jl) package.

## Return

The returned value is a dictionary that contains the following entries,
with the corresponding global network properties[1, 2]:
* `:density`: edge density, approximately equivalent to the global recurrence rate in the phase space.
* `:transitivity`: network transitivity, which describes the
global clustering of points following Barrat's and Weigt's formulation [3].
* `:averagepath`: mean value of the shortest path lengths taken over
all pairs of connected vertices, related to the average separation
between points in the phase.
* `:diameter`: maximum value of the shortest path lengths between
pairs of connected vertices, related to the phase space diameter.

## References

[1]: R.V. Donner *et al.* "Recurrence networks — a novel paradigm for nonlinear time series analysis",
*New Journal of Physics* 12, 033025 (2010)
[DOI:10.1088/1367-2630/12/3/033025](https://doi.org/10.1088/1367-2630/12/3/033025)

[2]: R.V. Donner *et al.*, The geometry of chaotic dynamics — a complex network perspective,
*Eur. Phys. J.* B 84, 653–672 (2011)
[DOI:10.1140/epjb/e2011-10899-1](https://doi.org/10.1140/epjb/e2011-10899-1)

[3]: A. Barrat & M. Weight, "On the properties of small-world network models",
*The European Physical Journal B* 13, 547–560 (2000)
[DOI:10.1007/s100510050067](https://doi.org/10.1007/s100510050067)
"""
function rna(args...; kwargs...)
    graph = SimpleGraph(args...; kwargs...)
    return Dict{Symbol, Float64}(
        :density => density(graph),
        :transitivity => global_clustering_coefficient(graph),
        :averagepath => averagepath(graph),
        :diameter => diameter(graph)
    )
end

"""
    averagepath(graph)

Calculates average minimum path length for a SimpleGraph.
Uses Donner, 2010, Eq. 26 on pg. 18.
"""
function averagepath(graph::SimpleGraph)
    num_verts = nv(graph)

    dist_mat = floyd_warshall_shortest_paths(graph).dists
    @. dist_mat[dist_mat == typemax(dist_mat)] = 0.

    return sum(dist_mat) / (num_verts * (num_verts - 1))
end