using EconPDEs

Base.@kwdef  struct CampbellCochraneModel
    # consumption process parameters
    μ::Float64 = 0.0189
    σ::Float64 = 0.015

    # utility
    γ::Float64 = 2.0
    ρ::Float64 = 0.116

    # habit
    κs::Float64 = 0.138
    b::Float64 = 0.0
end
# I choose persistence so that monthly simulation of the model matches processes in CC (1999)
# ρ = 12 * (1 - 0.89^(1/12))
# κs = 12 * (1 - 0.87^(1/12))

function initialize_stategrid(m::CampbellCochraneModel; smin = -300.0, n = 1000)
    μ = m.μ ; σ = m.σ ; γ = m.γ ; ρ = m.ρ ; κs = m.κs ; b = m.b
    Sbar = σ * sqrt(γ / (κs - b / γ))
    sbar = log.(Sbar)
    smax =  sbar + 0.5 * (1 - Sbar^2)
    # corresponds to Grid 3 in Wachter (2005)
    shigh = log.(range(0.0, stop = exp(smax), length = div(n, 10)))
    slow = range(smin, stop = shigh[2], length = n - div(n, 10))
    OrderedDict(:s => vcat(slow[1:(end-1)], shigh[2:end]))
end

function initialize_y(m::CampbellCochraneModel, stategrid)
    OrderedDict(:p => ones(length(stategrid[:s])))
end

function (m::CampbellCochraneModel)(state::NamedTuple, y::NamedTuple)
    μ = m.μ ; σ = m.σ ; γ = m.γ ; ρ = m.ρ ; κs = m.κs ; b = m.b
    s = state.s
    p, ps, pss = y.p, y.ps, y.pss

    # drift and volatility of  s and p
    Sbar = σ * sqrt(γ / (κs - b / γ))
    sbar = log(Sbar)
    λ = 1 / Sbar * sqrt(1 - 2 * (s - sbar)) - 1
    μs = - κs * (s - sbar)
    σs = λ * σ
    σp = ps / p * σs
    μp = ps / p * μs + 0.5 * pss / p * σs^2

    # market price of risk κ
    κ = γ * (σ + σs)

    # risk free rate  r
    r = ρ + γ * μ - (γ * κs - b) / 2 + b * (sbar - s)

    # PDE
    pt = p * (1 / p + μ + μp + σp * σ - r - κ * (σ + σp))
    return (pt,), (μs, ), (p = p, κ = κ, λ = λ, r = r, σp = σp, μs = μs, σs = σs)
end

# Campbell Cochrane (1999)
m = CampbellCochraneModel()
stategrid = initialize_stategrid(m)
y0 = initialize_y(m, stategrid)
y, result, distance = pdesolve(m, stategrid, y0)

# Wachter (2005) calibration
# m = CampbellCochraneModel(μ = 0.022, σ = 0.0086, γ = 2.0, ρ = 0.073, κs = 0.116, b = 0.011)
# stategrid = initialize_stategrid(m)
# y0 = initialize_y(m, stategrid)
# y, result, distance = pdesolve(m, stategrid, y0)


abstract type AbstractAcquisition end

setparams!(a, model) = nothing
function acquisitionfunction(a, model)
    x -> begin
        μ, σ² = mean_var(model, x)
        a.(μ, σ²)
    end
end

"""
The probability of improvement measures the probability that a point `x` leads
to an improvement upon an incumbent target `τ`. For Gaussian distributions it is
given by

    Φ[(μ(x) - τ)/σ(x)]

where `Φ` is the standard normal cumulative distribution function and `μ(x)`, `σ(x)`
are mean and standard deviation of the distribution at point `x`.
"""
mutable struct ProbabilityOfImprovement <: AbstractAcquisition
    τ::Float64
end
@inline function (a::ProbabilityOfImprovement)(μ, σ²)
    σ² == 0 && return float(μ > a.τ)
    normal_cdf(μ - a.τ, σ²)
end
ProbabilityOfImprovement(; τ = -Inf) = ProbabilityOfImprovement(τ)

"""
The expected improvement measures the expected improvement `x - τ` of a point `x`
upon an incumbent target `τ`. For Gaussian distributions it is given by

    (μ(x) - τ) * ϕ[(μ(x) - τ)/σ(x)] + σ(x) * Φ[(μ(x) - τ)/σ(x)]

where `ϕ` is the standard normal distribution function and `Φ` is the standard
normal cumulative function, and `μ(x)`, `σ(x)` are mean and standard deviation
of the distribution at point `x`.
"""
mutable struct ExpectedImprovement <: AbstractAcquisition
    τ::Float64
end
ExpectedImprovement(; τ = -Inf) = ExpectedImprovement(τ)
function setparams!(a::Union{ExpectedImprovement,ProbabilityOfImprovement}, model)
    a.τ = max(maxy(model), a.τ)
end
@inline function (a::ExpectedImprovement)(μ, σ²)
    σ² == 0 && return μ > a.τ ? μ - a.τ : 0.
    abs(μ - a.τ) * normal_cdf(μ - a.τ, σ²) + √σ² * normal_pdf(μ - a.τ, σ²)
end

abstract type BetaScaling end
"""
Scales `βt` of `UpperConfidenceBound` as

    βt = √(2 * log(t^(D/2 + 2) * π^2/(3δ)))

where `t` is the number of observations, `D` is the dimensionality of the input
data points and δ is a small constant (default δ = 0.1).

See Brochu E., Cora V. M., de Freitas N. (2010), "A Tutorial on Bayesian
Optimization of Expensive Cost Functions, with Application to Active User
Modeling and Hierarchical Reinforcement Learning", https://arxiv.org/abs/1012.2599v1
page 16.
"""
struct BrochuBetaScaling <: BetaScaling
    δ::Float64
end
"""
Applies no scaling to `βt` of `UpperConfidenceBound`.
"""
struct NoBetaScaling <: BetaScaling end
"""
For Gaussian distributions the upper confidence bound at `x` is given by
    μ(x) + βt * σ(x)

where `βt` is a fixed parameter in the case of `NoBetaScaling` or an observation
size dependent parameter in the case of e.g. `BrochuBetaScaling`.
"""
mutable struct UpperConfidenceBound{S} <: AbstractAcquisition
    scaling::S
    βt::Float64
end
"""
    UpperConfidenceBound(; scaling = BrochuBetaScaling(.1), βt = 1)
"""
UpperConfidenceBound(; scaling = BrochuBetaScaling(.1), βt = 1.) = UpperConfidenceBound(scaling, βt)
function setparams!(a::UpperConfidenceBound{BrochuBetaScaling}, model)
    D, nobs = dims(model)
    nobs == 0 && (nobs = 1)
    a.βt = sqrt(2*log(nobs^(D/2 + 2)*π^2/(3*a.scaling.δ)))
end
(a::UpperConfidenceBound)(μ, σ²) = μ + a.βt * √σ²

"""
The acquisition function associated with `ThompsonSamplingSimple` draws
independent samples for each input `x` a function value from the model. Together
with a gradient-free optimization method this leads to proposal points that
might be similarly distributed as the maxima of true Thompson samples from GPs.
True Thompson samples from a GP are simply functions from a GP. Maximizing these
samples can be tricky, see e.g. http://hildobijl.com/Downloads/GPRT.pdf
chapter 6.
"""
struct ThompsonSamplingSimple <: AbstractAcquisition end
acquisitionfunction(a::ThompsonSamplingSimple, model) = x -> myrand(model, x)

struct MaxMean <: AbstractAcquisition end
acquisitionfunction(a::MaxMean, model) = x -> mean_var(model, x)[1]

"""
The mutual information measures the amount of information gained by querying at
x. The parameter γ̂ gives a lower bound for the information on f from the queries
{x}. For a Gaussian this is
    γ̂ = ∑σ²(x)
and the mutual information at x is
    μ(x) + √(α)*(√(σ²(x)+γ̂) - √(γ̂))

where `μ(x)`, `σ(x)` are mean and standard deviation
of the distribution at point `x`.

See Contal E., Perchet V., Vayatis N. (2014), "Gaussian Process Optimization
with Mutual Information" http://proceedings.mlr.press/v32/contal14.pdf
"""
mutable struct MutualInformation <: AbstractAcquisition
    sqrtα::Float64
    γ̂::Float64
end
MutualInformation(; α = 1.0, γ̂ = 0.0) = MutualInformation(sqrt(α), γ̂)
function setparams!(a::MutualInformation, model)
    D, nobs = dims(model)
    if iszero(nobs)
        a.γ̂ = 0.0
    else
        last_x = @view model.x[:, end]
        μ, σ2 = mean_var(model, last_x)
        a.γ̂ += σ2
    end
end
(a::MutualInformation)(μ, σ²) = μ + a.sqrtα * (sqrt(σ² + a.γ̂) - sqrt(a.γ̂))

# TODO see
# https://github.com/HildoBijl/GPRT/blob/7166548b8587201fabc671a0647aac2ff96f3555/Chapter6/Chapter6.m#L723
# and corresponding thesis page 173
# mutable struct ThompsonSampling{K} <: AbstractAcquisition
# end

# TODO
# mutable struct EntropySearch <: AbstractAcquisition
# end

# TODO
# mutable struct PredictiveEntropySearch <: AbstractAcquisition
# end


"""
Riemann-Liouville sense fractional derivative algorithms, please refer to [Riemann-Liouville derivative](https://en.wikipedia.org/wiki/Fractional_calculus#Riemann%E2%80%93Liouville_fractional_derivative)
"""
abstract type RLDiff <: FracDiffAlg end

#RLDiff_LinearInterp maybe??
"""
# Riemann Liouville sense derivative approximation

    fracdiff(f, α, end_point, h, RLDiffApprox())

Using Linear interpolation to approximate fractional derivative in Riemann Liouville  fractional derivative sense.

### Example

```julia-repl
julia> fracdiff(x->x^5, 0.5, 2.5, 0.0001, RLDiffApprox())
141.59707906952633
```

!!! warning
    The RLDiffApprox algorithm only support for 0 < α < 1.
"""
struct RLDiffApprox <: RLDiff end

"""
# Riemann Liouville sense derivative using Triangular Strip Matrix to discrete and compute.

    fracdiff(f, α, end_point, h, RLDiffMatrix())

Using [Triangular Strip Matrix](https://en.wikipedia.org/wiki/Triangle_strip) to approximate fractional derivative.

### Example

```julia-repl
julia> fracdiff(x->x^5, 0.5, 2.5, 0.0001, RLInt_Matrix())
```

!!! info
    Triangular Strip Matrix method returns the derivative in the interval ``[0, T]`` in ```Vector```

```tex
@article{2009,
title={Matrix approach to discrete fractional calculus II: Partial fractional differential equations},
DOI={10.1016/j.jcp.2009.01.014},
author={Podlubny, Igor and Chechkin, Aleksei and Skovranek, Tomas and Chen, YangQuan and Vinagre Jara, Blas M.},
}
```
"""
struct RLDiffMatrix <: RLDiff end

"""
# Riemann Liouville sense linear spline interpolation.

    fracdiff(f, α, end_point, h, RLLinearSplineInterp())

Using linear spline interpolation method to approximate the Riemann Liouville fractional derivative.
"""
struct RLLinearSplineInterp <: RLDiff end

"""
# Riemann Liouville sense G1 scheme

    fracdiff(f, α, start_point, end_point, h, RLG1())

Remove the limit symbol in the definition of Grunwald-Letnikov fractional derivative, thereby leading to a discretization scheme in form of truncated series.

!!! tip
        **RLG1** also can be used to compute fractional integral~
        ``+\\alpha`` for fractional derivative and ``-\\alpha`` for fractional integral.

```tex
@inproceedings{Guo2015FractionalPD,
  title={Fractional Partial Differential Equations and their Numerical Solutions},
  author={Boling Guo and Xueke Pu and Feng-Hui Huang},
  year={2015}
}
```
"""
struct RLG1 <: RLDiff end

"""
# Riemann Liouville sense D scheme

        fracdiff(f, α, point, h, RLD())

```tex
@inproceedings{Guo2015FractionalPD,
  title={Fractional Partial Differential Equations and their Numerical Solutions},
  author={Boling Guo and Xueke Pu and Feng-Hui Huang},
  year={2015}
}
```
"""
struct RLD <: RLDiff end

################################################################
###                    Type definition done                  ###
################################################################

function fracdiff(f::FunctionAndNumber, α, end_point, h::Float64, ::RLDiffApprox)
    #checks(f, α, 0, end_point)
    typeof(f) <: Number ? (end_point == 0 ? (return 0) : (return f/sqrt(pi*end_point))) : nothing
    end_point == 0 ? (return 0) : nothing

    summation = 0
    n = floor(Int, end_point/h)

    @fastmath @inbounds @simd for i ∈ 0:n-1
        summation += (f(end_point-i*h) - f(end_point-(i+1)*h))*((i+1)^(1-α) - i^(1-α))
    end

    result = ((1-α)*f(0)/n^α+summation)*end_point^(-α)*n^α/gamma(2-α)
    return result
end

function fracdiff(f::FunctionAndNumber, α::Float64, end_point::AbstractArray, h::Float64, ::RLDiffApprox)::Vector
    result = map(x->fracdiff(f, α, x, h, RLDiffApprox()), end_point)
    return result
end

function fracdiff(f::Union{Function, Number}, α, end_point, h::Float64, ::RLDiffMatrix)
    N = round(Int, end_point/h+1)
    @views tspan = collect(0:h:end_point)
    return B(N, α, h)*f.(tspan)
end

#Compute the eliminator matrix Sₖ by omiting n-th row
function eliminator(n, row)
    temp = zeros(n, n) + I
    return @views temp[Not(row), :]
end

function B(N, p)
    result = zeros(N, N)
    temp = omega(N, p)

    @inbounds @simd for i ∈ 1:N
        @views result[i, 1:i]=reverse(temp[1:i])
    end

    return result
end
# Multiple dispatch for assigning step size *h*.
function B(N, p, h::Float64)
    result = B(N, p)

    return h^(-p)*result
end

#=
Numerical methods for fractional calculus by Li, Changpin
Page 57

Linear Spline Interpolation
=#
function fracdiff(f::FunctionAndNumber, α, x, h::Float64, ::RLLinearSplineInterp)
    typeof(f) <: Number ? (x == 0 ? (return 0) : (return f/sqrt(pi*x))) : nothing
    x == 0 ? (return 0) : nothing
    N = round(Int, x/h)

    result = 0

    @fastmath @inbounds @simd for k = 0:(N+1)
        result += z̄ₘₖ(N, k, α)*f(k*h)
    end

    return 1/(gamma(4-α)h^α)*result

end

function z̄ₘₖ(m, k, α)
    if k ≤ m-1
        return c̄ⱼₖ(m-1, k, α)-2*c̄ⱼₖ(m, k, α)+c̄ⱼₖ(m+1, k, α)
    elseif k == m
        return -2*c̄ⱼₖ(m, k, α)+c̄ⱼₖ(m+1, k, α)
    elseif k == m+1
        return c̄ⱼₖ(m+1, k, α)
    elseif k > m+1
        return 0
    end
end

function c̄ⱼₖ(j, k, α)
    if k==0
        return (j-1)^(3-α)-j^(2-α)*(j-3+α)
    elseif 1 ≤ k ≤ j-1
        return (j-k+1)^(3-α)-2*(j-k)^(3-α)+(j-k-1)^(3-α)
    elseif k == j
        return 1
    end
end

function fracdiff(f::FunctionAndNumber, α::Float64, end_point::AbstractArray, h::Float64, ::RLLinearSplineInterp)::Vector
    result = map(x->fracdiff(f, α, x, h, RLLinearSplineInterp()), end_point)
    return result
end

function fracdiff(f::FunctionAndNumber, α, start_point, end_point, h::Float64, ::RLG1)
    typeof(f) <: Number ? (end_point == 0 ? (return 0) : (return f/sqrt(pi*end_point))) : nothing
    end_point == 0 ? (return 0) : nothing

    N = round(Int, (end_point-start_point)/h)

    result = zero(Float64)
    @fastmath @inbounds @simd for j = 0:N-1
        result += gamma(j-α)/gamma(j+1)*f(end_point-j*h)
    end

    return h^(-α)/gamma(-α)*result
end

function fracdiff(f::FunctionAndNumber, α, point, h::Float64, ::RLD)
    typeof(f) <: Number ? (point == 0 ? (return 0) : (return f/sqrt(pi*point))) : nothing
    point == 0 ? (return 0) : nothing

    N = round(Int, point/h)

    result = zero(Float64)
    @fastmath @inbounds @simd for k = 0:N
        result += ωₖₙ(N, k, α)*f(point-k*h)
    end

    return point^(-α)/gamma(-α)*result

end

function ωₖₙ(n, k, α)
    temp = 0
    if k == 0
        temp = -1
    elseif 1 ≤ k ≤ n-1
        temp = 2*k^(1-α)-(k-1)^(1-α)-(k+1)^(1-α)
    elseif k == n
        temp = (α-1)*n^(-α)-(n-1)^(1-α)+n^(1-α)
    end
    return n^α/(α*(1-α))*temp
end


"""
    adaptif(f, a, b; delta=10^-9)

adalah fungsi yang digunakan untuk mencari nilai integral dari fungsi `f`
pada interval `[a,b]` menggunakan aturan kuadratur adaptif.
Secara default toleransi yang digunakan adalah `delta=1e-9`.

# Examples
```jldoctest
julia> f(x) = 13*(x-x.^2).*exp(-3*x/2);

julia> sol,err,SRmat = adaptif(f,0,4,delta=0.00001);

julia> sol
-1.5487882341253174

julia> err
2.9680861581356417e-6

julia> SRmat
20×5 Array{Float64,2}:
 0.0     0.0625   0.0228718  1.52153e-8  1.5625e-7
 0.0625  0.125    0.0594869  1.31649e-8  1.5625e-7
 0.125   0.1875   0.0843421  1.13722e-8  1.5625e-7
 0.1875  0.25     0.0996987  9.80639e-9  1.5625e-7
 ⋮
 2.5     2.75    -0.2701     7.07089e-8  6.25e-7
 2.75    3.0     -0.234747   5.4474e-8   6.25e-7
 3.0     3.5     -0.363898   1.03699e-6  1.25e-6
 3.5     4.0     -0.243138   4.10779e-7  1.25e-6
```
return solusi `sol`, estimasi kesalahan `err`, dan
matriks `SRmat` yang berisi sub-interval (kolom 1 dan 2), nilai integral
pada sub-interval (kolom 3), galat integral numerik (kolom 4), dan toleransi pada
sub-interval (kolom 5).

"""
function adaptif(f, a, b; delta=10^-9)
  iterating = 0;
  done = 1;
  SRvec = srule(f,a,b,delta);
  SRmat = SRvec;
  m = 1;
  state = iterating;
  while(state==iterating)
    n = m;
    for j = n:-1:1
      p = j;
      SR0vec = SRmat[p,:];
      err = SR0vec[5];
      delta = SR0vec[6];
      if (delta<=err)
        state=done;
        SR1vec=SR0vec;
        SR2vec=SR0vec;
        a = SR0vec[1];
        b = SR0vec[2];
        c =(a+b)/2;
        err=SR0vec[5];
        delta=SR0vec[6];
        delta2=delta/2;
        SR1vec=srule(f,a,c,delta2);
        SR2vec=srule(f,c,b,delta2);
        err = abs(SR0vec[3]-SR1vec[3]-SR2vec[3])/10;
        if err<delta
          SRmat[p,:]=SR0vec;
          SRmat[p,4]=SR1vec[3]+SR2vec[3];
          SRmat[p,5]=err;
        else
          SRmat = [SRmat; zeros(1,6)]
          SRmat[p+1:m+1,:] = SRmat[p:m,:];
          m=m+1;
          SRmat[p,:]=SR1vec;
          SRmat[p+1,:]=SR2vec;
          state=iterating;
        end
      end
    end
  end
  sol = sum(SRmat[:,4]);
  err = sum(abs.(SRmat[:,5]));
  SRmat = SRmat[1:m,[1,2,4,5,6]];
  return sol, err, SRmat
end

function srule(f,a0,b0,delta0)
  h = (b0-a0)/2;
  C = zeros(1,3)
  C = f.([a0 (a0+b0)/2 b0]);
  S = h*(C[1]+4*C[2]+C[3])/3;
  S2= S;
  delta1=delta0;
  err=delta0;
  Z = [a0 b0 S S2 err delta1];
  return Z
end


#Stationary iterative methods
#Templates, section 2.2
export jacobi, jacobi!, gauss_seidel, gauss_seidel!, sor, sor!, ssor, ssor!

jacobi(A::AbstractMatrix, b;
       tol=size(A,2)^3*eps(typeof(real(b[1]))), maxiter=size(A,2)^2) =
    jacobi!(zerox(A, b), A, b; tol=tol, maxiter=maxiter)

function jacobi!(x, A::AbstractMatrix, b;
        tol=size(A,2)^3*eps(typeof(real(b[1]))), maxiter=size(A,2)^2)
	n = size(A,2)
    xold = copy(x)
    z = zero(Amultype(A, x))
    tol = tol * norm(b)
	resnorms = zeros(typeof(real(b[1])), maxiter)
	for iter=1:maxiter
		for i=1:n
			xi = z
			for j=[1:i-1;i+1:n]
				xi += A[i,j]*xold[j]
			end
			A[i,i]==0 && throw(SingularError())
			x[i]=(b[i]-xi)/A[i,i]
		end
		#check convergence
		resnorms[iter] = norm(A*x-b)
		if resnorms[iter] < tol
			resnorms=resnorms[1:iter]
			break
		end
		copy!(xold, x)
	end
	x, ConvergenceHistory(resnorms[end]<tol, tol, length(resnorms), resnorms)
end

gauss_seidel(A::AbstractMatrix, b;
        tol=size(A,2)^3*eps(typeof(real(b[1]))), maxiter=size(A,2)^2) =
    gauss_seidel!(zerox(A, b), A, b; tol=tol, maxiter=maxiter)

function gauss_seidel!(x, A::AbstractMatrix, b;
        tol=size(A,2)^3*eps(typeof(real(b[1]))), maxiter=size(A,2)^2)
	n = size(A,2)
    xold = copy(x)
    z = zero(Amultype(A, x))
    tol = tol * norm(b)
	resnorms = zeros(typeof(real(b[1])), maxiter)
	for iter=1:maxiter
		for i=1:n
			σ=z
			for j=1:i-1
				σ+=A[i,j]*x[j]
			end
			for j=i+1:n
				σ+=A[i,j]*xold[j]
			end
			A[i,i]==0 && throw(SingularError())
			x[i]=(b[i]-σ)/A[i,i]
		end
		#check convergence
		resnorms[iter] = norm(A*x-b)
		if resnorms[iter] < tol
			resnorms=resnorms[1:iter]
			break
		end
		copy!(xold, x)
	end
	x, ConvergenceHistory(resnorms[end]<tol, tol, length(resnorms), resnorms)
end

#Successive overrelaxation
sor(A::AbstractMatrix, b, ω::Real;
    tol=size(A,2)^3*eps(typeof(real(b[1]))), maxiter=size(A,2)^2) =
    sor!(zerox(A, b), A, b, ω; tol=tol, maxiter=maxiter)

function sor!(x, A::AbstractMatrix, b, ω::Real;
        tol=size(A,2)^3*eps(typeof(real(b[1]))), maxiter=size(A,2)^2)
	0 < ω < 2 || warn("ω = $ω lies outside the range 0<ω<2 which is required for convergence")

	n = size(A,2)
    xold = copy(x)
    z = zero(Amultype(A, x))
    tol = tol * norm(b)
	resnorms = zeros(typeof(real(b[1])), maxiter)
	for iter=1:maxiter
		for i=1:n
			σ=z
			for j=1:i-1
				σ+=A[i,j]*x[j]
			end
			for j=i+1:n
				σ+=A[i,j]*xold[j]
			end
			A[i,i]==0 && throw(SingularError())
			σ=(b[i]-σ)/A[i,i]
			x[i]=xold[i]+ω*(σ-xold[i])
		end
		#check convergence
		resnorms[iter] = norm(A*x-b)
		if resnorms[iter] < tol
			resnorms=resnorms[1:iter]
			break
		end
		copy!(xold, x)
	end
	x, ConvergenceHistory(resnorms[end]<tol, tol, length(resnorms), resnorms)
end

#Symmetric successive overrelaxation
#A must be symmetric
ssor(A::AbstractMatrix, b, ω::Real;
     tol=size(A,2)^3*eps(typeof(real(b[1]))), maxiter=size(A,2)) =
    ssor!(zerox(A, b), A, b, ω; tol=tol, maxiter=maxiter)

function ssor!(x, A::AbstractMatrix, b, ω::Real;
        tol=size(A,2)^3*eps(typeof(real(b[1]))), maxiter=size(A,2))
	0 < ω < 2 || warn("ω = $ω lies outside the range 0<ω<2 which is required for convergence")

	n = size(A,2)
    xold = copy(x)
    z = zero(Amultype(A, x))
    tol = tol * norm(b)
	resnorms = zeros(typeof(real(b[1])), maxiter)
	for iter=1:maxiter
		for i=1:n #Do a SOR sweep
			σ=z
			for j=1:i-1
				σ+=A[i,j]*x[j]
			end
			for j=i+1:n
				σ+=A[i,j]*xold[j]
			end
			A[i,i]==0 && throw(SingularError())
			σ=(b[i]-σ)/A[i,i]
			x[i]=xold[i]+ω*(σ-xold[i])
		end
		copy!(xold, x)
		for i=n:-1:1 #Do a backward SOR sweep
			σ=z
			for j=1:i-1
				σ+=A[i,j]*xold[j]
			end
			for j=i+1:n
				σ+=A[i,j]*x[j]
			end
			A[i,i]==0 && throw(SingularError())
			σ=(b[i]-σ)/A[i,i] #This line is missing in the Templates reference
			x[i]=xold[i]+ω*(σ-xold[i])
		end
		#check convergence
		resnorms[iter] = norm(A*x-b)
		if resnorms[iter] < tol
			resnorms=resnorms[1:iter]
			break
		end
		copy!(xold, x)
	end
	x, ConvergenceHistory(resnorms[end]<tol, tol, length(resnorms), resnorms)
end


const fdc1_2 = [0, 1/2]
const fdc1_4 = [0, 2/3, -1/12]
const fdc1_6 = [0, 3/4, -3/20,1/60 ]
const fdc1_8 = [0, 4/5, -1/5 ,4/105, -1/280]
const fdc2_2 = [-2, 1]
const fdc2_4 = [-5/2,    4/3,-1/12]
const fdc2_6 = [-49/18 , 3/2,-3/20, 1/90]
const fdc2_8 = [-205/72, 8/5,-1/5,  8/315, -1/560]
const fdc1 = [fdc1_2, fdc1_4, fdc1_6, fdc1_8]
const fdc2 = [fdc2_2, fdc2_4, fdc2_6, fdc2_8]
fdcoeff1 = Vector{OffsetVector{Float64, Vector{Float64}}}(undef, 4)
fdcoeff2 = Vector{OffsetVector{Float64, Vector{Float64}}}(undef, 4)
function init!(fdcoeff1, fdcoeff2)
	 for i in 1:4
		tmp1 = zeros(2*i + 1)
		tmp1[i + 1] = fdc1[i][1]
		tmp2 = zeros(2*i + 1)
		tmp2[i + 1] = fdc2[i][1]
		for k in 1:i
			tmp1[i + 1 + k] =  fdc1[i][k+1]
			tmp1[i + 1 - k] = -fdc1[i][k+1]
			tmp2[i + 1 + k] =  fdc2[i][k+1]
			tmp2[i + 1 - k] =  fdc2[i][k+1]
		end
		fdcoeff1[i] = OffsetVector(tmp1, -i:i)
		fdcoeff2[i] = OffsetVector(tmp2, -i:i)
	end
end
init!(fdcoeff1, fdcoeff2)
cycindex(i,n) = (i + n - 1)%n + 1
function dfdt(f::TVF, dh::T; order = 2, bc = :Zero)where {T, TVF<:AbstractVector{T}}
	@assert order in Set([2,4,6,8])
	@assert bc in Set([:Zero, :Periodic])
	n = length(f)
	m = div(order, 2)
	g = similar(f)
	if bc == :Zero
		for i in 1:n
			for k in 1:m
				g[i] = fdcoeff1[m][0]*f[i]
				if i + k ≤ n
					g[i] += fdcoeff1[m][ k]*f[i+k]
				end
				if 1 ≤ i - k
					g[i] += fdcoeff1[m][-k]*f[i-k]
				end
			end
		end
	else
		for i in 1:n
			g[i] = fdcoeff1[m][0]*f[i]
			for k in 1:m
				g[i] += fdcoeff1[m][ k]*f[cycindex(i + k, n)] + fdcoeff1[m][-k]*f[cycindex(i - k, n)]
			end
		end
	end
	g ./= dh
	return g
end
function d2fdt(f::TVF, dh::T; order = 2, bc = :Zero)where {T, TVF<:AbstractVector{T}}
	@assert order in Set([2,4,6,8])
	@assert bc in Set([:Zero, :Periodic])
	n = length(f)
	m = div(order, 2)
	g = similar(f)
	if bc == :Zero
		for i in 1:n
			for k in 1:m
				g[i] = fdcoeff2[m][0]*f[i]
				if i + k ≤ n
					g[i] += fdcoeff2[m][ k]*f[i+k]
				end
				if 1 ≤ i - k
					g[i] += fdcoeff2[m][-k]*f[i-k]
				end
			end
		end
	else
		for i in 1:n
			g[i] = fdcoeff2[m][0]*f[i]
			for k in 1:m
				g[i] += fdcoeff2[m][ k]*f[cycindex(i + k, n)] + fdcoeff2[m][-k]*f[cycindex(i - k, n)]
			end
		end
	end
	g ./= dh^2
	return g
end
function makefd1mat(n::Integer, dh, order = 2; bc = :Zero)
	@assert order in Set([2,4,6,8])
	@assert bc in Set([:Zero, :Periodic])
	mat = zeros(typeof(float(dh)), (n, n))
	m = div(order, 2)
	if bc == :Zero
		for i in 1:n
			mat[i,i] = fdcoeff1[m][0]
			for k in 1:m
				if i + k ≤ n
					mat[i,i + k] = fdcoeff1[m][ k]
				end
				if 1 ≤ i - k
					mat[i,i - k] = fdcoeff1[m][-k]
				end
			end
		end
	else
		for i in 1:n
			mat[i,i] = fdcoeff1[m][0]
			for k in 1:m
				mat[i,cycindex(i + k, n)] = fdcoeff1[m][ k]
				mat[i,cycindex(i - k, n)] = fdcoeff1[m][-k]
			end
		end
	end
	mat ./= dh
	return mat
end
function makefd2mat(n::Integer, dh, order = 2; bc = :Zero)
	@assert order in Set([2,4,6,8])
	@assert bc in Set([:Zero, :Periodic])
	mat = zeros(typeof(float(dh)), (n, n))
	m = div(order, 2)
	if bc == :Zero
		for i in 1:n
			mat[i,i] = fdcoeff2[m][0]
			for k in 1:m
				if i + k ≤ n
					mat[i,i + k] = fdcoeff2[m][ k]
				end
				if 1 ≤ i - k
					mat[i,i - k] = fdcoeff2[m][-k]
				end
			end
		end
	else
		for i in 1:n
			mat[i,i] = fdcoeff2[m][0]
			for k in 1:m
				mat[i,cycindex(i + k, n)] = fdcoeff2[m][ k]
				mat[i,cycindex(i - k, n)] = fdcoeff2[m][-k]
			end
		end
	end
	mat ./= dh^2
	return mat
end


ENV["GKSwstype"] = "100" #src

# # Density Estimation
#
# Understanding the shape of your data is important for any data analysis.
#
# - Do you have continuous data? Categorical data? Both?
# - Are variables correlated?
# - What insights can you gain from univariate and bivariate data summaries?
#
# There are many ways to estimate and visualize the distribution of data.  In this module,
# we'll cover: TODO
#
# ## Empirical Density Estimation
#
# ### Empirical Cumulative Density Function (ECDF)
#
# The **`StatsBase`** package provides the `ecdf` function, which returns a function that
# can calculate the ECDF evaluated at a point, defined as
#
# $$f_{\text{ECDF}}(x) = \frac{\text{# values less than or equal to x}}{\text{total # values}}.$$
#
# In other words, the number returned by the ECDF is the probability that a random sample
# from the dataset is less than or equal to the argument.

using StatsBase, Plots

y = 1:10

f = ecdf(y)

plot(f, 0, 11, label="", xlab="x", ylab = "P(data <= x)")

# ### Univariate Data
#
# First we will look at how to estimate/plot univariate distributions of data.
# As we saw in the intro module, we can plot histograms of continuous datafrom the
# **`StatsBase`** package.

using StatsBase, Plots

y = randn(10_000)

h = fit(Histogram, y; closed = :left)

plot(h)

# Alternatively, we can create the plot with the `histogram` function from ``*Plots*``.

histogram(y)

# #### Kernel Density Estimation (KDE)
#
# Kernel Density Estimation (KDE) is a method of estimating the probability density function
# by averaging together a kernel function applied to neighboring points.  A KDE with kernel
# $K$ and bandwidth parameter $h>0$ is a function of data points $(x_1,\ldots,x_n)$ that looks like
#
# $$\hat f(x) = \frac{1}{nh} \sum_{i=1}^n K\left(\frac{x-x_i}{h}\right).$$
#
# Here are some examples of kernel functions:
#
# ![](https://user-images.githubusercontent.com/8075494/30523575-acd48de2-9bb1-11e7-8f0f-3ce2ab09c713.png)
#
# With the **`KernelDensity`** package, let's plot some KDEs with a variety of bandwidths
# using the default (Normal) kernel.  Note that the bandwidth choice has a very big effect
# on the shape of the estimated density.

using KernelDensity, StatsPlots

k = kde(y)

p = plot(k, label = "Bandwidth: Auto", w=2)

for bw in .1:.2:.9
    plot!(p, kde(y; bandwidth = bw), label = "Bandwidth: $bw", w=2)
end

histogram!(p, y, alpha = .1, normed=true)

# #### Averaged Shifted Histograms (ASH)
#
# The Averaged Shifted Histogram (ASH) estimator is a bit of a misnomer; It is essentially
# kernel density estimation performed on a fine-partition (many small bins) histogram.
#
# ASH has an advantage over KDEs in terms of performance since the density is calculated
# over the bins instead of all observations.  Similarly, ASH has an advantage over histograms
# in that smoothing the density over many small bins gives a more fine-grained view of the
# distribution.

using AverageShiftedHistograms

a = ash(y)

plot(a)

# The line is a bit noisy, but we can increase the amount of smoothing with parameter `m`

ash!(a; m = 15)

plot(a)

# But make sure you don't oversmooth!

ash!(a; m = 50)
plot(a)

# Let's compare all three of our density estimates.

plot(ash!(a, m = 15), hist=false)
histogram!(y, normed=true, alpha=.2, label = "Histogram")
plot!(k, label = "KDE")

# #### Categorical Data

using OnlineStats

y = rand([1,2,2,3,3,3,4,4,5], 10^5)

o = fit!(CountMap(Int), y)

plot(o)

# ### Bivariate Relationships
#
# So far, we've only looked at univariate distributions.  However, we can also use
# Histograms, KDEs, and ASH estimators on bivariate data.

x = randn(10^4)
y = x .+ randn(10^4)

h = fit(Histogram, (x,y); closed = :left)
k = kde((x,y))
a = ash(x, y)

plot(
    plot(h; title="Histogram"),
    plot(k; title="KDE"),
    plot(a; title = "ASH");
    layout=3
)

# ## Parametric Density Estimation
#
# Suppose you already know some things about a dataset's distribution or you want to see
# how well the data fits a specific distribution.  Here we'll use the **`Distributions`**
# package to fit data to distributions using maximum likelihood estimation (MLE).  We
# also need the **`StatsPlots`** package for plotting the distributions.

using Distributions

# In **`Distributions`**, each distribution is its own type, and there are a variety of
# methods that work for any distribution

for dist in [Normal(0,1), Gamma(5, 1), Poisson(5)]
    println("The probability density function for $dist evaluated at 0 is $(pdf(dist, 0))")
end

# The `fit` function can be used to pick a reasonable method of fitting the distribution
# (typically MLE).  MLE can be explicitly chosen using the `fit_mle` function.

y = 20 .+ randn(1000)

histogram(y, normed=true, label = "Histogram", xlim = extrema(y))

plot!(fit(Normal, y), label = "Normal Fit")
plot!(fit(Gamma, y), label = "Gamma Fit")


"""
    distance(x, y)

Computes euclidian distance between `x` and `y`.
"""
distance(x, y) = norm(x .- y, 2)

"""
    direction(x, y; normed = true)

Computes direction from `x` to `y`.
"""
function direction(x, y; normed = true)
    d = y .- x
    return normed ? d./norm(d) : d
end

"""
    direction_angle(d)

Computes angle in radians of the direction `d`.
"""
direction_angle(d) = atan(d[2], d[1])

"""
    direction_angle(x, y)

Computes angle in radians of the direction from `x` to `y`.
"""
direction_angle(x, y) = atan(direction(x, y))

"""
    euclidian(r::Real, ϕ::Real, origin = (0, 0))

Computes euclidian coordinates from the given polar coordinates coordinates `(r, ϕ)`.
"""
function euclidian(r::Real, ϕ::Real, origin = (0, 0))
    r >= 0 || throw(ArgumentError("r must be greater or equal to 0."))
    ϕ = mod1(ϕ + π, 2π) - π
    return origin .+ r .* (cos(ϕ), sin(ϕ))
end

"""
    polar(x::Real, y::Real)

Computes polar coordinates from the given 2D euclidian coordinates `(x, y)`.
"""
polar(x::Real, y::Real) = (hypot(x, y), atan(y, x))


function DolittleLU(A::Array; pivot=true)

  #A = the matrix of coefficients 'A' must be squared
  #B = The Vector 'B' right hand side vector Vertical
    m,n =size(A)

    if m != n
        error("Matrix must be squared")
    end

    # System of equations
    U = A
    P=Matrix{Float64}(I,m,m)
    p=zeros(m)
    L=zeros(m,m)
    #Forward Elimination
    for j=1:m-1
        if pivot==true
        k=argmax(abs.(U[j:m,j]))   #Find the Max value in the colum k to change the order
        Ind=j+k-1                  #Indicates de index of the row to change
        U[[j,Ind],:]=U[[Ind,j],:]  #Change the order of rows
        P[[j,Ind],:]=P[[Ind,j],:]
        L[[j,Ind],:]=L[[Ind,j],:]
        end
       for i=j+1:m

            f=U[i,j]/U[j,j]
            L[i,j]=f
            U[i,:]=U[i,:].-f.*U[j,:]
        end
    end
    for i=1:m
        L[i,i]=1
        p[i]=argmax(P[i,:])
    end
    U=UpperTriangular(U)
    L=LowerTriangular(L)

    return L,U, P, p
end


"""
A Julia Package for handling Spherical Geometry. Spherical geometry is the
geometry of the two-dimensional surface of a sphere. This package only
handles geometries generated using great circle sections (arcs).

Angles are specified in [deg].

Types:
* Point: specified by latitude ϕ [deg] and longitude λ [deg]
* Arc: specified as the shortest great circle between two points.
* Line: is a great circle line specified by a point and azimuth [deg]
* Arcs: is a string of continuous line sections defined by a set of points.
* Polygon: is a spherical polygon defined by a set of points.

It includes the calculation of:
* The angular distance to a point, line, arc, multi-arc, or polygon border
* The along line angular distance between a point and a line.
* The intersection points between lines, arcs, multi-arcs, and polygon borders.
* The self intersection points of multi-arcs and polygon borders
* The bounding box of a given polygon or set of arcs.
* The convexhull of a given polygon.
* The normalized point
* The (final) azimuth [deg] between two points
* The spherical angle [deg] and spherical excess [deg] between three points
* The midpoint between two points, of an arc, of an arcs
* The intermediate point at a given fraction between two points, of an arc, of an arcs
* The destination point given a start point, a direction and distance.
* The intersection points of arcs, and polygons
* The self intersection points of arcs or a polygon
* The highest/lowest latitude (point) of a great circle
* The area of a polygon/spherical triangle given a radius

And testing if:
* a point, arc, arcs or polygon is inside a polygon
* a point is on a line, arc, arcs or polygon border (within tolerance)
* a polygon/arcs is self-isselfintersecting
* a polygon is simple, complex, convex or concave
"""
module SphericalGeometry

import Base:(-)
import Base:(*)
import Base.Math.rad2deg, Base.Math.deg2rad
import LinearAlgebra.cross, LinearAlgebra.dot, LinearAlgebra.norm

include("types.jl")
include("utility.jl")
include("angles.jl")
include("distances.jl")
include("points.jl")
include("operations.jl")
include("area.jl")
end

#TODO Check https://spacetelescope.github.io/spherical_geometry/index.html for additional functions
#TODO Intersection ∩ of polygons
#TODO Union ∪ of polygons
#TODO Substraction - of polygons
#TODO isclockwise
#TODO iscounterclockwise


# Patrick Keast, MODERATE-DEGREE TETRAHEDRAL QUADRATURE FORMULAS
# http://mech.fsv.cvut.cz/oofem/resources/doc/oofemrefman/gaussintegrationrule_8C_source.html

function (::Type{QuadratureRule{Tetrahedron}})(quad_type::Gauss, order::Int)
    if order == 0 || order == 1
        points = [Tensors.Vec{3}([1.0/4.0, 1.0/4.0, 1.0/1.4])]
        weigths = [1.0/6.0]
    elseif order == 2
        a = ( 5. + 3. * √(5.) ) / 20.
        b = ( 5. - √(5.) ) / 20.
        w = 1. / 24.
        points = [Tensors.Vec{3}([a,b,b]),
                  Tensors.Vec{3}([b,a,b]),
                  Tensors.Vec{3}([b,b,a]),
                  Tensors.Vec{3}([b,b,b])]
        weigths = [w,w,w,w]
    elseif order == 3
        a1 = 1. / 4.
        a2 = 1. / 2.
        b2 = 1. / 6.
        w1 = -2. / 15.
        w2 = 3. / 40.
        points = [Tensors.Vec{3}([a1,a1,a1]),
                  Tensors.Vec{3}([a2,b2,b2]),
                  Tensors.Vec{3}([b2,a2,b2]),
                  Tensors.Vec{3}([b2,b2,a2]),
                  Tensors.Vec{3}([b2,b2,b2])]
        weigths = [w1,w2,w2,w2,w2]
    elseif order == 4
        a1 = 1. / 4.;
        w1 = -74. / 5625.;

        a2 = 5. / 70.;
        b2 = 11. / 14.;
        w2 = 343. / 45000.;

        a3 = ( 1. + √(5. / 14.) ) / 4.;
        b3 = ( 1. - √(5. / 14.) ) / 4.;
        w3 = 28. / 1125.;
        points = [Tensors.Vec{3}([a1,a1,a1]),
                  Tensors.Vec{3}([b2,a2,a2]),
                  Tensors.Vec{3}([a2,b2,a2]),
                  Tensors.Vec{3}([a2,a2,b2]),
                  Tensors.Vec{3}([a2,a2,a2]),
                  Tensors.Vec{3}([a3,a3,b3]),
                  Tensors.Vec{3}([a3,b3,a3]),
                  Tensors.Vec{3}([a3,b3,b3]),
                  Tensors.Vec{3}([b3,a3,a3]),
                  Tensors.Vec{3}([b3,a3,b3]),
                  Tensors.Vec{3}([b3,b3,a3])]

        weigths = [w1,w2,w2,w2,w2,w3,w3,w3,w3,w3,w3]
    else
        throw(ArgumentError("Gauss rule of order $order for tetrahedron not available"))
    end
    return QuadratureRule{Tetrahedron,3,Float64}(weigths, points)
end


module MySignalProcessing|

# Write your package code here.

# Dependancies
include("types.jl")
using .types: signal

# imports

# Importiong packages that will then be extended.
import Base.+
import Base.*
import DSP

#exports
export impseq
export signal
export stepseq
export sigshift
export realexp
export sigadd
export sigmult
export sigrand
export sigscale
export sigfold
export energy
export +
export *
export sinseq
export sinseqmn
export conv
export cexp

"""
    δ(start::Int, stop::Int)

Unit impulse signal
This function has the form of :

    δ(n) = 1 if n=0
         = 0 if n!=0

# parameters
    `start:: Int` : start of the impulse sequence n (horizontal axis)
    `stop:: Int` : stop of the impulse sequence n (horizontal axis)

# returns
    s::signal
"""
function impseq(start::Int, stop::Int)
    s = signal(0.0, start,stop); #constructor for initial signal

    for x in 1:length(s.n)

        s.A[x] = Float64(s.n[x] == 0) # elegant way to return 1

    end

    return s

end

"""
    stepseq(start::Int, stop::Int)

Unit Step Sequence
This function has the form of :

    stepseq(n) = 1 if n ≥ 0
                = 0 if n < 0

# parameters
    `start:: Int` : start of the sampple position n (horizontal axis)
    `stop:: Int` : stop of the sample position n (horizontal axis)

# returns
    s::signal

"""
function stepseq(start::Int, stop::Int)
    s = signal(0.0, start,stop); #constructor for initial signal

    for x in 1:length(s.n)

        s.A[x] = Float64(s.n[x] >= 0) # elegant way to return 1

    end

    return s

end

"""
    _sigshift(s::signal, m::Int)

Function to shift signal s by adding m to each element of s.n
It implements y(n) = x(n-n₀)

# parameters
    `s::signal` : signal to be shifted
    `n₀::Int` : shift amount

# returns
    s'::signal shifted
"""
function _sigshift(s::signal, n₀::Int)
    y = s.A;
    n = s.n .+ n₀;
    s = signal(y, n);
    return s

end

"""
    sigshift(n₀::Int)

# parameters
    `n₀::Int` : shift amount

# returns
    s:: anonimous  function that shifts a signal (designed to be composable)

"""
function sigshift(n₀::Int)

    return (sig -> _sigshift(sig, n₀))

end

"""
    realexp(a::Real, n₀::Int , nf::Int)

Function to generate a real exponential signal.

Signal of the form:
```math
r(a, n) = a^n
```

# parameters
    `a::Real` : real-valued parameter of the exponential
    `n₀::Int` : start  the sample positon signal
    `nf::Int` : stop  of the sample positon  signal

# returns
    s::signal

 """
function realexp(a::Real, n₀::Int, nf::Int)
    s = signal(0.0, n₀, nf); #constructor for initial signal

    for x in 1:length(s.n)

        s.A[x] = a^(s.n[x])

    end

    return s

end

"""
    sigadd(s1::signal, s2::signal)

Function to add two signal s1 and s2.
pls note that the signals must have the same length
If sequences are of unequal lenghts or if the sample positions are
    different for equal-length sequences, the we cannot directly add
        We have to first augment x1(n) and x2(n) , so that they have the same
         position vector n (and hence the same length)

# parameters
    `s1::signal` : signal to be added
    `s2::signal` : signal to be added

# returns
    `s::signal` : signal s1 + s2

"""
function sigadd(s₁::signal  , s₂::signal)

    n₀ =min(minimum(s₁.n), minimum(s₂.n)) # find the minimum sample position
    nf = max(maximum(s₁.n), maximum(s₂.n)) # find the maximum sample position
    n=n₀:nf; # create the sample position vector

    y₁ = signal(0.0, n₀, nf); #constructor for initial signal
    y₂ = signal(0.0, n₀, nf); #constructor for initial signal

    filter1 = findall((n .≥ minimum(s₁.n)) .& (n .≤ maximum(s₁.n)) .== 1)
    filter2 = findall((n .≥ minimum(s₂.n)) .& (n .≤ maximum(s₂.n)) .== 1)

    y₁.A[filter1] = s₁.A; # fill the signal with the values of s₁
    y₂.A[filter2] = s₂.A; # fill the signal with the values of s₂

    s = signal(0.0, n₀, nf); #constructor for initial signal
    s.A = y₁.A + y₂.A; # add the two signals

    return s

end

"""
    Base.+(s₁::signal  , s₂::signal)

Extends Base.+ to add two signal objects together.

# Arguments

    `s₁::signal` : signal to be added
    `s₂::signal` : signal to be added

# Returns
    `s::signal` : signal s1 + s2
"""
function +(s₁::signal  , s₂::signal)
    s = sigadd(s₁ , s₂)
    return s
end

"""
    sigmult(s1::signal, s2::signal)

Function to multiply two signal s1 and s2.
pls note that the signals must have the same length
If sequences are of unequal lenghts or if the sample positions are
    different for equal-length sequences, the we cannot directly add
        We have to first augment x1(n) and x2(n) , so that they have the same
         position vector n (and hence the same length)

# parameters
    `s1::signal` : signal to be added
    `s2::signal` : signal to be added

# returns
    `s::signal` : signal s1 * s2

"""
function sigmult(s₁::signal  , s₂::signal)

    n₀ =min(minimum(s₁.n), minimum(s₂.n)) # find the minimum sample position
    nf = max(maximum(s₁.n), maximum(s₂.n)) # find the maximum sample position
    n=n₀:nf; # create the sample position vector

    y₁ = signal(0.0, n₀, nf); #constructor for initial signal
    y₂ = signal(0.0, n₀, nf); #constructor for initial signal

    filter1 = findall((n .≥ minimum(s₁.n)) .& (n .≤ maximum(s₁.n)) .== 1)
    filter2 = findall((n .≥ minimum(s₂.n)) .& (n .≤ maximum(s₂.n)) .== 1)

    y₁.A[filter1] = s₁.A; # fill the signal with the values of s₁
    y₂.A[filter2] = s₂.A; # fill the signal with the values of s₂

    s = signal(0.0, n₀, nf); #constructor for initial signal
    s.A = y₁.A .* y₂.A; # add the two signals

    return s

end

"""
    *(s₁::signal  , s₂::signal)

Multiples two signal objects together.

# Arguments

    `s₁::signal` : signal to be multiplied
    `s₂::signal` : signal to be Multiples

# Returns
    `s::signal` : signal s1 * s2

"""
function *(s₁::signal  , s₂::signal)
    s = sigmult(s₁ , s₂)
    return s
end

"""
    sigrand(start::Int, stop::Int)

Function to generate a random signal starting at sample position start and ending at sample position stop.
The function generates a length N random sequence whose elements are uniformely distributed between 0 and 1.

# parameters
    `start::Int` : start  the sample positon signal
    `stop::Int` : stop  of the sample positon  signal

# returns
    s::signal

"""
function sigrand(start::Int, stop::Int)
    n = start:stop |> collect; # create the sample position vector
    y = rand.(length(n)); # generate a random sequence
    s = signal(y, n); #constructor for initial signal
    return s

end

"""
    _sigscale(s::signal, α::Real)

Each signal sample is multiple by a scalar α

# parameters
    `s::signal` : signal to be scaled
    `α::Real` : scalar

# returns
    `s::signal` : scaled signal
"""
function _sigscale(s::signal, α::Real)

    s1 = signal(s.A .* α, s.n); #constructor for initial signal

    return s1

end

"""
    sigscale(α::Real)

Each signal sample is multiple by a scalar α

# parameters
    `α::Real` : scalar

# returns
    anonimous function that takes a signal as argument and returns
         scaled signal (designed to be composable)

"""
function sigscale(α::Real)

    return (sig -> _sigscale(sig, α))
end

"""
    *(s₁::signal  , α::Real)

    scale signal by scalar.

# Arguments

    `s₁::signal` : signal to be multiplied
    `α::Real` : scalar

# Returns
    `s::signal` : signal s1 * α

"""
function *(s₁::signal  , α::Real)
    s = s₁ |> sigscale(α);
    return s
end

"""
    *(α::Real, s₁::signal)

scale signal by scalar.

# Arguments

    `s₁::signal` : signal to be multiplied
    `α::Real` : scalar

# Returns
    `s::signal` : signal s1 * α

"""
function *(α::Real, s₁::signal)
    s = s₁ |> sigscale(α);
    return s
end

"""
    sigfold(s::signal)

In this operation, each sample of x(n) is flipped around n=0 to obtain a folded sequence  y(n)

y(n) = {x(-n)}

# parameters
    `s::signal` : signal to be folded

# returns
    `s::signal` : folded signal

"""
function  sigfold(s::signal)
    s1 = signal(s.A, s.n); #constructor for initial signal
    s1.A = reverse(s.A);
    s1.n = -reverse(s.n);
    return s1
end

"""
    energy(s::signal)

Calculate the energy of a signal
The energy of a signal is given by the sum of the
    absolute squared values of the signal samples

# parameters
    `s::signal` : input signal

# returns
    `e::Real` : energy of the signal

"""
function energy(s::signal)
    e = sum(abs.(s.A) .^ 2);
    return e
end

"""
    sinSeq(start::Int, stop::Int, ω::Real, φ::Real)

Sinusoidal function

# parameters
    `start::Int` : start  the sample positon signal
    `stop::Int` : stop  of the sample positon  signal
    `ω::Real` : frequency of the sinusoid in radians
    `φ::Real` : phase of the sinusoid, in radians

# returns
    s::sinusoidal signal

"""
function sinseq(start::Int, stop::Int, ω::Real, φ::Real= 0.0)
    n = start:stop |> collect; # create the sample position vector
    y = sin.(n .* ω .+ φ); # generate a sinusoidal sequence
    s = signal(y, n); #constructor for initial signal
    return s
end

"""
    sinSeq(start::Int, stop::Int, M::Int,N::Int, φ::Real)

Sinusoidal function with more specific constructor

# parameters
    `start::Int` : start  the sample positon signal
    `stop::Int` : stop  of the sample positon  signal
    `M::Int` : multiple of 2π
    `N::Int` : number of periods before pattern repeats itself

# returns
    s::sinusoidal signal

"""
function sinseqmn(start::Int, stop::Int, M::Int,N::Int, φ::Real= 0.0)
    n = start:stop |> collect; # create the sample position vector
    y = sin.(n .* ((M / N)*2π .+ φ)); # generate a sinusoidal sequence
    s = signal(y, n); #constructor for initial signal
    return s
end

"""
    conv(s₁::signal, s₂::signal)

Convolution function

# parameters
    `s₁::signal` : input signal
    `s₂::signal` : impulse response signal

# returns
    s::convolution signal output

"""
function conv(s₁::signal, s₂::signal)
    n₁ = s₁.n;
    n₂ = s₂.n;
    A₁ = convert(Array{Float64,1}, s₁.A) #conv requires more specific type
    A₂ = convert(Array{Float64,1}, s₂.A) #conv requires more specific type

    Nyb = n₁[1] + n₂[1];
    Nye = n₁[length(A₁)] + n₂[length(A₂)];

    ny = collect(Nyb: Nye);
    y = DSP.conv(A₁, A₂);

    return signal(y, ny);
  end

  """
  function cexp(A::Vector, k, N)

      params:
          - A: Vector of amplitudes
          - n: element index within each Vector, from 0 .. N-1
          - k: index indicating each vector , from 0 .. N-1
          - N: dimension of signal (number of vectors)

      returns:
          - complex exponential signal

  """
  function cexp(A::Vector, k, N = 64)

      n = [n for n in range(0, N-1)]

      w = [exp(((2pi/64)*im)*n*k) for n in range(0, N-1)]
      s = A .*w

      return signal(s, n)

  end

end


module GridCreation

export CFFT, CInterSample, mkCartVecs, cartVecsToPolarGrid

const CFFT = -1;
const CInterSample = -2;

# this function is lifted from pkg Fourier.
# A little copying is better than a little dependency.
function _fftrange(n)
    if iseven(n)
        return -(n ÷ 2):(n÷2-1)
    end
    return -(n ÷ 2):(n÷2)
end
"""
    mkCartVecs(dx, size[, center])

Create a 2D "grid" by returning a row vector x and column vector y.

The grid will have sample spacing dx, size[0] == size(x), and size[1] == size(y).

If size is an int, it is broadcast to [size,size].

The grid will be centered according to such that the center element contains zero.  Special
values for center include -1 (default, FFT-like center) and -2, for "interpixel"
sampling, where 0 is at size/2.  Otherwise, zero is at the centerth element.

The centering rule may not be different between x and y.

# Examples

## FFT-aligned grid

Note that the ceil rounded center element contains zero.
```julia-repl
julia> x,y = mkCartVecs(.1, 4); # implicit center=CFFT
julia> x
1×4 LinearAlgebra.Adjoint{Float64,StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}}}:
 -0.2  -0.1  0.0  0.1
julia> collect(y)
4-element Array{Float64,1}:
 -0.2
 -0.1
  0.0
  0.1
```

## Intersample aligned grid
```julia-repl
x,y=mkCartVecs(.1, 4, center=CInterSample);

julia> x
1×4 LinearAlgebra.Adjoint{Float64,StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}}}:
 -0.15  -0.05  0.05  0.15
```

## index-centered grid
```
julia> x,y=mkCartVecs(.1, 4, center=1);
julia> x
1×4 LinearAlgebra.Adjoint{Float64,StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}}}:
 1.11022e-17  0.1  0.2  0.3
```
"""
function mkCartVecs(dx, size::Tuple{Integer,Integer}; center::Integer=CFFT)
    # fftrange produces a range object.  dot add shifts the range
    # and multiplication scales it.
    if center == CFFT
        X = (_fftrange(size[2])*dx)';
        Y = _fftrange(size[1])*dx;
    elseif center == CInterSample
        X = (_fftrange(size[2])*dx.+(dx/2))';
        Y = (_fftrange(size[1])*dx.+(dx/2));
    else
        X = _fftrange(size[2])*dx;
        Y = _fftrange(size[1])*dx;
        X = (X .- X[center])';
        Y = (Y .- Y[center]);
    end
    return X, Y;
end

function mkCartVecs(dx, size::Integer; center::Integer=CFFT)
    return mkCartVecs(dx, (size,size), center=center);
end

"""
    cartVecsToPolarGrid(X, Y)

Construct a polar grid (ρ,θ) from the row vector X and column vector Y.  Returns
a pair of 2D arrays containing the radial and azimuthal coordinates.

# Examples
```julia-repl
julia> X=(-2:1)'; Y=-2:1;
julia> ρ,θ=cartVecsToPolarGrid(X,Y);
julia> ρ
4×4 Array{Float64,2}:
 2.82843  2.23607  2.0  2.23607
 2.23607  1.41421  1.0  1.41421
 2.0      1.0      0.0  1.0
 2.23607  1.41421  1.0  1.41421
julia> θ
4×4 Array{Float64,2}:
 -2.35619  -2.03444  -1.5708  -1.10715
 -2.67795  -2.35619  -1.5708  -0.785398
  3.14159   3.14159   0.0      0.0
  2.67795   2.35619   1.5708   0.785398
```
See also: [`mkCartVecs`](@ref)
"""
function cartVecsToPolarGrid(X, Y)
    # I don't think allocating a meshgrid is avoidable here.
    # the rho matrix can be made with a dot on x and y,
    # but we would need a 2D view into both X and Y for atan
    # grid_x = [x for x in X, y in Y;
    # grid_y = [y for y in Y, x in X;
    ρ = @. sqrt(X^2 + Y^2);
    θ = [atan(y,x) for y in Y, x in X'];
    return ρ, θ
end

end


export ricker, dampedSinusoid, sinc, spectSynth

"""

    ricker(;f::Float64=20.0, n::Int64=100, dt::Float64=0.001, nargout::Bool=false)

Generate a Ricker wavelet.

# Arguments
- `f::Float64,`    : Peak frequnecy
- `n::Int64`    : Number of points
- `dt::Float64`    : Sampling interval

# Output
- `s`::Array{Float64,1}    : Computed Ricker wavelet
- `t`::Array{Float64,1}    : Time axis

"""
function ricker(;f::Float64=20.0, n::Int64=41, dt::Float64=0.05, tpoint::Float64=100.0)
    # generate time axis
    t = timeAxis(dt, 0., n)
    # create the wavelet
    s = (1.0 .- 2π^2 * f^2 .* (t.-tpoint).^2).*exp.(-π^2 * f^2 .* (t.-tpoint).^2)

    return (s, t)
end

"""

    dampedSinusoid(;A::Float64=1.0, ω::Float64=1.0, ϕ::Float64=0.0, n::Int64=100, dt::Float64=0.001, λ::Float64=2.0)

Generate a damped sinusoid.

# Arguments
- `A::Float64`    : Amplitude of the damped sinusoid
- `n::Int64`    : Number of points
- `dt::Float64`    : Sampling interval
- `ω::Float64`    : Frequency
- `λ::Float64`    : Decay constant
- `ϕ::Float64`    : Phase angle at t=0
- `η::Float64`    : Shift in function maximum

# Output
- `s::Array{Float64,1}`    : Computed damped sinusoid
- `t::Array{Float64,1}`    : Time axis

"""
function dampedSinusoid(;A::Float64=1.0, ω::Float64=1.0, ϕ::Float64=0.0, η::Float64=0.0, n::Int64=100, dt::Float64=0.001, t0::Float64=0.0, λ::Float64=2.0)
    # generate time axis
    t = timeAxis(dt, t0, n)
    # damped sinusoidal function
    s = A*exp.(-λ*abs.(t.-η)) .* cos.(ω*t .+ ϕ)

    return (s, t)
end

"""

    sinc(;A::Float64=1.0, ω::Float64=1.0, ϕ::Float64=0.0, n::Int64=100, dt::Float64=0.001, t0=0.0)

Generate a sinc (cardinal-sin) function

# Arguments
- `A::Float64`    : Amplitude of the damped sinusoid
- `n::Int64`    : Number of points
- `dt::Float64`    : Sampling interval
- `ω::Float64`    : Frequency
- `ϕ::Float64`    : Phase angle at t=0

# Output
- `s::Array{Float64,1}`    : Computed sinc
- `t::Array{Float64,1}`    : Time axis

"""
function sinc(;A::Float64=1.0, ω::Float64=1.0, ϕ::Float64=0.0, n::Int64=100, dt::Float64=0.001, t0::Float64=0.0)
    # generate time axis
    t = timeAxis(dt, t0, n)
    # sinc function
    s = A.*Base.sinc.(ω.*t.+ϕ)

    return (s, t)
end

"""

    chirp(;c::Array{Float64,1}=collect(range(0.15, stop=15.0, length=100)), tp::Array{Float64,1}=collect(range(0.04, stop=4.0, length=100)), mintp::Float64=0.04, maxtp::Float64=4.0, dist::Float64=1000.0, n::Int64=500, dt::Float64=0.002, t0::Float64=0.0)

# Arguments
- `c::Array{Float64,1}`    : phase velocity array
- `tp::Array{Float64,1}`    : period array
- `mintp::Float64`    : minimum period
- `maxtp::Float64`    : maximum period
- `dist::Float64`    : distance
- `n::Int64`    : number of samples in arrays
- `dt::Float64`    : sampling interval
- `t0::Float64`    : start time of time vector

# Output
- `s::Array{Float64,1}`    : Computed signal
- `t::Array{Float64,1}`    : Time axis

"""
function chirp(;c::Array{Float64,1}=collect(range(0.15, stop=15.0, length=100)), tp::Array{Float64,1}=collect(range(0.04, stop=4.0, length=100)), mintp::Float64=0.04, maxtp::Float64=4.0, dist::Float64=1000.0, n::Int64=500, dt::Float64=0.002, t0::Float64=0.0)
    # generate time axis
    t = timeAxis(dt, t0, n)
    # generate a chirp
    tp_ind = findall(x->(x≤maxtp && x≥mintp), tp)
    s = [sum(cos.((2π ./ tp[tp_ind]) .* (j .- dist ./ c[tp_ind]))) for j in time]

    return (s, t)
end

"""

    spectSynth(;A::Float64=1.0, n::Int64=100, dt::Float64=0.001, t0=0.0)

Generate a synthetic by definition in the frequency domain and inverse FFT

# Arguments
- `A::Float64`    : Amplitude of the damped sinusoid
- `n::Int64`    : Number of points
- `dt::Float64`    : Sampling interval

# Output
- `s::Array{Float64,1}`    : Computed signal
- `t::Array{Float64,1}`    : Time axis

"""
function spectSynth(;A::Float64=1.0, n::Int64=100, dt::Float64=0.001, t0::Float64=0.0)
    # generate time axis
    t = timeAxis(dt, t0, n)
    # generate a signal with flat amplitude spectrum and random phase in [-π, π]
    # this is of length (n/2)+1 to get a final signal of length n
    N = convert(Int64, floor(length(t)/2)+2)
    spectrum = A.+(((rand(N).-0.5).*2π).*im)
    # inverse FFT
    s = real(irfft(spectrum, n+1))

    return (s[2:end], t)
end

"""

    timeAxis(dt::Float64, t0::Float64, n::Float64)

Create a vector of times given sample frequency, number of points, and start time.

# Arguments
- `dt::Float64`    : Sample interval
- `t0::Int64`    : Start time
- `n::Int64`    : Number of points in the time vector

# Output
- `t::Array{Float64,1}`    : Time axis

"""
function timeAxis(dt::Float64, t0::Float64, n::Int64)
    T = dt * (n-1) # end time
    # create time axis from 0 to end time at dt spacing
    t = collect(0:dt:T) .+ t0

    return t
end


#!/usr/bin/env julia

"""
# Problem 4: Largest palindrome product

The prime factors of 13195 are 5, 7, 13 and 29.

A palindromic number reads the same both ways. The largest palindrome made from
the product of two 2-digit numbers is 9009 = 91 × 99.

Find the largest palindrome made from the product of two `d`-digit numbers.
"""
module Problem004

include("Digits.jl")

"""
    solve(; d::Integer = 3)

Returns the solution for [`Problem004`](@ref) with the given parameters.

## Preconditions
- `d > 0`
"""
function solve(; d::Integer = 3)
    # calculate max and min d-digit numbers
    base = convert(typeof(d), 10)
    max_factor = base^d - oneunit(d)
    min_factor = base^(d - oneunit(d))

    # search for palindromic products of d-digit numbers
    max_product = -oneunit(d)
    for n in max_factor:-oneunit(d):min_factor
        for m in n:-oneunit(d):min_factor
            product = n * m

            # stop checking products with n when we drop below max_product
            if max_product ≥ product
                break
            end

            # if product is a palindrome, it's the max possible for this n
            if Digits.is_palindrome(product)
                max_product = product
                break
            end
        end
    end

    return max_product
end

end


"""
NumDiffTools.gradient(f, x::AbstractArray, [abstol, reltol, h, t, maxiters])

Returns gradient `∇f` of `f` evaluted at `x`, assuming `f` is called as `f(x)`.

Optional Arguments:\n
`abstol`: Target absolute error in derivative estimate (default `1e-12`)\n
`reltol`: Target relative error in derivative estimate (default `1e-12`)\n
`h`: Initial step size (default `0.1`)\n
`t`: Ratio of step sizes for each Richardson iteration (default `2.0001`.  Should not be an exact integer for periodic functions)\n
`maxiters`: Maximum number of Richardson iterations to perform (default `6`)\n
"""
function gradient(f, x::AbstractArray; abstol::Real = 1e-12, reltol::Real = 1e-12, h::Real = 0.1, t::Real = 2.0001, maxiters::Integer = 6)
    fx = f(x)

    # Initialize Work Arrays
    A1 = Vector{typeof(fx)}(undef, maxiters)
    A2 = Vector{typeof(fx)}(undef, maxiters)

    # Vector of step sizes
    hh = [h/t^(i - 1) for i = 1:maxiters]
    # Vector of orders
    k = 1:maxiters

    result = Vector{typeof(fx)}(undef, length(x))
    dx = zero(x)
    for i = 1:length(x)
        dx[i] = 1

        result[i] = extrapolate(h->(f(x + h*dx) - fx)/h,          # Function to extrapolate
                                A1, A2,                           # Work Arrays
                                hh, k,                            # Sequence of step sizes (h)
                                abstol, reltol                    # tolerances
                                )
        dx[i] = 0
    end
    result
end

"""
NumDiffTools.gradient!(result::AbstractArray, f, x::AbstractArray, [abstol, reltol, h, t, maxiters])

Stores gradient `∇f` of `f` evaluted at `x` in `result`, assuming `f` is called as `f(x)`.

Optional Arguments:\n
`abstol`: Target absolute error in derivative estimate (default `1e-12`)\n
`reltol`: Target relative error in derivative estimate (default `1e-12`)\n
`h`: Initial step size (default `0.1`)\n
`t`: Ratio of step sizes for each Richardson iteration (default `2.0001`.  Should not be an exact integer for periodic functions)\n
`maxiters`: Maximum number of Richardson iterations to perform (default `6`)\n
"""
function gradient!(result::AbstractArray, f, x::AbstractArray; abstol::Real = 1e-12, reltol::Real = 1e-12, h::Real = 0.1, t::Real = 2.0001, maxiters::Integer = 6)
    fx = f(x)

    # Initialize Work Arrays
    A1 = Vector{typeof(fx)}(undef, maxiters)
    A2 = Vector{typeof(fx)}(undef, maxiters)

    # Vector of step sizes
    hh = [h/t^(i - 1) for i = 1:maxiters]
    # Vector of orders
    k = 1:maxiters

    dx = zero(x)
    for i = 1:length(x)
        dx[i] = 1

        result[i] = extrapolate(h->(f(x + h*dx) - fx)/h,          # Function to extrapolate
                                A1, A2,                           # Work Arrays
                                hh, k,                            # Sequence of step sizes (h)
                                abstol, reltol                    # tolerances
                                )
        dx[i] = 0
    end
    nothing
end


export Chebyshev

immutable Chebyshev <: ParametricFamily
    n::Int
end

domain(::Chebyshev) = -1..1

points(c::Chebyshev, T = Float64) = cospi.(((c.n:-1:1)-T(0.5))/c.n)

degf(c::Chebyshev) = c.n

"Recurrence relation for Chebyshev polynomials."
@inline _chebyshev_recurrence(x, T₋₁, T₋₂) = 2*x*T₋₁ - T₋₂, T₋₁

"Initialize T₋₁, T₋₂ for the Chebyshev recurrence."
@inline _chebyshev_recurrence_init{T}(x::T) = x, one(T)

macro _chebyshev_recurrence!(x, T₋₁, T₋₂)
    T₋₁ = esc(T₋₁)
    T₋₂ = esc(T₋₂)
    :(($(T₋₁), $(T₋₂)) = _chebyshev_recurrence($(esc(x)), $(T₋₁), $(T₋₂)))
end

function basis!{T}(c::Chebyshev, x::T, b::AbstractVector{T})
    @argcheck c.n == length(b)
    T₋₁, T₋₂ = _chebyshev_recurrence_init(x)
    for i in 1:c.n
        if i == 1
            b[i] = T₋₂
        else
            if i > 2
                @_chebyshev_recurrence! x T₋₁ T₋₂
            end
            b[i] = T₋₁
        end
    end
    b
end

# note: after benchmarking, it was found that this is faster than Clenshaw,
# and at least as accurate.
function evaluate{T}(c::Chebyshev, θ, x::T)
    T₋₁, T₋₂ = _chebyshev_recurrence_init(x)
    value = zero(T)
    for i in 1:c.n
        if i == 1
            value += T₋₂*θ[i]
        else
            if i > 2
                @_chebyshev_recurrence! x T₋₁ T₋₂
            end
            value += T₋₁*θ[i]
        end
    end
    value
end

function evaluate{T}(c::Chebyshev, θ, vp::ValuePartial{T})
    ## if T(x) = 2xT₋₁(x) - T₋₂(x)
    ## then T′(x) = 2T₋₁(x) + 2xT′₋₁(x) - T′₋₂(x), with T′₂ = 1 and T′₁ = 0
    x = vp.x
    value = partial = zero(T)
    T₋₁ = x
    T₋₂ = T′₋₁ = one(T)
    T′₋₂ = zero(T)
    for i in 1:c.n
        if i == 1
            value += T₋₂*θ[i]
        else
            if i > 2
                @_chebyshev_recurrence! x T′₋₁ T′₋₂
                T′₋₁ += 2*T₋₁
                @_chebyshev_recurrence! x T₋₁ T₋₂
            end
            value += T₋₁*θ[i]
            partial += T′₋₁*θ[i]
        end
    end
    value, partial
end

evaluate{T}(c::Chebyshev, θ, x::Partial{T}) = evaluate(c, θ, ValuePartial(x.x))[2]

function fit!{T}(c::Chebyshev, ys::AbstractVector{T}, θ::AbstractVector{T})
    @argcheck c.n == length(ys) == length(θ)
    x = points(c, T)
    T₋₂ = ones(T, c.n)
    T₋₁ = copy(x)
    for i in 1:c.n
        if i == 1
            θ[1] = sum(ys) / c.n
        else
            if i > 2
                for j in 1:c.n
                    @_chebyshev_recurrence! x[j] T₋₁[j] T₋₂[j]
                end
            end
            θ[i] = dot(T₋₁, ys) *2 / c.n
        end
    end
    θ
end


"""
Implementation of a Rauch-Tung-Striebel smoother based on
Ch.8 of Bayesian Filtering & Smoothing (Särkka, 2014).

Author: Wouter Kouw
Last update: 03-07-2021
"""

using Distributions
using Random

function fixedinterval_rts_smoother(observations,
                                    transition_matrix,
                                    emission_matrix,
                                    process_noise,
                                    measurement_noise,
                                    state0)
    """
    Fixed-interval Rauch-Tung-Striebel smoother (Th. 8.2)

    This filter is built for a linear Gaussian dynamical system with known
    transition coefficients, process and measurement noise.
    """

    # Dimensionality
    Dx = size(process_noise,1)
    Dy = size(measurement_noise,1)

    # Recast process noise to matrix
    if Dx == 1
        if typeof(process_noise) != Array{Float64,2}
            process_noise = reshape([process_noise], 1, 1)
        end
        if typeof(measurement_noise) != Array{Float64,2}
            measurement_noise = reshape([measurement_noise], 1, 1)
        end
    end

    # Time horizon
    time_horizon = length(observations)

    # Initialize estimate arrays
    mk = zeros(Dx, time_horizon)
    Pk = zeros(Dx, Dx, time_horizon)

    # Initial state prior
    m_0, P_0 = state0

    # Start previous state variable
    m_kmin = m_0
    P_kmin = P_0

    "Forward pass"
    for k = 1:time_horizon

        # Forward prediction step
        m_k_pred = transition_matrix*m_kmin
        P_k_pred = transition_matrix*P_kmin*transition_matrix' .+ process_noise

        # Forward update step
        v_k = observations[:,k] .- emission_matrix*m_k_pred
        S_k = emission_matrix*P_k_pred*emission_matrix' .+ measurement_noise
        K_k = P_k_pred*emission_matrix'*inv(S_k)
        m_k = m_k_pred .+ K_k*v_k
        P_k = P_k_pred .- K_k*S_k*K_k'

        # Store estimates
        mk[:,k] = m_k
        Pk[:,:,k] = P_k

        # Update previous state variable
        m_kmin = m_k
        P_kmin = P_k

    end

    # Initialize smoothing estimate arrays
    msk = zeros(Dx, time_horizon)
    Psk = zeros(Dx, Dx, time_horizon)

    # Smoothed estimates at time horizon
    msk[:,time_horizon] = mk[:,time_horizon]
    Psk[:,:,time_horizon] = Pk[:,:,time_horizon]

    "Backward pass"
    for k = time_horizon-1:-1:1

        # Backward prediction
        m_kplus = transition_matrix * mk[:,k]
        P_kplus = transition_matrix * Pk[:,:,k] * transition_matrix' .+ process_noise

        # Backward update step
        G_k = Pk[:,:,k]*transition_matrix' * inv(P_kplus)
        msk[:,k] = mk[:,k] + G_k*(msk[:,k+1] - m_kplus)
        Psk[:,:,k] = Pk[:,:,k] + G_k*(Psk[:,:,k+1] - P_kplus)*G_k'

    end

    return msk, Psk
end


# ---
# title: 390. Elimination Game
# id: problem390
# author: Indigo
# date: 2021-06-20
# difficulty: Medium
# categories:
# link: <https://leetcode.com/problems/elimination-game/description/>
# hidden: true
# ---
#
# There is a list of sorted integers from 1 to _n_. Starting from left to right,
# remove the first number and every other number afterward until you reach the
# end of the list.
#
# Repeat the previous step again, but this time from right to left, remove the
# right most number and every other number from the remaining numbers.
#
# We keep repeating the steps again, alternating left to right and right to
# left, until a single number remains.
#
# Find the last number that remains starting with a list of length _n_.
#
# **Example:**
#
#
#
#     Input:
#     n = 9,
#     _1_ 2 _3_ 4 _5_ 6 _7_ 8 _9_
#     2 _4_ 6 _8_
#     _2_ 6
#     6
#
#     Output:
#     6
#
#
#
## @lc code=start
using LeetCode

last_remaining(n::Int)::Int = n == 1 ? 1 : 2 * (n ÷ 2 + 1 - last_remaining(n ÷ 2))

## @lc code=end


using SortingAlgorithms

function main(points)
  try
    ch_points = []
    ch_points = convex_hull!(points)

    output = [[],[],[],[]]

    for ch_point in ch_points
      push!(output[1], ch_point[1])
      push!(output[2], ch_point[2])

      deleteat!(points, findfirst(points, ch_point))
    end

    for point in points
      push!(output[3], point[1])
      push!(output[4], point[2])
    end

    return output
  catch e
    if isa(e, MethodError)
      return "Please provide array of tuples where each element is Int64"
    else
      return e
    end
  end
end

function convex_hull!(points::Array{Tuple{Int64,Int64},1})
  if size(points, 1) < 3
    return points
  end

  # Sort all points
  points = sort!(union(points), alg = HeapSort)

  # Arrays for top and bottom parts of convex hull
  calculated_ch_top = []
  calculated_ch_bottom = []

  # Build top part of convex hull
  build_convex_hull_part!(calculated_ch_top, points, 1:1:length(points))

  # Build bottom part of convex hull
  build_convex_hull_part!(calculated_ch_bottom, points, length(points):-1:1)

  # Remove duplicated points:
  # End of top part == beginning of the bottom part
  pop!(calculated_ch_top)

  # End of bottom part == beginning of the top part
  pop!(calculated_ch_bottom)

  # Top + Bottom parts == convex hull
  convex_hull = [calculated_ch_top; calculated_ch_bottom]

  return convex_hull
end

function counter_clockwise(p1, p2, p3)
  return ((p2[1] - p1[1]) * (p3[2] - p1[2])) - ((p2[2] - p1[2]) * (p3[1] - p1[1]))
end

function generate_points(number_of_points, range_min, range_max)
  points = [(0,0)]
  pop!(points)
  for i = 1:number_of_points
    generated_point = (rand(range_min:range_max), rand(range_min:range_max))
    push!(points, generated_point)
  end
  return points
end

function build_convex_hull_part!(hull_points, points_to_check, iterator_range)
  for i = iterator_range
    point = points_to_check[i]

    while length(hull_points) > 1 && counter_clockwise(hull_points[end-1], hull_points[end], point) >= 0
      pop!(hull_points)
    end

    push!(hull_points, point)
  end
end


# Isomap
# ------
# A Global Geometric Framework for Nonlinear Dimensionality Reduction,
# J. B. Tenenbaum, V. de Silva and J. C. Langford, Science 290 (5500): 2319-2323, 22 December 2000

#### Isomap type
struct Isomap{T <: Real} <: AbstractDimensionalityReduction
    k::Int
    model::KernelPCA
    component::AbstractVector{Int}

    Isomap{T}(k::Int, model::KernelPCA) where T = new(k, model)
    Isomap{T}(k::Int, model::KernelPCA, cc::AbstractVector{Int}) where T = new(k, model, cc)
end

## properties
outdim(R::Isomap) = outdim(R.model)
eigvals(R::Isomap) = principalvars(R.model)
neighbors(R::Isomap) = R.k
vertices(R::Isomap) = R.component

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

## interface functions
function fit(::Type{Isomap}, X::AbstractMatrix{T}; k::Int=12, maxoutdim::Int=2) where {T<:Real}
    # Construct NN graph
    D, E = find_nn(X, k)
    G, C = largest_component(SimpleWeightedGraph(adjmat(D,E)))

    # Compute shortest path for every point
    n = length(C)
    DD = zeros(T, n, n)
    for i in 1:n
        dj = dijkstra_shortest_paths(G, i)
        DD[i,:] = dj.dists
    end

    broadcast!(x->-x*x/2, DD, DD)
    broadcast!((x,y)->(x+y)/2, DD, DD, DD') # remove roundoff error
    M = fit(KernelPCA, DD, kernel=nothing, maxoutdim=maxoutdim)

    return Isomap{T}(k, M, C)
end

transform(R::Isomap) = transform(R.model)


"""
# Usage

    solve(prob::FODESystem, h, NonLinearAlg())

Nonlinear algorithm for nonlinear fractional differential equations.

### References

Dingyu Xue, Northeastern University, China ISBN:9787030543981
"""
struct NonLinearAlg <: FractionalDiffEqAlgorithm end

function solve(prob::FODESystem, h, ::NonLinearAlg, L0=1e10)
    @unpack f, α, u0, tspan = prob
    t0 = tspan[1]; T = tspan[2]
    n = length(u0)
    m = round(Int, (T-t0)/h)+1
    g = genfun(1)
    g = g[:]
    u0 = u0[:]
    ha = h.^α
    z = zeros(n, m)
    x1 = copy(u0) # Here we pass the value of x0 to x1. Honestly, I kept finding this bug for almost a whole night😅

    # All of the min(m, L0+1) is to set the memory effect.
    SetMemoryEffect = Int64(min(m, L0+1))
    W = zeros(n, SetMemoryEffect) #Initializing W a n*m matrix

    @fastmath @inbounds @simd for i = 1:n
        W[i, :] = getvec(α[i], SetMemoryEffect, g)
    end

    du = zeros(n)
    @fastmath @inbounds @simd for k = 2:m
        tk = t0+(k-1)*h
        L = min(Int64(k-1), Int64(L0))
        f(du, x1, nothing, tk)

        @fastmath @inbounds @simd for i = 1:n
            x1[i] = du[i]*ha[i] - W[i, 2:L+1]'*z[i, k-1:-1:k-L] + u0[i]
        end
        z[:, k] = x1 - u0
    end

    result = (z + repeat(u0, 1, m))'

    return result
end

"""
P-th precision polynomial generate function

```math
g_p(z)=\\sum_{k=1}^p \\frac{1}{k}(1-z)^k
```
"""
function genfun(p)
    a = collect(1:p+1)
    A = Vandermonde(a)'
    return (1 .-a')*inv(A')
end

function getvec(α, n, g)
    p = length(g)-1
    b = 1 + α
    g0 = g[1]
    w = Float64[]
    push!(w, g[1]^α)

    @fastmath @inbounds @simd for m = 2:p
        M = m-1
        dA = b/M
        temp = (-(g[2:m] .*collect((1-dA):-dA:(1-b))))' *w[M:-1:1]/g0
        push!(w, temp)
    end

    @fastmath @inbounds @simd for k = p+1:n
        M = k-1
        dA = b/M
        temp = (-(g[2:(p+1)] .*collect((1-dA):-dA:(1-p*dA))))' *w[M:-1:(k-p)]/g0
        push!(w, temp)
    end
    return w
end


"""Julia program to implement Exponential Search algorithm.
Exponential Search Algorithm is an optimized Binary Search to search an element in sorted array.
It is specifically used when the size of array is infinite.
"""

function binary_search(arr, low, n, ele)
    high = n
    while high >= low
        mid = low + (high - low) ÷ 2
        # If the mid element is the required element, return that index
        if(arr[mid] == ele)
            return mid
        # ele is greater than mid element then ele would be present in first half of the array
        elseif (arr[mid] > ele)
            high = mid - 1
        #Else if  ele is smaller than mid element then ele would be present in last half of the array
        else
            low = mid + 1
        end
    end
    # If the element is not found return 0
    return 0
end

function exponential_search(arr, n, ele)
    if(arr[1] == ele)
        return false
    end
    i = 2
    while( i < n && arr[i] <= ele)
        i = i * 2
    end
    mini = i < (n-1) ? i : (n-1)
    return binary_search(arr, i÷2, mini, ele )
end

print("How many numbers are present in the array? ")
n = readline()
n = parse(Int, n)
if (n <= 0)
    println("Array is Empty!!!")
    exit()
end
arr = Int[]
print("Enter the numbers: ")
arr = [parse(Int, num) for num in split(readline())]
print("Which number do you want to search in the array? ")
ele = readline()
ele = parse(Int, ele)
# Sort the array in ascending order
arr = sort(arr)
res = exponential_search(arr, n, ele)
if (res == 0)
    print("The number $ele is not present in the array")
else
    print("The number $ele is present in the array.")
end

"""
Time Complexity - O(log(n)), where 'n' is the size of the array
Space Complexity - O(n)

SAMPLE INPUT AND OUTPUT

SAMPLE I
How many numbers are present in the array? 5
Enter the numbers: 1 2 3 4 5
Which number do you want to search in the array? 6
The number 6 is not present in the array

SAMPLE II
How many numbers are present in the array? 3
Enter the numbers: 3 1 2
Which number do you want to search in the array? 2
The number 2 is present in the array.
"""


"""
Package: Smoothers

    sma(x, n, center=false)

Smooth a vector of data using a simple moving average

# Arguments
- `x`: Vector of data.
- `n`: Size of the moving average.
- `center`: returns a vector of the same size of x with missing values on the tails.

# Returns
Vector of moving average values

# Examples
```julia-repl
julia> sma(1:5,3)
3-element Vector{Float64}:
 2.0
 3.0
 4.0

julia> sma(1:5,3,true)
5-element Vector{Union{Missing, Float64}}:
  missing
 2.0
 3.0
 4.0
  missing
```
"""
@inline function sma(x::AbstractVector{T}, n::Integer, center::Bool=false) where T<:Real

    n == 1 && return x
    N = length(x)
    @assert 1 <= n <= N

    P = Base.promote_op(/, T, T)
    res = Vector{P}(undef, N-n+1)

    # initial moving average value
    res[1] = ma = sum(x[1:n])/n
    for i in 1:N-n
        @inbounds res[1+i] = ma += (x[n+i] - x[i]) / P(n)
    end

    center ? vcat(repeat([missing], n÷2),res,repeat([missing], n-n÷2-1)) : res

end


"""
    rv(X, Y)
Compute the sample RV coefficient between matrices X and Y
* `X` : Matrix (n obs., p variables).
* `Y` : Matrix (n obs., q variables).

RV is bounded in [0, 1].

A dissimilarty measure between `X` and `Y` can be computed
by d = sqrt(2 * (1 - RV)).

## References

Escoufier, Y., 1973. Le Traitement des Variables Vectorielles. Biometrics 29, 751–760.
https://doi.org/10.2307/2529140

Josse, J., Pagès, J., Husson, F., 2008. Testing the significance of the RV coefficient.
Computational Statistics & Data Analysis 53, 82–91. https://doi.org/10.1016/j.csda.2008.06.012

Mayer, C.-D., Lorent, J., Horgan, G.W., 2011. Exploratory Analysis of Multiple Omics
Datasets Using the Adjusted RV Coefficient. Statistical Applications in Genetics and Molecular
Biology 10. https://doi.org/10.2202/1544-6115.1540

Smilde, A.K., Kiers, H.A.L., Bijlsma, S., Rubingh, C.M., van Erk, M.J., 2009.
Matrix correlations for high-dimensional data: the modified RV-coefficient.
Bioinformatics 25, 401–405. https://doi.org/10.1093/bioinformatics/btn634

Robert, P., Escoufier, Y., 1976. A Unifying Tool for Linear Multivariate Statistical Methods:
The RV-Coefficient. Journal of the Royal Statistical Society: Series C (Applied Statistics)
25, 257–265. https://doi.org/10.2307/2347233
"""

function rv(X, Y; centr = true)
    X = ensure_mat(X)
    Y = ensure_mat(Y)
    if centr
        X = center(X, mean(X, dims = 1))
        Y = center(Y, mean(Y, dims = 1))
    end
    YtX = Y' * X
    XtX = X' * X
    YtY = Y' * Y
    A = dot(YtX, YtX)
    B = dot(XtX, XtX)
    C = dot(YtY, YtY)
    rv = A / sqrt(B * C)
end


# This file calculates the field strength at the center of a Faraday cage. Warning, the run-time is long.
# See /Laplace.jl for a more interactive version.
using ApproxFun, SingularIntegralEquations

z_0 = 2.0
ui(x,y) = logabs(complex(x,y)-z_0)
g1(x,y) = 1/2

function electricfield(N,r)
    # Set the domains.
    cr = exp(im*2π*[0:N-1]/N)
    crl = (1-2r)cr
    crr = (1+2r)cr
    dom = ∪(Segment.(crl,crr))
    #dom = ∪(Circle.(cr,ones(length(cr))r))
    #dom = ∪(Segment.(crl[1:2:end],crr[1:2:end]) ∪ ∪(Circle.(cr[2:2:end],ones(length(cr[2:2:end]))r))

    sp = Space(dom)
    cwsp = CauchyWeight(sp⊗sp,0)
    uiΓ,⨍ = Fun(t->ui(real(t),imag(t))+0im,sp),DefiniteLineIntegral(dom)

    G = GreensFun(g1,cwsp;method=:Cholesky)

    ∂u∂n = ⨍[G]\uiΓ

    us(x,y) = -logkernel(∂u∂n,complex(x,y))/2
    ut(x,y) = ui(x,y) + us(x,y)
    h = 1e-5
    abs((ut(h,0.)-ut(-h,0.))/2h)
end

NNr1,NTr1 = [4:15],[4:15,17:2:20,30:10:80]
ENr1 = Float64[@time electricfield(N,1e-1im) for N in NNr1]
println("Done ENr1")
ETr1 = Float64[@time electricfield(N,1e-1) for N in NTr1]
println("Done ETr1")

NNr2,NTr2 = [[5:10],[15:5:20],[30:10:80]],[[5:10],[15:5:20],[30:10:100]]
ENr2 = Float64[@time electricfield(N,1e-2im) for N in NNr2]
println("Done ENr2")
ETr2 = Float64[@time electricfield(N,1e-2) for N in NTr2]
println("Done ETr2")

#Nr3 = [[5:10],[15:5:20],[30:10:90],[100:50:200]]
#@time Er3 = Float64[electricfield(N,1e-3) for N in Nr3]
#@time Er4 = Float64[electricfield(N,1e-4) for N in Nr3]


# ------ Root Finding Methods -----

"""
    brent(f, a, b; args=(), atol=2e-12, rtol=4*eps(), maxiter=100)

1D root finding using Brent's method.  Based off the brentq implementation in scipy.

**Arguments**
- `f`: scalar function, that optionally takes additional arguments
- `a`::Float, b::Float`: bracketing interval for a root - sign changes sign between: (f(a) * f(b) < 0)
- `args::Tuple`: tuple of additional arguments to pass to f
- `atol::Float`: absolute tolerance (positive) for root
- `rtol::Float`: relative tolerance for root
- `maxiter::Int`: maximum number of iterations allowed

**Returns**
- `xstar::Float`: a root of f
- `info::Tuple`: A named tuple containing:
    - `iter::Int`: number of iterations
    - 'fcalls::Int`: number of function calls
    - 'flag::String`: a convergence/error message.
"""
function brent(f, a, b; args=(), atol=2e-12, rtol=4*eps(), maxiter=100)

    xpre = a; xcur = b
    # xblk = 0.0; fblk = 0.0; spre = 0.0; scur = 0.0
    error_num = "INPROGRESS"

    fpre = f(xpre, args...)
    fcur = f(xcur, args...)
    xblk = zero(fpre); fblk = zero(fpre); spre = zero(fpre); scur = zero(fpre)
    funcalls = 2
    iterations = 0

    if fpre*fcur > 0
        error_num = "SIGNERR"
        return 0.0, (iter=iterations, fcalls=funcalls, flag=error_num)
    end
    if fpre == zero(fpre)
        error_num = "CONVERGED"
        return xpre, (iter=iterations, fcalls=funcalls, flag=error_num)
    end
    if fcur == zero(fcur)
        error_num = "CONVERGED"
        return xcur, (iter=iterations, fcalls=funcalls, flag=error_num)
    end

    for i = 1:maxiter
        iterations += 1
        if fpre*fcur < 0
            xblk = xpre
            fblk = fpre
            spre = scur = xcur - xpre
        end
        if abs(fblk) < abs(fcur)
            xpre = xcur
            xcur = xblk
            xblk = xpre

            fpre = fcur
            fcur = fblk
            fblk = fpre
        end

        delta = (atol + rtol*abs(xcur))/2
        sbis = (xblk - xcur)/2
        if fcur == zero(fcur) || abs(sbis) < delta
            error_num = "CONVERGED"
            return xcur, (iter=iterations, fcalls=funcalls, flag=error_num)
        end

        if abs(spre) > delta && abs(fcur) < abs(fpre)
            if xpre == xblk
                # interpolate
                stry = -fcur*(xcur - xpre)/(fcur - fpre)
            else
                # extrapolate
                dpre = (fpre - fcur)/(xpre - xcur)
                dblk = (fblk - fcur)/(xblk - xcur)
                stry = -fcur*(fblk*dblk - fpre*dpre)/(dblk*dpre*(fblk - fpre))
            end
            if 2*abs(stry) < min(abs(spre), 3*abs(sbis) - delta)
                # good short step
                spre = scur
                scur = stry
            else
                # bisect
                spre = sbis
                scur = sbis
            end
        else
            # bisect
            spre = sbis
            scur = sbis
        end

        xpre = xcur; fpre = fcur
        if abs(scur) > delta
            xcur += scur
        else
            xcur += (sbis > 0 ? delta : -delta)
        end

        fcur = f(xcur, args...)
        funcalls += 1
    end
    error_num = "CONVERR"
    return xcur, (iter=iterations, fcalls=funcalls, flag=error_num)
end

# TODO AN: replace w/ newer Brent method and automatic bracketing?


# !INPUT/OUTPUT PARAMETERS:
#   m : order of derivative (in,integer)
#   n : number of points (in,integer)
#   x : abscissa array (in,real(n))
#   f : function array (in,real(n))
#   g : (anti-)derivative of f (out,real(n))
# !DESCRIPTION:
#   Given function $f$ defined on a set of points $x_i$ then if $m\ge 0$ this
#   routine computes the $m$th derivative of $f$ at each point. If $m=-1$ the
#   anti-derivative of $f$ given by
#   $$ g(x_i)=\int_{x_1}^{x_i} f(x)\,dx $$
#   is calculated. Both derivatives and integrals are computed by first fitting
#   the function to a clamped cubic spline.
function fderiv!(m, n, x, f, g)

    @assert n > 0

    # automatic arrays
    cf = zeros(Float64,3,n)

    # high accuracy integration/differentiation from spline interpolation
    spline!(n, x, f, cf)
    if m == -1
        ss =0.0
        g[1] = 0.0
        for i in 1:n-1
          dx = x[i+1] - x[i]
          ss = ss + dx*( f[i] + dx*( 0.50*cf[1,i] + dx*( cf[2,i]/3 + dx*0.25*cf[3,i] ) ) )
          g[i+1] = ss
        end
    elseif m == 1
        for i in 1:n
            g[i] = cf[1,i]
        end
    elseif m == 2
        for i in 1:n
            g[i] = 2.0*cf[2,i]
        end
    elseif m == 3
        for i in 1:n
            g[i] = 6.0*cf[3,i]
        end
    else
        for i in 1:n
            g[i] = 0.0
        end
    end
    return
end


# libpurex:
# Exponential families paramterised by their means

include("binary_search.jl");

struct Gaussian
    σ2;
end

# convenience
Gaussian() = Gaussian(1);

struct Bernoulli
end

struct Exponential
end

struct Poisson
end

# KL divergence

rel_entr(x, y) = x==0 ? 0. : x*log(x/y);

d(expfam::Gaussian,    μ, λ) = (μ-λ)^2/(2*expfam.σ2);
d(expfam::Bernoulli,   μ, λ) = rel_entr(μ, λ) + rel_entr(1-μ, 1-λ);
d(expfam::Exponential, μ, λ) = λ == 0 ? Inf : μ/λ - log(μ/λ) - 1;
d(expfam::Poisson,     μ, λ) = rel_entr(μ, λ) - μ + λ;

# solution for λ in \min_λ d(μ, λ) - λ x
# i.e. λ satisfying (λ - μ)ϕ''(λ) == x

invh(expfam::Gaussian,    μ, x) = μ + x*expfam.σ2;
invh(expfam::Bernoulli,   μ, x) = 2μ/(1-x+sqrt((x-1)^2 + 4*x*μ));
invh(expfam::Exponential, μ, x) = x > 0 ? Inf : 2μ/(1 + sqrt(1 - 4*x*μ));
invh(expfam::Poisson,     μ, x) = μ/(1-x);

sample(rng, expfam::Gaussian,    μ) = μ + sqrt(expfam.σ2)*randn(rng);
sample(rng, expfam::Bernoulli,   μ) = rand(rng) ≤ μ;
sample(rng, expfam::Exponential, μ) = randexp(rng)*μ;

# upward and downward confidence intervals
dup(expfam::Gaussian, μ, v) = μ + sqrt(2*expfam.σ2*v);
ddn(expfam::Gaussian, μ, v) = μ - sqrt(2*expfam.σ2*v);

function dup(expfam::Bernoulli, μ, v)
    μ == 1 ? 1. : binary_search(λ -> d(expfam, μ, λ) - v, μ, 1);
end

function ddn(expfam::Bernoulli, μ, v)
    μ == 0 ? 0. : binary_search(λ -> v - d(expfam, μ, λ), 0, μ);
end

function dup(expfam::Exponential, μ, v)
    binary_search(λ -> d(expfam, μ, λ) - v, μ, μ*exp(v+1));
end

function ddn(expfam::Exponential, μ, v)
    binary_search(λ -> v - d(expfam, μ, λ), 0, μ);
end


function fifth_power_digit_sum(n::Int)

	a=Int[];

	while(n>=1)
		push!(a,n%10);
		n=n-n%10;
		n/=10;
		n=Int(n)
	end
	ans=0;
	for i in a
		ans+=i^5
	end
	return ans
end

function compute()
	ans=0
	for i=2:1000000
		if i==fifth_power_digit_sum(i)
			ans+=i
		end
	end
	return ans
end

println(compute())


# # Envelopes

"""
    struct ConstantProfile

This is the trivial profile
```math
envelope(z, t) = 1
```
which gives an infinite duration for the electromagnetic field configuration (we cannot
call this a laser pusle since a pulse implicitely has a finite duration).
"""
struct ConstantProfile <: AbstractTemporalProfile end

@doc """
    GaussProfile{V,T,L}

This envelope provides a finite duration for the laser pulse and thus can provide a more
realistic description of an actual laser pulse.
```math
envelope(z, t) = \\exp\\left[-\\left(\\frac{φ}{τ}\\right)^2\\right],
```
where
```math
\\varphi = (t - t_0) - \\frac{z - z_0}{c}\\,,
```
and
- `c` is the speed of light
- `τ` is the duration of the pulse (FWHM) and has the default value 18.02fs
- `t₀` is the origin of the time axis and it is 0 by default
- `z₀` is the initial position of the intensity peak and has the default value `-4*τ*c`
"""
GaussProfile

struct GaussProfile{T,IT,L} <: AbstractTemporalProfile
    inv_τ::IT
    t₀::T
    z₀::L
end

function GaussProfile(;τ, t₀=zero(τ), z₀)
    GaussProfile(inv(τ), t₀, z₀)
end

@doc """
    Cos²Profile{V,T,L}

This envelope provides a finite duration for the laser pulse and thus can provide a more
realistic description of an actual laser pulse.
```math
envelope(z, t) =
    \\begin{cases}
    \\cos\\left[\\left(\\frac{φ}{τ}\\right)\\right]^2, & \\text{for } |t - t₀| < τ / 2\\\\
    0 \\,, & \\text{otherwise}
    \\end{cases}
```
where
```math
\\varphi = (t - t_0) - \\frac{z - z_0}{c}\\,,
```
and
- `c` is the speed of light
- `τ` is the duration of the pulse and has the default value 18.02fs
- `t₀` is the origin of the time axis and it is 0 by default
- `z₀` is the initial position of the intensity peak and is 0 by default
"""
Cos²Profile

struct Cos²Profile{T,IT,L} <: AbstractTemporalProfile
    τ::T
    inv_τ::IT
    t₀::T
    z₀::L
end

function Cos²Profile(;τ, t₀=zero(τ), z₀)
    t₀, τ = promote(t₀, τ)
    Cos²Profile(τ, inv(τ), t₀, z₀)
end

@doc """
    QuasiRectangularProfile{V,T,L}

This envelope produces a pulse with a predominant constant part of width ``Δz``
which could offer better results than the Gaussian profile in the paraxial limit
(which is considered for the spatial profiles). The shape of the envelope is given by
```math
envelope(z, t) =
    \\begin{cases}
    \\exp\\left[-\\left(\\frac{φ + Δt/2}{τ}\\right)^2\\right], & \\text{for } φ ≤ \\frac{Δt}{2}\\\\
    1\\,, & \\text{for } \\text{otherwise}\\\\
    \\exp\\left[-\\left(\\frac{φ - Δt/2}{τ}\\right)^2\\right], & \\ φ > \\frac{Δt}{2}
    \\end{cases}
```
where
```math
\\varphi = (t - t_0) - \\frac{z - z_0}{c}\\,,
```
and
- `c` is the speed of light
- `τ` is the duration of the pulse (FWHM) and has the default value 18.02fs
- `t₀` is the origin of the time axis and it is 0 by default
- `z₀` is the initial position of the intensity peak and has the default value `-4*τ*c`
- `Δt` is the duration of the flat part of the profile and the default value `10*τ`
"""
QuasiRectangularProfile

struct QuasiRectangularProfile{T,IT,L} <: AbstractTemporalProfile
    inv_τ::IT
    t₀::T
    z₀::L
    Δt::T
end

function QuasiRectangularProfile(;τ, t₀=zero(τ), z₀, Δt=10τ)
    τ, t₀, Δt = promote(τ, t₀, Δt)
    QuasiRectangularProfile(inv(τ), t₀, z₀, Δt)
end

"""
    g(z, t, par)

The time dependence of the fields defined by this package is given by
```math
g(z, t) = \\exp(i ω t) envelope(z, t),
```
where
- `z` and `t` are the position on the ``Oz`` axis and the time
- `par` are the laser parameters which pass the corresponding profile to the envelope
and
- ``ω`` is the angular frequency of the laser pulse
- ``envelope(z, t)`` is a function that can be used to control the duration of the pulse
"""
function g(z, t, laser; inv_c)
    profile = laser.profile
    ω = immutable_cache(laser, :ω)

    exp(im*ω*t) * envelope(profile, z, t; inv_c)
end

@inline envelope(::ConstantProfile, z, t; inv_c) = 1

@inline function envelope(profile::GaussProfile, z, t; inv_c)
    @unpack inv_τ, t₀, z₀ = profile
    φ = (t - t₀) - (z - z₀) * inv_c

    exp(-(φ * inv_τ)^2)
end

@inline function envelope(profile::Cos²Profile, z, t; inv_c)
    @unpack inv_τ, τ, t₀, z₀ = profile
    φ = π*((t - t₀) - (z - z₀) * inv_c)

    if abs(φ) / π < τ / 2
        cos(-(φ * inv_τ))^2
    else
        zero(t * inv_τ)
    end
end

@inline function envelope(profile::QuasiRectangularProfile, z, t; inv_c)
    @unpack inv_τ, t₀, z₀, Δt = profile
    φ = (t - t₀/2) - (z - z₀/2) * inv_c

    if φ < -Δt/2
        exp(-((φ + Δt/2) * inv_τ)^2)
    elseif φ < Δt/2
        one(φ)
    else
        exp(-((φ - Δt/2) * inv_τ)^2)
    end
end


#script for solving the acoustic duct problem
# -a0(x)*\Delta u(x,y)+a1(x)*u(x,y)=f(x,y) for (x,y) ∈ Ω:= (0,2)×(0,1)
# Here a0(x,y)=-1, a1(x,y)=k^2, f(x,y)=0
# with Neumann boundary conditions ∂u/∂x(x,y)=cos(mπy) for x = 0 and
# Robin boundary conditions ∂u/∂x(x,y) + iku = 0 for x=2

include("../src/Solver2D.jl")
include("../src/SplinePlotting.jl")

a0(x,y) = 1
a1(x,y) = -k^2
f(x,y) = 0
k = 40
m = 2
kx = sqrt(k^2 - (m*π)^2)
LHS = [im*kx -im*kx; (k-kx)*exp(-2*im*kx) (k+kx)*exp(2*im*kx)]
RHS = [1, 0]
A = LHS\RHS

exact_sol(x,y) = cos(m*π*y)*(A[1]*exp(-im*kx*x)+A[2]*exp(im*kx*x))
deriv_exact_sol(x,y) = [cos(m*π*y)*(A[1]*(-im)*kx*exp(-im*kx*x)+A[2]*im*kx*exp(im*kx*x)),
                        -sin(m*π*y)*(m*π)*(A[1]*exp(-im*kx*x)+A[2]*exp(im*kx*x))]

#Define the boundary conditions
u_bound_left(x,y) = cos(m*π*y)
u_bound_right(x,y) = 0.
bound_left = Boundary2D("Neumann", "Left", u_bound_left)
bound_right = Boundary2D("Robin", "Right", -im*k, u_bound_right)
bound_all = [bound_left, bound_right]

#Define the domain geometry
cornerLowerLeft = [0., 0.]
lengthx = 2.
lengthy = 1.
degP = [3, 3]
numSubdiv = 80
nrb = nrbsquare(cornerLowerLeft, lengthx, lengthy, degP, numSubdiv)
IGAmesh = genMesh(nrb)
#plotBasisParam(IGAmesh)

gauss_rule = [genGaussLegendre(degP[1]+1), genGaussLegendre(degP[2]+1)]
stiff = assemble_stiff2D(IGAmesh, a0, gauss_rule)
mass = assemble_mass2D(IGAmesh, a1, gauss_rule)
rhs = assemble_rhs2D(IGAmesh, f, gauss_rule)
bcdof_all, elem_all = classifyBoundary2D(IGAmesh)
lhs = mass + stiff
lhs, rhs = applyBCnurbs(IGAmesh, bound_all, lhs, rhs, bcdof_all, elem_all, gauss_rule)

println("Solving linear system")
@time sol0 = lhs\rhs
println("Plotting...")
@time plotSol(IGAmesh, real(sol0), "AcousticDuctReal")
@time plotSolError(IGAmesh, real(sol0), exact_sol, "AcousticDuctRealError")
println("Computing error")
@time relL2Err, relH1Err = compErrorNorm(IGAmesh, real(sol0), exact_sol, deriv_exact_sol, a0, gauss_rule)
println("Relative L2-norm error is $relL2Err")
println("Relative H1-seminorm error is $relH1Err")
print("Done!")


"""
Return the Inorder traversal of a complete binary tree with given `height`.
(Root node will be `0` and placed in the middle of the array
"""
function binary_inorder(height::Integer)::Vector{Int}
    n = (1 << Int(height)) - 1
    out = Vector{Int}(undef, n)

    s = 2
    while n >= 1
        t = s >> 1
        b, a = n-1, n >> 1
        for (i, j) in enumerate(a:b)
            out[i*s - t] = j
        end
        n >>= 1
        s <<= 1
    end

    return out
end


# There are exactly ten ways of selecting three from five, 12345:
#
# 123, 124, 125, 134, 135, 145, 234, 235, 245, and 345
#
# In combinatorics, we use the notation, (5 3)=10.
#
# In general, (n r)=n!/(r!(n−r)!), where r≤n, n!=n×(n−1)×...×3×2×1, and 0!=10!=1.
#
# It is not until n=23 that a value exceeds one-million: (23 10)=1144066.
#
# How many, not necessarily distinct, values of (n r) for 1≤n≤100, are greater
# than one-million?

using ProjectEulerSolutions

# We note that (n r) = (n (n-r)), and that when r = n/2, the value of (n r) is
# maximal.  Thus we calculate from (n r/2-k) for k until we get a value of more
# than 1 million.  We save more time by further constraining the range of k
# values.  This is mostly a book-keeping exercise.
function p053solution(n_max::Integer=3, thresh=1_000)::Integer
    n_min = 23
    r = 3

    total = 0
    for n in n_max:-1:n_min
        # The value of r where we surpass the threshold will only get larger
        while binomial(n, r) < thresh
            r += 1
        end
        total += (n - r * 2  + 1)
    end
    return total
end

p053 = Problems.Problem(p053solution)

Problems.benchmark(p053, 100, 1_000_000)


# Legendre-Gauss quadrature weights and nodes
# This code was originally written for Matlab
#  by Greg von Winckel - 02/25/2004
# posted on Mathworks website and licensed under BSD License
# http://fr.mathworks.com/matlabcentral/fileexchange/4540-legendre-gauss-quadrature-weights-and-nodes
#
#

function lgwt(N,a,b)

	N=N-1;
	N1=N+1; N2=N+2;
	xu=linspace(-1,1,N1)';

	y=(cos((2*(0:N)'+1)*pi/(2*N+2))+(0.27/N1)*sin(pi*xu*N/N2))';

	L=zeros(N1,N2);

	Lp=zeros(N1,N2);
	y0=2;

	while maximum(abs(y-y0))>eps()
		L[:,1]=1;
		Lp[:,1]=0;
		L[:,2]=y;
		for k=2:N1
			L[:,k+1]=( (2*k-1)*y.*L[:,k]-(k-1)*L[:,k-1] )/k;
		end
		Lp=(N2)*( L[:,N1]-y.*L[:,N2] )./(1-y.^2);
		y0=y;
		y=y0-L[:,N2]./Lp;
	end

	x=(a*(1-y)+b*(1+y))/2;

	w=(b-a)./((1-y.^2).*Lp.^2)*(N2/N1)^2;

	return x,w
end


using LinearAlgebra

# coefficients from PD of π functions
π = [70 -30 0;
     -30 140 -50;
     0 -50 50]
# Force Vector
f = Float64[0 100 60]
F = transpose(f)

# Displacement vector
u = π\F
println("Nodal displacement vector (mm):")
display(u)


# hyperbolic_expansion.jl

module hypergeometric_expansion
export hypergeom_exp

include("arb_hypgeom.jl")
import .arb_hypgeom
using ArbNumerics

function arb_binomial(x, y; prec = 64)
    setextrabits(0)
    setprecision(ArbReal, prec)
    x = ArbReal("$x")
    y = ArbReal("$y")
    one = ArbReal("1")
    ArbNumerics.gamma(x + one) / (ArbNumerics.gamma(y + one) * ArbNumerics.gamma(x - y + one))
end

"""
This function implements the binomial expansion of the denominator of the integrals:

    ∫_0^1 cosh(zt) / (4a^2/β^2 - t^2)^(n+3/2) dt
    ∫_0^1 sinh(zt) / (4a^2/β^2 - t^2)^(n+3/2) dt

which is:

    ∑_{m=0}^∞ C(-n-3/2, m) (-1)^m (β/2a)^{2n+2m+3} ∫_0^1 cosh(zt) t^{2m} dt
    ∑_{m=0}^∞ C(-n-3/2, m) (-1)^m (β/2a)^{2n+2m+3} ∫_0^1 sinh(zt) t^{2m} dt.

and also substitutes the integrals for:

    ∫_0^1 cosh(zt) t^{2m} dt = ∑_{t=0}^∞ z^{2t} / ((2t+2m+1)⋅(2t)!)
    ∫_0^1 sinh(zt) t^{2m} dt = ∑_{t=0}^∞ z^{2t+1} / ((2t+2m+2)⋅(2t+1)!)

by replacing those integrals with one_F_two_fast() from arb_hypgeom.jl. h=0 gives cosh version, h=1 gives sinh version.
"""
function hypergeom_exp(z, n, β, a, h; prec = 64)
# h = 0 gives cosh, h = 1 gives sinh.

    # Initialise precision of ArbReal to prec.
    p = prec
    setextrabits(0)
    setprecision(ArbReal, p)

    z = ArbReal("$z")
    n = ArbReal("$n")
    β = ArbReal("$β")
    a = ArbReal("$a")

    m = ArbReal("0")
    result = ArbReal("0.0")
    term = ArbReal("1.0")
    err = eps(result)  # Machine accuracy of specified precision prec.

    while abs(midpoint(term)) > err * abs(midpoint(result))

        term = ArbReal(β * arb_binomial(-n - 3/2, m; prec = p) * (-1)^m * (β / (2 * a))^(2 * m) * arb_hypgeom.one_f_two_fast(z, m, h; prec = p) / a^(n + 2))
        result += term
        # println("term: m = ", m, "\nterm value: ", ball(ArbReal(term, bits = prec)), "\ncumulant result: ", ball(ArbReal(result, bits = prec)), "\n")
        m += ArbReal("1")

        # Double precision if rounding error in result exceeds accuracy specified by prec.
        if radius(result) > err * abs(midpoint(result))
            p *= 2
            setprecision(ArbReal, p)
            # println("Not precise enough. Error = ", abs(radius(result)/midpoint(result)), " > ", err, ". Increasing precision to ", p, " bits.\n")

            n = ArbReal("$n")
            z = ArbReal("$z")
            β = ArbReal("$β")
            a = ArbReal("$a")

            m = ArbReal("0")
            result = ArbReal("0.0")
            term = ArbReal("1.0")
        end
    end
    # println("z: ", ArbReal(z, bits = prec), ". Final result: ", ArbReal(result, bits = prec))
    ArbReal(result, bits = prec)
end

# β = ArbReal("2.0")
# α = ArbReal("7.0")
# v = ArbReal("5.8")
# w = ArbReal("1.6")
# R = ArbReal((v^2 - w^2) / (w^2 * v))
# a = ArbReal(sqrt(β^2 / 4 + R * β * coth(β * v / 2)))
# z = ArbReal("90.61")
# n = ArbReal("12.0")
# @time c = hypergeom_exp(z, n, β, a, 0; prec = 64)
# @show(c)
end # end module