# This file is part of the TaylorIntegration.jl package; MIT licensed

using TaylorIntegration
using Test

const _order = 28
const _abstol = 1.0E-20

eqs1(z, p, t) = -z
eqs2(z, p, t) = im*z
function eqs3!(Dz, z, p, t)
    Dz[1] = eqs1(z[1], p, t)
    Dz[2] = eqs2(z[2], p, t)
    nothing
end

@testset "Test integration of ODE with complex dependent variables (1)" begin
    z0 = complex(0.0, 1.0)
    tr = 0.0:pi/8:2pi
    ts = 0.0:pi:2pi
    zsol1 = taylorinteg(eqs1, z0, ts, _order, _abstol, maxsteps=1)
    @test size(zsol1) == (length(ts),)
    zsol1 = taylorinteg(eqs1, z0, tr, _order, _abstol, maxsteps=1, nothing)
    @test length(zsol1) == length(tr)
    ta = vec(tr)
    zsol1 = taylorinteg(eqs1, z0, ta, _order, _abstol, maxsteps=1)
    @test length(zsol1) == length(ta)
    zsol1 = taylorinteg(eqs1, z0, tr, _order, _abstol)
    @test zsol1[1] == z0
    @test isapprox( zsol1[2]  , z0*exp(-tr[2]) )
    @test isapprox( zsol1[6], z0*exp(-tr[6]) )
    @test isapprox( zsol1[end], z0*exp(-tr[end]) )
    zsol11 = taylorinteg(eqs1, z0, ta, _order, _abstol, nothing)
    @test zsol11 == zsol1
    @test zsol11[1] == z0
    @test isapprox( zsol11[2]  , z0*exp(-tr[2]) )
    @test isapprox( zsol11[6], z0*exp(-tr[6]) )
    @test isapprox( zsol11[end], z0*exp(-tr[end]) )
    tt, zsol1t = taylorinteg(eqs1, z0, 0.0, 2pi, _order, _abstol, maxsteps=1)
    @test length(tt) == 2
    @test length(zsol1t) == 2
    tt, zsol1t = taylorinteg(eqs1, z0, 0.0, 2pi, _order, _abstol)
    @test zsol1t[1] == z0
    @test isapprox( zsol1t[2]  , z0*exp(-tt[2]) )
    @test isapprox( zsol1t[end], z0*exp(-tt[end]) )
    @test zsol1t[end] == zsol1[end]
end

@testset "Test integration of ODE with complex dependent variables (2)" begin
    z0 = complex(0.0, 1.0)
    tr = 0.0:pi/8:2pi
    ts = 0.0:pi:2pi
    zsol2 = taylorinteg(eqs2, z0, ts, _order, _abstol, maxsteps=1, nothing)
    @test size(zsol2) == (length(ts),)
    zsol2 = taylorinteg(eqs2, z0, tr, _order, _abstol, maxsteps=1)
    @test length(zsol2) == length(tr)
    ta = vec(tr)
    zsol2 = taylorinteg(eqs2, z0, ta, _order, _abstol, maxsteps=1)
    @test length(zsol2) == length(ta)
    zsol2 = taylorinteg(eqs2, z0, tr, _order, _abstol)
    @test zsol2[1] == z0
    @test isapprox( zsol2[3], z0*exp( complex(0.0, tr[3])) )
    @test isapprox( zsol2[5], z0*exp( complex(0.0, tr[5])) )
    zsol22 = taylorinteg(eqs2, z0, ta, _order, _abstol, nothing)
    @test zsol22 == zsol2
    @test zsol22[1] == z0
    @test isapprox( zsol22[3], z0*exp( complex(0.0, tr[3])) )
    @test isapprox( zsol22[5], z0*exp( complex(0.0, tr[5])) )
    tt, zsol2 = taylorinteg(eqs2, z0, 0.0, 2pi, _order, _abstol, maxsteps=1)
    @test length(tt) == 2
    @test length(zsol2) == 2
    tt, zsol2 = taylorinteg(eqs2, z0, 0.0, 2pi, _order, _abstol)
    @test zsol2[1] == z0
    @test isapprox( zsol2[2], z0*exp( complex(0.0, tt[2])) )
    @test isapprox( zsol2[end], z0*exp( complex(0.0, tt[end])) )
end

@testset "Test integration of ODE with complex dependent variables (3)" begin
    z0 = complex(0.0, 1.0)
    zz0 = [z0, z0]
    tr = 0.0:pi/8:2pi
    ts = 0.0:pi:2pi
    zsol3 = taylorinteg(eqs3!, zz0, ts, _order, _abstol, maxsteps=1)
    @test size(zsol3) == (length(ts), length(zz0))
    zsol3 = taylorinteg(eqs3!, zz0, tr, _order, _abstol, maxsteps=1, nothing)
    @test size(zsol3) == (length(tr), length(zz0))
    ta = vec(tr)
    zsol3 = taylorinteg(eqs3!, zz0, ta, _order, _abstol, maxsteps=1)
    @test size(zsol3) == (length(ta), length(zz0))
    zsol3 = taylorinteg(eqs3!, [z0, z0], tr, _order, _abstol)
    @test isapprox( zsol3[4,1], z0*exp(-tr[4]) )
    @test isapprox( zsol3[7,1], z0*exp(-tr[7]) )
    @test isapprox( zsol3[4,2], z0*exp( complex(0.0, tr[4])) )
    @test isapprox( zsol3[7,2], z0*exp( complex(0.0, tr[7])) )
    zsol33 = taylorinteg(eqs3!, [z0, z0], ta, _order, _abstol, nothing)
    @test zsol33 == zsol3
    @test isapprox( zsol33[4,1], z0*exp(-tr[4]) )
    @test isapprox( zsol33[7,1], z0*exp(-tr[7]) )
    @test isapprox( zsol33[4,2], z0*exp( complex(0.0, tr[4])) )
    @test isapprox( zsol33[7,2], z0*exp( complex(0.0, tr[7])) )
    tt, zsol3 = taylorinteg(eqs3!, zz0, 0.0, 2pi, _order, _abstol, maxsteps=1)
    @test length(tt) == 2
    @test size(zsol3) == (length(tt), length(zz0))
    tt, zsol3 = taylorinteg(eqs3!, zz0, 0.0, 2pi, _order, _abstol)
    @test zsol3[1,1] == z0
    @test zsol3[1,2] == z0
    @test isapprox( zsol3[2,1], z0*exp( -tt[2]) )
    @test isapprox( zsol3[end,1], z0*exp( -tt[end]) )
    @test isapprox( zsol3[2,2], z0*exp( complex(0.0, tt[2])) )
    @test isapprox( zsol3[end,2], z0*exp( complex(0.0, tt[end])) )
end


struct FLSparseSynapseParameter
end

@snn_kw mutable struct FLSparseSynapse{VFT=Vector{Float32},FT=Float32}
    param::FLSparseSynapseParameter = FLSparseSynapseParameter()
    colptr::Vector{Int32} # column pointer of sparse W
    I::Vector{Int32}      # postsynaptic index of W
    W::VFT  # synaptic weight
    rI::VFT # postsynaptic rate
    rJ::VFT # presynaptic rate
    g::VFT  # postsynaptic conductance
    P::VFT  # <rᵢrⱼ>⁻¹
    q::VFT  # P * r
    u::VFT # force weight
    w::VFT # output weight
    f::FT = 0 # postsynaptic traget
    z::FT = 0.5randn()  # output z ≈ f
    records::Dict = Dict()
end

"""
[Force Learning Sparse Synapse](http://www.theswartzfoundation.org/docs/Sussillo-Abbott-Coherent-Patterns-August-2009.pdf)
"""
FLSparseSynapse

function FLSparseSynapse(pre, post; σ = 1.5, p = 0.0, α = 1, kwargs...)
    w = σ * 1 / √(p * pre.N) * sprandn(post.N, pre.N, p)
    rowptr, colptr, I, J, index, W = dsparse(w)
    rI, rJ, g = post.r, pre.r, post.g
    P = α .* (I .== J)
    q = zeros(post.N)
    u = 2rand(post.N) - 1
    w = 1 / √post.N * (2rand(post.N) - 1)
    FLSparseSynapse(;@symdict(colptr, I, W, rI, rJ, g, P, q, u, w)..., kwargs...)
end

function forward!(c::FLSparseSynapse, param::FLSparseSynapseParameter)
    @unpack W, rI, rJ, g, P, q, u, w, f, z = c
    z = dot(w, rI)
    g .= z .* u
    fill!(q, zero(Float32))
    @inbounds for j in 1:(length(colptr) - 1)
        rJj = rJ[j]
        for s = colptr[j]:(colptr[j+1] - 1)
            i = I[s]
            q[i] += P[s] * rJj
            g[i] += W[s] * rJj
        end
    end
end

function plasticity!(c::FLSparseSynapse, param::FLSparseSynapseParameter, dt::Float32, t::Float32)
    C = 1 / (1 + dot(q, rI))
    BLAS.axpy!(C * (f - z), q, w)
    @inbounds for j in 1:(length(colptr) - 1)
        for s in colptr[j]:(colptr[j+1] - 1)
            P[s] += -C * q[I[s]] * q[j]
        end
    end
end


using NumCME
using StaticArrays
using Test

states = [[@MVector([0, i]) for i in 0:10];[@MVector([1, i]) for i in 0:10]]
vals = [[0.0 for i in 0:10]; [1.0 for i in 0:10]]
v = FspVectorSparse(states, vals)
v1 = sum(v, [1])
v2 = sum(v, [2])
@test typeof(v) <: AbstractFspVector
@test typeof(v) <: FspVectorSparse{2, <:Integer, <:Real}
@test sum(v) == 11.0
@test nnz(v1) == 11
@test nnz(v2) == 2

@test_throws ArgumentError FspVectorSparse([@MVector([0, 1])], [1.0,2.0])
@test_throws ArgumentError FspVectorSparse([@MVector([0,1]), @MVector([1,2])], [1.0])


# sqrt:
# tests special values from csqrt man page
# as well as conj(sqrt(z)) = sqrt(conj(z))
@test isequal(sqrt(complex( 0.0, 0.0)), complex( 0.0, 0.0))
@test isequal(sqrt(complex( 0.0,-0.0)), complex( 0.0,-0.0))
@test isequal(sqrt(complex( 0.0, Inf)), complex( Inf, Inf))
@test isequal(sqrt(complex( 0.0,-Inf)), complex( Inf,-Inf))
@test isequal(sqrt(complex( 0.0, NaN)), complex( NaN, NaN))
@test isequal(sqrt(complex(-0.0, 0.0)), complex( 0.0, 0.0))
@test isequal(sqrt(complex(-0.0,-0.0)), complex( 0.0,-0.0))

@test isequal(sqrt(complex( 5.0, 0.0)), complex(sqrt(5.0), 0.0))
@test isequal(sqrt(complex( 5.0,-0.0)), complex(sqrt(5.0),-0.0))
@test isequal(sqrt(complex(-5.0, 0.0)), complex( 0.0, sqrt(5.0)))
@test isequal(sqrt(complex(-5.0,-0.0)), complex( 0.0,-sqrt(5.0)))

@test isequal(sqrt(complex( Inf, 0.0)), complex( Inf, 0.0))
@test isequal(sqrt(complex( Inf,-0.0)), complex( Inf,-0.0))
@test isequal(sqrt(complex( Inf, 5.0)), complex( Inf, 0.0))
@test isequal(sqrt(complex( Inf,-5.0)), complex( Inf,-0.0))
@test isequal(sqrt(complex( Inf, Inf)), complex( Inf, Inf))
@test isequal(sqrt(complex( Inf,-Inf)), complex( Inf,-Inf))
@test isequal(sqrt(complex( Inf, NaN)), complex( Inf, NaN))

@test isequal(sqrt(complex(-Inf, 0.0)), complex( 0.0, Inf))
@test isequal(sqrt(complex(-Inf,-0.0)), complex( 0.0,-Inf))
@test isequal(sqrt(complex(-Inf, 5.0)), complex( 0.0, Inf))
@test isequal(sqrt(complex(-Inf,-5.0)), complex( 0.0,-Inf))
@test isequal(sqrt(complex(-Inf, Inf)), complex( Inf, Inf))
@test isequal(sqrt(complex(-Inf,-Inf)), complex( Inf,-Inf))
@test isequal(sqrt(complex(-Inf, NaN)), complex( NaN, Inf))

@test isequal(sqrt(complex( NaN, 0.0)), complex( NaN, NaN))
@test isequal(sqrt(complex( NaN, 0.0)), complex( NaN, NaN))
@test isequal(sqrt(complex( NaN, Inf)), complex( Inf, Inf))
@test isequal(sqrt(complex( NaN,-Inf)), complex( Inf,-Inf))

# log:
#  log(conj(z)) = conj(log(z))

@test isequal(log(complex( 0.0, 0.0)), complex(-Inf, 0.0))
@test isequal(log(complex( 0.0,-0.0)), complex(-Inf,-0.0))
@test isequal(log(complex( 0.0, 1.0)), complex( 0.0, pi/2))
@test isequal(log(complex( 0.0,-1.0)), complex( 0.0,-pi/2))
@test isequal(log(complex( 0.0, Inf)), complex( Inf, pi/2))
@test isequal(log(complex( 0.0,-Inf)), complex( Inf,-pi/2))
@test isequal(log(complex( 0.0, NaN)), complex( NaN, NaN))
@test isequal(log(complex(-0.0, 0.0)), complex(-Inf, pi))
@test isequal(log(complex(-0.0,-0.0)), complex(-Inf,-pi))

@test isequal(log(complex( 5.0, 0.0)),complex(log(5.0), 0.0))
@test isequal(log(complex( 5.0,-0.0)),complex(log(5.0),-0.0))

@test isequal(log(complex( Inf, 5.0)), complex( Inf, 0.0))
@test isequal(log(complex( Inf,-5.0)), complex( Inf,-0.0))
@test isequal(log(complex( Inf, Inf)), complex( Inf, pi/4))
@test isequal(log(complex( Inf,-Inf)), complex( Inf,-pi/4))
@test isequal(log(complex( Inf, NaN)), complex( Inf, NaN))

@test isequal(log(complex(-Inf, 5.0)), complex( Inf, pi))
@test isequal(log(complex(-Inf,-5.0)), complex( Inf,-pi))
@test isequal(log(complex(-Inf, Inf)), complex( Inf, 3*pi/4))
@test isequal(log(complex(-Inf,-Inf)), complex( Inf,-3*pi/4))
@test isequal(log(complex(-Inf, NaN)), complex( Inf, NaN))

@test isequal(log(complex( NaN, 0.0)), complex( NaN, NaN))
@test isequal(log(complex( NaN, Inf)), complex( Inf, NaN))
@test isequal(log(complex( NaN,-Inf)), complex( Inf, NaN))
@test isequal(log(complex( NaN, NaN)), complex( NaN, NaN))

# exp:
#  exp(conj(z)) = conj(exp(z))

@test isequal(exp(complex( 0.0, 0.0)), complex(1.0, 0.0))
@test isequal(exp(complex( 0.0,-0.0)), complex(1.0,-0.0))
@test isequal(exp(complex( 0.0, Inf)), complex(NaN, NaN))
@test isequal(exp(complex( 0.0,-Inf)), complex(NaN, NaN))
@test isequal(exp(complex( 0.0, NaN)), complex(NaN, NaN))

@test isequal(exp(complex(-0.0, 0.0)), complex(1.0, 0.0))
@test isequal(exp(complex(-0.0,-0.0)), complex(1.0,-0.0))

@test isequal(exp(complex( 5.0, Inf)), complex(NaN, NaN))

@test isequal(exp(complex( Inf, 0.0)), complex(Inf, 0.0))
@test isequal(exp(complex( Inf,-0.0)), complex(Inf,-0.0))
@test isequal(exp(complex( Inf, 5.0)), complex(cos(5.0)*Inf,sin(5.0)* Inf))
@test isequal(exp(complex( Inf,-5.0)), complex(cos(5.0)*Inf,sin(5.0)*-Inf))
@test isequal(exp(complex( Inf, NaN)), complex(-Inf, NaN))
@test isequal(exp(complex( Inf, Inf)), complex(-Inf, NaN))
@test isequal(exp(complex( Inf,-Inf)), complex(-Inf, NaN))

@test isequal(exp(complex(-Inf, 0.0)), complex(0.0, 0.0))
@test isequal(exp(complex(-Inf,-0.0)), complex(0.0,-0.0))
@test isequal(exp(complex(-Inf, 5.0)), complex(cos(5.0)*0.0,sin(5.0)* 0.0))
@test isequal(exp(complex(-Inf,-5.0)), complex(cos(5.0)*0.0,sin(5.0)*-0.0))
@test isequal(exp(complex(-Inf, Inf)), complex(-0.0, 0.0))
@test isequal(exp(complex(-Inf,-Inf)), complex(-0.0,-0.0))
@test isequal(exp(complex(-Inf, NaN)), complex(-0.0, 0.0))

@test isequal(exp(complex( NaN, 0.0)), complex( NaN, 0.0))
@test isequal(exp(complex( NaN,-0.0)), complex( NaN,-0.0))
@test isequal(exp(complex( NaN, 5.0)), complex( NaN, NaN))
@test isequal(exp(complex( NaN, NaN)), complex( NaN, NaN))

# ^ (cpow)
#  equivalent to exp(y*log(x))
#    except for 0^0?
#  conj(x)^conj(y) = conj(x^y)
@test isequal(complex( 0.0, 0.0)^complex( 0.0, 0.0), complex(1.0, 0.0))
@test isequal(complex( 0.0, 0.0)^complex( 0.0,-0.0), complex(1.0, 0.0))
@test isequal(complex( 0.0, 0.0)^complex(-0.0, 0.0), complex(1.0,-0.0))
@test isequal(complex( 0.0, 0.0)^complex(-0.0,-0.0), complex(1.0,-0.0))

@test isequal(complex( 0.0,-0.0)^complex( 0.0, 0.0), complex(1.0,-0.0))
@test isequal(complex( 0.0,-0.0)^complex( 0.0,-0.0), complex(1.0,-0.0))
@test isequal(complex( 0.0,-0.0)^complex(-0.0, 0.0), complex(1.0, 0.0))
@test isequal(complex( 0.0,-0.0)^complex(-0.0,-0.0), complex(1.0, 0.0))

@test isequal(complex(-0.0, 0.0)^complex( 0.0, 0.0), complex(1.0, 0.0))
@test isequal(complex(-0.0, 0.0)^complex( 0.0,-0.0), complex(1.0, 0.0))
@test isequal(complex(-0.0, 0.0)^complex(-0.0, 0.0), complex(1.0,-0.0))
@test isequal(complex(-0.0, 0.0)^complex(-0.0,-0.0), complex(1.0,-0.0))

@test isequal(complex(-0.0,-0.0)^complex( 0.0, 0.0), complex(1.0,-0.0))
@test isequal(complex(-0.0,-0.0)^complex( 0.0,-0.0), complex(1.0,-0.0))
@test isequal(complex(-0.0,-0.0)^complex(-0.0, 0.0), complex(1.0, 0.0))
@test isequal(complex(-0.0,-0.0)^complex(-0.0,-0.0), complex(1.0, 0.0))

# sinh: has properties
#  sinh(conj(z)) = conj(sinh(z))
#  sinh(-z) = -sinh(z)

@test isequal(sinh(complex( 0.0, 0.0)), complex( 0.0, 0.0))
@test isequal(sinh(complex( 0.0,-0.0)), complex( 0.0,-0.0))
@test isequal(sinh(complex( 0.0, Inf)), complex( 0.0, NaN))
@test isequal(sinh(complex( 0.0,-Inf)), complex( 0.0, NaN))
@test isequal(sinh(complex( 0.0, NaN)), complex( 0.0, NaN))

@test isequal(sinh(complex(-0.0, 0.0)), complex(-0.0, 0.0))
@test isequal(sinh(complex(-0.0,-0.0)), complex(-0.0,-0.0))
@test isequal(sinh(complex(-0.0, Inf)), complex(-0.0, NaN))
@test isequal(sinh(complex(-0.0,-Inf)), complex(-0.0, NaN))
@test isequal(sinh(complex(-0.0, NaN)), complex(-0.0, NaN))

@test isequal(sinh(complex( 5.0, Inf)), complex( NaN, NaN))
@test isequal(sinh(complex( 5.0, NaN)), complex( NaN, NaN))

@test isequal(sinh(complex( Inf, 0.0)), complex( Inf, 0.0))
@test isequal(sinh(complex( Inf,-0.0)), complex( Inf,-0.0))
@test isequal(sinh(complex( Inf, 5.0)), complex(cos(5.0)*Inf,sin(5.0)*Inf))
@test isequal(sinh(complex( Inf, Inf)), complex( Inf, NaN))
@test isequal(sinh(complex( Inf,-Inf)), complex( Inf, NaN))
@test isequal(sinh(complex( Inf, NaN)), complex( Inf, NaN))

@test isequal(sinh(complex(-Inf, 0.0)), complex(-Inf, 0.0))
@test isequal(sinh(complex(-Inf,-0.0)), complex(-Inf,-0.0))
@test isequal(sinh(complex(-Inf, 5.0)), complex(cos(5.0)*-Inf,sin(5.0)*Inf))
@test isequal(sinh(complex(-Inf, Inf)), complex(-Inf, NaN))
@test isequal(sinh(complex(-Inf,-Inf)), complex(-Inf, NaN))
@test isequal(sinh(complex(-Inf, NaN)), complex(-Inf, NaN))

@test isequal(sinh(complex( NaN, 0.0)), complex( NaN, 0.0))
@test isequal(sinh(complex( NaN,-0.0)), complex( NaN,-0.0))
@test isequal(sinh(complex( NaN, 5.0)), complex( NaN, NaN))
@test isequal(sinh(complex( NaN, NaN)), complex( NaN, NaN))

# sin: defined in terms of sinh
#  sin(z) = -i*sinh(i*z)
#  i.e. if sinh(a+ib) = x+iy
#    then  sin(b-ia) = y-ix
#  sin(conj(z)) = conj(sin(z))
#  sin(-z) = -sin(z)

@test isequal(sin(complex( 0.0,-0.0)),complex( 0.0,-0.0))
@test isequal(sin(complex( 0.0, 0.0)),complex( 0.0, 0.0))
@test isequal(sin(complex( 0.0, Inf)),complex( 0.0, Inf))
@test isequal(sin(complex( 0.0,-Inf)),complex( 0.0,-Inf))
@test isequal(sin(complex( 0.0, NaN)),complex( 0.0, NaN))

@test isequal(sin(complex(-0.0, 0.0)),complex(-0.0, 0.0))
@test isequal(sin(complex(-0.0,-0.0)),complex(-0.0,-0.0))
@test isequal(sin(complex(-0.0, Inf)),complex(-0.0, Inf))
@test isequal(sin(complex(-0.0,-Inf)),complex(-0.0,-Inf))
@test isequal(sin(complex(-0.0, NaN)),complex(-0.0, NaN))

@test isequal(sin(complex( 5.0, Inf)),complex(sin(5.0)*Inf,cos(5.0)* Inf))
@test isequal(sin(complex( 5.0,-Inf)),complex(sin(5.0)*Inf,cos(5.0)*-Inf))
@test isequal(sin(complex( 5.0, NaN)),complex( NaN, NaN))

@test isequal(sin(complex( Inf, 0.0)), complex( NaN, 0.0))
@test isequal(sin(complex( Inf,-0.0)), complex( NaN,-0.0))
@test isequal(sin(complex( Inf, 5.0)), complex( NaN, NaN))
@test isequal(sin(complex( Inf, Inf)), complex( Inf, NaN))
@test isequal(sin(complex( Inf,-Inf)), complex( Inf, NaN))

@test isequal(sin(complex(-Inf, 0.0)), complex( NaN, 0.0))
@test isequal(sin(complex(-Inf,-0.0)), complex( NaN,-0.0))
@test isequal(sin(complex(-Inf, 5.0)), complex( NaN, NaN))
@test isequal(sin(complex(-Inf, Inf)), complex(-Inf, NaN))
@test isequal(sin(complex(-Inf,-Inf)), complex(-Inf, NaN))

@test isequal(sin(complex( NaN, 0.0)),complex( NaN, 0.0))
@test isequal(sin(complex( NaN,-0.0)),complex( NaN,-0.0))
@test isequal(sin(complex( NaN, 5.0)),complex( NaN, NaN))
@test isequal(sin(complex( NaN, Inf)),complex( NaN, Inf))
@test isequal(sin(complex( NaN,-Inf)),complex( NaN,-Inf))
@test isequal(sin(complex( NaN, NaN)),complex( NaN, NaN))

# cosh: has properties
#  cosh(conj(z)) = conj(cosh(z))
#  coshh(-z) = cosh(z)

@test isequal(cosh(complex( 0.0, 0.0)), complex( 1.0, 0.0))
@test isequal(cosh(complex( 0.0,-0.0)), complex( 1.0,-0.0))
@test isequal(cosh(complex( 0.0, Inf)), complex( NaN, 0.0))
@test isequal(cosh(complex( 0.0,-Inf)), complex( NaN,-0.0))
@test isequal(cosh(complex( 0.0, NaN)), complex( NaN, 0.0))

@test isequal(cosh(complex(-0.0,-0.0)), complex( 1.0, 0.0))
@test isequal(cosh(complex(-0.0, 0.0)), complex( 1.0,-0.0))
@test isequal(cosh(complex(-0.0, NaN)), complex( NaN, 0.0))

@test isequal(cosh(complex( 5.0, Inf)), complex( NaN, NaN))
@test isequal(cosh(complex( 5.0, NaN)), complex( NaN, NaN))

@test isequal(cosh(complex( Inf, 0.0)), complex( Inf, 0.0))
@test isequal(cosh(complex( Inf,-0.0)), complex( Inf,-0.0))
@test isequal(cosh(complex( Inf, 5.0)), complex(cos(5.0)*Inf,sin(5.0)* Inf))
@test isequal(cosh(complex( Inf,-5.0)), complex(cos(5.0)*Inf,sin(5.0)*-Inf))
@test isequal(cosh(complex( Inf, Inf)), complex( Inf, NaN))
@test isequal(cosh(complex( Inf,-Inf)), complex( Inf, NaN))
@test isequal(cosh(complex( Inf, NaN)), complex( Inf, NaN))

@test isequal(cosh(complex(-Inf,-0.0)), complex( Inf, 0.0))
@test isequal(cosh(complex(-Inf, 0.0)), complex( Inf,-0.0))
@test isequal(cosh(complex(-Inf,-5.0)), complex(cos(5.0)*Inf,sin(5.0)* Inf))
@test isequal(cosh(complex(-Inf, 5.0)), complex(cos(5.0)*Inf,sin(5.0)*-Inf))
@test isequal(cosh(complex(-Inf, Inf)), complex( Inf, NaN))
@test isequal(cosh(complex(-Inf,-Inf)), complex( Inf, NaN))
@test isequal(cosh(complex(-Inf, NaN)), complex( Inf, NaN))

@test isequal(cosh(complex( NaN, 0.0)), complex( NaN, 0.0))
@test isequal(cosh(complex( NaN, 5.0)), complex( NaN, NaN))
@test isequal(cosh(complex( NaN,-0.0)), complex( NaN,-0.0))
@test isequal(cosh(complex( NaN,-5.0)), complex( NaN, NaN))
@test isequal(cosh(complex( NaN, NaN)), complex( NaN, NaN))

# cos
#  cos(z) = cosh(iz)
#   i.e cos(b-ia) = cosh(a+ib)
#   and cos(b+ia) = cosh(a-ib)
#  cos(conj(z)) = conj(cos(z))
#  cos(-z) = cos(z)

@test isequal(cos(complex( 0.0, 0.0)), complex( 1.0,-0.0))
@test isequal(cos(complex( 0.0,-0.0)), complex( 1.0, 0.0))
@test isequal(cos(complex( 0.0,-Inf)), complex( Inf, 0.0))
@test isequal(cos(complex( 0.0, Inf)), complex( Inf,-0.0))
@test isequal(cos(complex( 0.0, NaN)), complex( NaN, 0.0))
@test isequal(cos(complex(-0.0, 0.0)), complex( 1.0, 0.0))
@test isequal(cos(complex(-0.0,-0.0)), complex( 1.0,-0.0))
@test isequal(cos(complex(-0.0,-Inf)), complex( Inf,-0.0))
@test isequal(cos(complex(-0.0, Inf)), complex( Inf, 0.0))
@test isequal(cos(complex(-0.0, NaN)), complex( NaN,-0.0))

@test isequal(cos(complex( 5.0,-Inf)), complex(cos(5.0)*Inf,sin(5.0)* Inf))
@test isequal(cos(complex( 5.0, Inf)), complex(cos(5.0)*Inf,sin(5.0)*-Inf))
@test isequal(cos(complex( 5.0, NaN)), complex( NaN, NaN))
@test isequal(cos(complex(-5.0,-Inf)), complex(cos(5.0)*Inf,sin(5.0)*-Inf))
@test isequal(cos(complex(-5.0, Inf)), complex(cos(5.0)*Inf,sin(5.0)* Inf))
@test isequal(cos(complex(-5.0, NaN)), complex( NaN, NaN))

@test isequal(cos(complex( Inf, 0.0)), complex( NaN,-0.0))
@test isequal(cos(complex( Inf, 5.0)), complex( NaN, NaN))
@test isequal(cos(complex( Inf, Inf)), complex( Inf, NaN))
@test isequal(cos(complex( Inf,-Inf)), complex( Inf, NaN))
@test isequal(cos(complex(-Inf, 0.0)), complex( NaN, 0.0))
@test isequal(cos(complex(-Inf, Inf)), complex( Inf, NaN))
@test isequal(cos(complex(-Inf,-Inf)), complex( Inf, NaN))

@test isequal(cos(complex( NaN, 0.0)), complex( NaN, 0.0))
@test isequal(cos(complex( NaN,-0.0)), complex( NaN, 0.0))
@test isequal(cos(complex( NaN, 5.0)), complex( NaN, NaN))
@test isequal(cos(complex( NaN, Inf)), complex( Inf, NaN))
@test isequal(cos(complex( NaN,-Inf)), complex( Inf, NaN))
@test isequal(cos(complex( NaN, NaN)), complex( NaN, NaN))

# tanh
#  tanh(conj(z)) = conj(tanh(z))
#  tanh(-z) = -tanh(z)
@test isequal(tanh(complex( 0.0, 0.0)),complex(0.0,0.0))
@test isequal(tanh(complex( 0.0,-0.0)),complex(0.0,-0.0))
@test_throws   tanh(complex( 0.0, Inf)) #complex(NaN,0.0)
@test_throws   tanh(complex( 0.0,-Inf)) #complex(NaN,-0.0)
@test isequal(tanh(complex( 0.0, NaN)),complex(NaN,NaN))

@test isequal(tanh(complex(-0.0, 0.0)),complex(-0.0,0.0))
@test isequal(tanh(complex(-0.0,-0.0)),complex(-0.0,-0.0))

@test_throws   tanh(complex( 5.0, Inf)) #complex(NaN,NaN)
@test isequal(tanh(complex( 5.0, NaN)),complex(NaN,NaN))

@test isequal(tanh(complex( Inf, 0.0)),complex(1.0, 0.0))
@test isequal(tanh(complex( Inf,-0.0)),complex(1.0,-0.0))
@test isequal(tanh(complex( Inf, 5.0)),complex(1.0,sin(2*5.0)* 0.0))
@test isequal(tanh(complex( Inf,-5.0)),complex(1.0,sin(2*5.0)*-0.0))
@test isequal(tanh(complex( Inf, Inf)),complex(1.0, 0.0))
@test isequal(tanh(complex( Inf,-Inf)),complex(1.0,-0.0))
@test isequal(tanh(complex( Inf, NaN)),complex(1.0, 0.0))

@test isequal(tanh(complex(-Inf, 0.0)),complex(-1.0, 0.0))
@test isequal(tanh(complex(-Inf,-0.0)),complex(-1.0,-0.0))
@test isequal(tanh(complex(-Inf, 5.0)),complex(-1.0,sin(2*5.0)* 0.0))
@test isequal(tanh(complex(-Inf,-5.0)),complex(-1.0,sin(2*5.0)*-0.0))
@test isequal(tanh(complex(-Inf, Inf)),complex(-1.0, 0.0))
@test isequal(tanh(complex(-Inf,-Inf)),complex(-1.0,-0.0))
@test isequal(tanh(complex(-Inf, NaN)),complex(-1.0, 0.0))

@test isequal(tanh(complex( NaN, 0.0)),complex(NaN, 0.0))
@test isequal(tanh(complex( NaN,-0.0)),complex(NaN,-0.0))
@test isequal(tanh(complex( NaN, 5.0)),complex(NaN, NaN))
@test isequal(tanh(complex( NaN,-5.0)),complex(NaN, NaN))
@test isequal(tanh(complex( NaN, NaN)),complex(NaN, NaN))

# tan
#  tan(z) = -i tanh(iz)

@test isequal(tan(complex( 0.0, Inf)),complex( 0.0, 1.0))
@test isequal(tan(complex( 0.0,-Inf)),complex( 0.0,-1.0))
@test isequal(tan(complex( 0.0, NaN)),complex( 0.0, NaN))
@test isequal(tan(complex(-0.0,-Inf)),complex(-0.0,-1.0))
@test isequal(tan(complex(-0.0, Inf)),complex(-0.0, 1.0))
@test isequal(tan(complex(-0.0, NaN)),complex(-0.0, NaN))

@test isequal(tan(complex( 5.0, Inf)),complex(sin(2*5.0)* 0.0, 1.0))
@test isequal(tan(complex( 5.0,-Inf)),complex(sin(2*5.0)* 0.0,-1.0))
@test isequal(tan(complex( 5.0, NaN)),complex( NaN, NaN))
@test isequal(tan(complex(-5.0, Inf)),complex(sin(2*5.0)*-0.0, 1.0))
@test isequal(tan(complex(-5.0,-Inf)),complex(sin(2*5.0)*-0.0,-1.0))
@test isequal(tan(complex(-5.0, NaN)),complex( NaN, NaN))

@test_throws   tan(complex( Inf, 5.0)) #complex(NaN,NaN)
@test isequal(tan(complex( Inf, Inf)),complex( 0.0, 1.0))
@test isequal(tan(complex( Inf,-Inf)),complex (0.0,-1.0))
@test isequal(tan(complex(-Inf, Inf)),complex(-0.0, 1.0))
@test isequal(tan(complex(-Inf,-Inf)),complex(-0.0,-1.0))

@test isequal(tan(complex( NaN, 5.0)),complex( NaN, NaN))
@test isequal(tan(complex( NaN, Inf)),complex( 0.0, 1.0))
@test isequal(tan(complex( NaN,-Inf)),complex( 0.0,-1.0))
@test isequal(tan(complex( NaN, NaN)),complex( NaN, NaN))

# acosh
#  acosh(conj(z)) = conj(acosh(z))

@test isequal(acosh(complex( 0.0, 0.0)), complex( 0.0, pi/2))
@test isequal(acosh(complex( 0.0,-0.0)), complex( 0.0,-pi/2))
@test isequal(acosh(complex( 0.0, Inf)), complex( Inf, pi/2))
@test isequal(acosh(complex( 0.0,-Inf)), complex( Inf,-pi/2))
@test isequal(acosh(complex(-0.0, 0.0)), complex( 0.0, pi/2))
@test isequal(acosh(complex(-0.0,-0.0)), complex( 0.0,-pi/2))
@test isequal(acosh(complex( 5.0, Inf)), complex( Inf, pi/2))
@test isequal(acosh(complex( 5.0,-Inf)), complex( Inf,-pi/2))
@test isequal(acosh(complex( 5.0, NaN)), complex( NaN, NaN))

@test isequal(acosh(complex( Inf, 0.0)), complex( Inf, 0.0))
@test isequal(acosh(complex( Inf,-0.0)), complex( Inf,-0.0))
@test isequal(acosh(complex( Inf, 5.0)), complex( Inf, 0.0))
@test isequal(acosh(complex( Inf,-5.0)), complex( Inf,-0.0))
@test isequal(acosh(complex( Inf, Inf)), complex( Inf, pi/4))
@test isequal(acosh(complex( Inf,-Inf)), complex( Inf,-pi/4))
@test isequal(acosh(complex( Inf, NaN)), complex( Inf, NaN))

@test isequal(acosh(complex(-Inf, 0.0)), complex( Inf, pi))
@test isequal(acosh(complex(-Inf,-0.0)), complex( Inf,-pi))
@test isequal(acosh(complex(-Inf, 5.0)), complex( Inf, pi))
@test isequal(acosh(complex(-Inf,-5.0)), complex( Inf,-pi))
@test isequal(acosh(complex(-Inf, Inf)), complex( Inf, 3*pi/4))
@test isequal(acosh(complex(-Inf,-Inf)), complex( Inf,-3*pi/4))
@test isequal(acosh(complex(-Inf, NaN)), complex( Inf, NaN))

@test isequal(acosh(complex( NaN, Inf)), complex( Inf, NaN))
@test isequal(acosh(complex( NaN,-Inf)), complex( Inf, NaN))
@test isequal(acosh(complex( NaN, NaN)), complex( NaN, NaN))

#
## acos
##  acos(conj(z)) = conj(acos(z))

@test isequal(acos(complex( 0.0, 0.0)),complex(pi/2,-0.0))
@test isequal(acos(complex( 0.0,-0.0)),complex(pi/2, 0.0))
@test isequal(acos(complex( 0.0, Inf)),complex(pi/2,-Inf))
@test isequal(acos(complex( 0.0,-Inf)),complex(pi/2, Inf))
@test isequal(acos(complex( 0.0, NaN)),complex(pi/2, NaN))

@test isequal(acos(complex(-0.0, 0.0)),complex(pi/2,-0.0))
@test isequal(acos(complex(-0.0,-0.0)),complex(pi/2, 0.0))
@test isequal(acos(complex(-0.0, NaN)),complex(pi/2, NaN))

@test isequal(acos(complex( 5.0, Inf)),complex(pi/2,-Inf))
@test isequal(acos(complex( 5.0,-Inf)),complex(pi/2, Inf))
@test isequal(acos(complex( 5.0, NaN)),complex( NaN, NaN))

@test isequal(acos(complex( Inf, 0.0)),complex( 0.0,-Inf))
@test isequal(acos(complex( Inf,-0.0)),complex( 0.0, Inf))
@test isequal(acos(complex( Inf, 5.0)),complex( 0.0,-Inf))
@test isequal(acos(complex( Inf,-5.0)),complex( 0.0, Inf))
@test isequal(acos(complex( Inf, Inf)),complex(pi/4,-Inf))
@test isequal(acos(complex( Inf,-Inf)),complex(pi/4, Inf))
@test isequal(acos(complex( Inf, NaN)),complex( NaN, Inf))

@test isequal(acos(complex(-Inf, 0.0)),complex(pi,-Inf))
@test isequal(acos(complex(-Inf,-0.0)),complex(pi, Inf))
@test isequal(acos(complex(-Inf, 5.0)),complex(pi,-Inf))
@test isequal(acos(complex(-Inf,-5.0)),complex(pi, Inf))
@test isequal(acos(complex(-Inf, Inf)),complex(3*pi/4,-Inf))
@test isequal(acos(complex(-Inf,-Inf)),complex(3*pi/4, Inf))
@test isequal(acos(complex(-Inf, NaN)),complex( NaN, Inf))

@test isequal(acos(complex( NaN, 0.0)),complex( NaN, NaN))
@test isequal(acos(complex( NaN, 5.0)),complex( NaN, NaN))
@test isequal(acos(complex( NaN, Inf)),complex( NaN,-Inf))
@test isequal(acos(complex( NaN,-Inf)),complex( NaN, Inf))
@test isequal(acos(complex( NaN, NaN)),complex( NaN, NaN))

## asinh
##  asinh(conj(z)) = conj(asinh(z))
##  asinh(-z) = -asinh(z)
@test isequal(asinh(complex( 0.0, 0.0)),complex( 0.0, 0.0))
@test isequal(asinh(complex( 0.0,-0.0)),complex( 0.0,-0.0))
@test isequal(asinh(complex( 0.0, Inf)),complex( Inf, pi/2))
@test isequal(asinh(complex( 0.0,-Inf)),complex( Inf,-pi/2))
@test isequal(asinh(complex( 0.0, NaN)),complex( NaN, NaN))

@test isequal(asinh(complex(-0.0, 0.0)),complex(-0.0, 0.0))
@test isequal(asinh(complex(-0.0,-0.0)),complex(-0.0,-0.0))
@test isequal(asinh(complex(-0.0, Inf)),complex(-Inf, pi/2))
@test isequal(asinh(complex(-0.0,-Inf)),complex(-Inf,-pi/2))

@test isequal(asinh(complex( 5.0, Inf)),complex( Inf, pi/2))
@test isequal(asinh(complex( 5.0,-Inf)),complex( Inf,-pi/2))
@test isequal(asinh(complex( 5.0, NaN)),complex( NaN, NaN))
@test isequal(asinh(complex(-5.0, Inf)),complex(-Inf, pi/2))
@test isequal(asinh(complex(-5.0,-Inf)),complex(-Inf,-pi/2))

@test isequal(asinh(complex( Inf, Inf)),complex( Inf, pi/4))
@test isequal(asinh(complex( Inf,-Inf)),complex( Inf,-pi/4))
@test isequal(asinh(complex( Inf, NaN)),complex( Inf, NaN))
@test isequal(asinh(complex(-Inf, Inf)),complex(-Inf, pi/4))
@test isequal(asinh(complex(-Inf,-Inf)),complex(-Inf,-pi/4))
@test isequal(asinh(complex(-Inf, NaN)),complex(-Inf, NaN))

@test isequal(asinh(complex( NaN, 0.0)),complex( NaN, 0.0))
@test isequal(asinh(complex( NaN,-0.0)),complex( NaN,-0.0))
@test isequal(asinh(complex( NaN, 5.0)),complex( NaN, NaN))
@test isequal(asinh(complex( NaN, Inf)),complex( Inf, NaN))
@test isequal(asinh(complex( NaN,-Inf)),complex( Inf, NaN))
@test isequal(asinh(complex( NaN, NaN)),complex( NaN, NaN))

# asin
#  asin(z) = -i*asinh(iz)

@test isequal(asin(complex( 0.0, 0.0)),complex( 0.0, 0.0))
@test isequal(asin(complex( 0.0,-0.0)),complex( 0.0,-0.0))
@test isequal(asin(complex(-0.0, 0.0)),complex(-0.0, 0.0))
@test isequal(asin(complex( 0.0, NaN)),complex( 0.0, NaN))
@test isequal(asin(complex(-0.0,-0.0)),complex(-0.0,-0.0))
@test isequal(asin(complex(-0.0, NaN)),complex(-0.0, NaN))
@test isequal(asin(complex( 5.0, NaN)),complex( NaN, NaN))

@test isequal(asin(complex( Inf, 0.0)),complex( pi/2, Inf))
@test isequal(asin(complex( Inf,-0.0)),complex( pi/2,-Inf))
@test isequal(asin(complex( Inf, 5.0)),complex( pi/2, Inf))
@test isequal(asin(complex( Inf,-5.0)),complex( pi/2,-Inf))
@test isequal(asin(complex( Inf, Inf)),complex( pi/4, Inf))
@test isequal(asin(complex( Inf,-Inf)),complex( pi/4,-Inf))
@test isequal(asin(complex( Inf, NaN)),complex( NaN, Inf))

@test isequal(asin(complex(-Inf, 0.0)),complex(-pi/2, Inf))
@test isequal(asin(complex(-Inf,-0.0)),complex(-pi/2,-Inf))
@test isequal(asin(complex(-Inf, 5.0)),complex(-pi/2, Inf))
@test isequal(asin(complex(-Inf,-5.0)),complex(-pi/2,-Inf))
@test isequal(asin(complex(-Inf, Inf)),complex(-pi/4, Inf))
@test isequal(asin(complex(-Inf,-Inf)),complex(-pi/4,-Inf))
@test isequal(asin(complex(-Inf, NaN)),complex( NaN, Inf))

@test isequal(asin(complex( NaN, 0.0)),complex( NaN, NaN))
@test isequal(asin(complex( NaN, 5.0)),complex( NaN, NaN))
@test isequal(asin(complex( NaN, Inf)),complex( NaN, Inf))
@test isequal(asin(complex( NaN,-Inf)),complex( NaN,-Inf))
@test isequal(asin(complex( NaN, NaN)),complex( NaN, NaN))

# atanh
#  atanh(conj(z)) = conj(atanh(z))
#  atanh(-z) = -atanh(z)

@test isequal(atanh(complex( 0.0, 0.0)),complex( 0.0, 0.0))
@test isequal(atanh(complex( 0.0,-0.0)),complex( 0.0,-0.0))
@test isequal(atanh(complex( 0.0, NaN)),complex( 0.0, NaN))
@test isequal(atanh(complex( 0.0, Inf)),complex( 0.0, pi/2))
@test isequal(atanh(complex( 0.0,-Inf)),complex( 0.0,-pi/2))

@test isequal(atanh(complex(-0.0, NaN)),complex(-0.0, NaN))
@test isequal(atanh(complex(-0.0, 0.0)),complex(-0.0, 0.0))
@test isequal(atanh(complex(-0.0,-0.0)),complex(-0.0,-0.0))
@test isequal(atanh(complex(-0.0, Inf)),complex(-0.0, pi/2))
@test isequal(atanh(complex(-0.0,-Inf)),complex(-0.0,-pi/2))

@test isequal(atanh(complex( 1.0, 0.0)),complex( Inf, 0.0))
@test isequal(atanh(complex(-1.0, 0.0)),complex(-Inf, 0.0))
@test isequal(atanh(complex( 5.0, Inf)),complex( 0.0, pi/2))
@test isequal(atanh(complex( 5.0,-Inf)),complex( 0.0,-pi/2))
@test isequal(atanh(complex( 5.0, NaN)),complex( NaN, NaN))
@test isequal(atanh(complex(-5.0, Inf)),complex(-0.0, pi/2))
@test isequal(atanh(complex(-5.0,-Inf)),complex(-0.0,-pi/2))
@test isequal(atanh(complex(-5.0, NaN)),complex( NaN, NaN))

@test isequal(atanh(complex( Inf, 0.0)),complex(0.0, pi/2))
@test isequal(atanh(complex( Inf,-0.0)),complex(0.0,-pi/2))
@test isequal(atanh(complex( Inf, 5.0)),complex(0.0, pi/2))
@test isequal(atanh(complex( Inf,-5.0)),complex(0.0,-pi/2))
@test isequal(atanh(complex( Inf, Inf)),complex(0.0, pi/2))
@test isequal(atanh(complex( Inf,-Inf)),complex(0.0,-pi/2))
@test isequal(atanh(complex( Inf, NaN)),complex(0.0, NaN))

@test isequal(atanh(complex(-Inf, 0.0)),complex(-0.0, pi/2))
@test isequal(atanh(complex(-Inf,-0.0)),complex(-0.0,-pi/2))
@test isequal(atanh(complex(-Inf, 5.0)),complex(-0.0, pi/2))
@test isequal(atanh(complex(-Inf,-5.0)),complex(-0.0,-pi/2))
@test isequal(atanh(complex(-Inf, Inf)),complex(-0.0, pi/2))
@test isequal(atanh(complex(-Inf,-Inf)),complex(-0.0,-pi/2))
@test isequal(atanh(complex(-Inf, NaN)),complex(-0.0, NaN))

@test isequal(atanh(complex( NaN, 0.0)),complex( NaN, NaN))
@test isequal(atanh(complex( NaN,-0.0)),complex( NaN, NaN))
@test isequal(atanh(complex( NaN, 5.0)),complex( NaN, NaN))
@test isequal(atanh(complex( NaN,-5.0)),complex( NaN, NaN))
@test isequal(atanh(complex( NaN, Inf)),complex( 0.0, pi/2))
@test isequal(atanh(complex( NaN,-Inf)),complex( 0.0,-pi/2))
@test isequal(atanh(complex( NaN, NaN)),complex( NaN, NaN))

# atan
#  atan(z) = -i*atanh(iz)

@test isequal(atan(complex( 0.0, 0.0)),complex( 0.0, 0.0))
@test isequal(atan(complex( 0.0,-0.0)),complex( 0.0,-0.0))
@test isequal(atan(complex( 0.0, 1.0)),complex( 0.0, Inf))
@test isequal(atan(complex( 0.0, Inf)),complex( pi/2, 0.0))
@test isequal(atan(complex( 0.0,-Inf)),complex( pi/2,-0.0))
@test isequal(atan(complex( 0.0, NaN)),complex( NaN, NaN))

@test isequal(atan(complex(-0.0, 0.0)),complex(-0.0, 0.0))
@test isequal(atan(complex(-0.0,-0.0)),complex(-0.0,-0.0))
@test isequal(atan(complex(-0.0, Inf)),complex(-pi/2, 0.0))
@test isequal(atan(complex(-0.0,-Inf)),complex(-pi/2,-0.0))
@test isequal(atan(complex(-0.0, NaN)),complex( NaN, NaN))

@test isequal(atan(complex( 5.0, Inf)),complex( pi/2, 0.0))
@test isequal(atan(complex( 5.0,-Inf)),complex( pi/2,-0.0))
@test isequal(atan(complex( 5.0, NaN)),complex( NaN, NaN))

@test isequal(atan(complex(-5.0, Inf)),complex(-pi/2, 0.0))
@test isequal(atan(complex(-5.0,-Inf)),complex(-pi/2,-0.0))
@test isequal(atan(complex(-5.0, NaN)),complex( NaN, NaN))

@test isequal(atan(complex( Inf, 0.0)),complex( pi/2, 0.0))
@test isequal(atan(complex( Inf,-0.0)),complex( pi/2,-0.0))
@test isequal(atan(complex( Inf, 5.0)),complex( pi/2, 0.0))
@test isequal(atan(complex( Inf,-5.0)),complex( pi/2,-0.0))
@test isequal(atan(complex( Inf, Inf)),complex( pi/2, 0.0))
@test isequal(atan(complex( Inf,-Inf)),complex( pi/2,-0.0))
@test isequal(atan(complex( Inf, NaN)),complex( pi/2, 0.0))

@test isequal(atan(complex(-Inf, 0.0)),complex(-pi/2, 0.0))
@test isequal(atan(complex(-Inf,-0.0)),complex(-pi/2,-0.0))
@test isequal(atan(complex(-Inf, 5.0)),complex(-pi/2, 0.0))
@test isequal(atan(complex(-Inf,-5.0)),complex(-pi/2,-0.0))
@test isequal(atan(complex(-Inf, Inf)),complex(-pi/2, 0.0))
@test isequal(atan(complex(-Inf,-Inf)),complex(-pi/2,-0.0))
@test isequal(atan(complex(-Inf, NaN)),complex(-pi/2, 0.0))

@test isequal(atan(complex( NaN, 0.0)),complex( NaN, 0.0))
@test isequal(atan(complex( NaN,-0.0)),complex( NaN,-0.0))
@test isequal(atan(complex( NaN, 5.0)),complex( NaN, NaN))
@test isequal(atan(complex( NaN,-5.0)),complex( NaN, NaN))
@test isequal(atan(complex( NaN, Inf)),complex( NaN, 0.0))
@test isequal(atan(complex( NaN,-Inf)),complex( NaN,-0.0))
@test isequal(atan(complex( NaN, NaN)),complex( NaN, NaN))

# lexcmp
@test lexcmp(1.0-1.0im, 1.0+0.0im) == -1
@test lexcmp(0.0+0.0im, 0.0+0.0im) == 0
@test lexcmp(1.0-1.0im, 0.0+0.0im) == 1

# misc.

@test complex(1//2,1//3)^2 === complex(5//36, 1//3)
@test complex(2,2)^2 === complex(0,8)
@test_throws complex(2,2)^(-2)
@test complex(2.0,2.0)^(-2) === complex(0.0, -0.125)

# robust division of Float64
# hard complex divisions from Fig 6 of arxiv.1210.4539
z7 = Complex{Float64}(3.898125604559113300e289, 8.174961907852353577e295)
z9 = Complex{Float64}(0.001953125, -0.001953125)
z10 = Complex{Float64}( 1.02951151789360578e-84, 6.97145987515076231e-220)
harddivs = ((1.0+im*1.0, 1.0+im*2^1023.0, 2^-1023.0-im*2^-1023.0), #1
      (1.0+im*1.0, 2^-1023.0+im*2^-1023.0, 2^1023.0+im*0.0), #2
      (2^1023.0+im*2^-1023.0, 2^677.0+im*2^-677.0, 2^346.0-im*2^-1008.0), #3
      (2^1023.0+im*2^1023.0, 1.0+im*1.0, 2^1023.0+im*0.0), #4
      (2^1020.0+im*2^-844., 2^656.0+im*2^-780.0, 2^364.0-im*2^-1072.0), #5
      (2^-71.0+im*2^1021., 2^1001.0+im*2^-323.0, 2^-1072.0+im*2^20.0), #6
      (2^-347.0+im*2^-54., 2^-1037.0+im*2^-1058.0, z7), #7
      (2^-1074.0+im*2^-1074., 2^-1073.0+im*2^-1074., 0.6+im*0.2), #8
      (2^1015.0+im*2^-989., 2^1023.0+im*2^1023.0, z9), #9
      (2^-622.0+im*2^-1071., 2^-343.0+im*2^-798.0, z10)#10
      )

# calculate "accurate bits" in range 0:53 by algorithm given in arxiv.1210.4539
function sb_accuracy(x,expected)
  min(logacc(real(x),real(expected)),
    logacc(imag(x),imag(expected)) )
end
relacc(x,expected) = abs(x-expected)/abs(expected)
function logacc(x::Float64,expected::Float64)
  x == expected && (return 53)
  expected == 0 && (return 0)
  (x == Inf || x == -Inf) && (return 0)
  isnan(x) && (return 0)
  ra = relacc(BigFloat(x),BigFloat(expected))
  max(ifloor(-log2(ra)),0)
end
# the robust division algorithm should have 53 or 52
# bits accuracy for each of the hard divisions
@test 52 <= minimum([sb_accuracy(h[1]/h[2],h[3]) for h in harddivs])

# division of non-Float64
function cdiv_test(a,b)
  c=convert(Complex{Float64},a)/convert(Complex{Float64},b)
  50 <= sb_accuracy(c,convert(Complex{Float64},a/b))
end
@test cdiv_test(complex(1//2, 3//4), complex(17//13, 4//5))
@test cdiv_test(complex(1,2), complex(8997,2432))

# inv
@test inv(1e300+0im) == 1e-300 - 0.0im


export build_many_body_op,
       build_sparse_many_body_op,
       build_spin1_many_body_op

"""
    build_many_body_op(L::Int, N::Int,
                       J::AbstractMatrix{T},
                       V::AbstractMatrix{T},
                       C::T=zero(T)) where T<:Number

Build many-body operator from the hopping matrix J with N particles.
"""
function build_many_body_op(L::Int, N::Int,
                            J::AbstractMatrix{T},
                            V::AbstractMatrix{T},
                            C::T=zero(T)) where T<:Number
    basis = make_LN_basis(L, N)
    return build_many_body_op(L, basis, J, V, C)
end

"""
    build_many_body_op(L::Int,
                       basis::Vector{Int},
                       J::AbstractMatrix{T},
                       V::AbstractMatrix{T},
                       C::T=zero(T)) where T<:Number

Build many-body operator from the hopping matrix J with a given basis of states.

# Arguments:
- L::Int: number of sites.
- basis::Vector{Int}: basis of Fock states for the mb operator.
- J::AbstractMatrix{T}: hopping matrix b^dagger_i*b_j.
- V::AbstractMatrix{T}: interaction matrix n_i*n_j.
- C::T=zero(T): constant term.
"""
function build_many_body_op(L::Int,
                            basis::Vector{Int},
                            J::AbstractMatrix{T},
                            V::AbstractMatrix{T},
                            C::T=zero(T)) where T<:Number
    # Dimension of the Hilbert space.
    dH = length(basis)

    # Use only upper triangular part of `V`.
    V = deepcopy(V)
    for i=1:L-1, j=i+1:L
        V[i, j] += V[j, i]
    end

    # Make the many-body operator of the same type as J.
    Op = zeros(T, (dH, dH))

    for s=1:dH
        state = basis[s]
        # Constant term.
        Op[s, s] += C

        # Diagonal terms.
        for i=0:L-1
            if (state>>i)&1 == 1
                Op[s, s] += J[i+1, i+1]
            end
            for j=i+1:L-1
                if !iszero(V[i+1, j+1]) && (state>>i)&1==1 && (state>>j)&1==1
                    Op[s, s] += V[i+1, j+1]
                end
            end
        end

        # Off-diagonal terms.
        for i=0:L-1
            for j=0:L-1
                if (!iszero(J[i+1, j+1])
                        && (state>>i)&1 == 0 && (state>>j)&1 == 1)
                    Jstate = state + 1<<i - 1<<j
                    js = searchsortedfirst(basis, Jstate)
                    Op[js, s] = J[i+1, j+1]
                end
            end
        end
    end
    return Op
end

"""
    build_sparse_many_body_op(L::Int,
                              basis::Vector{Int},
                              J::AbstractMatrix{T},
                              V::AbstractMatrix{T},
                              C::T=zero(T)) where {T<:Number}

Build sparse many-body operator from the matrix J with a N particles.

# Arguments:
- L::Int: number of sites.
- basis::Vector{Int}: basis of Fock states for the mb operator.
- J::AbstractMatrix{T}: hopping matrix b^dagger_i*b_j.
- V::AbstractMatrix{T}: interaction matrix n_i*n_j.
- C::T=zero(T): constant term.
"""
function build_sparse_many_body_op(L::Int,
                                   basis::Vector{Int},
                                   J::AbstractMatrix{T},
                                   V::AbstractMatrix{T},
                                   C::T=zero(T)) where {T<:Number}
    # Dimension of the Hilbert space.
    dH = length(basis)

    # Use only upper triangular part of `V`.
    V = deepcopy(V)
    for i=1:L-1, j=i+1:L
        V[i, j] += V[j, i]
    end

    # Number of non-zero off-diagonal elts in J.
    nnz_J = count(abs.(J) .> 1e-8) - count(abs.(diag(J)) .> 1e-8)
    # Number of nnz elts in Op due to off-diag terms and diag terms.
    nnz_in_Op = dH*(nnz_J + 1)
    rows = zeros(Int, nnz_in_Op)
    cols = zeros(Int, nnz_in_Op)
    vals = zeros(T, nnz_in_Op)

    cont = 1
    for s=1:dH
        state = basis[s]
        # Constant term.
        vals[cont] += C

        # Diagonal term.
        for i=0:L-1
            if (state>>i)&1 == 1
                vals[cont] += J[i+1, i+1]
            end
            for j=i+1:L-1
                if !iszero(V[i+1, j+1]) && (state>>i)&1==1 && (state>>j)&1==1
                    vals[cont] += V[i+1, j+1]
                end
            end
        end
        rows[cont] = s
        cols[cont] = s
        cont += 1

        # Off-diagonal terms.
        for i=0:L-1
            for j=0:L-1
                if (!iszero(J[i+1, j+1])
                        && (state>>i)&1 == 0 && (state>>j)&1 == 1)
                    Jstate = state + 1<<i - 1<<j
                    js = searchsortedfirst(basis, Jstate)
                    rows[cont] = js
                    cols[cont] = s
                    vals[cont] = J[i+1, j+1]
                    cont += 1
                end
            end
        end
    end
    # Remove zeros from rows, cols, vals.
    rows = rows[1:cont-1]
    cols = cols[1:cont-1]
    vals = vals[1:cont-1]
    Op = sparse(rows, cols, vals, dH, dH)
    return Op
end

"""
    build_spin1_many_body_op(L::Int,
                             Sz::Int,
                             J::AbstractMatrix{T},
                             W::AbstractMatrix{T},
                             Jz::Vector{T},
                             Jx::Vector{T},
                             C::T=zero(T)) where T<:Number

Build a spin 1 many-body operator from the hopping matrix J.
"""
function build_spin1_many_body_op(L::Int,
                                  Sz::Int,
                                  J::AbstractMatrix{T},
                                  W::AbstractMatrix{T},
                                  Jz::Vector{T},
                                  Jx::Vector{T},
                                  C::T=zero(T)) where T<:Number
    basis = make_spin1_LSz_basis(L, Sz)
    return build_spin1_many_body_op(L, basis, J, W, Jz, Jx, C)
end

"""
    build_spin1_many_body_op(L::Int,
                             basis::Vector{Int},
                             J::AbstractMatrix{T},
                             W::AbstractMatrix{T},
                             Jz::Vector{T},
                             Jx::Vector{T},
                             C::T=zero(T)) where T<:Number

Build a spin 1 many-body operator from the hopping matrix J.

# Arguments:
- L::Int: number of sites.
- basis::Vector{Int}: basis of Fock states for the mb operator.
- J::AbstractMatrix{T}: hopping matrix, ignore diagonal values S^+_i*S^-_j.
- W::AbstractMatrix{T}: interaction matrix S^z_i*S^z_j.
- Jz::Vector{T}: local Sz terms S^z_i.
- Jx::Vector{T}: local Sx terms S^x_i.
- C::T=zero(T): constant term.
"""
function build_spin1_many_body_op(L::Int,
                                  basis::Vector{Int},
                                  J::AbstractMatrix{T},
                                  W::AbstractMatrix{T},
                                  Jz::Vector{T},
                                  Jx::Vector{T},
                                  C::T=zero(T)) where T<:Number
    # Dimension of the Hilbert space.
    dH = length(basis)

    # Make the many-body operator of the same type as J.
    Op = zeros(T, (dH, dH))

    for s=1:dH
        state = basis[s]
        # Constant term.
        Op[s, s] += C

        # Sz and Sz^2 terms.
        for i=0:L-1
            Op[s, s] += Jz[i+1]*((state>>2i)&1 - (state>>(2i+1))&1)
            Op[s, s] += W[i+1, i+1]*((state>>2i)&1 + (state>>(2i+1))&1)
        end

        # S^z_i S^z_j terms, i < j.
        for j=1:L-1
            for i=0:j-1
                iszero(W[i+1, j+1]) && continue
                # Four possibilities to find the state. In positions i and j,
                # respectively: (+, +), (-, -), (-, +), (+, -).
                if (((state>>2i)&3 == 1 && (state>>2j)&3 == 1) ||
                    ((state>>2i)&3 == 2 && (state>>2j)&3 == 2))
                    Op[s, s] += W[i+1, j+1]
                elseif (((state>>2i)&3 == 2 && (state>>2j)&3 == 1) ||
                        ((state>>2i)&3 == 1 && (state>>2j)&3 == 2))
                    Op[s, s] -= W[i+1, j+1]
                end
            end
        end

        # S^+_i S^-_j, with i != j, terms.
        for i=0:L-1
            for j=0:L-1
                i == j && continue
                iszero(J[i+1, j+1]) && continue
                # Four possibilities to find the state. In positions i and j,
                # respectively: (0, +), (0, 0), (-, +), (-, 0).
                if (state>>2i)&3 == 0 && (state>>2j)&3 == 1
                    Jstate = state + 1<<2i - 1<<2j
                    js = searchsortedfirst(basis, Jstate)
                    Op[js, s] += 2*J[i+1, j+1]
                elseif (state>>2i)&3 == 0 && (state>>2j)&3 == 0
                    Jstate = state + 1<<2i + 2<<2j
                    js = searchsortedfirst(basis, Jstate)
                    Op[js, s] += 2*J[i+1, j+1]
                elseif (state>>2i)&3 == 2 && (state>>2j)&3 == 1
                    Jstate = state - 2<<2i - 1<<2j
                    js = searchsortedfirst(basis, Jstate)
                    Op[js, s] += 2*J[i+1, j+1]
                elseif (state>>2i)&3 == 2 && (state>>2j)&3 == 0
                    Jstate = state - 2<<2i + 2<<2j
                    js = searchsortedfirst(basis, Jstate)
                    Op[js, s] += 2*J[i+1, j+1]
                end
            end
        end
    end

    # Sx terms.
    if norm(Jx) > 1e-8
        for s=1:dH
            state = basis[s]

            # S^x_i terms.
            for i=0:L-1
                iszero(Jx[i+1]) && continue
                if (state>>2i)&3 == 0
                    # state_i = |1, 0>.
                    Jstate1 = state + 1<<2i # New state: |1, 1>.
                    js1 = searchsortedfirst(basis, Jstate1)
                    # If new state is in base.
                    if js1 <= dH && Jstate1 == basis[js1]
                        Op[js1, s] += Jx[i+1]/sqrt(2)
                    end

                    Jstate2 = state + 2<<2i # New state: |1, -1>.
                    js2 = searchsortedfirst(basis, Jstate2)
                    if js2 <= dH && Jstate2 == basis[js2]
                        Op[js2, s] += Jx[i+1]/sqrt(2)
                    end
                elseif (state>>2i)&3 == 1
                    # state_i = |1, 1>.
                    Jstate = state - 1<<2i # New state: |1, 0>.
                    js = searchsortedfirst(basis, Jstate)
                    if js <= dH && Jstate == basis[js]
                        Op[js, s] += Jx[i+1]/sqrt(2)
                    end
                elseif (state>>2i)&3 == 2
                    # state_i = |1, -1>.
                    Jstate = state - 2<<2i # New state: |1, 0>.
                    js = searchsortedfirst(basis, Jstate)
                    if js <= dH && Jstate == basis[js]
                        Op[js, s] += Jx[i+1]/sqrt(2)
                    end
                end
            end

        end
    end
    return Op
end


# slab_rhs!
using ClimaCore

struct ThermalSlabParameters# <: CLIMAParameters.AbstractEarthParameterSet{F}
    h::FT
    ρ::FT
    c::FT
    T_init::FT
end

# domain
function ShellDomain(; radius = 6371e3, Nel = 8, Nq = 2)
    domain = ClimaCore.Domains.SphereDomain(radius)
    mesh = ClimaCore.Meshes.EquiangularCubedSphere(domain, Nel)
    topology = ClimaCore.Topologies.Topology2D(mesh)
    quad = ClimaCore.Spaces.Quadratures.GLL{Nq}()
    space = ClimaCore.Spaces.SpectralElementSpace2D(topology, quad)
end

# init simulation
function slab_space_init(; radius = 6371e3, Nel = 8, Nq = 2)

    # construct domain spaces - get only surface layer (NB: z should be zero, not z = first central height)
    space = ShellDomain(radius = radius, Nel = Nel, Nq = Nq)
    coords = ClimaCore.Fields.coordinate_field(space)

    # initial condition
    T_sfc = map(coords) do coord
        T_sfc = 281.0 # close to the average of T_1 in atmos
        # x_rnge = xmax - xmin
        # T_sfc = 270.0 + 5.0 * coord.x / x_rnge
    end

    # prognostic variable
    Y = ClimaCore.Fields.FieldVector(T_sfc = T_sfc)

    return Y, space
end

# ode
function slab_rhs!(Y, dY, Ya, t)
    """
    Slab ocean:
    ∂_t T_sfc = F_sfc + G
    """
    p, F_sfc = Ya
    dT_sfc = dY.T_sfc

    dY.T_sfc .+= F_sfc ./ (p.h .* p.ρ .* p.c)
end

struct SlabSimulation{P, Y, D, I}
    params::P
    Y_init::Y
    domain::D
    integrator::I
end

function slab_init(
    ::Type{FT},
    tspan;
    stepper = Euler(),
    nelements = 6,
    npolynomial = 4,
    dt = 0.02,
    saveat = 1.0e10,
) where {FT}

    params = ThermalSlabParameters(0.5, 1500.0, 800.0, 260.0)

    Y, space = slab_space_init(Nel = nelements, Nq = npolynomial + 1)

    Ya = (params = params, F_sfc = ClimaCore.Fields.zeros(space), cp_d = [FT(0.0)]) #auxiliary
    problem = OrdinaryDiffEq.ODEProblem(slab_rhs!, Y, tspan, Ya)
    integrator = OrdinaryDiffEq.init(problem, stepper, dt = dt, saveat = saveat)

    simulation = SlabSimulation(params, Y, space, integrator)
end


#    Copyright (c) 2021 J.A. Duffek
#    Copyright (c) 2000 D.M. Spink
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, see <http://www.gnu.org/licenses/>.

"""
    bspeval(d::I,c::Matrix{F},k::Vector{F},u::Vector{F}
           )::Matrix{F} where {I<:Integer,F<:AbstractFloat}

Evaluate a univariate B-Spline.

# Arguments:
- `d`: degree of the B-Spline
- `c`: control points, matrix of size `(dim,nc)`
- `k`: knot sequence, vector of size `nk`
- `u`: parametric evaluation points, vector of size `nu`

# Output:
- `p`: Evaluated points, matrix of size `(dim,nu)`

# Examples:
```julia
julia> p = bspeval(d,c,k,u)
```

# Reference:
- Algorithm A3.1 from 'The NURBS BOOK' pg82 ([1997Piegl](@cite))
"""
function bspeval(d::I,c::Matrix{F},k::Vector{F},u::Vector{F}
                )::Matrix{F} where {I<:Integer,F<:AbstractFloat}
# TODO: the original C function passed c, k, u and p as reference
# TODO: this function has optimization potential

nu = length(u);
mc, nc = size(c);

# allocate space for evaluated points
p = zeros(F,(mc,nu));

# allocate space for the basis functions
N = zeros(F,d+1);

# for each parametric point
for col in 1:nu
  # find the span of u[col]
  s = findspan(nc-1,d,u[col],k);
  N = basisfun(s,u[col],d,k);

  tmp1 = s - d + 1;
  for row in 1:mc
    tmp2 = 0;
    for i in 0:d
      tmp2 += N[i+1] * c[row,tmp1+i];
    end # for i
    p[row,col] = tmp2;
  end # for row
end # for col
return p;
end # bspeval


# Copyright (c) 2021, Joseph M. Rutherford
# All rights reserved.
#
# Code provided under the license contained in the LICENSE file.
#
# This file depends upon sudoku_common.jl to be included first.

"""
    valid_subarray(rank,test)

Traverse an length rank*rank array of integers to report valid symbols 1:rank*rank.
Rank is required here because we could be getting Vector or Matrix arguments.
"""
function valid_subarray(rank::Integer,test::Array)
    if rank < 1 || Unsigned(rank) != rank
        throw(DomainError("Invalid rank"))
    end
    rank_squared = Unsigned(rank)*Unsigned(rank)
    if length(test) != rank_squared
        throw(DomainError("Array length mismatch to rank-squared"))
    end
    add_entries = Unsigned(sum(test)) == sum(1:rank_squared)
    multiply_entries = prod(test) == factorial(rank_squared)
    return add_entries && multiply_entries
end

"""
    valid_puzzle(puzzle)

Confirm that every row, column, and block of entries in 1:rank*rank.
"""
function valid_puzzle(puzzle::Matrix)
    rank=get_rank(puzzle)
    rank_squared = rank*rank
    result = true
    # use logical AND throughout loops
    for rowcol = 1:rank_squared
        # check full row and col
        if result
            result =
            valid_subarray(rank, puzzle[rowcol,:]) &&
            valid_subarray(rank, puzzle[:,rowcol])
        else
            break # don't bother checking more
        end
    end
    # Check rank x rank blocks
    for row_block = 1:rank
        if result
            start_row = rank*(row_block-1)+1
            stop_row = rank*(row_block-1)+rank
            for col_block = 1:rank
                if result
                    start_col = rank*(col_block-1)+1
                    stop_col = rank*(col_block-1)+rank
                    result = valid_subarray(rank, puzzle[start_row:stop_row,start_col:stop_col])
                else
                    break # don't bother checking more col blocks
                end
            end
        else
            break # don't bother checking more row or col blocks
        end
    end
    return result
end

"""
   satisfies(puzzle,solution)

Verify that a valid solution is a candidate for the puzzle.
"""
function satisfies(puzzle::Matrix,solution::Matrix)
    if size(solution) != size(puzzle)
        throw(DomainError("Solution cannot be checked against a different size puzzle"))
    end
    if !(valid_puzzle(solution))
        throw(DomainError("Invalid solution cannot be checked against a puzzle"))
    end
    satisfies = true
    for i = 1:length(puzzle)
        if puzzle[i] == 0
            continue
        elseif puzzle[i] != solution[i]
            satisfies = false
            break
        end
    end
    return satisfies
end


module Roots

if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@optlevel"))
    @eval Base.Experimental.@optlevel 1
end

using Printf
import CommonSolve
import CommonSolve: solve, solve!, init

export fzero,
       fzeros,
       secant_method

export find_zero, find_zero!, find_zeros,
       ZeroProblem, solve, solve!, init,
       Order0, Order1, Order2, Order5, Order8, Order16

export Bisection, FalsePosition

## load in files
include("utils.jl")
include("find_zero.jl")
include("bracketing.jl")
include("derivative_free.jl")
include("order0.jl")
include("simple.jl")
include("find_zeros.jl")
include("newton.jl")
include("lith.jl")
include("fzero.jl")

end


# This file is a part of SimilaritySearch.jl

export BinaryHammingDistance

"""
   BinaryHammingDistance()

Binary hamming uses bit wise operations to count the differences between bit strings
"""
struct BinaryHammingDistance <: PreMetric end

"""
    evaluate(::BinaryHammingDistance, a, b)
    evaluate(::BinaryHammingDistance, a::AbstractVector, b::AbstractVector) where {T<:Unsigned}

Computes the binary hamming distance for bit types and arrays of bit types
"""
function evaluate(::BinaryHammingDistance, a, b)
    d = 0
    @inbounds @simd for i in eachindex(a)
        d += count_ones(a[i] ⊻ b[i])
    end

    d
end

function evaluate(::BinaryHammingDistance, a::T, b::T)::Float64 where {T<:Unsigned}
    count_ones(a ⊻ b)
end


# SDE state

"""
Probabilistic SDE State
$FIELDS
"""
struct State{T<:Number}
  "mean z"
  μ::Vector{T}
  "covariance matrix"
  Σ::Matrix{T}
  function State(μ::Vector{T},Σ::Matrix{T}) where {T}
    p = length(μ)
    LinearAlgebra.checksquare(Σ)==p || error("Σ must be $p-by-$p")
    new{T}(μ,Σ)
  end
end

State(μ::Vector,Σ::Matrix) = State(promote(μ,Σ)...)

function Base.show(io::IO,ss::State{T}) where {T}
  elstr = if T<:Complex
    x->@sprintf("%0.3g+%0.3gim",real(x),imag(x))
  else
    x->@sprintf("%0.3g",x)
  end
  μs = elstr.(ss.μ)
  μstr = join(μs," ")
  Σstr = if iszero(ss.Σ)
    0
  else
    Σs = elstr.(ss.Σ)
    rows = join.(eachrow(Σs)," ")
    Σstr = join(rows,"; ")
    "["*Σstr*"]"
  end
  print(io,"μ = [",μstr,"] | Σ ",Σstr)
end

function Base.convert(::Type{State{T}},x::State) where {T}
  return State(convert(AbstractVector{T},x.μ),convert(AbstractMatrix{T},x.Σ))
end

Distributions.dim(state::State) = length(state.μ)

function Statistics.mean(states::Vector{<:State})
  μ = mean(getproperty.(states,:μ))
  Σ = mean(getproperty.(states,:Σ))
  return State(μ,Σ)
end

Base.rand(rng::AbstractRNG,state::State) = semiposdef_mvrand(rng,state.μ,state.Σ)

function State(Σ::AbstractMatrix{T}) where {T}
  p = size(Σ,1)
  μ = zeros(T,p)
  return State(μ,Σ)
end

function State(μ::AbstractVector{T}) where {T}
  p = length(μ)
  Σ = zeros(T,p,p)
  return State(μ,Σ)
end

import Base.:(==)
x::State==y::State = x.μ==y.μ && x.Σ==y.Σ

Base.hash(x::State, h::UInt) = hash(x.μ, hash(x.Σ, h))

Base.isapprox(x::State,y::State;kwargs...) = isapprox(x.μ,y.μ;kwargs...) && isapprox(x.Σ,y.Σ;kwargs...)

# """
# $SIGNATURES

# State of order `p`and compatible with data `y` and zero elsewhere
# """
# function compatiblestate(p,y)
#   z = similar(y,p)
#   z[:] .= 0
#   z[1] = y[1]
#   return State(z)
# end

# iscompatible(state::State,y) = !iszero(state.Σ) || state.μ[1]==y[1]

# function checkcompatible(state::State,y)
#   iscompatible(state,y) || error("state $state incompatible with data")
#   return state
# end

"
$(SIGNATURES)

Return deterministic state value. Throws an error if the state is not deterministic
"
function detstate(ss::State)
  if !iszero(ss.Σ)  error("Not a deterministic state")  end
  return ss.μ
end


# A semi-separable matrix is defined by
#
# S = triu(uvᵀ, u) + tril(pqᵀ, -bl),
#
# where u, bl >= 0. u and v are n × ru matrices and p and q are n × rl matrices.
# See [1] for the definition.

# [1] Chandrasekaran and Gu, Fast and stable algorithms for banded plus
# Semiseparable systems of linear equations, SIMAX, 25 (2003), pp. 373-384.

## Constructors

struct SemiseparableMatrix{T,LL<:AbstractMatrix{T},UU<:AbstractMatrix{T}} <: LayoutMatrix{T}
    L::LL
    U::UU
    l::Int
    u::Int

    function SemiseparableMatrix{T,LL,UU}(L::LL, U::UU, l::Integer, u::Integer) where {T,LL<:AbstractMatrix{T},UU<:AbstractMatrix{T}}
        Lm, Ln = size(L)
        Um, Un = size(U)
        Um == Un == Lm == Ln || throw(DimensionMismatch())
        (Un >= u && Lm >= l) || throw(ArgumentError("l and u should be compatible with dimensions"))
        -l < u || throw(ArgumentError("bands must not overlap"))
        new{T,LL,UU}(L, U, l, u)
    end
end

SemiseparableMatrix(A::LL, B::UU, l::Integer, u::Integer) where {T,LL<:AbstractMatrix{T},UU<:AbstractMatrix{T}} =
    SemiseparableMatrix{T,LL,UU}(A, B, l, u)

function convert(::Type{Matrix}, S::SemiseparableMatrix)
    return triu(Matrix(S.U), S.u) + tril(Matrix(S.L), -S.l)
end

size(S::SemiseparableMatrix) = size(S.L)

function getindex(S::SemiseparableMatrix{T}, k::Int, j::Int)  where T
    k-j ≥ S.l && return S.L[k,j]
    j-k ≥ S.u && return S.U[k,j]
    return zero(T)
end

# triangular

colsupport(L::SemiseparableMatrix{<:Any,<:Any,<:Zeros}, j) = minimum(j)+L.l:size(L,1)
colsupport(U::SemiseparableMatrix{<:Any,<:Zeros}, j) = 1:maximum(j)-U.u
rowsupport(L::SemiseparableMatrix{<:Any,<:Any,<:Zeros}, k) = 1:maximum(k)-L.l
rowsupport(U::SemiseparableMatrix{<:Any,<:Zeros}, k) = minimum(k)+U.u:size(U,2)


using BenchmarkTools
using Printf

"""
    evaluatefunctions(N)

Evaluates trigonometric functions on N values spaced
evenly between -1500.0 and 1500.0.
"""
function evaluatefunctions(N)
    #x = linspace(-1500.0, 1500.0, N)
    x = range(-1500.0, stop=1500.0, length=N)
    M = 10000
    for i in 1:M
        y = sin.(x)
        x = asin.(y)
        y = cos.(x)
        x = acos.(y)
        y = tan.(x)
        x = atan.(y)
    end
end

# Get the number of values from the command line.
N = parse(Int, ARGS[1])

println("-----------------------------------------")
println("Evaluate functions: ", N)
println("-----------------------------------------")
println(" ")

@btime evaluatefunctions(N)


import Base: *, ∈, isempty

export CartesianProduct,
       CartesianProduct!,
       swap

"""
    CartesianProduct{N, S1<:LazySet{N}, S2<:LazySet{N}} <: LazySet{N}

Type that represents the Cartesian product of two sets, that is the set

```math
Z = \\{ z ∈ \\mathbb{R}^{n + m} : z = (x, y),\\qquad x ∈ X, y ∈ Y \\}.
```
If ``X ⊆ \\mathbb{R}^n`` and ``Y ⊆ \\mathbb{R}^m``, then ``Z`` is ``n+m``-dimensional.

### Fields

- `X` -- first set
- `Y` -- second set

### Notes

The Cartesian product of three elements is obtained recursively.
See also [`CartesianProductArray`](@ref) for an implementation of a Cartesian product of
many sets without recursion, instead using an array, making the operations more efficient.

The `EmptySet` is the absorbing element for `CartesianProduct`.

The Cartesian product preserves convexity: if the set arguments are convex, then
their Cartesian product is convex as well.

In some docstrings the word "block" is used to denote each wrapped set, with the natural
order, i.e. we say that the first block of a Cartesian product `cp` is `cp.X` and
the second block is `cp.Y`.

### Examples

The Cartesian product between two sets `X` and `Y` can be constructed either
using `CartesianProduct(X, Y)` or the short-cut notation `X × Y` (to enter the times
symbol, write `\times[TAB]`).

```jldoctest cartesianproduct_constructor
julia> I1 = Interval(0, 1);

julia> I2 = Interval(2, 4);

julia> I12 = I1 × I2;

julia> typeof(I12)
CartesianProduct{Float64, Interval{Float64, IntervalArithmetic.Interval{Float64}}, Interval{Float64, IntervalArithmetic.Interval{Float64}}}
```
A hyperrectangle is the cartesian product of intervals, so we can convert `I12`
exactly to a `Hyperrectangle` type:

```jldoctest cartesianproduct_constructor
julia> convert(Hyperrectangle, I12)
Hyperrectangle{Float64, Vector{Float64}, Vector{Float64}}([0.5, 3.0], [0.5, 1.0])
```
"""
struct CartesianProduct{N, S1<:LazySet{N}, S2<:LazySet{N}} <: LazySet{N}
    X::S1
    Y::S2
    function CartesianProduct(X::LazySet{N}, Y::LazySet{N}) where {N}
        return new{N, typeof(X), typeof(Y)}(X, Y)
    end
end

isoperationtype(::Type{<:CartesianProduct}) = true
isconvextype(::Type{CartesianProduct{N, S1, S2}}) where {N, S1, S2} = isconvextype(S1) && isconvextype(S2)

# EmptySet is the absorbing element for CartesianProduct
@absorbing(CartesianProduct, EmptySet)

"""
```
    *(X::LazySet, Y::LazySet)
```

Alias for the binary Cartesian product.
"""
*(X::LazySet, Y::LazySet) = CartesianProduct(X, Y)

"""
    ×

Unicode alias constructor × (`times`) for the binary Cartesian product operator.

### Notes

Write `\\times[TAB]` to enter this symbol.
"""
×(X::LazySet, Y::LazySet) = CartesianProduct(X, Y)

"""
    swap(cp::CartesianProduct)

Return a new `CartesianProduct` object with the arguments swapped.

### Input

- `cp` -- Cartesian product of two sets

### Output

A new `CartesianProduct` object with the arguments swapped.
"""
function swap(cp::CartesianProduct)
    return CartesianProduct(cp.Y, cp.X)
end

"""
    dim(cp::CartesianProduct)

Return the dimension of a Cartesian product.

### Input

- `cp` -- Cartesian product

### Output

The ambient dimension of the Cartesian product.
"""
function dim(cp::CartesianProduct)
    return dim(cp.X) + dim(cp.Y)
end

"""
    σ(d::AbstractVector, cp::CartesianProduct)

Return the support vector of a Cartesian product.

### Input

- `d`  -- direction
- `cp` -- Cartesian product

### Output

The support vector in the given direction.
If the direction has norm zero, the result depends on the wrapped sets.
"""
function σ(d::AbstractVector, cp::CartesianProduct)
    n1 = dim(cp.X)
    return [σ(d[1:n1], cp.X); σ(d[n1+1:length(d)], cp.Y)]
end

"""
    ρ(d::AbstractVector, cp::CartesianProduct)

Return the support function of a Cartesian product.

### Input

- `d`  -- direction
- `cp` -- Cartesian product

### Output

The support function in the given direction.
If the direction has norm zero, the result depends on the wrapped sets.
"""
function ρ(d::AbstractVector, cp::CartesianProduct)
    n1 = dim(cp.X)
    return ρ(d[1:n1], cp.X) + ρ(d[n1+1:length(d)], cp.Y)
end

"""
    isbounded(cp::CartesianProduct)

Determine whether a Cartesian product is bounded.

### Input

- `cp` -- Cartesian product

### Output

`true` iff both wrapped sets are bounded.
"""
function isbounded(cp::CartesianProduct)
    return isbounded(cp.X) && isbounded(cp.Y)
end

"""
    ∈(x::AbstractVector, cp::CartesianProduct)

Check whether a given point is contained in a Cartesian product.

### Input

- `x`  -- point/vector
- `cp` -- Cartesian product

### Output

`true` iff ``x ∈ cp``.
"""
function ∈(x::AbstractVector, cp::CartesianProduct)
    @assert length(x) == dim(cp)

    n1 = dim(cp.X)
    return view(x, 1:n1) ∈ cp.X &&
           view(x, n1+1:length(x)) ∈ cp.Y
end

"""
    isempty(cp::CartesianProduct)

Return if a Cartesian product is empty or not.

### Input

- `cp` -- Cartesian product

### Output

`true` iff any of the sub-blocks is empty.
"""
function isempty(cp::CartesianProduct)
    return isempty(cp.X) || isempty(cp.Y)
end

"""
    constraints_list(cp::CartesianProduct)

Return the list of constraints of a (polyhedral) Cartesian product.

### Input

- `cp` -- Cartesian product

### Output

A list of constraints.
"""
function constraints_list(cp::CartesianProduct)
    return constraints_list(CartesianProductArray([cp.X, cp.Y]))
end

"""
    vertices_list(cp::CartesianProduct{N}) where {N}

Return the list of vertices of a (polytopic) Cartesian product.

### Input

- `cp` -- Cartesian product

### Output

A list of vertices.

### Algorithm

We assume that the underlying sets are polytopic.
Then the high-dimensional set of vertices is just the Cartesian product of the
low-dimensional sets of vertices.
"""
function vertices_list(cp::CartesianProduct{N}) where {N}
    # collect low-dimensional vertices lists
    vlist_low = (vertices_list(cp.X), vertices_list(cp.Y))

    # create high-dimensional vertices list
    vlist = Vector{Vector{N}}()
    m = length(vlist_low[1]) * length(vlist_low[2])
    sizehint!(vlist, m)
    for v1 in vlist_low[1]
        for v2 in vlist_low[2]
            push!(vlist, vcat(v1, v2))
        end
    end

    return vlist
end

"""
    linear_map(M::AbstractMatrix, cp::CartesianProduct)

Concrete linear map of a (polyhedral) Cartesian product.

### Input

- `M`  -- matrix
- `cp` -- Cartesian product of two sets

### Output

A polytope if `cp` is bounded and a polyhedron otherwise.

### Algorithm

We convert the Cartesian product to constraint representation and then call
`linear_map` on the corresponding polyhedron.

This is a fallback implementation and it will fail if the wrapped sets are not polyhedral.
"""
function linear_map(M::AbstractMatrix, cp::CartesianProduct)
    return linear_map_cartesian_product(M, cp)
end

function linear_map_cartesian_product(M, cp)
    @assert dim(cp) == size(M, 2) "a linear map of size $(size(M)) cannot " *
                                  "be applied to a set of dimension $(dim(cp))"

    # use constraint representation
    T = isbounded(cp) ? HPolytope : HPolyhedron
    P = T(constraints_list(cp))
    return linear_map(M, P)
end

function concretize(cp::CartesianProduct)
    return cartesian_product(concretize(cp.X), concretize(cp.Y))
end

function project(cp::CartesianProduct, block::AbstractVector{Int}; kwargs...)
    n1 = dim(cp.X)
    if block[end] <= n1
        # projection completely in the first block
        return project(cp.X, block; kwargs...)
    elseif block[1] > n1
        # projection completely in the second block
        return project(cp.Y, block .- n1; kwargs...)
    end
    # projection is a new Cartesian product of the block-wise projections
    for (i, bi) in enumerate(block)
        if bi > n1
            X = project(cp.X, block[1:i-1]; kwargs...)
            Y = project(cp.Y, block[i:end] .- n1; kwargs...)
            return CartesianProduct(X, Y)
        end
    end
end

"""
    project(cp::CartesianProduct{N, IT, HT}, block::AbstractVector{Int};
            [kwargs...]) where {N, IT<:Interval, HT<:AbstractHyperrectangle{N}}

Concrete projection of a Cartesian product between an interval and a hyperrectangle.

### Input

- `cp`       -- Cartesian product between an interval and a hyperrectangle
- `block`    -- block structure, a vector with the dimensions of interest

### Output

A hyperrectangle representing the projection of the cartesian product `cp` on the
dimensions specified by `block`.
"""
function project(cp::CartesianProduct{N, IT, HT}, block::AbstractVector{Int};
                 kwargs...) where {N, IT<:Interval, HT<:AbstractHyperrectangle{N}}
    I = cp.X
    H = cp.Y
    block_vec = collect(block)
    if 1 ∉ block_vec
        block_vec .-= 1
        cH = center(H)
        rH = radius_hyperrectangle(H)
    else
        cH = vcat(center(I), center(H))
        rH = vcat(radius_hyperrectangle(I), radius_hyperrectangle(H))
    end
    return Hyperrectangle(cH[block_vec], rH[block_vec], check_bounds=false)
end

"""
    project(cp::CartesianProduct{N, IT, ZT}, block::AbstractVector{Int};
            [kwargs...]) where {N, IT<:Interval, ZT<:AbstractZonotope{N}}

Concrete projection of the Cartesian product between an interval and a zonotopic set.

### Input

- `cp`       -- Cartesian product between an interval and a zonotopic set
- `block`    -- block structure, a vector with the dimensions of interest

### Output

A zonotope representing the projection of the cartesian product `cp` on the
dimensions specified by `block`.
"""
function project(cp::CartesianProduct{N, IT, ZT}, block::AbstractVector{Int};
                 kwargs...) where {N, IT<:Interval, ZT<:AbstractZonotope{N}}
    block_vec = collect(block)
    Z = cp.Y
    if 1 ∉ block_vec
        block_vec .-= 1
    else
        Z = convert(Zonotope, cp)
    end
    M = projection_matrix(block_vec, dim(Z), N)
    return linear_map(M, Z)
end

"""
    project(cp::CartesianProduct{N, IT, Union{VP1, VP2}},
            block::AbstractVector{Int};
            [kwargs...]) where {N, IT<:Interval, VP1<:VPolygon{N}, VP2<:VPolytope{N}}

Concrete projection of the Cartesian product between an interval and a set in vertex representation.

### Input

- `cp`       -- Cartesian product between an interval and a `VPolygon` or a `VPolytope`
- `block`    -- block structure, a vector with the dimensions of interest

### Output

A `VPolytope` representing the projection of the cartesian product `cp` on the
dimensions specified by `block`.
"""
function project(cp::CartesianProduct{N, IT, Union{VP1, VP2}},
                 block::AbstractVector{Int};
                 kwargs...) where {N, IT<:Interval, VP1<:VPolygon{N}, VP2<:VPolytope{N}}
    I = cp.X
    P = cp.Y
    block_vec = collect(block)
    if 1 ∉ block_vec
        Pout = project(P, block_vec .- 1; kwargs...)
    else
        out = cartesian_product(I, P)
        Pout = project(out, block_vec; kwargs...)
    end
    return Pout
end

function volume(cp::CartesianProduct)
    return volume(cp.X) * volume(cp.Y)
end


fn = normpath(joinpath(dirname(@__FILE__),"center_sizes.jld2"))
d = JLD2.jldopen(fn,"r")

tmp = d["ctrs"]
ctrs = [SVector(q...) for q in tmp]
rads = d["rads"]

tree = CD.Octree(ctrs, rads)

# extract all the triangles that (potentially) intersect octant (+,+,+)
pred(i) = all(ctrs[i].+rads[i] .> 0)
bb = SVector(0.5, 0.5, 0.5), 0.5
ids = collect(CD.searchtree(pred, tree, bb))
@test length(ids) == 178

N = 100
using DelimitedFiles
buf = readdlm(joinpath(@__DIR__,"assets","ctrs.csv"))
ctrs = vec(collect(reinterpret(SVector{3,Float64}, buf')))
rads = vec(readdlm(joinpath(@__DIR__,"assets","rads.csv")))

tree = CD.Octree(ctrs, rads)
pred(i) = all(ctrs[i] .+ rads[i] .> 0)
bb = @SVector[0.5, 0.5, 0.5], 0.5
ids = collect(CD.searchtree(pred, tree, bb))

@show ids
ids2 = findall(i -> all(ctrs[i].+rads[i] .> 0), 1:N)
@test length(ids2) == length(ids)
@test sort(ids2) == sort(ids)
@test ids == [26, 46, 54, 93, 34, 94, 75, 23, 86, 57, 44, 40, 67, 73, 77, 80]


using Base.Sort
using LinearAlgebra

@doc raw"""
This is a traditional cubic spline. It has N+1 points on the abcissa, ``\tau``.
It has N cubic interpolants in `c`, which is size 4 x N.
At each interval, the polynomial is in Horner Form,

``P_i(x) = c_{1,i} + c_{2,i}(x-x_i) + c_{3,i}(x-x_i)^2 + c_{4,i}(x-x_i)^3``

This spline has one type for the abcissa and one for the ordinate coefficients.
It should be the case that one(T) * one(X) is of type T.

This can be a spline of another order. It's just a matter of the dimensions
of the coefficient array, `c`.
"""
struct CubicSpline{X,T}
    τ::AbstractVector{X}
    c::AbstractMatrix{T}
end

"""
((cs.c[4, i] * Δ + cs.c[3, i]) * Δ + cs.c[2, i]) * Δ + cs.c[1, i]
"""
function horner_in_interval(cs::CubicSpline{X,T}, i, x) where {X,T}
    Δ = x - cs.τ[i]
    m = size(cs.c, 1)
    if m > 0
        total = cs.c[m, i]
        for d = (m - 1):-1:1
            total *= Δ
            total += cs.c[d, i]
        end
        total
    else
        zero(T)
    end
end

function (cs::CubicSpline)(x::A) where {A <: Real}
    # Index of first greater-than-or-equal-to x.
    i = Sort.searchsortedfirst(cs.τ, x) - 1
    # For points off the sides, use the closest polynomial.
    i = max(min(length(cs.τ) - 1, i), 1)
    horner_in_interval(cs, i, x)
end

function evaluate!(cs::CubicSpline, x::AbstractVector, y::AbstractVector)
    # Index of first greater-than-or-equal-to x.
    k = 1
    i = Sort.searchsortedfirst(cs.τ, x[k]) - 1
    i = max(min(length(cs.τ) - 1, i), 1)
    while k <= length(x)
        y[k] = horner_in_interval(cs, i, x[k])
        k += 1
        while i + 1 < length(cs.τ) && cs.τ[i + 1] < x[k]
            i += 1
        end
    end
end

"""
Integrate a spline from a knot at `i` to the value `x`.
# (((A(1/4)*cs.c[4, i] * Δ + A(1/3)*cs.c[3, i]) * Δ + A(1/2)*cs.c[2, i]) * Δ + cs.c[1, i])*Δ
"""
function integral_in_interval(cs::CubicSpline{X,T}, i, x::A) where {A <: Real, X, T}
    Δ = x - cs.τ[i]
    m = size(cs.c, 1)
    if m > 0
        total = zero(T)
        for d = m:-1:1
            total += cs.c[d, i] / T(d)
            total *= Δ
        end
        total
    else
        zero(T)
    end
end

"""
Integrate a spline from `x1` to `x2`.
"""
function integrate(cs::CubicSpline, x1::A, x2::A) where {A <: Real}
    # Index of first greater-than-or-equal-to x.
    i = Sort.searchsortedfirst(cs.τ, x1) - 1
    # For points off the sides, use the closest polynomial.
    i = max(min(length(cs.τ) - 1, i), 1)
    j = Sort.searchsortedfirst(cs.τ, x2) - 1
    # For points off the sides, use the closest polynomial.
    j = max(min(length(cs.τ) - 1, j), 1)
    intervening_intervals = zero(A)
    for k in i:(j-1)
        intervening_intervals += integral_in_interval(cs, k, cs.τ[k + 1])
    end
    intervening_intervals + integral_in_interval(cs, j, x2) - integral_in_interval(cs, i, x1)
end

"""
Given a spline, create a new spline that is its derivative.
"""
function derivative(cs::CubicSpline{X,T}) where {X,T}
    m = size(cs.c, 1)
    c2 = similar(cs.c[2:m, :])
    for i = 1:size(cs.c, 2)
        for d = 1:(m - 1)
            c2[d, i] = T(d) * cs.c[d + 1, i]
        end
    end
    CubicSpline{X,T}(cs.τ, c2)
end

"""
Multiply two splines of order `m1` and `m2` to get a spline of order `m1+m2`.
This only works if the two splines have the same abcissa.
"""
function Base.:*(cs1::CubicSpline{X,T}, cs2::CubicSpline{X,T}) where {X,T}
    m1 = size(cs1.c, 1)
    m2 = size(cs2.c, 1)
    c3 = zeros(T, m1 + m2 - 1, size(cs1.c, 2))
    for i in 1:size(cs1.c, 2)
        for d1 = 1:m1
            for d2 = 1:m2
                c3[d1 + d2 - 1, i] += cs1.c[d1, i] * cs2.c[d2, i]
            end
        end
    end
    CubicSpline{X, T}(cs1.τ, c3)
end

@doc raw"""
τ are the abcissa from 1 to N+1. f are the known values of length N+1.
fp_bounds are derivatives at the first and last endpoint.
fp is the output, of length N + 1.
This is from Conte and deBoor Sec. 4.8, Eq. 4.60. The equation is this, with ``i``
running from 2 to N.

``(\Delta x_i) s_{i-1}+2(\Delta x_{i-1}+\Delta x_i)s_i +(\Delta x_{i-1}s_{i+1}) =
3(f[x_{i-1},x_i]\Delta x_i+f[x_i,x_{i+1}]\Delta x_{i-1})``

Here, ``\Delta x_i = x_{i+1} -x{i}`` and

``f[x_i,x_{i+1}] = \frac{f[x_{i+1}] - f[x_i]}{x_{i+1}-x_i}``

"""
function global_derivatives!(τ, f::AbstractVector{T}, fp) where {T <: Real}
    # This implementation builds the tridiagonal matrix and then solves it.
    # You could write a Gaussian elimination to generate terms on the fly, saving memory.
    # There are N-1 equations.
    N = length(τ) - 1
    # lower, mid, and upper of tridiagonal matrix.
    dl = zeros(T, N - 2)  # indices from 3 to N
    d = zeros(T, N - 1) # indices from 2 to N
    du = zeros(T, N - 2) # indices from 2 to N-1
    rhs = zeros(T, N - 1) # indices from 2 to N

    for il = 3:N
        dl[il - 2] = τ[il + 1] - τ[il]  # \Delta x_i
    end
    for im = 2:N
        d[im - 1] = T(2) * (τ[im + 1] - τ[im - 1])  # 2(\Delta x_{i-1} + \Delta x_i)
    end
    for il = 2:(N - 1)
        du[il - 1] = τ[il] - τ[il - 1]  # \Delta x_{i-1}
    end
    for il = 2:N
        fleft = (f[il] - f[il - 1]) / (τ[il] - τ[il - 1])  # f[x_{i-1},x_i]
        fright = (f[il + 1] - f[il]) / (τ[il + 1] - τ[il])  # f[x_i,x_{i+1}]
        # 3(f[x_{i-1},x_i]\Delta x_i+f[x_i,x_{i+1}]\Delta x_{i-1})
        rhs[il - 1] = T(3) * (fleft * (τ[il + 1] - τ[il]) + fright * (τ[il] - τ[il - 1]))
    end
    # The known derivatives at the boundaries get moved from the left-hand side to the RHS.
    rhs[1] -= fp[1] * (τ[3] - τ[2])  # i = 2, (\Delta x_i) s_{i-1}
    rhs[N - 1] -= fp[end] * (τ[N] - τ[N - 1])  # i = N, \Delta x_{i-1} s_{i+1}
    tmat = Tridiagonal(dl, d, du)
    fpans = tmat \ rhs
    fp[2:(end - 1)] .= fpans
    nothing
end

"""
Given an abcissa, τ, of length N+1 values f of length N+1, and derivatives s
of length N+1, this computes the 4xN matrix c of cubic coefficients.
Conte and deBoor Eq. 4.55
"""
function cubic_spline_coefficients!(τ, f::AbstractVector{T}, s, c) where {T <: Real}
    for i = 1:(length(τ) - 1)
        c[1, i] = f[i]
        c[2, i] = s[i]
        Δ = τ[i + 1] - τ[i]
        dd = (f[i + 1] - f[i]) / Δ
        c4p = (s[i + 1] + s[i] - T(2) * dd) / Δ
        c[3, i] = (dd - s[i]) / Δ - c4p
        c[4, i] = c4p / Δ
    end
    nothing
end

"""
"Accurate Monotonicity Preserving Cubic Interpolation" by James M Hyman. 1983.
"""
function deboor_swartz_criterion(s::T, sm1, sp1) where {T <: Real}
    smin = min(sm1, sp1)
    smax = max(sm1, sp1)
    # Equation 2.3
    if zero(T) < smin
        s = min(max(zero(T), s), T(3) * smin)
    elseif smax < zero(T)
        s = max(min(zero(T), s), T(3) * smax)
    elseif sm1 * sp1 <= zero(T)
        s = zero(T)
    else
        s  # Data are locally monotone and within bounds.
    end
end

"""
This comes from Hyman's paper but is modified by the R implementation of splinefun,
which Simon Wood wrote. I didn't understand from the paper the exact structure of
decisions to take in the function.
"""
function hyman_criterion(s::T, sm1, sp1) where {T <: Real}
    # Equation 2.6 "extends" equation 2.3.
    # The paper doesn't specify setting s=sp1 _before_ the if-then about the sign of s.
    σ = s
    if sm1 * sp1 > 0
        σ = sp1  # The paper seems to say this should be set to zero.
    end
    if σ >= 0
        s = min(max(0, s), T(3) * min(abs(sm1), abs(sp1)))
    else
        s = max(min(0, s), -T(3) * min(abs(sm1), abs(sp1)))
    end
    s
end

"""
Uses Hyman piecewise monotonicity.
"""
function project_to_monotonicity!(x, f, s)
    s[1] = hyman_criterion(s[1], (f[2] - f[1])/(x[2] - x[1]), (f[2] - f[1])/(x[2] - x[1]))
    for i in 2:(length(s)-1)
        s[i] = hyman_criterion(
            s[i],
            (f[i] - f[i-1])/(x[i] - x[i-1]),
            (f[i+1] - f[i])/(x[i+1] - x[i])
            )
    end
    s[end] = hyman_criterion(
        s[end],
        (f[end] - f[end-1])/(x[end]-x[end-1]),
        (f[end] - f[end-1])/(x[end]-x[end-1])
        )
    nothing
end

"""
This is how splinefun sets the coefficients. Seems to have the same
result as the code above.
"""
function hyman_coefficients!(τ, f::AbstractVector{T}, s, c) where {T <: Real}
    for i = 1:(length(τ) - 1)
        c[1, i] = f[i]
        c[2, i] = s[i]
        if i < length(τ)
            Δ = τ[i + 1] - τ[i]
            y = -(f[i + 1] - f[i])
            c[3, i] = -(3y + (2s[i] + s[i+1])Δ) / Δ^2
            c[4, i] = (2y/Δ + s[i] + s[i+1])/Δ^2
        else
            # That's correct. This doesn't get called, but R's splinefun sets these.
            # Leaving here until I figure out why we need a polynomial defined for the
            # region to the right of the last input point.
            Δ = τ[i] - τ[i-1]
            y = -(f[i] - f[i-1])
            c[3, i] = (3y + (s[i-1] + 2s[i])Δ) / Δ^2
            c[4, i] = c[4, i-1]
        end
    end
    nothing
end

# Options for endpoints
struct ZeroDerivativeEndpoints
end

struct FlatEndpoints
end

# Options for monotonicity.
struct FreeSlope
end

function project_slope!(::FreeSlope, τ, f, s)
end

struct Monotonic
end

function project_slope!(::Monotonic, τ, f, s)
    project_to_monotonicity!(τ, f, s)
end

# Options for determination of derivatives
struct GlobalDerivatives
end

function cubic_spline(
    τ::AbstractVector{X}, f::AbstractVector{T}, fp; slope=FreeSlope()
    ) where {X <: Real, T <: Real}

    cubic_spline(τ, f, convert(typeof(f), fp); slope = slope)
end

function cubic_spline(
    τ::AbstractVector{X}, f::AbstractVector{T}, fp::AbstractVector{T};
    slope=FreeSlope()
    ) where {X <: Real, T <: Real}

    N = length(τ) - 1
    s = zeros(T, N + 1)
    s[1] = fp[1]
    s[end] = fp[2]
    global_derivatives!(τ, f, s)
    project_slope!(slope, τ, f, s)
    c = zeros(T, 4, N)
    cubic_spline_coefficients!(τ, f, s, c)
    CubicSpline{X,T}(τ, c)
end


depths = parse.(Int, readlines("input.txt"))

#! Part 1

function countLarger(vec)
    isLarger = a -> a > 0 ? 1 : 0;
    sum([isLarger(vec[j] - vec[i]) for i in 1:length(vec) for j = i+1 if j <= length(vec)])
end

countLarger(depths)

#! Part 2

# A (1) -> 1, 2, 3
# B (2) -> 2, 3, 4
# C (3) -> 3, 4, 5
# ⋮

giveIndices = window -> [window + 1, window + 2, window + 3]

countLarger([sum(depths[giveIndices(i)]) for i in 1:length(depths)-2 if max(giveIndices(i)...) <= length(depths)])


# TODO: Check usefulness/relevant size of neg_free
"""
	msfm()

	Calculate the shortest distance from a list of
	points to all other pixels in an image, using the
	Multistencil Fast Marching Method (MSFM).

	This method gives more accurate
	distances by using second order derivatives and cross neighbours.

	T=msfm2d(F, SourcePoints, UseSecond, UseCross)

	# inputs,
	  F: The speed image. The speed function must always be larger
				than zero (min value 1e-8), otherwise some regions will
				never be reached because the time will go to infinity.
	  SourcePoints : A list of starting points [2 x N] (distance zero)
	  UseSecond : Boolean Set to true if not only first but also second
	               order derivatives are used (default)
	  UseCross : Boolean Set to true if also cross neighbours
	               are used (default)
	# outputs
	  T : Image with distance from SourcePoints to all pixels

	# Literature
	  M. Sabry Hassouna et Al. Multistencils Fast Marching
	  Methods: A Highly Accurate Solution to the Eikonal Equation on
	  Cartesian Domains

	# Example
	  SourcePoint = [51; 51];
	  SpeedImage = ones([101 101]);
	  [X Y] = ndgrid(1:101, 1:101);
	  T1 = sqrt((X-SourcePoint(1)).^2 + (Y-SourcePoint(2)).^2);

	  # Run fast marching 1th order, 1th order multi stencil
	  # and 2th orde and 2th orde multi stencil

	  tic; T1_FMM1 = msfm2d(SpeedImage, SourcePoint, false, false); toc;
	  tic; T1_MSFM1 = msfm2d(SpeedImage, SourcePoint, false, true); toc;
	  tic; T1_FMM2 = msfm2d(SpeedImage, SourcePoint, true, false); toc;
	  tic; T1_MSFM2 = msfm2d(SpeedImage, SourcePoint, true, true); toc;

	  # Show results
	  fprintf('\nResults with T1 (Matlab)\n');
	  fprintf('Method   L1        L2        Linf\n');
	  Results = cellfun(@(x)([mean(abs(T1(:)-x(:))) mean((T1(:)-x(:)).^2) max(abs(T1(:)-x(:)))]), {T1_FMM1(:) T1_MSFM1(:) T1_FMM2(:) T1_MSFM2(:)}, 'UniformOutput',false);
	  fprintf('FMM1:   %9.5f %9.5f %9.5f\n', Results{1}(1), Results{1}(2), Results{1}(3));
	  fprintf('MSFM1:  %9.5f %9.5f %9.5f\n', Results{2}(1), Results{2}(2), Results{2}(3));
	  fprintf('FMM2:   %9.5f %9.5f %9.5f\n', Results{3}(1), Results{3}(2), Results{3}(3));
	  fprintf('MSFM2:  %9.5f %9.5f %9.5f\n', Results{4}(1), Results{4}(2), Results{4}(3));

	# Example multiple starting points,
	  SourcePoint=rand(2,100)*255+1;
	  SpeedImage = ones([256 256]);
	  tic; T1_MSFM2 = msfm2d(SpeedImage, SourcePoint, true, true); toc;
	  figure, imshow(T1_MSFM2,[]); colormap(hot(256));

	  # Credit
	  Function is written by D.Kroon University of Twente (June 2009)
	  Port from Matlab to Julia by Lars Hellemo (May 2014)
"""
function msfm(F::Array{Float64,2}, SourcePoints, usesecond::Bool, usecross::Bool,Ed=false::Bool)

	# Distance image, also used to store the index of narrowband pixels
	# during marching process
	T = fill(-1.0,size(F))
	# Augmented Fast Marching (For skeletonize)
	#Ed=false # Was: nargout>1;

	# Euclidian distance image
	if(Ed)
		Y = zeros(size(F))
	end

	# Pixels which are processed and have a final distance are frozen
	Frozen   = falses(size(F));

	# Free memory to store neighbours of the (segmented) region
	neg_free = 100000
	neg_pos=0
	if Ed
		neg_list = zeros(4,neg_free)
	else
		neg_list = zeros(3,neg_free)
	end

	# (There are 3 pixel classes:
	#   - frozen (processed)
	#   - narrow band (boundary) (in list to check for the next pixel with smallest distance)
	#   - far (not yet used)

	# Neighbours
	ne =[-1 0;
	1 0;
	0 -1;
	0 1];

	SourcePoints=round.(Int,floor.(SourcePoints))

	# set all starting points to distance zero and frozen
	for z=1:size(SourcePoints,2)
		# starting point
		x= SourcePoints[1,z]
		y= SourcePoints[2,z]
		# Set starting point to frozen and distance to zero
		Frozen[x,y]=true
		T[x,y]=0
	end

	# Add all neighbours of the starting points to narrow list
	for z=1:size(SourcePoints,2)
		# starting point
		x=SourcePoints[1,z]
		y=SourcePoints[2,z]
		for k=1:4
			# Location of neighbour
			i=x+ne[k,1]
			j=y+ne[k,2]
			# Check if current neighbour is not yet frozen and inside the
			# picture
			if((i>0)&&(j>0)&&(i<=size(F,1))&&(j<=size(F,2))&&(~Frozen[i,j]))
				Tt=1/max(F[i,j],eps())
				Ty=1;
				# Update distance in neigbour list or add to neigbour list
				if T[i,j]>0
					if neg_list[1,T[i,j]]>Tt
						neg_list[1,T[i,j]]=Tt
					end
					if Ed
						neg_list[4,T[i,j]]=min(Ty,neg_list[4,T[i,j]]);
					end
				else
					neg_pos=neg_pos+1
					# If running out of memory at a new block
					if neg_pos>neg_free
						neg_free = neg_free + 100000
						if Ed
							neg_list = hcat(neg_list,zeros(4,neg_free))# Was: neg_list(1,neg_free)=0;
						else
							neg_list = hcat(neg_list,zeros(3,neg_free))# Was: neg_list(1,neg_free)=0;
						end
					end
					if Ed
						neg_list[:,neg_pos]=[Tt;i;j;Ty]
					else
						neg_list[:,neg_pos]=[Tt;i;j]
					end
					T[i,j]=neg_pos
				end
			end
		end
	end

	# Loop through all pixels of the image
	for itt=1:length(F)
		# Get the pixel from narrow list (boundary list) with smallest
		# distance value and set it to current pixel location

		if neg_pos==0
			break
		end
		(t,index)=findmin(neg_list[1,1:neg_pos])
		x=round(Int,neg_list[2,index])
		y=round(Int,neg_list[3,index])
		Frozen[x,y]=true
		# T[x,y]=round(Int,neg_list[1,index])
		T[x,y]=neg_list[1,index]

		if Ed
			Y[x,y]=neg_list[4,index]
		end

		# Remove min value by replacing it with the last value in the array
		if index<neg_pos
			neg_list[:,index]=neg_list[:,neg_pos]
			x2=round(Int,neg_list[2,index])
			y2=round(Int,neg_list[3,index])
			T[x2,y2]=index
		end
		neg_pos =neg_pos-1

		# Loop through all 4 neighbours of current pixel
		for k=1:4
			# Location of neighbour
			i=x+ne[k,1]
			j=y+ne[k,2]

			# Check if current neighbour is not yet frozen and inside the
			# picture
			if (i>0)&&(j>0)&&(i<=size(F,1))&&(j<=size(F,2))&&(~Frozen[i,j])

				Tt=CalculateDistance(T,F[i,j],size(F),i,j,usesecond,usecross,Frozen)
				if Ed
					Ty=CalculateDistance(Y,1,size(F),i,j,usesecond,usecross,Frozen)
				end

				# Update distance in neigbour list or add to neigbour list
				if T[i,j]>0
					neg_list[1,round(Int,T[i,j])]=minimum([Tt,neg_list[1,round(Int,T[i,j])]])
					if Ed
						neg_list[4,round(Int,T[i,j])]=minimum([Ty,neg_list[4,round(Int,T[i,j])]])
					end
				else
					neg_pos=neg_pos+1
					# If running out of memory at a new block
					if neg_pos>neg_free
						neg_free = neg_free +100_000
						#Was: neg_list(1,neg_free)=0;
						if Ed
							neg_list = hcat(neg_list,zeros(4,neg_free))# Was: neg_list(1,neg_free)=0;
						else
							neg_list = hcat(neg_list,zeros(3,neg_free))# Was: neg_list(1,neg_free)=0;
						end
					end
					if Ed
						neg_list[:,neg_pos]=[Tt;i;j;Ty]
					else
						neg_list[:,neg_pos]=[Tt;i;j]
					end
					T[i,j]=neg_pos
				end
			end
		end
	end

	return T

end # End function msfm

function CalculateDistance(T,Fij,sizeF,i,j,usesecond,usecross,Frozen)
	#  Tpatch=infs(5,5)
	Tpatch = fill(Inf,(5,5))
	for nx = -2:2
		for ny = -2:2
			i_n = i + nx
			j_n = j + ny
			if i_n>0 && j_n>0 && i_n<=sizeF[1] && j_n<=sizeF[2] && Frozen[i_n,j_n]
				Tpatch[nx+3,ny+3] = T[i_n,j_n]
			end
		end
	end

	# The values in order is 0 if no neighbours in that direction
	# 1 if 1e order derivatives is used and 2 if second order
	# derivatives are used
	Order=zeros(Int,1,4);

	# Make 1e order derivatives in x and y direction
	Tm=zeros(1,4)
	Tm[1] = min( Tpatch[2,3] , Tpatch[4,3] )
	if isfinite(Tm[1])
		Order[1]=1
	end

	Tm[2] = min( Tpatch[3,2] , Tpatch[3,4])
	if isfinite(Tm[2])
		Order[2]=1
	end

	# Make 1e order derivatives in cross directions
	if usecross
		Tm[3] = minimum( [Tpatch[2,2] , Tpatch[4,4]])
		if isfinite(Tm[3])
			Order[3]=1
		end
		Tm[4] = min( Tpatch[2,4] , Tpatch[4,2])
		if isfinite(Tm[4])
			Order[4]=1
		end
	end

	# Make 2e order derivatives
	if usesecond
		Tm2=zeros(1,4)
		# pixels with a pixeldistance 2 from the center must be
		# lower in value otherwise use other side or first order
		ch1 = (Tpatch[1,3]<Tpatch[2,3])&&isfinite(Tpatch[2,3])
		ch2 = (Tpatch[5,3]<Tpatch[4,3])&&isfinite(Tpatch[4,3])

		if ch1&&ch2
			Tm2[1] =minimum([ (4*Tpatch[2,3]-Tpatch[1,3])/3 , (4*Tpatch[4,3]-Tpatch[5,3])/3 ])
			Order[1]=2
		elseif ch1
			Tm2[1] =(4*Tpatch[2,3]-Tpatch[1,3])/3
			Order[1]=2
		elseif ch2
			Tm2[1] =(4*Tpatch[4,3]-Tpatch[5,3])/3
			Order[1] = 2
		end

		ch1 = (Tpatch[3,1]<Tpatch[3,2])&&isfinite(Tpatch[3,2])
		ch2 = (Tpatch[3,5]<Tpatch[3,4])&&isfinite(Tpatch[3,4])

		if ch1&&ch2
			Tm2[2] = min( (4*Tpatch[3,2]-Tpatch[3,1])/3 , (4*Tpatch[3,4]-Tpatch[3,5])/3)
			Order[2]=2
		elseif ch1
			Tm2[2]=(4*Tpatch[3,2]-Tpatch[3,1])/3
			Order[2]=2
		elseif ch2
			Tm2[2]=(4*Tpatch[3,4]-Tpatch[3,5])/3
			Order[2]=2
		end

		if usecross
			ch1 = (Tpatch[1,1]<Tpatch[2,2])&&isfinite(Tpatch[2,2])
			ch2 = (Tpatch[5,5]<Tpatch[4,4])&&isfinite(Tpatch[4,4])
			if ch1&&ch2
				Tm2[3] =min( (4*Tpatch[2,2]-Tpatch[1,1])/3 , (4*Tpatch[4,4]-Tpatch[5,5])/3)
				Order[3]=2
			elseif ch1
				Tm2[3]=(4*Tpatch[2,2]-Tpatch[1,1])/3
				Order[3]=2
			elseif ch2
				Tm2[3]=(4*Tpatch[4,4]-Tpatch[5,5])/3
				Order[3]=2
			end

			ch1 = (Tpatch[1,5]<Tpatch[2,4])&&isfinite(Tpatch[2,4])
			ch2 = (Tpatch[5,1]<Tpatch[4,2])&&isfinite(Tpatch[4,2])
			if ch1&&ch2
				Tm2[4] = minimum([ (4*Tpatch[2,4]-Tpatch[1,5])/3 , (4*Tpatch[4,2]-Tpatch[5,1])/3])
				Order[4]=2
			elseif ch1
				Tm2[4]=(4*Tpatch[2,4]-Tpatch[1,5])/3
				Order[4]=2
			elseif ch2
				Tm2[4] = (4*Tpatch[4,2]-Tpatch[5,1])/3
				Order[4]=2
			end
		end
	else
		Tm2=zeros(1,4)
	end

	# Calculate the distance using x and y direction
	Coeff = [0 0 -1/(max(Fij^2,eps()))]
	for t=1:2
		if Order[t] == 1
			Coeff=Coeff+[1 -2*Tm[t] Tm[t]^2]
		elseif Order[t] ==2
			Coeff=Coeff+[1 -2*Tm2[t] Tm2[t]^2]*(2.2500)
		end
	end

	Tt=roots(Coeff)
	Tt=maximum(Tt)
	# Calculate the distance using the cross directions
	if usecross
		Coeff = Coeff + [0 0 -1/(maximum([Fij^2,eps()]))]
		for t=3:4
			if Order[t] == 1
				Coeff=Coeff+0.5*[1 -2*Tm[t] Tm[t]^2]
			elseif Order[t] == 2
				Coeff=Coeff+0.5*[1 -2*Tm2[t] Tm2[t]^2]*(2.2500)
			end
		end
		Tt2=roots(Coeff)
		Tt2=maximum(Tt2)
		# Select minimum distance value of both stensils
		if ~isempty(Tt2)
			Tt=minimum([Tt,Tt2])
		end
	end

	# Upwind condition check, current distance must be larger
	# then direct neighbours used in solution
	#DirectNeigbInSol=Tm(isfinite(Tm));
	#if(nnz(DirectNeigbInSol>=Tt)>0) # Will this ever happen?
	#    Tt=min(DirectNeigbInSol)+(1/(max(Fij,eps)));
	#end

end

function roots(Coeff)
	a=Coeff[1]
	b=Coeff[2]
	c=Coeff[3]
	d=maximum([(b*b)-4.0*a*c,0])
	z = zeros(2,1)
	if a != 0
		z[1]= (-b - sqrt(d)) / (2.0*a)
		z[2]= (-b + sqrt(d)) / (2.0*a)
	else
		z[1]= (2.0*c)/(-b - sqrt(d))
		z[2]= (2.0*c)/(-b + sqrt(d))
	end
end

function run_test()
	#	 include("ndgrid.jl")

	# Example/test
	#SourcePoint = [51; 51]
	#SpeedImage = ones(101, 101)
	#XY = ndgrid(1:101, 1:101)
	#T1 = sqrt((XY[1]-SourcePoint[1]).^2 + (XY[2]-SourcePoint[2]).^2)
	#tic()
	#T1_MSFM2 = msfm2d(SpeedImage, SourcePoint, true, true)
	#toc()

	# Example multiple starting points,
	SourcePoint=rand(2,100)*255+1
	SpeedImage = ones(256, 256)
	tic()
	T1_MSFM2 = msfm(SpeedImage, SourcePoint, true, true)
	toc()
	writecsv("tmp.csv",T1_MSFM2)
end

#    run_test()


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Description
# ==============================================================================
#
#   Functions related to the conversion from small Euler angles to quaternion.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

export smallangle_to_quat

"""
    smallangle_to_quat(θx::Number, θy::Number, θz::Number)

Create a quaternion from three small rotations of angles `θx`, `θy`, and `θz`
[rad] about the axes X, Y, and Z, respectively.

!!! note
    The quaternion is always normalized.

# Example

```jldoctest
julia> smallangle_to_quat(+0.01, -0.01, +0.01)
Quaternion{Float64}:
  + 0.999963 + 0.00499981⋅i - 0.00499981⋅j - 0.00499981⋅k
```
"""
function smallangle_to_quat(θx::Number, θy::Number, θz::Number)
    q0     = 1
    q1     = θx / 2
    q2     = θy / 2
    q3     = θz / 2
    norm_q = √(q0^2 + q1^2 + q2^2 + q3^2)

    return Quaternion(q0 / norm_q, q1 / norm_q, q2 / norm_q, q3 / norm_q)
end