using Flux
using Flux.Losses: mse
using Flux.Data: DataLoader

function build_model(; hidden_layer_size::Int64 = 32)
    return Chain(Dense(2, hidden_layer_size, relu), Dense(hidden_layer_size, 2))
end

function get_data(
    x_array::Array{Array{Float64}},
    disp_array::Array{Array{Float64}};
    batchsize::Int64 = 256,
)
    Xtrain = hcat(x_array...)
    Ytrain = hcat(disp_array...)

    DataLoader((Xtrain, Ytrain), batchsize = batchsize, shuffle = true)
end

function loss_fn(data, model)
    loss = 0.0
    num = 0
    for (x, y) in data
        ŷ = model(x)
        loss += mse(ŷ, y)
        num += size(x)[end]
    end
    loss / num
end

function train(
    x_array::Array{Array{Float64}},
    disp_array::Array{Array{Float64}};
    lr::Float64 = 1e-3,
    batchsize = 256,
    epochs::Int64 = 10,
)
    data = get_data(x_array, disp_array, batchsize = batchsize)
    model = build_model() |> cpu
    ps = Flux.params(model)
    opt = ADAM(lr)

    for epoch = 1:epochs
        for (x, y) in data
            gs = gradient(() -> mse(model(x), y), ps)
            Flux.Optimise.update!(opt, ps, gs)
        end
        loss = loss_fn(data, model)
        println("Epoch = ", epoch, " Loss = ", loss)
    end
    model
end

function predict(model, x::Array{Float64})::Array{Float64}
    x + model(x)
end

function fit_ensemble(
    x_array::Array{Array{Float64}},
    disp_array::Array{Array{Float64}};
    ensemble_size::Int64 = 10,
)
    models = []
    for ensemble=1:ensemble_size
        println("Fitting network ", ensemble, " in the ensemble")
        push!(models, train(x_array, disp_array))
    end
    models
end

function predict_ensemble(
    models,
    x::Array{Float64},
)::Array{Array{Float64}}
    predictions = []
    for model in models
        push!(predictions, predict(model, x))
    end
    predictions
end

function fit_ensembles(
    x_array::Array{Array{Array{Float64}}},
    disp_array::Array{Array{Array{Float64}}},
)
    ensembles = []
    n_actions = length(x_array)
    for a=1:n_actions
        println("Fitting ensembles for action ", a)
        push!(ensembles, fit_ensemble(x_array[a], disp_array[a]))
    end
    ensembles
end

function find_max_distance(
    predictions::Array{Array{Float64}}
)
    max_distance = 0.0
    for i=1:length(predictions)
        for j=i+1:length(predictions)
            x = predictions[i]
            y = predictions[j]
            max_distance = max(max_distance, norm(x - y))
        end
    end
    max_distance
end


using Test

using Dice
using Dice: CuddMgr, DistBool

code = @dice begin
    function test_rec(a::Int)
        if a <= 0
            flip(0.5)
        else
            flip(0.5) & test_rec(a - 1)
        end
    end

    test_rec(2)
end

@testset "Sugar Tests" begin

    @test infer((@dice if flip(0.5) flip(0.1) else flip(0.3) end), :bdd) ≈ 0.2
    @test infer((@dice if true flip(0.1) else flip(0.2) end), :bdd) ≈ 0.1

    @test infer(code, :bdd) ≈ 0.125

end


using CircularArrays

import Base.~

# First Half

const maxlbl₁ = 9
~(x::Int, y::Int) = mod1(x - y, maxlbl₁) # Circular subtraction

function simulate1!(cups, moves)

    function place(cups, pickup, destination)
        t = Vector(cups)
        for p in pickup
            pckpos = findfirst(x -> x == p, t)
            deleteat!(t, pckpos)
        end
        destpos = findfirst(x -> x == destination, t)
        CircularVector([t[1:destpos]; pickup; t[destpos + 1:end]])
    end

    current = cups[1]
    destination = 0
    for m = 1:moves
        currpos = findfirst(x -> x == current, cups)
        pickup = cups[currpos + 1:currpos + 3]
        # Step 1 - The crab picks up the three cups that are immediately
        # clockwise of the current cup. They are removed from the circle;

        destination = current ~ 1
        # Step 2 - The crab selects a destination cup: the cup with a label
        # equal to the current cup's label minus one.

        while destination ∈ pickup
            # If this would select one of the cups that was just picked up,
            # the crab will keep subtracting one until it finds a cup that
            # wasn't just picked up.

            destination = destination ~ 1
        end

        println("-- move $m --")
        println("cups: $cups")
        println("current: $current")
        println("pick up: $pickup")
        println("destination: $destination")
        println()

        cups = place(cups, pickup, destination)
        # Step 3 - The crab places the cups it just picked up so that they are
        # immediately clockwise of the destination cup. They keep the same
        # order as when they were picked up.

        currpos = findfirst(x -> x == current, cups)
        current = cups[currpos + 1]
        # Step 4 - The crab selects a new current cup: the cup which is
        # immediately clockwise of the current cup.
    end
    cups
end

puzzleinput = CircularVector([6, 8, 5, 9, 7, 4, 2, 1, 3])
puzzletestinput = CircularVector([3, 8, 9, 1, 2, 5, 4, 6, 7])
simulate1!(puzzleinput, 101)

# Second Half

const maxlbl₂ = 1000000
~(x::Int, y::Int) = mod1(x - y, maxlbl₂) # Circular subtraction

function simulate2!(labeling, moves)

    cups = collect([2:maxlbl2 + 1]...)
    labeling = [labeling; length(labeling) + 1]
    for i ∈ 1:9
        n = labeling[i]
        nx = labeling[i + 1]
        cups[n] = nx
    end
    cups[maxlbl2] = labeling[1]
    # The cups array store at position n the label of the cup immediately
    # clockwise of cup labeled n, that makes all so simple!

    cur = labeling[1]
    des = 0
    pck = [0, 0, 0]
    for m = 1:moves
        pck[1] = cups[cur]s
        pck[2] = cups[pck[1]]
        pck[3] = cups[pck[2]]
        # Step 1 - The crab picks up the three cups that are immediately
        # clockwise of the current cup. They are removed from the circle;

        des = cur ~ 1
        # Step 2 - The crab selects a destination cup: the cup with a label
        # equal to the current cup's label minus one.

        while des ∈ pck
            # If this would select one of the cups that was just picked up,
            # the crab will keep subtracting one until it finds a cup that
            # wasn't just picked up.

            des = des ~ 1
        end

        nexttodes = cups[des]
        cups[des] = pck[1]
        cups[cur] = cups[pck[3]]
        cups[pck[3]] = nexttodes
        # Step 3 - The crab places the cups it just picked up so that they are
        # immediately clockwise of the destination cup. They keep the same
        # order as when they were picked up.

        cur = cups[cur]
        # Step 4 - The crab selects a new current cup: the cup which is
        # immediately clockwise of the current cup.
    end

    cups[1] * cups[cups[1]]
end

puzzletestinput = [3, 8, 9, 1, 2, 5, 4, 6, 7]
puzzleinput = [6, 8, 5, 9, 7, 4, 2, 1, 3]

@time simulate2!(puzzleinput, 10000000)


input = readlines("d13.in")

function validids(ids)
    return map(x -> parse(Int, x), filter(x -> tryparse(Int, x) != nothing, ids))
end

function validpositions(ids)
    return findall(!=(nothing), tryparse.(Int, ids)) .- 1
end

function findearliest(timestamp, ids)
    delay, i = findmin(mod.(-timestamp,ids))
    return delay, ids[i]
end

function crt(n, a)
    Π = prod(n)
    return mod(sum(ai * invmod(Π ÷ ni, ni) * Π ÷ ni for (ni, ai) in zip(n, -a)), Π)
end

timestamp = parse(Int, input[1])
ids = split(input[2],",")

println("P1: ", prod(findearliest(timestamp, validids(ids))))
println("P2: ", crt(validids(ids), validpositions(ids)))


include("../utils.jl")
include("group_utils.jl")
using NLsolve

@testset "Special Linear group" begin
    @testset "basic properties" begin
        G = SpecialLinear(3)
        @test G === SpecialLinear(3, ℝ)
        @test repr(G) == "SpecialLinear(3, ℝ)"
        @test base_manifold(G) === SpecialLinear(3)
        @test decorated_manifold(G) == GeneralLinear(3)
        @test number_system(G) === ℝ
        @test manifold_dimension(G) == 8
        @test representation_size(G) == (3, 3)
        Gc = SpecialLinear(2, ℂ)
        @test decorated_manifold(Gc) == GeneralLinear(2, ℂ)
        @test repr(Gc) == "SpecialLinear(2, ℂ)"
        @test number_system(Gc) == ℂ
        @test manifold_dimension(Gc) == 6
        @test representation_size(Gc) == (2, 2)
        Gh = SpecialLinear(4, ℍ)
        @test repr(Gh) == "SpecialLinear(4, ℍ)"
        @test number_system(Gh) == ℍ
        @test manifold_dimension(Gh) == 4 * 15
        @test representation_size(Gh) == (4, 4)

        @test (@inferred invariant_metric_dispatch(G, LeftAction())) === Val(true)
        @test (@inferred invariant_metric_dispatch(G, RightAction())) === Val(false)
        @test is_default_metric(
            MetricManifold(G, InvariantMetric(EuclideanMetric(), LeftAction())),
        ) === true
        @test @inferred(Manifolds.default_metric_dispatch(G, EuclideanMetric())) ===
              Val(true)
        @test @inferred(
            Manifolds.default_metric_dispatch(
                G,
                InvariantMetric(EuclideanMetric(), LeftAction()),
            )
        ) === Val(true)
        @test @inferred(
            Manifolds.default_metric_dispatch(
                MetricManifold(G, InvariantMetric(EuclideanMetric(), LeftAction())),
            )
        ) === Val(true)
        @test Manifolds.allocation_promotion_function(Gc, exp!, (1,)) === complex
    end

    @testset "Real" begin
        G = SpecialLinear(3)

        @test_throws DomainError is_manifold_point(G, randn(2, 3), true)
        @test_throws DomainError is_manifold_point(G, Float64[2 1; 1 1], true)
        @test_throws DomainError is_manifold_point(G, [1 0 im; im 0 0; 0 -1 0], true)
        @test_throws DomainError is_manifold_point(G, zeros(3, 3), true)
        @test_throws DomainError is_manifold_point(G, Float64[1 3 3; 1 1 2; 1 2 3], true)
        @test_throws DomainError is_manifold_point(
            G,
            make_identity(SpecialLinear(2), ones(2, 2)),
            true,
        )
        @test is_manifold_point(G, Float64[1 1 1; 2 2 1; 2 3 3], true)
        @test is_manifold_point(G, make_identity(G, ones(3, 3)), true)
        @test_throws DomainError is_tangent_vector(
            G,
            Float64[2 3 2; 3 1 2; 1 1 1],
            randn(3, 3),
            true;
            atol=1e-6,
        )
        @test_throws DomainError is_tangent_vector(
            G,
            Float64[2 1 2; 3 2 2; 2 2 1],
            Float64[2 1 -1; 2 2 1; 1 1 -1],
            true;
            atol=1e-6,
        )
        @test is_tangent_vector(
            G,
            Float64[2 1 2; 3 2 2; 2 2 1],
            Float64[-1 -1 -1; 1 -1 2; -1 -1 2],
            true;
            atol=1e-6,
        )

        types = [Matrix{Float64}]
        pts =
            [[2 -1 -3; 4 -1 -6; -1 1 2], [0 2 1; 0 -3 -1; 1 0 2], [-2 0 -1; 1 0 0; -1 -1 2]]
        vpts = [[0 -1 -5; 1 2 0; 1 2 -2], [0 -2 1; -2 1 2; -4 2 -1]]

        retraction_methods = [
            Manifolds.GroupExponentialRetraction(LeftAction()),
            Manifolds.GroupExponentialRetraction(RightAction()),
        ]

        inverse_retraction_methods = [
            Manifolds.GroupLogarithmicInverseRetraction(LeftAction()),
            Manifolds.GroupLogarithmicInverseRetraction(RightAction()),
        ]

        for T in types
            gpts = convert.(T, pts)
            vgpts = convert.(T, vpts)
            test_group(G, gpts, vgpts, vgpts; test_diff=true, test_invariance=true)
            test_manifold(
                G,
                gpts;
                test_reverse_diff=false,
                test_forward_diff=false,
                test_injectivity_radius=false,
                test_project_point=true,
                test_project_tangent=true,
                test_musical_isomorphisms=true,
                test_default_vector_transport=true,
                vector_transport_methods=[
                    ParallelTransport(),
                    SchildsLadderTransport(),
                    PoleLadderTransport(),
                ],
                retraction_methods=retraction_methods,
                inverse_retraction_methods=inverse_retraction_methods,
                exp_log_atol_multiplier=1e10,
                retraction_atol_multiplier=1e8,
                is_tangent_atol_multiplier=1e10,
            )
        end

        @testset "project" begin
            p = randn(3, 3)
            @test !is_manifold_point(G, p)
            q = project(G, p)
            @test is_manifold_point(G, q)
            @test project(G, q) ≈ q

            X = randn(3, 3)
            @test !is_tangent_vector(G, q, X; atol=1e-6)
            Y = project(G, q, X)
            @test is_tangent_vector(G, q, Y; atol=1e-6)
            @test project(G, q, Y) ≈ Y
        end
    end

    @testset "Complex" begin
        G = SpecialLinear(2, ℂ)

        @test_throws DomainError is_manifold_point(G, randn(ComplexF64, 2, 3), true)
        @test_throws DomainError is_manifold_point(G, randn(2, 2), true)
        @test_throws DomainError is_manifold_point(
            G,
            ComplexF64[1 0 im; im 0 0; 0 -1 0],
            true,
        )
        @test_throws DomainError is_manifold_point(G, ComplexF64[1 im; im 1], true)
        @test_throws DomainError is_manifold_point(
            G,
            make_identity(SpecialLinear(2), ones(ComplexF64, 2, 2)),
            true,
        )
        @test is_manifold_point(G, ComplexF64[im 1; -2 im], true)
        @test is_manifold_point(G, make_identity(G, ones(3, 3)), true)
        @test_throws DomainError is_tangent_vector(
            G,
            ComplexF64[-1+im -1; -im 1],
            ComplexF64[1-im 1+im; 1 -1+im],
            true;
            atol=1e-6,
        )
        @test_throws DomainError is_tangent_vector(
            G,
            ComplexF64[1 1+im; -1+im -1],
            ComplexF64[1-im -1-im; -im im],
            true;
            atol=1e-6,
        )
        @test is_tangent_vector(
            G,
            ComplexF64[1 1+im; -1+im -1],
            ComplexF64[1-im 1+im; 1 -1+im],
            true;
            atol=1e-6,
        )

        types = [Matrix{ComplexF64}]
        pts = [
            [0+1im 0+2im; 0+1im 0+1im],
            [-2+0im 0+1im; 0-1im -1+0im],
            [0+0im 0-1im; 0-1im -1+3im],
        ]
        vpts = [[0-1im -1+1im; -1+0im 0+1im], [1+1im 0+0im; 0+1im -1-1im]]

        retraction_methods = [
            Manifolds.GroupExponentialRetraction(LeftAction()),
            Manifolds.GroupExponentialRetraction(RightAction()),
        ]

        inverse_retraction_methods = [
            Manifolds.GroupLogarithmicInverseRetraction(LeftAction()),
            Manifolds.GroupLogarithmicInverseRetraction(RightAction()),
        ]

        for T in types
            gpts = convert.(T, pts)
            vgpts = convert.(T, vpts)
            test_group(G, gpts, vgpts, vgpts; test_diff=true, test_invariance=true)
            test_manifold(
                G,
                gpts;
                test_reverse_diff=false,
                test_forward_diff=false,
                test_injectivity_radius=false,
                test_project_point=true,
                test_project_tangent=true,
                test_musical_isomorphisms=true,
                test_default_vector_transport=true,
                vector_transport_methods=[
                    ParallelTransport(),
                    SchildsLadderTransport(),
                    PoleLadderTransport(),
                ],
                retraction_methods=retraction_methods,
                inverse_retraction_methods=inverse_retraction_methods,
                exp_log_atol_multiplier=1e8,
                retraction_atol_multiplier=1e8,
                is_tangent_atol_multiplier=1e8,
            )
        end

        @testset "project" begin
            p = randn(ComplexF64, 2, 2)
            @test !is_manifold_point(G, p)
            q = project(G, p)
            @test is_manifold_point(G, q)
            @test project(G, q) ≈ q

            X = randn(ComplexF64, 2, 2)
            @test !is_tangent_vector(G, q, X; atol=1e-6)
            Y = project(G, q, X)
            @test is_tangent_vector(G, q, Y; atol=1e-6)
            @test project(G, q, Y) ≈ Y
        end
    end
end


using RandomMatrices
using Base.Test

srand(1)
dx = 0.001

let
p = WhiteNoiseProcess(dx)
@test start(p) == nothing
S = next(p)
@test done(p, S) == false
end

let
p = BrownianProcess(dx)
@test start(p) == 0
S = next(p, 0.0)
@test done(p, S) == false
end

let
p = AiryProcess(dx, 2.0)
S = start(p)
@test S == fill(-2/dx^2, 1, 1)
S = next(p, S)
@test done(p, S) == false
end


#=

  Euler #26 in Julia.

  Problem 26:
  """
  A unit fraction contains 1 in the numerator. The decimal representation of the
  unit fractions with denominators 2 to 10 are given:

      1/2	= 	0.5
      1/3	= 	0.(3)
      1/4	= 	0.25
      1/5	= 	0.2
      1/6	= 	0.1(6)
      1/7	= 	0.(142857)
      1/8	= 	0.125
      1/9	= 	0.(1)
      1/10	= 	0.1

  Where 0.1(6) means 0.166666..., and has a 1-digit recurring cycle. It can be
  seen that 1/7 has a 6-digit recurring cycle.

  Find the value of d < 1000 for which 1/d contains the longest recurring cycle in
  its decimal fraction part.
  """

  This Julia program was created by Hakan Kjellerstrand, hakank@gmail.com
  See also my Julia page: http://www.hakank.org/julia/

=#

include("Euler.jl")

function get_rep_len(n)
    foundRemainders = zeros(n)
    value = 1;
    position = 1;
    while foundRemainders[value] == 0 && value != 0
        foundRemainders[value] = position
        value = round(Int,1+((value*10) % n)) # added 1
        position += 1
    end
    return position-foundRemainders[value]
end

# 0.00195827s
function euler26a()
    maxLen = 0
    maxD = 0
    for d = 2:1000
        len = get_rep_len(d)
        if len > maxLen
            maxLen = len
            maxD = d
        end
    end
    return maxD
end

# Checks only primes
# 0.00048207s
function euler26b()
    maxLen = 0
    maxD = 0
    for d in 2:1000
        if isPrime(d)
            len = get_rep_len(d)
            if len > maxLen
                maxLen = len
                maxD = d
            end
        end
    end
    return maxD
end

# Only primes
# 0.01460937s
function euler26c()
    return sort(primes(999)
        .|>d->[get_rep_len(d),d]
        , by=x->x[1],rev=true)[1][2]
end

# filter primes first
# 0.01558240s
function euler26d()
    return sort(filter(isPrime, 2:1000)
           .|>d->[get_rep_len(d),d]
           , by=x->x[1],rev=true)[1][2]

end

# A variant. But it's still not complete chaining...
# 0.01890910s
function euler26e()
    return (sort((2:1000).|>x->[get_rep_len(x)*x*isPrime(x),x])|>last)[2]
end

# run_euler(euler26a)
run_euler(euler26b)
# run_euler(euler26c)
# run_euler(euler26d)
# run_euler(euler26e)


using DiffEqBase, DiffEqOperators, ForwardDiff, LinearAlgebra, Test
const A = rand(300,300)
f(du,u) = mul!(du,A,u)
f(u) = A*u
x = rand(300)
v = rand(300)
du = similar(x)

cache1 = ForwardDiff.Dual{DiffEqOperators.JacVecTag}.(x, v)
cache2 = ForwardDiff.Dual{DiffEqOperators.JacVecTag}.(x, v)
@test DiffEqOperators.num_jacvec!(du, f, x, v) ≈ ForwardDiff.jacobian(f,similar(x),x)*v rtol=1e-6
@test DiffEqOperators.num_jacvec!(du, f, x, v, similar(v), similar(v)) ≈ ForwardDiff.jacobian(f,similar(x),x)*v rtol=1e-6
@test DiffEqOperators.num_jacvec(f, x, v) ≈ ForwardDiff.jacobian(f,similar(x),x)*v rtol=1e-6

@test DiffEqOperators.auto_jacvec!(du, f, x, v) ≈ ForwardDiff.jacobian(f,similar(x),x)*v
@test DiffEqOperators.auto_jacvec!(du, f, x, v, cache1, cache2) ≈ ForwardDiff.jacobian(f,similar(x),x)*v
@test DiffEqOperators.auto_jacvec(f, x, v) ≈ ForwardDiff.jacobian(f,similar(x),x)*v

f(du,u,p,t) = mul!(du,A,u)
f(u,p,t) = A*u
L = JacVecOperator(f,x)
@test L*x ≈ DiffEqOperators.auto_jacvec(f, x, x)
@test L*v ≈ DiffEqOperators.auto_jacvec(f, x, v)
@test mul!(du,L,v) ≈ DiffEqOperators.auto_jacvec(f, x, v)
DiffEqBase.update_coefficients!(L,v,nothing,nothing)
@test mul!(du,L,v) ≈ DiffEqOperators.auto_jacvec(f, v, v)

L = JacVecOperator(f,x,autodiff=false)
DiffEqBase.update_coefficients!(L,x,nothing,nothing)
@test L*x ≈ DiffEqOperators.num_jacvec(f, x, x)
@test L*v ≈ DiffEqOperators.num_jacvec(f, x, v)
@test mul!(du,L,v) ≈ DiffEqOperators.num_jacvec(f, x, v) rtol=1e-6
DiffEqBase.update_coefficients!(L,v,nothing,nothing)
@test mul!(du,L,v) ≈ DiffEqOperators.num_jacvec(f, v, v) rtol=1e-6

L2 = JacVecOperator{Float64}(f)
DiffEqBase.update_coefficients!(L2,x,nothing,nothing)
@test L2*x ≈ DiffEqOperators.auto_jacvec(f, x, x)
@test L2*v ≈ DiffEqOperators.auto_jacvec(f, v, v)
@test mul!(du,L2,x) ≈ DiffEqOperators.auto_jacvec(f, x, x)

L2 = JacVecOperator{Float64}(f,autodiff=false)
DiffEqBase.update_coefficients!(L2,x,nothing,nothing)
@test L2*x ≈ DiffEqOperators.num_jacvec(f, x, x)
@test L2*v ≈ DiffEqOperators.num_jacvec(f, v, v) rtol=1e-6

using OrdinaryDiffEq
function lorenz(du,u,p,t)
 du[1] = 10.0(u[2]-u[1])
 du[2] = u[1]*(28.0-u[3]) - u[2]
 du[3] = u[1]*u[2] - (8/3)*u[3]
end
u0 = [1.0;0.0;0.0]
tspan = (0.0,100.0)
ff1 = ODEFunction(lorenz,jac_prototype=JacVecOperator{Float64}(lorenz,u0))
ff2 = ODEFunction(lorenz,jac_prototype=JacVecOperator{Float64}(lorenz,u0,autodiff=false))

for ff in [ff1, ff2]
  prob = ODEProblem(ff,u0,tspan)
  @test solve(prob,TRBDF2()).retcode == :Success
  @test solve(prob,TRBDF2(linsolve=LinSolveGMRES())).retcode == :Success
  @test solve(prob,Exprb32()).retcode == :Success
  @test_broken sol = solve(prob,Rosenbrock23())
  @test_broken sol = solve(prob,Rosenbrock23(linsolve=LinSolveGMRES(tol=1e-10)))
end


using JuMP, EAGO

                     m = Model()

                     EAGO.register_eago_operators!(m)

                     @variable(m, -1 <= x[i=1:4] <= 1)

                     @variable(m, -8.186501529651974 <= q <= 9.622483160840265)

                     add_NL_constraint(m, :(sigmoid(-0.9342689023247241 + -0.06643065035242302*sigmoid(-0.6967397920955185 + 0.4612305646899797*sigmoid(-0.7651411836742423 + 0.736339264438953*$(x[1]) + 0.06323249947984699*$(x[2]) + 0.6326865446578034*$(x[3]) + 0.8935006364653741*$(x[4])) + 0.5917923558177942*sigmoid(0.8959549878207418 + 0.6412415470262633*$(x[1]) + 0.6383904188579423*$(x[2]) + 0.9706157764666923*$(x[3]) + 0.8108132764672078*$(x[4]))) + 0.13022424784864484*sigmoid(-0.8092681495291991 + -0.6577545463201151*sigmoid(-0.7651411836742423 + 0.736339264438953*$(x[1]) + 0.06323249947984699*$(x[2]) + 0.6326865446578034*$(x[3]) + 0.8935006364653741*$(x[4])) + 0.816846389391447*sigmoid(0.8959549878207418 + 0.6412415470262633*$(x[1]) + 0.6383904188579423*$(x[2]) + 0.9706157764666923*$(x[3]) + 0.8108132764672078*$(x[4])))) + sigmoid(0.9502905692637253 + 0.2731932353707758*sigmoid(-0.6967397920955185 + 0.4612305646899797*sigmoid(-0.7651411836742423 + 0.736339264438953*$(x[1]) + 0.06323249947984699*$(x[2]) + 0.6326865446578034*$(x[3]) + 0.8935006364653741*$(x[4])) + 0.5917923558177942*sigmoid(0.8959549878207418 + 0.6412415470262633*$(x[1]) + 0.6383904188579423*$(x[2]) + 0.9706157764666923*$(x[3]) + 0.8108132764672078*$(x[4]))) + 0.7525923835832526*sigmoid(-0.8092681495291991 + -0.6577545463201151*sigmoid(-0.7651411836742423 + 0.736339264438953*$(x[1]) + 0.06323249947984699*$(x[2]) + 0.6326865446578034*$(x[3]) + 0.8935006364653741*$(x[4])) + 0.816846389391447*sigmoid(0.8959549878207418 + 0.6412415470262633*$(x[1]) + 0.6383904188579423*$(x[2]) + 0.9706157764666923*$(x[3]) + 0.8108132764672078*$(x[4])))) - $q <= 0.0))

                     @objective(m, Min, q)

                     return m


using ProgressMeter, BlackBoxOptim
include("util.jl")
const datafile = "inputs/23.txt"
struct Nanobot
    x::Int64
    y::Int64
    z::Int64
    r::Int64
end

function parse_input()::Array{Nanobot,1}
    lines = getlines(datafile)
    re = r"^pos=<(-?\d+),(-?\d+),(-?\d+)>, r=(\d+)$"
    nanobots = []
    for line in lines
        matches = match(re, line)
        try
            parts = [parse(Int64, matches[i]) for i in 1:4]
            nanobot = Nanobot(parts...)
            push!(nanobots, nanobot)
        catch
            println(line)
        end
    end
    return nanobots
end

function mdist(x,y,z,x2,y2,z2)::Int64
    return abs(x-x2)+abs(y-y2)+abs(z-z2)
end

function part_1()
    nanobots = parse_input()
    radii = sort(collect(nanobots), by=x->-x.r)
    biggest_radius = radii[1]
    in_range = []
    for nanobot in nanobots
        if mdist(biggest_radius.x, biggest_radius.y, biggest_radius.z,
            nanobot.x, nanobot.y, nanobot.z) <= biggest_radius.r
            push!(in_range, nanobot)
        end
    end
    println(length(in_range))
end

function minmaxxyz(nanobots::Array{Nanobot,1},r::Int64)
    x = []
    y = []
    z = []
    for nanobot in nanobots
        push!(x, nanobot.x)
        push!(y, nanobot.y)
        push!(z, nanobot.z)
    end
    return minimum(x)-r,maximum(x)+r,minimum(y)-r,maximum(y)+r,minimum(z)-r,maximum(z)+r
end

function corners(nb::Nanobot)::Array{Tuple{Int64,Int64,Int64}}
    retval = []
    push!(retval, (nb.x+nb.r,nb.y,nb.z))
    push!(retval, (nb.x-nb.r,nb.y,nb.z))
    push!(retval, (nb.x,nb.y+nb.r,nb.z))
    push!(retval, (nb.x,nb.y-nb.r,nb.z))
    push!(retval, (nb.x,nb.y,nb.z+nb.r))
    push!(retval, (nb.x,nb.y,nb.z-nb.r))
    return retval
end

function get_neighborhood(point::Tuple{Int64,Int64,Int64})::Array{Tuple{Int64,Int64,Int64}}
    retval = []
    for z in -15:15
        for y in -15:15
            for x in -15:15
                push!(retval, (point[1]+x,point[2]+y,point[3]+z))
            end
        end
    end
    return retval
end

function score(x::Int64,y::Int64,z::Int64,nb::Array{Nanobot})::Int64
    s = 0
    for n in nb
        if mdist(x,y,z,n.x,n.y,n.z) <= n.r
            s += 1
        end
    end
    return s
end

function score()
    nanobots = parse_input()
    return (w) -> begin
        x = round(w[1])
        y = round(w[2])
        z = round(w[3])
        score = 0
        for n in nanobots
            if mdist(x,y,z,n.x,n.y,n.z) <= n.r
                score += 1
            end
        end
        return float(1000-score)+(float(mdist(x,y,z,0,0,0))/848312125.0)
    end
end

nanobots = parse_input()
radii = sort(collect(nanobots), by=x->-x.r)
biggest_radius = radii[1]
minx, maxx, miny, maxy, minz, maxz = minmaxxyz(nanobots, biggest_radius.r)
fitness_func(f) = f[1]+(f[2]/mdist(0,0,0,maxx,maxy,maxz))

function part_2()
    nanobots = parse_input()
    radii = sort(collect(nanobots), by=x->-x.r)
    biggest_radius = radii[1]
    minx, maxx, miny, maxy, minz, maxz = minmaxxyz(nanobots, biggest_radius.r)
    mi = minimum([minx,miny,minz])
    ma = maximum([maxx,maxy,maxz])
    score_func = score()
    res = bboptimize(score_func; Method=:adaptive_de_rand_1_bin_radiuslimited, SearchRange = (float(mi),float(ma)), NumDimensions = 3, MaxTime = 1800.0, PopulationSize=1000)
    return res
end

part_1()
results = part_2()
results


# deprecated
function simulate(m::HeadTurn;
    onset::Real = 0,
    trial_duration::Real = 5,
    stochastic::Bool = false)

    @assert onset >= 0
    @assert trial_duration > 0

    # onset = m.onset
    duration = m.duration
    total_duration = trial_duration

    onset + duration <= total_duration || error("Movement extends beyond simulation event.")
    m.D ∈ ["left", "right"] || error("Direction not specified.")

    D = m.D == "left" ? -1 : 1
    # motion amplitude is noisy if stochastic simulation
    A = stochastic == 0 ? m.A : rand(Normal(m.A, 0.5))
    noise = m.sensor
    Δt = m.Δt
    f = 1/(duration) # frequency: single sinusiodal head turn

    timesteps = range(0, stop = total_duration, step = Δt)
    T = length(timesteps)

    κ1, κ2 = cupula_dynamics(Δt)
    # α = zeros(T)
    # ω = Array{Float64}(undef, T) # velocity
    # θ = Array{Float64}(undef, T) # position
    # c = Array{Float64}(undef, T) # cupula dynamics
    # y = Array{Float64}(undef, T) # sensory observations
    α = zeros(T)
    ω = zeros(T)
    θ = zeros(T)
    c = zeros(T)
    y = zeros(T)

    # start until motion onset
    j = 1
    t = round(timesteps[j]; digits = 3)

    while t < onset
        y[j] = ω[j] - c[j] + rand(noise)
        j += 1
        t = round(timesteps[j]; digits = 3)
    end

    for i ∈ (j+1):T
        t = round(timesteps[i]; digits = 3)
        # here, we add a small amount of noise to the acceleration if the
        # simulation is stochastic
        α[i] = t < onset + duration ? acceleration(D, A, f, t, onset, noisy = stochastic) : 0

        ω[i] = ω[i-1] + Δt * α[i-1]
        θ[i] = θ[i-1] + Δt * ω[i-1] + 1/2 * Δt^2 * α[i-1]
        c[i] = κ1 * c[i-1] + κ2 * ω[i]
        y[i] = ω[i] - c[i] + rand(noise)
    end
    out = (timesteps = collect(timesteps),
            y = y, α = α, ω = ω, θ = θ, c = c,
            Δt = Δt, onset = onset)
    return out
end

# deprecated. TODO: add deceleration
function simulate(m::ExternalDisturbance;
    onset::Real = 0, trial_duration::Real = 5)

    @assert onset >= 0
    @assert trial_duration > 0

    # onset = m.onset
    duration = m.duration
    total_duration = trial_duration

    onset + duration <= total_duration || error("Movement extends beyond simulation event.")
    m.D ∈ ["left", "right"] || error("Direction not specified.")

    D = m.D == "left" ? -1 : 1
    noise = m.sensor
    Δt = m.Δt
    f = 1/(duration) # frequency: single sinusiodal head turn

    timesteps = range(0, stop = total_duration, step = Δt)
    T = length(timesteps)

    κ1, κ2 = cupula_dynamics(Δt)
    # α = zeros(T)
    # ω = Array{Float64}(undef, T) # velocity
    # θ = Array{Float64}(undef, T) # position
    # c = Array{Float64}(undef, T) # cupula dynamics
    # y = Array{Float64}(undef, T) # sensory observations
    α = zeros(T)
    ω = zeros(T)
    θ = zeros(T)
    c = zeros(T)
    y = zeros(T)

    # start until motion onset
    j = 1
    t = round(timesteps[j]; digits = 3)

    while t < onset
        y[j] = ω[j] - c[j] + rand(noise)
        j += 1
        t = round(timesteps[j]; digits = 3)
    end

    for i ∈ (j+1):T
        t = round(timesteps[i]; digits = 3)
        α[i] = t < onset + duration ? D * m.A : 0

        ω[i] = ω[i-1] + Δt * α[i-1]
        θ[i] = θ[i-1] + Δt * ω[i-1] + 1/2 * Δt^2 * α[i-1]
        c[i] = κ1 * c[i-1] + κ2 * ω[i]
        y[i] = ω[i] - c[i] + rand(noise)
    end
    out = (timesteps = collect(timesteps),
            y = y, α = α, ω = ω, θ = θ, c = c,
            Δt = Δt, onset = onset)
    return out
end

# deprecated
function simulate(mᵤ::HeadTurn,
    mₑ::HeadTurn;
    onsetᵤ::Real = 0, onsetₑ::Real = 0,
    trial_duration::Real = 5,
    stochastic::Bool = false)

    @assert onsetᵤ >= 0
    @assert onsetₑ >= 0
    @assert trial_duration > 0

    # onset = m.onset
    duration1 = mᵤ.duration
    duration2 = mₑ.duration
    total_duration = trial_duration

    onsetᵤ + duration1 <= total_duration || error("Head turn beyond simulation event.")
    onsetₑ + duration2 <= total_duration || error("Disturbance extends beyond simulation event.")

    mᵤ.D ∈ ["left", "right"] || error("Direction not specified.")
    mₑ.D ∈ ["left", "right"] || error("Direction not specified.")

    D1 = mᵤ.D == "left" ? -1 : 1
    D2 = mₑ.D == "left" ? -1 : 1

    # motion amplitude is noisy if stochastic simulation
    A1 = stochastic == 0 ? mᵤ.A : rand(Normal(m1.A, 0.5))
    A2 = stochastic == 0 ? mₑ.A : rand(Normal(m2.A, 0.5))

    noise = mᵤ.sensor
    Δt = mᵤ.Δt
    f1 = 1/(duration1) # frequency: single sinusiodal head turn
    f2 = 1/(duration2)

    timesteps = range(0, stop = total_duration, step = Δt)
    T = length(timesteps)

    κ1, κ2 = cupula_dynamics(Δt)
    # α = zeros(T)
    # ω = Array{Float64}(undef, T) # velocity
    # θ = Array{Float64}(undef, T) # position
    # c = Array{Float64}(undef, T) # cupula dynamics
    # y = Array{Float64}(undef, T) # sensory observations
    α1 = zeros(T)
    α2 = zeros(T)
    α = zeros(T)
    ω1 = zeros(T)
    ω2 = zeros(T)
    ω = zeros(T)
    θ = zeros(T)
    c = zeros(T)
    y = zeros(T)

    for i ∈ 2:T

        t = round(timesteps[i]; digits = 3)

        α1[i] = (t > onsetᵤ) & (t < onsetᵤ + duration1) ? acceleration(D1, A1, f1, t, onsetᵤ, noisy = stochastic) : 0
        α2[i] = (t > onsetₑ) & (t < onsetₑ + duration2) ? acceleration(D2, A2, f2, t, onsetₑ, noisy = stochastic) : 0
        α[i] = α1[i] + α2[i]
        ω1[i] = ω1[i-1] + Δt * α1[i-1]
        ω2[i] = ω2[i-1] + Δt * α2[i-1]

        ω[i] =  ω1[i] + ω2[i]
        θ[i] = θ[i-1] + Δt * ω[i-1] + 1/2 * Δt^2 * α[i-1]
        c[i] = κ1 * c[i-1] + κ2 * ω[i]
        y[i] = ω[i] - c[i] + rand(noise)
    end

    out = (timesteps = collect(timesteps),
            y = y,
            αᵤ = α1, αₑ = α2,
            α = α,
            ωᵤ = ω1, ωₑ = ω2,
            ω = ω,
            θ = θ, c = c,
            Δt = Δt, onsetᵤ = onsetᵤ, onsetₑ = onsetₑ)
    return out

end


module VariableSelector

using DataFrames
using Statistics
using StatsBase
using HypothesisTests
export variable_importance

# Function for cat/con
# The test for such combo is "KruskalWallis"
function gamma(cat_var::Vector{String}, con_var::Vector{<: Number})
    data = DataFrame(levels = cat_var, values = con_var)
    levels = data.levels |> Set |> collect
    vec_list = Vector{Float64}[]
    for i in levels
      filtered_df = data[data.levels .== i, :]
      filtered_vec = filtered_df.values
      push!(vec_list, filtered_vec)
    end
    p = KruskalWallisTest(vec_list...) |> pvalue
    p = round(p, digits  = 3)
    return (p, "KruskalWallis")
  end

  # Function for con/cat
  # The test for such combo is "KruskalWallis"
  function gamma(con_var::Vector{<: Number}, cat_var::Vector{String})
    gamma(cat_var, con_var)
  end

  # Function for cat/cat
  # The test for such combo is "ChiSquared"
  function gamma(cat_var1::Vector{String}, cat_var2::Vector{String})
    data = DataFrame(levels1 = cat_var1, levels2 = cat_var2)
    ul1 = data.levels1 |> Set |> collect |> sort
    ul2 = data.levels2 |> Set |> collect |> sort
    output = zeros(Int64, length(ul1), length(ul2))
    for i in eachindex(ul1), j in eachindex(ul2)
      tv = (data.levels1 .== ul1[i]) .& (data.levels2 .== ul2[j])
      output[i,j] = sum(tv)
    end
    p = ChisqTest(output) |> pvalue
    return (p, "ChiSquared")
  end

  # Function for con/con
  function gamma(con_var1::Vector{<: Number}, con_var2::Vector{<: Number})
    th = 20 # Treshold for if vector should be category or not

    # Get the # of levels for each vector
    l1 = con_var1 |> Set |> length
    l2 = con_var2 |> Set |> length

    # Get the string vector of each vector
    con_var1_st = string.(con_var1)
    con_var2_st = string.(con_var2)

    # Treat vectors with low number of levels as categories and apply the right test
    if l1 <= th && l2 <= th
      gamma(con_var1_st, con_var1_st) # Apply ChiSquared test
    elseif l1 <= th && l2 > th
      gamma(con_var1_st, con_var2)  # Apply "KruskalWallis" test
    elseif l1 > th && l2 <= th
      gamma(con_var1, con_var2_st) # Apply "KruskalWallis" test
    else
      p = CorrelationTest(con_var1, con_var2) |> pvalue # Apply "Correlation" test
      return (p, "Correlation")
    end
  end

  # Finds the most important variables against the target
  function variable_importance(data::DataFrame, target::Symbol)
    X = select(data, Not(target)) # Get DataFrame X of x variables
    y = data[!, target] # Obtain y vector
    col_names = names(X) # Name vector of X dataframe
    output = DataFrame(feature = String[], test = String[], pvalue = Float64[])
    for i in col_names
      x = X[!, i]
      J = gamma(x, y)
      push!(output, [i, J[2], J[1]])
    end
    tv = (output.pvalue .< 0.05)
    results = output[tv, :]
    return results
  end

end


using Ferrite
using Ferrite: getnbasefunctions, CellIterator
using TopOpt.TrussTopOptProblems.TrussTopOptProblems: compute_local_axes, getE, getA
using TopOpt.TrussTopOptProblems: truss_reinit!

"""
    get_truss_Kσs(problem::TrussProblem{xdim, TT}, u, cellvalues) where {xdim, TT}

Compute the geometric stiffness matrix for **truss elements** (axial bar element, no bending or torsion). Matrix formulation defined in eq (3) and (4) in
https://people.duke.edu/~hpgavin/cee421/truss-finite-def.pdf

This matrix formulation is equivalent to the one used in
    M. Kočvara, “On the modelling and solving of the truss design problem with global stability constraints,” Struct Multidisc Optim, vol. 23, no. 3, pp. 189–203, Apr. 2002, doi: 10/br35mf.

# Arguments
`problem` = truss topopt problem struct
`u` = deformation vector (solved from first-order linear elasticity)
`cellvalues` = Ferrite cellvalues of the truss system

# Outputs
`Kσs` = a list of 2*ndim x 2*ndim element geometric stiffness matrix (in global cooridnate)
"""
function get_truss_Kσs(problem::TrussProblem{xdim,TT}, u, cellvalues) where {xdim,TT}
    Es = getE(problem)
    As = getA(problem)
    dh = problem.ch.dh

    # usually ndof_pc = xdim * n_basefuncs
    ndof_pc = ndofs_per_cell(dh)
    n_basefuncs = getnbasefunctions(cellvalues)
    @assert ndof_pc == xdim * n_basefuncs "$ndof_pc, $n_basefuncs"
    @assert n_basefuncs == 2

    global_dofs = zeros(Int, ndof_pc)
    Kσs = [zeros(TT, ndof_pc, ndof_pc) for i in 1:getncells(dh.grid)]
    Kσ_e = zeros(TT, ndof_pc, ndof_pc)

    for (cellidx, cell) in enumerate(CellIterator(dh))
        truss_reinit!(cellvalues, cell, As[cellidx])
        # get cell's dof's global dof indices, i.e. CC_a^e
        celldofs!(global_dofs, dh, cellidx)
        E = Es[cellidx]
        A = As[cellidx]
        L = norm(cell.coords[1] - cell.coords[2])
        R = compute_local_axes(cell.coords[1], cell.coords[2])
        # local axial projection operator (local axis transformation)
        γ = vcat(-R[:, 1], R[:, 1])
        u_cell = @view u[global_dofs]
        # compute axial force: first-order approx of bar force
        # better approx would be: EA/L * (u3-u1 + 1/(2*L0)*(u4-u2)^2) = EA/L * (γ'*u + 1/2*(δ'*u)^2)
        # see: https://people.duke.edu/~hpgavin/cee421/truss-finite-def.pdf
        q_cell = E * A / L * (γ' * u_cell)
        Kσ_e .= 0
        for i in 2:size(R, 2)
            δ = vcat(-R[:, i], R[:, i])
            # @assert δ' * γ ≈ 0
            Kσ_e .+= δ * δ'
        end
        Kσ_e .*= q_cell / L
        Kσs[cellidx] .= Kσ_e
    end
    return Kσs
end

"""
    buckling(problem::TrussProblem{xdim, T}, ginfo, einfo, vars=ones(T, getncells(getdh(problem).grid)), xmin = T(0.0); u=undef) where {xdim, T}

Assembly global geometric stiffness matrix of the given truss problem.

# Arguments
`problem` = truss topopt problem
`ginfo` = solver.globalinfo
`einfo` = solver.elementinfo
`vars` = (Optional) design variable values, default to all ones
`xmin` = (Optional) min x value, default to 0.0
`u` = (Optional) given displacement vector, if specified as undef, a linear solve will be performed on the first-order K to get u

# Outputs
`K` = global first-order stiffness matrix (same as ginfo.K)
`Kσ` = global geometric stiffness matrix
"""
function buckling(
    problem::TrussProblem{xdim,T},
    ginfo,
    einfo,
    vars=ones(T, getncells(getdh(problem).grid)),
    xmin=T(0.0);
    u=undef,
) where {xdim,T}
    dh = problem.ch.dh
    black = problem.black
    white = problem.white
    varind = problem.varind # variable index from cell index

    if u === undef
        u = ginfo.K \ ginfo.f
    end
    Kσs = get_truss_Kσs(problem, u, einfo.cellvalues)
    # TODO replace
    # assemble_k = TopOpt.AssembleK(problem)
    # Kσ = assemble_k(Kσs)

    Kσ = deepcopy(ginfo.K)

    if Kσ isa Symmetric
        Kσ.data.nzval .= 0
        assembler = Ferrite.AssemblerSparsityPattern(Kσ.data, T[], Int[], Int[])
    else
        Kσ.nzval .= 0
        assembler = Ferrite.AssemblerSparsityPattern(Kσ, T[], Int[], Int[])
    end

    # * assemble global geometric stiffness matrix
    global_dofs = zeros(Int, ndofs_per_cell(dh))
    Kσ_e = zeros(T, size(Kσs[1]))
    celliteratortype = CellIterator{typeof(dh).parameters...}
    celliterator::celliteratortype = CellIterator(dh)
    TK = eltype(Kσs)
    for (i, cell) in enumerate(celliterator)
        celldofs!(global_dofs, dh, i)
        Kσ_e = TK isa Symmetric ? Kσs[i].data : Kσs[i]
        if black[i]
            Ferrite.assemble!(assembler, global_dofs, Kσ_e)
        elseif white[i]
            # if PENALTY_BEFORE_INTERPOLATION
            px = xmin
            # else
            #     px = penalty(xmin)
            # end
            Kσ_e = px * Kσ_e
            Ferrite.assemble!(assembler, global_dofs, Kσ_e)
        else
            px = vars[varind[i]]
            # if PENALTY_BEFORE_INTERPOLATION
            # px = density(penalty(vars[varind[i]]), xmin)
            # else
            # px = penalty(density(vars[varind[i]], xmin))
            # end
            Kσ_e = px * Kσ_e
            Ferrite.assemble!(assembler, global_dofs, Kσ_e)
        end
    end

    #* apply boundary condition
    Kσ = apply_boundary_with_zerodiag!(Kσ, problem.ch)

    return ginfo.K, Kσ
end


function jt09()
#
#  test polynomial suggested by Jenkins and Traub
#
    p1 = [1;zeros(9);-1e-20]
    p2 = [1;zeros(9);1e20]
    p = conv(p1,p2)
    z1 = [-0.01000000000000;
          -0.00809016994375 + 0.00587785252292im;
          -0.00809016994375 - 0.00587785252292im;
          -0.00309016994375 + 0.00951056516295im;
          -0.00309016994375 - 0.00951056516295im;
          0.00309016994375 + 0.00951056516295im;
          0.00309016994375 - 0.00951056516295im;
          0.01000000000000;
          0.00809016994375 + 0.00587785252292im;
          0.00809016994375 - 0.00587785252292im;
         ]

    z2 = [-0.95105651629515 + 0.30901699437495im;
          -0.95105651629515 - 0.30901699437495im;
          -0.58778525229247 + 0.80901699437495im;
          -0.58778525229247 - 0.80901699437495im;
          -0.00000000000000 + 1.00000000000000im;
          -0.00000000000000 - 1.00000000000000im;
          0.95105651629515 + 0.30901699437495im;
          0.95105651629515 - 0.30901699437495im;
          0.58778525229247 + 0.80901699437495im;
          0.58778525229247 - 0.80901699437495im;
          ] * 100
    z = [[z1; z2] ones(20)]
    p, PolyZeros(z)
end


using Graphs
using Random

Random.seed!(1234)

function dijkstra_spt(g, v_src_initial)
  out = dijkstra_shortest_paths(g, [v_src_initial]; allpaths=true, trackvertices=true)
  paths = out.predecessors
  edges = Edge{Int}[]
  for v_dst_final in eachindex(paths)
    v_src = v_src_initial
    p = paths[v_dst_final]
    @show v_src_initial, p, v_dst_final
    for v_src in p
      push!(edges, Edge(v_src => v_dst_final))
    end
  end
  return edges
end

g = Graph(6, 10)

@show collect(edges(g))

t_mst = kruskal_mst(g)

@show t_mst

t_bfs = bfs_tree(g, 1)

@show collect(edges(t_bfs))

t_spt = dijkstra_spt(g, 1)

@show t_spt


using AcousticFWI,Seismic,PyPlot

function main()

    ns = 4
    ng = 250
    nz = 150
    nx = 250
    nt = 1024
    nf = 1024
    dz = 0.01
    dx = 0.01
    dt = 0.002
    f0 = 10.
    fmin = 0.5
    fmax = 30.
    ext = 50
    atten_max = 2.5

    vp = 2.1*ones(nz,nx)
    vp[61:100,:] = 3.0
    vp[101:end,:] = 2.7

    ot = zeros(ns)
    isz = 3*ones(Int,ns)
    isx = round(Int,collect(linspace(2,nx-1,ns)))
    igz = 3*ones(Int,ng)
    igx = round(Int,collect(linspace(1,nx,ng)))

    wav = Ricker(f0=f0,dt=dt)
    d = zeros(nt,ng,ns)
    for ishot = 1:ns
        u = Helmholtz(isz[ishot],isx[ishot],ot[ishot],vp,wav,fmin,fmax,nf,nt,dz,dx,dt,ext,atten_max)
        d[:,:,ishot] = u[:,3,igx]
    end

    vp0 = 2.5*ones(vp)

    # Perform 1 iteration of FWI to check if the Sensitivity operator passes the dot-product test

    nz = size(vp0,1) + 2*ext
    nx = size(vp0,2) + 2*ext

    L = Laplacian(nz,nx,dz,dx)
    M = MassMatrix(vp0,nz,nx,ext)
    A = Attenuation(nz,nx,ext,atten_max)
    R = Restriction(nz,nx,ext,igz,igx)

    D = fft([d ; zeros(nf-nt,ng,ns)],1)
    WAV = fft([wav;zeros(nf-length(wav))])
    fs = 1/dt
    df = fs/nf
    faxis = fftshift(-fs/2:df:fs/2-df)
    waxis = 2*pi*faxis

    _,iwmin = findmin(abs(fmin-faxis))
    _,iwmax = findmin(abs(fmax-faxis))

    param = Dict(:R=>R,:A=>A)

    iw = 15
    w = waxis[iw]
    s = zeros(eltype(D),nz*nx,ns)
    for ishot = 1:ns
        s[:,ishot] = Source(isz[ishot],isx[ishot],ot[ishot],WAV,waxis,w,nz,nx,ext)
    end
    H = lufact(L + w^2*M*A)
    U = zeros(eltype(D),nz*nx,ns)
    r = zeros(eltype(D),ng*ns)
    for ishot = 1:ns
        U[:,ishot] = H\s[:,ishot]
        r[(ishot-1)*ng+1:ishot*ng] = R*U[:,ishot] - D[iw,:,ishot]
    end
    param[:H] = H
    param[:U] = U
    param[:w] = w

    m = rand(nz*nx) + im*rand(nz*nx)
    r = rand(ns*ng) + im*rand(ns*ng)

    R = SensitivityMultiShot(m,false;param...)
    M = SensitivityMultiShot(r,true;param...)

    dotprodtest = (m[:]'*M[:])[1] - (r[:]'*R[:])[1]
    println("This number should be close to zero: ",dotprodtest)

end

main()


include("../matplotlib_settings.jl")

using GeometricIntegrators
using ChargedParticleDynamics.GuidingCenter4d.SymmetricLoop

using ChargedParticleDynamics.GuidingCenter4d: plot_integral_error, plot_loop, plot_trajectories

include("guiding_center_4d_settings_vprk.jl")
include("guiding_center_4d_poincare_invariant_1st.jl")


mutable struct MWSingleLayerField3D{K}
    wavenumber::K
end

mutable struct MWDoubleLayerField3D{K}
    wavenumber::K
end

"""
    MWSingleLayerField3D(wavenumber = error())

Create the single layer near field operator, for use with `potential`.
"""
MWSingleLayerField3D(;wavenumber = error("Missing arg: `wavenumber`")) = MWSingleLayerField3D(wavenumber)
MWDoubleLayerField3D(;wavenumber = error("Missing arg: `wavenumber`")) = MWDoubleLayerField3D(wavenumber)

const MWField3D = Union{MWSingleLayerField3D,MWDoubleLayerField3D}
quaddata(op::MWField3D,rs,els) = quadpoints(rs,els,(2,))
quadrule(op::MWField3D,refspace,p,y,q,el,qdata) = qdata[1,q]
function kernelvals(op::MWField3D,y,p)

        k = op.wavenumber
        r = y - cartesian(p)
        R = norm(r)

        kr = k * R
        expn = exp(-im * k * R)
        green = expn / (4pi*R)
        gradgreen = -(im*k + 1/R) * green / R * r

        krn = (trans=r, dist=R, green=green, gradgreen=gradgreen)
end

function integrand(op::MWSingleLayerField3D, krn, y, fp, p)

    γ = im * op.wavenumber

    j = fp.value
    ρ = fp.divergence

    G  = krn.green
    ∇G = krn.gradgreen

    -γ*G*j + ∇G*ρ/γ
end

function integrand(op::MWDoubleLayerField3D, krn, y, fp, p)

    j = fp.value
    ∇G = krn.gradgreen

    ∇G × j
end


using PyPlot
using Triangulation_diagrams

N = [ -1  -1  -1  0   0  1  0
       2   0  -2  0  -2  0  2 ]

T = [ 6  7  1  2  3  4
      7  2  2  3  5  5
      4  4  7  4  4  6 ]

E = [ 7  1  2  3  5  6
      1  2  3  5  6  7 ]

QN = 4

tri = Triangulation(N, T, E, QN)

figure(1)
draw_triangles(tri)
enumerate_triangles(tri)
enumerate_vertices(tri)
enumerate_bdry_edges(tri, 0.1)
mark_first_local_vertex(tri, 0.3)
axis("off")
savefig("example_triangulation.pdf")


const Curve_P_192 = ECP(
    p = 6277101735386680763835789423207666416083908700390324961279,
    n = 6277101735386680763835789423176059013767194773182842284081,
    #SEED = "3045ae6f c8422f64 ed579528 d38120ea e12196d5",
    #c = "3099d2bb bfcb2538 542dcd5f b078b6ef 5f3d6fe2 c745de65",
    b = "64210519 e59c80e7 0fa7e9ab 72243049 feb8deec c146b9b1",
    Gx = "188da80e b03090f6 7cbf20eb 43a18800 f4ff0afd 82ff1012",
    Gy = "07192b95 ffc8da78 631011ed 6b24cdd5 73f977a1 1e794811"
)

const Curve_P_244 = ECP(
    p = 26959946667150639794667015087019630673557916260026308143510066298881,
    n = 26959946667150639794667015087019625940457807714424391721682722368061,
    # SEED = "bd713447 99d5c7fc dc45b59f a3b9ab8f 6a948bc5",
    # c = "5b056c7e 11dd68f4 0469ee7f 3c7a7d74 f7d12111 6506d031 218291fb",
    b = "b4050a85 0c04b3ab f5413256 5044b0b7 d7bfd8ba 270b3943 2355ffb4",
    Gx = "b70e0cbd 6bb4bf7f 321390b9 4a03c1d3 56c21122 343280d6 115c1d21",
    Gy = "bd376388 b5f723fb 4c22dfe6 cd4375a0 5a074764 44d58199 85007e34"
)

const Curve_P_256 = ECP(
    p = 115792089210356248762697446949407573530086143415290314195533631308867097853951,
    n = 115792089210356248762697446949407573529996955224135760342422259061068512044369,
    #SEED = "c49d3608 86e70493 6a6678e1 139d26b7 819f7e90",
    #c = "7efba166 2985be94 03cb055c 75d4f7e0 ce8d84a9 c5114abc af317768 0104fa0d",
    b = "5ac635d8 aa3a93e7 b3ebbd55 769886bc 651d06b0 cc53b0f6 3bce3c3e 27d2604b",
    Gx = "6b17d1f2 e12c4247 f8bce6e5 63a440f2 77037d81 2deb33a0 f4a13945 d898c296",
    Gy = "4fe342e2 fe1a7f9b 8ee7eb4a 7c0f9e16 2bce3357 6b315ece cbb64068 37bf51f5"
)

const Curve_P_384 = ECP(
    p = 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319,
    n = 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643,
    #SEED = "a335926a a319a27a 1d00896a 6773a482 7acdac73",
    #c = "79d1e655 f868f02f ff48dcde e14151dd b80643c1 406d0ca1 0dfe6fc5 2009540a 495e8042 ea5f744f 6e184667 cc722483",
    b = "b3312fa7 e23ee7e4 988e056b e3f82d19 181d9c6e fe814112 0314088f 5013875a c656398d 8a2ed19d 2a85c8ed d3ec2aef",
    Gx = "aa87ca22 be8b0537 8eb1c71e f320ad74 6e1d3b62 8ba79b98 59f741e0 82542a38 5502f25d bf55296c 3a545e38 72760ab7",
    Gy = "3617de4a 96262c6f 5d9e98bf 9292dc29 f8f41dbd 289a147c e9da3113 b5f0b8c0 0a60b1ce 1d7e819d 7a431d7c 90ea0e5f"
)

const Curve_P_521 = ECP(
    p = 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151,
    n = 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449,
    #SEED = "d09e8800 291cb853 96cc6717 393284aa a0da64ba",
    #c = "0b4 8bfa5f42 0a349495 39d2bdfc 264eeeeb 077688e4 4fbf0ad8 f6d0edb3 7bd6b533 28100051 8e19f1b9 ffbe0fe9 ed8a3c22 00b8f875 e523868c 70c1e5bf 55bad637",
    b = "051 953eb961 8e1c9a1f 929a21a0 b68540ee a2da725b 99b315f3 b8b48991 8ef109e1 56193951 ec7e937b 1652c0bd 3bb1bf07 3573df88 3d2c34f1 ef451fd4 6b503f00",
    Gx = "c6 858e06b7 0404e9cd 9e3ecb66 2395b442 9c648139 053fb521 f828af60 6b4d3dba a14b5e77 efe75928 fe1dc127 a2ffa8de 3348b3c1 856a429b f97e7e31 c2e5bd66",
    Gy = "118 39296a78 9a3bc004 5c8a5fb4 2c7d1bd9 98f54449 579b4468 17afbd17 273e662c 97ee7299 5ef42640 c550b901 3fad0761 353c7086 a272c240 88be9476 9fd16650"
)

############### Koblitz curves ################

const Curve_K_163_PB = Koblitz{PB}(
    f = [163, 7, 6, 3, 0],
    a = 1,
    n = 5846006549323611672814741753598448348329118574063,
    Gx = "2 fe13c053 7bbc11ac aa07d793 de4e6d5e 5c94eee8",
    Gy = "2 89070fb0 5d38ff58 321f2e80 0536d538 ccdaa3d9"
)

const Curve_K_163_GNB = Koblitz{GNB}(
    n = 5846006549323611672814741753598448348329118574063,
    a = 1,
    m = 163,
    T = 4,
    Gx = "0 5679b353 caa46825 fea2d371 3ba450da 0c2a4541",
    Gy = "2 35b7c671 00506899 06bac3d9 dec76a83 5591edb2"
)

const Curve_K_233_PB = Koblitz{PB}(
    f = [233, 74, 0],
    a = 0,
    n = 3450873173395281893717377931138512760570940988862252126328087024741343,
    Gx = "172 32ba853a 7e731af1 29f22ff4 149563a4 19c26bf5 0a4c9d6e efad6126",
    Gy = "1db 537dece8 19b7f70f 555a67c4 27a8cd9b f18aeb9b 56e0c110 56fae6a3"
)

const Curve_K_233_GNB = Koblitz{GNB}(
    m = 233,
    T = 2,
    a = 0,
    n = 3450873173395281893717377931138512760570940988862252126328087024741343,
    Gx = "0fd e76d9dcd 26e643ac 26f1aa90 1aa12978 4b71fc07 22b2d056 14d650b3",
    Gy = "064 3e317633 155c9e04 47ba8020 a3c43177 450ee036 d6335014 34cac978"
)

const Curve_K_283_PB = Koblitz{PB}(
    f = [283, 12, 7, 5, 0],
    a = 0,
    n = 3885337784451458141838923813647037813284811733793061324295874997529815829704422603873,
    Gx = "503213f 78ca4488 3f1a3b81 62f188e5 53cd265f 23c1567a 16876913 b0c2ac24 58492836",
    Gy = "1ccda38 0f1c9e31 8d90f95d 07e5426f e87e45c0 e8184698 e4596236 4e341161 77dd2259"
)

const Curve_K_283_GNB = Koblitz{GNB}(
    m = 283,
    T = 6,
    a = 0,
    n = 3885337784451458141838923813647037813284811733793061324295874997529815829704422603873,
    Gx = "3ab9593 f8db09fc 188f1d7c 4ac9fcc3 e57fcd3b db15024b 212c7022 9de5fcd9 2eb0ea60",
    Gy = "2118c47 55e7345c d8f603ef 93b98b10 6fe8854f feb9a3b3 04634cc8 3a0e759f 0c2686b1"
)

const Curve_K_409_PB = Koblitz{PB}(
    f = [409, 87, 0],
    a = 0,
     n = 330527984395124299475957654016385519914202341482140609642324395022880711289249191050673258457777458014096366590617731358671,
    Gx = "060f05f 658f49c1 ad3ab189 0f718421 0efd0987 e307c84c 27accfb8 f9f67cc2 c460189e b5aaaa62 ee222eb1 b35540cf e9023746",
    Gy = "1e36905 0b7c4e42 acba1dac bf04299c 3460782f 918ea427 e6325165 e9ea10e3 da5f6c42 e9c55215 aa9ca27a 5863ec48 d8e0286b"
)

const Curve_K_409_GNB = Koblitz{GNB}(
    m = 409,
    T = 4,
    a = 0,
    n = 330527984395124299475957654016385519914202341482140609642324395022880711289249191050673258457777458014096366590617731358671,
    Gx = "1b559c7 cba2422e 3affe133 43e808b5 5e012d72 6ca0b7e6 a63aeafb c1e3a98e 10ca0fcf 98350c3b 7f89a975 4a8e1dc0 713cec4a",
    Gy = "16d8c42 052f07e7 713e7490 eff318ba 1abd6fef 8a5433c8 94b24f5c 817aeb79 852496fb ee803a47 bc8a2038 78ebf1c4 99afd7d6"
)

const Curve_K_571_PB = Koblitz{PB}(
    f = [571, 10, 5, 2, 0],
    n = 1932268761508629172347675945465993672149463664853217499328617625725759571144780212268133978522706711834706712800825351461273674974066617311929682421617092503555733685276673,
    a = 0,
    Gx = "26eb7a8 59923fbc 82189631 f8103fe4 ac9ca297 0012d5d4 60248048 01841ca4 43709584 93b205e6 47da304d b4ceb08c bbd1ba39 494776fb 988b4717 4dca88c7 e2945283 a01c8972",
    Gy = "349dc80 7f4fbf37 4f4aeade 3bca9531 4dd58cec 9f307a54 ffc61efc 006d8a2c 9d4979c0 ac44aea7 4fbebbb9 f772aedc b620b01a 7ba7af1b 320430c8 591984f6 01cd4c14 3ef1c7a3"
)

const Curve_K_571_GNB = Koblitz{GNB}(
    m = 571,
    T = 10,
    a = 0,
    n = 1932268761508629172347675945465993672149463664853217499328617625725759571144780212268133978522706711834706712800825351461273674974066617311929682421617092503555733685276673,
    Gx = "04bb2db a418d0db 107adae0 03427e5d 7cc139ac b465e593 4f0bea2a b2f3622b c29b3d5b 9aa7a1fd fd5d8be6 6057c100 8e71e484 bcd98f22 bf847642 37673674 29ef2ec5 bc3ebcf7",
    Gy = "44cbb57 de20788d 2c952d7b 56cf39bd 3e89b189 84bd124e 751ceff4 369dd8da c6a59e6e 745df44d 8220ce22 aa2c852c fcbbef49 ebaa98bd 2483e331 80e04286 feaa2530 50caff60"
)

##################### Binary curves ############################

const Curve_B_163_PB = EC2N{PB}(
    f = [163, 7, 6, 3, 0],
    a = 1,
    b = "2 0a601907 b8c953ca 1481eb10 512f7874 4a3205fd",
    n = 5846006549323611672814742442876390689256843201587,
    Gx = "3 f0eba162 86a2d57e a0991168 d4994637 e8343e36",
    Gy = "0 d51fbc6c 71a0094f a2cdd545 b11c5c0c 797324f1"
)

const Curve_B_163_GNB = EC2N{GNB}(
    m = 163,
    T = 4,
    a = 1,
    b = "6 645f3cac f1638e13 9c6cd13e f61734fb c9e3d9fb",
    n = 5846006549323611672814742442876390689256843201587,
    Gx = "0 311103c1 7167564a ce77ccb0 9c681f88 6ba54ee8",
    Gy = "3 33ac13c6 447f2e67 613bf700 9daf98c8 7bb50c7f"
)

const Curve_B_233_PB = EC2N{PB}(
    f = [233, 74, 0],
    a = 1,
    b = "066 647ede6c 332c7f8c 0923bb58 213b333b 20e9ce42 81fe115f 7d8f90ad",
    n = 6901746346790563787434755862277025555839812737345013555379383634485463,
    Gx = "0fa c9dfcbac 8313bb21 39f1bb75 5fef65bc 391f8b36 f8f8eb73 71fd558b",
    Gy = "100 6a08a419 03350678 e58528be bf8a0bef f867a7ca 36716f7e 01f81052"
)

const Curve_B_233_GNB = EC2N{GNB}(
    m = 233,
    T = 2,
    a = 1,
    b = "1a0 03e0962d 4f9a8e40 7c904a95 38163adb 82521260 0c7752ad 52233279",
    n = 6901746346790563787434755862277025555839812737345013555379383634485463,
    Gx = "18b 863524b3 cdfefb94 f2784e0b 116faac5 4404bc91 62a363ba b84a14c5",
    Gy = "049 25df77bd 8b8ff1a5 ff519417 822bfedf 2bbd7526 44292c98 c7af6e02"
)

const Curve_B_283_PB = EC2N{PB}(
    f = [283, 12, 7, 5, 0],
    n = 770675568902916283677847627294075626569625924376904889109196526770044277787378692871,
    a = 1,
    b = "27b680a c8b8596d a5a4af8a 19a0303f ca97fd76 45309fa2 a581485a f6263e31 3b79a2f5",
    Gx = "5f93925 8db7dd90 e1934f8c 70b0dfec 2eed25b8 557eac9c 80e2e198 f8cdbecd 86b12053",
    Gy = "3676854 fe24141c b98fe6d4 b20d02b4 516ff702 350eddb0 826779c8 13f0df45 be8112f4"
)

const Curve_B_283_GNB = EC2N{GNB}(
    m = 283,
    T = 6,
    a = 1,
    b = "157261b 894739fb 5a13503f 55f0b3f1 0c560116 66331022 01138cc1 80c0206b dafbc951",
    n = 7770675568902916283677847627294075626569625924376904889109196526770044277787378692871,
    Gx = "749468e 464ee468 634b21f7 f61cb700 701817e6 bc36a236 4cb8906e 940948ea a463c35d",
    Gy = "62968bd 3b489ac5 c9b859da 68475c31 5bafcdc4 ccd0dc90 5b70f624 46f49c05 2f49c08c"
)

const Curve_B_409_PB = EC2N{PB}(
    f = [409, 87, 0],
    n = 661055968790248598951915308032771039828404682964281219284648798304157774827374805208143723762179110965979867288366567526771,
    a = 1,
    b = "021a5c2 c8ee9feb 5c4b9a75 3b7b476b 7fd6422e f1f3dd67 4761fa99 d6ac27c8 a9a197b2 72822f6c d57a55aa 4f50ae31 7b13545f",
    Gx = "15d4860 d088ddb3 496b0c60 64756260 441cde4a f1771d4d b01ffe5b 34e59703 dc255a86 8a118051 5603aeab 60794e54 bb7996a7",
    Gy = "061b1cf ab6be5f3 2bbfa783 24ed106a 7636b9c5 a7bd198d 0158aa4f 5488d08f 38514f1f df4b4f40 d2181b36 81c364ba 0273c706"
)

const Curve_B_409_GNB = EC2N{GNB}(
    m = 409,
    T = 4,
    a = 1,
    b = "124d065 1c3d3772 f7f5a1fe 6e715559 e2129bdf a04d52f7 b6ac7c53 2cf0ed06 f610072d 88ad2fdc c50c6fde 72843670 f8b3742a",
    n = 661055968790248598951915308032771039828404682964281219284648798304157774827374805208143723762179110965979867288366567526771,
    Gx = "0ceacbc 9f475767 d8e69f3b 5dfab398 13685262 bcacf22b 84c7b6dd 981899e7 318c96f0 761f77c6 02c016ce d7c548de 830d708f",
    Gy = "199d64b a8f089c6 db0e0b61 e80bb959 34afd0ca f2e8be76 d1c5e9af fc7476df 49142691 ad303902 88aa09bc c59c1573 aa3c009a"
)

const Curve_B_571_PB = EC2N{PB}(
    f = [571, 10, 5, 2, 0],
    n = 3864537523017258344695351890931987344298927329706434998657235251451519142289560424536143999389415773083133881121926944486246872462816813070234528288303332411393191105285703,
    a = 1,
    b = "2f40e7e 2221f295 de297117 b7f3d62f 5c6a97ff cb8ceff1 cd6ba8ce 4a9a18ad 84ffabbd 8efa5933 2be7ad67 56a66e29 4afd185a 78ff12aa 520e4de7 39baca0c 7ffeff7f 2955727a",
    Gx = "303001d 34b85629 6c16c0d4 0d3cd775 0a93d1d2 955fa80a a5f40fc8 db7b2abd bde53950 f4c0d293 cdd711a3 5b67fb14 99ae6003 8614f139 4abfa3b4 c850d927 e1e7769c 8eec2d19",
    Gy = "37bf273 42da639b 6dccfffe b73d69d7 8c6c27a6 009cbbca 1980f853 3921e8a6 84423e43 bab08a57 6291af8f 461bb2a8 b3531d2f 0485c19b 16e2f151 6e23dd3c 1a4827af 1b8ac15b"
)

const Curve_B_571_GNB = EC2N{GNB}(
    m = 571,
    T = 10,
    a = 1,
    b = "3762d0d 47116006 179da356 88eeaccf 591a5cde a7500011 8d9608c5 9132d434 26101a1d fb377411 5f586623 f75f0000 1ce61198 3c1275fa 31f5bc9f 4be1a0f4 67f01ca8 85c74777",
    n = 3864537523017258344695351890931987344298927329706434998657235251451519142289560424536143999389415773083133881121926944486246872462816813070234528288303332411393191105285703,
    Gx = "0735e03 5def5925 cc33173e b2a8ce77 67522b46 6d278b65 0a291612 7dfea9d2 d361089f 0a7a0247 a184e1c7 0d417866 e0fe0feb 0ff8f2f3 f9176418 f97d117e 624e2015 df1662a8",
    Gy = "04a3642 0572616c df7e606f ccadaecf c3b76dab 0eb1248d d03fbdfc 9cd3242c 4726be57 9855e812 de7ec5c5 00b4576a 24628048 b6a72d88 0062eed0 dd34b109 6d3acbb6 b01a4a97"
)


using FastGaussQuadrature

function nearlyequal{T,U}(x::T,y::T,τ::U)
    X = norm(x)
    Y = norm(y)
    D = norm(x-y)

    x == y && return true

    (X <= τ && Y <= τ) && return true
    2 * D / (X+Y) < τ
end

function legendre(n,a,b)
    x,w = FastGaussQuadrature.gausslegendre(n)
    w *= (b-a)/2
    x = a+(x+1)*(b-a)/2
    return x, w
end

function dblquadints1{N}(v1,v2,v3,x,::Type{Val{N}},ri=-1.0,ro=1.0e15)
    G = 10000
    s, w = legendre(G, 0.0, 1.0)
    t1 = v1-v3
    t2 = v2-v3
    n = cross(t1,t2)
    I = zeros(eltype(x),N+3)
    K = zeros(typeof(x),N+3)
    a2 = norm(n)
    a2 < eps(eltype(x)) && return I, K
    n = normalize(n)
    d = dot(x-v1,n)
    ξ = x - d*n
    for g in 1:G
        u = s[g]
        for h in 1:G
            v = (1-u)*s[h]
            y = v3 + u*t1 + v*t2
            R = norm(x-y)
            (ri <= R <= ro) || continue
            j = a2 * w[g] * w[h] * (1-u)
            I[1] += j * d / R^3
            K[1] += j * (y-ξ) / R^3
            for n = -1 : N
                I[n+3] += j * R^n
                K[n+3] += j * (y-ξ) * R^n
            end
        end
    end
    return I, K
end


using ConjugatePriors

data = rand(Bernoulli(), 100)

prior = Beta(1, 1)
posterior = update(prior, data)

prior = Beta(1000, 100)
posterior = update(prior, data)


include("Conserved.jl")
include("RPCond.jl")

struct HLLC
end

function calcWaveSpeeds(left::RPCond,
	 				    right::RPCond)::Array{Float64,1}
	dl = left.density;
	pl = left.pressure;
	vl = left.velocity;
	cl = left.sound_speed
    dr = right.density;
	pr = right.pressure;
	vr = right.velocity;
	cr = right.sound_speed
	sl = min(vl - cl, vr - cr);
	sr = max(vl + cl, vr + cr);
	ss = (pr - pl + dl*vl*(sl - vl) - dr*vr*(sr - vr)) /
			(dl*(sl - vl) - dr*(sr - vr));
	return [sl, ss, sr];
end

function calcTotalEnergyDensity(state::RPCond)::Float64
	d = state.density
	p = state.pressure
	v = state.velocity
	e = state.energy
	return d*(e+v^2/2)
end

function calcStarredState(state::RPCond,
	 					  sk::Float64,
						  ss::Float64)::Conserved
	dk = state.density
	pk = state.pressure
	vk = state.velocity
	ds = dk*(sk - vk) / (sk - ss)
	ek = calcTotalEnergyDensity(state)
	mass = ds
	momentum = ds*ss
	energy = ek*ds / dk +
			ds*(ss - vk)*(ss + pk / dk / (sk - vk));
	return Conserved(mass, momentum, energy)
end

function calcMassDensity(state::RPCond)::Float64
	return state.density
end

function calcMomentumDensity(state::RPCond)::Float64
	return state.density*state.velocity
end

function primitive2conserved(state::RPCond)::Conserved
	return Conserved(calcMassDensity(state),
					 calcMomentumDensity(state),
					 calcTotalEnergyDensity(state))
end

function calcMassFlux(state::RPCond)::Float64
	return state.density*state.velocity
end

function calcMomentumFlux(state::RPCond)::Float64
	return state.density*state.velocity^2+state.pressure
end

function calcEnergyFlux(state::RPCond)::Float64
	energy_density = calcTotalEnergyDensity(state)
	enthalpy = energy_density+state.pressure
	return enthalpy*state.velocity
end

function primitive2flux(state::RPCond)::Conserved
	return Conserved(calcMassFlux(state),
					 calcMomentumFlux(state),
					 calcEnergyFlux(state))
end

function calcRestFrameFluxes(left::RPCond,
							 right::RPCond)::Conserved

	ws = calcWaveSpeeds(left, right)

	ul = primitive2conserved(left)
	ur = primitive2conserved(right)

	fl = primitive2flux(left)
	fr = primitive2flux(right)

	usl = calcStarredState(left, ws[1], ws[2])
	usr = calcStarredState(right, ws[3], ws[2])

	if ws[1]>0
		return fl
	elseif ws[2]>=0
		return fl + ws[1]*(usl-ul)
	elseif ws[3]>=0
		return fr + ws[3]*(usr-ur)
	else
		return fr
	end
end

function boostFlux(flux::Conserved,
				   velocity::Float64)::Conserved
	return Conserved(flux.mass,
					 flux.momentum+
					 velocity*flux.mass,
					 flux.energy+
					 0.5*flux.mass*velocity^2+
					 flux.momentum*velocity)
end

function boostCell(p::RPCond,
				   velocity::Float64)::RPCond
	return RPCond(p.density,
				  p.pressure,
				  p.velocity-velocity,
				  p.sound_speed,
				  p.energy)
end

function calcFlux(rs::HLLC,
				  left::RPCond,
				  right::RPCond,
				  velocity::Float64,
				  eos)::Conserved

	return boostFlux(calcRestFrameFluxes(boostCell(left,velocity),
										 boostCell(right,velocity)),
					 velocity)
end


function mform(n1::Int64,l1::Int64,m1::Int64,n2::Int64,l2::Int64,ρ::Float64,q::Float64)

    # The function wigner3j_f produces a vector of wigner symbols where each index corresponds to a value of l between
    # abs(l1-l2) and l1+l2 (and assumes m = -m1-m2) meaning that:
    #    collect(wigner3j_f(l1, l2, m1, m2)) ≈ [wigner3j(l1, l2, l3, m1, m2, -m1-m2) for l ∈ abs(l1-l2):l1+l2]

    wigner3j_l1_l2__0_0 = wigner3j_f(l1, l2, 0, 0)
    wigner3j_l1_l2__m1_negm2 = wigner3j_f(l1, l2, m1, -m1)

    δ = (ρ*n2+ n1)/(n1*ρ*n2)
    β = 2/n1
    α = 2*l1+1
    dd = -q^2/δ^2
    N1 = (4π*4/(ρ^(3/2) * (n1 * n2)^2)
          * √(gamma(n1-l1) * gamma(n2-l2)
              / (gamma(n1+l1+1) * gamma(n2+l2+1)))
          * (2/(n1))^l1 * (2/(ρ * n2))^l2 * √(π/(2q)))

    if n1 > l1 >= m1 && n2 > l2 >= m1
        # @floop from the package Floops.jl is able to parallelize a loop for us. If the loop contains a 'reduction'
        # (in our case, the sum), we just tell it which variable is being reduced with the @reduce macro and it takes
        # care of initializing the reduction variables and will make sure the multi-threading does not introduce a 'race
        # condition' which could cause the sum to be incorrect.
        @floop for l in abs(l1-l2):2:l1+l2
            for k in 0:n2-l2-1
                for k1 in 0:n1-l1-1
                    if (l1+l2) % 2 == 0
                        @reduce Sum += mfun(n1,l1,m1,n2,l2,ρ,q,k,k1,l, δ, β, α, dd, N1,
                                            wigner3j_l1_l2__0_0, wigner3j_l1_l2__m1_negm2)
                    else
                        @reduce Sum += mfun(n1,l1,m1,n2,l2,ρ,q,k,k1,l,  δ, β, α, dd, N1,
                                                 wigner3j_l1_l2__0_0, wigner3j_l1_l2__m1_negm2) #without the i factor
                    end
                end
            end
        end
        return N1 * Sum
    else
        "Incorrect Combination"
    end
end

function mfun(n1::Int64,l1::Int64,m1::Int64,n2::Int64,l2::Int64,ρ::Float64,q::Float64,k::Int64,k1::Int64,l::Int64,
              δ, β, α, dd, N1,
              wigner3j_l1_l2__0_0, wigner3j_l1_l2__m1_negm2)
    m=0
    n = n1-l1-1
    γ = l1+l2+k+3/2
    ν = l+1/2
    aa = (ν+γ+k1+1)/2
    bb = (ν+γ+k1+2)/2
    cc = 1+ν

    I1 = gamma(n2+l2+1) * gamma(n+α+1) * ( (-2)^k * (-β)^k1 * q^ν *gamma(ν+γ+k1+1)
          / (gamma(k+1) * gamma(n2-l2-k) * gamma(2*l2+k+2) * (ρ*n2)^k * gamma(k1+1)
             * gamma(n-k1+1) * gamma(α+k1+1) * 2^ν * gamma(ν+1) * δ^(ν+γ+k1+1)) * _₂F₁(aa,bb,cc,dd))

    if (l1+l2) % 2 == 0
        A1 = (-1)^(l/2+m1) * √((2*l+1) * (2*l1+1) * (2*l2+1) * (2*l+1) / (4π*4π))
    else
        A1 = (-1)^((l-1)/2+m1) * √((2*l+1) * (2*l1+1) * (2*l2+1) * (2*l+1) / (4π*4π))
    end
    return I1 * A1 * wigner3j_l1_l2__0_0[l] * wigner3j_l1_l2__m1_negm2[l]
end


function solve_for_slope(input, dx, dy)
	ret = 0
	x_coord = 1
	width, tail = Iterators.peel(input)
	width = length(width)
	for line in Iterators.partition(tail, dy)
		x_coord += dx
		if (x_coord > width)
			x_coord -= width
		end
		if (line[dy][x_coord] == '#')
			ret += 1
		end
	end
	ret
end

function solve_for_all(input)
	slopes = [(1, 1) (3, 1) (5, 1) (7, 1) (1, 2)]
	reduce(*, map(slp -> solve_for_slope(eachline(input), slp[1], slp[2]), slopes))
end

@assert solve_for_slope(eachline("input/test/test_day3.txt"), 3, 1) == 7 ["test case 1 failed"]
@assert solve_for_all("input/test/test_day3.txt") == 336 ["test case 2 failed"]

println("First star: " * string(solve_for_slope(eachline("input/day3.txt"), 3, 1)))
println("Second star: " * string(solve_for_all("input/day3.txt")))


# @testset "fit moments" begin
# end;

# @testset "fit to quantilepoint and mode - ignore mode" begin
# end;

# @testset "fit two quantiles same" begin
# end;

@testset "fit two quantiles" begin
    D = Weibull(1,1)
    #Plots.plot(D)
    median(D)
    qpl = @qp_m(median(D))
    qpu = @qp_u(quantile(D, 0.95))
    d = fit(Weibull, qpl, qpu);
    @test d == D
    #
    qpl = @qp_m(0.5)
    qpu = @qp_uu(5)
    d = fit(Weibull, qpl, qpu);
    @test median(d) == 0.5
    @test quantile(d, 0.975) ≈ 5
    #Plots.plot(d);
end;


# # Machine Learning Problem : Housing Dataset
#
# The housing problem functions as a starting point in Machine Learning.
# We'll be demonstrating the use of Julia's [Flux Package](https://fluxml.ai/)
# to do this problem.
#
# The data replicates the housing data example from the Knet.jl readme. Although we
# could have reused more of Flux (see the mnist example), the library's
# abstractions are very lightweight and don't force you into any particular
# strategy.
#
# [This](http://www.mit.edu/~6.s085/notes/lecture3.pdf) might help you know more about the fundamentals of what
# we're about to do. If you don't understand something there which is also not mentioned here in this file,
# you may overlook that (or search it up on google to quench your curiosity :-)

using Flux.Tracker, Statistics, DelimitedFiles
using Flux.Tracker: Params, gradient, update!
using DelimitedFiles, Statistics
using Flux: gpu

# ## Getting the data and other pre-processing.
# We'll start by getting `housing.data` and splitting it into
# training and test sets.
# Training Dataset is the sample of data used to **fit** the model while
# Test Dataset is the sample of data used to provide an unbiased evaluation
# of a final model fit on the training dataset.

# Our aim is to predict the price of the house. In this dataset, the last
# feature is the price and would therefore be our target.

cd(@__DIR__)

isfile("housing.data") ||
  download("https://raw.githubusercontent.com/MikeInnes/notebooks/master/housing.data",
           "housing.data")

rawdata = readdlm("housing.data")'

#-

# Specifying the split ratio and **x** and **y**
split_ratio = 0.1

x = rawdata[1:13,:] |> gpu
y = rawdata[14:14,:] |> gpu

# ### Normalising
# What is the need ?
# Normalization is a technique often applied as part of data preparation for machine learning.
# The goal of normalization is to change the values of numeric columns in the dataset to a common scale,
# without distorting differences in the ranges of values. For machine learning, every dataset does not require normalization.
# It is required only when features have different ranges like in this case.

x = (x .- mean(x, dims = 2)) ./ std(x, dims = 2)

# ### Splitting into test and training sets.

split_index = floor(Int,size(x,2)*split_ratio)
x_train = x[:,1:split_index]
y_train = y[:,1:split_index]
x_test = x[:,split_index+1:size(x,2)]
y_test = y[:,split_index+1:size(x,2)]

# ## The Model
# Here comes everyone's favourite part : implementing a machine learning model.
#
# A ML model is in it's simplest terms a mathematical model which has a number of parameters
# that need to be learned from the data provided. The data has an important task: to fit our model parameters.
# The more data we have, the more we can accurately predict the target.
#
# Hyperparameters aren't learnt during the training process. They can be treated as constants that are fixed for the
# entire process. These parameters express important properties of the model such as its complexity or how fast it should learn.
#
# We'll now define the Weight (W) and the Bias (b) terms. They are our hyperparameter which
# we tune to enhance our predictions during gradient descent.
# To get an intution about how gradientDescent actually works, check out Andrew Ng's awesome explaination
# here. [Video 1: Intution](https://www.youtube.com/watch?v=rIVLE3condE) |
# [Video 2: The Algorithm](https://www.youtube.com/watch?v=yFPLyDwVifc)

W = param(randn(1,13)/10) |> gpu
b = param([0.]) |> gpu

# Here are our prediction and loss functions.
# - The prediction functions returns our prediction of the price of the house as
# suggested by our 2 hyperparameters: W and b.
# - MSE is the average of the squared error that is used as the loss function for least squares regression.
# It is defined as the sum, over all the data points, of the square of the difference between the predicted and actual target
# variables, divided by the number of data points.
#
# Loss functions evaluate how well your algorithm models your dataset.
# If predictions are off, the loss function is high. If they're good, it'll be low.

predict(x) = W*x .+ b
meansquarederror(ŷ, y) = sum((ŷ .- y).^2)/size(y, 2)
loss(x, y) = meansquarederror(predict(x), y)

# ### Gradient Descent
# Optimizing our parameters to get accurate prediction. Learn more from the links mentioned above.

η = 0.1
θ = Params([W, b])

for i = 1:10
  g = gradient(() -> loss(x_train, y_train), θ)
  for x in θ
    update!(x, -g[x]*η)
  end
  @show loss(x_train, y_train)
end

# ## Predictions
# Now we're in a position to know how well our program works on the given data.

err = meansquarederror(predict(x_test),y_test)
println(err)

# The prepared model might not very good for predicting the housing prices and may have high error.
# One can improve the prediction results using many other possible machine learning algorithms and techniques.
# If this was your first ML project in Flux, Congrats!
#
# You should have gotten a gist of basic ML functionality in Flux Package using Julia by now.

# ## References :
# 1. [Introduction to Loss Functions](https://algorithmia.com/blog/introduction-to-loss-functions)
# 2. [Why Data Normalization is necessary for Machine Learning models](https://medium.com/@urvashilluniya/why-data-normalization-is-necessary-for-machine-learning-models-681b65a05029)
# 3. [About Train, Validation and Test Sets in Machine Learning](https://towardsdatascience.com/train-validation-and-test-sets-72cb40cba9e7)
# 4. [How to select the Right Evaluation Metric for Machine Learning Models: Part 1 Regression Metrics](https://towardsdatascience.com/how-to-select-the-right-evaluation-metric-for-machine-learning-models-part-1-regrression-metrics-3606e25beae0)
# 5. [MIT's Notes on Linear Regression](http://www.mit.edu/~6.s085/notes/lecture3.pdf)
# 6. [ML | Hyperparameters: An Understanding](https://www.geeksforgeeks.org/ml-hyperparameter-tuning/)
#


using Distributions
using Base.Test

# Core testing procedure

function test_mixture(g::UnivariateMixture, n::Int, ns::Int)
    X = zeros(n)
    for i = 1:10
        X[i] = rand(g)
    end

    cs = components(g)
    pr = probs(g)
    @assert length(cs) == length(pr)

    # mean
    mu = 0.0
    for k = 1:length(cs)
        mu += pr[k] * mean(cs[k])
    end
    @test_approx_eq mean(g) mu

    # ground-truth
    p0 = zeros(n)
    for i = 1:n
        p0_i = 0.0
        x_i = X[i]
        for k = 1:length(cs)
            p0_i += pr[k] * pdf(cs[k], x_i)
        end
        p0[i] = p0_i
    end
    lp0 = log(p0)

    for i = 1:n
        @test_approx_eq pdf(g, X[i]) p0[i]
        @test_approx_eq logpdf(g, X[i]) lp0[i]
    end

    p_e = pdf(g, X)
    lp_e = logpdf(g, X)
    @assert isa(p_e, Vector{Float64}) && length(p_e) == n
    @assert isa(lp_e, Vector{Float64}) && length(lp_e) == n

    @test_approx_eq p_e p0
    @test_approx_eq lp_e lp0

    # sampling
    Xs = rand(g, ns)
    @test isa(Xs, Vector{Float64})
    @test length(Xs) == ns
    @test_approx_eq_eps mean(Xs) mean(g) 0.01
end

function test_mixture(g::MultivariateMixture, n::Int, ns::Int)
    X = zeros(length(g), n)
    for i = 1:10
        X[:,i] = rand(g)
    end

    cs = components(g)
    pr = probs(g)
    @assert length(cs) == length(pr)

    # mean
    mu = 0.0
    for k = 1:length(cs)
        mu += pr[k] * mean(cs[k])
    end
    @test_approx_eq mean(g) mu

    # ground-truth
    p0 = zeros(n)
    for i = 1:n
        p0_i = 0.0
        x_i = X[:,i]
        for k = 1:length(cs)
            p0_i += pr[k] * pdf(cs[k], x_i)
        end
        p0[i] = p0_i
    end
    lp0 = log(p0)

    for i = 1:n
        @test_approx_eq pdf(g, X[:,i]) p0[i]
        @test_approx_eq logpdf(g, X[:,i]) lp0[i]
    end

    p_e = pdf(g, X)
    lp_e = logpdf(g, X)
    @assert isa(p_e, Vector{Float64}) && length(p_e) == n
    @assert isa(lp_e, Vector{Float64}) && length(lp_e) == n

    @test_approx_eq p_e p0
    @test_approx_eq lp_e lp0

    # sampling
    Xs = rand(g, ns)
    @test isa(Xs, Matrix{Float64})
    @test size(Xs) == (length(g), ns)
    @test_approx_eq_eps vec(mean(Xs, 2)) mean(g) 0.01
end

# Tests

println("    testing UnivariateMixture")

g_u = MixtureModel(Normal, [(0.0, 1.0), (2.0, 1.0), (-4.0, 1.5)], [0.2, 0.5, 0.3])
@test isa(g_u, MixtureModel{Univariate, Continuous, Normal})
@test length(components(g_u)) == 3
test_mixture(g_u, 1000, 10^6)

println("    testing MultivariateMixture")
g_m = MixtureModel(
    IsoNormal[ MvNormal([0.0, 0.0], 1.0),
               MvNormal([0.2, 1.0], 1.0),
               MvNormal([-0.5, -3.0], 1.6) ],
    [0.2, 0.5, 0.3])
@test isa(g_m, MixtureModel{Multivariate, Continuous, IsoNormal})
@test length(components(g_m)) == 3
@test length(g_m) == 2
test_mixture(g_m, 1000, 10^6)


"""
    CalculusWithJulia

A package to accompany [notes](calculuswithjulia.github.io) on using Julia with calculus.

This package does two things:

* It loads a few other packages making it easier to use (and install) the functionality provided by them and

* It defines a handful of functions for convenience. The exported ones
are `e`, `unzip`, `rangeclamp` `tangent`, `secant`, `D` (and the prime
notation), `divergence`, `gradient`, `curl`, and `∇`.

## Packages loaded by `CalculusWithJulia`

* The `SpecialFunctions` is loaded giving access to a few special functions used in these notes, e.g., `airyai`, `gamma`

* The `ForwardDiff` package is loaded giving access to its  `derivative`,  `gradient`, `jacobian`, and `hessian` functions for finding automatic derivatives of functions. In addition, this package defines `'` (for functions) to return a derivative (which commits [type piracy](https://docs.julialang.org/en/v1/manual/style-guide/index.html#Avoid-type-piracy-1)), `∇` to find the gradient (`∇(f)`), the divergence (`∇⋅F`). and the curl (`∇×F`), along with `divergence` and `curl`.

* The `LinearAlgebra` package is loaded for access to several of its functions for working with vectors `norm`, `cdot` (`⋅`), `cross` (`×`), `det`.

* The `PlotUtils` package is loaded so that its `adapted_grid` function is available.

## Packages with extra features added when loaded

The `Julia` package `Requires` allows for additional code to be run when another package is loaded. The following packages have additional code to load:

* `SymPy`: for symbolic math.

* `Plots`: the `Plots` package provides a plotting interface.

Several plot recipes are provided to ease the creation of plots in the notes.
`plotif`, `trimplot`, and `signchart` are used for plotting univariate functions;
`plot_polar` and `plot_parametric` are used to plot curves in 2 or 3 dimensions;
`plot_parametric` also makes the plotting og parameterically defined surfaces easier;
`vectorfieldplot` and `vectorfieldplot3d` can be used to plot vector fields; and
`arrow` is a simplified interface to `quiver` that also indicates 3D vectors.

* `MDBM`: the  `MDBM` package is useful for finding contours in ``2`` or ``3`` dimensions.

## Other packages with recurring roles in the notes

* `Roots` is used to find zeros of univariate functions

* `QuadGK` and `HCubature` are used for numeric integration

* `ImplicitEquations` is used to make 2-dimesional plots of implicit equations.

"""
module CalculusWithJulia

using Reexport
using RecipesBase
import PlotUtils
import ForwardDiff
export ForwardDiff

@reexport using LinearAlgebra
@reexport using SpecialFunctions

using EllipsisNotation
import EllipsisNotation: Ellipsis
export ..
import IntervalSets
import IntervalSets: ClosedInterval

include("multidimensional.jl")
include("derivatives.jl")
include("integration.jl")
include("plot-utils.jl")
include("plot-recipes.jl")
include("WeaveSupport/WeaveSupport.jl")

using Requires

function __init__()
    @require SymPy="24249f21-da20-56a4-8eb1-6a02cf4ae2e6" include("sympy.jl")
    @require Polynomials="f27b6e38-b328-58d1-80ce-0feddd5e7a45" include("polynomials.jl")
    @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" include("plots.jl")
    @require MDBM="ea0cff06-48de-41e3-bd0e-d3c1feffd247" include("implicit_equation.jl")
end

e = exp(1)
export e

export unzip, rangeclamp
export tangent, secant, D
export riemann
export divergence, gradient, curl, ∇

end # module


# Test naive implementation of entropy calculations towards efficient calculation of entropy on a manifold

export
  logmap_SO2,
  difftheta,
  addtheta,
  rbfAccAt!,
  rbf!,
  rbf,
  evaluateManifoldNaive1D!,
  manifoldLooCrossValidation,
  kde!_CircularNaiveCV,
  getCircMu,
  getCircLambda

global const reci_s2pi=1.0/sqrt(2.0*pi) # 1.0/2.5066282746310002

# On-manifold circular product callbacks

# manifold distance, add, and subtract
function logmap_SO2(Rl::Matrix{Float64})
  ct = abs(Rl[1,1]) > 1.0 ? 1.0 : Rl[1,1]  # reinserting the sign below
  -sign(Rl[2,1])*acos(ct)
end
difftheta(wth1, wth2)::Float64 = logmap_SO2(TUs.R(wth1)'*TUs.R(wth2))
addtheta(wth1, wth2)::Float64 = TUs.wrapRad( wth1+wth2 )

# manifold get Gaussian products mean
getCircMu = (m::Vector{Float64}, s::Vector{Float64}, dummy::Float64) -> TUs.wrapRad(get2DMu(m, s, diffop=difftheta, initrange=(-pi+0.0,pi+0.0)))
# getCircMu = (m::Vector{Float64}, s::Vector{Float64}, dummy::Float64) -> TUs.wrapRad(get2DMuMin(m, s, diffop=difftheta, initrange=(-pi+0.0,pi+0.0)))

getCircLambda(x) = getEuclidLambda(x)

"""
    $SIGNATURES

Probability density function `p(x)`, as estimated by kernels
```math
hatp_{-j}(x) = 1/(N-1) Σ_{i != j}^N frac{1}{sqrt{2pi}σ } exp{ -frac{(x-μ)^2}{2 σ^2} }
```
"""
function normDistAccAt!(ret::AV,
                        idx::Int,
                        x::Float64,
                        sigma::Float64,
                        w::Float64=1.0  )::Nothing where {AV <: AbstractVector}
    global reci_s2pi
    @fastmath ret[idx] += w*reci_s2pi/sigma * exp( - (x^2)/(2.0*(sigma^2)) )
    return nothing
end

function rbfAccAt!(ret::AV,
                   idx::Int,
                   x::Float64,
                   μ::Float64=0.0,
                   σ::Float64=1.0,
                   w::Float64=1.0,
                   diffop::Function=-)::Nothing  where {AV <: AbstractVector}
    #
    normDistAccAt!(ret, idx, diffop(x, μ), σ, w)
    nothing
end
function rbf!(ret::AV,
              x::Float64,
              μ::Float64=0.0,
              σ::Float64=1.0,
              diffop::Function=-)::Nothing  where {AV <: AbstractVector}
    #
    ret[1] = 0.0
    normDistAccAt!(ret, 1, diffop(x,μ), σ)
    nothing
end

function rbf(x::Float64, μ::Float64=0.0, σ::Float64=1.0)
    ret = Vector{Float64}(undef, 1) # initialized in rbf!(..)
    rbf!(ret, x, μ, σ)
    return ret[1]
end

"""
    $SIGNATURES

Evalute the KDE naively as equally weighted Gaussian kernels with common bandwidth.
This function does, however, allow on-manifold evaluations.
"""
function evaluateManifoldNaive1D!(ret::Vector{Float64},
                                  idx::Int,
                                  pts::Array{Float64,1},
                                  bw::Float64,
                                  x::Array{Float64,1},
                                  loo::Int=-1,
                                  diffop=-  )::Nothing
    #
    dontskip = loo == -1
    N = length(pts)
    reci_N = dontskip ? 1.0/N : 1.0/(N-1)
    for j in 1:N
        if dontskip || loo != j
            manifolddist = diffop(pts[loo], pts[j])
            normDistAccAt!(ret, idx, manifolddist, bw, reci_N)
        end
    end

    return nothing
end
function evaluateManifoldNaive1D!(ret::Vector{Float64},
                                  idx::Int,
                                  bd::BallTreeDensity,
                                  x::Array{Float64,1},
                                  loo::Int=-1,
                                  diffop=-  )::Nothing
    #
    evaluateManifoldNaive1D!(ret, idx, getPoints(bd)[:], getBW(bd)[1,1], x, loo, diffop )
end

"""
    $SIGNATURES

Calculate negative entropy with leave one out (j'th element) cross validation.

Background
==========

From: Silverman, B.: Density Estimation for Statistics and Data Analysis, 1986, p.52

Probability density function `p(x)`, as estimated by kernels
```math
hatp_{-j}(x) = 1/(N-1) Σ_{i != j}^N frac{1}{sqrt{2pi}σ } exp{ -frac{(x-μ)^2}{2 σ^2} }
```
and has Cross Validation number as the average log evaluations of leave one out `hatp_{-j}(x)`:
```math
CV(p) = 1/N Σ_i^N log hat{p}_{-j}(x_i)
```

This quantity `CV` is related to an entropy `H(p)` estimate via:
```math
H(p) = -CV(p)
```
"""
function manifoldLooCrossValidation(pts::Array,
                            bw::Float64;
                            own::Bool=true,
                            diffop::Function=-  )
    #
    N = maximum(size(pts))
    h = [bw;]
    loo = zeros(N)
    @inbounds for i in 1:N
        if !own
            # validation version
            pts99 = pts[[1:(i-1);(i+1):end]]
            p99 = kde!(pts99, h)
            loo[i] = log(p99([pts[i];])[1])
        else
            # own naive entropy calculation
            loo[i] = 0.0
            evaluateManifoldNaive1D!(loo, i, pts, bw, pts, i, diffop)
            loo[i] = log(loo[i])
        end
    end
    return  sum(loo)/N
end

function kde!_CircularNaiveCV(points::A) where {A <: AbstractArray{Float64,1}}

  # initial setup parameters
  dims = 1 # size(points,1)
  bwds = zeros(dims)
  # initial testing values
  lower = 0.001
  upper = 2pi

  # excessive for loop for leave one out likelihiood cross validation (Silverman 1986, p.52)
  for i in 1:dims
    minEntropyLOOCV = (bw) -> -manifoldLooCrossValidation(points, bw, own=true, diffop=difftheta)
    res = optimize(minEntropyLOOCV, lower, upper, GoldenSection(), x_tol=0.001)
    bwds[i] = res.minimizer
  end

  # cosntruct the kde with CV optimized bandwidth
  p = kde!( points, bwds, (addtheta,), (difftheta,) )

  return p
end

#


module _XFuzzEn
export XFuzzEn
using Statistics: mean, std
    """
        XFuzz, Ps1, Ps2 = XFuzzEn(Sig)

    Returns the cross-fuzzy entropy estimates (`XFuzz`) and the average fuzzy
    distances (m:Ps1, m+1:Ps2) for m = [1,2] estimated for the data sequences
    contained in 'Sig', using the default parameters: embedding dimension = 2,
    time delay = 1, fuzzy function (Fx) = 'default',
    fuzzy function parameters (r) = [0.2, 2], logarithm = natural

        XFuzz, Ps1, Ps2 = XFuzzEn(Sig::AbstractArray{T,2} where T<:Real; m::Int=2, tau::Int=1, r::Union{Real,Tuple{Real,Real}}=(.2,2), Fx::String="default", Logx::Real=exp(1))

    Returns the cross-fuzzy entropy estimates (`XFuzz`) for dimensions = [1,...,m]
    estimated for the data sequences in `Sig` using the specified 'keyword' arguments:

    # Arguments:
    `m`     - Embedding Dimension, a positive integer   [default: 2]    \n
    `tau`   - Time Delay, a positive integer            [default: 1]    \n
    `Fx`    - Fuzzy funtion name, one of the following:
              {`default`,`sigmoid`,`modsampen`,`gudermannian`,`linear`}\n
    `r`     - Fuzzy function parameters, a scalar or a 2 element tuple
              of positive values. The `r` parameters for each fuzzy
              function are defined as follows:\n
              sigmoid:       r(1) = divisor of the exponential argument
                             r(2) = value subtracted from argument (pre-division)
              modsampen:     r(1) = divisor of the exponential argument
                             r(2) = value subtracted from argument (pre-division)
              default:       r(1) = divisor of the exponential argument
                             r(2) = argument exponent (pre-division)
              gudermannian:  r    = a scalar whose value is the numerator of
                                    argument to gudermannian function:
                                    GD(x) = atan(tanh(`r`/x)).
              linear:        r    = an integer value. When `r` = 0, the
                                    argument of the exponential function is
                                    normalised between [0 1]. When `r` = 1,
                                    the minimuum value of the exponential
                                    argument is set to 0.     \n
    `Logx`  - Logarithm base, a positive scalar  \n

    For further information on the 'keyword' arguments, see the EntropyHub guide.

    # See also `FuzzEn`, `XSampEn`, `XApEn`, `FuzzEn2D`, `XMSEn`, `MSEn`

    # References:
        [1] Hong-Bo Xie, et al.,
            "Cross-fuzzy entropy: A new method to test pattern synchrony of
            bivariate time series."
            Information Sciences
            180.9 (2010): 1715-1724.

    """
    function XFuzzEn(Sig::AbstractArray{T,2} where T<:Real; m::Int=2, tau::Int=1,
         r::Union{Real,Tuple{Real,Real}}=(.2,2), Fx::String="default", Logx::Real=exp(1))

    (size(Sig,2) > size(Sig,1)) ? Sig = transpose(Sig) : nothing
    N = size(Sig,1)
    (N>10 && size(Sig,2)==2) ? nothing :  error("Sig:   must be a 2-columns matrix")
    (m > 0) ? nothing : error("m:     must be an integer > 0")
    (tau>0) ? nothing : error("tau:   must be an integer > 0")
    (minimum(r)>=0 && length(r)<=2) ? nothing :
        error("r:  must be a scalar or 2 element vector of positive values")
    (lowercase(Fx) in ["default","sigmoid","modsampen","gudermannian","linear"]) ?
        nothing : error("Fx:    must be one of the following strings -
        'default', 'sigmoid', 'modsampen', 'gudermannian', 'linear'")
    (Logx>0) ? nothing : error("Logx:   must be a positive number > 0")

    if length(r) == 2 && lowercase(Fx)=="linear"
        r = 0;
        print("Multiple values for r entered. Default value (0) used.\n")
    elseif length(r) == 2 && lowercase(Fx)=="gudermannian"
        r = r[1]
        print("Multiple values for r entered. First value used.\n")
    end

    S1 = Sig[:,1]; S2 = Sig[:,2]
    m += 1
    Fun = getfield(_XFuzzEn,Symbol(lowercase(Fx)))
    Sx1 = zeros(N,m)
    Sx2 = zeros(N,m)
    for k = 1:m
        Sx1[1:N-(k-1)*tau,k] = S1[1 + (k-1)*tau:N]
        Sx2[1:N-(k-1)*tau,k] = S2[1 + (k-1)*tau:N]
    end

    Ps1 = zeros(m)
    Ps2 = zeros(m-1)
    Ps1[1] = 1
    for k = 2:m
        N1 = N - k*tau
        N2 = N - (k-1)*tau
        A = Sx1[1:N2,1:k] .- mean(Sx1[1:N2,1:k],dims=2)
        B = Sx2[1:N2,1:k] .- mean(Sx2[1:N2,1:k],dims=2)
        d2 = zeros(N2,N2)
        for p = 1:N2
            Mu2 = maximum(abs.(transpose(A[p,:]) .- B),dims=2)
            d2[p,:] = Fun(Mu2,r)
        end
        Ps1[k] = mean(d2[1:N1,1:N1])
        Ps2[k-1] = mean(d2)
    end

    XFuzz = log.(Logx, Ps1[1:end-1]) .- log.(Logx, Ps2)
    return XFuzz, Ps1, Ps2
    end

    function sigmoid(x,r)
        if length(r) == 1
            error("When Fx = 'Sigmoid', r must be a two-element vector.")
        end
        y = inv.(1 .+ exp.((x.-r[2])/r[1]))
        return y
    end

    function modsampen(x,r)
        if length(r) == 1
            error("When Fx = 'Modsampen', r must be a two-element vector.")
        end
        y = inv.(1 .+ exp.((x.-r[2])/r[1]))
        return y
    end

    function default(x,r)
        if length(r) == 1
            error("When Fx = 'Default', r must be a two-element vector.")
        end
        y = exp.(-(x.^r[2])/r[1])
        return y
    end

    function gudermannian(x,r)
        if r <= 0
            error("When Fx = 'Gudermannian', r must be a scalar > 0.")
        end
        y = atan.(tanh.(r[1]./x))
        y ./= maximum(y)
        return y
    end

    function linear(x,r)
        if r == 0 && length(x)>1
            y = exp.(-(x .- minimum(x))/(maximum(x)-minimum(x)))
        elseif r == 1
            y = exp.(-(x .- minimum(x)))
        elseif r == 0 && length(x)==1
            y = [0]
        else
            error("When Fx = 'Linear', r must be 0 or 1.")
        end
        return y
    end

end

"""
Copyright 2021 Matthew W. Flood, EntropyHub

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

For Terms of Use see https://github.com/MattWillFlood/EntropyHub
"""


# This file is part of IntegerSequences.
# Copyright Peter Luschny. License is MIT.

(@__DIR__) ∉ LOAD_PATH && push!(LOAD_PATH, (@__DIR__))

module Fibonacci

using Nemo, IterTools

export ModuleFibonacci
export I000045, F000045, L000045, V000045, R000045, is000045, L039834

"""

* I000045, F000045, L000045, V000045, R000045, is000045
"""
const ModuleFibonacci = ""

struct FiboIterate
    length
end

function Base.iterate(I::FiboIterate)
    I.length == 0 && return nothing
    state = (fmpz(0), (fmpz(0), fmpz(1), fmpz(1)))
end

function Base.iterate(I::FiboIterate, (a, b, c))
    c >= I.length && return nothing
    (b, (b, a + b, c + 1))
end

Base.length(f::FiboIterate) = f.length
Base.eltype(f::FiboIterate) = fmpz

"""

Iterate over the first ``n`` Fibonacci numbers.
"""
I000045(n) = FiboIterate(n)

"""

Iterate over the Fibonacci numbers which do not exceed ``n``.
"""
F000045(n) = IterTools.takewhile(k -> k <= n, FiboIterate(n + 1))

"""

Return the first ``n`` Fibonacci numbers in an array.
"""
L000045(n) = Base.collect(FiboIterate(n))

"""

Return the ``n``-th Fibonacci number.
"""
function V000045(n)
    F = fmpz[1 1; 1 0]
    Fn = F^n
    Fn[2, 1]
end

"""

Return the ``n``-th Fibonacci number, explicite formula by Paul Hankin.
"""
function HankinFibonacci(n)
    n < 2 && return n
    a = <<(fmpz(4), (n - 1) * (n + 2))
    b = <<(fmpz(4), 2 * (n - 1))
    c = <<(fmpz(2), n - 1)
    div(a, b - c - 1) & (c - 1)
end

"""

Fibonacci function for real values, returns a Float64.
"""
function R000045(x::Float64)
    (Base.MathConstants.golden^x -
     cos(x * Base.MathConstants.pi) * Base.MathConstants.golden^(-x)) / sqrt(5)
end

"""

Query if ``n`` is a Fibonacci number, returns a Bool.
"""
function is000045(n)
    d = 0
    for f ∈ FiboIterate(n + 2)
        d = n - f
        d <= 0 && break
    end
    d == 0
end

"""

Return the signed Fibonacci numbers, dubbed negaFibonacci numbers.
"""
function L039834(n)
    L = Array{fmpz,1}(undef, n)
    x, y = 1, 1
    for n in 1:n
        L[n] = x
        x, y = y, x - y
    end
    L
end

#START-TEST-########################################################

using Test

function test()
    @testset "Fibonacci" begin
        @test isa(V000045(30), fmpz)
    end
end

function demo()
    println("V000045")
    for n ∈ 0:12
        print(V000045(n), ", ")
    end
    println()

    println("I000045")
    for f ∈ I000045(20)
        print(f, ", ")
    end
    println()

    println("F000045")
    for f ∈ F000045(20)
        print(f, ", ")
    end
    println()

    println("L000045")
    print(L000045(17))
    println()

    println("R000045")
    println([R000045(Float64(x / 2 + 0.5)) for x ∈ 0:9])
    println(typeof(R000045(2 + 0.5)))

    println("is000045")
    for n ∈ 0:150
        is000045(n) && print(n, " ")
    end
    println()
end

"""

for n ∈ 1:1000 V000045(n) end
    0.004861 seconds (8.49 k allocations: 320.141 KiB)
for fib ∈ I000045(10000) end
    0.006211 seconds (40.00 k allocations: 937.547 KiB)
L000045(10000)
    0.006533 seconds (49.49 k allocations: 1.137 MiB)
"""
function perf()
    GC.gc()
    @time (for n ∈ 1:1000
        V000045(n)
    end)
    @time (for fib ∈ I000045(10000)
    end)
    @time L000045(10000)
    # println("----")
    # @time (for n ∈ 1:1000 HankinFibonacci(n) end)
end

function main()
    test()
    demo()
    perf()
end

#main()
L039834(12) |> println

end # module

#=

Test Summary: | Pass  Total
Fibonacci     |    1      1

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181,

fmpz[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]

[0.568864, 1.0, 0.920442, 1.0, 1.48931, 2.0, 2.40975, 3.0, 3.89906, 5.0]

  0.323110 seconds (13.20 k allocations: 206.234 KiB)
  0.007040 seconds (40.00 k allocations: 937.547 KiB)
  0.006942 seconds (49.49 k allocations: 1.137 MiB)

=#

### EOF ############################################################

#= COMMENTS ########################################################
# An alternative implementation mimicking Python's elegant way with yield:

using ResumableFunctions

@resumable function fibonnaci(n::Int) :: Int
  a, b = 0, 1
  for i ∈ 1:n-1
    @yield a
    a, b = b, a + b
  end
  a
end

for fib ∈ fibonnaci(10)
    println(fib)
end

=#