import NumDiffTools
norm(x) = maximum(abs.(x))
using Test
using ForwardDiff

@info "Checking derivative(f,x) of scalar function"
err = NumDiffTools.derivative(sin, 2.0) - cos(2.0)
@test norm(err) < 1e-12

@info "Checking derivative(f,x) of vector function"
f(x) = [sin(x), cos(x)]
f!(y,x) = begin
    y[1] = sin(x)
    y[2] = cos(x)
    nothing
end
df(x) = [cos(x), -sin(x)]
err = NumDiffTools.derivative(f, 2.0) - df(2.0)
@test norm(err) < 1e-12

@info "Checking derivative(f!, y, x)"
y = zeros(2)
err =  NumDiffTools.derivative(f!, y, 2.0) - df(2.0)
@test norm(err) < 1e-12

@info "Checking derivative!(result, f, x)"
result = zeros(2)
NumDiffTools.derivative!(result, f, 2.0)
err = result - df(2)
@test norm(err) < 1e-12

@info "Checking derivative!(result, f, y, x)"
NumDiffTools.derivative!(result,f!, y, 2.0)
err = result - df(2.0)
@test norm(err) < 1e-12

@info "Checking gradient(f,x)"
f(x) = prod(cos.(x).^2 - sin.(x).^2)
gradf(x) = [ - 4*cos(x[1])*sin(x[1])*(cos(x[2])^2 - sin(x[2])^2),
             - 4*cos(x[2])*sin(x[2])*(cos(x[1])^2 - sin(x[1])^2)
             ]
err = NumDiffTools.gradient(f, [1.5, 2.5]) - gradf([1.5, 2.5])
@test norm(err) < 1e-12

@info "Checking gradient!(result, f, x)"
NumDiffTools.gradient!(result, f, [1.5, 2.5])
err = result - gradf([1.5, 2.5])
@test norm(err) < 1e-12

@info "checking jacobian(f, x)"
f(x) = [ prod(cos.(x).^2 - sin.(x).^2),
         sum(cos.(x).^2 - sin.(x).^2)]
f!(y, x) = begin
    y .= f(x)
    nothing
end
J(x) = [-4*cos(x[1])*sin(x[1])*(cos(x[2])^2 - sin(x[2])^2)  -4*cos(x[2])*sin(x[2])*(cos(x[1])^2 - sin(x[1])^2);
        (-2*cos(x[1])*sin(x[1]) - 2*sin(x[1])*cos(x[1]))  (-2*cos(x[2])*sin(x[2]) - 2*sin(x[2])*cos(x[2]))
        ]
err = NumDiffTools.jacobian(f, [1.5, 2.5]) - J([1.5, 2.5])
@test norm(err) < 1e-12

@info "checking jacobian(f!, y, x)"
err = NumDiffTools.jacobian(f!, y, [1.5, 2.5]) - J([1.5, 2.5])
@test norm(err) < 1e-12

@info "checking jacobian!(result, f, x)"
result = zeros(2,2)
NumDiffTools.jacobian!(result, f, [1.5, 2.5])
err = result - J([1.5, 2.5])
@test norm(err) < 1e-12

@info "Checking jacobian!(result, f!, y, x)"
result = zeros(2,2)
NumDiffTools.jacobian!(result, f!, y, [1.5, 2.5])
err = result - J([1.5, 2.5])
@test norm(err) < 1e-12

@info "Checking hessian(f, x) for scalar functions of real numbers"
err = NumDiffTools.hessian(sin, 1.0) + sin(1.0)
@test norm(err) < 1e-10

@info "checkking hessian(f, x) for scalar functions of vectors"
f(x) = prod(cos.(x).^2 - sin.(x).^2)

err = NumDiffTools.hessian(f, [1.5, 2.5]) - ForwardDiff.hessian(f, [1.5, 2.5])
@test norm(err) < 1e-10

@info "Checking hessian!(result, f, x)"
result = zeros(2,2)
NumDiffTools.hessian!(result, f, [1.5, 2.5])
err = result - ForwardDiff.hessian(f, [1.5, 2.5])
@test norm(err) < 1e-10


N = 100
h = 1.0/N
y = zeros(N)
y[1] = 0
t = linrange(0,2,N)

function func(t,x)
    return x^(1/3.0)
end

for i=1:N-1
    y[i+1] = y[i] + h*func(t[i], y[i])
end
println("y", "  ", "y(t)", "  ", "Error")
for i=1:N
    println(t[i]^(3/2.0), "  ", y[i], " ", abs(t[i]^1.5-y[i])/t[i]^1.5)
end


using LinearAlgebra
using Plots

include("cheb.jl")
include("parameters.jl")
include("geometry.jl")
include("profile.jl")
include("build_A.jl")
include("compute_spectrum.jl")
include("mesh.jl")
include("plot_fields.jl")

Omega = 2*π/(24*3600)
lat   = π/4

params=parameters(
     H = 2e3,
    Ly = 600e3,
    f₀ = 2*Omega*cos(lat),
     g = 10,
    Lj = 60e3,
    Uj = 1.0,
    Ny = 200,
    dk = 5e-2,
    kₘ = 2e0
    );

params.Fr = (params.f₀ * params.Lj)^2/(params.g * params.H);
params.Ro =  params.Uj/(params.f₀ * params.Lj)

print("\n")
print("Linear-Stability-Caluculator\n")
print("============================\n\n")
print("Repo:      Linear-Stability-Calculators\n")
print("Code:      ShallowWater/Julia/linear_stability_shallow_water.jl\n")
print("Model:     Rotating Shallow Water\n")
print("Geometry:  Cartesian\n")
print("Structure: Bickley Jet\n")
print("\n")
print("Nondimensional Parameters\n")
print("=========================\n")
print("Fr = ", params.Fr, "\n")
print("Ro = ", params.Ro, "\n")

# Wavenumbers
ks = collect(params.dk:params.dk:params.kₘ) / params.Lj;
Nk = length(ks);

# Grid and basic state
Dy, Dy2, y = geometry(params);
U,  E      = profile(         y, params);

plot_basic_state(y, U, E, params.Ly, "basic_state.png")

# initialize fields to store
Nmodes = 2
     σ = zeros(Nmodes, Nk);
     ω = zeros(Nmodes, Nk);
σmodes = zeros(ComplexF64, 3*params.Ny+1, Nmodes, Nk);

# Compute growth rates
for cnt in 1:Nk
    local k = ks[cnt]
    local A = build_A(k, U, E, Dy, y, params);
    local σ[:,cnt], ω[:,cnt], σmodes[:,:,cnt] = compute_spectrum(A, k, params, Nmodes);
end

plot_growth_rates(ks, σ, Nmodes, "growth_rates_Cartesian_Bickley_jet.png")

mode_number = 1;
      σ_max = maximum(σ[mode_number,:]);
    k_index = sortperm(σ[mode_number,:],rev=true)[1];
          k = ks[k_index]; # pick wavenumber

plot_1D_streamfunction(k_index, k, y, σmodes, mode_number, "modes_1D_streamfunction.png")
plot_2D_streamfunction(k_index, k, y, σmodes, mode_number, "modes_2D_streamfunction.png")
plot_2D_vorticity( Dy, k_index, k, y, σmodes, mode_number, "modes_2D_vorticity.png")


using QIntegrations, Test

@testset "QIntegrations" begin

#@test scipy_quad((x)->x,0,1)[1] == 0.5
res = integrate_1d((x)->exp(-2*x),0,1)
@test isapprox(res[1], (1-exp(-2))/2, atol=1e-8, rtol=1e-6)
res = integrate_1d((x)->exp(-2*x),0,Inf)
@test isapprox(res[1], 0.5, atol=1e-8, rtol=1e-6)
res = integrate_1d((x)->exp(-x^2),-Inf,Inf)
@test isapprox(res[1], sqrt(pi), atol=1e-8, rtol=1e-6)

end


export  w_legendre,
        build_w_jacobi,
        w_jacobi,
        w_laguerre,
        w_hermite,
        build_w_genhermite,
        build_w_genlaguerre,
        w_meixner_pollaczek,
        build_w_meixner_pollaczek,

        w_gaussian,
        w_uniform01,
        w_uniform_11,
        w_logistic,
        w_genhermite,
        build_w_beta,
        build_w_gamma

_throwError(t) = throw(DomainError(t, "not in support"))

function w_legendre(t)
    -1. <= t <= 1. ? 1. : _throwError(t)
end

function build_w_jacobi(a,b)
    return t->w_jacobi(t,a,b)
end

function w_jacobi(t,a,b)
    -1. <= t <= 1. ? (1-t)^a*(1+t)^b : _throwError(t)
end

function w_hermite(t)
    exp(-t^2)
end

function build_w_genhermite(mu)
    return t->w_genhermite(t,mu)
end

function w_genhermite(t,μ)
    abs(t)^(2*μ)*exp(-t^2)
end

function build_w_genlaguerre(a)
    return t -> w_genlaguerre(t,a)
end

function w_laguerre(t)
    t >= 0. ? exp(-t) : _throwError(t)
end

function w_meixner_pollaczek(t,lambda,phi)
    1 / (2pi) * exp((2*phi - pi)*t) * abs(gamma(lambda+im*t))^2
end

function build_w_meixner_pollaczek(lambda,phi)
    t->w_meixner_pollaczek(t,lambda,phi)
end

##################################################
# probability density functions
function w_gaussian(t)
    1 / (sqrt(2*pi))*exp(-0.5*t^2)
end

function build_w_beta(α,β)
    return t->w_beta(t,α,β)
end

function w_beta(t,α,β)
    -1 <= t <= 1 ? t^(α-1)*(1-t)^(β-1)/beta(α,β) : _throwError(t)
end

function build_w_gamma(α)
    return t->w_gamma(t,α)
end

function w_gamma(t,α)
    t >= 0. ? (1/gamma(α)*Float64(t)^(α-1)*exp(-t)) : _throwError(t)
end

function w_uniform01(t)
    0. <= t <= 1. ? 1. : _throwError(t)
end

function w_uniform_11(t)
    -1. <= t <= 1. ? 0.5 : _throwError(t)
end

function w_logistic(t)
    0.25*sech(0.5t)^2
end


module WeakDivPerpTests
   using Gridap
   using GridapGeosciences
   using Plots
   using Test

   include("ConvergenceAnalysisTools.jl")

   # Nondivergent velocity field tangent to the sphere
   # Reference:
   #     Lauritzen et. al. GMD 2012
   #     Eq. (18) and (19)
   function W(θϕ)
     θ,ϕ = θϕ
     u = 2*sin(θ)^2*sin(2*ϕ) + 2π/5*cos(ϕ)
     v = 2*sin(2*θ)*cos(ϕ)
     spherical_to_cartesian_matrix(VectorValue(θ,ϕ,1.0))⋅VectorValue(u,v,0)
   end

   """
   Spherical divergence on unit sphere applied to perp(W)
   """
   function divM_perpW(θϕ)
     n_times_W=(θϕ)->(W(θϕ)×normal_unit_sphere(θϕ))
     f=divergence_unit_sphere(n_times_W)
     f(θϕ)
   end

   function compute_error_weak_div_perp(model,order,degree)
     # Setup geometry
     Ω=Triangulation(model)
     dΩ=Measure(Ω,degree)
     n=get_normal_vector(model)

     # Setup H(div) spaces
     reffe_rt = ReferenceFE(raviart_thomas, Float64, order)
     V = FESpace(model, reffe_rt ; conformity=:HDiv)
     U = TrialFESpace(V)

     # H1 spaces
     reffe_lgn = ReferenceFE(lagrangian, Float64, order+1)
     S = FESpace(model, reffe_lgn; conformity=:H1)
     R = TrialFESpace(S)

     # Project the analytical velocity field W onto the H(div) space
     a1(u,v) = ∫(v⋅u)dΩ
     b1(v)   = ∫(v⋅(W∘xyz2θϕ))dΩ
     op      = AffineFEOperator(a1,b1,U,V)
     wh      = solve(op)

     e = (W∘xyz2θϕ)-wh
     println(sqrt(sum(∫(e⋅e)dΩ)))

     # Compute weak div_perp operator
     a2(u,v) = ∫(v*u)dΩ
     b2(v)   = ∫(⟂(∇(v),n)⋅(-wh) )dΩ
     op      = AffineFEOperator(a2,b2,R,S)
     divwh   = solve(op)
     e = divwh-divM_perpW∘xyz2θϕ

     sqrt(sum(∫(e*e)dΩ))
   end
   @time ahs0,ak0errors,as0=convergence_study(compute_error_weak_div_perp,generate_n_values(2),0,4)
   @test round(as0,digits=3) ≈ 2.084

   @time bihs0,bik0errors,bis0=convergence_study(compute_error_weak_div_perp,generate_n_values(2),1,0,4)
   @test round(bis0,digits=3) ≈ 0.947

   @time biqhs0,biqk0errors,biqs0=convergence_study(compute_error_weak_div_perp,generate_n_values(2),2,0,4)
   @test round(biqs0,digits=3) ≈ 2.083

  #  plotd=plot([ahs0,bihs0,biqhs0],[ak0errors,bik0errors,biqk0errors],
  #  xaxis=:log, yaxis=:log,
  #  label=["k=0 analytical map" "k=0 bilinear map" "k=0 biquadratic map"],
  #  shape=:auto,
  #  xlabel="h",ylabel="L2 error norm", legend=:bottomright)

  #  savefig(plotd,"L2_error_weak_div_perp.png")

end


function linreg_loss(w, b, x, y)
    yhat = sum(w .* x) + b
    loss = sum(abs2(yhat - y))
end

inputs = [:w => [1.0, 1.0], :b => 2.0, :x => [3.0, 3.0], :y => 4.0]
vals = ([v for (k,v) in inputs]...)
dlinreg_loss = xdiff(linreg_loss; inputs...)
dvals = dlinreg_loss(vals...)
@test dvals == (16.0, [96.0, 96.0], 32.0, [32.0, 32.0], -32.0)

# ReverseDiff doesn't support scalars yet
# See: https://github.com/JuliaDiff/ReverseDiff.jl/issues/40
# linreg_loss2(W, b, x, y) = sum(abs2.(y - (W * x .+ b)))
# test_compare(linreg_loss2; W=randn(1,11), b = rand(), x=randn(11,1000), y=randn(1,1000))


# ---
# title: 168. Excel Sheet Column Title
# id: problem168
# author: Tian Jun
# date: 2020-10-31
# difficulty: Easy
# categories: Math
# link: <https://leetcode.com/problems/excel-sheet-column-title/description/>
# hidden: true
# ---
#
# Given a positive integer, return its corresponding column title as appear in
# an Excel sheet.
#
# For example:
#
#
#
#         1 -> A
#         2 -> B
#         3 -> C
#         ...
#         26 -> Z
#         27 -> AA
#         28 -> AB
#         ...
#
#
# **Example 1:**
#
#
#
#     Input: 1
#     Output: "A"
#
#
# **Example 2:**
#
#
#
#     Input: 28
#     Output: "AB"
#
#
# **Example 3:**
#
#
#
#     Input: 701
#     Output: "ZY"
#
#
#
## @lc code=start
using LeetCode

## add your code here:
## @lc code=end


using EngEconomics

# Given
# Both
i = 0.08
# Option 1: Get 100,000 -> 120,000 -> 140,000 etc.
n = 30
A = 100000
G = 10000

totalOption1 = G * singlePaymentPresentWorthArithmetic(i, n) + A * seriesPresentAmountFactor(i, n)
totalOption2 = 2500000

if totalOption1 > totalOption2
	println("Don\'t take the money from Gill")
else
	println("Take the money from Gill")
end


# this function shows the behavior of the
# likelihood, the log-likelihood and the average
# log-likelihood as the sample size increases
using Distributions
function main(n)
p = 0.3
y = rand(Bernoulli(p),n)
f =  pdf(Bernoulli(p), y)
@show ℒ = prod(f)
@show lnℒ = sum(log.(f))
@show avglnℒ = (1/n)*sum(log.(f))
nothing
end


using Base.Test
using ToeplitzMatrices

ns = 101
nl = 2000

xs = randn(ns, 5)
xl = randn(nl, 5)

println("Toeplitz")
for (As, Al, st) in ((Toeplitz(0.9.^(0:ns-1), 0.4.^(0:ns-1)),
                        Toeplitz(0.9.^(0:nl-1), 0.4.^(0:nl-1)),
                            "Real general square"),
                     (Toeplitz(complex(0.9.^(0:ns-1)), complex(0.4.^(0:ns-1))),
                        Toeplitz(complex(0.9.^(0:nl-1)), complex(0.4.^(0:nl-1))),
                            "Complex general square"),
                     (Circulant(0.9.^(0:ns - 1)),
                        Circulant(0.9.^(0:nl - 1)),
                            "Real circulant"),
                     (Circulant(complex(0.9.^(0:ns - 1))),
                        Circulant(complex(0.9.^(0:nl - 1))),
                            "Complex circulant"),
                     (TriangularToeplitz(0.9.^(0:ns - 1), :U),
                        TriangularToeplitz(0.9.^(0:nl - 1), :U),
                            "Real upper triangular"),
                     (TriangularToeplitz(complex(0.9.^(0:ns - 1)), :U),
                        TriangularToeplitz(complex(0.9.^(0:nl - 1)), :U),
                            "Complex upper triangular"),
                     (TriangularToeplitz(0.9.^(0:ns - 1), :L),
                        TriangularToeplitz(0.9.^(0:nl - 1), :L),
                            "Real lower triangular"),
                     (TriangularToeplitz(complex(0.9.^(0:ns - 1)), :L),
                        TriangularToeplitz(complex(0.9.^(0:nl - 1)), :L),
                            "Complex lower triangular"))
    print("$st: ")
    @test_approx_eq As * xs full(As) * xs
    @test_approx_eq Al * xl full(Al) * xl
    @test_approx_eq A_ldiv_B!(As, LinAlg.copy_oftype(xs, eltype(As))) full(As) \ xs
    @test_approx_eq A_ldiv_B!(Al, LinAlg.copy_oftype(xl, eltype(Al))) full(Al) \ xl
    println("OK!")
end

print("Real general rectangular: ")
Ar1 = Toeplitz(0.9.^(0:nl-1), 0.4.^(0:ns-1))
Ar2 = Toeplitz(0.9.^(0:ns-1), 0.4.^(0:nl-1))
@test_approx_eq Ar1 * xs full(Ar1) * xs
@test_approx_eq Ar2 * xl full(Ar2) * xl
println("OK!")

print("Complex general rectangular: ")
Ar1 = Toeplitz(complex(0.9.^(0:nl-1)), complex(0.4.^(0:ns-1)))
Ar2 = Toeplitz(complex(0.9.^(0:ns-1)), complex(0.4.^(0:nl-1)))
@test_approx_eq Ar1 * xs full(Ar1) * xs
@test_approx_eq Ar2 * xl full(Ar2) * xl
println("OK!")

print("Symmetric Toeplitz: ")
As = SymmetricToeplitz(0.9.^(0:ns-1))
Ab = SymmetricToeplitz(abs(randn(ns)))
Al = SymmetricToeplitz(0.9.^(0:nl-1))
@test_approx_eq As * xs full(As) * xs
@test_approx_eq Ab * xs full(Ab) * xs
@test_approx_eq Al * xl full(Al) * xl
@test_approx_eq A_ldiv_B!(As, copy(xs)) full(As) \ xs
@test_approx_eq A_ldiv_B!(Ab, copy(xs)) full(Ab) \ xs
@test_approx_eq A_ldiv_B!(Al, copy(xl)) full(Al) \ xl
@test_approx_eq StatsBase.levinson(As, xs) full(As) \ xs
@test_approx_eq StatsBase.levinson(Ab, xs) full(Ab) \ xs
@test_approx_eq StatsBase.levinson(Al, xl) full(Al) \ xl
println("OK!")

println("\nHankel")
print("Real square: ")
H = Hankel([1.0,2,3,4,5],[5.0,6,7,8,0])
x = ones(5)
@test_approx_eq full(H)*x H*x

Hs = Hankel(0.9.^(ns-1:-1:0), 0.4.^(0:ns-1))
Hl = Hankel(0.9.^(nl-1:-1:0), 0.4.^(0:nl-1))
@test_approx_eq Hs * xs[:,1] full(Hs) * xs[:,1]
@test_approx_eq Hs * xs full(Hs) * xs
@test_approx_eq Hl * xl full(Hl) * xl
println("OK!")

print("Complex square: ")
H = Hankel(complex([1.0,2,3,4,5]), complex([5.0,6,7,8,0]))
x = ones(5)
@test_approx_eq full(H)*x H*x

Hs = Hankel(complex(0.9.^(ns-1:-1:0)), complex(0.4.^(0:ns-1)))
Hl = Hankel(complex(0.9.^(nl-1:-1:0)), complex(0.4.^(0:nl-1)))
@test_approx_eq Hs * xs[:,1] full(Hs) * xs[:,1]
@test_approx_eq Hs * xs full(Hs) * xs
@test_approx_eq Hl * xl full(Hl) * xl
println("OK!")

print("Real rectangular: ")
Hs = Hankel(0.9.^(ns-1:-1:0), 0.4.^(0:nl-1))
Hl = Hankel(0.9.^(nl-1:-1:0), 0.4.^(0:ns-1))
@test_approx_eq Hs * xl[:,1] full(Hs) * xl[:,1]
@test_approx_eq Hs * xl full(Hs) * xl
@test_approx_eq Hl * xs full(Hl) * xs
println("OK!")

print("Complex rectangular: ")
Hs = Hankel(complex(0.9.^(ns-1:-1:0)), complex(0.4.^(0:nl-1)))
Hl = Hankel(complex(0.9.^(nl-1:-1:0)), complex(0.4.^(0:ns-1)))
@test_approx_eq Hs * xl[:,1] full(Hs) * xl[:,1]
@test_approx_eq Hs * xl full(Hs) * xl
@test_approx_eq Hl * xs full(Hl) * xs
println("OK!")

if isdir(Pkg.dir("FastTransforms"))
    print("\nBigFloat")
    using FastTransforms
    T = Toeplitz(BigFloat[1,2,3,4,5], BigFloat[1,6,7,8,0])
    @test_approx_eq T*ones(BigFloat,5) [22,24,19,16,15]

    n = 512
    r = map(BigFloat,rand(n))
    T = Toeplitz(r,[r[1];map(BigFloat,rand(n-1))])
    @test_approx_eq T*ones(BigFloat,n) full(T)*ones(BigFloat,n)

    T = TriangularToeplitz(BigFloat[1,2,3,4,5],:L)
    @test_approx_eq T*ones(BigFloat,5) full(T)*ones(BigFloat,5)

    n = 512
    r = map(BigFloat,rand(n))
    T = TriangularToeplitz(r,:L)
    @test_approx_eq T*ones(BigFloat,n) full(T)*ones(BigFloat,n)

    T = TriangularToeplitz(BigFloat[1,2,3,4,5],:U)
    @test_approx_eq T*ones(BigFloat,5) full(T)*ones(BigFloat,5)

    n = 512
    r = map(BigFloat,rand(n))
    T = TriangularToeplitz(r,:U)
    @test_approx_eq T*ones(BigFloat,n) full(T)*ones(BigFloat,n)
    println("OK!")
end

println("Convert")

T = Toeplitz(ones(2),ones(2))

@test isa(convert(Matrix{Complex128},T),Matrix{Complex128})
@test isa(convert(AbstractMatrix{Complex128},T),Toeplitz{Complex128})
@test isa(convert(AbstractArray{Complex128},T),Toeplitz{Complex128})
@test isa(convert(ToeplitzMatrices.AbstractToeplitz{Complex128},T),Toeplitz{Complex128})
@test isa(convert(ToeplitzMatrices.Toeplitz{Complex128},T),Toeplitz{Complex128})

T = SymmetricToeplitz(ones(2))

@test isa(convert(Matrix{Float32},T),Matrix{Float32})
@test isa(convert(AbstractMatrix{Float32},T),SymmetricToeplitz{Float32})
@test isa(convert(AbstractArray{Float32},T),SymmetricToeplitz{Float32})
@test isa(convert(ToeplitzMatrices.AbstractToeplitz{Float32},T),SymmetricToeplitz{Float32})
@test isa(convert(ToeplitzMatrices.SymmetricToeplitz{Float32},T),SymmetricToeplitz{Float32})

T = Circulant(ones(2))

@test isa(convert(Matrix{Complex128},T),Matrix{Complex128})
@test isa(convert(AbstractMatrix{Complex128},T),Circulant{Complex128})
@test isa(convert(AbstractArray{Complex128},T),Circulant{Complex128})
@test isa(convert(ToeplitzMatrices.AbstractToeplitz{Complex128},T),Circulant{Complex128})
@test isa(convert(ToeplitzMatrices.Circulant{Complex128},T),Circulant{Complex128})

T = TriangularToeplitz(ones(2),:U)

@test isa(convert(Matrix{Complex128},T),Matrix{Complex128})
@test isa(convert(AbstractMatrix{Complex128},T),TriangularToeplitz{Complex128})
@test isa(convert(AbstractArray{Complex128},T),TriangularToeplitz{Complex128})
@test isa(convert(ToeplitzMatrices.AbstractToeplitz{Complex128},T),TriangularToeplitz{Complex128})
@test isa(convert(ToeplitzMatrices.TriangularToeplitz{Complex128},T),TriangularToeplitz{Complex128})

T = Hankel(ones(2),ones(2))

@test isa(convert(Matrix{Complex128},T),Matrix{Complex128})
@test isa(convert(AbstractMatrix{Complex128},T),Hankel{Complex128})
@test isa(convert(AbstractArray{Complex128},T),Hankel{Complex128})
@test isa(convert(ToeplitzMatrices.Hankel{Complex128},T),Hankel{Complex128})


#RSDeltaSigma: Quantizer functionnality
#-------------------------------------------------------------------------------

#Version from simulateDSM.m
#-------------------------------------------------------------------------------
#VERIFYME: Did this one actually get called? Was the other version the one being called??
"""`ds_quantize_DSM(y,n)`

Quantize y to:
 - an odd integer in [-n+1, n-1], if n is even, or
 - an even integer in [-n, n], if n is odd.

This definition gives the same step height for both mid-rise
and mid-tread quantizers.
"""
function ds_quantize_DSM(y, n::Int)
	if mod(n,2)==0 #mid-rise quantizer
		v = 2*floor.(0.5*y) .+ 1
	else #mid-tread quantizer
		v = 2*floor.(0.5*(y .+ 1))
	end

	#Limit the output
	for qi in 1:length(n) #Loop for multiple quantizers
		L = n[qi]-1
		i = v[qi,:] .> L
		if any(i)
			v[qi,i] .= L
		end
		i = v[qi,:] .< -L
		if any(i)
			v[qi,i] .= -L
		end
	end
	return v
end
#Pretty sure mod(n,2) code needs to be adapted for Vector:
ds_quantize_DSM(y, n::Vector{Int}) =
	throw(ErrorException("ds_quantizer: does not support n::Vector"))

#Version from ds_quantize.m:
#-------------------------------------------------------------------------------
#=
function v = ds_quantize(y,n)
%v = ds_quantize(y,n=2)
%Quantize y to
% an odd integer in [-n+1, n-1], if n is even, or
% an even integer in [-n+1, n-1], if n is odd.
%This definition gives the same step height for both mid-rise
%and mid-tread quantizers.
%n can be a column vector which specifies how to quantize the rows of y
if nargin<2
    n=2;
end
if length(n)==1
    n = n*ones(size(y));
elseif size(n,1)==size(y,1)
    n = n*ones(1,size(y,2));
end

i = rem(n,2)==0;
v = zeros(size(y));
v(i) = 2*floor(0.5*y(i))+1;	% mid-rise quantizer
v(~i) = 2*floor(0.5*(y(~i)+1));	% mid-tread quantizer

% Limit the output
L = n-1;
for m=[-1 1]
    i = m*v>L;
    if any(i(:))
	v(i) = m*L(i);
    end
end
=#

#Last line


"""
Compute the stopping times of numbers subjected to the Collatz sequence.
https://en.wikipedia.org/wiki/Collatz_conjecture
"""

include("../ragamuffin.jl")

function collatz_length(x)
    if x == 1
        return 0
    elseif x % 2 == 0
        return 1 + collatz_length(x / 2)
    else
        return 1 + collatz_length(3 * x + 1)
    end
end
N = 100
range = 1:N
expected = @time collect(map(collatz_length, range))

function ragamuffin_collatz()
    T = Int32
    program = initialize(map(T, range) |> collect, T(-1), Frame, 32, T)
    add!(program, :root, (:end, :increment), value_function=x -> x.value == 1)
    add!(program, :increment, :parity, dest_function = x -> 1)
    add!(program, :parity, (:even, :odd), value_function = x -> x.value % 2 == 0)
    add!(program, :even, value_function = x -> x.value > 0 ? Cell{T}(T(x.value / 2), x.depth, x.output_index) : x)
    add!(program, :odd, value_function = x -> Cell{T}(T(x.value * 3 + 1), x.depth, x.output_index))
    @time run(program)
    return program.dest
end

batched = ragamuffin_collatz()
@info batched
@assert expected == batched


# Matern 3/2 isotropic covariance function

"""
    Mat32Iso <: MaternIso

Isotropic Matern 3/2 kernel (covariance)
```math
k(x,x') = σ²(1 + √3|x-x'|/ℓ)\\exp(-√3|x-x'|/ℓ)
```
with length scale ``ℓ`` and signal standard deviation ``σ``.
"""
mutable struct Mat32Iso{T<:Real} <: MaternIso
    "Length scale"
    ℓ::T
    "Signal variance"
    σ2::T
    "Priors for kernel parameters"
    priors::Array
end

"""
Matern 3/2 isotropic covariance function

    Mat32Iso(ll::T, lσ::T)

# Arguments
  - `ll::Real`: length scale (given on log scale)
  - `lσ::Real`: signal standard deviation (given on log scale)
"""
Mat32Iso(ll::T, lσ::T) where T = Mat32Iso{T}(exp(ll), exp(2 * lσ), [])

function set_params!(mat::Mat32Iso, hyp::AbstractVector)
    length(hyp) == 2 || throw(ArgumentError("Matern 3/2 has two parameters, received $(length(hyp))."))
    mat.ℓ, mat.σ2 = exp(hyp[1]), exp(2 * hyp[2])
end

get_params(mat::Mat32Iso{T}) where T = T[log(mat.ℓ), log(mat.σ2) / 2 ]
get_param_names(mat::Mat32Iso) = [:ll, :lσ]
num_params(mat::Mat32Iso) = 2

cov(mat::Mat32Iso, r::Number) =
    (s = √3 * r / mat.ℓ; mat.σ2 * (1 + s) * exp(-s))

@inline dk_dll(mat::Mat32Iso, r::Real) =
    (s = √3 * r / mat.ℓ; mat.σ2 * s^2 * exp(-s))


module Exponential

using ..Auxiliary, ..TensorTrain

export trigevalmask, trigeval, trigrefmask, trigrefmask2, trigdiffmask, trigdiff, trigdec, trigdeceval!
export cosfactor, cosdec

function trigevalmask(t::Vector{T}, ν::Vector{T}) where {T<:AbstractFloat}
	n = length(t)
	r = length(ν)
	if r == 0
		throw(ArgumentError("the number of frequencies should be positive"))
	end
	V = zeros(T, n, 2*r)
	for α ∈ 1:r
		for i ∈ 1:n
			V[i,2*α-1] = cos(π*ν[α]*t[i])
			V[i,2*α  ] = sin(π*ν[α]*t[i])
		end
	end
	return V
end

function trigeval(t::Vector{T}, ν::Vector{T}, c::Vector{T}) where {T<:AbstractFloat}
	n = length(t)
	r = length(ν)
	if r == 0
		throw(ArgumentError("the number of frequencies should be positive"))
	end
	if length(c) ≠ 2*r
		throw(ArgumentError("ν and c are incompatible in size"))
	end
	u = zeros(T, length(t))
	for i ∈ 1:n
		for α ∈ 1:r
			u[i] += c[2*α-1]*cos(π*ν[α]*t[i])+c[2*α]*sin(π*ν[α]*t[i])
		end
	end
	return u
end

function trigrefmask(η::T, ν::Vector{T}) where {T<:AbstractFloat}
	r = length(ν)
	if r == 0
		throw(ArgumentError("the number of frequencies should be positive"))
	end
	W = zeros(T, 2*r, 2*r)
	for α ∈ 1:r
		W[2*α-1,2*α-1] =  cos(π*ν[α]*η)
		W[2*α,  2*α-1] = -sin(π*ν[α]*η)
		W[2*α-1,2*α  ] =  sin(π*ν[α]*η)
		W[2*α,  2*α  ] =  cos(π*ν[α]*η)
	end
	return W
end

function trigrefmask2(ν::Vector{T}) where {T<:AbstractFloat}
	r = length(ν)
	if r == 0
		throw(ArgumentError("the number of frequencies should be positive"))
	end
	W1 = trigrefmask(convert(T, -1/2), ν)
	W2 = trigrefmask(convert(T, 1/2), ν)
	W = [W1[:] W2[:]]
	W = reshape(W, 2*r, 2*r, 2)
	return W
end

function trigdiffmask(ν::Vector{T}) where {T<:AbstractFloat}
	r = length(ν)
	if r == 0
		throw(ArgumentError("the number of frequencies should be positive"))
	end
	W = zeros(T, 2*r, 2*r)
	for α ∈ 1:r
		W[2*α-1,2*α  ] =  π*ν[α]
		W[2*α,  2*α-1] = -π*ν[α]
	end
	return W
end

function trigdiff(ν::Vector{T}, c::Vector{T}) where {T<:AbstractFloat}
	r = length(ν)
	if length(c) ≠ 2*r
		throw(ArgumentError("ν and c are incompatible in size"))
	end
	d = zeros(T, 2*r)
	for α ∈ 1:r
		d[2*α-1] =  π*ν[α]*c[2*α]
		d[2*α]   = -π*ν[α]*c[2*α-1]
	end
	return d
end

function cosfactor(τ::Vector{T}, c::T) where {T<:AbstractFloat}
	n = length(τ)
	if n == 0
		throw(ArgumentError("τ should be nonempty"))
	end
	U = zeros(T, 2, n, 2)
	for i ∈ 1:n
		α = c*cos(τ[i])
		β = c*sin(τ[i])
		U[1,i,1] =  α
		U[2,i,1] =  β
		U[1,i,2] = -β
		U[2,i,2] =  α
	end
	return U
end

function cosdec(τ::Vector{Vector{T}}, c::Vector{T}) where {T<:AbstractFloat}
	L = length(τ)
	if length(c) ≠ L
		throw(ArgumentError("τ and c should be of the same length"))
	end
	n = [ size(τ[ℓ]) for ℓ ∈ 1:L ]
	if any(τ .== 0)
		throw(ArgumentError("all elements of τ should be nonempty"))
	end
	U = [ cosfactor(τ[ℓ], c[ℓ]) for ℓ ∈ 1:L ]
	U[1] = U[1][1:1,:,:]
	U[L] = U[L][:,:,1:1]
	U = dec(U)
	return U
end

function trigdec(ν::Vector{T}, c::Vector{T}, L::Int; major::String="last") where {T<:AbstractFloat}
	if major ∉ ("first", "last")
		throw(ArgumentError("major should be either \"last\" (default) or \"first\""))
	end
	r = length(ν)
	if length(c) ≠ 2*r
		throw(ArgumentError("ν and c are incompatible in size"))
	end
	U = dec(T, 1, L+1)
	U[1] = factor(Matrix{T}(c'), 1, [])
	for ℓ ∈ 1:L
		U[ℓ+1] = permutedims(trigrefmask2(ν/2^(ℓ-1)), (2,3,1))
	end
	(major == "last") && decreverse!(U)
	return U
end

function trigdeceval!(U::Dec{T,N}, t::Vector{T}, ν::Vector{T}; major::String="last") where {T<:AbstractFloat,N}
	if major ∉ ("first", "last")
		throw(ArgumentError("major should be either \"last\" (default) or \"first\""))
	end
	if N ≠ 3
		throw(ArgumentError("only one mode index per factor is currently supported"))
	end
	n = length(t)
	r = length(ν)
	L = declength(U)
	p = decrank(U)
	p = (major == "last") ? p[1] : p[L+1]
	if p ≠ 2*r
		throw(ArgumentError("the ", (major == "last") ? "first" : "last", " rank of U and the size of ν are incompatible"))
	end
	W = trigevalmask(t, ν)
	if major == "last"
		W = reshape(W, 1, n, 2*r)
		W = Array{T,N}(W)
		decpushfirst!(U, W)
	else
		W = reshape(transpose(W), 2*r, n, 1)
		W = Array{T,N}(W)
		decpush!(U, W)
	end
	return U
end

end


using ForwardDiff
using LinearAlgebra
include("backtracking_line_search.jl")
include("newton_method.jl")
include("gradient_algorithm_l2.jl")
include("coordinate_algorithm_l1.jl")
include("gradient_algorithm_l2.jl")
include("coordinate_algorithm_l1.jl")
dimension = 10 #defult
epsilon = 0.001 #default
objective(x) =  x' * x   #default
x = rand(dimension) #default, point selection is also an important step
alpha = 0.1 #default
beta = 0.5 #default
step = 100 #default

x = newton_algo(epsilon,objective,x)
x = coordinate_algo(dimension,epsilon,objective,x,alpha,beta,step)

print(x)
print(objective(x))

#brief readme: rough underestimator. for learning only


"""
    planepoint(point::Point, triangle::Polygon)

Take a triangular plane as a Polygon and a Point within that triangle
and returns the z-value at that point.

# Examples
```jldoctest
julia> point = Point([-175, 22])
Point([-175.0, 22.0])

julia> triangle = Polygon([[-174.55, 32.54, 55], [-186.94, 17.64, 24.5], [-167.95, 17.81, 33.6], [-174.55, 32.54, 55]])
Polygon(Array{Array{Float64,1},1}[[[-174.55, 32.54, 55.0], [-186.94, 17.64, 24.5], [-167.95, 17.81, 33.6], [-174.55, 32.54, 55.0]]])

julia> planepoint(point, triangle)
37.28550123965308
```
"""
function planepoint(point::Point, triangle::Polygon)
    pCoords = point.coordinates
    tCoords = triangle.coordinates

    outer = tCoords[1]
    length(outer) < 4 && throw(error("The outer ring of a Polygon must have 4 or more Points."))

    x = pCoords[1]
    y = pCoords[2]

    x1 = outer[1][1]
    y1 = outer[1][2]
    z1 = outer[1][3]

    x2 = outer[2][1]
    y2 = outer[2][2]
    z2 = outer[2][3]

    x3 = outer[3][1]
    y3 = outer[3][2]
    z3 = outer[3][3]

    return (z3 * (x - x1) * (y - y2) + z1 * (x - x2) * (y - y3) + z2 * (x - x3) * (y - y1) -
            z2 * (x - x1) * (y - y3) - z3 * (x - x2) * (y - y1) - z1 * (x - x3) * (y - y2)) /
           ((x - x1) * (y - y2) + (x - x2) * (y - y3) + (x - x3) * (y - y1) -
           (x - x1) * (y - y3) - (x - x2) * (y - y1) - (x - x3) * (y - y2))
end


# Locally Linear Embedding (LLE)
# ------------------------
# Nonlinear dimensionality reduction by locally linear embedding,
# Roweis, S. & Saul, L., Science 290:2323 (2000)

"""

    LLE{NN <: AbstractNearestNeighbors, T <: Real} <: AbstractDimensionalityReduction

The `LLE` type represents a locally linear embedding model constructed for `T` type data constructed with a help of the `NN` nearest neighbor algorithm.
"""
struct LLE{NN <: AbstractNearestNeighbors, T <: Real} <: AbstractDimensionalityReduction
    nearestneighbors::NN
    component::AbstractVector{Int}
    λ::AbstractVector{T}
    proj::Projection{T}
end

## properties
outdim(R::LLE) = size(R.proj, 1)
eigvals(R::LLE) = R.λ
neighbors(R::LLE) = R.nearestneighbors.k
vertices(R::LLE) = R.component

## show
summary(io::IO, R::LLE) = print(io, "LLE(outdim = $(outdim(R)), neighbors = $(neighbors(R)))")

## interface functions
"""
    fit(LLE, data; k=12, maxoutdim=2, nntype=BruteForce, tol=1e-5)

Fit a locally linear embedding model to `data`.

# Arguments
* `data`: a matrix of observations. Each column of `data` is an observation.

# Keyword arguments
* `k`: a number of nearest neighbors for construction of local subspace representation
* `maxoutdim`: a dimension of the reduced space.
* `nntype`: a nearest neighbor construction class (derived from `AbstractNearestNeighbors`)
* `tol`: an algorithm regularization tolerance

# Examples
```julia
M = fit(LLE, rand(3,100)) # construct LLE model
R = transform(M)          # perform dimensionality reduction
```
"""
function fit(::Type{LLE}, X::AbstractMatrix{T};
             k::Int=12, maxoutdim::Int=2, nntype=BruteForce, tol::Real=1e-5) where {T<:Real}
    # Construct NN graph
    NN = fit(nntype, X, k)
    D, E = knn(NN, X)
    _, C = largest_component(SimpleWeightedGraph(adjmat(D,E)))

    X = @view X[:, C]
    n = length(C)

    # Correct indexes of neighbors if more then one connected component
    Ec = E
    if size(E,2) != n
        R = Dict(zip(C, collect(1:n)))
        Ec = zeros(Int,k,n)
        for i in 1 : n
            Ec[:,i] = map(j->get(R,j,C[i]), E[:,C[i]])
        end
    end

    if k > maxoutdim
        @warn("k > maxoutdim: regularization will be used")
    else
        tol = 0
    end

    # Reconstruct weights and compute embedding:
    # M = (I - w)'(I - w) = I - w'I - Iw + w'w
    M = spdiagm(0 => fill(one(T), n))
    Ones = fill(one(T), k, 1)
    for i in 1 : n
        J = Ec[:,i]
        Z = view(X, :, J) .- view(X, :, i)
        G = transpose(Z)*Z
        G += I * tol # regularize
        w = vec(G \ Ones)
        w ./= sum(w)
        ww = w*transpose(w)
        for (l, j) in enumerate(J)
            M[i,j] -= w[l]
            M[j,i] -= w[l]
            for (m, jj) in enumerate(J)
                M[j,jj] = ww[l,m]
            end
        end
    end

    λ, V = decompose(M, maxoutdim)
    return LLE{nntype, T}(NN, C, λ, rmul!(transpose(V), sqrt(n)))
end

"""
    transform(R::LLE)

Transforms the data fitted to the LLE model `R` into a reduced space representation.
"""
transform(R::LLE) = R.proj


using GLM, QuantileRegression

# Load data
url = "http://vincentarelbundock.github.io/Rdatasets/csv/quantreg/engel.csv"
# TODO: Make automatic URL downloads work
dat = readtable("engel.csv")

# Fit least absolute deviation model (quantile =.5)
res = qreg(:(foodexp~income), dat, .5)
describe(res)

# Fit quantile regression for a bunch of different quantiles
dat_plot = [summary(qreg(:(foodexp ~ income), dat, i/10))[2,:] for i in 1:9]
dat_plot = reduce(vcat, dat_plot)

# Fit OLS model to compare
res_lm = lm(:(foodexp~income), dat)
ols = coeftable(res_lm)[2,1]
ols = rep(ols, 9)

# Plot results
using Winston

x = [i/10 for i=1:9]
y = dat_plot["Estimate"]
p = FramedPlot()
add(p, Curve(x, dat_plot["Estimate"]))
add(p, Curve(x, dat_plot["2.5%"], "type", "dash"))
add(p, Curve(x, dat_plot["97.5%"], "type", "dash"))
add(p, Curve(x, ols, "color", "red"))
setattr(p, "title", "Quantile regression: Food Expenditure ~ Income\n Red bar: OLS coefficient")
setattr(p, "xlabel", "Quantiles")
setattr(p, "ylabel", "Coefficient on Income")
file(p, "/Users/USERNAME/qreg_example_plot.png")

# > summary(res)
# 2x5 DataFrame:
#         Estimate Std.Error t value     2.5%    97.5%
# [1,]     81.4823   14.6345 5.56783  52.7987  110.166
# [2,]    0.560181 0.0131756 42.5164 0.534356 0.586005
# insheet using engel.csv
# qreg foodexp income, vce(iid, kernel(epan2))

# . qreg foodexp income, vce(iid, kernel(epan2))
# Iteration  1:  WLS sum of weighted deviations =  18051.196
#
# Iteration  1: sum of abs. weighted deviations =  18035.127
# Iteration  2: sum of abs. weighted deviations =  17815.249
# Iteration  3: sum of abs. weighted deviations =  17567.085
# Iteration  4: sum of abs. weighted deviations =  17559.932
#
# Median regression                                    Number of obs =       235
#   Raw sum of deviations 46278.06 (about 582.54126)
#   Min sum of deviations 17559.93                     Pseudo R2     =    0.6206
#
# ------------------------------------------------------------------------------
#      foodexp |      Coef.   Std. Err.      t    P>|t|     [95% Conf. Interval]
# -------------+----------------------------------------------------------------
#       income |   .5601805   .0131763    42.51   0.000     .5342206    .5861403
#        _cons |   81.48233   14.63518     5.57   0.000     52.64815    110.3165
# ------------------------------------------------------------------------------


immutable BetaPrime <: ContinuousUnivariateDistribution
    betad::Beta

    BetaPrime(α::Float64, β::Float64) = new(Beta(α, β))
    BetaPrime(α::Float64) = new(Beta(α))
    BetaPrime() = new(Beta())
end

@distr_support BetaPrime 0.0 Inf

#### Parameters

params(d::BetaPrime) = params(d.betad)

#### Statistics

mean(d::BetaPrime) = ((α, β) = params(d); β > 1.0 ? α / (β - 1.0) : NaN)

mode(d::BetaPrime) = ((α, β) = params(d); α > 1.0 ? (α - 1.0) / (β + 1.0) : 0.0)

function var(d::BetaPrime)
    (α, β) = params(d)
    β > 2.0 ? α * (α + β - 1.0) / ((β - 2.0) * (β - 1.0)^2) : NaN
end

function skewness(d::BetaPrime)
    (α, β) = params(d)
    if β > 3.0
        s = α + β - 1.0
        2.0 * (α + s) / (β - 3.0) * sqrt((β - 2.0) / (α * s))
    else
        return NaN
    end
end

#### Evaluation

function logpdf(d::BetaPrime, x::Float64)
    (α, β) = params(d)
    (α - 1.0) * log(x) - (α + β) * log1p(x) - lbeta(α, β)
end

pdf(d::BetaPrime, x::Float64) = exp(logpdf(d, x))

cdf(d::BetaPrime, x::Float64) = cdf(d.betad, x / (1.0 + x))
ccdf(d::BetaPrime, x::Float64) = ccdf(d.betad, x / (1.0 + x))
logcdf(d::BetaPrime, x::Float64) = logcdf(d.betad, x / (1.0 + x))
logccdf(d::BetaPrime, x::Float64) = logccdf(d.betad, x / (1.0 + x))

quantile(d::BetaPrime, p::Float64) = (x = quantile(d.betad, p); x / (1.0 - x))
cquantile(d::BetaPrime, p::Float64) = (x = cquantile(d.betad, p); x / (1.0 - x))
invlogcdf(d::BetaPrime, p::Float64) = (x = invlogcdf(d.betad, p); x / (1.0 - x))
invlogccdf(d::BetaPrime, p::Float64) = (x = invlogccdf(d.betad, p); x / (1.0 - x))

#### Sampling

function rand(d::BetaPrime)
    (α, β) = params(d)
    rand(Gamma(α)) / rand(Gamma(β))
end


using LinearAlgebra, Plots

Rx(θ::Real) = [1 0 0 0;
        0 cos(θ) -sin(θ) 0;
        0 sin(θ) cos(θ) 0;
        0 0 0 1]

Ry(θ::Real) = [cos(θ) 0 -sin(θ) 0;
        0 1 0 0;
        sin(θ) 0 cos(θ) 0;
        0 0 0 1]

Rz(θ::Real) = [cos(θ) -sin(θ) 0 0;
        sin(θ) cos(θ) 0 0;
        0 0 1 0;
        0 0 0 1]

Tx(d::Real) = [1 0 0 d;
        0 1 0 0;
        0 0 1 0;
        0 0 0 1]

π = pi
d = 2
angToRad(ang::Real) = (π/180)*ang
θ(ang::Real = 150) = angToRad(ang) #70
ω(ang::Real = 160) = angToRad(ang)

B1 = I(4) #nada a fazer

#B2 =  Ry(π)*Tx(d)

B2 = zeros(4,4)
B2[1,1] = -1.0
B2[2,2] = 1.0
B2[3,3] = -1.0
B2[4,4] = 1.0
B2[1,4] = -d

#B3 = Rx(π)*Rz(θ())*Ry(π)*Tx(d)

# tird atom
cθ,sθ = (cos(θ()),sin(θ()))
B3 = zeros(4,4)
B3[1,1] = -cθ
B3[1,2] = -sθ
B3[1,4] = -d*cθ
B3[2,1] = sθ
B3[2,2] = -cθ
B3[2,4] = d*sθ
B3[3,3] = 1.0
B3[4,4] = 1.0

function torsionmatrix(cosθ,sinθ,cosω,sinω,d34,sign::Bool)
	if sign == true

		B=zeros(4,4)
		B[1,1] = -cosθ
		B[1,2] = -sinθ
		B[1,4] = -d34*cosθ
		B[2,1] = sinθ*cosω
		B[2,2] = -cosθ*cosω
		B[2,3] = -sinω
		B[2,4] = d34*sinθ*cosω
		B[3,1] = sinθ*sinω
		B[3,2] = -cosθ*sinω
		B[3,3] = cosω
		B[3,4] = d34*sinθ*sinω
		B[4,4] = 1
	else

		B=zeros(4,4)
		B[1,1] = -cosθ
		B[1,2] = -sinθ
		B[1,4] = -d34*cosθ
		B[2,1] = sinθ*cosω
		B[2,2] = -cosθ*cosω
		B[2,3] = sinω
		B[2,4] = d34*sinθ*cosω
		B[3,1] = -sinθ*sinω
		B[3,2] = cosθ*sinω
		B[3,3] = cosω
		B[3,4] = -d34*sinθ*sinω
		B[4,4] = 1

	end
	return B
end

Bi(ω = ω(), θ = θ(), d = d) = torsionmatrix(cos(angToRad(θ)), sin(angToRad(θ)), cos(angToRad(ω)), sin(angToRad(ω)), d, false) #Bi(ω = ω(), θ = θ(), d = d) = Rx(ω)*Rx(π)*Rz(θ)*Ry(π)*Tx(d)

Bi′(B) = B*inv(Tx(d))

unk = [1 0 0 0;
        0 1 0 0;
        0 0 1 0;
        1 1 1 1]

function sysGenerate(B, α = 1; title = true)
    if title
		return ["x/|x|" "y/|y|" "z/|z|" "i"; B*unk*α]
	else
		return B*unk*α
	end
end

function flatProjection(A, x̂ = [cos(π/4) -cos(π/4)]'*0.5; x :: Bool = true)
	if x
		return [A[2:3,1]+x̂*A[1,1] A[2:3,2]+x̂*A[1,2] A[2:3,3]+x̂*A[1,3] A[2:3,4]+x̂*A[1,4]]
	else
		return [A[1:2,1]+x̂*A[3,1] A[1:2,2]+x̂*A[3,2] A[1:2,3]+x̂*A[3,3] A[1:2,4]+x̂*A[3,4]]
	end
end

function plotFlatProjection(B; sprain=1.01, static = false)
	param = [B[1,4]+B[2,4]im, B[1,4]+B[2,4]im+B[1,1]+B[2,1]im, Inf, B[1,4]+B[2,4]im, B[1,4]+B[2,4]im+B[1,2]+B[2,2]im, Inf, B[1,4]+B[2,4]im, B[1,4]+B[2,4]im+B[1,3]+B[2,3]im]
	if static
		plot!(param, arrow = 2)
	else
		plot(param, arrow = 2)
	end
	annotate!(B[1,4]*sprain, B[2,4]*sprain, text("v", :black, :right, 10))
	annotate!(B[1,4]+B[1,1]*sprain, B[2,4]+B[2,1]*sprain, text("x", :black, :right, 10))
	annotate!(B[1,4]+B[1,2]*sprain, B[2,4]+B[2,2]*sprain, text("y", :black, :right, 10))
	annotate!(B[1,4]+B[1,3]*sprain, B[2,4]+B[2,3]*sprain, text("z", :black, :right, 10))
	# ylims!((-1.5,2)); xlims!((-1.5,1.5))
end

print("DmdgpRealization included\n")

#rotationFactor = Rz(π/2)*Rx(π/2)*Rz(-θ(30))*Rx(π)
#sysGenerate(flatProjection(rotationFactor*B2*B3*Bi(ω(230),θ(120))), 20)
#Bo = B2*B3*Bi(ω(230),θ(120))
#B = Bi(ω(230),θ(120))
#sysGenerate(flatProjection(rotationFactor*Bo*B*B*B), 20)

# a good initial image

# plotFlatProjection(flatProjection(B1, [-cos(π/4) -cos(π/4)]'*0.707))
# plotFlatProjection(flatProjection(B1*Ry(π)*Tx(2.5), [-cos(π/4) -cos(π/4)]'*0.707), static = true)
# plotFlatProjection(flatProjection(B1*B2*Rx(π)*Rz(θ(75))*Ry(π)*Tx(2.5), [-cos(π/4) -cos(π/4)]'*0.707), static = true)
# ylims!((-1,2)); xlims!((-1,4))


# This file includes coding methods to map bit streams into symbol streams

export GrayCoding, invmap

abstract type AbstractCoding end

"""
    $TYPEDEF

Gray coding.

# Fields
    $TYPEDFIELDS

# Example
```julia
julia> gc = GrayCoding(2)
GrayCoding(Dict([0, 1] => 2,[1, 1] => 3,[0, 0] => 1,[1, 0] => 4))
```
"""
struct GrayCoding <: AbstractCoding
    pairs::Dict{Vector{Int}, Int}
    function GrayCoding(M::Int)
        ispow2(M) || throw(ArgumentError("Expected `M` to be a power of 2, got $M instead"))
        k = Int(log2(M))
        m = 0 : 1 << k - 1
        codes = m .⊻ (m .>> 1)
        codes = reverse.(digits.(codes, base=2, pad=k))
        new(Dict(zip(codes, m .+ 1)))
    end
end

show(io::IO, coding::GrayCoding) = print(io, "GrayCoding(M:$(length(coding.pairs)))")

# When a GrayCoding object is called, it maps its input bit stream to coressponding symbol stream.
function (coding::GrayCoding)(stream)
    codewords = collect(Iterators.partition(stream, Int(log2(constelsize(coding)))))
    alph = coding.pairs
    map(codewords) do codeword
        alph[codeword]
    end
end

"""
    $SIGNATURES

Returns constellation size of the coding.
"""
constelsize(coding::GrayCoding) = length(coding.pairs)

"""
    $SIGNATURES

Returns the inverse of the mapping. Inverse mapping maps the levels to bit chunks.

# Example
```julia
julia> gray = GrayCoding(3)
GrayCoding(Dict([0, 1, 1] => 3,[0, 0, 1] => 2,[1, 1, 0] => 5,[0, 0, 0] => 1,[1, 0, 0] => 8,[1, 0, 1] => 7,[0, 1, 0] => 4,[1, 1, 1] => 6))

julia> invmap(gray)
Dict{Int64,Array{Int64,1}} with 8 entries:
  7 => [1, 0, 1]
  4 => [0, 1, 0]
  2 => [0, 0, 1]
  3 => [0, 1, 1]
  5 => [1, 1, 0]
  8 => [1, 0, 0]
  6 => [1, 1, 1]
  1 => [0, 0, 0]
```
"""
invmap(coding::AbstractCoding) = Dict([(v, k) for (k, v) in coding.pairs])


@testset "Matrix multiplication" begin
    @testset "Matrix-vector" begin
        m = @SMatrix [1 2; 3 4]
        v = @SVector [1, 2]
        @test m*v === @SVector [5, 11]
        # More complicated eltype inference
        v = @SVector [CartesianIndex((1,3)), CartesianIndex((3,1))]
        x = @inferred(m*v)
        @test isa(x, SVector{2,CartesianIndex{2}})
        @test x == @SVector [CartesianIndex((7,5)), CartesianIndex((15,13))]

        m = @MMatrix [1 2; 3 4]
        v = @MVector [1, 2]
        @test (m*v)::MVector == @MVector [5, 11]

        m = @SArray [1 2; 3 4]
        v = @SArray [1, 2]
        @test m*v === @SArray [5, 11]

        m = @MArray [1 2; 3 4]
        v = @MArray [1, 2]
        @test (m*v)::MArray == @MArray [5, 11]
    end

    @testset "Vector-matrix" begin
        m = @SMatrix [1 2 3 4]
        v = @SVector [1, 2]
        @test v*m === @SMatrix [1 2 3 4; 2 4 6 8]
    end

    @testset "Matrix-matrix" begin
        m = @SMatrix [1 2; 3 4]
        n = @SMatrix [2 3; 4 5]
        nc = @SMatrix [CartesianIndex((2,2)) CartesianIndex((3,3));
                       CartesianIndex((4,4)) CartesianIndex((5,5))]
        @test m*n  === @SMatrix [10 13; 22 29]
        @test m*nc === @SMatrix [CartesianIndex((10,10)) CartesianIndex((13,13));
                                 CartesianIndex((22,22)) CartesianIndex((29,29))]

        @test m'*n === @SMatrix [14 18; 20 26]
        @test m*n' === @SMatrix [8 14; 18 32]
        @test m'*n' === @SMatrix [11 19; 16 28]
        @test m.'*n === @SMatrix [14 18; 20 26]
        @test m*n.' === @SMatrix [8 14; 18 32]
        @test m.'*n.' === @SMatrix [11 19; 16 28]

        m = @MMatrix [1 2; 3 4]
        n = @MMatrix [2 3; 4 5]
        @test (m*n)::MMatrix == @MMatrix [10 13; 22 29]

        m = @SArray [1 2; 3 4]
        n = @SArray [2 3; 4 5]
        @test m*n === @SArray [10 13; 22 29]

        m = @MArray [1 2; 3 4]
        n = @MArray [2 3; 4 5]
        @test (m*n)::MArray == @MArray [10 13; 22 29]

        # Alternative methods used between 8 < n <= 14 and n > 14
        m_array = rand(1:10, 10, 10)
        n_array = rand(1:10, 10, 10)
        a_array = m_array*n_array

        m = SMatrix{10,10}(m_array)
        n = SMatrix{10,10}(n_array)
        @test m*n === SMatrix{10,10}(a_array)

        m_array = rand(1:10, 16, 16)
        n_array = rand(1:10, 16, 16)
        a_array = m_array*n_array

        m = SMatrix{16,16}(m_array)
        n = SMatrix{16,16}(n_array)
        @test m*n === SMatrix{16,16}(a_array)

        # Mutating types follow different behaviour
        m_array = rand(1:10, 10, 10)
        n_array = rand(1:10, 10, 10)
        a_array = m_array*n_array

        m = MMatrix{10,10}(m_array)
        n = MMatrix{10,10}(n_array)
        @test (m*n)::MMatrix == a_array

        m_array = rand(1:10, 16, 16)
        n_array = rand(1:10, 16, 16)
        a_array = m_array*n_array

        m = MMatrix{16,16}(m_array)
        n = MMatrix{16,16}(n_array)
        @test (m*n)::MMatrix == a_array

        # Mutating BLAS types follow yet different behaviour
        m_array = randn(4, 4)
        n_array = randn(4, 4)
        a_array = m_array*n_array

        m = MMatrix{4,4}(m_array)
        n = MMatrix{4,4}(n_array)
        @test m*n::MMatrix ≈ a_array

        m_array = randn(10, 10)
        n_array = randn(10, 10)
        a_array = m_array*n_array

        m = MMatrix{10,10}(m_array)
        n = MMatrix{10,10}(n_array)
        @test m*n::MMatrix ≈ a_array

        m_array = randn(16, 16)
        n_array = randn(16, 16)
        a_array = m_array*n_array

        m = MMatrix{16,16}(m_array)
        n = MMatrix{16,16}(n_array)
        @test m*n::MMatrix ≈ a_array
    end

    @testset "A_mul_B!" begin
        m = @SMatrix [1 2; 3 4]
        n = @SMatrix [2 3; 4 5]

        a = MMatrix{2,2,Int,4}()
        A_mul_B!(a, m, n)
        @test a::MMatrix{2,2,Int,4} == @MMatrix [10 13; 22 29]

        Ac_mul_B!(a, m, n)
        @test a::MMatrix{2,2,Int,4} == @MMatrix [14 18; 20 26]
        A_mul_Bc!(a, m, n)
        @test a::MMatrix{2,2,Int,4} == @MMatrix [8 14; 18 32]
        Ac_mul_Bc!(a, m, n)
        @test a::MMatrix{2,2,Int,4} == @MMatrix [11 19; 16 28]
        At_mul_B!(a, m, n)
        @test a::MMatrix{2,2,Int,4} == @MMatrix [14 18; 20 26]
        A_mul_Bt!(a, m, n)
        @test a::MMatrix{2,2,Int,4} == @MMatrix [8 14; 18 32]
        At_mul_Bt!(a, m, n)
        @test a::MMatrix{2,2,Int,4} == @MMatrix [11 19; 16 28]

        a2 = MArray{(2,2),Int,2,4}()
        A_mul_B!(a2, m, n)
        @test a2::MArray{(2,2),Int,2,4} == @MArray [10 13; 22 29]

        # Alternative builtin method used for n > 8
        m_array = rand(1:10, 10, 10)
        n_array = rand(1:10, 10, 10)
        a_array = m_array*n_array

        m = MMatrix{10,10}(m_array)
        n = MMatrix{10,10}(n_array)
        a = MMatrix{10,10,Int}()
        A_mul_B!(a, m, n)
        @test a == a_array

        # BLAS used for n > 14
        m_array = randn(4, 4)
        n_array = randn(4, 4)
        a_array = m_array*n_array

        m = MMatrix{4,4}(m_array)
        n = MMatrix{4,4}(n_array)
        a = MMatrix{4,4,Float64}()
        A_mul_B!(a, m, n)
        @test a ≈ a_array

        m_array = randn(10, 10)
        n_array = randn(10, 10)
        a_array = m_array*n_array

        m = MMatrix{10,10}(m_array)
        n = MMatrix{10,10}(n_array)
        a = MMatrix{10,10,Float64}()
        A_mul_B!(a, m, n)
        @test a ≈ a_array

        m_array = rand(1:10, 16, 16)
        n_array = rand(1:10, 16, 16)
        a_array = m_array*n_array

        m = MMatrix{16,16}(m_array)
        n = MMatrix{16,16}(n_array)
        a = MMatrix{16,16,Int}()
        A_mul_B!(a, m, n)
        @test a ≈ a_array
    end
end


# Define and register smooth functions
_cos_wave(t, f, A, st, ϕ) = A*cos(2*π*f*(t - st) + ϕ)
_sin_wave(t, f, A, st, ϕ) = A*sin(2*π*f*(t - st) + ϕ)
_damped_sine_wave(t, f, A, st, ϕ, d) = exp((st-t)*d)*A*sin(2*π*f*(t-st) + ϕ)
_ramp(t, δ, st, et, h) = h/(et-st)*(_xH(t, δ, st) - _xH(t, δ, et))
_square_wave(t, δ, f, A, st) = A*2atan(sin(2π*(t-st)*f)/δ)/π
_step(t, δ, h, a) = h*(atan((t-a)/δ)/π + 0.5)
_triangular_wave(t, δ, f, A, st) = A*(1-2acos((1 - δ)sin(2π*(t-st)*f))/π)
_xH(t, δ, tₒ) = (t-tₒ)*(1+((t-tₒ)/sqrt((t-tₒ)^2+δ^2)))/2

@register_symbolic _cos_wave(t, f, A, st, ϕ)
@register_symbolic _sin_wave(t, f, A, st, ϕ)
@register_symbolic _damped_sine_wave(t, f, A, st, ϕ, damping)
@register_symbolic _ramp(t, δ, st, et, h)
@register_symbolic _square_wave(t, δ, f, A, st)
@register_symbolic _step(t, δ, h, a)
@register_symbolic _triangular_wave(t, δ, f, A, st)

"""
    ConstantVoltage(;name, V = 1.0)

Source for constant voltage,

# Parameters:
- `V`: [V] Voltage
"""
function ConstantVoltage(;name, V = 1.0)
    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters V=V
    eqs = [
        v ~ V
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

"""
    CosineVoltage(;name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0, phase=0.0)

Generate cosine voltage.

# Parameters:
- `frequency`: [Hz] Frequency of sine wave
- `amplitude`: [V] Amplitude of sine wave
- `phase`: [rad] Phase of sine wave
- `offset`: [V] Offset of output voltage
- `start_time`: [s] Output `y = offset` for `t < start_time`
"""
function CosineVoltage(;name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0, phase=0.0)
    δ = 0.00001

    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        amplitude=amplitude
        frequency=frequency
        start_time=start_time
        phase=phase
    end
    eqs = [
        v ~ _cos_wave(t, frequency, amplitude, start_time, phase) * _step(t, δ, 1.0, start_time) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

"""
Generate damped sine voltage.

# Parameters:
- `frequency`: [Hz] Frequency of sine wave
- `amplitude`: [V] Amplitude of sine wave
- `damping`: [1/s] Damping coefficient of sine wave
- `phase`: [rad] Phase of sine wave
- `offset`: [V] Offset of output voltage
- `start_time`: [s] Output `y = offset` for `t < start_time`
"""
function ExpSineVoltage(;name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0, phase=0.0, damping=0.0)
    δ = 0.00001
    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        amplitude=amplitude
        frequency=frequency
        start_time=start_time
        phase=phase
        damping=damping
    end
    eqs = [
        v ~ _damped_sine_wave(t, frequency, amplitude, start_time, phase, damping) * _step(t, δ, 1.0, start_time)
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

"""
    RampVoltage(;name, offset=0.0, start_time=0.0, duration=1.0, height=1.0)

Generate ramp voltage.

# Parameters:
- `height`: [V] Height of ramp
- `duration`: [s] Duration of ramp (= 0.0 gives a Step)
- `offset`: [V] Offset of output voltage
- `start_time`: [s] Output `y = offset` for `t < start_time`
"""
function RampVoltage(;name, offset=0.0, start_time=0.0, duration=1.0, height=1.0)
    δ = 0.00001
    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        height=height
        start_time=start_time
        duration=duration
    end
    eqs = [
        v ~ _ramp(t, δ, start_time, start_time + duration, height) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

"""
    SineVoltage(;name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0, phase=0.0)

Generate sine voltage.

# Parameters:
- `frequency`: [Hz] Frequency of sine wave
- `amplitude`: [V] Amplitude of sine wave
- `phase`: [rad] Phase of sine wave
- `offset`: [V] Offset of output voltage
- `start_time`: [s] Output `y = offset` for `t < start_time`
"""
function SineVoltage(;name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0, phase=0.0)
    δ = 0.00001

    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        amplitude=amplitude
        frequency=frequency
        start_time=start_time
        phase=phase
    end
    eqs = [
        v ~ _sin_wave(t, frequency, amplitude, start_time, phase) * _step(t, δ, 1.0, start_time) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

"""
    SquareVoltage(; name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0)

Generate square voltage.
"""
function SquareVoltage(; name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0)
    δ = 0.0001

    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        amplitude=amplitude
        start_time=start_time
    end
    eqs = [
        v ~ _square_wave(t, δ, frequency, amplitude, start_time) * _step(t, δ, 1.0, start_time) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

"""
    StepVoltage(;name, offset=0.0, start_time=0.0, height=1.0)

Generate step voltage.

# Parameters:
- `height`: [V] Height of step
- `offset`: [V] Offset of output voltage
- `start_time`: [s] Output `y = offset` for `t < start_time`
"""
function StepVoltage(;name, offset=0.0, start_time=0.0, height=1.0)
    δ = 0.0001

    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        height=height
        start_time=start_time
    end
    eqs = [
        v ~ _step(t, δ, height, start_time) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

function TriangularVoltage(; name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0)
    δ = 0.00001

    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        amplitude=amplitude
        frequency=frequency
        start_time=start_time
    end
    eqs = [
        v ~ _triangular_wave(t, δ, frequency, amplitude, start_time) * _step(t, δ, 1.0, start_time) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

# Current Sources ######################################################################################################
"""
    ConstantCurrent(;name, I = 1.0)

Source for constant current.

# Parameters:
- `I`: [A] Current
"""
function ConstantCurrent(;name, I = 1.0)
    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters I=I
    eqs = [
        i ~ I
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

"""
    CosineCurrent(;name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0, phase=0.0)

Generate cosine current.

# Parameters:
- `frequency`: [Hz] Frequency of sine wave
- `amplitude`: [A] Amplitude of sine wave
- `phase`: [rad] Phase of sine wave
- `offset`: [A] Offset of output current
- `start_time`: [s] Output `y = offset` for `t < start_time`
"""
function CosineCurrent(;name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0, phase=0.0)
    δ = 0.00001

    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        amplitude=amplitude
        frequency=frequency
        start_time=start_time
        phase=phase
    end
    eqs = [
        i ~ _cos_wave(t, frequency, amplitude, start_time, phase) * _step(t, δ, 1.0, start_time) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

"""
    ExpSineCurrent(;name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0, phase=0.0, damping=0.0)

Generate damped sine current.

# Parameters:
- `frequency`: [Hz] Frequency of sine wave
- `amplitude`: [A] Amplitude of sine wave
- `damping`: [1/s] Damping coefficient of sine wave
- `phase`: [rad] Phase of sine wave
- `offset`: [A] Offset of output current
- `start_time`: [s] Output `y = offset` for `t < start_time`
"""
function ExpSineCurrent(;name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0, phase=0.0, damping=0.0)
    δ = 0.00001
    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        amplitude=amplitude
        frequency=frequency
        start_time=start_time
        phase=phase
        damping=damping
    end
    eqs = [
        i ~ _damped_sine_wave(t, frequency, amplitude, start_time, phase, damping) * _step(t, δ, 1.0, start_time)
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

"""
    RampCurrent(;name, offset=0.0, start_time=0.0, duration=1.0, height=1.0)

Generate ramp current.

# Parameters:
- `height`: [A] Height of ramp
- `duration`: [s] Duration of ramp (= 0.0 gives a Step)
- `offset`: [A] Offset of output current
- `start_time`: [s] Output `y = offset` for `t < start_time`
"""
function RampCurrent(;name, offset=0.0, start_time=0.0, duration=1.0, height=1.0)
    δ = 0.00001
    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        height=height
        start_time=start_time
        duration=duration
    end
    eqs = [
        i ~ _ramp(t, δ, start_time, start_time + duration, height) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

"""
    SineCurrent(;name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0, phase=0.0)

Generate sine current.

# Parameters:
- `frequency`: [Hz] Frequency of sine wave
- `amplitude`: [A] Amplitude of sine wave
- `phase`: [rad] Phase of sine wave
- `offset`: [A] Offset of output current
- `start_time`: [s] Output `y = offset` for `t < start_time`
"""
function SineCurrent(;name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0, phase=0.0)
    δ = 0.00001

    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        amplitude=amplitude
        frequency=frequency
        start_time=start_time
        phase=phase
    end
    eqs = [
        i ~ _sin_wave(t, frequency, amplitude, start_time, phase) * _step(t, δ, 1.0, start_time) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

function SquareCurrent(; name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0)
    δ = 0.0001

    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        amplitude=amplitude
        start_time=start_time
    end
    eqs = [
        i ~ _square_wave(t, δ, frequency, amplitude, start_time) * _step(t, δ, 1.0, start_time) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

"""
    StepCurrent(;name, offset=0.0, start_time=0.0, height=1.0)

Generate step current.

# Parameters:
- `height`: [A] Height of step
- `offset`: [A] Offset of output current
- `start_time`: [s] Output `y = offset` for `t < start_time`
"""
function StepCurrent(;name, offset=0.0, start_time=0.0, height=1.0)
    δ = 0.0001

    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        height=height
        start_time=start_time
    end
    eqs = [
        i ~ _step(t, δ, height, start_time) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end

function TriangularCurrent(; name, offset=0.0, amplitude=1.0, frequency=1.0, start_time=0.0)
    δ = 0.00001

    @named oneport = OnePort()
    @unpack v, i = oneport
    pars = @parameters begin
        offset=offset
        amplitude=amplitude
        frequency=frequency
        start_time=start_time
    end
    eqs = [
        i ~ _triangular_wave(t, δ, frequency, amplitude, start_time) * _step(t, δ, 1.0, start_time) + offset
    ]

    extend(ODESystem(eqs, t, [], pars; name=name), oneport)
end


#=
  Aircrat position.
  From BLOG example/aircraft-position.blog

  Distributions of variable numAircraft (num:0)
  3.00000 =>    5857  (0.585700)
  4.00000 =>    3178  (0.317800)
  5.00000 =>     833  (0.083300)
  6.00000 =>     114  (0.011400)
  7.00000 =>      18  (0.001800)

  Also see ~/webppl/aircraft_position.wppl
=#

using Turing, StatsPlots, DataFrames
# using ReverseDiff, Zygote, Tracker
# Turing.setadbackend(:reversediff)
# Turing.setadbackend(:zygote)
# Turing.setadbackend(:tracker)

include("jl_utils.jl")

@model function aircraft_position()
    epsilon = 0.05

    # Poisson might get 0 which is not good for this model
    pp ~ Poisson(5)
    numAircraft ~ Dirac(pp == 0 ? 1 : pp)

    blips = tzeros(numAircraft)
    positions = tzeros(numAircraft)
    obsPos = tzeros(numAircraft)
    for b in 1:numAircraft
        blips[b] ~ flip(0.9)
        positions[b] ~ Normal(0,10)
        obsPos[b] ~ Normal(positions[b],1)
    end

    sumBlips ~ Dirac(sum(blips))

    function inRange(x,y,epsilon)
        Dirac(x > y - epsilon && x < y + epsilon)
    end

    true ~ Dirac(sumBlips == 3)

    inRange(obsPos[1],5.0,epsilon)
    numAircraftPositionLt5 ~ Dirac(sum([positions[a] > 5 ? 1 : 0 for a in 1:numAircraft]))
end

model = aircraft_position()

num_chns = 4
# chns = sample(model, Prior(), 1000)
# chns = sample(model, MH(), 10_000)
chns = sample(model, PG(5), 10_000)
# chns = sample(model, IS(), 10_000)
# chns = sample(model, SMC(), 10_000)
# chns = sample(model, SMC(), MCMCThreads(), 10_000, num_chns)

display(chns)
# display(plot(chns))

show_var_dist_pct(chns, :numAircraft)


using SchumakerSpline
from = 0.5
to = 10
x1 = collect(range(from, stop=to, length=40))
y1 = (x1).^2
x2 = [0.5,0.75,0.8,0.93,0.9755,1.0,1.1,1.4,2.0]
y2 = sqrt.(x2)
left_spline = Schumaker(x1,y1)
right_spline = Schumaker(x2,y2)

crossover_point = get_intersection_points(left_spline,right_spline)

splice_point = crossover_point[1]
spliced = splice_splines(left_spline, right_spline, splice_point)
# Testing at splice point
abs(evaluate(spliced, splice_point) - evaluate(right_spline, splice_point))          < 100*eps()
# As this was a continuous split
abs(evaluate(spliced, splice_point- 100*eps()) - evaluate(spliced, splice_point + 100*eps()))          < 10000*eps()
# Testing in left spline territory.
abs(evaluate(spliced, splice_point-0.5) - evaluate(left_spline, splice_point-0.5))   < 100*eps()
abs(evaluate(spliced, splice_point-0.5) - evaluate(right_spline, splice_point-0.5))  > 0.1
# Testing solidly into right spline territory.
abs(evaluate(spliced, splice_point+0.5) - evaluate(left_spline, splice_point+0.5))   > 100*eps()
abs(evaluate(spliced, splice_point+0.5) - evaluate(right_spline, splice_point+0.5))  < 0.1

splice_point = 1.7
spliced = splice_splines(left_spline, right_spline, splice_point)
# Testing at splice point
abs(evaluate(spliced, splice_point) - evaluate(right_spline, splice_point))          < 100*eps()
# As this was NOT a continuous split
abs(evaluate(spliced, splice_point- 100*eps()) - evaluate(spliced, splice_point + 100*eps()))  > 0.1
# Testing in left spline territory.
abs(evaluate(spliced, splice_point-0.5) - evaluate(left_spline, splice_point-0.5))   < 100*eps()
abs(evaluate(spliced, splice_point-0.5) - evaluate(right_spline, splice_point-0.5))  > 0.1
# Testing solidly into right spline territory.
abs(evaluate(spliced, splice_point+0.5) - evaluate(left_spline, splice_point+0.5))   > 100*eps()
abs(evaluate(spliced, splice_point+0.5) - evaluate(right_spline, splice_point+0.5))  < 0.1


export gauss_legendre_weights;   # For debugging only
export distributed_kernel_trapezoidal;    # For debugging only
export distributed_kernel_gauss_legendre; # For debugging only

"
#Creates the NEP associated with example in E. Jarlebring and
#W. Michiels and K. Meerbergen, The infinite  {Arnoldi} method and an application to time-delay systems with distributed delays, Delay Systems - Methods, Applications and New Trends, 2012
"
function gallery_dep_distributed()
    # E. Jarlebring and W. Michiels and K. Meerbergen},
    # The infinite  {Arnoldi} method and an application to time-delay systems with distributed delays,
    # Delay Systems - Methods, Applications and New Trends
    # 2012
    #
    # Some correct eigenvalues:
    # -0.400236388049641 + 0.970633098237807i
    # -0.400236388049641 - 0.970633098237807i
    #  2.726146249832675 + 0.000000000000000i
    # -1.955643591177653 + 3.364550574688863i
    # -1.955643591177653 - 3.364550574688863i
    #  4.493937056300693 + 0.000000000000000i
    # -1.631513006819252 + 4.555484848248613i
    # -1.631513006819252 - 4.555484848248613i
    # -1.677320660400946 + 7.496870451838560i
    # -1.677320660400946 - 7.496870451838560i
    #
    #

    A0 = Matrix(-1.0I, 3, 3)
    A1=[2.5    2.8   -0.5
        1.8    0.3    0.3
        -2.3   -1.4    3.5];
    A2=[1.7    0.7   -0.3
        -2.4   -2.1   -0.2
        2.0    0.7    0.4];
    A3=[1.4   -1.3    0.4
        1.4    0.7    1.0
        0.6    1.6    1.7];
    idop= S -> S
    oneop = S -> one(S)
    f1 = S -> exp(-S)
    N = 10
    f2 = S -> distributed_kernel_gauss_legendre(S, N)
    #N = 1000
    #f2 = S -> distributed_kernel_trapezoidal(Matrix(S), N)
    return SPMF_NEP([A0,A1,A2,A3],[idop,oneop,f1,f2])
end

function distributed_kernel_trapezoidal(S,N0)

    fS = x -> exp(x*S) * (exp((x+0.5)^2) - exp(1/4))

    F=zero(S)

    h=1/N0;
    for i=1:(N0+1) # Trapezoidal rule
        x=float((i-1)*h-1)
        if (i==1 || i==N0+1)
            F += 0.5*fS(x)*h;
        else
            F += fS(x)*h;
        end
    end
    return F
end
"""
Computes distributed kernel matrix function using
Gauss-Legender quadrature.
"""
function distributed_kernel_gauss_legendre(S,N)
    f=x-> (exp((x+0.5)^2)-exp(1/4))
    F=zero(S);
    xv,wv=gauss_legendre_weights(N,-1,0);
    local E = one(S)

    accumulative_expm_comp=true
    for i=1:length(xv)
        if (accumulative_expm_comp)
            # An accumulative way to compute E=exp(xv[i]*S) which is
            # faster due to the fact that scaling and squaring
            # for exp((xv[i]-xv[i-1])*S) is faster than
            # exp(xv[i]*S)
            if (i==1)
                E=exp(xv[1]*S);
            else
                E=E*exp((xv[i]-xv[i-1])*S)
            end
        else
            E=exp(xv[i]*S);

        end

        fSw=E*(f(xv[i])*wv[i]);
        F=F+fSw;
    end
    return F
end

function  gauss_legendre_weights(N,a,b)

    N1=N; N2=N+1;
    xu = range(-1, stop = 1, length = N1)

    # L will be the Legendre-Gauss Vandermonde Matrix
    L=zeros(N1,N2);

    # "Derivative" of L
    Lp=zeros(N1,N2);

    # Starting values of Newton's method
    y = cos.((2 * (0:(N-1)) .+ 1) * pi / (2 * (N-1) + 2)) + (0.27/N1) * sin.(pi * xu * (N-1) / N2)
    y0 = 2

    local Lp0
    # Newton's method to decide points
    # Iterate until new points are uniformly within epsilon of old points

    while maximum(abs.(y .- y0)) > eps()

        L[:,1]=ones(size(L,1));
        Lp[:,1]=zeros(size(Lp,1));

        L[:,2]=y;
        Lp[:,2]=ones(size(Lp,1));

        # Recurrence
        for k=2:N1
            L[:,k+1]=( (2*k-1)*(y).*L[:,k]-(k-1)*L[:,k-1] )/k;
        end

        Lp0 = N2 * (L[:,N1] - y .* L[:,N2]) ./ (1 .- y.^2)

        y0=y;
        y=y0-L[:,N2]./Lp0;
    end

    # Linear map from[-1,1] to [a,b]
    x = (a * (1 .- y) + b * (1 .+ y)) / 2

    # Compute the weights
    w = (b-a) ./ ((1 .- y.^2) .* Lp0.^2) * (N2/N1)^2

    return x,w
end


# # Implementation of the quartic root finding method described in:
# # A Geometric Interpretation of the Solution of the General Quartic Polynomial
# # Author(s): William M. Faucette
# # Source: The American Mathematical Monthly , Jan., 1996, Vol. 103, No. 1 (Jan., 1996), pp. 51-57
# # Published by: The Mathematical Association of America
# # Stable URL: https://www.jstor.org/stable/2975214

const S = [-1 1 1; -1 -1 -1; 1 1 -1; 1 -1 1] # permuting signs on the roots

"""
Defined by the taking the positive square root relationship
    (-x)^(1/2) = xsign(x)*sqrt(abs(x))
"""
function xsign(x::R) where {R<:Real}

    if sign(x) == 1.0
        return im
    elseif sign(x) == -1.0
        return 1.0
    else
        error("Sign of input value not comparable to ±1.0")
        return nothing
    end

end

"""
Return all the real roots of a quartic equation represented as a
vector of real coefficients for terms of increasing degree.
Note that any higher order coefficients (degree >= 5) are ignored.

Theory described in Faucette (1996).
"""
function solve_all_quartic_roots(polycoeff::Vector{T};
    warn_scaling::Bool=true,
    leading_tol::Float64=QUARTIC_ATOL,
    coeff_tol::Float64=1.0/QUARTIC_ATOL) where {T<:Real}

    Rts = Vector{Complex{Float64}}() # object to return with real roots inside.

    if length(polycoeff) < 5
        @error "There are too few coefficients given."
        return Rts
    end

    if warn_scaling && isapprox(polycoeff[5], zero(T), atol=leading_tol)
        @warn "The leading quartic coefficient is approximately zero. Returning empty array."
        ## TODO?: fall back to solving a cubic instead?
        return Rts
    end

    a = polycoeff[1:4]./polycoeff[5]

    if warn_scaling && poor_conditioning(a, coeff_tol)
        @warn "Large coefficients in scaled polynomial. Poor conditioning may occur."
    end

    # We reduce p(z) = a1 + a2*z + a3*z^2 + a4*z^3 + z^4 via z == x - a4/4 to
    # the standard transformed form P(x) = r + q*x + p*x^2 + x^4

    p = -3*(a[4]^2)/8   + a[3]
    q =    (a[4]^3)/8   - a[4]*a[3]/2 + a[2]
    r = -3*(a[4]^4)/256 + (a[4]^2)*a[3]/16 - a[4]*a[2]/4 + a[1]

    # The "resolvent cubic of P(x)" is h(z) = z^3 - 2p*z^2 + (p^2 - 4r)z + q^2.
    # We find it's roots:
    resolvent_coeff = [q^2, p^2 - 4r, -2p, 1]
    resolvent_roots = solve_all_cubic_roots(resolvent_coeff; warn_scaling, leading_tol, coeff_tol)

    alpha = convert(T,resolvent_roots[1])  # ensure first root is Real.
    beta, gamma = resolvent_roots[2:3]

    if !isreal(beta) && !isreal(gamma) && sign(alpha) == -1.0
        A = sqrt(-alpha)
        B = sqrt(-beta)
        C = sign(q)*sqrt(-gamma)
    elseif isreal(beta) && isreal(gamma)
        A = xsign(alpha)*sqrt(abs(alpha))
        B = xsign(real(beta))*sqrt(abs(real(beta)))
        C_sign = sign(q)/(xsign(alpha)*xsign(real(beta)))
        C = C_sign*sqrt(abs(real(gamma)))
    else
        error("Unrecognised case.")
    end

    R_res = S*([A; B; C])/2
    # R_res = S*(sqrt.(-resolvent_roots))/2

    # # A, B, C = sqrt.(-resolvent_roots)
    # # x1 = (-A + B + C)/2
    # # x2 = (-A - B - C)/2
    # # x3 = ( A + B - C)/2
    # # x4 = ( A - B + C)/2

    append!(Rts, R_res .- a[4]/4)

    return Rts
end


using MLJ
using PrettyPrinting
import DataFrames
import Statistics
using PyPlot
using StableRNGs

LGBMRegressor = @load LGBMRegressor

features, targets = @load_boston
features = DataFrames.DataFrame(features)
@show size(features)
@show targets[1:3]
first(features, 3) |> pretty

DataFrames.describe(features)

train, test = partition(collect(eachindex(targets)), 0.70, shuffle=true,
                        rng=StableRNG(52))

lgb = LGBMRegressor() #initialised a model with default params
lgbm = machine(lgb, features[train, :], targets[train, 1])
boostrange = range(lgb, :num_iterations, lower=2, upper=500)
curve = learning_curve!(lgbm, resampling=CV(nfolds=5),
                        range=boostrange, resolution=100,
                        measure=rms)

figure(figsize=(8,6))
plot(curve.parameter_values, curve.measurements)
xlabel("Number of rounds", fontsize=14)
ylabel("RMSE", fontsize=14)

lgb = LGBMRegressor() #initialised a model with default params
lgbm = machine(lgb, features[train, :], targets[train, 1])
learning_range = range(lgb, :learning_rate, lower=1e-3, upper=1, scale=:log)
curve = learning_curve!(lgbm, resampling=CV(nfolds=5),
                        range=learning_range, resolution=100,
                        measure=rms)

figure(figsize=(8,6))
plot(curve.parameter_values, curve.measurements)
xscale("log")
xlabel("Learning rate (log scale)", fontsize=14)
ylabel("RMSE", fontsize=14)

lgb = LGBMRegressor() #initialised a model with default params
lgbm = machine(lgb, features[train, :], targets[train, 1])

leaf_range = range(lgb, :min_data_in_leaf, lower=1, upper=50)

curve = learning_curve!(lgbm, resampling=CV(nfolds=5),
                        range=leaf_range, resolution=50,
                        measure=rms)

figure(figsize=(8,6))
plot(curve.parameter_values, curve.measurements)
xlabel("Min data in leaf", fontsize=14)
ylabel("RMSE", fontsize=14)

r1 = range(lgb, :num_iterations, lower=50, upper=100)
r2 = range(lgb, :min_data_in_leaf, lower=2, upper=10)
r3 = range(lgb, :learning_rate, lower=1e-1, upper=1e0)
tm = TunedModel(model=lgb, tuning=Grid(resolution=5),
                resampling=CV(rng=StableRNG(123)), ranges=[r1,r2,r3],
                measure=rms)
mtm = machine(tm, features, targets)
fit!(mtm, rows=train);

best_model = fitted_params(mtm).best_model
@show best_model.learning_rate
@show best_model.min_data_in_leaf
@show best_model.num_iterations

predictions = MLJ.predict(mtm, rows=test)
rms_score = round(rms(predictions, targets[test, 1]), sigdigits=4)

@show rms_score

# This file was generated using Literate.jl, https://github.com/fredrikekre/Literate.jl


#=
Copyright (c) 2015, Intel Corporation
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
=#

using HPAT
using MPI
using DocOpt

@acc hpat function kmeans(numCenter, iterNum, file_name)
    points = DataSource(Matrix{Float64},HDF5,"/points", file_name)
    D,N = size(points) # number of features, instances
    centroids = rand(D, numCenter)

    for l in 1:iterNum
        dist::Array{Array{Float64,1},1} = [ Float64[sqrt(sum((points[:,i]-centroids[:,j]).^2)) for j in 1:numCenter] for i in 1:N]
        labels::Array{Int,1} = [indmin(dist[i]) for i in 1:N]
        centroids::Array{Float64,2} = [ sum(points[j,labels.==i])/sum(labels.==i) for j in 1:D, i in 1:numCenter]
    end
    return centroids
end

function main()
    doc = """K-means clustering algorithm.

Usage:
  kmeans.jl -h | --help
  kmeans.jl [--iterations=<iterations>] [--file=<file>] [--centers=<centers>]

Options:
  -h --help                  Show this screen.
  --iterations=<iterations>  Specify number of iterations; defaults to 20.
  --file=<file>              Specify input file; defaults to HPAT's default generated data file.
  --centers=<centers>        Specify number of centers; defaults to 5.

"""
    arguments = docopt(doc)

    if (arguments["--iterations"] != nothing)
        iterations = parse(Int, arguments["--iterations"])
    else
        iterations = 20
    end

    if (arguments["--file"] != nothing)
        file_name::ASCIIString = arguments["--file"]
    else
        file_name = HPAT.getDefaultDataPath()*"kmeans_large.hdf5"
    end

    if (arguments["--centers"] != nothing)
        numCenter = parse(Int, arguments["--centers"])
    else
        numCenter = 5
    end

    srand(0)
    rank = MPI.Comm_rank(MPI.COMM_WORLD)
    pes = MPI.Comm_size(MPI.COMM_WORLD)

    if rank==0 println("iterations = ", iterations) end
    if rank==0 println("file= ", file_name) end
    if rank==0 println("centers= ", numCenter) end

    tic()
    kmeans(numCenter, 2, file_name)
    time = toq()
    if rank==0 println("SELFPRIMED ", time) end
    MPI.Barrier(MPI.COMM_WORLD)

    tic()
    centroids_out = kmeans(numCenter, iterations, file_name)
    time = toq()
    if rank==0 println("result = ", centroids_out) end
    if rank==0 println("rate = ", iterations / time, " iterations/sec") end
    if rank==0 println("SELFTIMED ", time) end

end

main()


# estimate the mean by numerical integration over uniform percentiles
estimateMean(d::ContinuousUnivariateDistribution;kwargs...) =
  meanFunOfProb(d;kwargs...,fun=(d,p)->quantile(d,p))

# estimate variance by numerical integration over uniform percentiles
function estimateVariance(d::ContinuousUnivariateDistribution; mean=missing, kwargs...)
    m = ismissing(mean) ? Distributions.mean(d) : mean
    function squaredDiff(d,p)
        t = quantile(d, p) - m
        t*t
    end
    meanFunOfProb(d;kwargs...,fun=squaredDiff)
end

"""
compute mean over a function(uniformly distributed probabilities)

used to estimate moments of logitnorm
"""
function meanFunOfProb(d::ContinuousUnivariateDistribution;relPrec = 1e-4, maxCnt=2^18, fun=(d,p)->quantile.(d,p) )
    δ = 1/32 # start with 31 points (32 intervals between 0 and 1)
    p = δ:δ:(1-δ)
    # for K=1/δ intervals, there are (K-1) points at c_i
    # The first points at δ represents interval (δ/2,3/2δ)
    # The following picture shows points and intervals for K = 4
    #---|---|---|---#
    # |---|---|---| #
    # we need to add points for δ/4 and 1-δ/4 representing the edges
    # but their weight is only half, because they represents half an inverval
    #m = sum(c_i*δ) + el*(δ/2) + er*(δ/2) = (sum(c_i) + er/2 + el/2)*δ
    s = sum(fun.(d,p))   # sum at points c_i
    el = fun(d,δ/4)  #
    er = fun(d,1-δ/4)
    m = (s + el/2 + er/2)*δ
    relErr = 1
    while 1/δ < maxCnt
        mPrev = m
        δ  = δ / 2
        # to double the number of reference points, half the interval
        # for each second point we already computed fun
        # only need to add the new points to the sum of central points
        p = δ:δ*2:(1-δ) # points at the center of current intervals
        s += sum(fun.(d,p))
        el = fun(d,δ/4)
        er = fun(d,1-δ/4)
        m = (s + el/2 + er/2)*δ
        relErr = abs(m - mPrev)/m
        #println("cnt=$(1/δ), m=$m, mPrev=$mPrev, relErr=$relErr")
        #if the estimate did not change much, can return
        relErr <= relPrec && break
    end
    relErr > relPrec && @warn "Returning meanFunOfProb results of low relative precision of $relErr"
    m
end


#=
Test function

Properties
----------

- There are 9 steady states:
    - Stable
        - (-2.39,2.35)    (2.3,-2.34) (-2.18,-2.12)   (2.06,2.12)
    - Unstable
        - (-2.27,-0.23)   (2.17,0.22) (-0.12,-2.23)   (0.33,2.22) (0.1,0.01)

Reference
---------
1. Source code from https://static-content.springer.com/esm/art%3A10.1038%2Fs41598-017-15889-2/MediaObjects/41598_2017_15889_MOESM2_ESM.rar
=#

module SampleDE

"""
Reference
---------
- `ODE.m`
"""
function ODE!(du,u,p,t)
    du[1]= -1 + 9 *u[1] - 2*u[1]^3 + 9 *u[2] - 2*u[2]^3;
    du[2]=  1 - 11*u[1] + 2*u[1]^3 + 11*u[2] - 2*u[2]^3;
end

function ODE(u,p,t)
    du = deepcopy(u)
    return ODE!(du,u,p,t)
end

u0 = [1.0, 2.0]
p = [2.0, 3.0]

"""
Reference
---------
- `exact_potential.m`
"""
function exact_potential(points, RefPointNum)
    pot = zeros(length(points), 1)
    for i in 1:length(points)
        x = points[i][1]; y = points[i][2];
        pot[i] = -5*(x^2+y^2)+(x^4+y^4)/2+x*y+x;
    end
    pot = pot .- pot[RefPointNum]
    return pot
end

"""
Reference
---------
- `Readme.pdf`
"""
ODE_features = Dict(
    "StableStates" => [ (-2.39,2.35),(2.3,-2.34), (-2.18,-2.12),   (2.06,2.12)  ],
    "UnstableStates" => [(-2.27,-0.23) ,  (2.17,0.22), (-0.12,-2.23),   (0.33,2.22), (0.1,0.01)],
    "SteadyStates" => [(-2.39,2.35),(2.3,-2.34), (-2.18,-2.12),   (2.06,2.12), (-2.27,-0.23) ,  (2.17,0.22), (-0.12,-2.23),   (0.33,2.22) ,(0.1,0.01)]
)

function gradient(dt,x)

    x = reshape(x, 2, 3)

    F=[ [(dt*(1/dt - 6*x[1, 1]^2 + 9)*(9*x[1, 1] + 9*x[2, 1] + (x[1, 1] - x[1, 2])/dt - 2*x[1, 1]^3 - 2*x[2, 1]^3 - 1))/2 + (dt*(6*x[1, 1]^2 - 11)*(11*x[2, 1] - 11*x[1, 1] + (x[2, 1] - x[2, 2])/dt + 2*x[1, 1]^3 - 2*x[2, 1]^3 + 1))/2,x[1, 1]^3 - (9*x[2, 1])/2 - (x[1, 1] - x[1, 2])/(2*dt) - (9*x[1, 1])/2 + x[2, 1]^3 + (dt*(1/dt - 6*x[1, 2]^2 + 9)*(9*x[1, 2] + 9*x[2, 2] + (x[1, 2] - x[1, 3])/dt - 2*x[1, 2]^3 - 2*x[2, 2]^3 - 1))/2 + (dt*(6*x[1, 2]^2 - 11)*(11*x[2, 2] - 11*x[1, 2] + (x[2, 2] - x[2, 3])/dt + 2*x[1, 2]^3 - 2*x[2, 2]^3 + 1))/2 + 1/2,x[1, 2]^3 - (9*x[2, 2])/2 - (x[1, 2] - x[1, 3])/(2*dt) - (9*x[1, 2])/2 + x[2, 2]^3 + 1/2 ],[
    (dt*(1/dt - 6*x[2, 1]^2 + 11)*(11*x[2, 1] - 11*x[1, 1] + (x[2, 1] - x[2, 2])/dt + 2*x[1, 1]^3 - 2*x[2, 1]^3 + 1))/2 - (dt*(6*x[2, 1]^2 - 9)*(9*x[1, 1] + 9*x[2, 1] + (x[1, 1] - x[1, 2])/dt - 2*x[1, 1]^3 - 2*x[2, 1]^3 - 1))/2,(11*x[1, 1])/2 - (11*x[2, 1])/2 - (x[2, 1] - x[2, 2])/(2*dt) - x[1, 1]^3 + x[2, 1]^3 + (dt*(1/dt - 6*x[2, 2]^2 + 11)*(11*x[2, 2] - 11*x[1, 2] + (x[2, 2] - x[2, 3])/dt + 2*x[1, 2]^3 - 2*x[2, 2]^3 + 1))/2 - (dt*(6*x[2, 2]^2 - 9)*(9*x[1, 2] + 9*x[2, 2] + (x[1, 2] - x[1, 3])/dt - 2*x[1, 2]^3 - 2*x[2, 2]^3 - 1))/2 - 1/2,(11*x[1, 2])/2 - (11*x[2, 2])/2 - (x[2, 2] - x[2, 3])/(2*dt) - x[1, 2]^3 + x[2, 2]^3 - 1/2]]
    return hcat(F...)'
end

end


using Plots
using ImplicitEquations
using ForwardDiff

a, b = 1 / √2, 1 / √2
f(x, y) = x^2 + y^2 - 1
f(xy) = f(xy[1], xy[2])
g = xy -> ForwardDiff.gradient(f, xy)

"""
x' = x-a
y' = y-b
"""
function shift(f::Function, a, b)
    t = [
        1 0 a;
        0 1 b;
        0 0 1
    ]
    function tf(xy)
        x, y = xy
        tx, ty, tz = t * [x, y, 1]
        txy = [tx, ty]
        return f(txy)
    end
    tf(x, y) = tf([x, y])
    return tf
end

"""
x'' = x'
y'' = f_x(a,b)*x' + f_y(a,b)*y'
"""
function cvt(f, a, b)
    fx, fy = g([a, b])
    t = [
        1 0 0;
        -fx / fy 1 / fy 0;
        0 0 1
    ]
    t = t / fy
    function tf(xy)
        x, y = xy
        tx, ty, tz = t * [x, y, 1]
        txy = [tx, ty]
        return f(txy)
    end
    tf(x, y) = tf([x, y])
    return tf
end

plot(aspect_ratio = :equal)
plot!(Eq(f, 0))
sf = shift(f, a, b)
plot!(Eq(sf, 0))
plot!(Eq(cvt(sf, a, b), 0))


#=
this file includes functionality for estimating linear scaling regions and
defines the `generalized_dim` function.
=#

export linear_region, linear_regions, estimate_boxsizes, linreg
#####################################################################################
# Functions and methods to deduce linear scaling regions
#####################################################################################
using Statistics
using Statistics: covm, varm
# The following function comes from a version in StatsBase that is now deleted
# StatsBase is copyrighted under the MIT License with
# Copyright (c) 2012-2016: Dahua Lin, Simon Byrne, Andreas Noack, Douglas Bates,
# John Myles White, Simon Kornblith, and other contributors.
"""
    linreg(x, y) -> a, b
Perform a linear regression to find the best coefficients so that the curve:
`z = a + b*x` has the least squared error with `y`.
"""
function linreg(x::AbstractVector, y::AbstractVector)
    # Least squares given
    # Y = a + b*X
    # where
    # b = cov(X, Y)/var(X)
    # a = mean(Y) - b*mean(X)
    if size(x) != size(y)
        throw(DimensionMismatch("x has size $(size(x)) and y has size $(size(y)), " *
            "but these must be the same size"))
    end
    mx = Statistics.mean(x)
    my = Statistics.mean(y)
    # don't need to worry about the scaling (n vs n - 1)
    # since they cancel in the ratio
    b = covm(x, mx, y, my)/varm(x, mx)
    a = my - b*mx
    return a, b
end

slope(x, y) = linreg(x, y)[2]

"""
    linear_regions(x, y; dxi::Int = 1, tol = 0.25) -> (lrs, tangents)
Identify regions where the curve `y(x)` is linear, by scanning the
`x`-axis every `dxi` indices sequentially
(e.g. at `x[1] to x[5], x[5] to x[10], x[10] to x[15]` and so on if `dxi=5`).

If the slope (calculated via linear regression) of a region of width `dxi` is
approximatelly equal to that of the previous region,
within tolerance `tol`,
then these two regions belong to the same linear region.

Return the indices of `x` that correspond to linear regions, `lrs`,
and the _correct_ `tangents` at each region
(obtained via a second linear regression at each accumulated region).
"""
function linear_regions(
        x::AbstractVector, y::AbstractVector;
        method = :sequential, dxi::Int = method == :overlap ? 3 : 1, tol = 0.25,
    )
    @assert length(x) == length(y)
    return if method == :overlap
        linear_regions_overlap(x, y, dxi, tol)
    elseif method == :sequential
        linear_regions_sequential(x, y, dxi, tol)
    end
end

function linear_regions_sequential(x, y, dxi, tol)
    maxit = length(x) ÷ dxi

    tangents = Float64[slope(view(x, 1:max(dxi, 2)), view(y, 1:max(dxi, 2)))]

    prevtang = tangents[1]
    lrs = Int[1] #start of first linear region is always 1
    lastk = 1

    # Start loop over all partitions of `x` into `dxi` intervals:
    for k in 1:maxit-1
        tang = slope(view(x, k*dxi:(k+1)*dxi), view(y, k*dxi:(k+1)*dxi))
        if isapprox(tang, prevtang, rtol=tol, atol = 0)
            # Tanget is similar with initial previous one (based on tolerance)
            continue
        else
            # Tangent is not similar.
            # Push new tangent for a new linear region
            push!(tangents, tang)

            # Set the START of a new linear region
            # which is also the END of the previous linear region
            push!(lrs, k*dxi)
            lastk = k
        end

        # Set new previous tangent (only if it was not the same as current)
        prevtang = tang
    end
    push!(lrs, length(x))
    # create new tangents that do have linear regression weighted
    tangents = Float64[]
    for i in 1:length(lrs)-1
        push!(tangents, linreg(view(x, lrs[i]:lrs[i+1]), view(y ,lrs[i]:lrs[i+1]))[2])
    end
    return lrs, tangents
end

"""
    linear_region(x, y; kwargs...) -> ((ind1, ind2), slope)
Call [`linear_regions`](@ref) and identify and return the largest linear region
and its slope. The region starts and stops at `x[ind1:ind2]`.

The keywords `dxi, tol` are propagated as-is to [`linear_regions`](@ref).
The keyword `ignore_saturation = true` ignores saturation that (sometimes) happens
at the start and end of the curve `y(x)`, where the curve flattens.
The keyword `sat = 0.01` decides what saturation is (while `abs(y[i]-y[i+1])<sat` we
are in a saturation regime).

The keyword `warning = true` prints a warning if the linear region is less than 1/3
of the available x-axis.
"""
function linear_region(x::AbstractVector, y::AbstractVector;
    dxi::Int = 1, tol::Real = 0.2, ignore_saturation = true, warning = true, sat = 0.01)

    isat = 0
    if ignore_saturation
        j = findfirst(i -> abs(y[i] - y[i-1]) > sat, length(y):-1:2)
        if !isnothing(j)
            i = (length(y):-1:2)[j]
            x, y = x[1:i], y[1:i]
        end
        k = findfirst(i -> abs(y[i+1] - y[i]) > sat, 1:length(y)-1)
        if !isnothing(k)
            x, y = x[k:end], y[k:end]
            isat = k-1
        end
    end

    lrs, tangents = linear_regions(x,y; dxi, tol)
    # Find biggest linear region:
    j = findmax(diff(lrs))[2]
    if lrs[j+1] - lrs[j] ≤ length(x)÷3 && warning
        @warn "Found linear region spans less than a 3rd of the available x-axis "*
              "and might imply inaccurate slope or insufficient data. "*
              "Recommended: plot `x` vs `y`."
    end
    return (lrs[j] + isat, lrs[j+1] + isat), tangents[j]
end

#####################################################################################
# Autotomatic estimation for proper `ε` from a Dataset
#####################################################################################
"""
    estimate_boxsizes(A::Dataset; kwargs...) → εs
Return `k` exponentially spaced values: `εs = base .^ range(lower + w, upper + z; length = k)`,
that are a good estimate for sizes ε that are used in calculating a [Fractal Dimension](@ref).
It is strongly recommended to [`standardize`](@ref) input dataset `A` before using this
function.

Let `d₋` be the minimum pair-wise distance in `A` and `d₊` the average total length of `A`
along each of the dimensions of `A`.
Then `lower = log(base, d₋)` and `upper = log(base, d₊)`.
Because by default `w=1, z=-1`, the returned sizes are an order of mangitude
larger than the minimum distance, and an order of magnitude smaller than the maximum
distance.

## Keywords
* `w = 1, z = -1, k = 20` : as explained above.
* `base = MathConstants.e` : the base used in the `log` function.
* `warning = true`: Print some warnings for bad estimates.
* `autoexpand = true`: If the final estimated range does not cover at least 2 orders of
  magnitude, it is automatically expanded by setting `w -= we` and `z -= ze`.
  You can set different default values to the keywords `we = w, ze = z`.
"""
function estimate_boxsizes(
        A::AbstractDataset;
        k::Int = 20, z = -1, w = 1, base = MathConstants.e,
        warning = true, autoexpand = true, ze = z, we = w
    )

    mi, ma = minmaxima(A)
    max_d = mean(ma - mi)
    min_d, _ = minimum_pairwise_distance(A)
    if min_d == 0 && warning
        @warn(
        "Minimum distance in the dataset is zero! Probably because of having data "*
        "with low resolution, or duplicate data points. Setting to `d₊/base^4` for now.")
        min_d = max_d/(base^4)
    end

    lower = log(base, min_d)
    upper = log(base, max_d)

    if lower ≥ upper
        error("`lower ≥ upper`. There must be something fundamentally wrong with dataset.")
    elseif lower+w ≥ upper+z && warning
        @warn(
        "Automatic boxsize determination was inappropriate: `lower+w` was found ≥ than "*
        "`upper+z`. Returning `base .^ range(lower, upper; length = k)`. "*
        "Please adjust keywords or provide a bigger dataset.")
        εs = float(base) .^ range(lower, upper; length = k)
    elseif abs(upper+z - (lower+w)) < 2 && autoexpand
        if warning
            @warn(
            "Boxsize limits do not differ by at least 2 orders of magnitude. "*
            "Setting `w-=$(we)` and `z+=$(ze)`, please adjust keywords `w, z` otherwise.")
        end
        εs = float(base) .^ range(lower+w-we, upper+z-ze; length = k)
    else
        εs = float(base) .^ range(lower+w, upper+z; length = k)
    end
    return εs
end

"""
    minimum_pairwise_distance(A::Dataset, metric = Euclidean())
Return `min_d, min_pair`: the minimum pairwise distance
of all points in the dataset, and the corresponding point pair.
"""
function minimum_pairwise_distance(A::AbstractDataset, metric = Euclidean())
    tree = KDTree(A, metric)
    min_d = eltype(A[1])(Inf)
    max_d = -min_d
    min_pair = max_pair = (0, 0)
    theiler = Theiler(0)
    for i in 1:length(A)
        inds, dists = Neighborhood.knn(tree, A[i], 1, theiler(i); sortds=false)
        ind, dist = inds[1], dists[1]
        if dist < min_d
            min_d = dist
            min_pair = (i, ind)
        end
    end
    return min_d, min_pair
end


module MatrixChainMultiply

export matrixchainmultiply

### mcm = matrix chain multiply

"""
  `matrixchainmultiply(A,B,C,D,...)`

Algorithm for **Matrix-chain multiplication** (where matrices are also
generalizations of scalars and vectors). Using *Cormen* Ed. 3, p. 371.

Arguments
---------
`A,B,C,...` are matrices, vectors, or scalars of appropriate sizes.

"""
function matrixchainmultiply(A...)
  p = mcm_makep(A...)
  m, s, n = mcm_cost_matrices(p)
  mcm_compute(s, 1, n, A...)
end
"""
  `matrixchainmultiply(fn_name::String, A...)`

Print out the optimal matrix order as an expresion and its
cost. Also use this to...

Construc a function for repeated use (Julia v0.6-?)
---------------------------------------------------
Sometimes, you will do a chain multiplication many times with matrices of the same size. Another use case for this is to do the analysis on the CPU but use the generated function on the GPU. In either case, you only need to find the optimal order and JIT compile that function once. To save this function to the global scope:

```julia
a = rand(Float32, 10000,2000)
b = rand(Float32, 2000,100)
c = rand(Float32, 100)

eval(matrixchainmultiply("mcm_abc", a,b,c))

mcm_abc(a,b,c)

# multiple-dispatch is nice; taking advantage below
using ArrayFire
aa = AFArray(a)
ab = AFArray(b)
ac = AFArray(c)

mcm_abc(aa,ab,ac)  # no new analysis; ke
```
"""
function matrixchainmultiply(fn_name::String, A...)
  matrixchainmultiply_fn(fn_name, A...)
end
function matrixchainmultiply_fn(fn_name::String, A...)
  p = mcm_makep(A...)
  m, s, n = mcm_cost_matrices(p)
  ex = parse(fn_name * "(A...) = " * mcm_print(s, 1, n))
  cost = m[1,n]
  println("Operation: $ex")
  println("Cost: $cost")
  ex
end

"Cost information tells which order to do."
function mcm_cost_matrices(p::Vector{Int})
  ∞ = typemax(Int)  # watch for overflow?
  n = length(p) - 1  # number of matrices
  @assert (n > 1) "TODO: allow to use for short lists"
  # stores costs
  m = zeros(Int, (n, n))
  # records corresponding indices
  s = zeros(Int, (n-1, n-1))  # Cormen has 2nd index going 2..n
  # zero on diagonals
  for i ∈ 1:n
    m[i,i] = 0
  end
  # l is chain length
  for l ∈ 2:n
    for i ∈ 1:(n-l+1)
      j = i + l - 1
      m[i,j] = ∞  # this should be replaced?!
      for k ∈ i:(j-1)
        q1 = m[i,k]
        q2 = m[k+1,j]
        q3 = p[i] * p[k+1] * p[j+1]  # Cormen p vec indexed from 0
        q = +(q1, q2, q3)
        if q < m[i,j]
          m[i,j] = q
          s[i,j-1] = k
        end
      end
    end
  end
  m, s, n
end

"Use this if you want to see what the operation looks like."
function mcm_print(s::Matrix, i::Int, j::Int)
  str = ""
  if i!=j
    a = mcm_print(s, i, s[i, j-1])
    b = mcm_print(s, s[i, j-1] + 1, j)
    return "($a * $b)"
  else
    return "A[$i]"
  end
end

"""
The function describing the optimal order for the input of a specific size.
"""
function mcm_optimalorder(s, i, j, mats...)
  if i!=j
    m1 = mcm_compute(s, i, s[i, j-1], mats...)
    m2 = mcm_compute(s, s[i, j-1] + 1, j, mats...)
    return :(m1 * m2)
  else
    return mats[i]
  end
end

"Do the actual matrix chain multiplication computation."
function mcm_compute(s, i, j, mats...)
  if i!=j
    m1 = mcm_compute(s, i, s[i, j-1], mats...)
    m2 = mcm_compute(s, s[i, j-1] + 1, j, mats...)
    return (m1 * m2)
  else
    return mats[i]
  end
end

"The matrix sizes."
function mcm_makep(mats...)
  # Made some assumptions below that disallow these
  lenmats = length(mats)
  # convert to matrix-style sizes
  msizes = map(msize, mats)
  # produce unique number list
  p = zeros(Int, lenmats + 1)
  for i in 1:lenmats
    firstiter::Bool = (i==1)
    if firstiter
      p[1] = msizes[1][1]
    else
      check1 = msizes[i-1][2]
      check2 = msizes[i][1]
      @assert (check1 == check2) "Matrix sizes don't match ($check1 != $check2)!"
    end
    p[i+1] = msizes[i][2]
  end
  p
end

###

"""
Matrix-style sizes. Return type looks like `(i,j)` where `i` and `j` are `Ints`.

If other packages with matrix types (e.g., OpenCl or ArrayFire) inherit from
AbstractArray, there shouldn't be issues with other packages using this one.
"""
msize(m::AbstractMatrix) = size(m)
msize(v::AbstractVector) = (size(v,1), 1)
msize(s::Number) = (1, 1)

end # module


#### Beta diversity calculation functions

## Function to calculate beta diversity statistics
"""
    betadiv(Y::Matrix)

Computes the beta diversity statistics from the community matrix `Y` based on
Legendre & De Cáceres (2013). This function was based on the `beta.div` R
function from the supplementary material of that paper, and results were also
tested against the `beta.div` function from `adespatial`.
"""
function betadiv(Y::Matrix)
    @assert !any(isnothing, Y) "Y must only contain observed sites and no nothing values"
    # S -> squared deviations from column mean
    S = (Y .- mean(Y; dims=1)) .^ 2.0
    # SStotal -> total sum of squares
    SStotal = sum(S)
    # BDtotal -> index of beta diversity, unbiased & comparable estimator of Var(Y)
    BDtotal = SStotal / (size(Y, 1) - 1)
    # SSj -> sum of squares for species j
    SSj = sum(S; dims=1)
    # SCBDj -> species contribution to beta diversity (species j, relative)
    SCBDj = SSj ./ SStotal
    # SSi -> sum of squares for site i
    SSi = sum(S; dims=2)
    # LCBD -> local contribution to beta diversity (site i, relative)
    LCBDi = SSi ./ SStotal
    # Combine results in tuple
    res = (
        S=S, SStotal=SStotal, BDtotal=BDtotal, SSj=SSj, SCBDj=SCBDj, SSi=SSi, LCBDi=LCBDi
    )
    return res
end


logistic(x) = 1 ./ (1 + exp.(-x))
@scalardiff logistic(x::Number) 1 (logistic(x) .* (1 .- logistic(x)))

function autoencoder_cost(We1, We2, Wd, b1, b2, x)
    firstLayer = logistic(We1 * x .+ b1)
    encodedInput = logistic(We2 * firstLayer .+ b2)
    reconstructedInput = logistic(Wd * encodedInput)
    cost = sum((reconstructedInput .- x) .^ 2.0)
    return cost
end

function mlp1(w1, w2, w3, x1)
    xx2 = w1 * x1
    x2 = log.(1. + exp.(xx2))
    xx3 = w2 * x2
    x3 = log.(1. + exp.(xx3))
    x4 = w3 * x3
    sum(1. ./ (1. + exp.(-x4)))
end

function mlp2(w1, w2, w3, b1, b2,  b3, x1)
    x2 = logistic(w1 * x1 .+ b1)
    x3 = logistic(w2 * x2 .+ b2)
    x4 = logistic(w3 * x3 .+ b3)
    sum(x4)
end

function rnn(Wxh, Whh, Wy, hprev, x, y)
    h = tanh.(Whh * hprev + Wxh * x)
    yhat = Why * h
    cost = sum((yhat .- y) .^ 2.0)
    return h, cost
end


using SymPy

function lagrange(order)

  is=collect(-floor(Int,order/2) : 1 : floor(Int,order/2));
  x=symbols("x");
  L=x^0;
  Ls=Array(typeof(L),(order,));

  for j = 1 : order
    ith=is[j];
    for i in is
      if i!= ith
        L=L*(x-i)/(ith-i);
      end
    end
    Ls[j]=copy(L);
    L=x^0;
  end

  return Ls;

end

function fd(order,d;t=Rational)
  Ls=lagrange(order);
  x =symbols("x");

  is=collect(-floor(Int,order/2) : 1 : floor(Int,order/2));
  D=Array(typeof(x),(order,order));
  for i = 1 : order
    for j = 1 : order
      D[i,j] =diff(Ls[j],x,d)(x=>is[i]);
    end
  end

  return Array{t}(D);

end

order=4
D=fd(order+1,2,t=Float64);