#=
in this example, we just put all the data into the
network at once over and over again to optimizes the
parameters of network.
=#

# first class data and lable
using Plots
input1 = 3*randn(2,1024);
label1 = zeros(2,1024);
label1[1,:] .= 1.0;

# second class data and lable
t = 0:pi/1000:2*pi;
input2 = zeros(2,length(t));
label2 = zeros(2,length(t));
label2[2,:] .= 1.0;
for i = 1:length(t)
    r = randn() + 18.0;
    input2[1,i] = r * cos(t[i]);
    input2[2,i] = r * sin(t[i]);
end

# make model's input and label
input = hcat(input1,input2)
label = hcat(label1,label2)
x = Variable(input);
l = Variable(label);
N = size(x,2)

scatter(input2[1,:],input2[2,:],color=:blue)
scatter!(input1[1,:],input1[2,:],color=:red)

function runme(epoch, lr, opt)
    mlpmodel  = Chain(Dense(2,32),
                      Dense(32,32),
                      Dense(32,32),
                      Linear(32,2))

    params  = xparamsof(mlpmodel)
    optim   = opt(params;lr=lr)
    lossval = zeros(epoch,1)

    for i = 1:epoch
        outs = forward(mlpmodel, x)
        COST = crossEntropyCost(softmax(outs;dims=1), l)
        backward()
        update!(optim)
        zerograds!(optim)
        lossval[i] = COST/N
    end
    return lossval
end

plot(vec(log.(runme(400, 1e-4, SGD))), color=:red,label="SGD");
plot!(vec(log.(runme(400, 1e-4, Momentum))), color=:orange,label="Momentum");
plot!(vec(log.(runme(400, 1e-4, Adam))), color=:green,label="Adam");
plot!(vec(log.(runme(400, 1e-3, RMSProp))), color=:blue,label="RMSProp");
plot!(vec(log.(runme(400, 1e-3, AdaGrad))), color=:cyan,label="AdaGrad")

L1 = 1.0
function runmeL1(epoch, lr, opt, L1)
    mlpmodel  = Chain(Dense(2,32),
                      Dense(32,32),
                      Dense(32,32),
                      Linear(32,2))

    params  = xparamsof(mlpmodel)
    optim   = opt(params;lr=lr,L1decay=L1)
    lossval = zeros(epoch,1)

    for i = 1:epoch
        outs = forward(mlpmodel, x)
        COST = crossEntropyCost(softmax(outs;dims=1), l)
        backward()
        update!(optim)
        zerograds!(optim)
        lossval[i] = COST/N
    end
    return lossval
end
plot(vec(log.(runmeL1(400, 1e-4, SGDL1, L1))), color=:red,label="SGDL1")
plot!(vec(log.(runmeL1(400, 1e-4, MomentumL1, L1))), color=:orange,label="MomentumL1")
plot!(vec(log.(runmeL1(400, 1e-4, AdamL1, L1))), color=:green,label="AdamL1")
plot!(vec(log.(runmeL1(400, 1e-3, RMSPropL1, L1))), color=:blue,label="RMSPropL1")
plot!(vec(log.(runmeL1(400, 1e-3, AdaGradL1, L1))), color=:cyan,label="AdaGradL1")

L2 = 1.0
function runmeL2(epoch, lr, opt, L2)
    mlpmodel  = Chain(Dense(2,32),
                      Dense(32,32),
                      Dense(32,32),
                      Linear(32,2))

    params  = xparamsof(mlpmodel)
    optim   = opt(params;lr=lr,L2decay=L2)
    lossval = zeros(epoch,1)

    for i = 1:epoch
        outs = forward(mlpmodel, x)
        COST = crossEntropyCost(softmax(outs;dims=1), l)
        backward()
        update!(optim)
        zerograds!(optim)
        lossval[i] = COST/N
    end
    return lossval
end
plot(vec(log.(runmeL2(400, 1e-4, SGDL2, L2))), color=:red,label="SGDL2")
plot!(vec(log.(runmeL2(400, 1e-4, MomentumL2, L2))), color=:orange,label="MomentumL2")
plot!(vec(log.(runmeL2(400, 1e-4, AdamL2, L2))), color=:green,label="AdamL2")
plot!(vec(log.(runmeL2(400, 1e-3, RMSPropL2, L2))), color=:blue,label="RMSPropL2")
plot!(vec(log.(runmeL2(400, 1e-3, AdaGradL2, L2))), color=:cyan,label="AdaGradL2")

L1 = 1.0
L2 = 1.0
function runmeL1L2(epoch, lr, opt, L1, L2)
    mlpmodel  = Chain(Dense(2,32),
                      Dense(32,32),
                      Dense(32,32),
                      Linear(32,2))

    params  = xparamsof(mlpmodel)
    optim   = opt(params;lr=lr,L1decay=L1,L2decay=L2)
    lossval = zeros(epoch,1)

    for i = 1:epoch
        outs = forward(mlpmodel, x)
        COST = crossEntropyCost(softmax(outs;dims=1), l)
        backward()
        update!(optim)
        zerograds!(optim)
        lossval[i] = COST/N
    end
    return lossval
end
plot(vec(log.(runmeL1L2(400, 1e-4, SGDL1L2, L1, L2))),color=:red,label="SGDL1L2")
plot!(vec(log.(runmeL1L2(400, 1e-4, MomentumL1L2, L1, L2))),color=:orange,label="MomentumL1L2")
plot!(vec(log.(runmeL1L2(400, 1e-4, AdamL1L2, L1, L2))),color=:green,label="AdamL1L2")
plot!(vec(log.(runmeL1L2(400, 1e-5, RMSPropL1L2, L1, L2))),color=:blue,label="RMSPropL1L2")
plot!(vec(log.(runmeL1L2(400, 1e-5, AdaGradL1L2, L1, L2))),color=:cyan,label="AdaGradL1L2")


using Quaternions
using Test
using Random
using LinearAlgebra

@testset "Quaternions Tests" begin
  @testset "basics" begin
    for T in (Int64, Float64)
      @test real(Quaternion{T}) == T
      @test quaternion(T) == Quaternion{T}
      @test quaternion(Quaternion{T}) == Quaternion{T}
    end

    @test sprint(show, quaternion(1, 0, 0, 0)) == "1 + 0im + 0jm + 0km"
    @test sprint(show, Quaternion{Int8}(0, 0, 0, typemin(Int8))) == "0 + 0im + 0jm - 128km"

    @test real(Quaternion(1, 2, 3, 4)) == 1
    @test imagi(Quaternion(1, 2, 3, 4)) == 2
    @test imagj(Quaternion(1, 2, 3, 4)) == 3
    @test imagk(Quaternion(1, 2, 3, 4)) == 4
    @test imag(Quaternion(1, 2, 3, 4)) == Quaternion(0, 2, 3, 4)
    @test conj(Quaternion(1, 2, 3, 4)) == Quaternion(1, -2, -3, -4)
  end

  @testset "unary operator on quaternion boolean" begin
    p = [true, false]
    for components in Iterators.product(p, p, p, p)
      @test +Quaternion(components...) === Quaternion(map(+,components)...)
      @test -Quaternion(components...) === Quaternion(map(-,components)...)
    end
  end

  @testset "random values" begin
    for T in (Float16, Float32, Float64)
      @test typeof(rand(Quaternion{T})) == Quaternion{T}
      @test typeof(randn(Quaternion{T})) == Quaternion{T}
    end
    for T in (Int8, Int16, Int32, Int64, Int128)
      @test typeof(rand(Quaternion{T})) == Quaternion{T}
    end
  end

  @testset "equivalent complex matrix" begin
    x = rand(QuaternionF64, 100,100)
    @test isequal(Quaternions.equiv(Quaternions.equiv(x)), x)
  end

  @testset "linear algebra" begin
    N = 100
    M = 200
    x = rand(QuaternionF64, N, N)
    y = rand(QuaternionF64, M, N)
    atol = 1e-12

    x_inv = inv(x)
    @test x_inv isa typeof(x)
    x_mul = x_inv * x
    @test isapprox(real(x_mul), I(N); atol = atol)
    @test isapprox(imagi(x_mul), zeros(N, N); atol = atol)
    @test isapprox(imagj(x_mul), zeros(N, N); atol = atol)
    @test isapprox(imagk(x_mul), zeros(N, N); atol = atol)

    y_pinv = pinv(y)
    @test y_pinv isa typeof(y)
    y_mul = y_pinv * y
    @test isapprox(real(y_mul), I(N); atol = atol)
    @test isapprox(imagi(y_mul), zeros(N, N); atol = atol)
    @test isapprox(imagj(y_mul), zeros(N, N); atol = atol)
    @test isapprox(imagk(y_mul), zeros(N, N); atol = atol)
  end

  @testset "math functions" begin
    for T in (Float16, Float32, Float64)
      x = rand(Quaternion{T})
      @test exp(x) isa typeof(x)
      @test log(x) isa typeof(x)
      @test cos(x) isa typeof(x)
      @test sin(x) isa typeof(x)
      @test tan(x) isa typeof(x)
      @test cosh(x) isa typeof(x)
      @test sinh(x) isa typeof(x)
      @test tanh(x) isa typeof(x)
      @test sqrt(x) isa typeof(x)
      @test x^x isa typeof(x)
      @test x^2 isa typeof(x)
      @test acos(x) isa typeof(x)
      @test asin(x) isa typeof(x)
      @test atan(x) isa typeof(x)
      @test norm(x) isa T
    end
    @test round(Quaternion(0.1, 0.1, 0.1, 0.1)) == Quaternion(0.0)
    @test round(Quaternion(0.9, 0.9, 0.9, 0.9)) == Quaternion(1.0, 1.0, 1.0, 1.0)
  end

  @testset "type handling" begin
    for T in (Int8, Int16, Int32, Int64, Int128)
      @test float(Quaternion{T}) == Quaternion{float(T)}
    end
    for T in (QuaternionF16, QuaternionF32, QuaternionF64)
      @test float(T) == T
    end
  end
end


# This file is a part of AstroLib.jl. License is MIT "Expat".
# Copyright (C) 2016 Mosè Giordano.

function calz_unred(wave::T, flux::T, ebv::T, r_v::T) where {T<:AbstractFloat}
    x  = 10000 / wave # Wavelength in inverse microns
    if 6300 <= wave <= 22000
        klam = 2.659*(-1.857 + 1.040*x) + r_v
    elseif 912 <= wave < 6300
        klam = 2.659*(@evalpoly(x, -2.156, 1.509, -0.198, 0.011)) + r_v
    else
        return flux
    end
    return flux * exp10(0.4 * klam * ebv)
end

"""
    calz_unred(wave, flux, ebv[, r_v]) -> deredden_wave

### Purpose ###

Deredden a galaxy spectrum using the Calzetti et al. (2000) recipe.

### Explanation ###

Calzetti et al.  (2000, ApJ 533, 682;
http://adsabs.harvard.edu/abs/2000ApJ...533..682C) developed a recipe for
dereddening the spectra of galaxies where massive stars dominate the radiation
output, valid between \$0.12\$ to \$2.2\$ microns.  (`calz_unred` extrapolates
between \$0.12\$ and \$0.0912\$ microns.)

### Arguments ###

* `wave`: wavelength (Angstroms)
* `flux`: calibrated flux.
* `ebv`: color excess E(B-V).  If a negative `ebv` is supplied, then
  fluxes will be reddened rather than deredenned.  Note that the supplied color
  excess should be that derived for the stellar continuum, EBV(stars), which is
  related to the reddening derived from the gas, EBV(gas), via the Balmer
  decrement by EBV(stars) = 0.44*EBV(gas).
* `r_v` (optional): ratio of total to selective extinction, default is
  4.05.  Calzetti et al. (2000) estimate ``r_v = 4.05 \\pm 0.80`` from optical-IR
  observations of 4 starbursts.

### Output ###

Unreddened flux, same units as `flux`.  Flux values will be left unchanged outside valid
domain (\$0.0912\$ - \$2.2\$ microns).

### Example ###

Estimate how a flat galaxy spectrum (in wavelength) between \$1200 Å\$ and
\$3200 Å\$ is altered by a reddening of E(B-V) = 0.1.

```julia
wave = collect(1200:50:3150);
flux = ones(wave);
flux_new = calz_unred.(wave, flux, -0.1);
```

Using a plotting tool you can visualize the unreddend flux.  For example, with
[PyPlot.jl](https://github.com/JuliaPy/PyPlot.jl)

```julia
using PyPlot
plot(wave, flux_new)
```

### Notes ###

Code of this function is based on IDL Astronomy User's Library.
"""
calz_unred(wave::Real, flux::Real, ebv::Real, r_v::Real=4.05) =
    calz_unred(promote(float(wave), float(flux), float(ebv), float(r_v))...)


todisplay = VERSION <= VersionNumber("1.2") ? true : false

using QHull
using Random
using SparseArrays
using LinearAlgebra
using LinearAlgebraicRepresentation
Lar = LinearAlgebraicRepresentation
using CAGD
if todisplay
	using ViewerGL
	GL = ViewerGL
	include("../examples/3d/views.jl")
end

V,CV = Lar.rod()([3,1])
ch = QHull.chull(convert(Lar.Points,V'))
FV = ch.simplices
EV = Lar.simplexFacets(FV)
GL.VIEW([ GL.GLGrid(V,EV,GL.COLORS[1],1) ]);

cyl = Lar.Struct([ Lar.t(0,0,0), (V,FV,EV) ]) #(0,0,-1.5)
cyl = Lar.struct2lar(cyl)

#V,FV = ([1.0 1.0 -0.5 -0.5 -0.5 -0.5; 0.0 0.0 0.86603 0.86603 -0.86603 -0.86603; 0.0 3.0 0.0 3.0 0.0 3.0],
#Array{Int64,1}[[3, 4, 1, 2], [3, 4, 5, 6], [5, 6, 1, 2], [3, 1, 5], [2, 4, 6]])
#EV = [[1,3],[3,5],[5,1], [2,4],[4,6],[6,2], [1,2],[3,4],[5,6]]
#cyl = (V,FV,EV)

tris = Lar.Struct([
    cyl,
    Lar.Struct([Lar.r(pi/2,0,0), cyl ]) ,
    Lar.Struct([Lar.r(0,pi/2,0), cyl ]) ,
    Lar.Struct([Lar.r(0,pi,0), cyl ]) ,
])
V,FV,EV = Lar.struct2lar(tris)

model = CAGD.Model(V)
scs = convert(Lar.ChainOp, Lar.coboundary_1(model.G, FV, EV))
CAGD.addModelCells!(model, 1, EV, signed = true)
CAGD.addModelCells!(model, 2, scs)

if todisplay  displayModel(model)  end

split_model = CAGD.pairwise_decomposition(model)

if todisplay  displayModel(split_model)  end

congr_model = CAGD.mergeModelVertices(split_model, signed_merge=true)

if todisplay  displayModel(congr_model)  end
gift_model = deepcopy(congr_model)
FCcycles, bicon_comps = CAGD.tgw(congr_model, 3, atol = atol)
CAGD.addModelCells!(gift_model, 3, convert(Lar.ChainOp, FC'))

if todisplay  viewExplode(gift_model, complete = false)  end

# Since first face is inner, then retrieving the external is needed

externals = [bc[CAGD.getExternalBoundary(congr_model, 3, FCcycles, bc)] for bc in bicon_comps]
internals = [setdiff(bicon_comps[i], externals[i]) for i = 1 : length(externals)]
assembled_model = deepcopy(congr_model)

FC = FCcycles[:, internals...]
JK = ones(Int8, length(externals))
FC = [FCcycles * SparseArrays.sparse(externals, JK, JK, size(FCcycles, 2), 1) FC]
CAGD.addModelCells!(assembled_model, 3, convert(Lar.ChainOp, FC'))

if todisplay  viewExplode(assembled_model)  end


module SfLaser

using Unitful

export diffraction_limited_element_size, diffraction_limited_spot_diameter, diffraction_limited_range, diffraction_limited_wavelength,
	attenuated_beam_power, approximate_breakdown_threshold, beam_intensity

"""
    diffraction_limited_spot_diameter(r::Unitful.Length, d_element::Unitful.Length, λ::Unitful.Length)

Approximate minimum spot size for a diffraction-limited gaussian laser beam of wavelength `λ` with
initial diameter `d_element` at range `r`.
"""
function diffraction_limited_spot_diameter(r::Unitful.Length, d_element::Unitful.Length, λ::Unitful.Length, m2 = 1)
	if r <= 0u"m"
		throw(DomainError(r, "Range r must be greater than zero"))
	elseif d_element <= 0u"m"
		throw(DomainError(d_element, "Element diameter must be greater than zero"))
	elseif λ <= 0u"m"
		throw(DomainError(λ, "Wavelength λ must be greater than zero"))
	elseif m2 < 1
		throw(DomainError(m2, "M² cannot be less than 1"))
	end

	if r < d_element
		@warn "Range r less than element size, result will be inaccurate" r element_size
	end

	return 2m2 * λ / (π * atan(d_element, 2r)) |> unit(d_element)
end

function diffraction_limited_element_size(r::Unitful.Length, d_spot::Unitful.Length, λ::Unitful.Length, m2 = 1)
	if r <= 0u"m"
		throw(DomainError(r, "Range r must be greater than zero"))
	elseif d_spot <= 0u"m"
		throw(DomainError(d_spot, "Spot diameter must be greater than zero"))
	elseif λ <= 0u"m"
		throw(DomainError(λ, "Wavelength λ must be greater than zero"))
	elseif m2 < 1
		throw(DomainError(m2, "M² cannot be less than 1"))
	end

	d_element =  4r * tan(m2 * λ / (π * d_spot))

	if r < d_element
		@warn "Range r less than element size, result will be inaccurate" r d_element
	end

	return d_element |> unit(d_spot)
end

function diffraction_limited_range(d_element::Unitful.Length, d_spot::Unitful.Length, λ::Unitful.Length, m2 = 1)
	if d_element <= 0u"m"
		throw(DomainError(d_element, "Element diameter must be greater than zero"))
	elseif d_spot <= 0u"m"
		throw(DomainError(d_spot, "Spot diameter must be greater than zero"))
	elseif λ <= 0u"m"
		throw(DomainError(λ, "Wavelength λ must be greater than zero"))
	elseif m2 < 1
		throw(DomainError(m2, "M² cannot be less than 1"))
	end

	r = d_element / 4tan(m2 * λ / (π * d_spot))

	if r < d_element
		@warn "Range r less than element size, result will be inaccurate" r d_element
	end

	return upreferred(r)
end

function diffraction_limited_wavelength(r::Unitful.Length, d_element::Unitful.Length, d_spot::Unitful.Length, m2 = 1)
	if d_element <= 0u"m"
		throw(DomainError(d_element, "Element diameter must be greater than zero"))
	elseif d_spot <= 0u"m"
		throw(DomainError(d_spot, "Spot diameter must be greater than zero"))
	elseif r <= 0u"m"
		throw(DomainError(r, "Range r must be greater than zero"))
	elseif m2 < 1
		throw(DomainError(m2, "M² cannot be less than 1"))
	end

	if r < d_element
		@warn "Range r less than element size, result will be inaccurate" r d_element
	end

	return tan(d_element /2r) * π * d_spot / 2m2 |> u"nm"
end

attenuated_beam_power(r::Unitful.Length, power::Unitful.Power, attenuation_length::Unitful.Length) = power * exp(-r / attenuation_length)

# http://panoptesv.com/SciFi/LaserDeathRay/Breakdown.html
function approximate_breakdown_threshold(λ::Unitful.Length, ambient_pressure::Unitful.Pressure)
	p = ustrip(ambient_pressure |> u"atm")
	l = ustrip(λ |> u"m")

	return ((0.31 / (l * p^0.6)^2) * u"W*cm^-2") |> u"W/m^2"
end

beam_intensity(power::Unitful.Power, d_spot::Unitful.Length) = power / (π * d_spot^2 / 4)

end


export AbstractPhyHermite, AbstractProHermite, CstMaxDegree

struct AbstractPhyHermite <: Hermite
    P::ImmutablePolynomial
    scaled::Bool
end

(P::AbstractPhyHermite)(x::T) where {T<:Real} = P.P(x)*exp(-x^2/2)

struct AbstractProHermite <: Hermite
    P::ImmutablePolynomial
    scaled::Bool
end

(P::AbstractProHermite)(x) = P.P(x)*exp(-x^2/4)

const CstMaxDegree = 40


module TestHopfToLC
include("preamble_plots.jl")

using Bifurcations
using Bifurcations: special_intervals, LimitCycleProblem, limitcycles
using Bifurcations.Examples.PredatorPrey

solver0 = init(PredatorPrey.prob)
solve!(solver0)

hopf_point, = special_intervals(solver0, Codim1.PointTypes.hopf)

period_max = 36.0
prob_lc = LimitCycleProblem(
    hopf_point, solver0;
    period_bound = (0.0, period_max),
    num_mesh = 20,
    degree = 3,
)
solver1 = init(
    prob_lc;
    start_from_nearest_root = true,
    verbose = true,
)
@info "Solving LimitCycleProblem..."
@time solve!(solver1)

@info "Testing LimitCycleProblem solution..."
periods = [lc.period for lc in limitcycles(solver1)]
@test length(periods) > 0
@test all(!isnan, periods)
@test maximum(periods) >= period_max  # it is stopped due to out-of-domain
@test minimum(periods) > 0

@info "Plotting LimitCycleProblem solution..."
@time plt_multi_panel = plot(
    plot(solver1, (:x=>:parameter, :y=>1, :color=>:period)),
    plot(solver1, (:x=>:parameter, :y=>:period, :color=>:period)),
    plot_state_space(solver1);
    clims = (10, period_max),  # TODO: make it work without this
)

@time @test_nothrow nullshow(plt_multi_panel)

@info "Done!"

end  # module


using Test
using CMPFit

## testlinfit
x = [-1.7237128E+00,1.8712276E+00,-9.6608055E-01,
     -2.8394297E-01,1.3416969E+00,1.3757038E+00,
     -1.3703436E+00,4.2581975E-02,-1.4970151E-01,
     8.2065094E-01]

y = [1.9000429E-01,6.5807428E+00,1.4582725E+00,
     2.7270851E+00,5.5969253E+00,5.6249280E+00,
     0.787615,3.2599759E+00,2.9771762E+00,
     4.5936475E+00]

e = fill(0., size(y)) .+ 0.07
param = [1., 1.]

function linfunc(x::Vector{Float64}, p::Vector{Float64})
    return @. p[1] + p[2]*x
end

res = cmpfit(x, y, e, linfunc, param)

@test res.bestnorm ≈ 2.756285  rtol=1.e-5
@test res.status   == 1
@test res.npar     == 2
@test res.nfree    == 2
@test res.npegged  == 0
@test res.dof      == 8

zero = res.perror .- [0.0222102, 0.018938];
@test minimum(zero) ≈ 0 atol=1.e-5
@test maximum(zero) ≈ 0 atol=1.e-5

zero = res.param .- [3.209966, 1.770954];
@test minimum(zero) ≈ 0 atol=1.e-5
@test maximum(zero) ≈ 0 atol=1.e-5

## testquadfit
x = [-1.7237128E+00,1.8712276E+00,-9.6608055E-01,
	 -2.8394297E-01,1.3416969E+00,1.3757038E+00,
	 -1.3703436E+00,4.2581975E-02,-1.4970151E-01,
	 8.2065094E-01]
y = [2.3095947E+01,2.6449392E+01,1.0204468E+01,
	 5.40507,1.5787588E+01,1.6520903E+01,
	 1.5971818E+01,4.7668524E+00,4.9337711E+00,
	 8.7348375E+00]

e = fill(0., size(y)) .+ 0.2
param = [1., 1., 1.]

function quadfunc(x::Vector{Float64}, p::Vector{Float64})
    return @. p[1] + p[2]*x + p[3]*(x*x)
end

res = cmpfit(x, y, e, quadfunc, param)

@test res.bestnorm ≈ 5.679323  rtol=1.e-5
@test res.status   == 1
@test res.npar     == 3
@test res.nfree    == 3
@test res.npegged  == 0
@test res.dof      == 7

zero = res.perror .- [0.097512, 0.054802, 0.054433];
@test minimum(zero) ≈ 0 atol=1.e-5
@test maximum(zero) ≈ 0 atol=1.e-5

zero = res.param .- [4.703829, 0.062586, 6.163087];
@test minimum(zero) ≈ 0 atol=1.e-5
@test maximum(zero) ≈ 0 atol=1.e-5

## testquadfix
param = [1., 0., 1.]
pinfo = CMPFit.Parinfo(length(param))
pinfo[2].fixed = 1

res = cmpfit(x, y, e, quadfunc, param, parinfo=pinfo)

@test res.bestnorm ≈ 6.983588  rtol=1.e-5
@test res.status   == 1
@test res.npar     == 3
@test res.nfree    == 2
@test res.npegged  == 0
@test res.dof      == 8

zero = res.perror .- [0.097286, 0., 0.053743];
@test minimum(zero) ≈ 0 atol=1.e-5
@test maximum(zero) ≈ 0 atol=1.e-5

zero = res.param .- [4.696254, 0., 6.172954];
@test minimum(zero) ≈ 0 atol=1.e-5
@test maximum(zero) ≈ 0 atol=1.e-5

## testgaussfit
x = [-1.7237128E+00,1.8712276E+00,-9.6608055E-01,
	 -2.8394297E-01,1.3416969E+00,1.3757038E+00,
	 -1.3703436E+00,4.2581975E-02,-1.4970151E-01,
	 8.2065094E-01]
y = [-4.4494256E-02,8.7324673E-01,7.4443483E-01,
	 4.7631559E+00,1.7187297E-01,1.1639182E-01,
	 1.5646480E+00,5.2322268E+00,4.2543168E+00,
	 6.2792623E-01]

e = fill(0., size(y)) .+ 0.5
param = [0.0, 1.0, 1.0, 1.0]

function gaussfunc(x::Vector{Float64}, p::Vector{Float64})
    sig2 = p[4] * p[4];
    xc = @. x - p[3];
    return @. p[2] * exp(-0.5 * xc *xc / sig2) + p[1]
end

res = cmpfit(x, y, e, gaussfunc, param)

@test res.bestnorm ≈ 10.350032  rtol=1.e-5
@test res.status   == 1
@test res.npar     == 4
@test res.nfree    == 4
@test res.npegged  == 0
@test res.dof      == 6

zero = res.perror .- [0.232234, 0.395434, 0.074715, 0.089997];
@test minimum(zero) ≈ 0 atol=1.e-5
@test maximum(zero) ≈ 0 atol=1.e-5

zero = res.param .- [0.480441, 4.550754, -0.062562, 0.397473];
@test minimum(zero) ≈ 0 atol=1.e-5
@test maximum(zero) ≈ 0 atol=1.e-5

## testgaussfit (with explicit derivatives)
gaussfunc_deriv(x::Vector{Float64}, p::Vector{Float64}) = gaussfunc(x, p)
function gaussfunc_deriv(x::Vector{Float64}, p::Vector{Float64}, pderiv::Vector{Int}, vderiv::Vector{Vector{Float64}})
    sig2 = p[4] * p[4];
    xc = x .- p[3];
    ee = exp.(-0.5 .* xc.^2 ./ sig2)

    for i in 1:length(pderiv)
        ipar = pderiv[i]
        if ipar == 1
            vderiv[i] .= 1.
        elseif ipar == 2
            vderiv[i] .= ee
        elseif ipar == 3
            vderiv[i] .= p[2] .* ee .* (x .- p[3]) ./ sig2
        elseif ipar == 4
            vderiv[i] .= p[2] .* ee .* (x .- p[3]).^2 ./ sig2 ./ p[4]
        end
    end

    return p[2] .* ee .+ p[1]
end

pinfo = CMPFit.Parinfo(length(param))
setfield!.(pinfo, :side, Int32(3))
res = cmpfit(x, y, e, gaussfunc_deriv, param, parinfo=pinfo)

## testgaussfix
param = [0.0, 1.0, 0.0, 0.1]
pinfo = CMPFit.Parinfo(length(param))
pinfo[1].fixed = 1
pinfo[3].fixed = 1

res = cmpfit(x, y, e, gaussfunc, param, parinfo=pinfo)

@test res.bestnorm ≈ 15.516134  rtol=1.e-5
@test res.status   == 1
@test res.npar     == 4
@test res.nfree    == 2
@test res.npegged  == 0
@test res.dof      == 8

zero = res.perror .- [0., 0.329307, 0., 0.053804];
@test minimum(zero) ≈ 0 atol=1.e-5
@test maximum(zero) ≈ 0 atol=1.e-5

zero = res.param .- [0., 5.059244, 0., 0.479746];
@test minimum(zero) ≈ 0 atol=1.e-5
@test maximum(zero) ≈ 0 atol=1.e-5


module Day12

export get_inputs, get_solution1, get_solution2

## Input getting
function get_inputs()
    test_input1 = test_input2 = read_inputs("test_input1.txt")
    test_output1 = 25
    test_output2 = 286
    data = read_inputs("input.txt")
    return (; test_input1, test_input2, test_output1, test_output2, data)
end

function read_inputs(f_name)
    lines = readlines(joinpath(@__DIR__, f_name))
    return [Symbol(line[1])=>parse(Int, line[2:end]) for line in lines]
end

## Solution functions
# instruction => (trans, rot)
const move_dict = Dict(
    :N => ( im,   0),
    :S => (-im,   0),
    :E => (  1,   0),
    :W => ( -1,   0),
    :L => (  0,  im),
    :R => (  0, -im),
)

Base.@kwdef mutable struct Ship
    pos::Complex{Int} = 0
    dir::Complex{Int} = 1
    waypoint::Complex{Int} = 10+im
end

function get_solution(data, fun=move!)
    ship = Ship()
    for line in data
        fun(ship, line)
    end
    pos = ship.pos
    return abs(pos.re) + abs(pos.im)
end

# Part 1
function move!(ship, (instruction, amount))
    if instruction==:F
        ship.pos += amount * ship.dir
    else
        trans, rot = move_dict[instruction]
        ship.pos += amount * trans
        ship.dir *= rot ^ (amount ÷ 90)
    end
    return ship
end

get_solution1(data) = get_solution(data)

# Part 2
function move_with_waypoint!(ship, (instruction, amount))
    if instruction==:F
        ship.pos += ship.waypoint * amount
    else
        trans, rot = move_dict[instruction]
        ship.waypoint += amount * trans
        ship.waypoint *= rot ^ (amount ÷ 90)
    end
    return ship
end

get_solution2(data) = get_solution(data, move_with_waypoint!)

end


"""
Computes random samples of functions from aligned data using Gaussian model

    gauss_model(fn, timet, qn, gam; n=1, sort_samples=false)
    :param fn: aligned functions (M,N)
    :param timet: vector (M) describing time
    :param qn: aligned srvfs (M,N)
    :param gam: warping functions (M,N)
    :param n: number of samples
    :param sort_samples: sort samples

    Returns Dict containing
    :return fs: random aligned functions
    :return gams: random warping functions
    :return ft: random functions
"""
function gauss_model(fn, timet, qn, gam; n=1, sort_samples=false)
    # Parameters
    eps1 = eps(Float64);
    binsize = mean(diff(timet));
    M = length(timet);

    # compute mean and covariance in q-domain
    mq_new = mean(qn,2);
    mididx = round(Integer, M/2);
    m_new = sign(fn[mididx,:]) .* sqrt(abs(fn[mididx,:]));
    mqn = vec([mq_new; mean(m_new)]);
    qn2 = vcat(qn, m_new');
    C = Base.covm(qn2, mean(qn2,2), 2);

    q_s = mvnrand(mqn, C, n);

    # compute the correspondence to the orignal function domain
    fs = zeros(M, n);
    for k in 1:n
        fs[:, k] = cumtrapzmid(timet, q_s[1:M,k].*abs(q_s[1:M,k]),
                               sign(q_s[M+1,k])*(q_s[M+1,k]^2));
    end

    # random warping generation
    rgam = random_gamma(gam, n);
    gams = zeros(M, n);
    for k in 1:n
        gams[:,k] = invert_gamma(rgam[:, k]);
    end

    if sort_samples
        mx = maximum(fs,1);
        seq1 = sortperm(vec(mx));

        # compute the psi-function
        psi = zeros(size(rgam));
        for ii in 1:size(rgam, 2)
            fy = gradient(rgam[:, ii], binsize);
            psi[:,ii] = fy ./ sqrt(abs(fy) + eps1);
        end
        ip = zeros(n);
        len = zeros(n);

        for i in 1:n
            tmp = ones(1,M);
            psi_tmp = (psi[:,i]/M);
            ip[i] = (tmp * psi_tmp)[1];
            len[i] = acos(ip[i]);
        end

        seq2 = sortperm(len);

        # combine x-variability and y-variability
        ft = zeros(M, n);
        time0 = [0:(M-1)]./(M-1);
        for k in 1:n
            ft[:,k] = approx(time0, fs[:,seq1[k]], gams[:,seq2[k]]);
            tmp = isnan(ft[:, k]);
            while any(tmp)
                rgam2 = random_gamma(gam, 1);
                gam_tmp = invert_gamma(rgam2);
                ft[:, k] = approx(time0, fs[:,seq1[k]], gam_tmp);
            end
        end
    else
        # combine x-variability and y-variability
        ft = zeros(M, n);
        time0 = collect(0:(M-1))./(M-1);
        for k in 1:n
            ft[:,k] = approx(time0, fs[:,k], gams[:,k]);
            tmp = isnan(ft[:, k]);
            while any(tmp)
                rgam2 = random_gamma(gam, 1);
                gam_tmp = invert_gamma(rgam2);
                ft[:, k] = approx(time0, fs[:,k], gam_tmp);
            end
        end
    end

    out = Dict("fs" => fs, "gams" => rgam, "ft" => ft);
    return out

end


using ApproxFun, Test

@testset "BigFloat" begin
    @testset "BigFloat constructor" begin
        single_sin = Fun(sin,Interval(0.f0,1.f0))
        double_sin = Fun(sin,Interval(0.,1.))
        big_sin = Fun(sin,Interval(parse(BigFloat,"0.0"), parse(BigFloat,"1.0")))

        @test ncoefficients(single_sin) <= ncoefficients(double_sin)
        @test ncoefficients(double_sin) <= ncoefficients(big_sin)

        @test eltype(coefficients(single_sin)) == Float32
        @test eltype(coefficients(double_sin)) == Float64
        @test eltype(coefficients(big_sin)) == BigFloat

        single_double_err = coefficients(single_sin-double_sin)[1:ncoefficients(single_sin)]
        @test norm(single_double_err) < 10eps(Float32)

        single_double_err = coefficients(double_sin-big_sin)[1:ncoefficients(double_sin)]
        @test norm(single_double_err) < 10eps(Float64)
    end

    @testset "BigFloat roots" begin
        a = Fun(ChebyshevInterval{BigFloat}(),BigFloat[1,2,3])
        # 12 is some bound on how accurately the polynomial can be evaluated
        # near its roots, where its evaluation is ill-conditioned
        @test norm(a.(roots(a))) ≤ 12eps(BigFloat)

        a = Fun(Taylor(Circle(BigFloat)),BigFloat[0.5,2,3])
        @test norm(a.(complexroots(a)))  ≤ 12eps(BigFloat)
    end

    @testset "BigFloat relative tolerance bug test Issue #557" begin
		if false # disabled due to Julia issue https://github.com/JuliaLang/julia/issues/25853
	        x = Fun(BigFloat(0)..BigFloat(20_000));
	        ν = BigFloat(1568)
	        f = x^(ν/2-1) * exp(-x/2)
	        # TODO: JacobiWeight should support general types to avoid warning here
	        @test f(10.0) ≈ 10.0^(ν/2-1) * exp(-10.0/2) rtol=eps()
	        ex_mathematica = parse(BigFloat, "8.6494114955713766301430915207861966687081153e778")
	        @test (cumsum(f)(10.0) - ex_mathematica)/ex_mathematica ≤ eps()

	        x = Fun(BigFloat(0)..BigFloat(Inf))
	        ν = BigFloat(1568)
            f = x^(ν/2-1) * exp(-x/2)
            @test f(10.0) ≈ 10.0^(ν/2-1) * exp(-10.0/2) rtol=eps()
            @test_skip (cumsum(f)(10.0) - ex_mathematica)/ex_mathematica ≤ eps()
        end
    end

    @testset "#641" begin
        f64 = Fun(Laurent(),[2, -1, -1])
        frat = Fun(Laurent(),Rational.(BigInt.([2, -1, -1])))
        fr2 = (frat*frat)
        f642 = f64*f64
        @test fr2 isa Fun{<:Laurent, Rational{BigInt}}
        @test fr2.coefficients ≈ f642.coefficients
    end
end


using Stheno, Random, Plots
using Stheno: @model, Titsias

###########################  Define our model  ###########################

σ² = 1e-1

# Define a distribution over f₁, f₂, and f₃, where f₃(x) = f₁(x) + f₂(x).
@model function model()

    # Define latent processes.
    f₁ = GP(ConstantMean(randn()), EQ())
    f₂ = GP(EQ())
    f₃ = f₁ + f₂

    # Define noisy versions of latent processes which we are permitted to observe.
    y₁ = f₁ + GP(Noise(σ²))
    y₂ = f₂ + GP(Noise(σ²))
    y₃ = f₃ + GP(Noise(σ²))

    return f₁, f₂, f₃, y₁, y₂, y₃
end

f₁, f₂, f₃, y₁, y₂, y₃ = model();

# Randomly sample `N₁` locations at which to measure `f` using `y1`, and `N2` locations
# at which to measure `f` using `y2`.
rng, N₁, N₃, M = MersenneTwister(123546), 100, 100, 50;
Z = range(-15.0, stop=15.0, length=M);
X₁ = sort(rand(rng, N₁) * 10);
X₃ = sort(rand(rng, N₃) * 10);

# Generate some toy observations of `f₁` and `f₃` and approximately condition.
ŷ₁, ŷ₃ = rand(rng, [y₁(X₁), y₃(X₃)]);
f₁′, f₂′, f₃′ = (f₁, f₂, f₃) | Titsias([f₁(X₁)←ŷ₁, f₃(X₃)←ŷ₃], [f₁(Z), f₃(Z)], sqrt(σ²));

# Define some plotting stuff.
Np, S = 500, 25;
Xp = range(-2.5, stop=12.5, length=Np);

# Sample jointly from the posterior over each process.
f₁′Xp, f₂′Xp, f₃′Xp = rand(rng, [f₁′(Xp), f₂′(Xp), f₃′(Xp)], S);

# Compute posterior marginals.
μf₁′, σf₁′ = marginals(f₁′(Xp));
μf₂′, σf₂′ = marginals(f₂′(Xp));
μf₃′, σf₃′ = marginals(f₃′(Xp));

@show logpdf([y₁(X₁), y₃(X₃)], [ŷ₁, ŷ₃]);
fb, yb = BlockGP([f₁(X₁), f₃(X₃)]), BlockVector([ŷ₁, ŷ₃]);
@show elbo(fb, yb, fb, sqrt(σ²));

# Perform exact computations.
f′1, f′2, f′3 = (f₁, f₂, f₃) | (y₁(X₁)←ŷ₁, y₃(X₃)←ŷ₃);
f′1Xp, f′2Xp, f′3Xp = rand(rng, [f′1(Xp), f′2(Xp), f′3(Xp)], S);
mf′1, σf′1 = marginals(f′1(Xp));
mf′2, σf′2 = marginals(f′2(Xp));
mf′3, σf′3 = marginals(f′3(Xp));

###########################  Plot results  ###########################

plotly();
approximate_posterior_plot = plot();

# Plot posterior marginal variances
plot!(approximate_posterior_plot, Xp, [μf₁′ μf₁′];
    linewidth=0.0,
    fillrange=[μf₁′ .- 3 .* σf₁′, μf₁′ .+ 3 * σf₁′],
    fillalpha=0.3,
    fillcolor=:red,
    label="");
plot!(approximate_posterior_plot, Xp, [μf₂′ μf₂′];
    linewidth=0.0,
    fillrange=[μf₂′ .- 3 .* σf₂′, μf₂′ .+ 3 * σf₂′],
    fillalpha=0.3,
    fillcolor=:green,
    label="");
plot!(approximate_posterior_plot, Xp, [μf₃′ μf₃′];
    linewidth=0.0,
    fillrange=[μf₃′ .- 3 .* σf₃′, μf₃′ .+ 3 * σf₃′],
    fillalpha=0.3,
    fillcolor=:blue,
    label="");

# Plot joint posterior samples
plot!(approximate_posterior_plot, Xp, f₁′Xp,
    linecolor=:red,
    linealpha=0.2,
    label="");
plot!(approximate_posterior_plot, Xp, f₂′Xp,
    linecolor=:green,
    linealpha=0.2,
    label="");
plot!(approximate_posterior_plot, Xp, f₃′Xp,
    linecolor=:blue,
    linealpha=0.2,
    label="");

# Plot posterior means
plot!(approximate_posterior_plot, Xp, μf₁′;
    linecolor=:red,
    linewidth=2.0,
    label="f1");
plot!(approximate_posterior_plot, Xp, μf₂′;
    linecolor=:green,
    linewidth=2.0,
    label="f2");
plot!(approximate_posterior_plot, Xp, μf₃′;
    linecolor=:blue,
    linewidth=2.0,
    label="f3");

# Plot observations
scatter!(approximate_posterior_plot, X₁, ŷ₁;
    markercolor=:red,
    markershape=:circle,
    markerstrokewidth=0.0,
    markersize=4,
    markeralpha=0.7,
    label="");
scatter!(approximate_posterior_plot, X₃, ŷ₃;
    markercolor=:blue,
    markershape=:circle,
    markerstrokewidth=0.0,
    markersize=4,
    markeralpha=0.7,
    label="");

# display(approximate_posterior_plot);

# # savefig(approximate_posterior_plot, "process_decomposition.png")

posterior_plot = plot();

items = [
    (mf′1, σf′1, f′1Xp, :red, "f1"),
    (mf′2, σf′2, f′2Xp, :green, "f2"),
    (mf′3, σf′3, f′3Xp, :blue, "f3"),
];

# Plot posterior marginal variances
for (μ, σ, _, colour, name) in items
    plot!(posterior_plot, Xp, [μ, μ];
        linewidth=0.0,
        fillrange=[μ .- 3 .* σ, μ .+ 3 * σ],
        fillalpha=0.3,
        fillcolor=colour,
        label="");
end
for (μ, σ, f, colour, name) in items
    plot!(posterior_plot, Xp, f,
        linecolor=colour,
        linealpha=0.2,
        label="");
end
for (μ, σ, f, colour, name) in items
    plot!(posterior_plot, Xp, μ;
        linecolor=colour,
        linewidth=2.0,
        label=name);
end

# Plot observations
scatter!(posterior_plot, X₁, ŷ₁;
    markercolor=:red,
    markershape=:circle,
    markerstrokewidth=0.0,
    markersize=4,
    markeralpha=0.7,
    label="");
scatter!(posterior_plot, X₃, ŷ₃;
    markercolor=:blue,
    markershape=:circle,
    markerstrokewidth=0.0,
    markersize=4,
    markeralpha=0.7,
    label="");

joint_plot = plot(approximate_posterior_plot, posterior_plot; layout=(2, 1));
display(joint_plot);


@testset "StaticDimer vs BBDimer" begin

heading1("TEST: StaticDimer, BBDimer with and without Preconditioning")

heading2("Muller potential")

V = MullerPotential()
x0, v0 = ic_dimer(V, :near)
E, dE = objective(V)

dimer = StaticDimer(a_trans=0.002, a_rot=0.002, len=1e-3, verbose=verbose)
x, v, log = run!(dimer, E, dE, x0, v0)
@test res_trans(log)[end] <= dimer.tol_trans
@test res_rot(log)[end] <= dimer.tol_rot

bbdimer = BBDimer(a0_trans=0.002, a0_rot=0.002, verbose=verbose)
xbb, vbb, bblog = run!(bbdimer, E, dE, x0, v0)
@test res_trans(bblog)[end] <= dimer.tol_trans
@test res_rot(bblog)[end] <= dimer.tol_rot
@test vecnorm(xbb - x, Inf) < 1e-4

heading2("Muller potential and preconditioning")

dimer = StaticDimer( a_trans=0.25, a_rot=0.5, len=1e-3, verbose=verbose,
      precon=eye(2), precon_rot=true, precon_prep! = (P,x) -> hessprecond(V, x) )
x, v, log = run!(dimer, E, dE, x0, v0)
@test res_trans(log)[end] <= dimer.tol_trans
@test res_rot(log)[end] <= dimer.tol_rot

dimer = BBDimer( a0_trans=0.25, a0_rot=0.5, len=1e-3, verbose=verbose,
      precon=eye(2), precon_rot=true, precon_prep! = (P,x) -> hessprecond(V, x) )
x, v, log = run!(dimer, E, dE, x0, v0)
@test res_trans(log)[end] <= dimer.tol_trans
@test res_rot(log)[end] <= dimer.tol_rot

heading2("Standard double-well")

V = DoubleWell()
x0, v0 = ic_dimer(V, :near)
E, dE = objective(V)

dimer = StaticDimer(a_trans=0.66, a_rot=0.4, len=1e-3, verbose=verbose)
x, v, log = run!(dimer, E, dE, x0, v0)
@test res_trans(log)[end] <= dimer.tol_trans
@test res_rot(log)[end] <= dimer.tol_rot

bbdimer = BBDimer(a0_trans=0.5, a0_rot=0.5, verbose=verbose)
xbb, vbb, bblog = run!(bbdimer, E, dE, x0, v0)
@test res_trans(bblog)[end] <= dimer.tol_trans
@test res_rot(bblog)[end] <= dimer.tol_rot
@test vecnorm(xbb - x, Inf) < 1e-4

heading2("Ill-conditioned double-well")

V = DoubleWell(diagm([1.0, 10.0]))
x0, v0 = ic_dimer(V, :near)
E, dE = objective(V)

dimer = StaticDimer(a_trans=0.1, a_rot=0.1, len=1e-3, verbose=verbose)
x, v, log = run!(dimer, E, dE, x0, v0)
@test res_trans(log)[end] <= dimer.tol_trans
@test res_rot(log)[end] <= dimer.tol_rot

bbdimer = BBDimer(a0_trans=0.1, a0_rot=0.1, verbose=verbose)
xbb, vbb, bblog = run!(bbdimer, E, dE, x0, v0)
@test res_trans(bblog)[end] <= dimer.tol_trans
@test res_rot(bblog)[end] <= dimer.tol_rot
@test vecnorm(xbb - x, Inf) < 1e-4

heading2("Ill-conditioned double-well, but now with preconditioner")

dimer = StaticDimer(a_trans=0.66, a_rot=0.4, len=1e-3, verbose=verbose,
      precon=V.A, precon_rot=true)
x, v, log = run!(dimer, E, dE, x0, v0)
@test res_trans(log)[end] <= dimer.tol_trans
@test res_rot(log)[end] <= dimer.tol_rot

bbdimer = BBDimer(a0_trans=0.66, a0_rot=0.4, precon=V.A,
      precon_rot=true, verbose=verbose)
xbb, vbb, bblog = run!(bbdimer, E, dE, x0, v0)
@test res_trans(bblog)[end] <= dimer.tol_trans
@test res_rot(bblog)[end] <= dimer.tol_rot
@test vecnorm(xbb - x, Inf) < 1e-4

end


"""
# References
[1] T. Bian and Z.-P. Jiang, “Value Iteration, Adaptive Dynamic Programming, and Optimal Control of Nonlinear Systems,” in 2016 IEEE 55th Conference on Decision and Control (CDC), Las Vegas, NV, USA, Dec. 2016, pp. 3375–3380. doi: 10.1109/CDC.2016.7798777.
"""
struct TwoDimensionalNonlinearPolynomialSystem <: AbstractEnv
end

function State(env::TwoDimensionalNonlinearPolynomialSystem)
    return function (x1::Real=-2.9, x2::Real=-2.9)
        ComponentArray(x1=x1, x2=x2)
    end
end

function Dynamics!(env::TwoDimensionalNonlinearPolynomialSystem)
    @Loggable function dynamics!(dx, x, p, t; u)
        @assert length(u) == 1
        @log state = x
        @log input = u
        _u = u[1]  # Real or Array
        @unpack x1, x2 = x
        dx.x1 = -(1/2)*x1^3 - x1 - 2*x2
        dx.x2 = (1/8)*x2^3 - x2 + (1/2)*_u^3
        nothing
    end
end

function OptimalControl(env::TwoDimensionalNonlinearPolynomialSystem)
    return function (x::ComponentArray, p, t)
        @unpack x1, x2 = x
        -x2
    end
end

function OptimalValue(env::TwoDimensionalNonlinearPolynomialSystem)
    return function (x::ComponentArray)
        @unpack x1, x2 = x
        x1^2 + x2^2
    end
end

function RunningCost(env::TwoDimensionalNonlinearPolynomialSystem)
    return function (x::ComponentArray, _u)
        @assert length(_u) == 1
        u = _u[1]  # Real or Array
        @unpack x1, x2 = x
        x1^4 + 2*(x1+x2)^2 + (3/4)*u^4
    end
end


"""
    NonTraditionalFPlane{FT} <: AbstractRotation

A Coriolis implementation that accounts for both the locally vertical and
the locally horizontal components of the rotation vector. Traditionally
(see [`FPlane`](@ref)) only the locally vertical component is accounted for.
"""
struct NonTraditionalFPlane{FT} <: AbstractRotation
    fz :: FT
    fy :: FT
end

"""
    NonTraditionalFPlane([FT=Float64;] fz=nothing, fy=nothing,
                                       rotation_rate=Ω_Earth, latitude=nothing)

Returns a parameter object for constant rotation about an axis in the `y-z` plane
with `y`- and `z`-components `fy/2` and `fz/2`, and the background vorticity is
`(0, fy, fz)`.

In oceanography `fz` and `fy` represent the components of planetary voriticity which
are perpendicular and parallel to the ocean surface in a domain in which `x, y, z`
correspond to the directions east, north, and up.

If `fz` and `fy` are not specified, they are calculated from `rotation_rate` and `latitude`
according to the relations `fz = 2 * rotation_rate * sind(latitude)` and
`fy = 2 * rotation_rate * cosd(latitude)`, respectively. By default, `rotation_rate`
is assumed to be Earth's.
"""
function NonTraditionalFPlane(FT=Float64; fz=nothing, fy=nothing, rotation_rate=Ω_Earth, latitude=nothing)

    use_f = !isnothing(fz) && !isnothing(fy) && isnothing(latitude)
    use_planet_parameters = !isnothing(latitude) && isnothing(fz) && isnothing(fy)

    if !xor(use_f, use_planet_parameters)
        throw(ArgumentError("Either both rotation_rate and latitude must be " *
                            "specified, *or* both f and f′ must be specified."))
    elseif use_planet_parameters
        fz = 2rotation_rate*sind(latitude)
        fy = 2rotation_rate*cosd(latitude)
    end
    return NonTraditionalFPlane{FT}(fz, fy)
end

# This function is eventually interpolated to fcc to contribute to x_f_cross_U.
@inline fʸw_minus_fᶻv(i, j, k, grid, coriolis, U) =
    coriolis.fy * ℑzᵃᵃᶜ(i, j, k, grid, U.w) - coriolis.fz * ℑyᵃᶜᵃ(i, j, k, grid, U.v)

@inline x_f_cross_U(i, j, k, grid, coriolis::NonTraditionalFPlane, U) =   ℑxᶠᵃᵃ(i, j, k, grid, fʸw_minus_fᶻv, coriolis, U)
@inline y_f_cross_U(i, j, k, grid, coriolis::NonTraditionalFPlane, U) =   coriolis.fz * ℑxyᶜᶠᵃ(i, j, k, grid, U.u)
@inline z_f_cross_U(i, j, k, grid, coriolis::NonTraditionalFPlane, U) = - coriolis.fy * ℑxzᶜᵃᶠ(i, j, k, grid, U.u)

Base.show(io::IO, f_plane::NonTraditionalFPlane{FT}) where FT =
    print(io, "NonTraditionalFPlane{$FT}: ", @sprintf("fz = %.2e, fy = %.2e", f_plane.fz, f_plane.fy))


@doc raw"""
    kf = KalmanFilter(x, F, H, B, P, Q, R, offset)

Create a KalmanFilter object.

for the dynamical system
```math
\begin{aligned}
x_{t+1} & = Fx_t + Bu_t + w_t + \text{offset}\\
y_t     & = Hx_t + v_t,
\end{aligned}
```
where $x_t\in \mathbb{R}^n$ are the states, $u\in \mathbb{R}^p$ are known inputs
 $y_t\in \mathbb{R}^m$ are measured outputs and
 $w_t\in \mathbb{R}^n,\ v_t \in \mathbb{R}^m$ are unknown, uncorrelated disturbances,
 with positive definite covariance matrices $Q \in \mathbb{R}^{n\times n}$ and
 $R \in \mathbb{R}^{m\times m}$ respectively. The offset is an $n$-dimensional constant.
"""
function KalmanFilter(
    x::AbstractVector{T},
    F::AbstractMatrix{T},
    H::AbstractMatrix{T},
    B::AbstractMatrix{T},
    P::AbstractMatrix{T},
    Q::AbstractMatrix{T},
    R::AbstractMatrix{T},
    offset::AbstractVector{T},
    ) where T<: AbstractFloat

    n = size(x)[1]
    m = size(H)[1]
    p = size(B)[2]
    # Validate Args
    size(F)[1] != n ? DimensionMismatch("Dimension mismatch of F") |> throw : nothing
    size(H)[2] != n ? DimensionMismatch("Dimension mismatch of H") |> throw : nothing
    size(B)[1] != n ? DimensionMismatch("Dimension mismatch of B") |> throw : nothing
    size(P)[1] != n ? DimensionMismatch("P and F incompatible") |> throw : nothing
    size(Q)[1] != n ? DimensionMismatch("Q and F incompatible") |> throw : nothing
    size(R)[1] != m ? DimensionMismatch("R and C incompatible") |> throw : nothing
    !isposdef(P) ? DomainError("P must be positive-definite") |> throw : nothing
    !isposdef(Q) ? DomainError("Q must be positive-definite") |> throw : nothing
    !isposdef(R) ? DomainError("R must be positive-definite") |> throw : nothing

    c = Base.RefValue{T}(0)
    pdy = Base.RefValue{T}(1)
    x0 = copy(x)
    F = copy(F)
    H = copy(H)
    B = copy(B)
    P = copy(P)
    Q = copy(Q)
    R = copy(R)
    offset = copy(offset)
    KalmanFilter{T}(x0, F, H, B, P, Q, R, offset, c, pdy)
end

# lazy wrappers
function KalmanFilter(x0, F, H, P0, Q, R)
    size(x0) == () ? x0 = [x0] : nothing
    B = zeros(eltype(x0),size(x0,1))
    KalmanFilter(x0, F, H, B, P0, Q, R)
end

function KalmanFilter(x0, F, H, B, P0, Q, R)
    size(x0) == () ? x0 = [x0] : nothing
    offset = zeros(eltype(x0),size(x0))
    KalmanFilter(x0, F, H, B, P0, Q, R, offset)
end

function KalmanFilter(x0, F, H, B, P0, Q, R, offset)
    size(x0) == () ? x0 = [x0] : nothing
    size(F) == () ? F = fill(F,1,1) : nothing
    size(H) == () ? H = fill(H,1,1) : nothing
    size(B) == () ? B = fill(B,1,1) :
        typeof(B) <: AbstractVector ? B = reshape(B, (:,1)) : nothing
    size(P0) == () ? P0 = fill(P0, 1,1) : nothing
    size(Q) == () ? Q = fill(Q, 1,1) : nothing
    size(R) == () ? R = fill(R, 1,1) : nothing
    size(offset) == () ? offset = [offset] : nothing

    x0, F, H, B, P0, Q, R, offset = promote_arrays(x0, F, H, B, P0, Q, R, offset)

    return KalmanFilter(x0, F, H, B, P0, Q, R, offset)

end

# Utility functions
function promote_arrays(arrays...)
    eltype = Base.promote_eltype(arrays...)
    eltype <: AbstractFloat ? nothing : eltype = Float64
    return tuple([convert(Array{eltype}, array) for array in arrays]...)
end


struct DivConformity <: Conformity end

"""
    RaviartThomasRefFE(::Type{et},p::Polytope,order::Integer) where et

The `order` argument has the following meaning: the divergence of the  functions in this basis
is in the Q space of degree `order`.
"""
function RaviartThomasRefFE(::Type{et},p::Polytope,order::Integer) where et

  D = num_dims(p)

  prebasis = QCurlGradMonomialBasis{D}(et,order)

  nf_nodes, nf_moments = _RT_nodes_and_moments(et,p,order)

  face_own_dofs = _face_own_dofs_from_moments(nf_moments)

  face_dofs = face_own_dofs

  dof_basis = MomentBasedDofBasis(nf_nodes, nf_moments)

  ndofs = num_dofs(dof_basis)

  metadata = nothing

  reffe = GenericRefFE(
    ndofs,
    p,
    prebasis,
    dof_basis,
    DivConformity(),
    metadata,
    face_dofs)

  reffe
end

function get_face_own_dofs(reffe::GenericRefFE{DivConformity}, conf::DivConformity)
  get_face_dofs(reffe)
end

function _RT_nodes_and_moments(::Type{et}, p::Polytope, order::Integer) where et

  @notimplementedif ! is_n_cube(p)

  D = num_dims(p)
  ft = VectorValue{D,et}
  pt = Point{D,et}

  nf_nodes = [ zeros(pt,0) for face in 1:num_faces(p)]
  nf_moments = [ zeros(ft,0,0) for face in 1:num_faces(p)]

  fcips, fmoments = _RT_face_values(p,et,order)
  frange = get_dimrange(p,D-1)
  nf_nodes[frange] = fcips
  nf_moments[frange] = fmoments

  if (order > 0)
    ccips, cmoments = _RT_cell_values(p,et,order)
    crange = get_dimrange(p,D)
    nf_nodes[crange] = ccips
    nf_moments[crange] = cmoments
  end

  nf_nodes, nf_moments
end

# Ref FE to faces geomaps
function _ref_face_to_faces_geomap(p,fp)
  cfvs = get_face_coordinates(p,num_dims(fp))
  nc = length(cfvs)
  freffe = LagrangianRefFE(Float64,fp,1)
  fshfs = get_shapefuns(freffe)
  cfshfs = fill(fshfs, nc)
  fgeomap = lincomb(cfshfs,cfvs)
end

function _nfaces_evaluation_points_weights(p, fgeomap, fips, wips)
  nc = length(fgeomap)
  c_fips = fill(fips,nc)
  c_wips = fill(wips,nc)
  pquad = evaluate(fgeomap,c_fips)
  c_fips, pquad, c_wips
end

function _broadcast(::Type{T},n,b) where T
  c = Array{T}(undef,size(b))
  for (ii, i) in enumerate(b)
    c[ii] = i*n
  end
  return c
end

function _RT_face_moments(p, fshfs, c_fips, fcips, fwips)
  nc = length(c_fips)
  cfshfs = fill(fshfs, nc)
  cvals = evaluate(cfshfs,c_fips)
  cvals = [fwips[i].*cvals[i] for i in 1:nc]
  # fns, os = get_facet_normals(p)
  fns = get_facet_normals(p)
  os = get_facet_orientations(p)
  # @santiagobadia : Temporary hack for making it work for structured hex meshes
  cvals = [ _broadcast(typeof(n),n*o,b) for (n,o,b) in zip(fns,os,cvals)]
  #cvals = [ _broadcast(typeof(n),n,b) for (n,b) in zip(fns,cvals)]
  return cvals
end

# It provides for every face the nodes and the moments arrays
function _RT_face_values(p,et,order)

  # Reference facet
  @assert is_simplex(p) || is_n_cube(p) "We are assuming that all n-faces of the same n-dim are the same."
  fp = Polytope{num_dims(p)-1}(p,1)

  # geomap from ref face to polytope faces
  fgeomap = _ref_face_to_faces_geomap(p,fp)

  # Nodes are integration points (for exact integration)
  # Thus, we define the integration points in the reference
  # face polytope (fips and wips). Next, we consider the
  # n-face-wise arrays of nodes in fp (constant cell array c_fips)
  # the one of the points in the polytope after applying the geopmap
  # (fcips), and the weights for these nodes (fwips, a constant cell array)
  # Nodes (fcips)
  degree = (order+1)*2
  fquad = Quadrature(fp,degree)
  fips = get_coordinates(fquad)
  wips = get_weights(fquad)

  c_fips, fcips, fwips = _nfaces_evaluation_points_weights(p, fgeomap, fips, wips)

  # Moments (fmoments)
  # The RT prebasis is expressed in terms of shape function
  fshfs = MonomialBasis(et,fp,order)

  # Face moments, i.e., M(Fi)_{ab} = q_RF^a(xgp_RFi^b) w_Fi^b n_Fi ⋅ ()
  fmoments = _RT_face_moments(p, fshfs, c_fips, fcips, fwips)

  return fcips, fmoments

end

function _RT_cell_moments(p, cbasis, ccips, cwips)
  # Interior DOFs-related basis evaluated at interior integration points
  ishfs_iips = evaluate(cbasis,ccips)
  return cwips.*ishfs_iips
end

# It provides for every cell the nodes and the moments arrays
function _RT_cell_values(p,et,order)
  # Compute integration points at interior
  degree = 2*(order+1)
  iquad = Quadrature(p,degree)
  ccips = get_coordinates(iquad)
  cwips = get_weights(iquad)

  # Cell moments, i.e., M(C)_{ab} = q_C^a(xgp_C^b) w_C^b ⋅ ()
  cbasis = QGradMonomialBasis{num_dims(p)}(et,order-1)
  cmoments = _RT_cell_moments(p, cbasis, ccips, cwips )

  return [ccips], [cmoments]

end

function _face_own_dofs_from_moments(f_moments)
  face_dofs = Vector{Int}[]
  o = 1
  for moments in f_moments
    ndofs = size(moments,2)
    dofs = collect(o:(o+ndofs-1))
    push!(face_dofs,dofs)
    o += ndofs
  end
  face_dofs
end

struct MomentBasedDofBasis{P,V} <: Dof
  nodes::Vector{P}
  face_moments::Vector{Array{V}}
  face_nodes::Vector{UnitRange{Int}}

  function MomentBasedDofBasis(nodes,f_moments,f_nodes)
    P = eltype(nodes)
    V = eltype(eltype(f_moments))
    new{P,V}(nodes,f_moments,f_nodes)
  end

  function MomentBasedDofBasis(f_nodes,f_moments)
    P = eltype(eltype(f_nodes))
    V = eltype(eltype(f_moments))
    nodes = P[]
    face_nodes = UnitRange{Int}[]
    nfaces = length(f_nodes)
    n = 1
    for fi in 1:nfaces
      nodes_fi = f_nodes[fi]
      nini = n
      for node_fi in nodes_fi
        push!(nodes,node_fi)
        n += 1
      end
      nend = n-1
      push!(face_nodes,nini:nend)
    end
    new{P,V}(nodes,f_moments,face_nodes)
  end
end

get_nodes(b::MomentBasedDofBasis) = b.nodes
get_face_moments(b::MomentBasedDofBasis) = b.face_moments
get_face_nodes_dofs(b::MomentBasedDofBasis) = b.face_nodes

function num_dofs(b::MomentBasedDofBasis)
  n = 0
  for m in b.face_moments
    n += size(m,2)
  end
  n
end

function dof_cache(b::MomentBasedDofBasis,field)
  cf = field_cache(field,b.nodes)
  vals = evaluate_field!(cf,field,b.nodes)
  ndofs = num_dofs(b)
  r = _moment_dof_basis_cache(vals,ndofs)
  c = CachedArray(r)
  (c, cf)
end

function _moment_dof_basis_cache(vals::AbstractVector,ndofs)
  T = eltype(vals)
  r = zeros(eltype(T),ndofs)
end

function _moment_dof_basis_cache(vals::AbstractMatrix,ndofs)
  _, npdofs = size(vals)
  T = eltype(vals)
  r = zeros(eltype(T),ndofs,npdofs)
end

function evaluate_dof!(cache,b::MomentBasedDofBasis,field)
  c, cf = cache
  vals = evaluate_field!(cf,field,b.nodes)
  dofs = c.array
  _eval_moment_dof_basis!(dofs,vals,b)
  dofs
end

function _eval_moment_dof_basis!(dofs,vals::AbstractVector,b)
  o = 1
  z = zero(eltype(dofs))
  face_nodes = b.face_nodes
  face_moments = b.face_moments
  for face in 1:length(face_moments)
    moments = face_moments[face]
    if length(moments) != 0
      nodes = face_nodes[face]
      ni,nj = size(moments)
      for j in 1:nj
        dofs[o] = z
        for i in 1:ni
          dofs[o] += moments[i,j]⋅vals[nodes[i]]
        end
        o += 1
      end
    end
  end
end

function _eval_moment_dof_basis!(dofs,vals::AbstractMatrix,b)
  o = 1
  na = size(vals,2)
  z = zero(eltype(dofs))
  face_nodes = b.face_nodes
  face_moments = b.face_moments
  for face in 1:length(face_moments)
    moments = face_moments[face]
    if length(moments) != 0
      nodes = face_nodes[face]
      ni,nj = size(moments)
      for j in 1:nj
        for a in 1:na
          dofs[o,a] = z
          for i in 1:ni
            dofs[o,a] += moments[i,j]⋅vals[nodes[i],a]
          end
        end
        o += 1
      end
    end
  end
end


@testset "plots.jl" begin
    n = 5000

    y = rand(GeneralizedExtremeValue(1.0, 1.0, 0.1), n)
    fm = MaximumLikelihoodEVA(BlockMaxima(Variable("y", y)), [1.0, 0.0, 0.1])

    x = rand(n)
    yns = rand.(GeneralizedExtremeValue.(x, 1.0, 0.1))
    fmns = MaximumLikelihoodEVA(BlockMaxima(Variable("y", yns), locationcov = [Variable("x", x)]), [0.0, 1.0, 0.0, 0.1])

    @testset "probplot_data(fm)" begin
        df = probplot_data(fm)

        # Returns a dataframe with n values in column Model
        @test length(df[:, :Model]) == n

        # Returns a dataframe with n values in column Empirical
        @test length(df[:, :Empirical]) == n

    end

    @testset "probplot(fm)" begin
        # Plot does not throw
        @test_logs probplot(fm)

    end

    @testset "qqplot_data(fm)" begin
        df = qqplot_data(fm)

        # Returns a dataframe with n values in column Model
        @test length(df[:, :Model]) == n

        # Returns a dataframe with n values in column Empirical
        @test length(df[:, :Empirical]) == n

        # Info for non-stationary model but does not throw
        @test_logs (:info, "The graph is optimized for stationary models and the model provided is not.") qqplot_data(fmns)

    end

    @testset " qqplot(fm)" begin
        # Plot does not throw
        @test_logs qqplot(fm)

    end

    @testset "returnlevelplot_data(fm)" begin
        df = returnlevelplot_data(fm)

        # Returns a dataframe with n values in column Data
        @test length(df[:, :Data]) == n

        # Returns a dataframe with n values in column Period
        @test length(df[:, :Period]) == n

        # Returns a dataframe with n values in column Level
        @test length(df[:, :Level]) == n

    end

    @testset "returnlevelplot(fm)" begin
        # Plot does not throw
        @test_logs returnlevelplot(fm)

    end

    @testset "histplot_data(fm)" begin
        dfs = histplot_data(fm)

        # Returns a dataframe with n values in column Data
        @test length(dfs[:h][:, :Data]) == n

        # Returns a dataframe with 1000 values in column DataRange
        @test length(dfs[:d][:, :DataRange]) == 1000

        # Returns a dataframe with 1000 values in column Density
        @test length(dfs[:d][:, :Density]) == 1000

        # Returns a dictionary with key nbin
        @test haskey(dfs, :nbin)

        # Returns a dictionary with key xmin
        @test haskey(dfs, :xmin)

        # Returns a dictionary with key xmax
        @test haskey(dfs, :xmax)

        # Info for non-stationary model but does not throw
        @test_logs (:info, "The graph is optimized for stationary models and the model provided is not.") histplot_data(fmns)

    end

    @testset "histplot(fm)" begin
        # Plot does not throw
        @test_logs histplot(fm)

    end

    @testset "diagnosticplots(fm)" begin
        # Plots do not throw
        @test_logs diagnosticplots(fm)

    end

    @testset "mrlplot_data(y)" begin

        σ = 1.0
        ξ = 0.1
        pd = GeneralizedPareto(σ, ξ)
        y = rand(pd, 1000)

        @test_throws AssertionError mrlplot_data(y, -3)

        df = mrlplot_data(y, 10)

        # Returns a dictionary with the correct keys
        @test hasproperty(df, :Threshold)
        @test hasproperty(df, :mrl)
        @test hasproperty(df, :lbound)
        @test hasproperty(df, :ubound)

        # Theoretical values
        m = (σ .+ ξ * df[:, :Threshold]) / (1 - ξ)

        @test df[1, :lbound] < m[1] < df[1, :ubound]
        @test df[5, :lbound] < m[5] < df[5, :ubound]

    end

    @testset "mrlplot(y)" begin
        # Plots do not throw
        @test_logs mrlplot([.5; .8; .9])

    end

end


@testset "Initial Convergence Handling" begin
    f(x) = x[1]^2
    function g!(out, x)
        out[1] = 2.0 * x[1]
    end
    function h!(out, x)
        out[1,1] = 2.0
    end
    for Optimizer in (AcceleratedGradientDescent,
                      GradientDescent, ConjugateGradient, LBFGS, BFGS,
                      MomentumGradientDescent)

        res = optimize(f, g!, [0.], Optimizer())
        @test Optim.minimizer(res)[1] ≈ 0.
    end

    for Optimizer in (Newton, NewtonTrustRegion)
        res = optimize(f, g!, h!, [0.], Optimizer())
        @test Optim.minimizer(res)[1] ≈ 0.
    end
end


using SubpixelRegistration
using FFTW
using Test

## Testing the registration on a small 4d array
@testset "Testing the registration on a small 4d array" begin
    test4d = zeros(40,40,20,2)

    test4d[10:20,10:20,2:10,1] .= 1
    test4d[5:15,15:25,5:13,2] .= 1

    ## No subpixel resolution
    dftResults = stackDftReg(test4d,ufac=1)

    @test length(dftResults) == size(test4d)[4]
    @test dftResults[2]["shift"] == [5,-5,-3]

    ## Factor 2
    dftResults = stackDftReg(test4d,ufac=2)

    @test length(dftResults) == size(test4d)[4]
    @test dftResults[2]["shift"] == [5,-5,-3]

    # More refined (needs dftups)
    dftResults = stackDftReg(test4d,ufac=3)

    @test length(dftResults) == size(test4d)[4]
    @test dftResults[2]["shift"] == [5,-5,-3]

    ## Testing the translation
    back4d = alignFromDict(test4d,dftResults)
    @test all(round.(back4d[:,:,:,2]-back4d[:,:,:,1]).<eps())
end


using UtilitiesForMRI, LinearAlgebra, Test, Random, AbstractLinearOperators

# Cartesian domain
n = (256,256,256)
h = (abs(randn()), abs(randn()), abs(randn()))
X = spatial_sampling(Float64, n; h=h)

# Cartesian sampling in k-space
phase_encoding = (1,2)
subsampling = (1:256^2)[randperm(256^2)][1:32]
K = kspace_Cartesian_sampling(X; phase_encoding=phase_encoding, subsampling=subsampling)

# Fourier operator
tol = 1e-6
F = nfft(X, K; tol=tol)

# Adjoint test (linear operator)
nt, nk = size(K)
θ = 1e-3*pi*randn(Float64, nt, 6)
Fθ = F(θ)
u = randn(ComplexF64, n)
d = randn(ComplexF64, nt, nk)
@test dot(Fθ*u, d) ≈ dot(u, Fθ'*d) rtol=1e-6

# Jacobian & consistency test
d, _, ∂Fθu = ∂(F()*u, θ)
@test d ≈ Fθ*u rtol=1e-6

# Adjoint test (Jacobian)
Δθ = randn(Float64, nt, 6); Δθ *= norm(θ)/norm(Δθ)
ΔF_ = ∂Fθu*Δθ; ΔF = randn(ComplexF64, size(ΔF_)); ΔF *= norm(ΔF_)/norm(ΔF)
@test dot(∂Fθu*Δθ, ΔF) ≈ dot(Δθ, ∂Fθu'*ΔF) rtol=1e-6

# Gradient test
Δθ = randn(Float64, nt, 6); Δθ *= norm(θ)/norm(Δθ)
t = 1e-6
Fθu_p1 = F(θ+0.5*t*Δθ)*u
Fθu_m1 = F(θ-0.5*t*Δθ)*u
@test (Fθu_p1-Fθu_m1)/t ≈ ∂Fθu*Δθ rtol=1e-3

# Gauss-Newton Hessian
w = randn(ComplexF64, size(ΔF))
W = linear_operator(ComplexF64, size(ΔF), size(ΔF), d->w.*d, d-> conj(w).*d)
h = randn(ComplexF64, size(ΔF))
H = linear_operator(ComplexF64, size(ΔF), size(ΔF), d->h.*d, d-> conj(h).*d)
HF = sparse_matrix_GaussNewton(∂Fθu; W=W, H=H)

# Consistency test
Δθ = randn(Float64, nt, 6)
@test HF*vec(Δθ) ≈ vec(real(∂Fθu'*W'*(H*(W*(∂Fθu*Δθ))))) rtol=1e-6


@benchgroup "mapwindow" begin
    img1d = randn(1000)
    img2d = randn(30,30)
    img3d = randn(10,11,12)
    @bench "cheap f, tiny window" mapwindow(first, img1d, (1,))
    @bench "extrema" mapwindow(extrema, img2d, (5,5))
    @bench "median!" mapwindow(median!, img2d, (5,5))
    @bench "mean, small window" mapwindow(mean, img1d, (3,))
    @bench "mean, large window" mapwindow(mean, img3d, (5,5,5))
    @bench "expensive f" mapwindow(x -> quantile(vec(x), 0.7), img3d, (3,3,3))
end


"""
$(SIGNATURES)
"""
function plot_as_quiver(
    mesh::FaceMesh;
    n_steps = 20,
    scale = 30,
    max_length = N_Pixel_Width / n_steps,
    fliprc = false,
    reverser = false,
    reversec = false,
)

    height, width = size(mesh)
    stride = Int64(ceil(max(height, width) / n_steps))

    row_s = Float64[]
    col_s = Float64[]
    v_rows = Float64[]
    v_cols = Float64[]

    # The gradient vectors reflect change of luminosity:outward flow = bigger cells = more luminosity
    # Plots are cosmetically look better when showing the light focusing to a specific location.
    # Those gradients are the opposite direction.
    ϕ = -mesh.corners.ϕ

    v_length = sqrt.(mesh.corners.vr .^ 2 + mesh.corners.vr .^ 2)
    v_max = maximum(v_length)

    mat_vr = (reverser ? -1 : 1) .* mesh.corners.vr * scale / v_max
    mat_vc = (reversec ? -1 : 1) .* mesh.corners.vc * scale / v_max

    mat_vr = clamp.(mat_vr, -max_length, max_length)
    mat_vc = clamp.(mat_vc, -max_length, max_length)

    for row = 1:stride:height, col = 1:stride:width
        reverser ? push!(row_s, row) : push!(row_s, -row)
        reversec ? push!(col_s, -col) : push!(col_s, col)

        push!(v_rows, mat_vr[row, col])
        push!(v_cols, mat_vc[row, col])
    end

    display(
        fliprc ? quiver(row_s, col_s, quiver = (v_rows, v_cols)) :
        quiver(col_s, row_s, quiver = (v_cols, v_rows)),
    )
    return nothing
end

"""
$(SIGNATURES)
"""
function plot_as_quiver(
    ϕ;
    n_steps = 20,
    scale = 300,
    max_length = N_Pixel_Width / n_steps,
    fliprc = false,
    reverser = false,
    reversec = false,
)

    height, width = size(ϕ)
    stride = Int64(ceil(max(height, width) / n_steps))

    row_s = Float64[]
    col_s = Float64[]
    Δrow = Float64[]
    Δcol = Float64[]

    for row = 1:stride:height, col = 1:stride:width
        dr = (-(ϕ[row, col+1] - ϕ[row, col]) * scale)
        dc = (ϕ[row+1, col] - ϕ[row, col]) * scale

        reverser ? push!(row_s, col) : push!(row_s, width + 1 - col)
        reversec ? push!(col_s, height + 1 - row) : push!(col_s, row)
        reverser && (dr = -dr)
        reversec && (dc = -dc)

        push!(Δrow, clamp(dr, -maxlength, max_length))
        push!(Δcol, clamp(dc, -maxlength, max_length))
    end

    # By default, we flip to match the original image.
    display(
        fliprc ? quiver(row_s, col_s, quiver = (Δrow, Δcol)) :
        quiver(col_s, row_s, quiver = (Δcol, Δrow)),
    )

    return nothing
end

"""
$(SIGNATURES)
"""
function plot_velocities_as_quiver(
    vr,
    vc;
    n_steps = 20,
    scale = 300,
    max_length = N_Pixel_Width / n_steps,
)

    height, width = size(vr)
    stride = Int64(ceil(max(height, width) / n_steps))

    row_s = Float64[]
    col_s = Float64[]
    Δrow = Float64[]
    Δcol = Float64[]

    dr = max.(vr, 0.001)
    dc = max.(vc, 0.001)

    for row = 1:stride:height, col = 1:stride:width
        push!(row_s, height - row)
        push!(col_s, col)

        push!(Δrow, dr[row, col])
        push!(Δcol, dc[row, col])
    end

    # readline()
    display(quiver(row_s, col_s, quiver = (Δrow, Δcol), aspect_ratio = :equal))
end

"""
$(SIGNATURES)
"""
function save_plot_scalar_field!(scalar_field, filename)
    blue = zeros(Float64, size(scalar_field))
    red = zeros(Float64, size(scalar_field))
    green = zeros(Float64, size(scalar_field))

    height, width = size(scalar_field)

    for r ∈ 1:height, c ∈ 1:width
        field = scalar_field[r, c]
        if field < -1.0
            # Green if luminosity error < -1.0
            red[r, c] = 0.0
            blue[r, c] = 0.0
            green[r, c] = 1.0

        elseif -1.0 <= field < 0.0
            # Red shade if negative
            red[r, c] = -field
            blue[r, c] = 0.0
            green[r, c] = 0.0

        elseif 0.0 <= field < 1.0
            # Blue shade if positive
            red[r, c] = 0.0
            blue[r, c] = field
            green[r, c] = 0.0

        elseif 1.0 <= field
            # White  if > 1
            red[r, c] = 1.0
            blue[r, c] = 1.0
            green[r, c] = 1.0
        end
    end

    rgbImg = RGB.(red, green, blue)
    save("./examples/$(filename).png", rgbImg)
end

"""
$(SIGNATURES)
"""
function save_plot_scalar_field(scalar_field, prefix, img)
    normalised_D_max = scalar_field
    normalised_D_min = scalar_field
    # normalised_D_max = scalar_field ./ maximum(scalar_field)
    # normalised_D_min = scalar_field ./ minimum(scalar_field)

    blue = zeros(size(scalar_field))
    red = zeros(size(scalar_field))
    green = zeros(size(scalar_field))

    blue[scalar_field.>0] = normalised_D_max[scalar_field.>0]
    red[scalar_field.<0] = -normalised_D_min[scalar_field.<0]

    rgbImg = map(clamp01nan, RGB.(red, green, blue))
    save("./examples/$(prefix)_loss.png", map(clamp01nan, rgbImg))

    println("Saving output image:")
    println(typeof(img))
    E = Gray.(scalar_field)
    println(typeof(E))
    outputImg = img - E
    save("./examples/$(prefix)_actual.png", outputImg)
end


# Contains all functions handling geometry

"""
    Calculate spherical coordinates arrays representing the surface of the particle
This can be either 1D arrays for axi-symmetric particles or 2D arrays for general 3D particles
"""
function get_r_θ_ϕ_arrays(geometry; angular_resolution=0.5, θ_array=nothing, ϕ_array=nothing)
    # TODO: use a package that takes a mesh and do integrals
    # check: https://github.com/jareeger/Smooth_Closed_Surface_Quadrature_RBF-julia

    if geometry isa String
        if geometry in ["wire", "cylinder"]
        elseif geometry in ["spheroid", "ellipsoid"]
        end
    elseif geometry isa Function
        # TODO: check if the function has one or two arguments

    else
        throw(DomainError("Please define geometry as 'String', 'Function', or 'Filepath'"))
    end
end

function create_array_from_elements(x1, x2, x3)
    # TODO: @Alok, should I use SVector? what are the drawbacks?
    # return [x1,x2,x3]
    return SVector(x1, x2, x3)
end

"""
    Claculate radial coordinate of ellipsoidal particle surface, given θ points
returns r and n̂ coordinate as a function of θ_array
"""
function ellipsoid(rx, rz, θ_array)
    r = rx .* rz ./ sqrt.((rx .* cos.(θ_array)).^2 + (rz .* sin.(θ_array)).^2)
    ∂r_by_∂θ = (rx.^2 - rz.^2) / (rx.^2 * rz.^2) .* r.^3 .* sin.(θ_array) .* cos.(θ_array)

    n̂_r_comp = r ./ sqrt.(r.^2 + ∂r_by_∂θ.^2)
    n̂_θ_comp = -∂r_by_∂θ ./ sqrt.(r.^2 + ∂r_by_∂θ.^2)
    n̂_ϕ_comp = zeros(Real, size(r)) # TODO: I need to make the type the same as the type of r

    # n̂ = create_array_from_elements.(n̂_r_comp, n̂_θ_comp, n̂_ϕ_comp) # TODO: verify that you can delete this line
    # n̂ = SVector.(n̂_r_comp, n̂_θ_comp, n̂_ϕ_comp)
    # n̂ = Vector.(n̂_r_comp, n̂_θ_comp, n̂_ϕ_comp)
    n̂ = reshape([Vector([n̂_r_comp[id], n̂_θ_comp[id], n̂_ϕ_comp[id]]) for id in eachindex(n̂_r_comp)], size(n̂_r_comp))
    return r, n̂
end

"""
    Create 1D mesh, then get r, θ and n̂_θ_comp for points between each two consecutive points defined by r_edges_array, θ_edges_array
It returns a Matrix, first and second columns are for r,θ, respectively. The third, fourth and fifth columns are for the three compoonents of the normal direction.
"""
function create_1D_mesh_and_get_r_n̂(r_edges_array::AbstractVector{R}, θ_edges_array::AbstractVector{R}) where R <: Real
    z = find_point_splitting_line_between_two_points_and_normal_vector_to_this_line.(r_edges_array[1:end - 1], θ_edges_array[1:end - 1], r_edges_array[2:end], θ_edges_array[2:end]);
    """
    r_array = [z[id][1] for id = 1 : length(z)]
    θ_array = [z[id][2] for id = 1 : length(z)]
    n̂_array = [z[id][3] for id = 1 : length(z)]
    """
    return vcat(z...)
end

function create_1D_mesh_and_get_r_n̂_with_plotting(r_edges_array::AbstractVector{R}, θ_edges_array::AbstractVector{R}) where R <: Real
    z = create_1D_mesh_and_get_r_n̂(r_edges_array, θ_edges_array);
end

"""
    Find a point (r,θ) that split a line connecting two points (r1, θ1), and (r2, θ2) into two equal splits, and find the normal unit vector is spherical coordinates
"""
function find_point_splitting_line_between_two_points_and_normal_vector_to_this_line(r1::R, θ1::R, r2::R, θ2::R) where R <: Real
    S = calculate_distance_between_two_points(r1, θ1, r2, θ2)
    angle_between_r1_and_S = asin(r2 * sin(θ2 - θ1) / S)
    # to fix the inverse sin of obtuse angle
    if r2 > r1; angle_between_r1_and_S = pi - angle_between_r1_and_S; end
    r = sqrt(r1^2 + (S / 2)^2 - 2 * r1 * (S / 2) * cos(angle_between_r1_and_S))
    angle_between_r1_and_r = asin(S / 2 * sin(angle_between_r1_and_S) / r) # TODO: do the check for the "obtuse" angle
    θ = angle_between_r1_and_r + θ1

    γ = pi - angle_between_r1_and_S - angle_between_r1_and_r

    n̂_θ_comp = -cos(γ)
    n̂_r_comp = sin(γ)

    # println("r1=$r1, θ1=$θ1, r2=$r2, θ2=$θ2, r=$r, S=$S, θ=$θ, angle_between_r1_and_S/pi=$(angle_between_r1_and_S/pi), asin($(r2 * sin(θ2-θ1) / S)), θ2-θ1=$(θ2-θ1)")
    return hcat(r, θ, n̂_r_comp, n̂_θ_comp, zero(R))
    # return hcat(r, θ, n̂_θ_comp)
end

"""
    Find distance between two points (r1, θ1), and (r2, θ2)
"""
function calculate_distance_between_two_points(r1::R, θ1::R, r2::R, θ2::R) where R <: Real
    return sqrt(r1^2 + r2^2 - 2 * r1 * r2 * cos(θ2 - θ1))
end

"""
    Calculate the total length of a 1D mesh, defined as r and θ arrays.
"""
function calculate_total_length_of_1D_mesh(r_array::AbstractVector{R}, θ_array::AbstractVector{R}) where R <: Real
    return sum(
        calculate_distance_between_two_points.(
            r_array[1:end - 1], θ_array[1:end - 1], r_array[2:end], θ_array[2:end]
        )
    )
end

"""
    Calculate the surface area of an axisymmetric particle, defined by a 1D mesh.
"""
function calculate_surface_area_of_axisymmetric_particle(r_array::AbstractVector{R}, θ_array::AbstractVector{R}) where R <: Real
    return sum(
        calculate_lateral_surface_area_of_truncated_cone.(
            r_array[1:end - 1], θ_array[1:end - 1], r_array[2:end], θ_array[2:end]
        )
    )
end

"""
    Calculate surface area of a truncated cone defined by two points revolving about the Z axis.
"""
function calculate_lateral_surface_area_of_truncated_cone(r1::R, θ1::R, r2::R, θ2::R) where R <: Real
    r = r1 * sin(θ1)
    RR = r2 * sin(θ2)
    s = abs(r1 * cos(θ1) - r2 * cos(θ2))
    return pi * (r + RR) * sqrt((r - RR)^2 + s^2)
end

"""
    Calculate the volume of an axisymmetric particle, defined by a 1D mesh.
"""
function calculate_volume_of_axisymmetric_particle(r_array::AbstractVector{R}, θ_array::AbstractVector{R}) where R <: Real
    return sum(
        calculate_volume_of_truncated_cone.(
            r_array[1:end - 1], θ_array[1:end - 1], r_array[2:end], θ_array[2:end]
        )
    )
end

"""
    Calculate volume of a truncated cone defined by two points revolving about the Z axis.
"""
function calculate_volume_of_truncated_cone(r1::R, θ1::R, r2::R, θ2::R) where R <: Real
    r = r1 * sin(θ1)
    RR = r2 * sin(θ2)
    s = abs(r1 * cos(θ1) - r2 * cos(θ2))
    return 1 / 3 * pi * (r^2 + r * RR + RR^2) * s
end

"""
    Calculate surface area for an ellipsoid
    following equations in SMARTIES paper: 1. Somerville, W. R. C., Auguié, B. & Le Ru, E. C. Smarties: User-friendly codes for fast and accurate calculations of light scattering by spheroids. J. Quant. Spectrosc. Radiat. Transf. 174, 39–55 (2016).
    verified against results from this area and volume calculator https://planetcalc.com/149/
"""
function get_volume_area_for_ellipsoid(rx, rz)
    h = rx / rz
    if h < 1; h = 1 / h; end  # h is the aspect ratio that is larger than 1
    e = sqrt(h^2 - 1) / h

    volume = 4 / 3 * pi * rx^2 * rz  # eq. 9
    if rx > rz  # oblate
        area_surface = (
            2 * pi * rx^2 * (1 + ((1 - e^2) / e) * atanh(e))
        )  # eq. 11
    elseif rx == rz  # sphere
        area_surface = 4 * pi * rx^2
    else  # prolate
        area_surface = 2 * pi * rx^2 * (1 + rz / (rx * e) * asin(e))  # eq. 11
    end
    return volume, area_surface
end

function get_volume_area_for_geometry(geometry_name, geometry_parameters...)
    if geometry_name in ["spheroid", "ellipsoid"]
        return get_volume_area_for_ellipsoid(geometry_parameters...)
    end
end

function convert_coordinates_Cart2Sph(x, y, z)
    r = sqrt((x^2) + (y^2) + (z^2))
    theta = atan(sqrt((x^2) + (y^2)), z)
    phi = atan(y, x)
    return hcat(r, theta, phi)
end

function convert_coordinates_Sph2Cart(r, theta, phi)

    x = r * sin(theta) * cos(phi)
    y = r * sin(theta) * sin(phi)
    z = r * cos(theta)

    return hcat(x, y, z)
end

"""
    Given r and θ arrays, this function returns another r and θ arrays covering the same range of θ, with normal direction vector
Note that the first and last elements of θ_array are assumed to be 0 and pi, respectively (or numbers very close to these). We assume that the n̂ is pointing at +ve and -ve z directions at these points.
"""
function get_r_θ_n̂_arrays_from_r_θ_arrays_axisymmetrix(r_array::AbstractVecOrMat{R}, θ_array::AbstractVecOrMat{R}) where R <: Real
    # TODO: I need to make sure that the first and last elements of θ_array are 0 and pi, respectively (or numbers very close to these). Note that we assume that the n̂ is pointing at +ve and -ve z directions at these points.
    r_theta_n̂ = create_1D_mesh_and_get_r_n̂(r_array, θ_array)
    return vcat(
        [r_array[1], θ_array[1], one(R), zero(R), zero(R)]',
        r_theta_n̂,
        [r_array[end], θ_array[end], one(R), zero(R), zero(R)]',
    )
end

"""
Increase mesh density by 4x, by dividing every line element into two equal parts.
Works only for 1D mesh.
"""
function double_mesh_density(r_array, θ_array)
    z = Tmatrix.create_1D_mesh_and_get_r_n̂(r_array, θ_array)
    r_middles = z[:,1]
    θ_middles = z[:,2]
    # return hcat(r_middles, θ_middles)

    """
    r_all = vcat(r_array,r_middles)
    r_all[1:2:end] = r_array
    r_all[2:2:end] = r_middles

    θ_all = vcat(θ_array,θ_middles)
    θ_all[1:2:end] = θ_array
    θ_all[2:2:end] = θ_middles
    return hcat(r_all, θ_all)
    """

    return hcat(
        add_elements_from_two_arrays_in_turn(r_array, r_middles),
        add_elements_from_two_arrays_in_turn(θ_array, θ_middles),
        )
end

"""
Increase mesh density by 4x, by dividing every line element into four equal parts.
Works only for 1D mesh.
"""
function quadruple_mesh_density(r_array, θ_array)
    r_theta_array_double = double_mesh_density(r_array, θ_array)
    return double_mesh_density(r_theta_array_double[:,1], r_theta_array_double[:,2])
end

"""
Increase mesh density by 8x, by dividing every line element into eight equal parts.
Works only for 1D mesh.
"""
function octuple_mesh_density(r_array, θ_array)
    r_theta_array_quadruple = quadruple_mesh_density(r_array, θ_array)
    return double_mesh_density(r_theta_array_quadruple[:,1], r_theta_array_quadruple[:,2])
end

"""
Increase mesh density by 16x, by dividing every line element into sixteen equal parts.
Works only for 1D mesh.
"""
function sexdecuple_mesh_density(r_array, θ_array)
    r_theta_array_octuple = octuple_mesh_density(r_array, θ_array)
    return double_mesh_density(r_theta_array_octuple[:,1], r_theta_array_octuple[:,2])
end

"""
julia> a = [1,2,3]; b = [10,20];
julia> add_elements_from_two_arrays_in_turn(a,b)
julia> 5-element Vector{Int64}:
        1
        10
        2
        20
        3
"""
function add_elements_from_two_arrays_in_turn(array_1, array_2)
    return [isodd(i) ? array_1[Int((i + 1) / 2)] : array_2[Int(i / 2)] for i = 1:(length(array_1) + length(array_2))]
end


"""
Correlogram(data::AbstractVector, lag::Int64 = 20, fft::Bool = true)

Correlogram function calculates the autocorrelation function of given data. To use this software, you must input your dataset into a 'data' argument. You have two options to calculate autocorrelation, which use Fourier fast transformation (FFT) and calculate by its definition. We calculate the autocorrelation function by lag if you set 'lag' arguments. If you set 'fft' argument true, we calculate the autocorrelation function by FFT. Otherwise, we calculate them by definition.
"""
function Correlogram(d::AbstractVector, l::Int64 = 20, f::Bool = true ;data::AbstractVector = d, lag::Int64 = l, fft::Bool = f)
    n::Int64 = length(data)
    s::Float64 = zeros(Float64, 2*n-1 )
    s[1:n] .= data
    Fs = real.(ifft(abs2.(fft(s))))
    return Fs
end


# Example of vOptGeneric, for a 2IP (here a 2LAP)
# July 2017

n  =  3

C1 = [  3    9   7   ;
       16   10   6   ;
        2    7  11     ]

C2 = [ 16   15   6   ;
        5    7  13   ;
        1    2  13     ]

using vOptGeneric
using GLPK ; using GLPKMathProgInterface

m = vModel( solver = GLPKSolverMIP() )
@variable( m , x[1:n,1:n] , Bin )
@addobjective( m , Min, sum( C1[i,j]*x[i,j] for i=1:n,j=1:n ) )
@addobjective( m , Min, sum( C2[i,j]*x[i,j] for i=1:n,j=1:n ) )
@constraint( m , cols[i=1:n], sum(x[i,j] for j=1:n) == 1 )
@constraint( m , rows[j=1:n], sum(x[i,j] for i=1:n) == 1 )
@time solve( m , method = :epsilon , step = 0.5 )

print_X_E(m)
getY_N(m)


export Lanczos

abstract type AbstractLanczos <: InterpolationType end

"""
    Lanczos{N}(a=4)

Lanczos resampling via a kernel with scale parameter `a` and support over `N` neighbors.

This form of interpolation is merely the discrete convolution of the samples with a Lanczos kernel of size `a`. The size is directly related to how "far" the interpolation will reach for information, and has `O(N^2)` impact on runtime. An alternative implementation matching `lanczos4` from OpenCV is available as Lanczos4OpenCV.
"""
struct Lanczos{N} <: AbstractLanczos
    a::Int

    function Lanczos{N}(a) where N
        N < a && @warn "Using a smaller support than scale for Lanczos window. Proceed with caution."
        new{N}(a)
    end
end

Lanczos(a=4) = Lanczos{a}(a)

"""
    LanczosInterpolation
"""
struct LanczosInterpolation{T,N,IT <: DimSpec{AbstractLanczos},A <: AbstractArray{T,N},P <: Tuple{Vararg{AbstractArray,N}}} <: AbstractInterpolation{T,N,IT}
    coefs::A
    parentaxes::P
    it::IT
end

@generated degree(::Lanczos{N}) where {N} = :($N)

getknots(itp::LanczosInterpolation) = axes(itp)
coefficients(itp::LanczosInterpolation) = itp.coefs
itpflag(itp::LanczosInterpolation) = itp.it

size(itp::LanczosInterpolation) = map(length, itp.parentaxes)
axes(itp::LanczosInterpolation) = itp.parentaxes
lbounds(itp::LanczosInterpolation) = map(first, itp.parentaxes)
ubounds(itp::LanczosInterpolation) = map(last, itp.parentaxes)

function interpolate(A::AbstractArray{T}, it::AbstractLanczos) where T
    Apad = copy_with_padding(float(T), A, it)
    return LanczosInterpolation(Apad, axes(A), it)
end

@inline function (itp::LanczosInterpolation{T,N})(x::Vararg{<:Number,N}) where {T,N}
    @boundscheck (checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x))
    wis = weightedindexes((value_weights,), itpinfo(itp)..., x)
    itp.coefs[wis...]
end

function weightedindex_parts(fs, it::AbstractLanczos, ax::AbstractUnitRange{<:Integer}, x)
    pos, δx = positions(it, ax, x)
    (position = pos, coefs = fmap(fs, it, δx))
end

function positions(it::AbstractLanczos, ax, x)
    xf = floorbounds(x, ax)
    δx = x - xf
    fast_trunc(Int, xf) - degree(it) + 1, δx
end

function value_weights(it::Lanczos, δx::S) where S
    N = degree(it)
    # short-circuit if integral
    isinteger(δx) && return ntuple(i->convert(float(S), i == N - δx), Val(2N))

    # LUTs
    #it.a === N === 4 && return _lanczos4(δx)

    cs = ntuple(i -> lanczos(N - i + δx, it.a, N), Val(2N))
    sum_cs = sum(cs)
    normed_cs = ntuple(i -> cs[i] / sum_cs, Val(length(cs)))
    return normed_cs
end

function padded_axis(ax::AbstractUnitRange, it::AbstractLanczos)
    N = degree(it)
    return first(ax) - N + 1:last(ax) + N
end

# precise implementations for fast evaluation of common kernels

"""
    lanczos(x, a, n=a)

Implementation of the [Lanczos kernel](https://en.wikipedia.org/wiki/Lanczos_resampling)
"""
lanczos(x::T, a::Integer, n=a) where {T} = abs(x) < n ? T(sinc(x) * sinc(x / a)) : zero(T)


module LinearVortexSource

using LinearAlgebra
using StaticArrays
using Base.Iterators

using ..AeroMDAO: AbstractPanel, AbstractPanel2D, Panel2D, WakePanel2D, Point2D, collocation_point, p1, p2, transform_panel, affine_2D, panel_length, panel_angle, panel_tangent, panel_normal, panel_dist, rotation, inverse_rotation, midpair_map, panel_velocity, pressure_coefficient, wake_panel, wake_panels, panel_points

include("singularities.jl")

export constant_source_velocity, linear_source_velocity_a, linear_source_velocity_b, linear_vortex_velocity_a, linear_vortex_velocity_b

include("matrix.jl")

export total_velocity, source_velocity, vortex_velocity, vortex_influence_matrix, source_influence_matrix, neumann_boundary_condition, kutta, two_point_matrix, source_matrix, vortex_matrix, constant_source_matrix, constant_source_boundary_condition

end


using CompScienceMeshes
using BEAST
using Test

sphere = readmesh(joinpath(dirname(@__FILE__),"assets","sphere5.in"))
numcells(sphere)

κ = 2π
direction = point(0,0,1)
f = BEAST.HH3DPlaneWave(direction, κ)

v1 = f(point(0,0,0))
v2 = f(point(0,0,0.5))

@test v1 ≈ +1
@test v2 ≈ -1

import BEAST.∂n
p = ∂n(f)

s = chart(m,first(cells(m)))
c = neighborhood(s, [1,1]/3)

r = cartesian(c)
n = normal(s)

w1 = p(c)
w2 = -im*κ*dot(direction, n)*f(r)

w1 ≈ w2

N = BEAST.HH3DHyperSingularFDBIO(im*κ)
X = BEAST.lagrangec0d1(sphere)

numfunctions(X)

Nxx = assemble(N, X, X)

@test size(Nxx) == (numfunctions(X), numfunctions(X))


using LinearAlgebra, Statistics
# commandline arguments.  In reality, use ArgParse equivalents
num = parse(Int64,ARGS[2])
ϵ = randn(num)
println("mean = $(mean(ϵ)), std = $(std(ϵ)) of $num values")


"""
    checkfeasible(command::String, logicset::LogicalCombo; verbose=true, all=false, countany=false)

Is called when the user would like to check if a command produces a valid result,
possible result, or invalid result. The result is returned as a decimal from 0.0
to 1.0. With 0.0 being no matches and 1.0 being all matches.

### Arguments
* `verbose` : controls print
* `countall` : all sets have to be feasible or return 0
* `countany` : any set can be non-zero to return 1

### Examples
```julia
julia> myset = logicalparse("a, b, c ∈ red, blue, green")
a, b, c ∈ red, blue, green       feasible outcomes 27 ✓          :red blue blue

julia> myset = logicalparse("a != b,c; b = c ==> a = 'blue'", myset)
a != b,c                 feasible outcomes 12 ✓          :green blue blue
b = c ==> a = 'blue'     feasible outcomes 8 ✓           :blue green green

julia> checkfeasible("a = 'green' ==> b = 'red'", myset)
Check: a = 'green' ==> b = 'red' ... a = 'green' ==> b = 'red'   feasible outcomes 17 ✓          :blue red green
possible,  17 out of 21 possible combinations 'true'.
```
"""
function checkfeasible(command::String,
    logicset::LogicalCombo = LogicalCombo();
    verbose=true, countall=false, countany=false)

  clearcounter()

  countany && countall && throw("Both any and all can't be set to true")

  rowsin = sum(logicset.logical)

  if rowsin == 0
      println("No outcomes feasible with set!")
      return missing
  end

  verbose && !countall && !countany && print("check: $command ... ")
  verbose && countall && print("all: $command ... ")
  verbose && countany && print("any: $command ... ")

  logicsetout = logicalparse(command, logicset=logicset, verbose=verbose)

  rowsout = sum(logicsetout.logical)
  outcomeratio = rowsout/rowsin

  if countall
        outcomeratio != 1 && print("false")
        outcomeratio == 1 && print("true")
  elseif countany
        outcomeratio == 0 && print("false")
        outcomeratio != 0 && print("true")
  else
      outcomeratio == 1 && print("true")
      outcomeratio == 0 && print("false")
      (1 > outcomeratio > 0) && print("possible")
  end

  verbose && println(", $rowsout out of $rowsin possible combinations 'true'.")

  return [outcomeratio, logicsetout]
end


# https://raw.githubusercontent.com/sswatson/cheatsheets/master/plotsjl-cheatsheet.pdf

using Logging
logger = SimpleLogger(stdout, Logging.Info)
global_logger(logger)

using Plots
plotly()

using Polynomials

pt = plot(title="Learn how to plot", size=(600,600))
f = Polynomial([0,0,1])
plot!(pt, f, -2, 2)

gui(pt)

nothing


n = 5
println("The n = ", n, " spiral matrix:")
a = zeros(Int, (n, n))
for (i, s) in enumerate(spiral(n))
    a[s] = i-1
end
println(pretty(a))

m = 3
println()
println("Generalize to a non-square matrix (", m, "x", n, "):")
a = zeros(Int, (m, n))
for (i, s) in enumerate(spiral(m, n))
    a[s] = i-1
end
println(pretty(a))

p = primes(10^3)
n = 7
println()
println("An n = ", n, " prime spiral matrix:")
a = zeros(Int, (n, n))
for (i, s) in enumerate(spiral(n))
    a[s] = p[i]
end
println(pretty(a))


export UnionFind, addnode!, unify!, clusterize!, clusterid

@doc doc"""
Union-find algorithm.
"""
mutable struct UnionFind
    parents :: Vector{Int}
    weights :: Vector{Int}
    ids :: Vector{Int}
    nnodes :: Int
    nclusters :: Int
    UnionFind(n::Int=0) = new(collect(1:n),ones(Int,n),zeros(Int,n),n,n)
end

numnodes(u::UnionFind) = u.nnodes
numclusters(u::UnionFind) = u.nclusters

isroot(u::UnionFind, n::Integer) = u.parents[n] == n

@doc doc"""
    root!(u::UnionFind, n::Integer)

Returns the root node of the cluster where `n` belongs.
This may changes graph connection by "Path halving" method.
"""
function root!(u::UnionFind, n::Integer)
    @inbounds while !isroot(u,n)
        p = u.parents[n]
        u.parents[n] = u.parents[p]
        n = p
    end
    return n
end

@doc doc"""
    unify!(u, n1, n2)

Connects `n1` and `n2` nodes and returns the root.
"""
function unify!(u::UnionFind, n1::Integer, n2::Integer)
    r1 = root!(u,n1)
    r2 = root!(u,n2)
    @inbounds if r1 != r2
        u.nclusters -= 1
        if u.weights[r1] < u.weights[r2]
            r1,r2 = r2,r1
        end
        u.parents[r2] = r1
        u.weights[r2] += u.weights[r1]
    end
    return r1
end

@doc doc"""
    addnode!(u::UnionFind)

Adds a new node into `u` and returns the number of nodes including the added node.
"""
function addnode!(u::UnionFind)
    push!(u.parents,length(u.parents)+1)
    push!(u.weights,1)
    push!(u.ids, 0)
    u.nclusters += 1
    u.nnodes += 1
    return u.nnodes
end

@doc doc"""
    clusterize!(u::UnionFind)

Assigns cluster ID to each node and returns the number of clusters.
"""
function clusterize!(u::UnionFind)
    u.nclusters = 0
    @inbounds for i in 1:length(u.parents)
        if isroot(u,i)
            u.nclusters += 1
            u.ids[i] = u.nclusters
        end
    end
    @inbounds for i in 1:length(u.parents)
        u.ids[i] = u.ids[root!(u,i)]
    end
    return u.nclusters
end

@doc doc"""
    clusterid(u::UnionFind, i::Integer)

Returns the index of the cluster where `i` node belongs.
"""
clusterid(u::UnionFind, i::Integer) = u.ids[i]


"""
Finite set {0,1} × ... × {0,1}
"""
struct BooleanCube{S} <: AbstractProduct
    itr :: UnitRange{S}
    n :: Int
end
const BoolCube = BooleanCube
const BCube = BooleanCube

"""
construct a boolean cube with given number of dimensions
"""
BCube(n :: Int) = BCube(0:(2^n - 1), n)

# INTERFACE
iterator(bc :: BCube) = bc
ndims(bc :: BCube) = bc.n
length(bc :: BCube) = 2^ndims(bc)
factors(bc :: BCube) = tuple(fill(BCube(1), ndims(bc))...)

# Iteration utilities
postprocess(:: Nothing, n) = nothing
postprocess(p, n) = (BoolVec(p[1], n), p[2])

function iterate(bc :: BCube)
    p = iterate(bc.itr)
    return postprocess(p, ndims(bc))
end

function iterate(bc :: BCube, state)
    p = iterate(bc.itr, state)
    return postprocess(p, ndims(bc))
end

# direct product of two bool cubes
direct(bc1 :: BCube, bc2 :: BCube) = BCube(ndims(bc1) + ndims(bc2))

# show utilities
Base.show(io :: IO, bc :: BCube) = print(io, "{0,1}^$(ndims(bc))")

#### Boolean vectors

struct BooleanVector
    k :: Int
    n :: Int
end

const BoolVec = BooleanVector
const BooleanVec = BooleanVector

BoolVec(vs :: Union{NTuple, Vector}) = BoolVec(itr2bin(vs), length(vs))
BoolVec(v :: BoolVec) = v
value(v :: BoolVec) = v.k
value(x) = x   # fallback

function getindex(v :: BoolVec, i :: Int)
    n = ndims(v)
    return (value(v) >> (n - i)) & 1
end

ndims(v :: BoolVec) = v.n
length(v :: BoolVec) = ndims(v)
iterate(v :: BoolVec) = (v[1], 2)
iterate(v :: BoolVec, i :: Int) = i > ndims(v) ? nothing : (v[i], i+1)

dot(v1 :: BoolVec, v2 :: BoolVec) = sum(collect(v1) .* collect(v2)) % 2
# v1 <= v2 in lexicographic order?
isless(v1 :: BoolVec, v2 :: BoolVec) = (value(v1) & value(v2) == value(v1))

function Base.show(io :: IO, v :: BoolVec)
    n = ndims(v) - 1
    s = bitstring(value(v))[end - n : end]
    print(io, "[", s, ']')
end

# change representation to BoolVec
# (bc :: BCube)(x :: Union{NTuple, Array}) = BoolVec(x)
# (bc :: BCube)(x :: Int) = x


# # Finding boundaries

# The output of a `wombling` operation can be used to pick boundaries, *i.e.*
# areas where the values on the grid transition sharply. We will illustrate this
# with a simple example of a three-patch landscape.

using SpatialBoundaries
using StatsPlots
default(; dpi=500, size=(600, 600), aspectratio=1, c=:batlow, frame=:box)

# Let's create a landscape with two values:

A = rand(Float64, 200, 150);
A[1:80, 1:85] .+= 5.0;
A[110:end, 130:end] .+= 3.0;

# We can check out what this landscape looks likes:

heatmap(A)

# We can apply wombling to this landscape, assuming that all cells have the same
# size:

W = wombling(A);

# Let's look at the rate of change:

heatmap(W.m; c=:nuuk)

# Picking the boundaries is done by passing the wombling output to the
# `boundaries` function, with a specific threshold giving the proportion of
# points that should be retained as part of the boundaries. Checking what the
# effect of this threshold is would be a good idea:

thresholds = LinRange(0.0, 0.2, 200)
patches = [length(boundaries(W; threshold=t)) for t in thresholds]

plot(thresholds, log1p.(patches), aspectratio=:none)
xaxis!("Threshold", (0., 0.2))
yaxis!("log(boundary patches + 1)", (0., 9.))

# Let's eyeball this as a 0.01, and see how the patches are distributed.

# Another way we can look at the boundaries is to see *when* a patch is
# considered to be a boundary. To do so we will create an empty matrix, and fill
# each position with the lowest threshold at which it is considered to be a
# boundary:

b = similar(W.m)

for t in reverse(LinRange(0.0, 1.0, 200))
    b[boundaries(W; threshold=t)] .= t
end

heatmap(b, c=:tofino, clim=(0,1))

# This also suggests that we will get well delineated patches for low values of
# the threshold.

B = boundaries(W; threshold=0.01);

# In the following figure, cells identified as candidate boundaries are marked
# in white:

heatmap(A)
scatter!([(reverse(x.I)) for x in B], leg=false, msw=0, c=:white)

# We can see that the boundaries of the patches have been well identified!