"""
 Tests for  https://link.springer.com/article/10.1007/s10543-007-0130-3 with test problems as in the paper.
 RS1, RS2
 and for https://www.sciencedirect.com/science/article/pii/S0377042706003906
 NON, COM, NON2
"""

import Statistics # for mean values of trajectories
import LinearAlgebra # for the normn
using StochasticDiffEq
using Test
using Random
using DiffEqDevTools
#using DiffEqGPU

function prob_func(prob, i, repeat)
    remake(prob,seed=seeds[i])
end

"""
 Test Scalar SDEs (oop)
"""

@info "Scalar oop noise"

numtraj = Int(5e6) # in the paper they use 1e9
u₀ = 0.1
p = [1.5, 0.1]
f(u,p,t) = p[1]*u
g(u,p,t) = p[2]*u
dts = 1 .//2 .^(5:-1:0)
tspan = (0.0,1.0)

h1(z) = z

seed = 250
Random.seed!(seed)
seeds = rand(UInt, numtraj)

prob = SDEProblem(f,g,u₀,tspan, p)
ensemble_prob = EnsembleProblem(prob;
        output_func = (sol,i) -> (h1(sol[end]),false),
        prob_func = prob_func
        )

sim = test_convergence(dts,ensemble_prob,RS1(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=u₀*exp(1.0*(p[1]+0.5*p[2]^2))
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.3
println("RS1:", sim.𝒪est[:weak_final])

sim = test_convergence(dts,ensemble_prob,RS2(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=u₀*exp(1.0*(p[1]+0.5*p[2]^2))
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.39 #order is 2.36
println("RS2:", sim.𝒪est[:weak_final])

sim = test_convergence(dts,ensemble_prob,NON(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=u₀*exp(1.0*(p[1]+0.5*p[2]^2))
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.5
println("NON:", sim.𝒪est[:weak_final])

numtraj = Int(5e6)
seed = 100
Random.seed!(seed)
seeds = rand(UInt, numtraj)
sim = test_convergence(dts,ensemble_prob,NON2(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=u₀*exp(1.0*(p[1]+0.5*p[2]^2))
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.3
println("NON2:", sim.𝒪est[:weak_final])

dts = 1 .//2 .^(4:-1:0)
numtraj = Int(1e6)
seed = 10
Random.seed!(seed)
seeds = rand(UInt, numtraj)

sim = test_convergence(dts,ensemble_prob,COM(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=u₀*exp(1.0*(p[1]+0.5*p[2]^2))
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.3
println("COM:", sim.𝒪est[:weak_final])

"""
 Test Scalar SDEs (iip)
"""

@info "Scalar iip noise"

f!(du,u,p,t) = du[1] = p[1]*u[1]
g!(du,u,p,t) = du[1] = p[2]*u[1]

numtraj = Int(5e6)
seed = 100
Random.seed!(seed)
seeds = rand(UInt, numtraj)
dts = 1 .//2 .^(5:-1:0)

prob = SDEProblem(f!,g!,[u₀],tspan, p)
ensemble_prob = EnsembleProblem(prob;
        output_func = (sol,i) -> (h1(sol[end][1]),false),
        prob_func = prob_func
        )

sim = test_convergence(dts,ensemble_prob,RS1(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=u₀*exp(1.0*(p[1]+0.5*p[2]^2))
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.4
println("RS1:", sim.𝒪est[:weak_final])

sim = test_convergence(dts,ensemble_prob,RS2(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=u₀*exp(1.0*(p[1]+0.5*p[2]^2))
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.4 #order is 2.36
println("RS2:", sim.𝒪est[:weak_final])

sim = test_convergence(dts,ensemble_prob,NON(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=u₀*exp(1.0*(p[1]+0.5*p[2]^2))
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.3
println("NON:", sim.𝒪est[:weak_final])

numtraj = Int(5e6)
seed = 100
Random.seed!(seed)
seeds = rand(UInt, numtraj)
sim = test_convergence(dts,ensemble_prob,NON2(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=u₀*exp(1.0*(p[1]+0.5*p[2]^2))
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.3
println("NON2:", sim.𝒪est[:weak_final])

dts = 1 .//2 .^(4:-1:0)
numtraj = Int(1e6)
seed = 10
Random.seed!(seed)
seeds = rand(UInt, numtraj)

sim = test_convergence(dts,ensemble_prob,COM(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=u₀*exp(1.0*(p[1]+0.5*p[2]^2))
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.3
println("COM:", sim.𝒪est[:weak_final])

"""
 Test Diagonal noise SDEs (iip)
"""

@info "Diagonal noise"

u₀ = [0.1,0.1]
function f3!(du,u,p,t)
  du[1] = 299//200*u[1]
  du[2] = 299//200*u[2]
end
function g3!(du,u,p,t)
  du[1] = 1//10*u[1]
  du[2] = 1//10*u[2]
end
dts = 1 .//2 .^(4:-1:0)
tspan = (0.0,1.0)

h3(z) = z^2 # == 1//10**exp(3//2*t) if h3(z) = z and  == 1//100**exp(301//100*t) if h3(z) = z^2 )

prob = SDEProblem(f3!,g3!,u₀,tspan)
ensemble_prob = EnsembleProblem(prob;
        output_func = (sol,i) -> (h3(sol[end][1]),false),
        prob_func = prob_func
        )

numtraj = Int(4e4)
seed = 100
Random.seed!(seed)
seeds = rand(UInt, numtraj)

sim = test_convergence(dts,ensemble_prob,RS1(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=1//100*exp(301//100)
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.34 # order is 1.67
println("RS1:", sim.𝒪est[:weak_final])

sim = test_convergence(dts,ensemble_prob,RS2(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=1//100*exp(301//100)
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.3
println("RS2:", sim.𝒪est[:weak_final])

numtraj = Int(1e5)
seed = 100
Random.seed!(seed)
seeds = rand(UInt, numtraj)

sim = test_convergence(dts,ensemble_prob,NON(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=1//100*exp(301//100)
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.3
println("NON:", sim.𝒪est[:weak_final])

sim = test_convergence(dts,ensemble_prob,NON2(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=1//100*exp(301//100)
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.3
println("NON2:", sim.𝒪est[:weak_final])

numtraj = Int(1e6)
seed = 100
Random.seed!(seed)
seeds = rand(UInt, numtraj)

sim = test_convergence(dts,ensemble_prob,COM(),
    save_everystep=false,trajectories=numtraj,save_start=false,adaptive=false,
    weak_timeseries_errors=false,weak_dense_errors=false,
    expected_value=1//100*exp(301//100)
    )
@test abs(sim.𝒪est[:weak_final]-2) < 0.3
println("COM:", sim.𝒪est[:weak_final])


using Aerospace
using Base.Test

# write your own tests here
pitchi = 45.0*D2R;
yawi = 90.0*D2R;
rolli = 10.0*D2R;
Tr_bni = TR_BN(rolli, pitchi, yawi);

# Testing of the Algorithms
quat = QuatInit(rolli, pitchi, yawi);
Tr_bn = QuatToDCM(quat);
euler = ComputeEuler( Tr_bn );

# write your own tests here
@test (euler[1] -  rolli) < 1.0e-10;
@test (euler[2] -  pitchi) < 1.0e-10;
@test (euler[3] -  yawi) < 1.0e-10;
@test norm(Tr_bni - Tr_bn) < 1.0e-10;


using InteractionNetworkModels, BenchmarkTools, Plots, Distributions, StatsPlots
using Distances, Distances, ProgressMeter

d = Hamming()
V = 20
mode = rand(Bool,V,V)
model = CER(
    mode, 0.05
)
n = 10
data = draw_sample(model, n)

plot(map(x->d(x,mode), data))

G_prior = CER(mode, 0.5)
α_prior = ScaledBeta(1,10,0,0.5)
plot(α_prior)

posterior = CerPosterior(data, G_prior, α_prior)

mcmc = CerPosteriorMcmc(τ=0.001, ε=0.01)

@time out = mcmc(
    posterior,
    desired_samples=10000, burn_in=0, lag=1,
    G_init=rand_perturb(mode, 0.001),
    α_init=0.02
)
acceptance_prob(mcmc)
plot(out, mode)

plot(log_posterior_prob(out))

hline!([model.α], subplot=2)

# Verify alpha is unbiased
function sim(
    true_model::CER,
    mcmc::CerPosteriorMcmc,
    n_train::Int,
    n_reps::Int
    )

    # α_true = true_model.α
    α_ests = Float64[]
    @showprogress for i in 1:n_reps
        data = draw_sample(true_model, n_train)
        G_prior = CER(true_model.mode, 0.5)
        α_prior = ScaledBeta(1,10,0,0.5)
        posterior = CerPosterior(data, G_prior, α_prior)
        out = mcmc(
            posterior,
            G_init=rand_perturb(true_model.mode, 0.001),
            α_init=true_model.α+0.1,
            loading_bar=false
        )
        push!(α_ests, mean(out.α_sample))
    end
    return α_ests
end

mcmc = CerPosteriorMcmc(
    τ=0.001, ε=0.01,
    desired_samples=250,
    burn_in=5_000,
)
ests = sim(model, mcmc, 20, 500)
boxplot(ests)
hline!([model.α])


"""
Variational inference for Bayesian NMF
"""
module NMF
using Distributions
using StatsFuns.logsumexp, SpecialFunctions.digamma

export NMFModel
export sample_data, VI

####################
## Types
struct NMFModel
    a_t::Array{Float64, 2} # D x K
    b_t::Array{Float64, 2} # D x L
    a_v::Float64 # 1 dim
    b_v::Float64 # 1 dim
end

function sqsum(mat::Array, idx)
    return squeeze(sum(mat, idx), idx)
end

####################
## functions
function init(X::Array{Int64, 2}, model::NMFModel)
    D, N = size(X)
    K = size(model.a_t, 2)
    S = zeros(D, K, N)
    A_t = rand(D, K)
    B_t = rand(D, K)
    A_v = rand(K, N)
    B_v = rand(K, N)
    for d in 1 : D
        for k in 1 : K
            for n in 1 : N
                S[d,k,n] = X[d,n] * A_t[d,k] * B_t[d,k] * A_v[k,n] * B_v[k,n]
            end
        end
    end
    return S, A_t, B_t, A_v, B_v
end

function update_S(X::Array{Int64, 2}, A_t::Array{Float64, 2}, B_t::Array{Float64, 2}, A_v::Array{Float64, 2}, B_v::Array{Float64, 2})
    D, K = size(A_t)
    N = size(A_v, 2)
    S = zeros(D, K, N)
    for d in 1 : D
        for n in 1 : N
            # K dim
            ln_P = (digamma.(A_t[d,:]) + log.(B_t[d,:])
                    + digamma.(A_v[:,n]) + log.(B_v[:,n])
                    )
            ln_P = ln_P - logsumexp(ln_P)
            S[d,:,n] = X[d,n] * exp.(ln_P)
        end
    end
    return S
end

function update_T(S::Array{Float64, 3}, A_v::Array{Float64, 2}, B_v::Array{Float64, 2}, model::NMFModel)
    D, K, N = size(S)
    a_t = model.a_t # DxK
    b_t = model.b_t # DxK
    A_t = a_t + sqsum(S, 3)
    B_t = (a_t ./ b_t + repmat(sqsum(A_v.*B_v, 2)', D, 1)).^(-1)
    return A_t, B_t
end

function update_V(S::Array{Float64, 3}, A_t::Array{Float64, 2}, B_t::Array{Float64, 2}, model::NMFModel)
    a_v = model.a_v
    b_v = model.b_v
    D, K, N = size(S)
    A_v = a_v + sqsum(S, 1)
    B_v = (a_v / b_v + repmat(sqsum(A_t.*B_t, 1), 1, N)).^(-1)
    return A_v, B_v
end

"""
Sample data given hyperparameters.
"""
function sample_data(N::Int, model::NMFModel)
    # TODO; check b or 1/b ?
    D, K = size(model.a_t)

    T = zeros(D, K)
    for d in 1 : D
        for k in 1 : K
            T[d,k] = rand(Gamma(model.a_t[d,k], 1.0/model.b_t[d,k])) # TODO: check
        end
    end

    V = reshape(rand(Gamma(model.a_v, 1.0/model.b_v), K*N) , K, N) # TODO: check

    S = zeros(D, K, N)
    for d in 1 : D
        for k in 1 : K
            for n in 1 : N
                S[d,k,n] = T[d,k] * V[k,n]
            end
        end
    end
    #X = sqsum(S, 2) + 0.0 # zero noise
    X = sqsum(S, 2)
    return X, T, S, V
end

function update_model(A_t::Array{Float64, 2}, B_t::Array{Float64, 2}, model::NMFModel)
    return NMFModel(A_t, B_t, model.a_v, model.b_v)
end

"""
Compute variational posterior distributions.
"""
function VI(X::Array{Int64, 2}, model::NMFModel, max_iter::Int)
    K = size(model.a_t, 2)
    D, N = size(X)
    S, A_t, B_t, A_v, B_v = init(X, model)
    for iter in 1 : max_iter
        # latent
        S = update_S(X, A_t, B_t, A_v, B_v)
        A_v, B_v = update_V(S, A_t, B_t, model)

        # param
        A_t, B_t = update_T(S, A_v, B_v, model)
    end

    return update_model(A_t, B_t, model), S, A_t.*B_t, A_v.*B_v
end

end


# exponential kernel with total variation distance
struct TVExponentialKernel <: KernelFunctions.SimpleKernel end

KernelFunctions.kappa(kernel::TVExponentialKernel, d) = exp(-d)
KernelFunctions.metric(::TVExponentialKernel) = TotalVariation()

# Simplifies computations of distances but is type piracy... :(
function KernelFunctions._scale(t::ScaleTransform, metric::TotalVariation, x, y)
    return first(t.s) * evaluate(metric, x, y)
end


module CovarianceMatrices

# using Requires
#
# function __init__()
#     @info("INIT CALLED")
#     @require GLM="38e38edf-8417-5370-95a0-9cbb8c7f171a" include("glm.jl")
# end

using LinearAlgebra
using Statistics

#include("varhac.jl")
include("types.jl")
include("HAC.jl")
include("HC.jl")
include("CRHC.jl")
include("covariance.jl")
include("methods.jl")
include("glm.jl")

export QuadraticSpectralKernel, TruncatedKernel, ParzenKernel, BartlettKernel,
       TukeyHanningKernel, VARHAC, HC0, HC1, HC2, HC3, HC4, HC4m, HC5, CRHC0, CRHC1,
       CRHC2, CRHC3, NeweyWest, Andrews,
       HACCache, HCCache, CRHCCache, CovarianceMatrix, vcov, stderr

end # module


using Distributions
using Base.Test

d = Normal()
@test Distributions.quantile_newton(d, 0.5) == quantile(d, 0.5)
@test Distributions.cquantile_newton(d, 0.5) == cquantile(d, 0.5)


# Routine for estimating Lasso coefficients using GLMNet

function getLassoCoeff(outputs::Array{Float64,2},inputs::Array{Float64,2})

	numDependentVar::Int64 = size(outputs,2)
	numIndependentVar::Int64 = size(inputs,2)

	if(size(outputs,1) != size(inputs,1))
		throw(ErrorException("Number of output samples != number of input samples"))
	end

	coeff = zeros(Float64,numIndependentVar,numDependentVar)

	for d = 1:numDependentVar
		path_cv = glmnetcv(inputs[:,2:numIndependentVar],outputs[:,d]);
		path_ind::Int64 = indmin(path_cv.meanloss)
		coeff[2:numIndependentVar,d] = path_cv.path.betas[:,path_ind]
	end

	tmpMat = outputs - inputs[:,2:numIndependentVar]*coeff[2:numIndependentVar,:]
	for d = 1:numDependentVar
		coeff[1,d] = mean(tmpMat[:,d])
	end

	return coeff
end


# ---
# title: 483. Smallest Good Base
# id: problem483
# author: Tian Jun
# date: 2020-10-31
# difficulty: Hard
# categories: Math, Binary Search
# link: <https://leetcode.com/problems/smallest-good-base/description/>
# hidden: true
# ---
#
# For an integer n, we call k>=2 a _**good base**_ of n, if all digits of n base
# k are 1.
#
# Now given a string representing n, you should return the smallest good base of
# n in string format.
#
# **Example 1:**
#
#
#
#     Input: "13"
#     Output: "3"
#     Explanation: 13 base 3 is 111.
#
#
#
#
# **Example 2:**
#
#
#
#     Input: "4681"
#     Output: "8"
#     Explanation: 4681 base 8 is 11111.
#
#
#
#
# **Example 3:**
#
#
#
#     Input: "1000000000000000000"
#     Output: "999999999999999999"
#     Explanation: 1000000000000000000 base 999999999999999999 is 11.
#
#
#
#
# **Note:**
#
#   1. The range of n is [3, 10^18].
#   2. The string representing n is always valid and will not have leading zeros.
#
#
#
#
## @lc code=start
using LeetCode

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


using Symbolics

include("./kinematics.jl")
using .Kinematics

N = 2
@variables Q[1:3N]
@variables Ξ[1:N]

println(Q[1])

Ps, Rs = calc_local(N, Q, Ξ);

Φs, Θs = calc_global(N, Ps, Rs);


include("../utils.jl")

@testset "SphereSymmetricMatrices" begin
    M = SphereSymmetricMatrices(3)
    M_complex = SphereSymmetricMatrices(3, ℂ)
    A = [1 2 3; 2 4 -5; 3 -5 6] / norm([1 2 3; 2 4 -5; 3 -5 6])
    B = [1 2; 2 4] / norm([1 2; 2 4])                                     #wrong dimensions
    C = [-3 -im 4; im 5 im; 4 -im 0] / norm([-3 -im 4; im 5 im; 4 -im 0]) #complex
    D = [1 2 3; 4 5 6; 7 8 9] / norm([1 2 3; 4 5 6; 7 8 9])               #not symmetric
    E = [1 2 3; 2 4 -5; 3 -5 6]                                           #not of unit norm
    J = [1.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0]
    K = [0.0 1.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0]
    @testset "Real Sphere Symmetric Matrices Basics" begin
        @test repr(M) == "SphereSymmetricMatrices(3, ℝ)"
        @test representation_size(M) == (3, 3)
        @test base_manifold(M) === M
        @test typeof(get_embedding(M)) === ArraySphere{Tuple{3,3},ℝ}
        @test check_point(M, A) === nothing
        @test_throws DomainError is_point(M, B, true)
        @test_throws DomainError is_point(M, C, true)
        @test_throws DomainError is_point(M, D, true)
        @test_throws DomainError is_point(M, E, true)
        @test check_vector(M, A, zeros(3, 3)) === nothing
        @test_throws DomainError is_vector(M, A, B, true)
        @test_throws DomainError is_vector(M, A, C, true)
        @test_throws DomainError is_vector(M, A, D, true)
        @test_throws DomainError is_vector(M, D, A, true)
        @test_throws DomainError is_vector(M, A, E, true)
        @test_throws DomainError is_vector(M, J, K, true)
        @test manifold_dimension(M) == 5
        A2 = similar(A)
        @test A == project!(M, A2, A)
        @test A == project(M, A)
        embed!(M, A2, A)
        A3 = embed(M, A)
        @test A2 == A
        @test A3 == A
        F = [2 4 -1; 4 9 7; -1 7 5] / norm([2 4 -1; 4 9 7; -1 7 5])
        G = [-10 9 -8; 9 7 6; -8 6 5] / norm([-10 9 -8; 9 7 6; -8 6 5])
        test_manifold(
            M,
            [A, F, G],
            test_injectivity_radius=false,
            test_forward_diff=false,
            test_reverse_diff=false,
            test_vector_spaces=true,
            test_project_tangent=true,
            test_musical_isomorphisms=true,
            test_default_vector_transport=true,
            is_tangent_atol_multiplier=2,
            test_inplace=true,
        )
    end
    @testset "Complex Sphere Symmetric Matrices Basics" begin
        @test repr(M_complex) == "SphereSymmetricMatrices(3, ℂ)"
        @test manifold_dimension(M_complex) == 8
        H =
            [7.0 -1.5im 0.0; 1.5im 3.0 -1.0im; 0.0 1.0im 4.5] /
            norm([7.0 -1.5im 0.0; 1.5im 3.0 -1.0im; 0.0 1.0im 4.5])
        I =
            [2.0 4.0 5.0im; 4.0 -1.0 -9.0; -5.0im -9.0 2.5] /
            norm([2.0 4.0 5.0im; 4.0 -1.0 -9.0; -5.0im -9.0 2.5])
        test_manifold(
            M_complex,
            [C, H, I],
            test_injectivity_radius=false,
            test_forward_diff=false,
            test_reverse_diff=false,
            test_vector_spaces=true,
            test_project_tangent=true,
            test_musical_isomorphisms=true,
            test_default_vector_transport=true,
            is_tangent_atol_multiplier=2,
            is_point_atol_multiplier=2,
            projection_atol_multiplier=2,
            exp_log_atol_multiplier=2,
            test_inplace=true,
        )
    end
end


using SymPy: N, Sym, diff, expand, solve, subs

"""
Axisymmetric Solov'ev equilibra with X-point in (R/R₀,Z/R₀,phi) coordinates.
Based on Cerfon & Freidberg, Physics of Plasmas 17, 032502, 2010.

Parameters:
 * `R₀`: position of magnetic axis
 * `B₀`: B-field at magnetic axis
 * `ϵ`:  inverse aspect ratio
 * `κ`:  elongation
 * `δ`:  triangularity
 * `a`:  free constant, determined to match a given beta value
 * `xₛₑₚ`: x position of the X point
 * `yₛₑₚ`: y position of the X point
"""
struct SolovevXpoint{T <: Number} <: AbstractSolovevEquilibrium
    name::String
    R₀::T
    B₀::T
    ϵ::T
    κ::T
    δ::T
    a::T
    xₛₑₚ::T
    yₛₑₚ::T
    c::Vector{T}

    function SolovevXpoint{T}(R₀::T, B₀::T, ϵ::T, κ::T, δ::T, a::T, xₛₑₚ::T, yₛₑₚ::T, c::Vector{T}) where T <: Number
        new("SolovevXpointEquilibrium", R₀, B₀, ϵ, κ, δ, a, xₛₑₚ, yₛₑₚ, c)
    end
end

function SolovevXpoint(R₀::T, B₀::T, ϵ::T, κ::T, δ::T, a::T, xₛₑₚ::T, yₛₑₚ::T) where T <: Number

    n = 12

    x = [Sym("x" * string(i)) for i in 1:3]
    c = [Sym("c" * string(i)) for i in 1:n]

    ψ = (ψ₀(x,a) + c[1]  * ψ₁(x)
                 + c[2]  * ψ₂(x)
                 + c[3]  * ψ₃(x)
                 + c[4]  * ψ₄(x)
                 + c[5]  * ψ₅(x)
                 + c[6]  * ψ₆(x)
                 + c[7]  * ψ₇(x)
                 + c[8]  * ψ₈(x)
                 + c[9]  * ψ₉(x)
                 + c[10] * ψ₁₀(x)
                 + c[11] * ψ₁₁(x)
                 + c[12] * ψ₁₂(x) )

    eqs = [
        expand(subs(subs(ψ, x[1], 1+ϵ), x[2], 0)),
        expand(subs(subs(ψ, x[1], 1-ϵ), x[2], 0)),
        expand(subs(subs(ψ, x[1], 1-δ*ϵ), x[2], κ*ϵ)),
        expand(subs(subs(ψ, x[1], xₛₑₚ), x[2], yₛₑₚ)),
        expand(subs(subs(diff(ψ, x[2]), x[1], 1+ϵ), x[2], 0)),
        expand(subs(subs(diff(ψ, x[2]), x[1], 1-ϵ), x[2], 0)),
        expand(subs(subs(diff(ψ, x[1]), x[1], 1-δ*ϵ), x[2], κ*ϵ)),
        expand(subs(subs(diff(ψ, x[1]), x[1], xₛₑₚ), x[2], yₛₑₚ)),
        expand(subs(subs(diff(ψ, x[2]), x[1], xₛₑₚ), x[2], yₛₑₚ)),
        expand(subs(subs(diff(ψ, x[2], 2), x[1], 1+ϵ), x[2], 0) - (1 + asin(δ))^2 / (ϵ * κ^2) * subs(subs(diff(ψ, x[1]), x[1], 1+ϵ), x[2], 0)),
        expand(subs(subs(diff(ψ, x[2], 2), x[1], 1-ϵ), x[2], 0) + (1 - asin(δ))^2 / (ϵ * κ^2) * subs(subs(diff(ψ, x[1]), x[1], 1-ϵ), x[2], 0)),
        expand(subs(subs(diff(ψ, x[1], 2), x[1], 1-δ*ϵ), x[2], κ*ϵ) - κ / (ϵ * (1 - δ^2)) * subs(subs(diff(ψ, x[2]), x[1], 1-δ*ϵ), x[2], κ*ϵ))
    ]

    csym = solve(eqs, c)
    cnum = [N(csym[c[i]]) for i in 1:n]

    SolovevXpoint{T}(R₀, B₀, ϵ, κ, δ, a, xₛₑₚ, yₛₑₚ, cnum)
end

SolovevXpointITER() = SolovevXpoint(6.2, 5.3, 0.32, 1.7, 0.33, -0.155, 0.88, -0.60)
SolovevXpointNSTX() = SolovevXpoint(0.85, 0.3, 0.78, 2.0, 0.35, -0.05, 0.70, -1.71)

function Base.show(io::IO, equ::SolovevXpoint)
    print(io, "Solovev Xpoint Equilibrium with\n")
    print(io, "  R₀ = ", equ.R₀, "\n")
    print(io, "  B₀ = ", equ.B₀, "\n")
    print(io, "  ϵ  = ", equ.ϵ, "\n")
    print(io, "  κ  = ", equ.κ, "\n")
    print(io, "  δ  = ", equ.δ, "\n")
    print(io, "  a  = ", equ.a, "\n")
    print(io, "  xₛₑₚ  = ", equ.xₛₑₚ, "\n")
    print(io, "  yₛₑₚ  = ", equ.yₛₑₚ)
end

function A₃(x::AbstractArray{T,1}, equ::SolovevXpoint) where {T <: Number}
    (ψ₀(x, equ.a) + equ.c[1]  * ψ₁(x)
                  + equ.c[2]  * ψ₂(x)
                  + equ.c[3]  * ψ₃(x)
                  + equ.c[4]  * ψ₄(x)
                  + equ.c[5]  * ψ₅(x)
                  + equ.c[6]  * ψ₆(x)
                  + equ.c[7]  * ψ₇(x)
                  + equ.c[8]  * ψ₈(x)
                  + equ.c[9]  * ψ₉(x)
                  + equ.c[10] * ψ₁₀(x)
                  + equ.c[11] * ψ₁₁(x)
                  + equ.c[12] * ψ₁₂(x) )
end

macro solovev_xpoint_equilibrium(R₀, B₀, ϵ, κ, δ, a, xₛₑₚ, yₛₑₚ)
    generate_equilibrium_code(SolovevXpoint(R₀, B₀, ϵ, κ, δ, a, xₛₑₚ, yₛₑₚ); output=false)
end


using LowRankModels

m,n,k = 10,20,3
Y = rand(3,20)
A = rand(10,20)

ry = Regularizer[fixed_latent_features(Y[:,i]) for i=1:n]
glrm = GLRM(A, QuadLoss(), SimplexConstraint(), ry, k)
X, Yp, ch = fit!(glrm)

@assert(Yp == Y)


module loss

function logistic(y_true, y_pred)::Float64
    y_true == y_pred && return 0
    y_true != y_pred && y_pred in (0, 1) && return Inf

    return -(y_true * log(y_pred) + (1 - y_true) * log(1 - y_pred))
end

end


struct MatrixLangevin{N,T,n,k} <: ParameterizedMeasure{N}
    par::NamedTuple{N,T}
    manifold::Stiefel{n,k,ℝ}
end
function MatrixLangevin(n, k; kwargs...)
    par = NamedTuple(kwargs)
    return MatrixLangevin(par, Stiefel(n, k))
end

const MatrixVonMisesFisher = MatrixLangevin

Manifolds.base_manifold(d::MatrixLangevin) = getfield(d, :manifold)

function MeasureTheory.basemeasure(μ::MatrixLangevin{(:F,)})
    return normalize(Hausdorff(base_manifold(μ), μ.F))
end

function MeasureTheory.logdensity(d::MatrixLangevin{(:F,)}, x::AbstractMatrix)
    F = d.F
    return tr_At_B(F, x) - log(pFq((), (n//2,), rmul!(F'F, 1//4)))
end

function Base.rand(rng::AbstractRNG, T::Type, d::MatrixLangevin{(:F,)})
    F = d.F
    trΛ = sum(svdvals(F))
    while true
        x = rand(rng, T, basemeasure(d))
        rand(rng) < exp(tr_At_B(F, x) - trΛ) && return x
    end
end


include("./Functions.jl")
using Functions

function factorial(n)
  total = 1
  for i = 1:n
    total = total * i
  end
  total
end

println(digitSum(factorial(BigInt(100))))


	#
	#	Script for calculation of far-field pattern in the Dirichlet case (prolate)
	#

	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	# User configurable parameters
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	# Physical parameters
	k_0 = 4 ; # Wave number in media 0
	a = 1 ; # Major semiaxis spheroid
	b = 0.8 ; # Minor semiaxis spheroid
	theta_inc = pi/4 ; # Incidence angle in radians

	# Software parameters
	M = 8 ;
	method = 2 ;
	delta_eta = 0.005 ;

	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	# Calculation
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	# Conversion theta to eta
	eta_inc = cos( theta_inc ) ;
	# Coefficient evaluation
	Amn = Coeff_ProDN( k_0, a, b, M, method, eta_inc, "D" ) ;

	# Far-Field pattern calculation
	Pattern = Pattern_LiquidPro( k_0, a, b, M, method, eta_inc, Amn, delta_eta ) ;

	# Saving to disk
	writedlm( "Out.Pattern.dat", Pattern , '\t' ) ;


using FiniteDiff: finite_difference_gradient
using BifurcationInference, Test
using Flux: Optimise

error_tolerance = 0.004
@testset "Gradients" begin

	@testset "Saddle Node" begin
		include("minimal/saddle-node.jl")

		@time for θ ∈ [[5.0,1.0],[5.0,-1.0],[-1.0,-1.0],[2.5,-1.0],[-1.0,2.5],[-4.0,1.0]]
			x, y = finite_difference_gradient(θ->loss(F,θ,X),θ), ∇loss(F,θ,X)[2]
			@test acos(x'y/(norm(x)*norm(y)))/π < error_tolerance
		end
	end

	@testset "Pitchfork" begin
		include("minimal/pitchfork.jl")

		@time for θ ∈ [[1.0,1.0],[3.0,3.0],[1.0,-1.0],[3.0,-3.0],[-1.0,-1.0],[-3.0,-3.0],[-1.0,1.0],[-3.0,3.0]]
			x, y = finite_difference_gradient(θ->loss(F,θ,X),θ), ∇loss(F,θ,X)[2]
			@test acos(x'y/(norm(x)*norm(y)))/π < error_tolerance
		end
	end

	@testset "Two State" begin
		include("applied/two-state.jl")

		@time for θ ∈ [[0.5, 0.5, 0.5470, 2.0, 7.5],[0.5, 0.5, 1.5470, 2.0, 7.5],[0.5, 0.88, 1.5470, 2.0, 7.5]]
			x, y = finite_difference_gradient(θ->loss(F,θ,X),θ), ∇loss(F,θ,X)[2]
			@test acos(x'y/(norm(x)*norm(y)))/π < error_tolerance
		end
	end
end

error_tolerance = 0.1
@testset "Optimisations" begin
include("applied/two-state.jl")

	@testset "Mutual Inhibition" begin
		parameters = ( θ=SizedVector{5}(0.5,0.5,0.5470,1.0,1.5), p=minimum(X.parameter) )
		trajectory = train!( F, parameters, X;  iter=300, optimiser=Optimise.ADAM(0.01) )

		steady_states = deflationContinuation(F,X.roots,(p=minimum(X.parameter),θ=trajectory[end]),getParameters(X))
		bifurcations = [ s.z for branch ∈ steady_states for s ∈ branch if s.bif ]

		@test all( z->any(isapprox.(X.targets,z.p; atol=error_tolerance)), bifurcations)
	end

	@testset "Mutual Activation" begin
		parameters = ( θ=SizedVector{5}(0.246,-1.113,-0.87,-1.36,1.28), p=minimum(X.parameter) )
		trajectory = train!( F, parameters, X;  iter=300, optimiser=Optimise.ADAM(0.01) )

		steady_states = deflationContinuation(F,X.roots,(p=minimum(X.parameter),θ=trajectory[end]),getParameters(X))
		bifurcations = [ s.z for branch ∈ steady_states for s ∈ branch if s.bif ]

		@test all( z->any(isapprox.(X.targets,z.p; atol=error_tolerance)), bifurcations)
	end

	include("applied/double-exclusive.jl")
	@testset "Double Exclusive" begin
		parameters = ( θ=SizedVector{21}(-0.5498107083383627, -0.30983502459648393, 0.631939862866151, -1.3407390261868093, 0.6331343612914606, -0.4049028383285403, -0.8524704190293824, -0.11432918583984555, -1.7299987854365662, -2.079032179004907, 0.4916870522978877, -0.010788745553567142, -0.3878513750140796, -0.08334187371052072, -0.2772816729274815, 0.8285363415116678, 0.054887187754647675, -0.49494957503215126, -0.8693294194842291, 0.9289938806228071, 0.22800165770588346), p=minimum(X.parameter) )
		trajectory = train!( F, parameters, X;  iter=300, optimiser=Optimise.ADAM(0.01) )

		steady_states = deflationContinuation(F,X.roots,(p=minimum(X.parameter),θ=trajectory[end]),getParameters(X))
		bifurcations = [ s.z for branch ∈ steady_states for s ∈ branch if s.bif ]

		@test all( z->any(isapprox.(X.targets,z.p; atol=error_tolerance)), bifurcations)
	end
end


using LossFunctions, Plots
pyplot()

_mkpath(fname) = joinpath(dirname(@__FILE__()), fname)
srand(123)

# ---------------------------------------------------------------
# README Plots

xld  = "\$\\hat{y} - y\$"
xlm  = "\$y \\cdot \\hat{y}\$"
yld  = "\$L(\\hat{y} - y)\$"
yldd = "\$L'(\\hat{y} - y)\$"
ylm  = "\$L(y \\cdot \\hat{y})\$"
ylmd = "\$L'(y \\cdot \\hat{y})\$"
default(bg_outside=RGBA(1,1,1,0))

plot([L2DistLoss(), L1DistLoss(), LPDistLoss(.5), LogitDistLoss(),
      HuberLoss(), L1EpsilonInsLoss(1), L2EpsilonInsLoss(1),
      QuantileLoss(0.7)],
     -4:0.01:4,
     xlab=xld, ylab=yld,
     xlim=(-4,4), ylim=(0,5), palette=:darktest, grid=false,
     linestyle=[:dash :solid :solid :dashdot :dash :solid :solid :solid])
Plots.svg(_mkpath("distance"))

plot([ZeroOneLoss(), L1HingeLoss(),L2HingeLoss(), LogitMarginLoss(),
      ModifiedHuberLoss(), PerceptronLoss(), SmoothedL1HingeLoss(1),
      L2MarginLoss(), ExpLoss(), SigmoidLoss(), DWDMarginLoss(1)],
     -2:0.001:2,
     xlab=xlm, ylab=ylm,
     ylim=(0,7), palette=:darktest, grid=false,
     linestyle=[:dash :solid :solid :dashdot :dash :solid :solid :dash :solid :dot :dot])
Plots.svg(_mkpath("margin"))

# ---------------------------------------------------------------
# Readthedocs Distance Plots

default(bg_outside=RGBA(1,1,1,0), aspect_ratio = 1, palette=:darktest, size = (350, 350))

# LP Dist Loss
plot([LPDistLoss(0.5), LPDistLoss(1.5), LPDistLoss(3)],
     xlab=xld, ylab=yld,
     -2:0.01:2, ylim=(0,4))
Plots.svg(_mkpath("LPDistLoss1"))
plot([LPDistLoss(0.5)', LPDistLoss(1.5)', LPDistLoss(3)'],
     xlab=xld, ylab=yldd,
     -2:0.005:2, ylim=(-2,2))
Plots.svg(_mkpath("LPDistLoss2"))

# L1 Dist Loss
plot(L1DistLoss(), -2:0.05:2, ylim=(0,4), xlab=xld, ylab=yld)
Plots.svg(_mkpath("L1DistLoss1"))
plot(L1DistLoss()', -2:0.01:2, ylim=(-2,2), xlab=xld, ylab=yldd)
Plots.svg(_mkpath("L1DistLoss2"))

# L2 Dist Loss
plot(L2DistLoss(), -2:0.05:2, ylim=(0,4), xlab=xld, ylab=yld)
Plots.svg(_mkpath("L2DistLoss1"))
plot(L2DistLoss()', -2:0.05:2, ylim=(-2,2), xlab=xld, ylab=yldd)
Plots.svg(_mkpath("L2DistLoss2"))

# Logit Dist Loss
plot(LogitDistLoss(), -2:0.05:2, ylim=(0,4), xlab=xld, ylab=yld)
Plots.svg(_mkpath("LogitDistLoss1"))
plot(LogitDistLoss()', -2:0.05:2, ylim=(-2,2), xlab=xld, ylab=yldd)
Plots.svg(_mkpath("LogitDistLoss2"))

# HuberLoss
plot([HuberLoss(0.5), HuberLoss(1), HuberLoss(1.5)],
     xlab=xld, ylab=yld,
     -2:0.01:2, ylim=(0,4))
Plots.svg(_mkpath("HuberLoss1"))
plot([HuberLoss(0.5)', HuberLoss(1)', HuberLoss(1.5)'],
     xlab=xld, ylab=yldd,
     -2:0.005:2, ylim=(-2,2))
Plots.svg(_mkpath("HuberLoss2"))

# L1EpsilinInsLoss
plot([L1EpsilonInsLoss(0.5), L1EpsilonInsLoss(1), L1EpsilonInsLoss(1.5)],
     xlab=xld, ylab=yld,
     -2:0.01:2, ylim=(-0.05,4))
Plots.svg(_mkpath("L1EpsilonInsLoss1"))
plot([L1EpsilonInsLoss(0.5)', L1EpsilonInsLoss(1)', L1EpsilonInsLoss(1.5)'],
     xlab=xld, ylab=yldd,
     -2:0.005:2, ylim=(-2.05,2))
Plots.svg(_mkpath("L1EpsilonInsLoss2"))

# L2EpsilonInsLoss
plot([L2EpsilonInsLoss(0.5), L2EpsilonInsLoss(1), L2EpsilonInsLoss(1.5)],
     xlab=xld, ylab=yld,
     -2:0.01:2, ylim=(-0.05,4))
Plots.svg(_mkpath("L2EpsilonInsLoss1"))
plot([L2EpsilonInsLoss(0.5)', L2EpsilonInsLoss(1)', L2EpsilonInsLoss(1.5)'],
     xlab=xld, ylab=yldd,
     -2:0.005:2, ylim=(-2.05,2))
Plots.svg(_mkpath("L2EpsilonInsLoss2"))

# PeriodicLoss
plot([PeriodicLoss(.5), PeriodicLoss(1), PeriodicLoss(1.5)],
     xlab=xld, ylab=yld,
     -1:0.01:1, ylim=(0,2))
Plots.svg(_mkpath("PeriodicLoss1"))
plot([PeriodicLoss(.5)', PeriodicLoss(1)', PeriodicLoss(1.5)'],
     xlab=xld, ylab=yldd,
     -8:0.01:8, ylim = (-8,8))
Plots.svg(_mkpath("PeriodicLoss2"))

# QuantileLoss
plot([QuantileLoss(0.25), QuantileLoss(0.5), QuantileLoss(.75)],
     xlab=xld, ylab=yld,
     -2:0.01:2, ylim=(0,4))
Plots.svg(_mkpath("QuantileLoss1"))
plot([QuantileLoss(0.25)', QuantileLoss(0.5)', QuantileLoss(.75)'],
     xlab=xld, ylab=yldd,
     -2:0.005:2, ylim=(-2,2))
Plots.svg(_mkpath("QuantileLoss2"))

# ---------------------------------------------------------------
# Readthedocs Margin Plots

# Zero One Loss
plot(ZeroOneLoss(), -2:0.01:2, ylim=(-.05,4), xlab=xlm, ylab=ylm)
Plots.svg(_mkpath("ZeroOneLoss1"))
plot(ZeroOneLoss()', -2:0.05:2, ylim=(-2.05,2), xlab=xlm, ylab=ylmd)
Plots.svg(_mkpath("ZeroOneLoss2"))

# Perceptron Loss
plot(PerceptronLoss(), -2:0.01:2, ylim=(-.05,4), xlab=xlm, ylab=ylm)
Plots.svg(_mkpath("PerceptronLoss1"))
plot(PerceptronLoss()', -2:0.005:2, ylim=(-2.05,2), xlab=xlm, ylab=ylmd)
Plots.svg(_mkpath("PerceptronLoss2"))

# Hinge Loss
plot(L1HingeLoss(), -2:0.01:2, ylim=(-.05,4), xlab=xlm, ylab=ylm)
Plots.svg(_mkpath("L1HingeLoss1"))
plot(L1HingeLoss()', -2:0.005:2, ylim=(-2.05,2), xlab=xlm, ylab=ylmd)
Plots.svg(_mkpath("L1HingeLoss2"))

# L2 Hinge Loss
plot(L2HingeLoss(), -2:0.01:2, ylim=(-.05,4), xlab=xlm, ylab=ylm)
Plots.svg(_mkpath("L2HingeLoss1"))
plot(L2HingeLoss()', -2:0.005:2, ylim=(-2.05,2), xlab=xlm, ylab=ylmd)
Plots.svg(_mkpath("L2HingeLoss2"))

# Hinge Loss
plot([SmoothedL1HingeLoss(0.5),SmoothedL1HingeLoss(1.0),SmoothedL1HingeLoss(1.5),SmoothedL1HingeLoss(2.0)],
      -2:0.01:2, ylim=(-.05,4),
      xlab=xlm, ylab=ylm)
Plots.svg(_mkpath("SmoothedL1HingeLoss1"))
plot([SmoothedL1HingeLoss(0.5)',SmoothedL1HingeLoss(1.0)',SmoothedL1HingeLoss(1.5)',SmoothedL1HingeLoss(2.0)'],
     -2:0.005:2, ylim=(-2.05,2),
     xlab=xlm, ylab=ylmd)
Plots.svg(_mkpath("SmoothedL1HingeLoss2"))

# Modified Huber Loss
plot(ModifiedHuberLoss(), -4:0.01:4, xticks=[-4:4...], yticks=[0:8...], ylim=(-.05,8), xlab=xlm, ylab=ylm)
Plots.svg(_mkpath("ModifiedHuberLoss1"))
plot(ModifiedHuberLoss()', -4:0.005:4, xticks=[-4:4...], yticks=[-4:4...], ylim=(-4.05,4), xlab=xlm, ylab=ylmd)
Plots.svg(_mkpath("ModifiedHuberLoss2"))

# DWD Margin Loss
plot([DWDMarginLoss(0.5),DWDMarginLoss(1.0),DWDMarginLoss(1.5)],
      -2:0.01:2, ylim=(0,4),
      xlab=xlm, ylab=ylm)
Plots.svg(_mkpath("DWDMarginLoss1"))
plot([DWDMarginLoss(0.5)',DWDMarginLoss(1.0)',DWDMarginLoss(1.5)'],
     -2:0.005:2, ylim=(-2,2),
     xlab=xlm, ylab=ylmd)
Plots.svg(_mkpath("DWDMarginLoss2"))

# L2 Margin Loss
plot(L2MarginLoss(), -2:0.01:2, ylim=(0,4), xlab=xlm, ylab=ylm)
Plots.svg(_mkpath("L2MarginLoss1"))
plot(L2MarginLoss()', -2:0.005:2, ylim=(-2,2), xlab=xlm, ylab=ylmd)
Plots.svg(_mkpath("L2MarginLoss2"))

# Logit Margin Loss
plot(LogitMarginLoss(), -2:0.01:2, ylim=(0,4), xlab=xlm, ylab=ylm)
Plots.svg(_mkpath("LogitMarginLoss1"))
plot(LogitMarginLoss()', -2:0.005:2, ylim=(-2,2), xlab=xlm, ylab=ylmd)
Plots.svg(_mkpath("LogitMarginLoss2"))

# Exp Loss
plot(ExpLoss(), -2:0.01:2, ylim=(0,4), xlab=xlm, ylab=ylm)
Plots.svg(_mkpath("ExpLoss1"))
plot(ExpLoss()', -2:0.005:2, ylim=(-2,2), xlab=xlm, ylab=ylmd)
Plots.svg(_mkpath("ExpLoss2"))

# Sigmoid Loss
plot(SigmoidLoss(), -2:0.01:2, ylim=(0,4), xlab=xlm, ylab=ylm)
Plots.svg(_mkpath("SigmoidLoss1"))
plot(SigmoidLoss()', -2:0.005:2, ylim=(-2,2), xlab=xlm, ylab=ylmd)
Plots.svg(_mkpath("SigmoidLoss2"))


using Base.Test
#using DataStructures
using PermutationsA

# is this a good idea ?
@test permlist(1,2,3) == permlist([1,2,3])

n = 10
c  = PermList([8,2,10,4,6,9,7,3,5,1])
c1 = PermList([6,8,5,7,10,9,2,3,4,1])

import Base: length

for a in (PermList([10,1,3,6,9,8,4,5,7,2]),
          PermMat([10,1,3,6,9,8,4,5,7,2]),
          PermSparse([10,1,3,6,9,8,4,5,7,2]))
    @test isperm(a)
    @test plength(a) == n
    b = copy(a)
    @test isperm(b)
    @test a == b
    b[1] = b[1] + 1
    @test !( a == b )
    @test isperm(b) == false
    @test a * a == a^2
    @test a * a * a == a^3
    @test a * a * c == a^2 * c
    @test a^order(a) == one(a)
    @test a^(order(a)+1) == a
    @test a^-1 * a == one(PermList,n)
    @test isid(a^-1 * a)
    @test a^-1 == inv(a)
    @test matrix(a)^order(a) == eye(Int,n)
    @test isid( a * a')
    @test a * "abcdefghijk" == "jacfihdegbk"
    @test sparse(a) == a == full(a)
    @test full(a) == a == sparse(a)
    @test eye(a) != a
    @test a != eye(a)
    @test kron(a,a) == kron(full(a),full(a))
    @test cyclelengths(a) == cyclelengths(list(a))
end

@test (c * eye(Int,n)) * (c' * eye(Int,n)) == eye(Int,n)
@test c * eye(Int,n) == c
@test c * eye(Int,n) * c' == eye(Int,n)

@test sparse(PermList([3,5,6,1,2,4])) == sparse(PermSparse([3,5,6,1,2,4]))

for a in (PermList([7,9,4,1,3,2,8,6,10,5]), PermList([10,1,3,6,9,8,4,5,7,2]),
          PermSparse([10,1,3,6,9,8,4,5,7,2]))
    #  not true for matrix
    @test length(a) == n
    for i in 1:n
        @test a * i == a[i]
        @test (a * i) / a == i
        @test a \ (a * i) == i
        @test a * float(i) == a * i
        @test a * BigInt(i) == a * i
    end
end

@test distance(c1,c1) == 0
@test distance(c1,inv(c1)) == 10
@test c == c
@test !( c != c )
@test c != c1
@test ! ( c < c )
@test ! ( c > c )
@test c >= c
@test c <= c
@test c > c1
@test c1 < c
@test c >= c1
@test c1 <= c
@test c1 > one(c1)

@test one(PermList,n) == PermList([1:n])

# This is ok, but trans is broken in general
#@test sign(a) == (-1)^length(trans(a))
# TODO, get sign(c::PermCycs) efficiently

p = PermList([3,2,4,10,1,6,5,8,7,9])
q = PermList([3,8,4,9,7,6,1,10,5,2])
#P = matrix(p)
#Q = matrix(q)
#@test q * p == matrix(p*q)
@test p' == inv(p)
@test string(p) == "( 3  2  4  10 1  6  5  8  7  9  )"
@test string(PermCycs(((1, 6, 2, 7, 9),(3, 8, 4,10, 5)))) == "((16279)(384105))"

a = PermList([20,15,6,7,19,10,18,3,12,14,17,9,2,5,13,1,11,16,8,4])
a1 = permlist(20,15,6,7,19,10,18,3,12,14,17,9,2,5,13,1,11,16,8,4)
@test a == a1

ac = permcycs([1,20,4,7,18,16],[2,15,13],[3,6,10,14,5,19,8],[9,12],[11,17])
# Second method no longer works. Depends on version of Julia
ac1 = permcycs((1,20,4,7,18,16),(2,15,13),(3,6,10,14,5,19,8),(9,12),(11,17))
@test ac == ac1

@test cycles(a) == ac
@test a == permlist(ac)
@test cycles(permlist(ac)) == ac
@test permlist(cycles(a)) == a
@test ac == copy(ac)

@test collect(cycletype(a)) == collect(counter([7=>1,2=>2,3=>1,6=>1]))

# Constructing this way does not check validity.
@test isperm(PermCycs(collect(([1,2,3],[4,5,6])))) == true
@test isperm(PermCycs(collect(([1,2,3],[3,4,5])))) == false
@test isperm(PermCycs(collect(([1,2,3],[4,5,5])))) == false

# Constructing this way *does* check validity
@test isperm( permcycs( (1,2),(3,4) ) ) == true
@test isperm( permcycs( [1,2], [3,4] ) ) == true
@test_throws ErrorException permcycs( (1,2),(3,2) )

@test distance(PermList([1,2,3,4,5]),PermList([1,2,3,4,5])) == 0
@test distance(PermList([1,2,3,4,5]),PermList([1,2,3,4,5,6])) == 0
@test distance(PermList([1,2,3,4,5]),PermList([1,2,3,4,6,5])) == 2
@test distance(PermList([2,1,3,4,5,6,7,8,9,10]),PermList([1,2,3,4,5])) == 2

a = permcycs([1,5,2,3,4],[7,9,10,8])
@test isid(a^order(a))
@test typeof(a^order(a)) == PermCycs{Int}
@test isid(a^-1 * a)
@test isid(a * a^-1)
@test isid(a * list(a)^-1)
@test isid(list(a) * a^-1)
@test typeof(convert(PermList,a)) == PermList{Int}
@test typeof(convert(PermCycs,convert(PermList,a))) == PermCycs{Int}

for a in (permcycs([1,2,3,4],[7,10],[9,8]),permcycs([10,7], [1,2,3,4],[9,8]),
          permcycs([1,2,3,4],[7,10]))
    @test support(a) == support(list(a))
    @test fixed(a) == fixed(list(a))
    @test sign(a) == sign(list(a))
    @test cyclelengths(a) == cyclelengths(list(a))
    @test greatestmoved(a) == greatestmoved(list(a))
    @test leastmoved(a) == leastmoved(list(a))
    @test a == cycles(list(a))
    @test supportsize(a) == length(support(a))
    @test supportsize(list(a)) == length(support(a))
    @test isid( a * a')
end

# canonical ordering
@test permcycs([1,2,3,4],[7,10],[9,8]) == permcycs([10,7], [1,2,3,4],[9,8])

# empty permutations
@test permcycs() == PermCycs()
@test list(permcycs()) == permlist()

@test list(permcycs([1,3],[6,4])) == PermList([3,2,1,6,5,4])
@test list(permcycs([1,3],[6,4]),10) == PermList([3,2,1,6,5,4,7,8,9,10])
@test list(permcycs(),5) == PermList([1,2,3,4,5])

v = ["cat","dog","zebra"]
@test v[PermList([1,3,2])] ==  v[cycles(PermList([1,3,2]))]

for c1 in ( permcycs((1,2), (3,4), (5,7,9,11)), permcycs((12,1,8), (3,4), (5,7,9,11)) )
    @test c1 == psparse(c1)
    @test c1 == list(c1)
    @test c1 == matrix(c1)
    @test det(c1) == sign(c1)
    @test rank(c1) == rank(full(c1))
    @test full(c1) == sparse(c1)
    @test sign(psparse(c1)) == sign(list(c1)) == sign(c1)
    @test plength(psparse(c1)) == plength(list(c1)) == plength(c1)
    @test null(psparse(c1)) == null(list(c1)) == null(c1) == null(full(c1))
    @test trace(psparse(c1)) == trace(list(c1)) == trace(c1) == trace(full(c1)) == trace(sparse(c1))
end

@test typeof(randperm(PermList,n)) == PermList{Int}
@test typeof(randperm(PermCycs,n)) == PermCycs{Int}
@test typeof(randperm(PermSparse,n)) == PermSparse{Int}
@test typeof(randperm(PermMat,n)) == PermMat{Int}

@test typeof(randperm(PermList{BigInt},n)) == PermList{BigInt}
@test typeof(randperm(PermCycs{BigInt},n)) == PermCycs{BigInt}
@test typeof(randperm(PermSparse{BigInt},n)) == PermSparse{BigInt}
@test typeof(randperm(PermMat{BigInt},n)) == PermMat{BigInt}
@test list(c1) * 7 == psparse(c1) * 7

@test (PermSparse( ( (1,2,3), (4,5) )) ; true )
@test (PermSparse(Int32, ( (1,2,3), (4,5) )) ; true )
@test (PermSparse(BigInt, ( (1,2,3), (4,5) )) ; true )
@test (PermCycs( ( (1,2,3), (4,5) )) ; true )
@test (PermCycs(Int32, ( (1,2,3), (4,5) )) ; true )
@test (PermCycs(BigInt, ( (1,2,3), (4,5) )) ; true )

@test PermSparse() == PermCycs() == PermList()
@test PermList([1:10]) == PermList([1:20])


# Rank-based correlations
#
# - Spearman's correlation
# - Kendall's correlation
#

#######################################
#
#   Spearman correlation
#
#######################################

"""
    corspearman(x, y=x)

Compute Spearman's rank correlation coefficient. If `x` and `y` are vectors, the
output is a float, otherwise it's a matrix corresponding to the pairwise correlations
of the columns of `x` and `y`.
"""
function corspearman(x::RealVector, y::RealVector)
    n = length(x)
    n == length(y) || throw(DimensionMismatch("vectors must have same length"))
    (any(isnan, x) || any(isnan, y)) && return NaN
    return cor(tiedrank(x), tiedrank(y))
end

function corspearman(X::RealMatrix, y::RealVector)
    size(X, 1) == length(y) ||
        throw(DimensionMismatch("X and y have inconsistent dimensions"))
    n = size(X, 2)
    C = Matrix{Float64}(I, n, 1)
    any(isnan, y) && return fill!(C, NaN)
    yrank = tiedrank(y)
    for j = 1:n
        Xj = view(X, :, j)
        if any(isnan, Xj)
            C[j,1] = NaN
        else
            Xjrank = tiedrank(Xj)
            C[j,1] = cor(Xjrank, yrank)
        end
    end
    return C
end

function corspearman(x::RealVector, Y::RealMatrix)
    size(Y, 1) == length(x) ||
        throw(DimensionMismatch("x and Y have inconsistent dimensions"))
    n = size(Y, 2)
    C = Matrix{Float64}(I, 1, n)
    any(isnan, x) && return fill!(C, NaN)
    xrank = tiedrank(x)
    for j = 1:n
        Yj = view(Y, :, j)
        if any(isnan, Yj)
            C[1,j] = NaN
        else
            Yjrank = tiedrank(Yj)
            C[1,j] = cor(xrank, Yjrank)
        end
    end
    return C
end

function corspearman(X::RealMatrix)
    n = size(X, 2)
    C = Matrix{Float64}(I, n, n)
    anynan = Vector{Bool}(undef, n)
    for j = 1:n
        Xj = view(X, :, j)
        anynan[j] = any(isnan, Xj)
        if anynan[j]
            C[:,j] .= NaN
            C[j,:] .= NaN
            C[j,j] = 1
            continue
        end
        Xjrank = tiedrank(Xj)
        for i = 1:(j-1)
            Xi = view(X, :, i)
            if anynan[i]
                C[i,j] = C[j,i] = NaN
            else
                Xirank = tiedrank(Xi)
                C[i,j] = C[j,i] = cor(Xjrank, Xirank)
            end
        end
    end
    return C
end

function corspearman(X::RealMatrix, Y::RealMatrix)
    size(X, 1) == size(Y, 1) ||
        throw(ArgumentError("number of rows in each array must match"))
    nr = size(X, 2)
    nc = size(Y, 2)
    C = Matrix{Float64}(undef, nr, nc)
    for j = 1:nr
        Xj = view(X, :, j)
        if any(isnan, Xj)
            C[j,:] .= NaN
            continue
        end
        Xjrank = tiedrank(Xj)
        for i = 1:nc
            Yi = view(Y, :, i)
            if any(isnan, Yi)
                C[j,i] = NaN
            else
                Yirank = tiedrank(Yi)
                C[j,i] = cor(Xjrank, Yirank)
            end
        end
    end
    return C
end

#######################################
#
#   Kendall correlation
#
#######################################

# Knight, William R. “A Computer Method for Calculating Kendall's Tau with Ungrouped Data.”
# Journal of the American Statistical Association, vol. 61, no. 314, 1966, pp. 436–439.
# JSTOR, www.jstor.org/stable/2282833.
function corkendall!(x::RealVector, y::RealVector, permx::AbstractVector{<:Integer}=sortperm(x))
    if any(isnan, x) || any(isnan, y) return NaN end
    n = length(x)
    if n != length(y) error("Vectors must have same length") end

    # Initial sorting
    permute!(x, permx)
    permute!(y, permx)

    # Use widen to avoid overflows on both 32bit and 64bit
    npairs = div(widen(n) * (n - 1), 2)
    ntiesx = ndoubleties = nswaps = widen(0)
    k = 0

    @inbounds for i = 2:n
        if x[i - 1] == x[i]
            k += 1
        elseif k > 0
            # Sort the corresponding chunk of y, so the rows of hcat(x,y) are
            # sorted first on x, then (where x values are tied) on y. Hence
            # double ties can be counted by calling countties.
            sort!(view(y, (i - k - 1):(i - 1)))
            ntiesx += div(widen(k) * (k + 1), 2) # Must use wide integers here
            ndoubleties += countties(y,  i - k - 1, i - 1)
            k = 0
        end
    end
    if k > 0
        sort!(view(y, (n - k):n))
        ntiesx += div(widen(k) * (k + 1), 2)
        ndoubleties += countties(y, n - k, n)
    end

    nswaps = merge_sort!(y, 1, n)
    ntiesy = countties(y, 1, n)

    # Calls to float below prevent possible overflow errors when
    # length(x) exceeds 77_936 (32 bit) or 5_107_605_667 (64 bit)
    (npairs + ndoubleties - ntiesx - ntiesy - 2 * nswaps) /
     sqrt(float(npairs - ntiesx) * float(npairs - ntiesy))
end

"""
    corkendall(x, y=x)

Compute Kendall's rank correlation coefficient, τ. `x` and `y` must both be either
matrices or vectors.
"""
corkendall(x::RealVector, y::RealVector) = corkendall!(copy(x), copy(y))

function corkendall(X::RealMatrix, y::RealVector)
    permy = sortperm(y)
    return([corkendall!(copy(y), X[:,i], permy) for i in 1:size(X, 2)])
end

function corkendall(x::RealVector, Y::RealMatrix)
    n = size(Y, 2)
    permx = sortperm(x)
    return(reshape([corkendall!(copy(x), Y[:,i], permx) for i in 1:n], 1, n))
end

function corkendall(X::RealMatrix)
    n = size(X, 2)
    C = Matrix{Float64}(I, n, n)
    for j = 2:n
        permx = sortperm(X[:,j])
        for i = 1:j - 1
            C[j,i] = corkendall!(X[:,j], X[:,i], permx)
            C[i,j] = C[j,i]
        end
    end
    return C
end

function corkendall(X::RealMatrix, Y::RealMatrix)
    nr = size(X, 2)
    nc = size(Y, 2)
    C = Matrix{Float64}(undef, nr, nc)
    for j = 1:nr
        permx = sortperm(X[:,j])
        for i = 1:nc
            C[j,i] = corkendall!(X[:,j], Y[:,i], permx)
        end
    end
    return C
end

# Auxilliary functions for Kendall's rank correlation

"""
    countties(x::RealVector, lo::Integer, hi::Integer)

Return the number of ties within `x[lo:hi]`. Assumes `x` is sorted.
"""
function countties(x::AbstractVector, lo::Integer, hi::Integer)
    # Use of widen below prevents possible overflow errors when
    # length(x) exceeds 2^16 (32 bit) or 2^32 (64 bit)
    thistiecount = result = widen(0)
    checkbounds(x, lo:hi)
    @inbounds for i = (lo + 1):hi
        if x[i] == x[i - 1]
            thistiecount += 1
        elseif thistiecount > 0
            result += div(thistiecount * (thistiecount + 1), 2)
            thistiecount = widen(0)
        end
    end

    if thistiecount > 0
        result += div(thistiecount * (thistiecount + 1), 2)
    end
    result
end

# Tests appear to show that a value of 64 is optimal,
# but note that the equivalent constant in base/sort.jl is 20.
const SMALL_THRESHOLD = 64

# merge_sort! copied from Julia Base
# (commit 28330a2fef4d9d149ba0fd3ffa06347b50067647, dated 20 Sep 2020)
"""
    merge_sort!(v::AbstractVector, lo::Integer, hi::Integer, t::AbstractVector=similar(v, 0))

Mutates `v` by sorting elements `x[lo:hi]` using the merge sort algorithm.
This method is a copy-paste-edit of sort! in base/sort.jl, amended to return the bubblesort distance.
"""
function merge_sort!(v::AbstractVector, lo::Integer, hi::Integer, t::AbstractVector=similar(v, 0))
    # Use of widen below prevents possible overflow errors when
    # length(v) exceeds 2^16 (32 bit) or 2^32 (64 bit)
    nswaps = widen(0)
    @inbounds if lo < hi
        hi - lo <= SMALL_THRESHOLD && return insertion_sort!(v, lo, hi)

        m = midpoint(lo, hi)
        (length(t) < m - lo + 1) && resize!(t, m - lo + 1)

        nswaps = merge_sort!(v, lo,  m, t)
        nswaps += merge_sort!(v, m + 1, hi, t)

        i, j = 1, lo
        while j <= m
            t[i] = v[j]
            i += 1
            j += 1
        end

        i, k = 1, lo
        while k < j <= hi
            if v[j] < t[i]
                v[k] = v[j]
                j += 1
                nswaps += m - lo + 1 - (i - 1)
            else
                v[k] = t[i]
                i += 1
            end
            k += 1
        end
        while k < j
            v[k] = t[i]
            k += 1
            i += 1
        end
    end
    return nswaps
end

# insertion_sort! and midpoint copied from Julia Base
# (commit 28330a2fef4d9d149ba0fd3ffa06347b50067647, dated 20 Sep 2020)
midpoint(lo::T, hi::T) where T <: Integer = lo + ((hi - lo) >>> 0x01)
midpoint(lo::Integer, hi::Integer) = midpoint(promote(lo, hi)...)

"""
    insertion_sort!(v::AbstractVector, lo::Integer, hi::Integer)

Mutates `v` by sorting elements `x[lo:hi]` using the insertion sort algorithm.
This method is a copy-paste-edit of sort! in base/sort.jl, amended to return the bubblesort distance.
"""
function insertion_sort!(v::AbstractVector, lo::Integer, hi::Integer)
    if lo == hi return widen(0) end
    nswaps = widen(0)
    @inbounds for i = lo + 1:hi
        j = i
        x = v[i]
        while j > lo
            if x < v[j - 1]
                nswaps += 1
                v[j] = v[j - 1]
                j -= 1
                continue
            end
            break
        end
        v[j] = x
    end
    return nswaps
end


"""
Resample curve

    resamplecurve(x, N=100)
    :param x: array describing curve (n,T)
    :param N: Number of samples to re-sample curve, N usually is > T
"""
function resamplecurve(x, N=100)
    n, T = size(x);
    xn = zeros(n, N);

    delta = zeros(T);

    for r in 2:T
        delta[r] = norm(x[:,r]-x[:,r-1]);
    end

    cumdel = cumsum(delta)/sum(delta);
    newdel = linspace(0,1,N);

    for r in 1:n
        s = Spline1D(cumdel,vec(x[r,:]));
        xn[r,:] = evaluate(s,newdel);
    end

    return xn
end

"""
Calculate centroid of curve

    calculatecentroid(beta)
    :param beta: array describing curve (n,T)
"""
function calculatecentroid(beta)
    n, T = size(beta);
    betadot = zeros(n,T);
    for i = 1:n
        betadot[i,:] = gradient(vec(beta[i,:]), 1.0/(T-1));
    end
    normbetadot = zeros(T);
    integrand = zeros(n,T);
    for i = 1:T
        normbetadot[i] = norm(betadot[:,i]);
        integrand[:,i] = beta[:,i]*normbetadot[i];
    end
    scale = trapz(collect(linspace(0,1,T)), normbetadot);
    centroid = trapz(collect(linspace(0,1,T)), integrand, 2)./scale;

    return centroid
end

"""
Convert curve to square-root velocity function (srvf)

    curve_to_q(beta)
    :param beta: array describing curve (n,T)
"""
function curve_to_q(beta, wscale::Bool=false)
    n, T = size(beta);
    v = zeros(n,T);
    for i = 1:n
        v[i,:] = gradient(vec(beta[i,:]), 1.0/(T-1));
    end

    len = sum(sqrt(sum(v.*v)))/T;
    v = v./len;
    q = zeros(n,T);
    for i = 1:T
        L = sqrt(norm(v[:,i]));
        if (L > 0.0001)
            q[:,i] = v[:,i]./L;
        else
            q[:,i] = v[:,i]*0.0001;
        end
    end

    if (!wscale)
        q = q./sqrt(innerprod_q2(q, q));
    end

    return q
end

"""
Convert srvf to curve

    q_to_curve(q)
    :param q: array describing srvf (n,T)
"""
function q_to_curve(q)
    T = size(q,2);
    qnorm = zeros(T);
    for i = 1:T
        qnorm[i] = norm(q[:,i]);
    end

    integrand = zeros(2,T);
    integrand[1,:] = vec(q[1,:]).*qnorm;
    integrand[2,:] = vec(q[2,:]).*qnorm;
    beta = cumtrapz(collect(1.:T), integrand, 2)./T;

    return beta
end

function optimum_reparam(beta1::Array{Float64,2}, beta2::Array{Float64,2},
                         lam::Float64=0.0; method::String="DP", w=0.01,
                         rotated::Bool=true, isclosed::Bool=false)
    n1, M = size(beta2);
    timet = collect(linspace(0,1,M));
    skipm = 4;
    auto = 2;
    tau = 0;
    if (method == "DP")
        # Optimze over SO(n) x Gamma
        q1 = curve_to_q(beta1)

        # Optimzie over SO(n)
        beta2, R, tau = find_rotation_seed_coord(beta1, beta2);
        q2 = curve_to_q(beta2);

        # Optimzie over Gamma
        q1i = vec(reshape(q1, M*n1, 1));
        q2i = vec(reshape(q2, M*n1, 1));
        G = zeros(M);
        T = zeros(M);
        sizei = Cdouble[0];
        ccall((:DynamicProgrammingQ2, libfdasrsf), Void,
            (Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Int32,
            Int32, Int32, Ptr{Float64},Ptr{Float64}, Int32, Int32,
            Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Float64), q2i, timet,
            q1i, timet, n1, M, M, timet, timet, M, M, G, T, sizei, lam)
        G = G[1:round(Integer,sizei[1])];
        T = T[1:round(Integer,sizei[1])];
        yi = interpolate((T,), G, Gridded(Linear()))
        gam = yi[timet];

    elseif (method == "DP2")
        c1 = vec(reshape(beta1', M*n1, 1));
        c2 = vec(reshape(beta2', M*n1, 1));
        opt = zeros(M+n1*n1+1);
        swap = false;
        fopts = zeros(5);
        comtime = zeros(5);
        @cpp ccall((:optimum_reparam, libgropt), Void,
                   (Ptr{Float64}, Ptr{Float64}, Int32, Int32, Float64, Bool,
                    Bool, Bool, Int32, Int32, Ptr{Float64}, Bool,
                    Ptr{Float64}, Ptr{Float64}), c1, c2, M, n1, 0.0, true,
                    rotated, isclosed, skipm, auto, opt, swap, fopts,
                    comtime)

        gam = opt[1:end-5];
        R = reshape(opt[end-4:end-1],2,2);

        if swap
            gam = invertGamma(gam);
            R = R';
        end

    else
        c1 = vec(reshape(beta1', M*n1, 1));
        c2 = vec(reshape(beta2', M*n1, 1));
        opt = zeros(M+n1*n1+1);
        swap = false;
        fopts = zeros(5);
        comtime = zeros(5);
        @cpp ccall((:optimum_reparam, libgropt), Void,
                   (Ptr{Float64}, Ptr{Float64}, Int32, Int32, Float64, Bool,
                    Ptr{Float64}, Bool, Bool, Bool, Int32, Int32, Ptr{Float64},
                    Ptr{Float64}), c1, c2, M, n1, w, false, rotated, isclosed,
                    skipm, auto, opt, swap, fopts, comtime)

        if fopts[1] == 1000
            @cpp ccall((:optimum_reparam, libgropt), Void,
                       (Ptr{Float64}, Ptr{Float64}, Int32, Int32, Float64, Bool,
                        Ptr{Float64}, Bool, Bool, Bool, Int32, Int32,
                        Ptr{Float64}, Ptr{Float64}), c1, c2, M, n1, 0.0, true,
                        rotated, isclosed, skipm, auto, opt, swap, fopts,
                        comtime)
        end

        gam = opt[1:end-5];
        R = reshape(opt[end-4:end-1],2,2);

        if swap
            gam = invertGamma(gam);
            R = R';
        end
    end

    gam = (gam-gam[1]) ./ (gam[end] - gam[1]);

    return gam, R, tau
end

"""
Find best rotation between two srvfs

    find_best_roation(q1,q2)
    :param q1: array (n, T)
    :param q2: array (n, T)
"""
function find_best_rotation(q1, q2)
    epsilon = eps(Float64);
    n, T = size(q1);
    A = q1*q2';
    U, S, V = svd(A);
    if (abs(det(U) * det(V) - 1) < 10 * epsilon)
        S = eye(n)
    else
        S = eye(n)
        S[:,end] = -S[:,end];
    end
    R = U*S*V';
    q2new = R*q2;

    return q2new, R
end

"""
Calculate variance along curve

    calculate_variance(beta)
    :param beta: array (n, T)
"""
function calculate_variance(beta)
    n, T = size(beta);
    betadot = zeros(n, T);
    for i = 1:n
        betadot[i,:] = gradient(vec(beta[i,:]), 1.0/(T-1));
    end
    normbetadot = zeros(T);
    centroid = calculatecentroid(beta);
    integrand = zeros(n, n, T);
    t = collect(linspace(0,1,T));
    for i = 1:T
        normbetadot[i] = norm(betadot[:,i]);
        a1 = beta[:,i] - centroid;
        integrand[:, :, i] = a1 * a1' * normbetadot[i];
    end
    l = trapz(t, normbetadot);
    variance = trapz(t, integrand, 3);
    variance /= l;

    return variance
end

"""
Calculate psi

    psi(x,a,q)
    :param x: array (n, T) of curve
    :param a: vector (n) of centroid
    :param q: array (n, T) of srvf
"""
function psi(x, a, q)
    T = size(q,2);
    covmat = calculate_variance(x+repmat(a,1,T));
    psi1 = covmat[1,1] - covmat[2,2];
    psi2 = covmat[1,2];
    psi3 = x[1,end];
    psi4 = x[2,end];

    return psi1, psi2, psi3, psi4
end

"""
Find basis normal to srvf

    find_basis_normal(q)
    :param q: array (n, T)
"""
function find_basis_normal(q)
    n, T = size(q);

    f1 = zeros(n,T);
    f2 = zeros(n,T);
    for i = 1:T
        f1[:,i] = q[1,i]*q[:,i]/norm(q[:,i])+[norm(q[:,i]);0];
        f2[:,i] = q[2,i]*q[:,i]/norm(q[:,i])+[0;norm(q[:,i])];
    end
    h3 = copy(f1);
    h4 = copy(f2);
    integrandb3 = zeros(T);
    integrandb4 = zeros(T);
    for i = 1:T
        integrandb3[i] = (q[:,i]'*h3[:,i])[1];
        integrandb4[i] = (q[:,i]'*h4[:,i])[1];
    end
    b3 = h3 - q*trapz(collect(linspace(0,1,T)),integrandb3);
    b4 = h4 - q*trapz(collect(linspace(0,1,T)),integrandb4);

    basis = Array(Any,2);
    basis[1] = b3;
    basis[2] = b4;

    return basis
end

"""
Calculate Jacobian of basis

    calc_j(basis)
    :param basis: array of (2) from find_basis_normal
"""
function calc_j(basis)
    b1 = basis[1];
    b2 = basis[2];
    T = size(b1,2);

    integrand11 = zeros(T);
    integrand12 = zeros(T);
    integrand22 = zeros(T);

    for i = 1:T
        integrand11[i] = (b1[:,i]'*b1[:,i])[1];
        integrand12[i] = (b1[:,i]'*b2[:,i])[1];
        integrand22[i] = (b2[:,i]'*b2[:,i])[1];
    end

    j = zeros(2,2);
    j[1,1] = trapz(collect(linspace(0,1,T)), integrand11);
    j[1,2] = trapz(collect(linspace(0,1,T)), integrand12);
    j[2,2] = trapz(collect(linspace(0,1,T)), integrand22);
    j[2,1] = j[1,2];

    return j
end

"""
Shift curve f by tau

    shift_f(f, tau)
    :param f: array (n,T)
    :param tau: scalar
"""
function shift_f(f, tau)
    n, T = size(f);
    fn = zeros(n,T);
    fn[:,1:T-1] = circshift(f[:,1:T-1],[0 tau]);
    fn[:,T] = fn[:,1];

    return fn
end

"""
Finds best rotation and seed value between two curves beta1, beta2

    find_rotation_seed_coord(beta1,beta2)
    :param beta1: array (n,T)
    :param beta2: array (n,T)

    :return beta2new: rotated curve
    :return O_hat1: rotation matrix
    :return tau: seed value
"""
function find_rotation_seed_coord(beta1,beta2)
    n,T = size(beta1);
    q1 = curve_to_q(beta1);
    Ltwo = zeros(T);
    Rlist = zeros(n, n, T);
    for ctr = 1:T
        beta2n = shift_f(beta2, ctr);
        beta2new, R = find_best_rotation(beta1, beta2n);
        q2new = curve_to_q(beta2new);
        Ltwo[ctr] = innerprod_q2(q1-q2new, q1-q2new);
        Rlist[:,:,ctr] = R;
    end

    tau = indmin(Ltwo);
    O_hat = Rlist[:,:,tau];
    beta2new = shift_f(beta2, tau);
    beta2new = O_hat * beta2new;

    return beta2new, O_hat, tau
end

"""
Find best rotation and seed value between two srvfs, q1 and q2

    find_rotation_and_seed_q(q1,q2)
    :param q1: array (n,T)
    :param q2: array (n,T)

    :return q2new: rotated srvf
    :return O_hat: rotation matrix
    :return tau: seed value
"""
function find_rotation_and_seed_q(q1,q2)
    n, T = size(q1);
    Ltwo = zeros(T);
    Rlist = zeros(n, n, T);
    for ctr = 1:T
        q2n = shift_f(q2, ctr);
        q2new, R = find_best_rotation(q1, q2n);
        Ltwo[ctr] = innerprod_q2(q1-q2new, q1-q2new);
        Rlist[:, :, ctr] = R;
    end

    tau = indmin(Ltwo);
    O_hat = Rlist[:,:,tau];
    q2new = shift_f(q2,tau);
    q2new = O_hat * q2new;

    return q2new, O_hat, tau
end

"""
Warp srvf by gamma

    group_action_by_gamma(q, gamma)
    :param q: array (n,T)
    :param gamma: vector (T)
"""
function group_action_by_gamma(q, gamma)
    n, T = size(q);
    gammadot = gradient(gamma, 1.0/T);
    qn = zeros(n, T);
    timet = collect(linspace(0, 1, T));

    for j = 1:n
        s = Spline1D(timet, vec(q[j, :]));
        qn[j, :] = evaluate(s, gamma) .* sqrt(gammadot);
    end

    qn /= sqrt(innerprod_q2(qn,qn));

    return qn
end

"""
Warp curve f by gamma

    group_action_by_gamma_coord(f, gamma)
    :param f: array (n,T)
    :param gamma: vector (T)
"""
function group_action_by_gamma_coord(f, gamma)
    n, T = size(f);
    fn = zeros(n,T);
    timet = collect(linspace(0,1,T));

    for j = 1:n
        s = Spline1D(timet, vec(f[j, :]));
        fn[j,:] = evaluate(s, gamma);
    end

    return fn
end

"""
Project curve onto normal tangent space

    project_curve(q)
    :param q: array (n,T)
"""
function project_curve(q)
    T = size(q,2);
    tol = 1e-5;
    maxit = 200;
    itr = 0;
    delta = 0.5;
    x = q_to_curve(q);
    a = -1 * calculatecentroid(x);

    psi1, psi2, psi3, psi4 = psi(x,a,q);

    r = [psi3,psi4];
    rnorm = zeros(maxit+1);
    rnorm[1] = norm(r);

    while itr <= maxit
        basis = find_basis_normal(q);

        # calculate Jacobian
        J = calc_j(basis);

        # Newton-Raphson step to update q
        y = J\(-r);
        dq = delta * (y[1]*basis[1] + y[2]*basis[2]);
        normdq = sqrt(innerprod_q2(dq,dq));
        q = cos(normdq) * q + sin(normdq) * dq/normdq;
        q /= sqrt(innerprod_q2(q,q));

        # update x and a from the new q
        beta_new = q_to_curve(q);
        x = copy(beta_new);
        a = -1 * calculatecentroid(x);
        beta_new = x + repmat(a,1,T);

        # calculate the new value of psi
        psi1, psi2, psi3, psi4 = psi(x,a,q);
        r = [psi3,psi4];
        rnorm[itr+1] = norm(r);

        if norm(r) < tol
            break
        end

        itr += 1;

    end

    rnorm = rnorm[1:itr-1];

    return q
end

"""
Pre-processes curve by centering and projecting to tangent space

    pre_proc_curve(beta, T=100)
    :param beta: array (n,T)
    :param T: number of re-sample points

    :return betanew: re-sampled and centered curve
    :return qnew: projected srvf
    :return A: rotation matrix
"""
function pre_proc_curve(beta, T=100)
    beta = resamplecurve(beta, T);
    q = curve_to_q(beta);
    qnew = project_curve(q);
    x = q_to_curve(qnew);
    a = -1 * calculatecentroid(x);
    betanew = x + repmat(a,1,T);
    A = eye(2)

    return betanew, qnew, A
end

"""
Calculate shooting vector and distance between two curves beta1 and beta2

    inverse_exp_coord(beta1, beta2)
    :param beta1: array (n,T)
    :param beta2: array (n,T)
    :param method: optimization method to find warping, default is
                   Dynamic Programming ("DP"). Other options are
                   Coordinate Descent ("DP2"), Riemannian BFGS
                   ("RBFGS")
    :param wscale: keep scale (false)

    :return v: shooting vector
    :return dist: shape distance
"""
function inverse_exp_coord(beta1, beta2; method::AbstractString="DP",
                           wscale=false)
    T = size(beta1,2);
    centroid1 = calculatecentroid(beta1);
    beta1 -= repmat(centroid1,1,T);
    centroid2 = calculatecentroid(beta2);
    beta2 -= repmat(centroid2,1,T);

    q1 = curve_to_q(beta1);

    # Iteratively optimize over SO(n) x Gamma using old DP
    gam, R, tau = optimum_reparam(beta1, beta2, method=method);
    beta2 = R * shift_f(beta2, tau);
    gamI = invert_gamma(gam);
    beta2 = group_action_by_gamma_coord(beta2, gamI);
    beta2, R, tau = find_rotation_seed_coord(beta1, beta2);
    q2n = curve_to_q(beta2);

    # Compute geodesic distance
    q1dotq2 = innerprod_q2(q1, q2n);
    if wscale
        dist = sqrt(innerprod_q2(q1-q2n, q1-q2n))
    else
        dist = acos(q1dotq2);
    end

    # Compute shooting vector
    if q1dotq2 > 1
        q1dotq2 = 1.;
    end

    u = q2n - q1dotq2 * q1;
    normu = sqrt(innerprod_q2(u, u));

    if normu > 1e-4
        v = u * acos(q1dotq2)/normu;
        if wscale
            v = q2n - q1
        end
    else
        v = zeros(2, T);
    end

    return v, dist
end

"""
Calculate shooting vector between two srvfs q1 and q2

    inverse_exp(q1, q2, beta2)
    :param q1: array (n,T)
    :param q2: array (n,T)
    :param beta2: array (n,T)

    :return v: shooting vector
"""
function inverse_exp(q1, q2, beta2)
    T = size(q1,2);
    centroid1 = calculatecentroid(beta2);
    beta2 -= repmat(centroid1,1,T);

    # Optimize over SO(n) x Gamma
    beta1 = q_to_curve(q1);
    gam, R, tau = optimum_reparam(beta1, beta2);
    gamI = invert_gamma(gam);
    beta2 = R * shift_f(beta2, tau);

    # Applying optimal re-parameterization to the second curve
    beta2 = group_action_by_gamma_coord(beta2, gamI);
    q2 = curve_to_q(beta2);

    # Optimize over SO(n)
    q2, R2, tau = find_rotation_and_seed_q(q1,q2);

    # Compute geodesic distance
    q1dotq2 = innerprod_q2(q1, q2);
    dist = acos(q1dotq2);

    # Compute shooting vector
    if q1dotq2 > 1
        q1dotq2 = 1.;
    end

    u = q2 - q1dotq2 * q1;
    normu = sqrt(innerprod_q2(u, u));

    if normu > 1e-4
        v = u * acos(q1dotq2) / normu;
    else
        v = zeros(2, T);
    end

    return v
end

"""
Gram-Schmidt Orthogonalization of basis

    gram_schmidt(basis)
    :param basis: basis out of find_basis_normal
"""
function gram_schmidt(basis)
    b1 = basis[1];
    b2 = basis[2];

    basis1 = b1 / sqrt(innerprod_q2(b1, b1));
    b2 = b2 - innerprod_q2(basis1,b2)*basis1;
    basis2 = b2 / sqrt(innerprod_q2(b2, b2));

    basis_o = Array(Any,2);
    basis_o[1] = basis1;
    basis_o[2] = basis2;

    return basis_o
end

"""
Project srvf onto tangent space

    project_tangent(w, q, basis)
    :param w: shooting vector
    :param q: array (n,T)
    :param basis: basis out of find_basis_normal
"""
function project_tangent(w, q, basis)
    w = w - innerprod_q2(w, q) * q;
    bo = gram_schmidt(basis);

    wproj = w - innerprod_q2(w, bo[1])*bo[1] - innerprod_q2(w,bo[2])*bo[2];

    return wproj
end

"""
Scale curve to unit length

    scale_curve(beta)
    :param beta: array (n,T)
"""
function scale_curve(beta)
    n, T = size(beta);
    normbetadot = zeros(T);
    betadot = zeros(n,T);
    for i = 1:n
        betadot[i,:] = gradient(vec(beta[i,:]), 1.0/T);
    end
    for i = 1:T
        normbetadot[i] = norm(betadot[:,i]);
    end
    scale = trapz(collect(linspace(0,1,T)), normbetadot);
    beta_scaled = beta / scale;

    return beta_scaled, scale
end

"""
Parallel translate srvf  along tangent space

    parallel_translate(w, q1, q2, basis, mode='O')
    :param w: shooting vector
    :param q1: array (n,T)
    :param q2: array (n,T)
    :param basis: basis out of find_basis_normal
    :param mode: Open ('O') or Closed ('C') curves
"""
function parallel_translate(w, q1, q2, basis, mode='O')
    wtilde = w - 2*innerprod_q2(w,q2) / innerprod_q2(q1+q2, q1+q2) * (q1+q2);
    l = sqrt(innerprod_q2(wtilde, wtilde));

    if mode == 'C'
        wbar = project_tangent(wtilde, q2, basis);
        normwbar = sqrt(innerprod_q2(wbar, wbar));
        if normwbar > 1e-4
            wbar = wbar * l / normwbar;
        end
    else
        wbar = wtilde;
    end

    return wbar
end

"""
Calculate zero crossing for optimal regression between curves
"""
function curve_zero_crossing(Y, beta, bt, y_max, y_min, gmax, gmin)
    T = size(beta,2);
    betanu = q_to_curve(bt);
    max_itr = 100;
    a = zeros(max_itr);
    a[1] = 1.;
    f = zeros(max_itr);
    f[1] = y_max - Y;
    f[2] = y_min - Y;
    mrp = f[1];
    mrn = f[2];
    mrp_ind = 1;  # most recent positive index
    mrn_ind = 2;  # most recent negative index

    for ii = 3:max_itr
        x1 = a[mrp_ind];
        x2 = a[mrn_ind];
        y1 = mrp;
        y2 = mrn;
        a[ii] = (x1 * y1 - x2 * y2) / float(y2-y1);

        beta1, O_hat, tau = find_rotation_seed_coord(betanu, beta);
        gamma = a[ii] * gmax + (1-a[ii]) * gmin;

        beta1 = group_action_by_gamma_coord(beta1, gamma);
        beta1, O_hat1, tau = find_rotation_seed_coord(betanu, beta1);
        q1 = curve_to_q(beta1);
        f[ii] = innerprod_q2(q1, bt) - Y;

        if abs(f[ii]) < 1e-5
            break
        elseif f[ii] > 0
            mrp = f[ii];
            mrp_ind = ii;
        else
            mrn = f[ii];
            mrn_ind = ii;
        end
    end

   gamma = a[ii] * gmax + (1-a[ii]) * gmin;

   beta1, O_hat, tau = find_rotation_seed_coord(betanu, beta1);
   beta1 = group_action_by_gamma_coord(beta1, gamma);
   beta1, O_hat1, tau = find_rotation_seed_coord(betanu, beta1);
   O_hat = O_hat*O_hat1;

  return gamma, O_hat, tau
end

"""
Find rotation matrix for angle theta

    rot_mat(theta)
"""
function rot_mat(theta)
    O = [cos(theta) -1*sin(theta); sin(theta) cos(theta)];

    return O
end

"""
Find inner product between two srvfs, q1 and q2

    innerprod_q2(q1, q2)
    :param q1: array (n,T)
    :param q2: array (n,T)
"""
function innerprod_q2(q1, q2)
    T = size(q1,2);
    val = sum(sum(q1.*q2))/T;

    return val
end

"""
Calculate elastic shape distance between two curves beta1 and beta2

    calc_shape_dist(beta1, beta2)
    :param beta1: array (n,T)
    :param beta2: array (n,T)
    :param method: optimization method to find warping, default is
                   Dynamic Programming ("DP"). Other options are
                   Coordinate Descent ("DP2"), Riemannian BFGS
                   ("RBFGS")
    :param wscale: with scale (false)
"""
function calc_shape_dist(beta1::Array{Float64,2}, beta2::Array{Float64,2};
                         method::AbstractString="DP", wscale=false)

    T1 = size(beta1,2)
    T2 = size(beta2,2)

    if (T1 != T2)
        if (T1>T2)
            beta2 = resamplecurve(beta2, T1)
        else
            beta1 = resamplecurve(beta1, T2)
        end
    end

    v, d = inverse_exp_coord(beta1, beta2, wscale=wscale)

    return d
end

"""
Pairwise align two curves

    curve_pair_align(beta1::Array{Float64,2}, beta2::Array{Float64,2})
    :param beta1: array (n,T)
    :param beta2: array (n,T)

    Returns
    :return beta2n: aligned curve 2 to 1
    :return q2n: aligned srvf 2 to 1
    :return gam: warping function
    :return q1: srvf of curve 1
"""
function curve_pair_align(beta1::Array{Float64,2}, beta2::Array{Float64,2})

    T = size(beta1,2);
    centroid1 = calculatecentroid(beta1);
    beta1 -= repmat(centroid1,1,T);
    centroid2 = calculatecentroid(beta2);
    beta2 -= repmat(centroid2,1,T);

    q1 = curve_to_q(beta1);

    # Iteratively optimize over SO(n) x Gamma using old DP
    gam, R, tau = optimum_reparam(beta1, beta2);
    beta2n = R * shift_f(beta2, tau);
    gamI = invert_gamma(gam);
    beta2n = group_action_by_gamma_coord(beta2n, gamI);
    beta2n, R, tau = find_rotation_seed_coord(beta1, beta2n);
    q2n = curve_to_q(beta2n);

    return beta2n, q2n, gamI, q1
end

"""
Form geodesic between two curves

    curve_geodesic(beta1::Array{Float64,2}, beta2::Array{Float64,2}, k::Integer=5)
    :param beta1: array (n,T)
    :param beta2: array (n,T)
    :param k: number of curves along geodesic
    :param wscale: with scale (false)

    Returns
    :return geod: curves along geodesic (n,T,k)
    :return geod_q: srvf's along geodesic
"""
function curve_geodesic(beta1::Array{Float64,2}, beta2::Array{Float64,2},
                        k::Integer=5; wscale=false)

    n, T = size(beta1);
    beta1 = resamplecurve(beta1, T);
    beta2 = resamplecurve(beta2, T);
    centroid1 = calculatecentroid(beta1);
    beta1 -= repmat(centroid1,1,T);
    centroid2 = calculatecentroid(beta2);
    beta2 -= repmat(centroid2,1,T);

    q1 = curve_to_q(beta1);

    # Iteratively optimize over SO(n) x Gamma using old DP
    gam, R, tau = optimum_reparam(beta1, beta2);
    beta2n = R * shift_f(beta2, tau);
    gamI = invert_gamma(gam);
    beta2n = group_action_by_gamma_coord(beta2n, gamI);
    beta2n, R, tau = find_rotation_seed_coord(beta1, beta2n);
    q2n = curve_to_q(beta2n);

    # form geodesic between the registered curves
    if wscale
        dist = sqrt(innerprod_q2(q1-q2n,q1-q2n))
    else
        dist = acos(innerprod_q2(q1,q2n));
    end
    geod = zeros(n,T,k);
    geod_q = zeros(n,T,k);

    for tau = 1:k
        s = dist*(tau-1)/(k-1);
        geod_q[:,:,tau] = (sin(dist-s)*q1+sin(s)*q2n)/sin(dist);
        geod[:,:,tau] = q_to_curve(geod_q[:,:,tau]);
    end

    return geod, geod_q
end


using InverseIsing
using Test

function anneal_test1()
    h = Dict(1 => -1)
    J = Dict((1, 2) => 1)
    res = anneal(h, J)
    @test res.states == [[-1, -1]]
    @test res.indices == [1, 2]
    @test res.sample == Dict(1 => -1, 2 => -1)
    @test res.energies == [-1.5]
end

function anneal_test2()
    # with indices
    h = Dict(:a => 1)
    J = Dict((:a, :b) => -1)
    res = anneal(h, J, n_read = 10)
    @test hcat(res.states...) == repeat([1, -1] ,1, 10)
    @test res.indices == [:a, :b]
    @test res.sample == Dict(:a => 1, :b => -1)
    @test res.energies == repeat([-1.5], 10)
end

function anneal_test3()
    # anti-ferromagnetic
    N = 5 # Number of units
    h = Dict(1 => 1)
    J = Dict(((i,i+1) ,-1) for i in 1:N-1)
    res = anneal(h, J, n_read=10)
    @test res.states[1] == [(-1)^(i+1) for i in 1:N]
    @test [res.sample[key] for key in 1:N] == res.states[argmin(res.energies)]
    @test res.indices == collect(1:N)
end

@testset "Test anneal" begin
    anneal_test1()
    anneal_test2()
    anneal_test3()
end


"""
    extrapolatetoboundary(v, x̄, bc::Tuple{BoundaryCondition, BoundaryCondition})

Returns a `length(x̄)`-vector whose `2:(length(x̄)-1)` elements are `v`,
the first and last element are extrapolated `v` on the boundaries of `x̄` according to
boundary conditions `bc` given.

The first element of `bc` is applied to the lower bound, and second element of `bc` to the upper.
# Examples
```jldoctest; setup = :(using SimpleDifferentialOperators)
julia> x̄ = -2:2
0:5

julia> x = interiornodes(x̄)
  1
  2
  3

julia> v = (x -> x^2).(x)
3-element Array{Int64,1}:
  1
  0
  1

julia> extrapolatetoboundary(v, x̄, (Absorbing(), Absorbing()))
5-element Array{Int64,1}:
  0
  1
  0
  1
  0

julia> extrapolatetoboundary(v, x̄, (Absorbing(), Reflecting()))
 5-element Array{Int64,1}:
  0
  1
  0
  1
  1

julia> extrapolatetoboundary(v, x̄, (Mixed(ξ = 3.0), Reflecting()))
5-element Array{Float64,1}:
 -0.5
  1.0
  0.0
  1.0
  1.0

```
"""
function extrapolatetoboundary(v, x̄, bc::Tuple{BoundaryCondition, BoundaryCondition})
    # setup for extension
    T = eltype(v)

    # default extrapolated boundaries (absorbing for both sides)
    lb_extended = zero(T)
    ub_extended = zero(T)

    # extrapolate on the lower bound
    if (typeof(bc[1]) <: Reflecting)
        lb_extended = v[1]
    elseif (typeof(bc[1]) <: Mixed)
        ξ_lb = bc[1].ξ
        Δ_1_minus = x̄[2] - x̄[1]

        # apply extrapolation based on the directions
        if (bc[1].direction != :backward)
          lb_extended = v[1]/(1-ξ_lb*Δ_1_minus)
        else
          lb_extended = (1+ξ_lb*Δ_1_minus)*v[1]
        end
    end

    # extrapolate on the upper bound
    if (typeof(bc[2]) <: Reflecting)
        ub_extended = v[end]
    elseif (typeof(bc[2]) <: Mixed)
        ξ_ub = bc[2].ξ
        Δ_M_plus = x̄[end] - x̄[end-1]

        # apply extrapolation based on the directions
        if (bc[2].direction != :forward)
          ub_extended = v[end]/(1+ξ_ub*Δ_M_plus)
        else
          ub_extended = (1-ξ_ub*Δ_M_plus)*v[end]
        end
    end

    return [lb_extended; v; ub_extended]
end


# ---
# title: 898. Bitwise ORs of Subarrays
# id: problem898
# author: Tian Jun
# date: 2020-10-31
# difficulty: Medium
# categories: Dynamic Programming, Bit Manipulation
# link: <https://leetcode.com/problems/bitwise-ors-of-subarrays/description/>
# hidden: true
# ---
#
# We have an array `A` of non-negative integers.
#
# For every (contiguous) subarray `B = [A[i], A[i+1], ..., A[j]]` (with `i <=
# j`), we take the bitwise OR of all the elements in `B`, obtaining a result
# `A[i] | A[i+1] | ... | A[j]`.
#
# Return the number of possible results.  (Results that occur more than once are
# only counted once in the final answer.)
#
#
#
# **Example 1:**
#
#
#
#     Input: [0]
#     Output: 1
#     Explanation:
#     There is only one possible result: 0.
#
#
# **Example 2:**
#
#
#
#     Input: [1,1,2]
#     Output: 3
#     Explanation:
#     The possible subarrays are [1], [1], [2], [1, 1], [1, 2], [1, 1, 2].
#     These yield the results 1, 1, 2, 1, 3, 3.
#     There are 3 unique values, so the answer is 3.
#
#
# **Example 3:**
#
#
#
#     Input: [1,2,4]
#     Output: 6
#     Explanation:
#     The possible results are 1, 2, 3, 4, 6, and 7.
#
#
#
#
# **Note:**
#
#   1. `1 <= A.length <= 50000`
#   2. `0 <= A[i] <= 10^9`
#
#
## @lc code=start
using LeetCode

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


module TestDatasetsSynthetic

using Test
using MLJBase.DatasetsSynthetic

@testset "make_blobs Tests" begin

    @testset "make_blobs Tests: n=110, p=2, center=3" begin

        n, p, centers = 110, 2, 3
        X, y = DatasetsSynthetic.make_blobs(n; p=p, centers=centers)
        @test (n, p) == size(X)
        @test n == length(y)
        @test centers == length(unique(y))
        end;

    @testset "make_blobs Tests n=90, p=3, center=4" begin
        n, p, centers = 90, 3, 4
        X, y = DatasetsSynthetic.make_blobs(n; p=p, centers=centers)
        @test (n, p) == size(X)
        @test n == length(y)
        @test centers == length(unique(y))
    end;
end;

@testset "make_circles Tests" begin
    n = 55
    X, y = DatasetsSynthetic.make_circles(n)
    @test (n, 2) == size(X)
    @test n == length(y)
    @test 2 == length(unique(y))
end;

@testset "make_moons Tests" begin
    n = 55
    X, y = DatasetsSynthetic.make_moons(n)
    @test (n, 2) == size(X)
    @test n == length(y)
    @test 2 == length(unique(y))
end;

end # module
true


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

doc"""An `IntervalBox` is an $N$-dimensional rectangular box, given
by a Cartesian product of $N$ `Interval`s.
"""
struct IntervalBox{N,T} <: StaticVector{N, Interval{T}}
    data::NTuple{N,Interval{T}}
end

# StaticArrays.Size{N,T}(::Type{IntervalBox{N,T}}) = StaticArrays.Size(N) # @pure not needed, I think...
Base.getindex(a::IntervalBox, i::Int) = a.data[i]

IntervalBox(x::Interval) = IntervalBox( (x,) )  # single interval treated as tuple with one element

## arithmetic operations
# Note that standard arithmetic operations are implemented automatically by FixedSizeArrays.jl

mid(X::IntervalBox) = mid.(X)

## set operations

# TODO: Update to use generator
⊆(X::IntervalBox{N,T}, Y::IntervalBox{N,T}) where {N,T} =
    all(X .⊆ Y)

∩(X::IntervalBox{N,T}, Y::IntervalBox{N,T}) where {N,T} =
    IntervalBox(X .∩ Y)
∪(X::IntervalBox{N,T}, Y::IntervalBox{N,T}) where {N,T} =
    IntervalBox(X .∪ Y)

#=
On Julia 0.6 can now write
∩{N,T}(X::IntervalBox{N,T}, Y::IntervalBox{N,T}) = IntervalBox(NTuple{N, Interval{Float64}}( (X[i] ∩ Y[i]) for i in 1:N))
=#

isempty(X::IntervalBox) = any(isempty, X)

diam(X::IntervalBox) = maximum(diam.(X))

emptyinterval(X::IntervalBox{N,T}) where {N,T} = IntervalBox(emptyinterval.(X))

import Base.×
×(a::Interval...) = IntervalBox(a...)
×(a::Interval, b::IntervalBox) = IntervalBox(a, b...)
×(a::IntervalBox, b::Interval) = IntervalBox(a..., b)
×(a::IntervalBox, b::IntervalBox) = IntervalBox(a..., b...)

IntervalBox(x::Interval, ::Type{Val{n}}) where {n} = IntervalBox(SVector(ntuple(i->x, Val{n})))

IntervalBox(x::Interval, n::Int) = IntervalBox(x, Val{n})


# %% permutation entropy
using DrWatson
@quickactivate "NonlinearDynamicsTextbook"
include(srcdir("style.jl"))
using DynamicalSystems, PyPlot, Random

using Random, Combinatorics, Statistics
Random.seed!(356)
o = 3
N = 8
x = rand(N)

fig = figure(figsize = (figx, figx/3.5))
ax1 = subplot2grid((1, 3), (0, 0))
ax1.plot(1:N, x, marker = "o", color = "C0", mfc = "C2", ms = 15, zorder = 99)
ax1.set_title("timeseries", size = 28)
ax1.spines["bottom"].set_position("center")
ax1.spines["right"].set_color("none")
ax1.spines["top"].set_color("none")
ax1.set_xticks([])
ax1.set_yticks([])
ax1.set_xlabel("\$t\$")
ax1.set_ylabel("\$x\$", rotation = 0)
ax1.xaxis.set_label_coords(1.0, 0.5)
ax1.yaxis.set_label_coords(-0.05, 0.9)
ax1.set_ylim(-0.1, 1.1)
# ax1.axis("off")

# add pattern indications
for (s, n) in zip(("2", "3", "3"), (2, 4, 6))
    xs = [n, n, n+o-1, n+o-1]
    ym = minimum(x[xs[1]:xs[end]]) - 0.1
    yd = ym - 0.02
    ys = [ym, yd, yd, ym] #.+ 0.05
    ax1.plot(xs, ys, color = "k", lw = 1)
    ax1.text(mean(xs), minimum(ys)-0.02, "#=$s", ha="center", va = "top")
end

# ax1.annotate("4", (2, -0.2), xytext = (4, -0.2), ha="center")

ax2 = subplot2grid((1, 3), (0, 1), colspan = 2)
p = permutations([0, 0.5, 1], o) |> collect
counts = [0, 1, 3, 1, 0, 1]
for (i, a) in enumerate(p)
    ax2.plot((1:o) .+ i*o .+ 1, a, marker = "o",
    color = "C0", mfc = "C2", ms = 10)
    ax2.text(o÷2 + i*o + 1, 1.2, "$i")
    ax2.text(o÷2 + i*o + 1, -0.5, "$(counts[i])")
end
ax2.text(o+1, 1.2, "#", ha = "right")
ax2.text(o+1, 0.5, "pattern", ha = "right")
ax2.text(o+1, -0.5, "count", ha = "right")
ax2.set_ylim(-1, 1.5)
ax2.set_xlim(0, ax2.get_xlim()[2])
ax2.set_title("relative amplitude permutations, \$d=$o\$", size = 28)
ax2.axis("off")

fig.tight_layout()
fig.subplots_adjust(top = 0.9, bottom = 0.02, right = 0.95, left = 0.05, wspace=0.1, hspace = 0.1)

wsave(plotsdir("6", "permentropy"), fig)


# Julia1_3.2.jl  Looping Control Structures
#
# while loops keeps evaluating body of loop
#   until condition is false
# as with other blocks value defined in block
#    is not available in outer blocks
#    but is available to a nested inner blocks
i=0
while i<26
    global i
    print('A'+i)
    i+=1
end

# for loop
# i steps through <range> 0 to 25 by 1
# loop index not available outside loop block
for i=0:25
    print('a'+i)
end

# i steps through range by 2s*
for i=0:2:25
    print('a'+i)
end

# for can loop over any container
for i in [2,0,19]
    print('a'+i)
end

# The word <break> will exit a <while> or <for> loop
# This is useful for iterations that can terminate
#   early either because it is diverging or
#   has converged below an error bound.
i=0
while i<26
    global i
    print('A'+i)
    i+=1
    if i>5; break; end
end

# <continue> key word skips to next loop start
# in a for loop
for i=0:25
    2<i<23 && continue
    print('a'+i)
end

###

# nested <for> loops for matrices and in general
M=Array{Int,2}(undef,3,3)
(d1,d2)=size(M)
for i=1:d1, j=1:d2
    global M
    M[i,j]=i*j
end
println(M)

# exception handling
# try <do something>
# catch <if error, handle it>
# end
function fsqrt(x)
    try; sqrt(x)
    catch; sqrt(Complex(x,0))
    end
end
fsqrt(-4)

# <throw> your own error
# there is a list of 35 Built-in exceptions
#  but you can add your own if you don't find
#  it on the list
f(x)= x≥0 ? exp(-x) : throw(DomainError(x, "arg must be non-negative"))
#
f(-1)
f(1)


using OrdinaryDiffEq, DiffEqBase, Test, DiffEqProblemLibrary, DiffEqDevTools,
      Random

using DiffEqProblemLibrary.ODEProblemLibrary: importodeproblems; importodeproblems()
import DiffEqProblemLibrary.ODEProblemLibrary: prob_ode_bigfloatlinear,
                                               prob_ode_bigfloat2Dlinear,
                                               prob_ode_2Dlinear

## Convergence Testing
println("Convergence Test on Linear")

testTol = 1
prob = prob_ode_2Dlinear
println("Feagin RKs")
dts = (1//2) .^ (4:-1:2)
sol = solve(prob,Feagin10(),dt=dts[1])
prob = remake(prob_ode_bigfloat2Dlinear,tspan=(big(0)//1,big(1)//1))
sol = solve(prob,Feagin10(),dt=dts[1])

prob = remake(prob_ode_bigfloat2Dlinear,tspan=(big(0.0),big(1.0)))
dts = (1//2) .^ (4:-1:2)
sim = test_convergence(dts,prob,Feagin10())
@test abs(sim.𝒪est[:final]-8) < testTol #Lowered due to low test dt

sim = test_convergence(dts,prob,Feagin12())
@test abs(sim.𝒪est[:final]-12) < testTol

sim = test_convergence(dts,prob,Feagin14())
@test abs(sim.𝒪est[:final]-15) < testTol #Upped to 15 for test

prob = prob_ode_bigfloatlinear

dts = (1//2) .^ (6:-1:3)
sim = test_convergence(dts,prob,Feagin10())
@test abs(sim.𝒪est[:final]-10) < testTol

dts = (1//2) .^ (4:-1:2)
sim = test_convergence(dts,prob,Feagin12())
@test abs(sim.𝒪est[:final]-12) < testTol

sim = test_convergence(dts,prob,Feagin14())
@test abs(sim.𝒪est[:final]-15) < testTol #Upped to 15 for test

prob = prob_ode_bigfloat2Dlinear

#compile
sol =solve(prob,Feagin10(),dt=dts[1])
sol =solve(prob,Feagin12(),dt=dts[1])
sol =solve(prob,Feagin14(),dt=dts[1])

#test
@time sol =solve(prob,Feagin10(),dt=dts[1])
@time sol =solve(prob,Feagin12(),dt=dts[1])
@time sol =solve(prob,Feagin14(),dt=dts[1])


import ThreeJS
import Colors: colormap

# Run in Escher to get updatable surf and mesh plots

function surf(f::Function)
 ThreeJS.outerdiv() <<
            (ThreeJS.initscene() <<
                [
                    ThreeJS.mesh(0.0, 0.0, 0.0) <<
                    [
                        ThreeJS.parametric(100,100,-10:10, -10:10,f),
                        ThreeJS.material(Dict(:kind=>"lambert",:color=>"white",
                        :colorkind=>"vertex"));
                    ],
                    ThreeJS.ambientlight(),
                    ThreeJS.camera(30.0, 30.0, 30.0)
                ]
            )
end

function mesh(f::Function)
 ThreeJS.outerdiv() <<
            (ThreeJS.initscene() <<
                [
                    ThreeJS.meshlines(50, 50, -10:10, -10:10, f),
                    ThreeJS.ambientlight(),
                    ThreeJS.camera(30.0, 30.0, 30.0);
                ]
            )
end
main(window) =  begin
    push!(window.assets,("ThreeJS","threejs"))
    push!(window.assets,"widgets")
    push!(window.assets, "codemirror")
    push!(window.assets, "layout2")
    inp = Signal(Dict())
    s = sampler() # A thing that lets you watch widgets/behaviors upon updates to other behaviors
    default = "(x,y) -> sin(x) * cos(y)"
    editor = watch!(s, :code, codemirror(default))
    code_cell = trigger!(s, :submit, keypress("ctrl+enter shift+enter", editor))
    t, plots = wire(
                    tabs(["Surf";"Mesh";]),
                    pages(
                    [
                        map(inp) do f
                            fn = get(f,:code,default)
                            try
                                surf(eval(parse(fn)))
                            catch
                                Elem(:div, "Something went wrong. Please check your syntax and try again. Contact rohitvarkey@gmail.com for more assistance.")
                            end
                        end;
                        map(inp) do f
                            fn = get(f,:code,default)
                            try
                                mesh(eval(parse(fn)))
                            catch
                                Elem(:div, "Something went wrong. Please check your syntax and try again. Contact rohitvarkey@gmail.com for more assistance.")
                            end
                        end;
                    ]
                    ),
                    :tabschannel,
                    :selected
                )
    vbox(
            md"""Enter an anonymous function with 2 variables.
                `ctrl+enter` or `shift+enter` to redraw the plot.
                Use the mouse the drag, zoom and pan.
                The function is plotted with x and y between -10 and 10 and
                with 100 steps and 50 steps in both axes for the surf and the
                mesh respectively. Try resizing the browser if you cant see a codebox""",
            intent(s, code_cell) >>> inp,
            t, plots
        ) |> pad(2em)
end


B = Array{Int}(2, 3, 4, 5, 6)

n = 0
for i5=1:6, i4=1:5, i3=1:4
    n += 1
    B[.., i3, i4, i5] = n
end
# B => 2×3×4×5×6 Array{Int64,5}:
# [:, :, 1, 1, 1] =
#  1  1  1
#  1  1  1
#
# [:, :, 2, 1, 1] =
#  2  2  2
#  2  2  2
#
#  :
#
# [:, :, 4, 5, 6] =
#  120  120  120
#  120  120  120

@test B[1, 1, ..] == B[1, 1, :, :, :] == reshape(1:120, 4, 5, 6)
@test B[1, 1, .., 1] == B[1, 1, :, :, 1] == reshape(1:20, 4, 5)

C = Array{Int}(2, 3, 4, 5, 6)

n = 0
for i3=1:4, i2=1:3, i1=1:2
    n += 1
    C[i1, i2, i3, ..] = n
end
# C => 2×3×4×5×6 Array{Int64,5}:
# [:, :, 1, 1, 1] =
#  1  3  5
#  2  4  6
#
# [:, :, 2, 1, 1] =
#  7   9  11
#  8  10  12
#
#  :
#
# [:, :, 4, 5, 6] =
#  19  21  23
#  20  22  24

@test C[.., 1, 1] == C[:, :, :, 1, 1] == reshape(1:24, 2, 3, 4)
@test C[1, .., 1, 1] == C[1, :, :, 1, 1] == reshape(1:2:24, 3, 4)

D = ones(Int, 1, 2, 3, 4)
D[1, .., 2] = 2
@test D == [i1 == 1 && i4 == 2 ? 2 : 1 for i1=1:1, i2=1:2, i3=1:3, i4=1:4]


### A Pluto.jl notebook ###
# v0.12.10

using Markdown
using InteractiveUtils

# ╔═╡ 9f18ccd0-8e25-11eb-20e6-ab941f663550
using Plots

# ╔═╡ 10a9374c-8e1a-11eb-10b4-fb8272dd46d1
md"""
## SIR Model Simulation

#### Intro

The **SIR Model** has been developed to simulate an epidemic over time. The model consists of a system of 3 differential equations that express the rates of change of 3 variables over time. The 3 variables are:

1. **S** - the susceptibles of getting the infection
2. **I** - the infected
3. **R** - the recovered from the infection
3. D - the number of dead

#### The Model
Here follow the 3 equations that govern the model dynamics:

${ \begin{align*}
\frac{\mathrm{d}S}{\mathrm{d}t} & = -\beta \cdot S \cdot I + \delta \cdot R \\
\frac{\mathrm{d}I}{\mathrm{d}t} & = \beta \cdot S \cdot I + \gamma \cdot I \\
\frac{\mathrm{d}R}{\mathrm{d}t} & = \gamma \cdot I - \delta \cdot R \\
\end{align*} }$

#### Model simulation
The following is a simulation of the model described above
"""

# ╔═╡ 43970fa0-8e1b-11eb-3052-1701966a4478
begin
	#Model parameters
	beta1 = 0.14 	#rate of infection
	gamma1 = 0.07 	#rate of recovery (try also 0.07)
	delta1 = 0.00 	#rate of immunity loss
	rho1 = 0.001 	#rate of death of infected
	N1 = 6*10^7 	#Total population N = S + I + R + D
	I0 = 100.0 	#initial number of infected
	beta2 = 0.0 	#rate of infection
	gamma2 = 0.035 	#rate of recovery (try also o.07)
	delta2 = 0.0 	#rate of immunity loss
	rho2 = 0.2 	#rate of death of infected
	beta3 = 0.0 	#rate of infection from group 1 to group 2
	beta4 = 0.0 	#rate of infection from group 2 to group 1
	N2 = 1*10^6 #Total population N = S + I + R + D
	J0 = 0.0 	#initial number of infected
	gamma = 0.0
	T = 38*30 	#period of 300 daysplot
	dt = 1/400 	#time interval of 6 hours (1/4 of a day)

	md"""
	Value of parameter R10 is $(beta1/gamma1) | Value of parameter R20 is $(beta2/gamma2)
	"""
end

# ╔═╡ 883a4ad8-8e26-11eb-06c4-e30f3c0f70c1
# Plots that display the epidemic outbreak
tt = 0:dt:T-dt;

# ╔═╡ 5f948160-8e27-11eb-1003-fbcf284800e6
#Map
#plot(tt,[I1,I2,D1,D2],color=[:blue :red :green :yellow], lw=2, grid=true, xlabel="Days", ylabel="Number of individuals", label=["I1" "I2" "D1" "D2"])

# ╔═╡ c30b3fb4-8e1c-11eb-05c7-d7f95aa97fcf
md"Function that calculates the model evolution at time period T"

# ╔═╡ 01a236b0-8e1d-11eb-2630-ebff3f351bb3
function sir_model(beta1,gamma1,delta1,rho1,N1,beta2,gamma2,delta2,rho2,N2,beta3,beta4,I0,J0,T,dt)
	S1 = zeros(Int(T/dt))
	S1[1] = N1
	I1 = zeros(Int(T/dt))
	I1[1] = I0
	R1 = zeros(Int(T/dt))
	D1 = zeros(Int(T/dt))
	S2 = zeros(Int(T/dt))
	S2[1] = N2
	I2 = zeros(Int(T/dt))
	I2[1] = J0
	R2 = zeros(Int(T/dt))
	D2 = zeros(Int(T/dt))
	I = zeros(Int(T/dt))
	J1 = zeros(Int(T/dt))
	J2 = zeros(Int(T/dt))
	gamma = gamma1

	for tt ∈ 1:Int(T/dt)-1
		if I2[tt] + I1[tt] > 10
			if tt > 10/dt
				if J2[tt-Int(5/dt)] > 200
					gamma = 2.8*gamma1
				end
				if J2[tt-Int(5/dt)] < 17
					gamma = gamma1
				end
			end

			dS1 = (-beta1*I1[tt]*S1[tt]/N1+delta1*R1[tt]-beta4*S1[tt]*I2[tt]/N1) * dt
			dI1 = (beta1*I1[tt]*S1[tt]/N1-gamma*I1[tt]+beta4*S1[tt]*I2[tt]/N1) * dt
			dR1 = (gamma*(1-rho1)*I1[tt]-delta1*R1[tt]) * dt
			dD1 = (gamma*rho1*I1[tt]) * dt
			dJ1 = beta1*I1[tt]*S1[tt]/N1

			S1[tt+1] = S1[tt] + dS1
			I1[tt+1] = I1[tt] + dI1
			R1[tt+1] = R1[tt] + dR1
			D1[tt+1] = D1[tt] + dD1
			J1[tt+1] = J1[tt] + dJ1

			dS2 = (-beta2*I2[tt]*S2[tt]/N2+delta2*R2[tt]-beta3*I1[tt]*S2[tt]/N1) * dt
			dI2 = (beta2*I2[tt]*S2[tt]/N2-gamma2*I2[tt]+beta3*I1[tt]*S2[tt]/N1) * dt
			dR2 = (gamma2*(1-rho2)*I2[tt] - delta2*R2[tt]) * dt
			dD2 = (gamma2*rho2*I2[tt]) * dt

			S2[tt+1] = S2[tt] + dS2
			I2[tt+1] = I2[tt] + dI2
			R2[tt+1] = R2[tt] + dR2
			D2[tt+1] = D2[tt] + dD2
			if tt > 7/dt +1
				#I[tt+1] = I1[tt+1]+I2[tt+1]+R1[tt+1]+R2[tt+1]-(I1[tt-7/dt]+I2[tt-7/dt]+R1[tt-7/dt]+R2[tt-7/dt])
				#J1[tt+1] = (I1[tt+1]+R1[tt+1]+D1[tt+1]-D1[tt-7/dt]-(I1[tt-7/dt]+R1[tt-7/dt]))*100000/N1
				J2[tt+1] = (J1[tt+1] - J1[tt-Int(7/dt)])*100000/N1
				#J2[tt+1] = I2[tt+1]+R2[tt+1]+D2[tt+1]-D2[tt-7/dt]-(I2[tt-7/dt]+R2[tt-7/dt])
			end
		else
			print("boom")
			break
		end
	end
	return S1, I1, R1, D1, S2, I2, R2, D2, I, J1, J2
end;

# ╔═╡ 81f4c3f4-8e23-11eb-2f4e-f1b0173d3ef6
#Calculate the model
S1, I1, R1, D1, S2, I2, R2, D2, I, J1, J2 = sir_model(beta1,gamma1,delta1,rho1,N1,beta2,gamma2,delta2,rho2,N2,beta3,beta4,I0,J0,T,dt);

# ╔═╡ 116cc80c-8e25-11eb-1120-911f0eae6a3c
# Curves
plot(tt,[S1,I1,R1,D1],color=[:blue :red :green :yellow], label=["S" "I" "R" "D"], lw=5, grid=true,xlabel="Days",ylabel="Number of individuals")

# ╔═╡ c201ce30-8e26-11eb-15a1-99f683ff6d20
plot(tt,[S2,I2,R2,D2],color=[:blue :red :green :yellow], label=["S2" "I2" "R2" "D2"], lw=2, grid=true,xlabel="Days",ylabel="Number of individuals")

# ╔═╡ f7b43f62-8e27-11eb-1675-55e0b4f3462e
#plot incidence
plot(tt,J1,color=:blue,lw=2,xlabel="Days",ylabel="7 day incidence",label="I")

# ╔═╡ 5f5686ac-8e28-11eb-24a1-299e86f64808
#plot incidence
plot(tt,J2,color=:blue,lw=2,xlabel="Days",ylabel="7 day incidence",label="I")

# ╔═╡ Cell order:
# ╟─10a9374c-8e1a-11eb-10b4-fb8272dd46d1
# ╠═43970fa0-8e1b-11eb-3052-1701966a4478
# ╠═81f4c3f4-8e23-11eb-2f4e-f1b0173d3ef6
# ╠═883a4ad8-8e26-11eb-06c4-e30f3c0f70c1
# ╠═116cc80c-8e25-11eb-1120-911f0eae6a3c
# ╠═c201ce30-8e26-11eb-15a1-99f683ff6d20
# ╠═5f948160-8e27-11eb-1003-fbcf284800e6
# ╠═f7b43f62-8e27-11eb-1675-55e0b4f3462e
# ╠═5f5686ac-8e28-11eb-24a1-299e86f64808
# ╟─c30b3fb4-8e1c-11eb-05c7-d7f95aa97fcf
# ╠═01a236b0-8e1d-11eb-2630-ebff3f351bb3
# ╠═9f18ccd0-8e25-11eb-20e6-ab941f663550


# scep256k1 constants
A = 0
B = 7
P = big(2)^256 - 2^32 - 977
N = big"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"

"Element in an scep256k1 field"
struct S256Element <: PrimeField
     𝑛::BigInt
     𝑝::BigInt
     S256Element(𝑛,𝑝=P) = !infield(𝑛,𝑝) ? throw(DomainError("𝑛 is not in field range")) : new(𝑛,𝑝)
end

S256Element(x::S256Element) = x

"Formats S256Element showing 𝑛 in hexadecimal format"
function show(io::IO, z::S256Element)
    print(io, string(z.𝑛, base = 16),"\n(in scep256k1 field)")
end

"Returns the square root of an S256Element"
function sqrt(𝑃::S256Element)
    return 𝑃^fld(ECC.P + 1, 4)
end

A = S256Element(A)
B = S256Element(B)

"""
    S256Point{T<:Number}

S256Point(𝑥::T, 𝑦::T) where {T<:Union{S256FieldElement, Integer, Infinity}}
represents a point in an scep256k1 field.
"""
struct S256Point{T<:Number} <: AbstractPoint
    𝑥::T
    𝑦::T
    𝑎::S256Element
    𝑏::S256Element
    S256Point{T}(𝑥,𝑦,𝑎=A,𝑏=B) where {T<:Number} = new(𝑥,𝑦,𝑎,𝑏)
end

S256Point(::Infinity,::Infinity) = S256Point{Infinity}(∞,∞)
S256Point(𝑥::S256Element,𝑦::S256Element) = !iselliptic(𝑥,𝑦,A,B) ? throw(DomainError("Point is not on curve")) : S256Point{S256Element}(𝑥,𝑦)
S256Point(x::Integer,y::Integer) = S256Point{S256Element}(S256Element(big(x)),S256Element(big(y)))

"Formats S256Point as (𝑥, 𝑦) in hexadecimal format"
function show(io::IO, z::S256Point)
    if typeof(z.𝑥) <: PrimeField
        x, y = z.𝑥.𝑛, z.𝑦.𝑛
    else
        x, y = z.𝑥, z.𝑦
    end
    print(io, "scep256k1 Point(𝑥,𝑦):\n", string(x, base = 16), ",\n", string(y, base = 16))
end

"Compares two S256Point, returns true if coordinates are equal"
==(x::S256Point, y::S256Point) = x.𝑥 == y.𝑥 && x.𝑦 == y.𝑦

"Scalar multiplication of an S256Point"
function *(λ::Integer,𝑃::S256Point)
    𝑅 = S256Point(∞, ∞)
    λ =  mod(λ, N)
    while λ > 0
        if λ & 1 != 0
            𝑅 += 𝑃
        end
        𝑃 += 𝑃
        λ >>= 1
    end
    return 𝑅
end

"""
Serialize an S256Point() to compressed SEC format, uncompressed if false is set
as second argument.

'point2sec(P::T, compressed::Bool=true) where {T<:S256Point} -> Array{UInt8,1}'
"""
function point2sec(P::T, compressed::Bool=true) where {T<:S256Point}
    xbin = int2bytes(P.𝑥.𝑛)
    if compressed
        if mod(P.𝑦.𝑛, 2) == 0
            prefix = 0x02
        else
            prefix = 0x03
        end
        return cat(prefix,xbin;dims=1)
    else
        return cat(0x04,xbin,int2bytes(P.𝑦.𝑛);dims=1)
    end
end

"""
Parse a SEC binary to an S256Point()

sec2point(sec_bin::AbstractArray{UInt8}) -> S256Point
"""
function sec2point(sec_bin::AbstractArray{UInt8})
    if sec_bin[1] == 4
        𝑥 = bytes2int(sec_bin[2:33])
        𝑦 = bytes2int(sec_bin[34:65])
        return S256Point(𝑥, 𝑦)
    end
    is_even = sec_bin[1] == 2
    𝑥 = ECC.S256Element(bytes2int(sec_bin[2:end]))
    α = 𝑥^3 + ECC.S256Element(ECC.B)
    β = sqrt(α)
    if mod(β.𝑛, 2) == 0
        evenβ = β
        oddβ = S256Element(ECC.P - β.𝑛)
    else
        evenβ = S256Element(ECC.P - β.𝑛)
        oddβ = β
    end
    if is_even
        return S256Point(𝑥, evenβ)
    else
        return S256Point(𝑥, oddβ)
    end
end

"""
Returns true if Signature is valid for 𝑧 given 𝑃, false if not

verify(𝑃::AbstractPoint, 𝑧::Integer, sig::Signature) -> Bool
"""
function verify(𝑃::AbstractPoint,𝑧::Integer,sig::Signature)
    𝑠⁻¹ = powermod(sig.𝑠, N - 2, N)
    𝑢 = mod(𝑧 * 𝑠⁻¹, N)
    𝑣 = mod(sig.𝑟 * 𝑠⁻¹, N)
    𝑅 = 𝑢 * G + 𝑣 * 𝑃
    return 𝑅.𝑥.𝑛 == sig.𝑟
end

# scep256k1 generator point
G = S256Point(big"0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
              big"0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")