#The m-by-n Hilbert matrix has matrix elements
# H_{ij} = 1/(i+j-1)
export Hilbert, InverseHilbert

struct Hilbert{T} <: AbstractMatrix{T}
    m :: Int
    n :: Int
end
Hilbert(T::Type, m::Integer, n::Integer) = Hilbert{T}(m, n)
Hilbert(T::Type{<:Integer}, m::Integer, n::Integer) = Hilbert(Rational{T}, m, n)
Hilbert(m::Integer, n::Integer) = Hilbert(Int, m, n)
Hilbert(n::Integer) = Hilbert(n, n)

# Define its size
size(H::Hilbert, dim::Integer) = dim==1 ? H.m : dim==2 ? H.n : 1
size(H::Hilbert)= size(H,1), size(H,2)

# Index into a Hilbert matrix
function getindex(H::Hilbert{T}, i::Integer, j::Integer) where {T}
    return one(T)/(i+j-1)
end

# Dense version
Matrix(H::Hilbert) = [H[i,j] for i=1:size(H,1), j=1:size(H,2)]

# Some properties
ishermitian(H::Hilbert) = H.m==H.n
isposdef(H::Hilbert) = H.m==H.n
det(H::Hilbert) = inv(det(inv(H)))

# Inverse of a Hilbert matrix
struct InverseHilbert{T} <: AbstractMatrix{T}
    n :: Int
end
InverseHilbert(T::Type, n::Integer) = InverseHilbert{T}(n)
InverseHilbert(n::Integer) = InverseHilbert(Int, n)

# Define its size
size(A::InverseHilbert, dim::Integer) = dim==1 || dim==2 ? A.n : 1
size(A::InverseHilbert) = size(A,1), size(A,2)

# Index into a inverse Hilbert matrix
function getindex(A::InverseHilbert{T}, i::Integer, j::Integer) where {T}
    return T((-1)^(i+j)*(i+j-1)*binomial(A.n+i-1,A.n-j)*
              binomial(A.n+j-1,A.n-i)*binomial(i+j-2,i-1)^2)
end
# Use bigger numbers in case of bigger types
function getindex(A::InverseHilbert{T}, i::Integer, j::Integer) where {T<:Union{BigInt,BigFloat}}
    N = big(A.n)
    return T((-1)^(i+j)*(i+j-1)*binomial(N+i-1,N-j)*
              binomial(N+j-1,N-i)*binomial(big(i+j-2),i-1)^2)
end

# Explicit formula for the determinant
det(A::InverseHilbert{T}) where {T} = prod(T,(2k+1)*binomial(2k,k)^2 for k=1:A.n-1)

# Dense version
Matrix(A::InverseHilbert) = [A[i,j] for i=1:size(A,1), j=1:size(A,2)]

# Define Base.inv
function inv(H::Hilbert{T}) where {T}
    H.m == H.n || throw(ArgumentError("Works only for square Hilbert matrices."))
    return InverseHilbert(T,H.n)
end
function inv(A::InverseHilbert{T}) where {T}
    HT = promote_type(T,typeof(Rational(one(T))))
    return Hilbert(HT,A.n)
end


function getIja(Ia::Matrix{T}, j::Int, N::Int) where T

    Id = [one(T) zero(T); zero(T) one(T)]

    Ias = collect( Id for i = 1:N )
    Ias[j] = Ia
    return Kronecker.kron(Ias...)
end

function productIaIb(Ixs, Iys, Izs, a::Int, b::Int)
    return Ixs[a]*Ixs[b] + Iys[a]*Iys[b] + Izs[a]*Izs[b]
end

"""
For operator A.

Example:
N = 3
Ixs = collect( getIja(Ix, i, N) for i = 1:N )
Iys = collect( getIja(Iy, i, N) for i = 1:N )
Izs = collect( getIja(Iz, i, N) for i = 1:N )

# sanity check.
out1 = productIaIb(Ixs, Iys, Izs, 1, 3)
out2 = productIjIk(Ixs, Iys, Izs, [1;], [3;])
norm(out1-out2)
"""
function productIjIk(Ixs, Iys, Izs, inds_j::Vector{Int}, inds_k::Vector{Int})

    #

    out = sum(Ixs[j] for j in inds_j) * sum(Ixs[k] for k in inds_k) +
        sum(Iys[j] for j in inds_j) * sum(Iys[k] for k in inds_k) +
        sum(Izs[j] for j in inds_j) * sum(Izs[k] for k in inds_k)

    return out
end

"""
for operator B.

Example:
N = 3
Ixs = collect( getIja(Ix, i, N) for i = 1:N )
Iys = collect( getIja(Iy, i, N) for i = 1:N )
Izs = collect( getIja(Iz, i, N) for i = 1:N )

# # sanity check.
# out1 = productIaIb(Ixs, Iys, Izs, 1, 3)
# out2 = productIjIk(Ixs, Iys, Izs, [1;], [3;])

out1 = pairwiseproductIa(Ixs, Iys, Izs, [1; 2; 3])
out2 = productIaIb(Ixs, Iys, Izs, 1, 3) + productIaIb(Ixs, Iys, Izs, 2, 3) + productIaIb(Ixs, Iys, Izs, 1, 2)
norm(out1-out2)
"""
function pairwiseproductIa(Ixs, Iys, Izs, inds::Vector{Int})

    out = zeros(Float64, size(Ixs[1]))
    for i = 1:length(inds)

        ind1 = inds[i]
        for j = i+1:length(inds)
            ind2 = inds[j]

            out += productIaIb(Ixs, Iys, Izs, ind1, ind2)
        end
    end

    return out
end


function LC(p, var)
    d = maxdegree(p, var)
    d == 0 && return zero(p)
    return polynomial([subs(t, var => 1) for t in terms(p) if degree(t, var) == d])
end

function pseudorem(f, g, y)
    r = f
    lc = LC(g, y)
    m = maxdegree(g, y)
    d = maxdegree(r, y)

    while d ≥ m && !iszero(r)
        r = lc * r - LC(r, y) * g * y ^ (d - m)
        d = maxdegree(r, y)
    end

    return r
end

function triangulize(H, vars)
    n = length(H)

    T = similar(H)
    @inbounds for i = n:-1:1
        degrees = Int[] # degrees of polynomials in C
        indeces = Int[] # indeces in H of polynomials in C

        # initialize
        @inbounds for (idx, p) in enumerate(H)
            deg = maxdegree(p, vars[i])
            if deg > 0
                push!(degrees, deg)
                push!(indeces, idx)
            end
        end

        # If there are several polynomials in C but none has degree 1 in x, transform the
        # set until there is a polynomial of degree 1 or only one polynomial is left
        C = H[indeces]
        while minimum(degrees) > 1 && length(C) > 1
            _, idx_max = findmax(degrees)
            _, idx_min = findmin(degrees)

            # special case when all  have same degree
            if idx_max == idx_min
                idx_min = 1
                idx_max = 2
            end

            r = pseudorem(C[idx_max], C[idx_min], vars[i])
            d = maxdegree(r, vars[i])
            if iszero(d)
                H[indeces[idx_max]] = r
                deleteat!(C, idx_max)
                deleteat!(degrees, idx_max)
                deleteat!(indeces, idx_max)
            else
                C[idx_max] = r
                degrees[idx_max] = d
            end
        end

        # if only one polynomial in C, add to triangular set and go to the next variable
        if length(indeces) == 1
            T[i] = H[indeces[1]]
            deleteat!(H, indeces[1])
            continue
        end

        # if multiple polynomials but at least one has degree 1, add this to triangular set
        # and replace the others in H with the pseudoremainder
        idx = findlast(==(1), degrees)
        if !isnothing(idx)
            pivot = H[indeces[idx]]
            T[i] = pivot
            #pushfirst!(T, pivot)

            @inbounds for j in indeces
                if j != indeces[idx]
                    H[j] = pseudorem(H[j], pivot, vars[i])
                end
            end
            deleteat!(H, indeces[idx])
            continue
        end

    end
    return T
end


# # Even reduction

#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/generated/Symmetry/even_reduction.ipynb)
#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/generated/Symmetry/even_reduction.ipynb)

using Test #src
using DynamicPolynomials
@polyvar x

# We would like to find the minimum value of the following polynomial:

poly = x^4 - 2x^2

using SumOfSquares

# We define the custom action as follows:

struct OnSign <: Symmetry.OnMonomials end
using PermutationGroups
import SymbolicWedderburn
SymbolicWedderburn.coeff_type(::OnSign) = Float64
function SymbolicWedderburn.action(::OnSign, p::Permutation, mono::AbstractMonomial)
    if isone(p) || iseven(DynamicPolynomials.degree(mono))
        return 1 * mono
    else
        @assert p.perm == perm"(1,2)"
        return -1 * mono
    end
end
G = PermGroup([perm"(1,2)"])

# We can exploit the symmetry as follows:

import CSDP
solver = CSDP.Optimizer
model = Model(solver)
@variable(model, t)
@objective(model, Max, t)
pattern = Symmetry.Pattern(G, OnSign())
con_ref = @constraint(model, poly - t in SOSCone(), symmetry = pattern)
optimize!(model)
@test value(t) ≈ -1 #src
value(t)

# We indeed find `-1`, let's verify that symmetry was exploited:

@test length(gram_matrix(con_ref).sub_gram_matrices) == 2 #src
@test gram_matrix(con_ref).sub_gram_matrices[1].basis.polynomials == [x^2, 1] #src
@test gram_matrix(con_ref).sub_gram_matrices[2].basis.polynomials == [x] #src
gram_matrix(con_ref)


function binary(n::Int)
	ans=Int[]
	while n>=1

		push!(ans,n%2)
		n=floor(Int,n/2)

	end

	return reverse(ans)
end

function is_decimal_binary_palindrome(n::Int)
	s=string(n)
	if s!=reverse(s)
		return false
	end

	bin=binary(n)

	return bin==reverse(bin)
end

function compute()

	ans= sum(i for i=1:1000000 if is_decimal_binary_palindrome(i))

	return ans
end

println(compute())


################################################
## Specialized routines for tridiagonal matrices
################################################

export numnegevals

"""
Computes the number of negative eigenvalues of T - σI, a.k.a. spectrum slicing

Inputs:
    T: A SymTridiagonal{<:Real} matrix
    σ: The shift parameter

Outputs:
    ν: The number of negative eigenvalues

Reference:
    B. N. Parlett, "The symmetric eigenvalue problem", Section 3.3.1, p. 52.
"""
function numnegevals(T::SymTridiagonal{S}, σ::S = zero(S)) where S
    α = T.dv
    β = T.ev
    n = length(α)
    ϵ = eps(S)
    δ = α[1] - σ
    ν = δ < 0 ? 1 : 0
    for k = 1:n - 1
        if δ == 0
        info("zero in iteration $k")
        δ = ϵ * (β[k]+ϵ) #Parlett prefers adjusting σ and starting again
    end
        δ = (α[k + 1] - σ) - β[k]*(β[k]/δ)
    ν += (δ < 0)
    end
    ν
end


using JuMP
using Cbc

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

function solve_model(solver, model_log, solver_options=Dict())
    model = Model(solver)

    for (k, v) in solver_options
        set_optimizer_attribute(model, k, v)
    end

    n_variables = 7
    n_constraints = 4

    @variable(model, 0 <= x[1:6] <= 10, Int)
    @variable(model, 0 <= y <= 1, Int)

    ocoeffs = [60 70 40 70 20 90]
    ccoeffs = [30 20 30 90 30 70]

    @constraint(model, sum((x[i]*ccoeffs[i] for i in 1:6)) <= 2000)
    @constraint(model, x[3] <= 10*x[4])
    @constraint(model, x[1] + x[2] >= 4*y)
    @constraint(model, x[5] + x[6] >= 4*(1 - y))

    constraints_f = []
    push!(constraints_f, (v) -> sum((v[i]*ccoeffs[i] for i in 1:6)) <= 2000)
    push!(constraints_f, (v) -> v[3] <= 10*v[4])
    push!(constraints_f, (v) -> v[1] + v[2] >= 4*v[7])
    push!(constraints_f, (v) -> v[5] + v[6] >= 4*(1 - v[7]))

    @objective(model, Max, sum(x[i]*ocoeffs[i] for i in 1:6))
    obj_f(v) = sum(v[i]*ocoeffs[i] for i in 1:6)

    starting_point = zeros(Float64, n_variables)
    solution_point = zeros(Float64, n_variables)

    ###############################################################
    #save model textual description
    open(model_log, "w") do f
        print(f, model)

        ###############################################################
        #solve model
        elapsed_time = @elapsed optimize!(model)

        println(f, "-----------------------------------------------")
        println(f, "Elapsed time: $elapsed_time")
        println(f, "-----------------------------------------------")
        println(f, "Status:    $(termination_status(model))")
        println(f, "Objective: $(objective_value(model))")
        println(f, "-----------------------------------------------")
        println(f, "Variables:")

        for i in 1:(n_variables-1)
            println(f, "$(x[i]): $(value(x[i]))")
            solution_point[i] = value(x[i])
        end

        println(f, "$(y): $(value(y))")
        solution_point[n_variables] = value(y)
    end

    variable_names = vcat(["x[$(string(i))]" for i in 1:(n_variables-1)], ["y"])
    constraint_names = ["c$(string(c))" for c in 1:n_constraints]

    obj_val = objective_value(model)

    return obj_f, constraints_f, variable_names, constraint_names, starting_point, solution_point, obj_val
end

#The datatype used in the tuples is used to determine if the variable is "real" or integer
variable_domains = [(0, 12), (0, 12), (0, 12), (0, 12), (0, 12), (0, 12), (0, 1)]

optimizers = [Cbc.Optimizer]
opt_names = ["cbc"]

problems = Dict()

for (opt, opt_name) in zip(optimizers, opt_names)
    objective_f, constraints_f, variable_names, constraint_names, starting_point, solution_point, obj_val = solve_model(opt, "knapsack_$(opt_name).log")
    problems[opt_name] = (objective_f, constraints_f, variable_names, constraint_names, starting_point, solution_point, obj_val)
end

#All functions are equal, get first to initialize OptMap
objective_f, constraints_f, variable_names, constraint_names, _, _, obj_val = problems[opt_names[1]]
O = OptMap(variable_names, variable_domains, objective_f; constraint_names=constraint_names, constraints_f=constraints_f, verbose=true)
create_map!(O)

for opt_name in opt_names
    _, _, _, _, starting_point, solution_point, _ = problems[opt_name]

    path_to_solution = zeros(Float64, length(variable_domains), 2)
    path_to_solution[:,1] = starting_point
    path_to_solution[:,2] = solution_point

    draw_solution!(O, path_to_solution)
    save_all(O, "knapsack_$(opt_name)", ".")
end


function eval_metric(::Val{:mse}, p::AbstractMatrix{T}, y::AbstractVector{T}, α=0.0) where {T <: AbstractFloat}
    eval = zero(T)
    @inbounds for i in eachindex(y)
        eval += (p[1,i] - y[i])^2
    end
    eval /= length(y)
    return eval
end

function eval_metric(::Val{:rmse}, p::AbstractMatrix{T}, y::AbstractVector{T}, α=0.0) where {T <: AbstractFloat}
    eval = zero(T)
    @inbounds for i in eachindex(y)
        eval += (p[1,i] - y[i])^2
    end
    eval = sqrt(eval / length(y))
    return eval
end

function eval_metric(::Val{:mae}, p::AbstractMatrix{T}, y::AbstractVector{T}, α=0.0) where {T <: AbstractFloat}
    eval = zero(T)
    @inbounds for i in eachindex(y)
        eval += abs(p[1,i] - y[i])
    end
    eval /= length(y)
    return eval
end

function eval_metric(::Val{:logloss}, p::AbstractMatrix{T}, y::AbstractVector{T}, α=0.0) where {T <: AbstractFloat}
    eval = zero(T)
    @inbounds for i in eachindex(y)
        pred = sigmoid(p[1,i])
        eval -= y[i] * log(pred) + (1 - y[i]) * log(1 - pred)
    end
    eval /= length(y)
    return eval
end

function eval_metric(::Val{:mlogloss}, p::AbstractMatrix{T}, y::AbstractVector{S}, α=0.0) where {T <: AbstractFloat,S <: Integer}
    eval = zero(T)
    p_prob = exp.(p) ./ sum(exp.(p), dims=1)
    @inbounds for i in eachindex(y)
        eval -= log(p_prob[y[i], i])
    end
    eval /= length(y)
    return eval
end

function eval_metric(::Val{:poisson}, p::AbstractMatrix{T}, y::AbstractVector{T}, α=0.0) where {T <: AbstractFloat}
    eval = zero(T)
    @inbounds for i in eachindex(y)
        eval += exp(p[1,i]) * (1 - y[i]) + log(factorial(y[i]))
    end
    eval /= length(y)
    return eval
end

function eval_metric(::Val{:gaussian}, p::AbstractMatrix{T}, y::AbstractVector{T}, α=0.0) where {T <: AbstractFloat}
    eval = zero(T)
    @inbounds for i in eachindex(y)
        eval += p[2,i] + (y[i] - p[1,i])^2 / (2 * exp(2 * p[2,i]))
    end
    eval /= length(y)
    return eval
end

function eval_metric(::Val{:quantile}, p::AbstractMatrix{T}, y::AbstractVector{T}, α=0.0) where {T <: AbstractFloat}
    eval = zero(T)
    for i in eachindex(y)
        eval += α * max(y[i] - p[1,i], zero(T)) + (1 - α) * max(p[1,i] - y[i], zero(T))
    end
    eval /= length(y)
    return eval
end

function gini_raw(y::T, p::S) where {T,S}
    if length(y) < 2
        return 0.0
    end
    random = (1:length(p)) ./ length(p)
    l_sort = y[sortperm(p)]
    l_cum_w = cumsum(l_sort) ./ sum(y)
    gini = sum(l_cum_w .- random)
    return gini
end

function gini_norm(y::T, p::S) where {T,S}
    if length(y) < 2
        return 0.0
    end
    return gini_raw(y, p) / gini_raw(y, y)
end

function eval_metric(::Val{:gini}, p::AbstractMatrix{T}, y::AbstractVector{T}, α=0.0) where {T <: AbstractFloat}
    return -gini_norm(y, view(p, 1, :))
end


using Printf
K = 0; N = 10000000;
@time begin
for i in 1:N
  if (rand()^2 + rand()^2) < 1.0
    global K += 1
  end
end
end
p = (4*K)/N
@printf "Estimation of PI is %.6f\n" p


# # Symmetry reduction

#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/generated/Symmetry/symmetry_reduction.ipynb)
#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/generated/Symmetry/symmetry_reduction.ipynb)
# **Adapted from**: [SymbolicWedderburn example](https://github.com/kalmarek/SymbolicWedderburn.jl/blob/tw/ex_sos/examples/ex_C4.jl)

import MutableArithmetics
const MA = MutableArithmetics
using MultivariatePolynomials
const MP = MultivariatePolynomials
using MultivariateBases
const MB = MultivariateBases

using Test #src
using DynamicPolynomials
@polyvar x[1:4]

# We would like to find the minimum value of the polynomial

poly = sum(x) + sum(x.^2)

# As we can decouple the problem for each `x[i]` for which `x[i] + x[i]^2` has
# minimum value 0.25, we would expect to get `-1` as answer.
# Can this decoupling be exploited by SumOfSquares as well ?
# For this, we need to use a certificate that can exploit the permutation symmetry of the polynomial.

using SumOfSquares

# We define the symmetry group as a permutation group in the variables.
# In order to do that, we define the action of a permutation on a monomial
# as the monomial obtained after permuting the variables.

using PermutationGroups
G = PermGroup([perm"(1,2,3,4)"])

# We can use this certificate as follows:

import CSDP
solver = CSDP.Optimizer
model = Model(solver)
@variable(model, t)
@objective(model, Max, t)
pattern = Symmetry.Pattern(G, Symmetry.VariablePermutation())
con_ref = @constraint(model, poly - t in SOSCone(), symmetry = pattern)
optimize!(model)
@test value(t) ≈ -1 rtol=1e-6 #src
value(t)

# We indeed find `-1`, let's verify that symmetry was exploited:

g = gram_matrix(con_ref).sub_gram_matrices    #src
@test length(g) == 4                          #src
@test length(g[1].basis.polynomials) == 2     #src
@test g[1].basis.polynomials[1] == 1.0        #src
@test g[1].basis.polynomials[2] ≈ -0.5 * sum(x) #src
@test size(g[1].Q) == (2, 2)                  #src
@test g[1].Q[1, 1] ≈ 1.0 atol=1e-6            #src
@test g[1].Q[1, 2] ≈ -1.0 atol=1e-6            #src
@test g[1].Q[2, 2] ≈ 1.0 atol=1e-6            #src
@test length(g[2].basis.polynomials) == 1     #src
@test g[2].basis.polynomials[1] ≈ (x[3] - x[1]) / √2 #src
@test size(g[2].Q) == (1, 1)                  #src
@test g[2].Q[1, 1] ≈ 1.0 atol=1e-6            #src
@test length(g[3].basis.polynomials) == 1     #src
@test g[3].basis.polynomials[1] ≈ (x[4] - x[2]) / √2 #src
@test size(g[3].Q) == (1, 1)                  #src
@test g[3].Q[1, 1] ≈ 1.0 atol=1e-6            #src
@test length(g[4].basis.polynomials) == 1     #src
@test g[4].basis.polynomials[1] ≈ (-x[1] + x[2] - x[3] + x[4]) / 2 #src
@test size(g[4].Q) == (1, 1)                  #src
@test g[4].Q[1, 1] ≈ 1.0 atol=1e-6            #src
for g in gram_matrix(con_ref).sub_gram_matrices
    println(g.basis.polynomials)
end


# Single run of model with 2 parameters
using Turing, Statistics, Distributions, Random, LinearAlgebra, StatsPlots
using AdvancedMH

# the data generating process
function dgp(θ, n)
    [rand.(Exponential(θ[1]),n) rand.(Poisson(θ[2]),n)]
end

# summary statistics for estimation
function moments(y)
    n = size(y,1)
    sqrt(n) .* [mean(y, dims=1)[:]; std(y, dims=1)[:]]
end

@model function abc(z, S, n, calibrated)
    # create the prior: the product of the following array of marginal priors
    θ  ~ arraydist([LogNormal(1.,1.); LogNormal(1.,1.)])
    # sample from the model, at the trial parameter value, and compute statistics
    y = zeros(n,2)
    zs = zeros(S, size(z,1))
    @inbounds for i = 1:S
        y .= dgp(θ, n)
        zs[i,:] .= moments(y) # simulated summary statistics
    end
    # the asymptotic Gaussian distribution of the statistics
    m = mean(zs, dims=1)[:]
    calibrated ? Σ = Symmetric((1. + 1/S)*cov(zs)) : Σ = I
    z ~ MvNormal(m, Σ)
end;

function main(calibrated=true)
    θ⁰ = [2.; 3.] # true parameters
    n = 100 # sample size
    S = 100 # number of simulation draws
    # get data and statistics
    y = dgp(θ⁰, n)
    z = moments(y)
    # sample chains
    length = 5000
    burnin = 500
    chain = sample(abc(z, S, n, calibrated),
        MH(:θ => AdvancedMH.RandomWalkProposal(MvNormal(zeros(2), 0.25*I))),
        MCMCThreads(), length+burnin, 4)
    chain = chain[burnin+1:end,:,:]
    @show display(chain)
    p = plot(chain)
    chain = Array(chain)
    q1 = quantile(chain[:,1], [0.025, 0.975])
    q2 = quantile(chain[:,2], [0.025, 0.975])
    println()
    println("is first true parameter inside 95% CI?")
    display(q1[1] <= θ⁰[1] && q1[2] >= θ⁰[1])
    println()
    println("is second true parameter inside 95% CI?")
    display(q2[1] <= θ⁰[2] && q2[2] >= θ⁰[2])
    p
end
println("first a well-calibrated run")
p1 = main(true)
println("now a poorly calibrated run")
p2 = main(false)
plot(p1, p2)


"""
    wdist(d; h = 2, cri = 4, squared = false)
Compute weights from distances, using a decreasing exponential function.
* `d` : A vector of distances.
* `h` : A scaling positive scalar defining the shape of the function.
* `cri` : A positive scalar defining outliers in the distances vector.
* `squared`: If true, distances are replaced by the squared distances;
    the weight function is then a Gaussian (RBF) kernel function.

Weights are computed by exp(-d / (h * MAD(d))), or are set to 0 for
distances > Median(d) + cri * MAD(d).

The weights decrease with increasing distances. Lower is h, sharper is the decreasing function.
Weights are set to 0 for outliers (extreme distances).
"""
function wdist(d; h = 2, cri = 4, squared = false)
    w = copy(d)
    wdist!(w; h = h, cri = cri, squared = squared)
    w
end

function wdist!(d; h = 2, cri = 4, squared = false)
    # d, out : (n,)
    squared ? d = d.^2 : nothing
    zmed =  Statistics.median(d)
    zmad = Jchemo.mad(d)
    cutoff = zmed + cri * zmad
    d .= map(x -> ifelse(x <= cutoff, exp(-x / (h * zmad)), zero(eltype(d))), d)
    d .= d / maximum(d)
    d[isnan.(d)] .= 1
    return
end


using Colors
using Luxor

struct Pos
    x::Float64
    y::Float64
end

function jarvis_cross(point1::Pos, point2::Pos, point3::Pos)
    vec1 = Pos(point2.x - point1.x, point2.y - point1.y)
    vec2 = Pos(point3.x - point2.x, point3.y - point2.y)
    ret_cross = vec1.x*vec2.y - vec1.y*vec2.x
    return ret_cross*ret_cross
end

function jarvis_march(points::Vector{Pos})
    hull = Vector{Pos}()

    # sorting array based on leftmost point
    sort!(points, by = item -> item.x)
    push!(hull, points[1])

    i = 1
    curr_point = points[2]

    # Find cross product between points
    curr_product = jarvis_cross(Pos(0,0), hull[1], curr_point)
    while (curr_point != hull[1])
        for point in points
                product = 0.0
            if (i == 1)
                if (hull[i] != point)
                    product = jarvis_cross(Pos(0,0), hull[i], point)
                end
            else
                if (hull[i] != point && hull[i-1] != point)
                    product = jarvis_cross(hull[i-1], hull[i], point)
                end
            end
            if (product > curr_product)
                curr_point = point
                curr_product = product
            end
        end
        push!(hull, curr_point)
        curr_product = 0
        i += 1
    end

    return hull
end

function main(pts)
    hull = jarvis_march(pts)
end

# Auxiliary drawing functions
function make_drawing(width, height, img_path, bkg_color, origin_p)
    d = Drawing(width, height, img_path)
    background(bkg_color)
    origin(origin_p)
    return d
end

map2luxor(p) = Point(p.x, -p.y)
map2luxor(p) = Point(p...)

## Canvas constants
width = 500
height = 500
path = "arcane_march.png"
color = RGBA(0, 0, 0)
op = [width / 2, height / 2]

# Setting up drawing
my_draw = make_drawing(width, height, path, color, Point(op...))

# pts_list = [Pos(200, 15), Pos(100, 100), Pos(-20, -200), Pos(-130, 100)]
pts_list = [Pos(rand(-250:250), rand(-250:250)) for _ in 1:10]

hull = main(pts_list)
println(hull)
hull = [Point(p.x, -p.y) for p in hull]
println(hull)
pts_list = [Point(p.x, -p.y) for p in pts_list]

sethue(RGBA(1.0, 1.0, 0.0))
circle.(pts_list, 15; action = :fill)

sethue(RGBA(1.0, 1.0, 1.0))
poly(hull, action = :stroke; close = true)

finish()


using LinearAlgebra
using GLM
using LsqFit
using Distributions
using StatsBase
using Printf
using Plots

#linear fit wrapper

function lfit(ndf::DataFrame)
    lr = lm(@formula(y_mean ~ x_mean), ndf)
    c = coef(lr)
    return x -> c[1] + c[2]*x, predict(lr), c
end

struct RFit
	fitpar::Vector{<:Number}
	fitstd::Vector{<:Number}
	ci::Vector{Tuple{Number, Number}}
	g::Function
end

"""
    gpol1(ct::Vector{<:Real})::Function
return a degree 1 polynomial function with parameters ct.
"""
function gpol1(ct::Vector{<:Real})::Function
    function f1(z::Real)
        return ct[1] + ct[2] * z
    end
    return f1
end

"""
    gpol2(ct::Vector{<:Real})::Function
return a degree 2 polynomial function with parameters ct.
"""
function gpol2(ct::Vector{<:Real})::Function
    function f2(z::Real)
        return ct[1] + ct[2] * z + ct[3] * z^2
    end
    return f2
end

"""
    gpol2(ct::Vector{<:Real})::Function
return a degree 3 polynomial function with parameters ct.
"""
function gpol3(ct::Vector{<:Real})::Function
    function f3(z::Real)
        return ct[1] + ct[2] * z + ct[3] * z^2 + ct[4] * z^3
    end
    return f3
end

"""
    func1dfit(ffit::Function, x::Vector{<:Real},
              y::Vector{<:Real}, p0::Vector{<:Real},
              lb::Vector{Float64}, ub::Vector{Float64})

Fit a function to the data x, y with start prediction p0
and return coefficients and errors.
"""
function func1dfit(ffit::Function, x::Vector{<:Real},
                   y::Vector{<:Real}, p0::Vector{<:Real},
                   lb::Vector{Float64}, ub::Vector{Float64})
    fq = curve_fit(ffit, x, y, p0, lower=lb, upper=ub)
    cfq = coef(fq)
    @info "coef(fq)" cfq
    sfq = stderror(fq)
    @info "std(fq)" sfq
    @info "margin_of_error (90%)" margin_error(fq, 0.1)
    @info " confidence_interval (90%)" confidence_interval(fq, 0.1)
    return cfq, sfq
end

"""
    func1dfit(ffit::Function, x::Vector{<:Real}, y::Vector{<:Real},
              yerr::Vector{<:Real}, p0::Vector{<:Real},
              lb::Vector{<:Real}, ub::Vector{<:Real})
Fit function ffit to data x,y taking into account the errors
on the y (yerr). Assumes weights of 1/sigma^2 as in standard least squares
and returns fit result.
"""
function func1dfit(ffit::Function, x::Vector{<:Real}, y::Vector{<:Real},
	               yerr::Vector{<:Real}, p0::Vector{<:Real},
                   lb::Vector{<:Real}, ub::Vector{<:Real})
	fq = curve_fit(ffit, x, y, yerr.^-2, p0, lower=lb, upper=ub)
    return fq
end

"""
    fit_pol1(x::Vector{<:Real}, y::Vector{<:Real}, ci=0.1)
Fit a 1st degree polynomial to the data x, y and return
a RFit object with the result.
"""
function fit_pol1(x::Vector{<:Real}, y::Vector{<:Real}, ci=0.1)
    @. pol(x, p) = p[1] + p[2] * x
    p0 = [1.0, 1.0]
    fq = curve_fit(pol, x, y, p0)

    RFit(coef(fq), stderror(fq), confidence_interval(fq, ci),
         gpol1(coef(fq)))
end

"""
    fit_pol2(x::Vector{<:Real}, y::Vector{<:Real}, ci=0.1)
Fit a 2nd degree polynomial to the data x, y and return
a RFit object with the result.
"""
function fit_pol2(x::Vector{<:Real}, y::Vector{<:Real}, ci=0.1)
    @. pol(x, p) = p[1] + p[2] * x + p[3] * x^2
    p0 = [1.0, 1.0, 1.0]
    fq = curve_fit(pol, x, y, p0)
    RFit(coef(fq), stderror(fq), confidence_interval(fq, ci),
         gpol2(coef(fq)))
end

"""
    fit_pol3(x::Vector{<:Real}, y::Vector{<:Real}, ci=0.1)
Fit a 3rd degree polynomial to the data x, y and return
a RFit object with the result.
"""
function fit_pol3(x::Vector{<:Real}, y::Vector{<:Real}, ci=0.1)
    @. pol(x, p) = p[1] + p[2] * x + p[3] * x^2 + p[4] * x^3
    p0 = [1.0, 1.0, 1.0, 1.0]
    fq = curve_fit(pol, x, y, p0)
    RFit(coef(fq), stderror(fq), confidence_interval(fq, ci),
         gpol3(coef(fq)))
end

struct FGauss
	mu ::Vector{<:Real}
	std::Vector{<:Real}
	C  ::Vector{<:Real}
	h  ::Histogram
	X  ::Vector{<:Real}
	Y  ::Vector{<:Real}
	g  ::Vector{Function}
end

"""
    gausg(μ::Real, σ::Real, C::Real)::Function
Return a Gaussian function with parameters μ, σ and normalisation C.
"""
function gausg(μ::Real, σ::Real, C::Real)::Function
	function gausx(x::Real)
		return C * pdf(Normal(μ, σ,), x)
	end
	return gausx
end

"""
    gausg2(μ1::Real, σ1::Real, C1::Real, μ2::Real, σ2::Real, C2::Real)
Return a function for the sum of two Gaussians with parameters
    μ1, σ1 and μ2, σ2 and normalisations C1 and C2.
"""
function gausg2(μ1::Real, σ1::Real, C1::Real, μ2::Real, σ2::Real, C2::Real)::Function
	function gausx(x::Real)
		return C1 * pdf(Normal(μ1, σ1,), x) + C2 * pdf(Normal(μ2, σ2,), x)
	end
	return gausx
end

@. gauss1(x, p)   = p[1] * pdf(Normal(p[2], p[3]), x)
@. gauss2(x, p)   = p[1] * pdf(Normal(p[2], p[3]), x) + p[4] * pdf(Normal(p[5], p[6]), x)
@. gauss2cm(x, p) = p[1] * pdf(Normal(p[2], p[3]), x) + p[4] * pdf(Normal(p[2], p[5]), x)

"""
    gaussfm(mu::Real)::Function
Return a Gaussian Fitting function with a fixed mean mu.
"""
function gaussfm(mu::Real)::Function
	function gauss(x::Vector{<:Real}, p::Vector{<:Real})
		return @. p[1]* pdf(Normal(mu, p[2]), x)
	end
	return gauss
end

"""
    gauss2fm(mu::Real)::Function
Return a function of the sum of two Gaussians with a fixed common mean mu.
"""
function gauss2fm(mu::Real)::Function
	function gauss2(x::Vector{<:Real}, p::Vector{<:Real})
		return @. p[1]* pdf(Normal(mu, p[2]), x) + p[3]* pdf(Normal(mu, p[4]), x)
	end
	return gauss2
end

"""
	fit_gauss(h::Histogram)

Fit a normal distribution to a Histogram and return FGauss object.
"""
function fit_gauss(h::Histogram)
	c = centers(h)
	w = eltype(c).(h.weights)
	@debug "histo"  w c
	mu, sigma = mean_and_std(c, Weights(h.weights); corrected = false)
	@debug "mu, std" mu, sigma

	# fit parameters lb, ub, po are lower, upper bounds and pars
    lb = [         0.0, mu - 100.0 * sigma, sigma / 100.0]
    ub = [100 * sum(w), mu + 100.0 * sigma, 100.0 * sigma]
    p0 = [      sum(w), mu                ,         sigma]
	(CC, μ, σ), _ = func1dfit(gauss1, c, w, p0, lb, ub)
	gx = gausg(μ, σ, CC)
	return FGauss([μ], [σ], [CC], h, c, gx.(c), [gx])

end

"""
	fit_gauss(x::Vector{Float64}, xmin::Float64, xmax::Float64;
              bins::Integer=50, norm=false)

Fit a normal distribution to data x,y and return FGauss object.
"""
fit_gauss(x::Vector{Float64}, xmin::Float64, xmax::Float64;
	      bins::Integer=50, norm=false) = fit_gauss(hist1d(x, bins, xmin, xmax, norm))

"""
	fit_gauss_fm(x::Vector{Float64}, xmin::Float64, xmax::Float64, bins=50, fm=0.0)

Fit a gaussian with a fixed mean fm and return an FGauss object.
"""
function fit_gauss_fm(x::Vector{Float64}, xmin::Float64, xmax::Float64;
	                  bins=50, norm=false, fm=0.0)

	# fit the unbinned distribution
	xx =  in_range(x, xmin, xmax)
	σ  = std(xx)
	@debug "gfit_gauss_fm: σ = $σ"

	# bin distribution
    h = hist1d(xx, bins, xmin, xmax, norm)
    c = centers(h)
    w = eltype(c).(h.weights)
    @debug "histo w and c"  w c

	# fit parameters lb, ub, po are lower, upper bounds and pars
    lb = [            0.0, σ / 10.0]
    ub = [1.0e+6 * sum(w), σ * 10.0]
    p0 = [         sum(w), σ       ]

	g1 = gaussfm(fm)
	(CC, sigma), _ = func1dfit(g1, c, w, p0, lb, ub)

	@debug "CC,  sigma"  CC  sigma
	gx = gausg(fm, sigma, CC)

	return FGauss([fm], [sigma], [CC], h, c, gx.(c), [gx])
end

"""
    plot_fit_gauss(x::Vector{Float64}, xs::String, ys::String,
                   bins::Integer, xmin::Float64, xmax::Float64;
                   xgmin::Float64, xgmax::Float64, gbins::Integer=50)
Fit a Gaussian to the histogram of data x between xmin and xmax with bins
and plot the results.
"""
function plot_fit_gauss(x::Vector{Float64}, xs::String, ys::String,
                        bins::Integer, xmin::Float64, xmax::Float64;
                        xgmin::Float64, xgmax::Float64, gbins::Integer=50)

    h, _ = hist1d(x, xs, bins, xmin, xmax, norm=true)
    fg   = fit_gauss(x, xgmin, xgmax, bins=gbins, norm=true)
    gx   = fg.g[1]
    X    = centers(h)
    Y    = h.weights
    σY   = sqrt.(Y)
    lbl  = @sprintf " μ=%5.1f, σ =%5.1f " fg.mu[1] fg.std[1]
    lbl  = string("gaussian fit:\n", lbl)
    p    = scatter(X, Y, yerror=σY,fmt=:png, shape=:circle, color=:black, label="data", legend=true)
    p    = plot!(p, X, gx.(X), lw=2, label=lbl, legend=true, fmt = :png)
    xlabel!(xs)
    xlabel!(ys)
    return fg, p
end

"""
    fitg1(x::Vector{<:Real}, xs::String, nbins::Int64, xmin::Real, xmax::Real;
	      xgmin::Real, xgmax::Real, fbins::Int64=100, norm::Bool=true,
          fm::Real=0.0, flex_mean=false)

Histograms data x and returns a plot and a Gaussian fit to the histogram.
Option flex_mean allows the mean to float or be fixed to fm.
"""
function fitg1(x::Vector{<:Real}, xs::String, nbins::Int64, xmin::Real, xmax::Real;
	           xgmin::Real, xgmax::Real, fbins::Int64=100, norm::Bool=true,
               fm::Real=0.0, flex_mean=false)

	h, p = hist1d(x, xs, nbins, xmin, xmax, norm=norm, legend=true)
    if flex_mean
        fg = fit_gauss(x, xgmin, xgmax, bins=fbins, norm=norm)
    else
        fg = fit_gauss_fm(x, xgmin, xgmax, bins=fbins, norm=norm, fm=fm)
    end
	gx  = fg.g[1]
	X   = centers(h)
	lbl = @sprintf "σ =%4.1f " fg.std[1]
    p   = plot!(p, X, gx.(X), lw=2, label=lbl, legend=true, fmt = :png)
	xlabel!(xs)
	return fg, p
end

"""
	gfit_gauss2_cmean(y, xmin, xmax, bins, sigmas, cs, cmean=0.0)

Fit a double gaussian (with sigmas -->[sigma1, sigma2] cs -->[c1, c2] )
and a fixed mean, cmean, to data.
"""
function gfit_gauss2_cmean(y::Vector{Float64}, xmin::Float64, xmax::Float64,
	                       bins::Integer, sigmas::Vector{Float64}, cs::Vector{Float64},
						   norm=false, cmean=0.0)

    x = in_range(y, xmin, xmax)
    h = hist1d(x, bins, xmin, xmax, norm)
    c = centers(h)
    w = h.weights
    @debug "histo centers and weights in full region"  w c

    g2 = gauss2fm(cmean)
    # fit parameters lb, ub, po are lower, upper bounds and pars

    lb = [cs[1]/100.0, sigmas[1]/5.0, cs[2]/100.0, sigmas[2]/5.0]
    ub = [cs[1]*100.0, sigmas[1]*5.0, cs[2]*100.0, sigmas[2]*5.0]
    p0_bounds = [cs[1], sigmas[1], cs[2], sigmas[2]]

    @debug "pars" p0 lb ub
    # fit double gaussian
    fq = curve_fit(g2, c, w, p0_bounds, lower=lb, upper=ub)
    C1, sigma1, C2,  sigma2   = coef(fq)
    @debug "C1 sigma1 C2 sigma2" C1 sigma1 C2  sigma2

    #
    gx  = gausg2(cmean, sigma1, C1, cmean, sigma2, C2)
    gx1 = gausg( cmean, sigma1, C1)
    gx2 = gausg( cmean, sigma2, C2)

	return FGauss([cmean, cmean], [sigma1, sigma2], [C1, C2],
			       h, c, gx.(c), [gx, gx1, gx2])
    end

"""
	fit_2gauss_cmean(data, gp, g1p, g2p, cm)

Fit two gaussian with common mean JJ Review!!!
"""
function fit_2gauss_cmean(data, gp, g1p, g2p, cm, norm=false)
    gf1 = fit_gauss_fm(data, g1p.xmin,g1p.xmax, bins=g1p.nbin, norm=norm, fm=cm)
    @debug gf1
    gf2 = fit_gauss_fm(data, g2p.xmin,g2p.xmax,bins=g2p.nbin, norm= norm, fm=cm)
    @debug gf2
    gf = gfit_gauss2_cmean(data, gp.xmin,gp.xmax,gp.nbin,
	                       [gf1.std[1], gf2.std[1]], [gf1.C[1], gf2.C[1]], norm)
    @debug gf
    return gf
end

"""
    fitg2(x, xs, xmin, xmax, xg1min, xg1max, xg2min, xg2max, xgmin, xgmax; bins=100)
Fits 2 gaussians with common mean (0 by default) to vector x. JJ Review!!
"""
function fitg2(x::Vector{<:Real}, xs::String, bins::Int64, xmin::Real, xmax::Real;
	           xg1min::Real, xg1max::Real, xg2min::Real, xg2max::Real, xgmin::Real, xgmax::Real, cm=0.0,
         	   g1bins::Int64=100, g2bins::Int64=100, gbins::Int64=100, norm::Bool=true)

	_, p = hist1d(x, xs, bins, xmin, xmax, norm=norm, legend=true)

    g1p = (xmin = xg1min, xmax = xg1max, nbin=g1bins)
    g2p = (xmin = xg2min, xmax = xg2max, nbin=g2bins)
    gp  = (xmin = xgmin , xmax = xgmax , nbin=gbins )

    fg  = fit_2gauss_cmean(x, gp, g1p, g2p, cm, norm)
	gx  = fg.g[1]
	gx1 = fg.g[2]
	gx2 = fg.g[3]

	lbl = @sprintf "σt =%4.1f mm, σ =%4.1f mm" fg.std[1] fg.std[2]
	st  = @sprintf "σt =%4.1f mm " fg.std[1]
	sf  = @sprintf "σ  =%4.1f mm" fg.std[2]
    p = plot!(p, fg.X, fg.Y, label=lbl, lw=2, fmt = :png)
    p = plot!(p, fg.X, gx1.(fg.X), label=st, lw=1, fmt = :png)
    p = plot!(p, fg.X, gx2.(fg.X), label=sf, lw=1, fmt = :png)
    #xlabel!(xs)
    return fg, p
end

"""
	fit_profile(x1, x2, tx1, ty1, fit="pol1", bins=25)
    fit_profile(df1, c1, c2, tx1, ty1, fit="pol1", bins=25)
Create and fit a profile with pol1 or poli2 functions.
Return fit parameters, fit function and plot
"""
fit_profile(df1::DataFrame, c1::String, c2::String,
            tx1::String, ty1::String, fit="pol1", bins=25;
            ybin_width::Real=0.1, ymin::Real=352.4, ymax::Real=392.4,
            min_proportion::Real=0.0) =
	fit_profile(df1[!,c1], df1[!,c2], tx1, ty1, fit, bins,
                ybin_width=ybin_width, ymin=ymin, ymax=ymax, min_proportion=min_proportion)

function fit_profile(x1::Vector{<:Real}, x2::Vector{<:Real},
	                 tx1::String, ty1::String, fit="pol1", bins=25;
                     ybin_width::Real=0.1, ymin::Real=minimum(x2),
                     ymax::Real=maximum(x2), min_proportion::Real=0.0)

    pdf1, _ = p1df(x1,x2, bins, ybin_width=ybin_width, ymin=ymin, ymax=ymax, min_proportion=min_proportion)

    if fit == "pol1"
        fr = fit_pol1(pdf1.x_mean, pdf1.y_mean)
    elseif fit == "pol2"
        fr = fit_pol2(pdf1.x_mean, pdf1.y_mean)
	elseif fit == "pol3"
		fr = fit_pol3(pdf1.x_mean, pdf1.y_mean)
	else
		println("option not implemented")
		return nothing
    end

    p1 = scatter(pdf1.x_mean, pdf1.y_mean, yerror=pdf1.y_std, fmt=:png,
	             shape=:circle, color=:black, legend=false)
    p1 = plot!(p1, pdf1.x_mean, fr.g.(pdf1.x_mean), fmt=:png)
    xlabel!(tx1)
    ylabel!(ty1)

   return fr, p1
end


#The lattice
using LinearAlgebra

#print("Enter dim value: "); d = parse(Int, readline(stdin)) # use it for IO input of d! else use fixed d!
d = 4;
v = Matrix{Any}(zeros(d,d));
h = Matrix{Any}(zeros(d,d));
p = Matrix{Float64}(undef,d,d);
x = Matrix{Float64}(undef,d,d);
y = Matrix{Float64}(undef,d,d);
Φ = Matrix{Float64}(undef,d,d);

for i = 1:d
    h[i,d] = Matrix{Float64}(I,d,d)[i,:];
end

#=for k = d:-1:1
    v[k,k] = (x[k,k] + y[k,k]im)*h[k,k];

    for j = k-1:-1:1
        v[j,k] = (x[j,k] + y[j,k]im) * sqrt(complex(p[j,k])) * h[j,k] + sqrt(complex(1-p[j,k])) * v[j+1,k];
        h[j,k-1] = (x[j,k] + y[j,k]im) * sqrt(complex(1-p[j,k])) * h[j,k] - sqrt(complex(p[j,k])) * v[j+1,k];
        println("k:",k,"\t","j:",j)
    end
end=#

function H(m::Int, n::Int)
    for k = d:-1:1
        v[k,k] = (x[k,k] + y[k,k]im)*h[k,k];

        for j = k-1:-1:1
            v[j,k] = (x[j,k] + y[j,k]im) * sqrt(complex(p[j,k])) * h[j,k] + sqrt(complex(1-p[j,k])) * v[j+1,k];
            h[j,k-1] = (x[j,k] + y[j,k]im) * sqrt(complex(1-p[j,k])) * h[j,k] - sqrt(complex(p[j,k])) * v[j+1,k];
        end
    end
    return (h[m,n])
end

#Ist = transpose(v);

#-----------------------------------------------------------------------------------------------------
#DFT

m(n::Int) = 1/sqrt(n)*[exp(2*pi*im*(r-1)*(s-1)/n) for r in 1:n, s in 1:n];

#Soll = m(d);

#------------------------------------------------------------------------------------------------------
#The solution of the algorithm

Denom(j::Int,k::Int) = dot(Matrix{Float64}(I,d,d)[j,:], H(j,k)) * (j==1 ? 1.0 : prod(sqrt(complex(1-p[l,k])) for l = 1:j-1));

Num(j::Int,k::Int) = j==1 ? 0.0 : sum((x[i,k] + y[i,k]im) * dot(Matrix{Float64}(I,d,d)[j,:], H(i,k)) * sqrt(complex(p[i,k])) * (i==1 ? 1.0 : prod(sqrt(complex(1-p[l,k])) for l = 1:i-1)) for i = 1:j-1);

Z(j::Int,k::Int) = (m(d)[j,k] - Num(j,k)) / Denom(j,k);

#=function Angle(Re,Im,j,k)
    if abs(exp(asin(Float32(Im))*im) - (Re + Im*im)) < 10.0^-12
        Φ[j,k] = asin(Float32(Im));
    else
        Φ[j,k] = -asin(Float32(Im)) - pi;
    end
end=#
#Angle(Re,Im,j,k) = abs(exp(asin(Float32(Im))*im) - (Re + Float32(Im)*im)) < 10.0^-12 ? Φ[j,k] = asin(Float32(Im)) : Φ[j,k] = -asin(Float32(Im)) - pi;
Angle(Re::Float16,Im::Float16,j::Int,k::Int) = abs2(exp(asin(Im)*im) - (Re + Im*im)) < 10.0^-12 ? Φ[j,k] = asin(Im) : Φ[j,k] = -asin(Im) - pi;
# the results vary depending on the precision of the 'Float' used!

#-----------------------------------------------------------------------------------------------------
#Evaluation

@time for k = d:-1:1
    for j = 1:k-1
        p[j,k] = abs2(Z(j,k));
        x[j,k] = real(Z(j,k)/sqrt(complex(p[j,k])));
        y[j,k] = imag(Z(j,k)/sqrt(complex(p[j,k])));
        Angle(Float16(x[j,k]),Float16(y[j,k]),j,k);
        println("p(", j, ",", k, ") = ", p[j,k], "\tΦ(", j, ",", k, ") = ", Φ[j,k]*180/pi);
    end

    x[k,k] = real(Z(k,k));
    y[k,k] = imag(Z(k,k));
    Angle(Float16(x[k,k]),Float16(y[k,k]),k,k);
    println("Φ(", k, ",", k, ") = ", Φ[k,k]*180/pi);
end

#------------------------------------------------------------------------------------------------------


# example with constraint in struct
# struct Vector2D{T<:Number}
#    x::T
#    y::T
# end

# function dot(u::Vector2D{T}, v::Vector2D{T}) where T
#     u.x*v.x + u.y*v.y
# end

# function cross(u::Vector2D{T}, v::Vector2D{T}) where T
#     u.x*v.y - u.y*v.x
# end

# julia> a = Vector2D(1, 2)
# Vector2D{Int64}(1, 2)

# julia> b = Vector2D(3, 4)
# Vector2D{Int64}(3, 4)

# julia> c =Vector2D{Char}('A', 'B')
# ERROR: TypeError: in Vector2D, in T, expected T<:Number, got Type{Char}
# Stacktrace:
#  [1] top-level scope
#    @ REPL[11]:1

# julia> dot(a,b)
# 11

# julia> cross(a,b)
# -2

# dot(a,c)

# same example with simpler method signature
# in this version  u and v can be of different Number subtype and still works
# struct Vector2D{T<:Number}
#    x::T
#    y::T
# end

# function dot(u::Vector2D, v::Vector2D)
#     u.x*v.x + u.y*v.y
# end

# function cross(u::Vector2D, v::Vector2D)
#     u.x*v.y - u.y*v.x
# end

# julia> a = Vector2D(1, 2)
# Vector2D{Int64}(1, 2)

# julia> b = Vector2D(3.0, 4.0)
# Vector2D{Float64}(3.0, 4.0)

# julia> c =Vector2D{Char}('A', 'B')
# ERROR: TypeError: in Vector2D, in T, expected T<:Number, got Type{Char}
# Stacktrace:
#  [1] top-level scope
#    @ REPL[7]:1

# julia> dot(a,b)
# 11.0

# julia> cross(a,b)
# -2.0

# example with constraint in method - allows non Number Vecttor2
# but methods are type protected
# struct Vector2D{T}
#    x::T
#    y::T
# end

# function dot(u::Vector2D{T}, v::Vector2D{T}) where T <: Number
#     u.x*v.x + u.y*v.y
# end

# function cross(u::Vector2D{T}, v::Vector2D{T}) where T <: Number
#     u.x*v.y - u.y*v.x
# end

# julia> a = Vector2D(1, 2)
# Vector2D{Int64}(1, 2)

# julia> b = Vector2D(3, 4)
# Vector2D{Int64}(3, 4)

# julia> c =Vector2D{Char}('A', 'B')
# Vector2D{Char}('A', 'B')

# julia> a = Vector2D(1, 2)
# Vector2D{Int64}(1, 2)

# julia> b = Vector2D(3, 4)
# Vector2D{Int64}(3, 4)

# julia> c =Vector2D{Char}('A', 'B')
# Vector2D{Char}('A', 'B')

# julia> dot(a,b)
# 11

# julia> cross(a,b)
# -2

# julia> dot(a,c)
# ERROR: MethodError: no method matching dot(::Vector2D{Int64}, ::Vector2D{Char})
# Closest candidates are:
#   dot(::Vector2D{T}, ::Vector2D{T}) where T<:Number at REPL[4]:1
# Stacktrace:
#  [1] top-level scope
#    @ REPL[14]:1

# julia> cross(a,c)
# ERROR: MethodError: no method matching cross(::Vector2D{Int64}, ::Vector2D{Char})
# Closest candidates are:
#   cross(::Vector2D{T}, ::Vector2D{T}) where T<:Number at REPL[5]:1
# Stacktrace:
#  [1] top-level scope
#    @ REPL[15]:1


#=

    This is a port of the SPPL model election.pynb

    This model:
    Summary Statistics
    parameters        mean        std   naive_se      mcse         ess      rhat   ess_per_sec
        Symbol     Float64    Float64    Float64   Float64     Float64   Float64       Float64

        param    299.7774    28.9717     0.2897    0.3653   6479.7518    0.9999      866.9724
            p      0.4814     0.0318     0.0003    0.0004   6508.3665    1.0002      870.8010
        votes   1925.2743   131.9134     1.3191    1.7207   6442.0098    1.0003      861.9226
          win      0.2902     0.4539     0.0045    0.0056   6597.9243    0.9999      882.7836

=#

using Turing, StatsPlots, Distributions, StatsBase
using CSV
include("jl_utils.jl")

#=
from sppl.sym_util import binspace
n = 4000
param ~= randint(low=250, high=350)
switch (param) cases (b in range(250, 350)):
    p ~= beta(a=277, b=b)
switch (p) cases (x in binspace(0, 1, 20)):
    votes ~= binom(n=n, p=(x.left + x.right)/2)
win ~= votes > 0.5*n

=#
@model function election(n=4000)
    param ~ DiscreteUniform(250,350)
    p ~ Beta(277,param)
    votes ~ Binomial(n,p)

    win ~ Dirac(votes > 0.5*n)

end

n = 4000
model = election(n)

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

# chns = sample(model, HMC(0.1,6), 1_000)
# chns = sample(model, NUTS(), 1_000)

display(chns)


module NormalSmoothingSplines

###### Inteface definition
export prepare_approximation, construct_approximation, approximate
export prepare_interpolation, construct_interpolation, interpolate
export evaluate, evaluate_at
export evaluate_gradient, evaluate_derivative
export NormalSpline, RK_H0, RK_H1, RK_H2

export get_epsilon, estimate_epsilon, get_cond, estimate_cond
export estimate_accuracy
######

using LinearAlgebra

abstract type ReproducingKernel end
abstract type ReproducingKernel_0 <: ReproducingKernel end
abstract type ReproducingKernel_1 <: ReproducingKernel_0 end
abstract type ReproducingKernel_2 <: ReproducingKernel_1 end

abstract type AbstractSpline end

"""
`struct NormalSpline{T, RK} <: AbstractSpline where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Define a structure containing full information of a normal spline
# Fields
- `_kernel`: a reproducing kernel spline was built with
- `_compression`: factor of transforming the original node locations into unit hypercube
- `_nodes`: transformed function value interpolation nodes
- `_nodes_b`: transformed function value approximation nodes
- `_values`: function values at interpolation nodes
- `_values_lb`: function lower bound values at approximation nodes
- `_values_ub`: function upper bound values at approximation nodes
- `_d_nodes`: transformed function directional derivative interpolation nodes
- `_d_nodes_b`: transformed function directional derivative approximation nodes
- `_es`: normalized derivative directions at interpolation nodes
- `_es_b`: normalized derivative directions at approximation nodes
- `_d_values`: function directional derivative values at interpolation nodes
- `_d_values_lb`: function lower bound directional derivative values at approximation nodes
- `_d_values_ub`: function upper bound directional derivative values at approximation nodes
- `_min_bound`: minimal bounds of the original node locations area
- `_gram`: Gram matrix of the problem
- `_chol`: Cholesky factorization of the Gram matrix
- `_mu`: spline coefficients
- `_active`: active inequality constraint numbers at solution
- `_cond`: estimation of the Gram matrix condition number
- `_ier`: An integer flag. If it is equal to 0, the optimal solution was found.
          If it is equal to 1, the approximate solution was found. QP algorithm iterations were stopped
          because of small spline norm change.
          If it is equal to 2, the approximate solution was found. QP algorithm iterations were stopped
          because of maximum allowed number of iterations was reached.
          If it is equal to -1, the solution was not calculated.
"""
struct NormalSpline{T, RK} <: AbstractSpline where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    _kernel::RK
    _compression::T
    _nodes::Union{Matrix{T}, Nothing}
    _nodes_b::Union{Matrix{T}, Nothing}
    _values::Union{Vector{T}, Nothing}
    _values_lb::Union{Vector{T}, Nothing}
    _values_ub::Union{Vector{T}, Nothing}
    _d_nodes::Union{Matrix{T}, Nothing}
    _d_nodes_b::Union{Matrix{T}, Nothing}
    _es::Union{Matrix{T}, Nothing}
    _es_b::Union{Matrix{T}, Nothing}
    _d_values::Union{Vector{T}, Nothing}
    _d_values_lb::Union{Vector{T}, Nothing}
    _d_values_ub::Union{Vector{T}, Nothing}
    _min_bound::Union{Vector{T}, Nothing}
    _gram::Union{Matrix{T}, Nothing}
    _chol::Union{Cholesky{T, Matrix{T}}, Nothing}
    _mu::Union{Vector{T}, Nothing}
    _active::Union{Vector{Int}, Nothing}
    _cond::T
    _ier::Int
end

include("./NormalInterpolatingSplines.jl")
include("./ReproducingKernels.jl")
include("./GramMatrix.jl")
include("./Utils.jl")
include("./QP.jl")
include("./Interpolate.jl")
include("./Approximate.jl")

##
#include("./examples/Main.jl")
##

"""
`prepare_approximation(nodes_b::Matrix{T}, kernel::RK = RK_H0())
                       where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Prepare the approximating normal spline by constructing and factoring a Gram matrix of the problem.
Initialize the `NormalSpline` object.
# Arguments
- `nodes_b`: function value approximation nodes.
          This should be an `n×n_1_b` matrix, where `n` is dimension of the sampled space and
          `n_1_b` is the number of function value approximation nodes. It means that each column in the matrix defines one node.
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H0` if the spline is constructing as a continuous function,
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.

Return: prepared `NormalSpline` object.
"""
function prepare_approximation(nodes_b::Matrix{T},
                               kernel::RK = RK_H0()
                              ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
     spline = _prepare_approximation(nodes_b, kernel)
     return spline
end

"""
`prepare_approximation(nodes::Matrix{T}, nodes_b::Matrix{T}, kernel::RK = RK_H0())
                       where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Prepare the approximating normal spline by constructing and factoring a Gram matrix of the problem.
Initialize the `NormalSpline` object.
# Arguments
- `nodes`: function value interpolation nodes.
           This should be an `n×n_1` matrix, where `n` is dimension of the sampled space and
           `n_1` is the number of function value interpolation nodes. It means that each column in the matrix defines one node.
- `nodes_b`: function value approximation nodes.
          This should be an `n×n_1_b` matrix, where `n` is dimension of the sampled space and
          `n_1_b` is the number of function value approximation nodes. It means that each column in the matrix defines one node.
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H0` if the spline is constructing as a continuous function,
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.

Return: prepared `NormalSpline` object.
"""
function prepare_approximation(nodes::Matrix{T},
                               nodes_b::Matrix{T},
                               kernel::RK = RK_H0()
                              ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
     spline = _prepare_approximation(nodes, nodes_b, kernel)
     return spline
end

"""
`construct_approximation(spline::NormalSpline{T, RK},
                         values_lb::Vector{T}, values_ub::Vector{T},
                         maxiter::Int, ftol::T)
                         where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

construct the approximating normal spline by calculating its coefficients and
completely initializing the `NormalSpline` object.
# Arguments
- `spline`: the partly initialized `NormalSpline` object returned by `prepare` function.
- `values_lb`: function lower bound values at approximation nodes
- `values_ub`: function upper bound values at approximation nodes
- `maxiter`: Maximum allowed number of iterations.
- `ftol`: convergence tolerance. The iteration stops when relative spline norm change is smaller than ftol.

Return: constructed `NormalSpline` object and the number of QP algorithm iterations done.
"""
function construct_approximation(spline::NormalSpline{T, RK},
                                 values_lb::Vector{T},
                                 values_ub::Vector{T},
                                 maxiter::Int,
                                 ftol::T = T(1.e-3)
                                ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    spline, nit_done = _construct_approximation(spline, values_lb, values_ub, maxiter, ftol)
    return spline, nit_done
end

"""
`construct_approximation(spline::NormalSpline{T, RK}, values::Vector{T},
                         values_lb::Vector{T}, values_ub::Vector{T}, maxiter::Int, ftol::T)
                         where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

construct the approximating normal spline by calculating its coefficients and
completely initializing the `NormalSpline` object.
# Arguments
- `spline`: the partly initialized `NormalSpline` object returned by `prepare` function.
- `values`: function values at interpolation nodes.
- `values_lb`: function lower bound values at approximation nodes
- `values_ub`: function upper bound values at approximation nodes
- `maxiter`: Maximum allowed number of iterations.
- `ftol`: convergence tolerance. The iteration stops when relative spline norm change is smaller than ftol.

Return: constructed `NormalSpline` object and the number of QP algorithm iterations done.
"""
function construct_approximation(spline::NormalSpline{T, RK},
                                 values::Vector{T},
                                 values_lb::Vector{T},
                                 values_ub::Vector{T},
                                 maxiter::Int,
                                 ftol::T = T(1.e-3)
                                ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    spline, nit_done = _construct_approximation(spline, values, values_lb, values_ub, maxiter, ftol)
    return spline, nit_done
end

"""
`approximate(nodes::Matrix{T}, values::Vector{T}, nodes_b::Matrix{T}, values_lb::Vector{T}, values_ub::Vector{T},
             kernel::RK, maxiter::Int, ftol::T) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Prepare and construct the approximating normal spline.
# Arguments
- `nodes_b`: function value approximation nodes.
        This should be an `n×n_1_b` matrix, where `n` is dimension of the sampled space and
        `n_1_b` is the number of function value approximation nodes. It means that each column in the matrix defines one node.
- `values_lb`: function lower bound values at approximation nodes
- `values_ub`: function upper bound values at approximation nodes
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H0` if the spline is constructing as a continuous function,
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.
- `maxiter`: Maximum allowed number of iterations.
- `ftol`: convergence tolerance. The iteration stops when relative spline norm change is smaller than ftol.

Return: constructed `NormalSpline` object.
"""
function approximate(nodes_b::Matrix{T},
                     values_lb::Vector{T},
                     values_ub::Vector{T},
                     kernel::RK,
                     maxiter::Int,
                     ftol::T = T(1.e-3)
                    ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
     spline = _prepare_approximation(nodes_b, kernel)
     spline, nit_done = _construct_approximation(spline, values_lb, values_ub, maxiter, ftol)
     return spline
end

"""
`approximate(nodes::Matrix{T}, values::Vector{T}, nodes_b::Matrix{T}, values_lb::Vector{T}, values_ub::Vector{T},
             kernel::RK, maxiter::Int, ftol::T) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Prepare and construct the approximating normal spline.
# Arguments
- `nodes`: function value interpolation nodes.
           This should be an `n×n_1` matrix, where `n` is dimension of the sampled space
           and `n_1` is the number of function value nodes.
           It means that each column in the matrix defines one node.
- `nodes_b`: function value approximation nodes.
        This should be an `n×n_1_b` matrix, where `n` is dimension of the sampled space and
        `n_1_b` is the number of function value approximation nodes. It means that each column in the matrix defines one node.
- `values`: function values at interpolation nodes.
- `values_lb`: function lower bound values at approximation nodes
- `values_ub`: function upper bound values at approximation nodes
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H0` if the spline is constructing as a continuous function,
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.
- `maxiter`: Maximum allowed number of iterations.
- `ftol`: convergence tolerance. The iteration stops when relative spline norm change is smaller than ftol.

Return: constructed `NormalSpline` object.
"""
function approximate(nodes::Matrix{T},
                     values::Vector{T},
                     nodes_b::Matrix{T},
                     values_lb::Vector{T},
                     values_ub::Vector{T},
                     kernel::RK,
                     maxiter::Int,
                     ftol::T = T(1.e-3)
                    ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
     spline = _prepare_approximation(nodes, nodes_b, kernel)
     spline, nit_done = _construct_approximation(spline, values, values_lb, values_ub, maxiter, ftol)
     return spline
end

"""
`evaluate(spline::NormalSpline{T, RK}, points::Matrix{T}) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Evaluate the spline values at `points` locations.

# Arguments
- `spline: constructed `NormalSpline` object.
- `points`: locations at which spline values are evaluating.
            This should be an `n×m` matrix, where `n` is dimension of the sampled space
            and `m` is the number of locations where spline values are evaluating.
            It means that each column in the matrix defines one location.

Return: `Vector{T}` of the spline values at the locations defined in `points`.
"""
function evaluate(spline::NormalSpline{T, RK}, points::Matrix{T}
                 ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    return _evaluate(spline, points)
end

"""
`evaluate_at(spline::NormalSpline{T, RK}, point::Vector{T}) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Evaluate the spline value at the `point` location.

# Arguments
- `spline`: constructed `NormalSpline` object.
- `point`: location at which spline value is evaluating.
           This should be a vector of size `n`, where `n` is dimension of the sampled space.

Return: spline value at the location defined in `point`.
"""
function evaluate_at(spline::NormalSpline{T, RK}, point::Vector{T}
                    ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    return _evaluate(spline, reshape(point, :, 1))[1]
end

"""
`evaluate_gradient(spline::NormalSpline{T, RK}, point::Vector{T}) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Evaluate gradient of the spline at the location defined in `point`.

# Arguments
- `spline`: the `NormalSpline` object returned by `interpolate` or `construct` function.
- `point`: location at which gradient value is evaluating.
           This should be a vector of size `n`, where `n` is dimension of the sampled space.

Note: Gradient of spline built with reproducing kernel RK_H0 does not exist at the spline nodes.

Return: `Vector{T}` - gradient of the spline at the `point` location.
"""
function evaluate_gradient(spline::NormalSpline{T, RK},
                           point::Vector{T}
                          ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    return _evaluate_gradient(spline, point)
end

########

"""
`get_epsilon(spline::NormalSpline{T, RK}) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Get the 'scaling parameter' of Bessel Potential space the spline was built in.
# Arguments
- `spline`: prepared `NormalSpline` object.

Return: `ε` - the 'scaling parameter'.
"""
function get_epsilon(spline::NormalSpline{T, RK}
                    ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    return spline._kernel.ε
end

"""
`estimate_epsilon(nodes::Matrix{T}, kernel::RK = RK_H0()) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Get the estimation of the 'scaling parameter'2 of Bessel Potential space the spline being built in.
It coincides with the result returned by `get_epsilon` function.
# Arguments
- `nodes`: function value interpolation nodes.
           This should be an `n×n_1` matrix, where `n` is dimension of the sampled space
           and `n_1` is the number of function value nodes.
           It means that each column in the matrix defines one node.
- `kernel`: reproducing kernel of Bessel potential space the normal spline will be constructed in.
           It must be a struct object of the following type:
             `RK_H0` if the spline is constructing as a continuous function,
             `RK_H1` if the spline is constructing as a differentiable function,
             `RK_H2` if the spline is constructing as a twice differentiable function.
Return: estimation of `ε`.
"""
function estimate_epsilon(nodes::Matrix{T},
                          kernel::RK = RK_H0()
                         ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    ε = _estimate_epsilon(nodes, kernel)
    return ε
end

"""
`estimate_epsilon(nodes::Matrix{T}, d_nodes::Matrix{T}, kernel::RK = RK_H1()) where {T <: AbstractFloat, RK <: ReproducingKernel_1}`

Get an the estimation of the 'scaling parameter' of Bessel Potential space the spline being built in.
It coincides with the result returned by `get_epsilon` function.
# Arguments
- `nodes`: function value interpolation nodes.
           This should be an `n×n_1` matrix, where `n` is dimension of the sampled space
           and `n_1` is the number of function value nodes.
           It means that each column in the matrix defines one node.
- `d_nodes`: function directional derivative nodes.
           This should be an `n×n_2` matrix, where `n` is dimension of the sampled space and
           `n_2` is the number of function directional derivative nodes.
- `kernel`: reproducing kernel of Bessel potential space the normal spline will be constructed in.
            It must be a struct object of the following type:
            `RK_H1` if the spline is constructing as a differentiable function,
            `RK_H2` if the spline is constructing as a twice differentiable function.

Return: estimation of `ε`.
"""
function estimate_epsilon(nodes::Matrix{T},
                          d_nodes::Matrix{T},
                          kernel::RK = RK_H1()
                         ) where {T <: AbstractFloat, RK <: ReproducingKernel_1}
    ε = _estimate_epsilon(nodes, d_nodes, kernel)
    return ε
end

"""
`estimate_cond(spline::NormalSpline{T, RK}) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Get an estimation of the Gram matrix condition number. It needs the `spline` object is prepared and requires O(N^2) operations.
(C. Brás, W. Hager, J. Júdice, An investigation of feasible descent algorithms for estimating the condition number of a matrix. TOP Vol.20, No.3, 2012.)
# Arguments
- `spline`: prepared `NormalSpline` object.

Return: an estimation of the Gram matrix condition number.
"""
function estimate_cond(spline::NormalSpline{T, RK}
                      ) where {T <: AbstractFloat, RK <: ReproducingKernel}
    return spline._cond
end

"""
`estimate_accuracy(spline::NormalSpline{T, RK}) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Assess accuracy of interpolation results by analyzing residuals.
# Arguments
- `spline`: constructed `NormalSpline` object.

Return: estimation of the number of significant digits in the interpolation result.
"""
function estimate_accuracy(spline::NormalSpline{T, RK}) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    return _estimate_accuracy(spline)
end

############################## One-dimensional case

"""
`prepare_approximation(nodes_b::Vector{T}, kernel::RK = RK_H0())
                       where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Prepare the 1D approximating normal spline by constructing and factoring a Gram matrix of the problem.
Initialize the `NormalSpline` object.
# Arguments
- `nodes_b`: function value approximation nodes.
             This should be an `n_1_b` vector where `n_1_b` is the number of function value approximation nodes.
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H0` if the spline is constructing as a continuous function,
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.

Return: prepared `NormalSpline` object.
"""
function prepare_approximation(nodes_b::Vector{T},
                               kernel::RK = RK_H0()
                              ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}

     spline = _prepare_approximation(Matrix(nodes_b'), kernel)
     return spline
end

"""
`prepare_approximation(nodes::Vector{T}, nodes_b::Vector{T}, kernel::RK = RK_H0())
                       where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Prepare the 1D approximating normal spline by constructing and factoring a Gram matrix of the problem.
Initialize the `NormalSpline` object.
# Arguments
- `nodes`: function value interpolation nodes.
           This should be an `n_1` vector where `n_1` is the number of function value interpolation nodes.
- `nodes_b`: function value approximation nodes.
           This should be an `n_1_b` vector where `n_1_b` is the number of function value approximation nodes.
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H0` if the spline is constructing as a continuous function,
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.

Return: prepared `NormalSpline` object.
"""
function prepare_approximation(nodes::Vector{T},
                               nodes_b::Vector{T},
                               kernel::RK = RK_H0()
                              ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
     spline = _prepare_approximation(Matrix(nodes'), Matrix(nodes_b'), kernel)
     return spline
end

"""
`approximate(values::Vector{T}, nodes_b::Vector{T}, values_lb::Vector{T}, values_ub::Vector{T},
             kernel::RK, maxiter::Int, ftol::T) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Prepare and construct the 1D approximating normal spline.
# Arguments
- `nodes_b`: function value approximation nodes.
          This should be an `n_1_b` vector where `n_1_b` is the number of function value approximation nodes.
- `values_lb`: function lower bound values at approximation nodes
- `values_ub`: function upper bound values at approximation nodes
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H0` if the spline is constructing as a continuous function,
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.
- `maxiter`: Maximum allowed number of iterations.
- `ftol`: convergence tolerance. The iteration stops when relative spline norm change is smaller than ftol.

Return: constructed `NormalSpline` object.
"""
function approximate(nodes_b::Vector{T},
                     values_lb::Vector{T},
                     values_ub::Vector{T},
                     kernel::RK,
                     maxiter::Int,
                     ftol::T = T(1.e-3)
               ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
     spline = _prepare_approximation(Matrix(nodes_b'), kernel)
     spline, nit_done = _construct_approximation(spline, values_lb, values_ub, maxiter, ftol)
     return spline
end

"""
`approximate(nodes::Vector{T}, values::Vector{T}, nodes_b::Vector{T}, values_lb::Vector{T}, values_ub::Vector{T},
             kernel::RK, maxiter::Int, ftol::T) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Prepare and construct the 1D approximating normal spline.
# Arguments
- `nodes`: function value interpolation nodes.
          This should be an `n_1` vector where `n_1` is the number of function value interpolation nodes.
- `values`: function values at interpolation nodes.
- `nodes_b`: function value approximation nodes.
          This should be an `n_1_b` vector where `n_1_b` is the number of function value approximation nodes.
- `values_lb`: function lower bound values at approximation nodes
- `values_ub`: function upper bound values at approximation nodes
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H0` if the spline is constructing as a continuous function,
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.
- `maxiter`: Maximum allowed number of iterations.
- `ftol`: convergence tolerance. The iteration stops when relative spline norm change is smaller than ftol.

Return: constructed `NormalSpline` object.
"""
function approximate(nodes::Vector{T},
                     values::Vector{T},
                     nodes_b::Vector{T},
                     values_lb::Vector{T},
                     values_ub::Vector{T},
                     kernel::RK,
                     maxiter::Int,
                     ftol::T = T(1.e-3)
               ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
     spline = _prepare_approximation(Matrix(nodes'), Matrix(nodes_b'), kernel)
     spline, nit_done = _construct_approximation(spline, values, values_lb, values_ub, maxiter, ftol)
     return spline
end

"""
`evaluate(spline::NormalSpline{T, RK}, points::Vector{T}) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Evaluate the 1D spline values at the `points` locations.

# Arguments
- `spline`: constructed `NormalSpline` object.
- `points`: locations at which spline values are evaluating.
            This should be a vector of size `m` where `m` is the number of evaluating points.

Return: spline value at the `point` location.
"""
function evaluate(spline::NormalSpline{T, RK}, points::Vector{T}
                               ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    return _evaluate(spline, Matrix(points'))
end

"""
`evaluate_at(spline::NormalSpline{T, RK}, point::T) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Evaluate the 1D spline value at the `point` location.

# Arguments
- `spline`: constructed `NormalSpline` object.
- `point`: location at which spline value is evaluating.

Return: spline value at the `point` location.
"""
function evaluate_at(spline::NormalSpline{T, RK},
                                       point::T
                                      ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    v_points = Vector{T}(undef, 1)
    v_points[1] = point
    return _evaluate(spline, Matrix(v_points'))[1]
end

"""
`evaluate_derivative(spline::NormalSpline{T, RK}, point::T) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Evaluate the 1D spline derivative at the `point` location.

# Arguments
- `spline`: the `NormalSpline` object returned by `interpolate` or `construct` function.
- `point`: location at which spline derivative is evaluating.

Note: Derivative of spline built with reproducing kernel RK_H0 does not exist at the spline nodes.

Return: spline derivative value at the `point` location.
"""
function evaluate_derivative(spline::NormalSpline{T, RK},
                             point::T
                            ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    v_points = Vector{T}(undef, 1)
    v_points[1] = point
    return _evaluate_gradient(spline, v_points)[1]
end

"""
`estimate_epsilon(nodes::Vector{T}, kernel::RK = RK_H0()) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Get an the estimation of the 'scaling parameter' of Bessel Potential space the spline being built in.
It coincides with the result returned by `get_epsilon` function.
# Arguments
- `nodes`: function value interpolation nodes.
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H0` if the spline is constructing as a continuous function,
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.

Return: estimation of `ε`.
"""
function estimate_epsilon(nodes::Vector{T},
                          kernel::RK = RK_H0()
                         ) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
    ε = _estimate_epsilon(Matrix(nodes'), kernel)
    return ε
end

"""
`estimate_epsilon(nodes::Vector{T}, d_nodes::Vector{T}, kernel::RK = RK_H1()) where {T <: AbstractFloat, RK <: ReproducingKernel_1}`

Get an the estimation of the 'scaling parameter' of Bessel Potential space the spline being built in.
It coincides with the result returned by `get_epsilon` function.
# Arguments
- `nodes`: function value interpolation nodes.
- `d_nodes`: function derivative nodes.
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.

Return: estimation of `ε`.
"""
function estimate_epsilon(nodes::Vector{T},
                          d_nodes::Vector{T},
                          kernel::RK = RK_H1()
                         ) where {T <: AbstractFloat, RK <: ReproducingKernel_1}
    ε = _estimate_epsilon(Matrix(nodes'), Matrix(d_nodes'), kernel)
    return ε
end

"""
`get_cond(nodes::Matrix{T}, kernel::RK = RK_H0()) where {T <: AbstractFloat, RK <: ReproducingKernel_0}`

Get a value of the Gram matrix spectral condition number. It is obtained by means of the matrix SVD decomposition.
# Arguments
- `nodes`: function value interpolation nodes.
           This should be an `n×n_1` matrix, where `n` is dimension of the sampled space and
           `n_1` is the number of function value nodes. It means that each column in the matrix defines one node.
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H0` if the spline is constructing as a continuous function,
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.

Return: a value of the Gram matrix spectral condition number.
"""
function get_cond(nodes::Matrix{T}, kernel::RK) where {T <: AbstractFloat, RK <: ReproducingKernel_0}
  return _get_cond(nodes, kernel)
end

"""
`get_cond(nodes::Matrix{T}, d_nodes::Matrix{T}, es::Matrix{T}, kernel::RK = RK_H1()) where {T <: AbstractFloat, RK <: ReproducingKernel_1}`

Get a value of the Gram matrix spectral condition number. It is obtained by means of the matrix SVD decomposition.
# Arguments
- `nodes`: function value interpolation nodes.
           This should be an `n×n_1` matrix, where `n` is dimension of the sampled space and
           `n_1` is the number of function value nodes.
            It means that each column in the matrix defines one node.
- `d_nodes`: function directional derivatives nodes.
             This should be an `n×n_2` matrix, where `n` is dimension of the sampled space and
             `n_2` is the number of function directional derivative nodes.
- `es`: Directions of the function directional derivatives.
        This should be an `n×n_2` matrix, where `n` is dimension of the sampled space and
        `n_2` is the number of function directional derivative nodes.
        It means that each column in the matrix defines one direction of the function directional derivative.
- `kernel`: reproducing kernel of Bessel potential space the normal spline is constructed in.
            It must be a struct object of the following type:
              `RK_H1` if the spline is constructing as a differentiable function,
              `RK_H2` if the spline is constructing as a twice differentiable function.

Return: value of the Gram matrix spectral condition number.
"""
function get_cond(nodes::Matrix{T}, d_nodes::Matrix{T}, es::Matrix{T}, kernel::RK = RK_H1()
                 ) where {T <: AbstractFloat, RK <: ReproducingKernel_1}
    return _get_cond(nodes, d_nodes, es, kernel)
end

end # module


"""
    skewness(x; method=:moment)

Calculates the skewness using the specified method.

# Methods
- Moment (default)
- Fisher-Pearson
- Sample

# Arguments
- `x`:          Vector of values.
- `method`:     Estimation method: `:moment`, `:fisher_pearson` or `:sample`.
"""
function skewness(x; method::Symbol=:moment)
    n = length(x)
    mean_devs = x .- mean(x)

    if method == :moment
        # Moment
        return mean(mean_devs.^3) / sqrt(mean(mean_devs.^2))^3  # sqrt(x)^3 faster than x^1.5 !
    elseif method == :fisher_pearson
        # Fisher-Pearson
        if n > 2
            return sqrt(n*(n-1))/(n-2) * mean(mean_devs.^3) / sqrt(mean(mean_devs.^2))^3  # sqrt(x)^3 faster than x^1.5 !
        else
            return NaN
        end
    elseif method == :sample
        # Sample
        return n/((n-1)*(n-2)) * sum(mean_devs.^3 / sqrt(mean(mean_devs.^2))^3)
    end

    throw(ArgumentError("Passed method parameter '$(method)' is invalid, must be one of :moment, :fisher_pearson, :sample."))
end

"""
    kurtosis(x; method=:excess)

Calculates the kurtosis using on the specified method.

# Methods
- Excess (default)
- Moment
- Cornish-Fisher

# Arguments
- `x`:          Vector of values.
- `method`:     Estimation method: `:excess`, `:moment` or `:cornish_fisher`.
"""
function kurtosis(x; method::Symbol=:excess)
    n = length(x)
    mean_devs = x .- mean(x)

    if method == :excess
        # Excess
        return sum(mean_devs.^4 / mean(mean_devs.^2)^2 ) / n - 3
    elseif method == :moment
        # Moment
        return sum(mean_devs.^4 / mean(mean_devs.^2)^2 ) / n
    elseif method == :cornish_fisher
        # Cornish-Fisher
        return ((n+1)*(n-1)*((sum(x.^4)/n)/(sum(x.^2)/n)^2 -
            (3*(n-1))/(n+1)))/((n-2)*(n-3))
    end

    throw(ArgumentError("Passed method parameter '$(method)' is invalid, must be one of :excess, :moment, :cornish_fisher."))
end

"""
    lower_partial_moment(returns, threshold, n, method)

This function calculates the Lower Partial Moment (LPM) for a given threshold.

# Arguments
- `returns`:     Vector of asset returns.
- `threshold`:   Scalar value or vector denoting the threshold returns.
- `n`:           `n`-th moment to calculate.
- `method`:      One of `:full` or `:partial`. Indicates whether to use the number of all returns (`:full`), or only the number of returns below the threshold (`:partial`) in the denominator.
"""
function lower_partial_moment(returns, threshold, n, method::Symbol)
    if method == :full
        denominator = length(returns)
    elseif method == :partial
        denominator = count(returns .< threshold)
    else
        throw(ArgumentError("Passed method parameter '$(method)' is invalid, must be one of :full, :partial."))
    end
    excess = threshold .- returns
    sum(map(x -> max(0.0, x)^n, excess)) / denominator
end

"""
    higher_partial_moment(returns, threshold, n, method)

This function calculates the Higher Partial Moment (HPM) for a given threshold.

# Arguments
- `returns`:     Vector of asset returns.
- `threshold`:   Scalar value or vector denoting the threshold returns.
- `n`:           `n`-th moment to calculate.
- `method`:      One of `:full` or `:partial`. Indicates whether to use the number of all returns (`:full`), or only the number of returns above the threshold (`:partial`) in the denominator.
"""
function higher_partial_moment(returns, threshold, n, method::Symbol)
    if method == :full
        denominator = length(returns)
    elseif method == :partial
        denominator = count(returns .> threshold)
    else
        throw(ArgumentError("Passed method parameter '$(method)' is invalid, must be one of :full, :partial."))
    end
    excess = returns .- threshold
    sum(map(x -> max(0.0, x)^n, excess)) / denominator
end


# Author: Lukasz Bratos

module Diff
using Plots
gr()

export ilorazyRoznicowe, warNewton, naturalna, rysujNnfx

"""
Funkcja oblicza iloraz różnicowy

Dane:
x – wektor długości n + 1 zawierający węzły x0, ..., xn
    x[1] = x0, ..., x[n+1] = xn
f – wektor długości n + 1 zawierający wartości interpolowanej funkcji w węzłach
    f(x0), ..., f(xn)

Wyniki:
fx– wektor długości n + 1 zawierający obliczone ilorazy różnicowe
    fx[1] = f[x0],
    fx[2] = f[x0, x1], ..., fx[n] = f[x0, ..., xn−1], fx[n+1] = f[x0, ..., xn]
"""
function ilorazyRoznicowe(x::Vector{Float64}, f::Vector{Float64})
    len = length(f)
    fx = Vector{Float64}(undef, len)

    for i in 1:len
        fx[i] = f[i]
    end

    for i in 2:len
        for j in len:-1:i
            fx[j] = (fx[j] - fx[j - 1]) / (x[j] - x[j - i + 1])
        end
    end

    return fx
end

"""
Funkcja wyliczajaca wartosc wielomianu interpolacyjnego stopnia n w postaci Newtona Nn(x)
za pomoca uogolnionego algorytmu Hornera

Dane:
x – wektor długości n + 1 zawierający węzły x0, ..., xn
    x[1] = x0, ..., x[n+1] = xn
fx – wektor długości n+ 1 zawierający ilorazy różnicowe
    fx[1] = f[x0],
    fx[2] = f[x0, x1], ..., fx[n] = f[x0, ..., xn−1], fx[n+1] =f [x0, ..., xn]
t – punkt, w którym należy obliczyć wartość wielomianu

Wyniki:
nt – wartość wielomianu w punkcie t
"""
function warNewton(x::Vector{Float64}, fx::Vector{Float64}, t::Float64)
    len = length(x)
    nt = fx[len]

    for i in (len - 1):-1:1
        nt = fx[i] + (t - x[i]) * nt
    end

    return nt
end

"""
Funkcja wyliczajaca wspolczynniki wielomianu w postaci normanej z wielomianu
interpolacyjnego w postaci Newtona

Dane:
x – wektor długości n+ 1 zawierający węzły x0, ..., xn
    x[1] = x0, ..., x[n+1] = xn
fx – wektor długości n + 1 zawierający ilorazy różnicowe
    fx[1] = f[x0],
    fx[2] = f[x0, x1], ..., fx[n] = f[x0, ..., xn−1], fx[n+1] = f[x0, ..., xn]

Wyniki:
a – wektor długości n+ 1 zawierający obliczone współczynniki postaci naturalnej
    a[1] = a0,
    a[2] = a1, ..., a[n] = an−1, a[n+1] = an.
"""
function naturalna(x::Vector{Float64}, fx::Vector{Float64})
    len = length(x)
    a = Vector{Float64}(undef, len)
    a[len] = fx[len]

    for i in (len - 1):-1:1
        a[i] = fx[i] - a[i + 1] * x[i]

        for j in (i + 1):(len - 1)
            a[j] = a[j] - a[j + 1] * x[i]
        end
    end

    return a
end

"""
Funkcja rysująca wielomian interpolacyjny i interpolowaną funkcję na danym przedziale

Dane:
f – funkcja f(x) zadana jako anonimowa funkcja,
a, b – przedział interpolacji
n – stopień wielomianu interpolacyjnego
filename - nazwa pliku wynikowego

Wyniki:
– funkcja rysuje wielomian interpolacyjny i interpolowaną funkcję w przedziale [a, b]
"""
function rysujNnfx(f, a::Float64, b::Float64, n::Int, filename::String)
    dist = (b - a) / n

    x = Vector{Float64}(undef, n + 1)
    y = Vector{Float64}(undef, n + 1)

    for i in 1:(n + 1)
        x[i] = a + (i - 1) * dist
        y[i] = f(x[i]);
    end

    fx = ilorazyRoznicowe(x, y)

    numOfPoints = 100
    plot_dist = (b - a) / numOfPoints

    fx_val = Vector{Float64}(undef, numOfPoints + 1)
    wx_val = Vector{Float64}(undef, numOfPoints + 1)

    for i in 1:(numOfPoints + 1)
        t = a + (i - 1) * plot_dist
        fx_val[i] = f(t)
        wx_val[i] = warNewton(x, fx, t)
    end

    plot(range(a, stop=b, length=(numOfPoints + 1)), fx_val, color = "red", label = "f(x)")
    plot!(range(a, stop=b, length=(numOfPoints + 1)), wx_val, color = "blue", label = "w(x)")
    savefig("./plots/$filename.png")
end

end