# Parts of this code were taken / derived from NetworkX. See LICENSE for
# licensing details.

using Base.Threads

"""
    pagerank(g, α=0.85, n=100, ϵ=1.0e-6)

Calculate the [PageRank](https://en.wikipedia.org/wiki/PageRank) of the
graph `g` parameterized by damping factor `α`, number of iterations
`n`, and convergence threshold `ϵ`. Return a vector representing the
centrality calculated for each node in `g`, or an error if convergence
is not reached within `n` iterations.
"""
function pagerank(
    g::AbstractGraph{U},
    α=0.85,
    n::Integer=100,
    ϵ=1.0e-6
    ) where U <: Integer
    α_div_outdegree = Vector{Float64}(undef,nv(g))
    dangling_nodes = Vector{U}()
    for v in vertices(g)
        if outdegree(g, v) == 0
            push!(dangling_nodes, v)
        end
        α_div_outdegree[v] = (α/outdegree(g, v))
    end
    N = Int(nv(g))
    # solution vector and temporary vector
    x = fill(1.0 / N, N)
    xlast = copy(x)
    for _ in 1:n
        dangling_sum = 0.0
        for v in dangling_nodes
            dangling_sum += x[v]
        end
        # flow from teleprotation
        for v in vertices(g)
            xlast[v] = (1 - α + α * dangling_sum) * (1.0 / N)
        end
        # flow from edges

        for v in vertices(g)
            for u in inneighbors(g, v)
                xlast[v] += (x[u] * α_div_outdegree[u])
            end
        end
        # l1 change in solution convergence criterion
        err = 0.0
        for v in vertices(g)
            err += abs(xlast[v] - x[v])
            x[v] = xlast[v]
        end
        if (err < N * ϵ)
            return x
        end
    end
    error("Pagerank did not converge after $n iterations.") # TODO 0.7: change to InexactError with appropriate msg.
end


export armijo
export goldenLS

"""
armijo(f,fk,dfk,xk,pk)

Backtracked Armijo linesearch
"""
function armijo(f::Function,fk,dfk,xk,pk;maxIter=10, c1=1e-4,b=0.5)
LS = 1
t  = 1
while LS<=maxIter
    if f(xk+t*pk) <= fk + t*c1*dot(dfk,pk)
        break
    end
    t *= b
    LS += 1
end
if LS>maxIter
	LS= -1
	t = 0.0
end
return t,LS
end

"""
goldenLS(f,xk,pk,a,b)

Golden section linesearch
"""
function goldenLS(f::Function,xk,pk,a,b;atol=1e-2,maxIter=40)
	ratio = (sqrt(5)-1)/2.0
	# make sure points we have an interval
	a,b   = (a<b)? (a,b) :  (b,a)
	# evaluate f at left point
	al    = b - ratio*(b-a)
	fl    = f(xk + al*pk)
	# evaluate f at right point
	ar    = a + ratio*(b-a)
	fr    = f(xk + ar*pk)

	LS    = 0
	while (LS<maxIter && abs(ar-al)>atol)
		if fl<fr # continue search in [a,ar]
			b  = ar
			ar = al
			fr = fl
			al = b - ratio*(b-a)
			fl = f(xk + al*pk)
		else # continue search in [al,b]
			a  = al
			al = ar
			fl = fr
			ar = a + ratio*(b-a)
			fr = f(xk + ar*pk)
		end
		# @printf "iter=%d, [al,ar]=[%1.2f,%1.2f], [fl,fr]=[%1.2e,%1.2e]\n" LS al ar fl fr
		LS += 1
	end
	return (al+ar)/2,LS
end


# One dimensional wave equation
# Test systems of conservation laws

@testset "1D Sytems Algorithms" begin

using ConservationLawsDiffEq
using OrdinaryDiffEq
CL = ConservationLawsDiffEq

CFL = 0.45
Tend = 1.0
cc = 1.0

Jf(u::AbstractVector) = [0.0 cc;cc 0.0]
f(u::AbstractVector) = [0.0 cc;cc 0.0]*u
f0(x) = [sin(4*π*x), 0.0]

Nflux(ϕl, ϕr) = 0.5*(f(ϕl)+f(ϕr))
exact_sol(x::Float64, t::Float64) = hcat(0.5*(sin.(4*π*(-t+x))+sin.(4*π*(t+x))),
0.5*(sin.(4*π*(-t+x))-sin.(4*π*(t+x))))

mesh = Uniform1DFVMesh(20, [-1.0, 1.0])

function get_problem(alg::CL.AbstractFVAlgorithm, use_threads, mesh, Tend,CFL)
  # Now get a explicit semidiscretization (discrete in space) du_h(t)/dt = f_h(u_h(t))
  f_h = getSemiDiscretization(f,alg,mesh,[Periodic()]; Df = Jf, use_threads = use_threads,numvars = 2)

  #Compute discrete initial data
  u0 = getInitialState(mesh,f0,use_threads = true)

  #Setup ODE problem for a time interval = [0.0,1.0]
  ode_prob = ODEProblem(f_h,u0,(0.0,Tend))

  #Setup callback in order to fix CFL constant value
  cb = getCFLCallback(f_h, CFL)

  #Estimate an initial dt
  dt = update_dt!(u0, f_h, CFL)
  return ode_prob, cb, dt
end

function test_scheme(alg, maxerror, threaded_mode=true)
  println("Testing ",split("$alg","(")[1],": ")
  prob,cb,dt = get_problem(alg, false, mesh, Tend,CFL)
  @time sol = solve(prob,SSPRK22(); dt = dt, callback = cb, save_everystep = false)
  @test get_L1_error(exact_sol, fv_solution(sol,mesh)) < maxerror
  if threaded_mode
    println("in threaded mode: ")
    prob,cb,dt = get_problem(alg, true, mesh, Tend,CFL)
    @time sol1 = solve(prob,SSPRK22(); dt = dt, callback = cb, save_everystep = false)
    @test get_L1_error(exact_sol, fv_solution(sol1,mesh)) ≈ get_L1_error(exact_sol, fv_solution(sol,mesh))
  end
end

# Test Schemes error
test_scheme(FVSKTScheme(), 1.16)
test_scheme(FVTecnoScheme(Nflux;order=2), 1.152, false)
test_scheme(FVTecnoScheme(Nflux;order=3), 1.14, false)
test_scheme(FVTecnoScheme(Nflux;order=5), 0.92, false)
test_scheme(LaxFriedrichsScheme(), 1.16)
test_scheme(LocalLaxFriedrichsScheme(), 1.16, false)
test_scheme(GlobalLaxFriedrichsScheme(), 1.16)
test_scheme(LaxWendroffScheme(), 37.0)    #way off
test_scheme(LaxWendroff2sScheme(), 1.152)
test_scheme(FVCompWENOScheme(), 0.94)
test_scheme(FVCompMWENOScheme(), 0.68)
test_scheme(FVSpecMWENOScheme(), 0.68, false)
test_scheme(FVCUScheme(), 1.16)
test_scheme(FVDRCUScheme(), 1.17)
test_scheme(FVDRCU5Scheme(), 0.48)

end


# # Coriolis Parameter
#
#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/notebooks/example_f.ipynb)
#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/notebooks/example_f.ipynb)
#
# [from wikipedia:](https://en.wikipedia.org/wiki/Coriolis_frequency)
#
# The Coriolis frequency `ƒ`, also called the Coriolis parameter or Coriolis
# coefficient, is equal to twice the rotation rate `Ω` of the Earth multiplied
# by the `sine` of the latitude `φ`.
#
# $$f=2\Omega \sin \varphi$$

using PhysicalOceanography
isapprox(f(45),1.03125867182e-4)


function constantdiffusionproblem(stepper; nx=128, Lx=2π, kappa=1e-2, nsteps=1000, dev=CPU())
   τ = 1/kappa  # time-scale for diffusive decay
  dt = 1e-9 * τ # dynamics are resolved

  prob = Diffusion.Problem(nx=nx, Lx=Lx, kappa=kappa, dt=dt, stepper=stepper, dev=dev)
  g = prob.grid

  # a gaussian initial condition c(x, t=0)
  c0ampl, σ = 0.01, 0.2
  c0func(x) = @. c0ampl*exp(-x^2/(2σ^2))
  c0 = c0func.(g.x)

  # analytic solution for for 1D heat equation with constant κ
  tfinal = nsteps*dt
  σt = sqrt(2*kappa*tfinal + σ^2)
  cfinal = @. c0ampl*σ/σt * exp(-g.x^2/(2*σt^2))

  Diffusion.set_c!(prob, c0)
  tcomp = @elapsed stepforward!(prob, nsteps)
  Diffusion.updatevars!(prob)

  prob, c0, cfinal, nsteps, tcomp
end

function constantdiffusionproblem2D(stepper; nx=128, ny=64, Lx=2π, Ly=4π, kappa=1e-2, nsteps=1000, dev=CPU())
    τ = 1/kappa  # time-scale for diffusive decay
    dt = 1e-9 * τ # dynamics are resolved

prob = Diffusion2D.Problem(nx=nx, ny=ny, Lx=Lx, Ly=Ly, kappa=kappa, dt=dt, stepper=stepper, dev=dev)
g = prob.grid

#Gaussian Initial Condition
c0, σ0 = 0.1, 0.2
c1, σ1 = 0.1, 0.2

c0_x_func(x) = @. c0*exp(-x^2/(2*(σ0)^2))
c0_y_func(y) = @. c1*exp(-y^2/(2*(σ1)^2))

c0_val = c0_x_func.(g.x) .* c0_y_func.(g.y)

#Analytic Solution
tfinal = nsteps*dt

σ0t = sqrt(2*kappa*tfinal + (σ0)^2)
σ1t = sqrt(2*kappa*tfinal + (σ1)^2)

cfinal_x = @. c0 * (σ0/σ0t) * exp(-g.x^2/(2*(σ0t)^2))
cfinal_y = @. c1 * (σ1/σ1t) * exp(-g.y^2/(2*(σ1t)^2))

cfinal_val = cfinal_x .* cfinal_y

#Numerical Solution
Diffusion2D.set_c!(prob, c0_val)
tcomp = @elapsed stepforward!(prob, nsteps)
Diffusion2D.updatevars!(prob)

prob, c0_val, cfinal_val, nsteps, tcomp
end

function varyingdiffusionproblem(stepper; nx=128, Lx=2π, kappa=1e-2, nsteps=1000, dev=CPU())
   τ = 1/kappa  # time-scale for diffusive decay
  dt = 1e-9 * τ # dynamics are resolved

  kappa = kappa*ones(nx) # this is actually a constant diffusion but defining it
                         # as an array makes stepforward! call function calcN!
                         # instead of just the linear coefficients L*sol

  prob = Diffusion.Problem(nx=nx, Lx=Lx, kappa=kappa, dt=dt, stepper=stepper, dev=dev)
  g = prob.grid

  # a gaussian initial condition c(x, t=0)
  c0ampl, σ = 0.01, 0.2
  c0func(x) = @. c0ampl*exp(-x^2/(2σ^2))
  c0 = c0func.(g.x)

  # analytic solution for for 1D heat equation with constant κ
  tfinal = nsteps*dt
  σt = sqrt(2*kappa[1]*tfinal + σ^2)
  cfinal = @. c0ampl*σ/σt * exp(-g.x^2/(2*σt^2))

  Diffusion.set_c!(prob, c0)
  tcomp = @elapsed stepforward!(prob, nsteps)
  Diffusion.updatevars!(prob)

  prob, c0, cfinal, nsteps, tcomp
end

function constantdiffusiontest(stepper, dev::Device=CPU(); kwargs...)
  prob, c0, c1, nsteps, tcomp = constantdiffusionproblem(stepper; kwargs...)
  normmsg = "$stepper: relative error ="
  @printf("% 40s %.2e (%.3f s)\n", normmsg, norm(c1-prob.vars.c)/norm(c1), tcomp)
  isapprox(c1, prob.vars.c, rtol=nsteps*rtol_timesteppers)
end

function constantdiffusiontest2D(stepper, dev::Device=CPU(); kwargs...)
    prob, ci, cf, nsteps, tcomp = constantdiffusionproblem2D(stepper; kwargs...)
    normmsg = "$stepper: relative error ="
    @printf("% 40s %.2e (%.3f s)\n", normmsg, norm(cf-prob.vars.c)/norm(cf), tcomp)
    isapprox(cf, prob.vars.c, rtol=nsteps*rtol_timesteppers)
end

function varyingdiffusiontest(stepper, dev::Device=CPU(); kwargs...)
  prob, c0, c1, nsteps, tcomp = varyingdiffusionproblem(stepper; kwargs...)
  normmsg = "$stepper: relative error ="
  @printf("% 40s %.2e (%.3f s)\n", normmsg, norm(c1-prob.vars.c)/norm(c1), tcomp)
  isapprox(c1, prob.vars.c, rtol=nsteps*rtol_timesteppers)
end


# ------------------------------------------------------------------------------------------
# # Minimizing functions - how a computer learns
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# In the previous notebooks, we've seen that by changing **parameters** in a function, we
# could find a "best fit" of that function to some data. We use a **loss function** to
# quantify the "goodness" of a set of parameters and we look for those parameters that
# **optimize**, in fact **minimize**, the loss function. If we teach a machine how to
# minimize the loss function on its own, we say that machine is able to **learn** how to
# model our data.
#
# In the last notebook, we threw around terms like **derivative**, **gradient**, and
# **gradient descent** to give you a rough sense of how we minimize a function on a
# computer. In this notebook, we will step through these concepts more carefully, with the
# aim of being able to implement them using Julia.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## Minimizing a 1D function using calculus
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Let's draw the 1D loss function $L_1$ from a previous notebook again:
# ------------------------------------------------------------------------------------------

σ(x) = 1 / (1 + exp(-x))
f(x, w) = σ(w * x)

x1 = 2
y1 = 0.8

L1(w) = (y1 - f(x1, w))^2

using Plots; gr()
using Interact

plot(L1, -2, 1.5, xlabel="w", ylabel="L1(w)", leg=false)

# ------------------------------------------------------------------------------------------
# By eye, we can see that the minimum is around $w=0.6$. But how can we get the computer to
# work this out on its own?
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# In the previous notebook, we thought of this function plot as a hill, viewed from the
# side. We could find the minimum by making the hill sticky, and letting a ball roll down
# it. The ball will find and settle in the minimum of the function.  Now let's see how to
# teach a computer to do this.
#
# We need to find the downhill direction along the hill, which is related to its *slope*
# (how steep it is). Calculus provides us with tools to calculate that slope!
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Namely, the slope of a curve $L_1(w)$ at $w$ is given by its **derivative** $L_1'(w)$;
# geometrically, this is the slope of the **tangent line** to the curve at that point, i.e.
# the straight line which touches the curve at that point.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Calculus provides us with some rules to calculate an analytical formula for the
# derivative, and we will see later how to apply these rules, albeit indirectly, for machine
# learning.
# To gain understanding, however, we will see here how to get the computer to help us out by
# calculating the derivatives numerically instead!
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## Approximating derivatives
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Let's recall that the derivative $L_1'(w)$ of a function is defined as
#
# $$L_1'(w) \simeq \frac{L_1(w+h) - L_1(w)}{h},$$
#
# for a small step size $h$. (Strictly speaking, we must take the limit when $h$ tends to
# $0$ to obtain the exact value of the derivative.)
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 1
#
# Write a function to calculate the derivative of a function at a given point. Note that in
# Julia, we can easily pass functions as arguments to other functions!
# The function should take the function, the point $w$ at which to calculate the derivative,
# and the value of $h$, which should have a default value of 0.001.
#
# *Note*: A function input argument can have a *default value* if we set the input argument
# equal to that default value when we define the function. For example,
#
# ```julia
# f(x, a = 3) = a * x^2
# ```
#
# The function `f` will square the input we give it and multiply by `a`. However,  if we
# choose to call `f(x)` *without* passing it an `a` input, it will assume `a` is `3` and
# return `3*x^2`.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 2
#
# Write an interactive visualization of the tangent line to the graph of $L_1$, so that we
# can visualize the tangent at any point on $L_1$. Include the current value of the
# derivative in the title.
#
# *Hint*: Recall that the straight line through the point $(x_0, y_0)$ with slope $m$ is
# given by
#
# $$ \frac{y - y_0}{x - x_0} = m,$$
#
# so
#
# $$y = y_0 + m*(x - x_0).$$
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 3
#
# What is the value of the derivative (slope of the tangent line) at a minimum? Can this
# happen anywhere else?
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## Minimization by gradient descent
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# The tangent line at a point is a good approximation of the function near that point.
# Therefore the derivative tells us in which direction, and how fast, a function grows or
# shrinks when we move a small distance away from that point.
#
# As we saw in the previous notebook, we can think of the function as being a hill, and
# having a ball that we want to move down the hill. Gravity will automatically pull the ball
# in the direction *down* the hill; we want to emulate this in the computer!
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 4
#
# When the derivative $L_1'(w)$ is positive, that means that $L_1$ increases from left to
# right at point $w$.
#
# If the derivative $L_1'(w)$ is positive (> 0), in which direction should we move $w$ to
# *decrease* $L_1$?
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 5
#
# If the derivative $L_1'(w)$ is negative (< 0), in which direction should we move $w$ to
# *decrease* $L_1$?
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# We can use this information to tell the computer which way to take a step. This
# constitutes the numerical algorithm called **gradient descent**; it is called this since
# we are descending (moving downwards) along the graph of the function by using information
# about its gradient (slope).
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 6
#
# Implement gradient descent by following this prescription for an **iterative (repetitive)
# algorithm**:
#
# 1. Start at an initial guess $w_0$ for $w$.
#
# 2. At each step, calculate the derivative, $L_1'(w_n)$ at the current value of $w_n$ using
# the function that you created above.
#
# 3. Modify the value of $w$ by a small multiple (for example, $\eta=0.01$) of the value of
# the derivative you just created, via $w_{n+1} = w_n - \eta L_1'(w_n)$.
#
# For this problem, start with $w_0 = -2.0$. Repeat steps 2 and 3 a total of `2000` times.
#
# Package this code into a function called `gradient_descent` that takes two inputs, a
# function and a range for values of $w$, and returns the final value of $w$ and $L_1(w)$.
#
# Using `L1` and `-2:0.01:1.5` as your inputs to `gradient_descent`, for what value of $w$
# is $L_1$ at a minimum?
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 7
#
# Modify your code for gradient descent to return the result once it has found an answer
# within some *tolerance*, rather than taking a set number of steps. The new prescription
# for this algorithm is:
#
# 1. Start at an initial guess $w_0$ for $w$.
#
# 2. At each step, calculate the derivative, $L_1'(w_n)$ at the current value of $w_n$,
# using the function that you created above.
#
# 3. Modify the value of $w$ by a small multiple (for example, $\eta=0.01$) of the value of
# the derivative you just created, via $w_{n+1} = w_n - \eta L_1'(w_n)$.
#
# 4. Check how different $w_{n+1}$ is from $w_n$. If you're satisfied that $L_1(w_{n+1})$ is
# minimized, return $w_{n+1}$ and $L_1(w_{n+1})$. Otherwise, go to step (2) and continue.
#
# Edit `gradient_descent` so that it takes three inputs: a function, a range for values of
# $w$, and a tolerance that tells you how close $w_{n+1}$ must be to $w_n$ before you can
# stop iterating.
#
# Using `L1`, `-2:0.01:1.5`, and `.000001` as your inputs to `gradient_descent`, for what
# value of $w$ is $L_1$ at a minimum?
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 8
#
# Alter the function `gradient_descent` so that it stores the results `(w, L1(w))` at each
# step of the algorithm as an array and returns this array. How many steps does the
# algorithm take for input parameters `(L1, -2:0.01:1.5, .000001)` before terminating? You
# should count your starting $w_0$ as your first step.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 9
#
# Overlay the steps taken in the last exercise with a plot of $L_1(w)$ vs. $w$.
#
# Where does our algorithm take the largest steps? (Where does the ball move fastest down
# the hill?)
#
# A) Between w = -2:-1

# B) Between w = -1:0

# C) Between w = 0:.6
#
# *Hint*: It may be easier to see what's going on if you only plot, for example, every 15th
# step.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# B) The following code generates the desired figure:
#
# ```julia
# wsteps = gradient_descent(C1, -2:0.01:1.5, .000001)
# fig1 = plot(C1, -2, 1.5, xlabel="w", ylabel="C1(w)", leg=false)
# for step in wsteps[1:15:end]
#    scatter!([step[1]], [step[2]])
# end
# display(fig1)
# ```
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## Functions of 2 variables and their derivatives
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# So far, we have been looking at the minimizing a loss function $L_1(w)$ that depends on a
# single parameter, $w$. Now let's turn to the cost function $L_2(w, b)$ from a previous
# notebook, that is a function of *two* parameters, $w$ and $b$, and try to minimize it.
#
# As we've seen, we get a **surface**, instead of a curve, when we graph $L_2$ as a function
# of both of its parameters.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# **Exercise 10**
#
# Draw a surface plot of $L_2$, given by
#
# $$L_{2}(w, b) = \sum_i(y_i - g(x_i, w, b))^2$$
#
# using the `surface` function from `Plots.jl`. For this plot, use the values of `xs` and
# `ys` from notebook 5:
#
# ```julia
# xs = [2, -3, -1, 1]
# ys = [0.8, 0.3, 0.4, 0.4]
# ```
#
# We can get a nice interactive 3D plot by using the Plotly backend of `Plots.jl` by
# executing the command
#
#     plotly()
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ### Finding the minimum
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# We can just about see, by rotating the graph, that $L_2$ has a single minimum. We want to
# find the values of $w$ and $b$ where this minimum value is reached.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Following what we did for the function $L_1$ above, we expect that we will need to
# calculate derivatives of $L_2$. Since the function is more complicated, though, the
# derivatives are too!
#
# It turns out that the right concept is that of the
# [**gradient**](https://en.wikipedia.org/wiki/Gradient) of $L_2$, denoted $\nabla L_2(w,
# b)$. This is a **vector** consisting of $2$ numbers if there are $2$ parameters [or, in
# general, $n$ numbers if there are $n$ parameters].
#
# The numbers that form the gradient $\nabla L_2(w, b)$ are called the **partial
# derivatives** of $L_2$ with respect to $w$ and $b$,  written as
#
# $$\frac{\partial L_2}{\partial w} \quad \text{and} \quad \frac{\partial L_2}{\partial
# b}.$$
#
# Although this notation might look complicated, all it means is that we calculate
# derivatives just like we did before, except that we fix the value of the other variable.
# For example, to calculate $\frac{\partial L_2}{\partial w}$, the partial derivative of
# $L_2$ with respect to $w$, we fix the value of $b$ and think of the resulting function as
# a function of the single variable $w$; then we use the formula for derivatives of
# functions of a single variable.
#
# [Note that $\frac{\partial L_2}{\partial w}$ is itself a function of $w$ and $b$; we could
# write $\frac{\partial L_2}{\partial w}(w, b)$.]
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 11
#
# Write functions that will allow you to calculate the partial derivatives of a function $f$
# of two variables.
#
# In particular, declare functions called `partial_w` and `partial_b`. Each should take four
# inputs - a function $f$ of two variables, the first input argument to $f$, the second
# input argument to $f$, and a step size `h` with default value `0.001`. `partial_w` should
# return the partial derivative of $f$ with respect to its first input argument and
# `partial_b` should return the partial derivative of $f$ with respect to its second input
# argument.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 12
#
# Use `partial_b` from the last exercise to find the partial derivative of $L_2$ with
# respect to $w$ at b = 0.3, $\frac{\partial L_2}{\partial w}|_{b = 0.3}$ for `w =
# -2:0.01:1`
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 13
#
# Plot the cross section of the surface of $L_2(w, b)$ at $b = 0.3$. Make this plot
# interactive with `@manipulate` to show that the function `partial_w` gives the slope of
# the tangent to this cross section for any point `w` in the range `-2:0.01:1`.
#
# For what value of $w$ in this range is the slope of the cross section closest to -1?
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## ***Optional**: Functions with $n$ inputs
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# If a function $f$ takes $n$ input arguments, we can write them as $p_1, \ldots, p_n$,
# where $p_i$ means the "$i$th parameter". In Julia, we can wrap them up into a single
# **vector**. Now we can calculate the partial derivative $\frac{\partial L_2}{\partial
# p_i}$ with respect to the $i$th variable.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 14
#
# For the next exercise, you will need to use the splat command, `...`. You can use this
# command to "open up" a collection and pass all the elements of that collection as inputs
# to a function.
#
# For example, say you have an array, `numbers`,
#
# ```julia
# numbers = [4, 3, 2]
# ```
#
# and you want to use `numbers` to create a $4\times3\times3$ randomly populated array via
# `rand`. `rand(numbers)` will not do what you want. You could index into `numbers` to grab
# the values you want and pass them to `rand`, as in
#
# ```julia
# rand(numbers[1], numbers[2], numbers[3])
# ```
#
# or you could use a splat:
#
# ```julia
# rand(numbers...)
# ```
#
# Use `...` to pass the contents of `inputs`
#
# ```julia
# inputs = [30, 12, "cats"]
# ```
#
# to the function `dreams`
#
# ```julia
# dreams(i, j, perfect_mammal) = "I wish I had $(i + j) $perfect_mammal."
# ```
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 15:
#
# Write a function, `partial`, to calculate the $i$th partial derivative of a function. This
# function should have four inputs
#
# * a function, $f$, for which you want to compute the partial derivative
# * an array, *p*, specifying the values of all input arguments to $f$ at the point where
# you want $\frac{\partial f}{\partial p_i}$ computed
# * the index, $i$, of the variable with respect to which you want to calculate the partial
# derivative of $f$
# * a step size with default value 0.001
#
# Hint: you will need to `copy` and modify `p` within `partial`.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## Gradient descent in 2 dimensions
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# It turns out that the gradient vector of the function $L_2(w, b)$ gives the direction in
# the plane $(w, b)$ in which the function $L_2$ **increases fastest**.
#
# In other words, if you start at the position $(w_0, b_0)$ and take a small step of length
# $\eta$ to new values $(w', b')$, the value of the function will change to a new value
# $L_2(w', b')$. How do we get to the minimum of $L_2(w, b)$ as fast as possible? We want to
# step in the direction where $L_2$ decreases fastest!
#
# In multivariable calculus courses, it is shown that $L_2$ will *increase* fastest if you
# take a step **in the direction of the gradient $\nabla L_2(w, b)$**! To decrease $L_2$ the
# fastest, we should take a step in the *opposite* direction, $-\nabla L_2(w, b)$.
#
# Let's now generalize the gradient descent algorithm that we wrote previously to work our
# 2-dimensional function.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 16
#
# Extend your 1D implementation of `gradient_descent` so that it will minimize the function
# $L_2$.
#
# Requirements:
#
# * Your new method for `gradient_descent` will take four input arguments: the function $f$
# for which you seek the minimum, the range of values for $f$'s first input argument that
# you will consider, the range of values for $f$'s second input argument that you will
# consider, and a tolerance that will specify the maximum allowable step size, $\sum_i \eta
# \frac{\partial f}{\partial p_i}$
#
# * Use $\eta = .01$. For example, for a function $f(w, b)$, update $w$ such that $w_{n+1} =
# w_n - 0.01 * \frac{\partial f}{\partial w_n}$
#
# * Seed `gradient_descent` with the starting coordinates [-2.0, -2.0], i.e. $w_0 = -2.0$
# and $b_0 = -2.0$.
#
# * Return all steps (their coordinates) taken during gradient descent and the values of the
# loss function at these coordinates.
#
# Once you have done this, execute
#
# ```julia
# gradient_descent(L2, -2:0.02:2, -2:0.02:2, .001)
# ```
#
# How many steps were taken by gradient descent?
#
# Hint: Do not count your starting coordinates `[-2.0, -2.0]` as a step.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Exercise 17
#
# Use the `surface` and `scatter!` commands to illustrate the path taken by
# `gradient_descent` from [-2.0, -2.0] to wherever the algorithm terminates.
#
# Where do the scattered points representing the steps of gradient descent appear the most
# dense?
#
# A) Near the starting point at [-2.0, -2.0]

# B) Near the point [-1.8, 0] where $C_2$ appears nearly flat

# C) Near the minimum of $C_2$
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## What we have learnt
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# To recap, in this notebook, we have seen that the computer can calculate approximate
# derivatives, and that we can use those inside a relatively simple algorithm, gradient
# descent, to minimize functions of 1 and 2 variables!
#
# Now that we've used gradient descent to **learn** the best values of `b` and `w` for a
# cost function, we can imagine how we could do our classification of apples vs. bananas by
# allowing the machine to choose our parameters! To do this, we need to define a loss
# function for apple vs. banana classification, using images that have already been
# categorised as apples or bananas.
# ------------------------------------------------------------------------------------------

.nbexports/introductory-tutorials/broader-topics-and-ecosystem/intro-to-ml/10. Tools - Minimizing functions - how a computer learns.jl


# L-infinity norm

export NormLinf

"""
**``L_∞`` norm**

    NormLinf(λ=1.0)

Returns the function
```math
f(x) = λ⋅\\max\\{|x_1|, …, |x_n|\\},
```
for a nonnegative parameter `λ`.
"""
NormLinf(lambda::R=1.0) where {R <: Real} = Conjugate(IndBallL1(lambda))

function (f::Conjugate{IndBallL1{R}})(x::AbstractArray{S}) where {R <: Real, S <: RealOrComplex}
  return (f.f.r)*norm(x, Inf)
end

function gradient!(y::AbstractArray{T}, f::Conjugate{IndBallL1{R}}, x::AbstractArray{T}) where {T <: RealOrComplex, R <: Real}
  absxi, i = findmax(abs.(x)) # Largest absolute value
  y .= 0
  y[i] = f.f.r*sign(x[i])
  return f.f.r*absxi
end

fun_name(f::Postcompose{Conjugate{IndBallL1{R}}, R}) where {R <: Real} = "weighted L-infinity norm"
fun_expr(f::Postcompose{Conjugate{IndBallL1{R}}, R}) where {R <: Real} = "x ↦ λ||x||_∞ = λ⋅max(abs(x))"
fun_params(f::Postcompose{Conjugate{IndBallL1{R}}, R}) where {R <: Real} = "λ = $(f.a*(f.f.f.r))"


using Test, LinearAlgebra
using BellScenario: stirling2, stirling2_partitions, stirling2_matrices, permutation_matrices, base_n_val, n_choose_k_matrices

@testset "./src/combinatorics.jl" begin

@testset "stirling2()" begin
    @testset "domain errors" begin
        @test_throws DomainError stirling2(1,0)
        @test_throws DomainError stirling2(1,2)
    end

    @test stirling2(1,1) == 1

    @testset "n = 2" begin
        @test stirling2(2,1) == 1
        @test stirling2(2,2) == 1
    end

    @testset "n = 3" begin
        @test stirling2(3,1) == 1
        @test stirling2(3,2) == 3
        @test stirling2(3,3) == 1
    end

    @testset "n = 4" begin
        @test stirling2(4,1) == 1
        @test stirling2(4,2) == 7
        @test stirling2(4,3) == 6
        @test stirling2(4,4) == 1
    end

    @testset "n = 5" begin
        @test stirling2(5,1) == 1
        @test stirling2(5,2) == 15
        @test stirling2(5,3) == 25
        @test stirling2(5,4) == 10
        @test stirling2(5,5) == 1
    end

    @testset "n = 6" begin
        @test stirling2(6,1) == 1
        @test stirling2(6,2) == 31
        @test stirling2(6,3) == 90
        @test stirling2(6,4) == 65
        @test stirling2(6,5) == 15
        @test stirling2(6,6) == 1
    end

    @testset "n = 10" begin
        @test stirling2(10,1) == 1
        @test stirling2(10,2) == 511
        @test stirling2(10,3) == 9330
        @test stirling2(10,4) == 34105
        @test stirling2(10,5) == 42525
        @test stirling2(10,6) == 22827
        @test stirling2(10,7) == 5880
        @test stirling2(10,8) == 750
        @test stirling2(10,9) == 45
        @test stirling2(10,10) == 1
    end
end

@testset "stirling2_partitions()" begin
    @testset "base cases" begin
        @test stirling2_partitions(0,0) == []
        @test stirling2_partitions(1,0) == []
        @test stirling2_partitions(1,2) == []
        @test stirling2_partitions(5,1) == [[[1,2,3,4,5]]]
    end

    @testset "simple cases" begin
        @test stirling2_partitions(3,2) == [[[1,2],[3]],[[2],[1,3]],[[1],[2,3]]]

        @test stirling2_partitions(4,2) == [
            [[1, 2, 3], [4]],
            [[3], [1, 2, 4]],
            [[1, 2], [3, 4]],
            [[1, 3], [2, 4]],
            [[2], [1, 3, 4]],
            [[2, 3], [1, 4]],
            [[1], [2, 3, 4]],
        ]

        @test stirling2_partitions(4,4) == [[[1],[2],[3],[4]]]
    end

    @testset "general partitions" begin
        for n in 2:7
            for k in 1:n
                partitions = stirling2_partitions(n,k)
                @test length(partitions) == stirling2(n,k)
                @test length(unique(partitions)) == stirling2(n,k)
                @test all(s -> length(s) == k, partitions)
                @test all(s -> sort(cat(s...,dims=1)) == [1:n...], partitions)
            end
        end
    end
end

@testset "stirling2_matrices()" begin
    @testset "domain errors" begin
        @test_throws DomainError n_choose_k_matrices(2,3)
        @test_throws DomainError n_choose_k_matrices(2,0)
    end

    @testset "simple cases" begin
        @test stirling2_matrices(4,1) == [
            [1 1 1 1]
        ]
        @test stirling2_matrices(3,2) == [
            [1 1 0;0 0 1],[0 1 0;1 0 1],[1 0 0;0 1 1]
        ]
    end

    @testset "general cases" begin
        for n in 2:7
            for k in 1:n
                matrices = stirling2_matrices(n,k)

                @test length(matrices) == stirling2(n,k)
                @test length(unique(matrices)) == stirling2(n,k)
            end
        end
    end
end

@testset "permutation_matrices()" begin
    @testset "dim = 2" begin
        @test permutation_matrices(2) == [[1 0; 0 1], [0 1; 1 0]]
    end

    @testset "dim = 3" begin
        @test issetequal(permutation_matrices(3), [
            [1 0 0; 0 1 0; 0 0 1],
            [1 0 0; 0 0 1; 0 1 0],
            [0 1 0; 1 0 0; 0 0 1],
            [0 1 0; 0 0 1; 1 0 0],
            [0 0 1; 1 0 0; 0 1 0],
            [0 0 1; 0 1 0; 1 0 0],
        ])
    end

    @testset "permtutation map validity" for N in 4:5
        perms = permutation_matrices(N)

        @testset "maps are unique" begin
            @test size(perms) == (factorial(N),)
            @test size(perms) == size(unique(perms))
        end

        @testset "matrix dimension, determinant, and invertibility" for perm in perms
            # dimension
            @test size(perm) == (N,N)

            # parity
            parity = det(perm)
            @test (parity == -1) || (parity == 1)

            # invertibility
            @test perm*perm' == Matrix{Int64}(I,N,N)
        end
    end
end

@testset "n_choose_k_matrices()" begin
    @testset "Domain Errors" begin
        @test_throws DomainError n_choose_k_matrices(2,3)
        @test_throws DomainError n_choose_k_matrices(2,0)
    end

    @testset "simple testcases" begin
        @test n_choose_k_matrices(3,3) == [[1 0 0;0 1 0;0 0 1]]

        @test n_choose_k_matrices(3,2) == [
            [1 0;0 1;0 0],[1 0;0 0;0 1],[0 0;1 0;0 1]
        ]

        @test n_choose_k_matrices(4,2) == [
            [1 0;0 1;0 0;0 0],[1 0;0 0;0 1;0 0],[1 0;0 0;0 0;0 1],
            [0 0;1 0;0 1;0 0],[0 0;1 0;0 0;0 1],[0 0;0 0;1 0;0 1]
        ]
    end

    @testset "general tests" begin
        for n in 2:5
            for k in 1:n
                matrices = n_choose_k_matrices(n,k)

                @test length(matrices) == binomial(n,k)
                @test length(unique(matrices)) == binomial(n,k)
            end
        end
    end
end

@testset "base_n_val()" begin
    @testset "little endian" begin
        @test 1 == base_n_val(digits(1,base=2,pad=2), 2, big_endian = false)
        @test 57 == base_n_val(digits(57,base=3,pad=5), 3, big_endian = false)
    end

    @testset "big endian" begin
        @test 12 == base_n_val([1,1,0,0],2)
        @test 974 == base_n_val([1,2,3,4,4],5)
    end

    @testset "argument errors" begin
        @test_throws ArgumentError base_n_val([1,2,0,5],5)
        @test_throws ArgumentError base_n_val([1,2,0,5],4)
        @test_throws ArgumentError base_n_val([-1,0,1],2)
    end
end

end


# Tested in Julia 1.5.1
using DelimitedFiles
data = readdlm("./data/adventInput05.txt")
function findRowColumn(string; first = 0, last = 127, upStr = 'B', lowStr = 'F')
    planeRows = first:last
    cStr = string[1]
    for s in string
        if s == upStr
            first = planeRows.start + Int(length(planeRows)/2)
            last = planeRows.stop
            planeRows = first:last
        elseif s == lowStr
            first = planeRows.start
            last = first + Int(length(planeRows)/2-1)
            planeRows = first:last
        end
        cStr = s
    end
    cStr == lowStr ? planeRows.start : planeRows.stop
end
function maxIdBoardingPass(data)
    idBoardingPass = 0
    for s in data
        posRow = findRowColumn(s)
        posCol = findRowColumn(s; first=0, last = 7, upStr = 'R', lowStr = 'L')
        idBoadPass = posRow *8 + posCol
        idBoardingPass =  idBoadPass > idBoardingPass ? idBoadPass : idBoardingPass
    end
    idBoardingPass
end
function IdsBoardingPass(data)
    idsBPass = []
    for s in data
        posRow = findRowColumn(s)
        posCol = findRowColumn(s; first=0, last = 7, upStr = 'R', lowStr = 'L')
        idBoadPass = posRow *8 + posCol
        push!(idsBPass, idBoadPass)
    end
    idsBPass
end
function findSeat(IdsBP)
    for s_id in 1:maximum(IdsBP)
        ismyseat = s_id ∉ IdsBP
        isplus1  = s_id + 1 ∈ IdsBP
        isminus1 = s_id - 1 ∈ IdsBP
        if ismyseat && isplus1 && isminus1
            return s_id
        end
    end
end
maximum(IdsBoardingPass(data))
findSeat(IdsBoardingPass(data))


using SpecialFunctions # for gamma function reasons
using LinearAlgebra    # for Gauss quadrature
using OffsetArrays     # for notational clarity
using SparseArrays     # for data structures

using GLMakie          # plotting
using LaTeXStrings     # plotting

"""
unimesh1D(xmin, xmax, K)
# Description
    Generates a uniform 1D mesh
# Arguments
    xmin: smallest value of array
    xmax: largest values of array
    K: number of elements in an array
# Return Values: VX, EtoV
    VX: vertex values | an Array of size K+1
    EtoV: element to node connectivity | a Matrix of size Kx2
# Example
xmin = -1
xmax =  1
K    =  4
VX, EtoV = unimesh1D(xmin, xmax, K)
"""
function unimesh1D(xmin, xmax, K)
    VX = collect(0:K) ./ K .* (xmax - xmin) .+ xmin
    EtoV = Int.(ones(K, 2))
    for i = 1:K
        EtoV[i,1] = Int(i)
        EtoV[i,2] = Int(i+1)
    end
    return VX, EtoV
end

# Mathy aliases
const Γ = gamma

# Coefficients in the Jacobi polynomial recurrence relations.
aᴾ(α, β, n) = 2/(2n+α+β) * √(n * (n+α+β) * (n+α) * (n+β) / (2n+α+β-1) / (2n+α+β+1))
bᴾ(α, β, n) = -(α^2 - β^2) / (2n+α+β) / (2n+α+β+2)

# code checked against the matlab code
"""
jacobi(x, α, β, n)
# Description
- Evaluates the jacobi polynomial at the point x
# Arguments
- `x`: point at which you will evaluate the jacobi polynomial
- `α`: first parameter for Jacobi polynomials
- `β`: second parameter for Jacobi polynomials
- `n` : order
# Return
-  `y`: the value of the of the Jacobi polynomial
"""
function jacobi(x, α, β, n::Int)
    Pᵅᵝ = n <= 1 ? OffsetArray(zeros(2), 0:1) : OffsetArray(zeros(n+1), 0:n)
    Pᵅᵝ[0] = √(2.0^-(α+β+1) * Γ(α+β+2) / Γ(α+1) / Γ(β+1))
    Pᵅᵝ[1] = Pᵅᵝ[0]/2 * √((α+β+3) / (α+1) / (β+1)) * ((α+β+2)*x + α - β)
    for n′ in 1:n-1
        Pᵅᵝ[n′+1] = ((x - bᴾ(α,β,n′)) * Pᵅᵝ[n′] - aᴾ(α,β,n′) * Pᵅᵝ[n′-1]) / aᴾ(α, β, n′+1)
    end
    return Pᵅᵝ[n]
end

"""
djacobi(x, α, β, n)
# Description
- Evaluates the derivative of the jacobi polynomial at the point x
# Arguments
- `x`: point at which you will evaluate the derivative of the jacobi polynomial
- `α`: first parameter for Jacobi polynomials
- `β`: second parameter for Jacobi polynomials
- `n` : order
# Return
-  `y`: the derivative of the of the Jacobi polynomial
"""
djacobi(x, α, β, n::Int) = √(n * (n+α+β+1)) * jacobi(x, α+1, β+1, n-1)

"""
vandermonde(x, α, β, N)
# Description
    Return vandermonde matrix of order N at the values x
    Allocates a little bit of memory
# Arguments
-   `x`: points at which to evaluate the Jacobi polynomials
-   `α`: first parameter for Jacobi polynomials
-   `β`: second parameter for Jacobi polynomials
-   `N`: maximum order of Jacobi polynomial to include
# Return Values
-   `v`: vandermonde matrix
# Example
    See LegendreTests.jl
"""
function vandermonde(x, α, β, N)
    # compute first two coefficients
    γ0 = 2^(α + β + 1) * factorial(α) * factorial(β) / ((α + β + 1) * factorial(α + β))
    γ1 = (α + 1) * (β + 1) / (α + β + 3) * γ0

    # create view to assign values
    v = zeros(length(x), N+1)
    v1 = view(v, :, 1)
    @. v1 = 1 / sqrt(γ0)

    # explicitly compute second coefficient
    if N == 0
        return v
    end

    v2 = view(v, :, 2)
    @. v2 = ( (α + β + 2) * x/2 + (α - β)/2) / sqrt(γ1)

    if N == 1
        return v
    end

    aʲ = 2 / (2 + α + β) * sqrt((α+1) * (β+1) / (α + β + 3))

    for i in 3:(N+1)
        # get views for ith, i-1th, and i-2th columns
        vi = view(v, :, i)
        vM1 = view(v, :, i-1)
        vM2 = view(v, :, i-2)

        # compute new a and b values
        h1 = 2 * (i-2) + α + β
        aⁱ = 2 / (h1 + 2) * sqrt((i-1) * (i-1 + α + β) * (i-1 + α) * (i-1 + β) / ((h1 + 1) * (h1 + 3)))
        bⁱ = - (α^2 - β^2) / (h1 * (h1 + 2))

        # compute coefficients for ith column
        @. vi = 1 / aⁱ * (-aʲ * vM2 + (x - bⁱ) * vM1)

        # save a coefficient for next iteration
        aʲ = aⁱ
    end

    return v
end

"""
dvandermonde(x, α, β, N)
# Description
    Return the gradient of the vandermonde matrix of order N at the values x
    Allocates a little bit of memory
# Arguments
-   `x`: points at which to evaluate the Jacobi polynomials
-   `α`: first parameter for Jacobi polynomials
-   `β`: second paramater for Jacobi polynomials
-   `N`: maximum order of Jacobi polynomial to include
# Return Values
-   `vr`: gradient of vandermonde matrix
# Example
    See LegendreTests.jl
"""
function dvandermonde(x, α, β, N)
    # create empty matrix (also handles first set of derivatives)
    vr = zeros(length(x), N+1)

    if N == 0
        return vr
    end

    # set values using vandermonde matrix
    v = vandermonde(x, α+1, β+1, N)
    for i in 1:N
        vi = view(v, :, i)
        vrP1 = view(vr, :, i+1)
        @. vrP1 = sqrt(i * (α + β + i+1)) * vi
    end

    return vr
end

"""
dmatrix(x, α, β, N)
# Description
    Return the differentiation matrix of order N at the values x
    Allocates too much memory
# Arguments
-   `x`: points at which to evaluate the Jacobi polynomials
-   `α`: first parameter for Jacobi polynomials
-   `β`: second paramater for Jacobi polynomials
-   `N`: maximum order of Jacobi polynomial to include
# Return Values
-   `D`: the differentiation matrix
# Example
    See LegendreTests.jl
"""
function dmatrix(x, α, β, N)
    # calculate vandermonde matrix and grad of vandermonde matrix
    vr = dvandermonde(x, α, β, N)
    v  =  vandermonde(x, α, β, N)

    # calculate values using D = vr * v^-1
    d = vr / v

    return d
end

"""
lift1D(V, y)
for computing fluxes
helps compute a surface integral of a quantity
note that the parentheses are necessary to prevent too much multiplcation
the E function takes the surface integrals are presents it
with respect to the full space inside an element
the entire operator represents how fluxes flow
into the interior of an element
"""
function lift1D(V)
    m,n = size(V)

    E = zeros(m , 2)
    E[1,1] = 1.0
    E[m,2] = 1.0

    return V * (transpose(V) * E)
end

"""
lift1D_v2(V, y)
for computing fluxes
nodal form
helps compute a surface integral of a quantity
note that the parantheses are necessary to prevent too much multiplcation
the E function takes the surface integrals are presents it
with respect to the full space inside an element
the entire operator represents how fluxes flow
into the interior of an element
"""
function lift1D_v2(V)
    m,n = size(V)

    E = zeros(m , 2)
    E[1,1] = 1.0
    E[m,2] = 1.0

    return E
end

"""
jacobiGQ(α, β, N)
# Description
    Guass Quadrature points and weights for the Jacobi Polynomial (α,β)
# Input
α, β: Jacobi polynomial descriptors
N:    order of quadrature points
# Return: x,w
x: quadrature points | array of size N+1
w: quadrature weights | array of size N+1
#Example
α = 0
β = 0
N = 4
x, w = jacobiGQ(α, β, N)
"""
function jacobiGQ(α, β, N)
    N == 0 && return [(α-β) / (α+β+2)], [2]

    # Form symmetric matrix from recurrence.
    dv = OffsetArray(zeros(N+1), 0:N)  # diagonal vector
    ev = OffsetArray(zeros(N+1), 0:N)  # sub/super-diagonal vector

    for n in 0:N
        dv[n] = bᴾ(α, β, n)
        ev[n] = aᴾ(α, β, n)
    end

    # Create full matrix combining the two.
    # Need to pass arrays that are not offset.
    J = SymTridiagonal(dv[0:N], ev[1:N])
    (α + β) ≈ 0 && (J[1, 1] = 0)

    # Compute quadrature points and weights by eigenvalue solve.
    x, V = eigen(J)
    w = @. V[1, :]^2 * 2^(α+β+1) / (α+β+1)
    @. w *= factorial(α) * factorial(β) / factorial(α+β)

    return x, w
end

"""
jacobiGL(α, β, N)
# Description
    Guass Labatto quadrature points for the Jacobi Polynomial (α,β)
    The quadrature weights are computed as well (but not returned)
# Arguments
- `α, β`: Jacobi polynomial descriptors
- `N`:    order of quadrature
# Return: x
- `x`: quadrature points  | array of size N+1
# Examples
```julia-repl
julia> x = jacobiGL(0, 0, 4)
5-element Array{Float64,1}:
 -1.0
 -0.6546536707079759
  4.440892098500626e-16
  0.6546536707079771
  1.0
```
"""
function jacobiGL(α, β, N)
    N == 0 && error("What are you doing? Go back to finite volume land.")
    N == 1 && return [-1, 1]

    x = zeros(N+1)
    x[1], x[N+1] = -1, 1

    x_GQ, _ = jacobiGQ(α+1, β+1, N-2)
    x[2:N] .= x_GQ

    return x
end

abstract type AbstractMesh end

"""
gridvalues1D(xmin, xmax, K)

# Description

    Generates physical gridpoints with each element

# Arguments

    VX: vertex values | an Array of size K+1

    EtoV: element to node connectivity | a Matrix of size Kx2

    r: LGL nodes in reference element | an array

# Return Values: x

    x: physical coordinates of solution

# Example (uses ../utils.jl as well)

xmin = 0
xmax = 2π
K = 4
# call functions
VX, EtoV = unimesh1D(xmin, xmax, K)
r = jacobiGL(0, 0, 4)
x = gridvalues1D(VX, EtoV, r)
# x[:,1] is the physical coordinates within the first element
# for plotting
f(x) = sin(x)
plot(x, f.(x))
# scatter(x,f.(x)) tends to work better
"""
function gridvalues1D(VX, EtoV, r)
    # get low and high edges
    va = view(EtoV, :, 1)
    vb = view(EtoV, :, 2)

    # compute physical coordinates of the grid points
    x = ones(length(r),1) * (VX[va]') .+ 0.5 .* (r .+ 1 ) * ((VX[vb] - VX[va])')
    return x
end

"""
facemask1D(r)

# Description

    creates face mask

# Arguments

    r: GL points

# Return Values: x

    fmask1: standard facemask
    fmask2: alternate form

# Example | ../utils.jl

r = jacobiGL(0, 0, 4)
fmask = fmask1D(r)

"""
function fmask1D(r)
    # check if index is left or right edge
    fm1 = @. abs(r+1) < eps(1.0);
    fm2 = @. abs(r-1) < eps(1.0);
    fmask1 = (fm1,fm2)

    # alternate form
    tmp = collect(1:length(r))
    fmask2  = [tmp[fm1]; tmp[fm2]]

    fmask = (fmask1, fmask2)
    return fmask
end

"""
edgevalues1D(fmask, x)

# Description

    calculates edge values

# Arguments

    fmask: face mask for GL edges

    x:  physical coordinates of solution on each element

# Return Values: x

    fx: face values of x

# Example | ../utils.jl

r = jacobiGL(0, 0, 4)
x = gridvalues1D(VX, EtoV, r)
fmask = fmask1D(r)[1]
fx = edgevalues1D(fmask,x)

# the locations of the edges in element 1 is fx[:, 1]

"""
function edgevalues1D(fmask, x)
    # compute x values at selected indices
    fx1 = x[fmask[1],:]
    fx2 = x[fmask[2],:]

    # return list of physical edge positions
    fx = [fx1; fx2]
    return fx
end

"""
normals1D(K)

# Description

    calculates face normals

# Arguments

    K: number of elements

# Return Values: normals

    normals: face normals along each grid

# Example

"""
function normals1D(K)
    normals  = ones(2,K)
    @. normals[1,:] *= -1
    return normals
end

"""
geometric_factors(x, Dʳ)

# Description

    computes the geometric factors for local mappings of 1D elements

# Arguments

    x: physical coordinates of solution for each element

    Dʳ:

# Return Values: rx, J

    rx: inverse jacobian

    J: jacobian (in 1D a scalar)

# Example

"""
function geometric_factors(x, Dʳ)
    J = Dʳ * x
    rx = 1 ./ J # for 1D
    return rx, J
end

"""
connect1D(EtoV)

# Description

    builds global connectivity arrays for 1D

# Arguments

    EtoV: element to node connectivity | a Matrix of size Kx2

# Return Values: EtoE, EtoF

    EtoE: element to element connectivity
    EtoF: element to face connectivity

# Example

"""
function connect1D(EtoV)
    nFaces = 2 # for 1d elements

    # Find number of elements and vertices
    K = size(EtoV,1)
    total_faces = nFaces * K
    Nv = K+1

    # list of local face to local vertex connections
    vn = [1, 2]

    # build global face to vertex array
    FtoV = Int.(spzeros(total_faces, Nv))
    let sk = 1
        for k = 1:K
            for faces = 1:nFaces
                FtoV[sk, EtoV[k, vn[faces]]] = 1;
                sk += 1
            end
        end
    end

    # build global face to face array
    FtoF = FtoV * (FtoV') - sparse(I, total_faces, total_faces)

    # find all face to face connections
    # check
    #(faces1, faces2) = findnz(FtoF)
    faces1, faces2 = findnz(FtoF .== 1)

    # convert global face number to element and face numbers
    element1 = @. floor(Int, (faces1 - 1) / nFaces) + 1
    face1    = @. Int( mod( (faces1 - 1),  nFaces) + 1)
    element2 = @. floor(Int, (faces2 - 1) / nFaces) + 1
    face2    = @. Int( mod( (faces2 - 1),  nFaces) + 1)

    # Rearrange into Nelement x Nfaces sized arrays
    ind = diag( LinearIndices(ones(K, nFaces))[element1,face1] ) # this line is a terrible idea.
    EtoE = collect(1:K) * ones(1, nFaces)
    EtoF = ones(K, 1) * (collect(1:nFaces)')
    EtoE[ind] = copy(element2);
    EtoF[ind] = face2;
    return EtoE, EtoF
end

"""
buildmaps1D(K, nGL, nFP, nFaces, fmask, EtoE, EtoF, x)
# Description

    connectivity matrices for element to elements and elements to face

# Arguments

-   `K`: number of elements
-   `nGL`: number of points within an element (polynomial degree + 1)
-   `nFP`: 1
-   `nFaces`: 2
-   `fmask`: an element by element mask to extract edge values
-   `EtoE`: element to element connectivity
-   `EtoF`: element to face connectivity
-   `x`: Guass lobatto points

# Return Values: vmapM, vmapP, vmapB, mapB, mapI, mapO, vmapI, vmapO

-   `vmapM`: vertex indices, (used for interior u values)
-   `vmapP`: vertex indices, (used for exterior u values)
-   `vmapB`: vertex indices, corresponding to boundaries
-   `mapB`: use to extract vmapB from vmapM
-   `mapI`: Index of left boundary condition
-   `mapO`: Index of right boundary condition

# Example | uses ../utils.jl

K = 3
n = 3; α = 0; β = 0; xmin = 0; xmax = 2π;
nGL = n + 1
nFP = 1
nFaces = 2

r = jacobiGL(α, β, n)

VX, EtoV = unimesh1D(xmin, xmax, K)
EtoE, EtoF = connect1D(EtoV)
x = gridvalues1D(VX, EtoV, r)
fx = edgevalues1D(r,x)

vmapM, vmapP, vmapB, mapB, mapI, mapO, vmapI, vmapO = buildmaps1D(K, nGL, nFP, nFaces, fmask, EtoE, EtoF, x)
"""
function buildmaps1D(K, nGL, nFP, nFaces, fmask, EtoE, EtoF, x)
    # number volume nodes consecutively
    nodeids = reshape(collect(1:(K*nGL)), nGL, K)
    vmapM = zeros(nFP, nFaces, K)
    vmapP = zeros(nFP, nFaces, K)
    # find index of face nodes wrt volume node ordering
    for k1 in 1:K
        for f1 in 1:nFaces
            vmapM[:, f1, k1] = nodeids[fmask[f1], k1]
        end
    end

    for k1 = 1:K
        for f1 = 1:nFaces
            # find neighbor
            k2 = Int.( EtoE[k1, f1])
            f2 = Int.( EtoF[k1, f1])

            # find volume node numbers of left and right nodes
            vidM = Int.( vmapM[:, f1, k1])
            vidP = Int.( vmapM[:, f2, k2])

            x1 = x[vidM]
            x2 = x[vidP]

            # compute distance matrix
            # need to figure out this part
            D = @. (x1 - x2)^2
            m = length(x1)
            for j = 1:m
                if D[j] < eps(1.0)*10^5
                    vmapP[j, f1, k1] = vidP[j]
                end
            end
        end
    end

    # reshape arrays
    vmapP = Int.( reshape(vmapP, length(vmapP)) )
    vmapM = Int.( reshape(vmapM, length(vmapM)) )

    # Create list of boundary nodes
    mapB = Int.( collect(1:length(vmapP))[vmapP .== vmapM] )
    vmapB = Int.( vmapM[mapB] )

    # inflow and outflow maps
    mapI = 1
    mapO = K * nFaces
    vmapI = 1
    vmapO = K*nGL
    return vmapM, vmapP, vmapB, mapB, mapI, mapO, vmapI, vmapO
end

"""
make_periodic1D!(vmapP, u)

# Description

    makes the grid periodic by modifying vmapP.
    Assumes that the first node is connected to the last.

# Arguments

    vmapP: exterior vertex map
    u: vertex vector

# Return Values: none

# Example

"""
function make_periodic1D!(vmapP, u)
    vmapP[1] = length(u)
    vmapP[end] = 1

    return nothing
end

struct Mesh{T,S,U,W} <: AbstractMesh
    # inputs
    K::S
    n::S

    # face stuff
    nFP::S
    nFaces::S

    # GL points
    r::U
    x::T

    # vertex maps
    vmapM::W
    vmapP::W
    vmapB::W
    mapB::W

    # inflow/outflow maps
    mapI::S
    mapO::S
    vmapI::S
    vmapO::S

    # structures for computation
    D::T
    M::T
    Mi::T
    lift::T
    rx::T
    normals::T
    fscale::T
end

"""
mesh(K, n, xmin, xmax)

# Description

    outer_constructor mesh struct

# Arguments

- `K`: number of elements
- `n`: polynomial order
- `xmin`: lower bound
- `xmax`: upper bound

# Return Values: x
    return grid values

"""
function Mesh(KK, nn, xmin, xmax; periodic = false)
    # initialize parameters
    K = KK
    α = 0; β = 0;
    n = nn

    # number of vertices
    nGL = n+1
    nFP = 1
    nFaces = 2

    # compute Gauss Lobatto grid
    r = jacobiGL(α, β, n)

    # build grid
    VX, EtoV = unimesh1D(xmin, xmax, K)

    # build coordinates of all the nodes
    x = gridvalues1D(VX, EtoV, r)

    # build connectivity matrix
    EtoE, EtoF = connect1D(EtoV)

    # build face masks
    fmask1,fmask2 = fmask1D(r)

    # build connectivity maps
    vmapM,vmapP,vmapB,mapB, mapI,mapO,vmapI,vmapO = buildmaps1D(K, nGL,nFP,nFaces, fmask1, EtoE,EtoF, x)

    # build differentiation matrix
    D = dmatrix(r, α, β, n)

    # build surface integral terms
    V = vandermonde(r, α, β, n)
    lift = lift1D(V)

    # build mass matrix and inverse of mass matrix
    Mi = V * V'
    M = inv(Mi)

    # calculate geometric factors
    rx,J = geometric_factors(x, D)

    # build surface normals
    normals = normals1D(K)

    # build inverse metric at the surface
    fscale = 1 ./ J[fmask2,:]

    # hack for periodicity
    if periodic
        vmapP[1] = vmapM[end]
        vmapP[end] = vmapM[1]
    end

    return Mesh{typeof(x),typeof(K),typeof(r),typeof(vmapP)}(K, n, nFP, nFaces, r, x, vmapM, vmapP, vmapB, mapB, mapI, mapO, vmapI, vmapO, D, M, Mi, lift, rx, normals, fscale)
end

function compute_volume_terms(data::AbstractArray, mesh::Mesh)
    q = mesh.D * data
    @. q *= mesh.rx
    return q
end

function compute_surface_terms(data::AbstractArray, 𝒢::Mesh)
    diffs = reshape( (data[𝒢.vmapM] - data[𝒢.vmapP]), (𝒢.nFP * 𝒢.nFaces, 𝒢.K ))
    # Include factor of 2 for the weak-strong form
    @. diffs *= 1.0 / 2.0
    # Compute Lift Operator
    lifted = - 𝒢.lift * (𝒢.fscale .* 𝒢.normals .* diffs)
    return lifted
end


# function Crst_direct(r::Int, s::Int, t::Int)
#     # direct method to calculate Crst
#     # see Products of Laguerre Polynomials
#     # Joseph Gillis and  George Weiss
#     # Mathematics of Computation, Vol. 14, No. 69 (Jan., 1960), pp. 60-63
#     # this function implements equation (7)
#     # this is computationally expensive.
#     # use recurrence relation instead.
#     (r >= 0 && s >= 0 && t >= 0) || throw("negative orders not allowed.")
#     # t needs to be in |r-s| and r+s
#     # otherwise the coef is zero
#     if t < abs(r - s) || t > r + s
#         return 0
#     end

#     p = r + s - t
#     # define nmax and nmin and find them
#     nmax = r
#     if s < nmax
#         nmax =  s
#     end
#     if p < nmax
#         nmax = p
#     end
#     nmin = ceil(Int, p / 2)
#     # use rational numbers for now to keep it exact.
#     C = 0 // 1
#     for n in nmin:nmax
#         C += (2 // 1)^(2 * n) * factorial(r + s - n) / ( factorial(r - n) * factorial(s - n) * factorial(2n - p) * factorial(p - n))
#     end
#     # prefactor
#     C *= (-1 // 2)^p
# end

function Crst(r::Int, s::Int, t::Int)
    # this implements equation (22) and (23) in the same paper.
    if r < 0 || s < 0 || t < 0
        return 0 // 1
    end
    # (r >= 0 && s >= 0 && t >= 0) || throw("negative orders not allowed.")
    # notice that C_{rst} is symmetric with respect to any swapping of indices.
    # take r to be the larer one of r and s
    # the recurrence is given for t+1
    # shift it to calculate for t;
    if r < s
        r, s = s, r # swap.
    end
    # t needs to be in |r-s| and r+s
    # otherwise the coef is zero
    if t < r - s || t > r + s
        return 0 // 1
    end

    sigma = r + s + 1 // 1
    delta = r - s
    if t == delta
        return Rational(binomial(r, s))
    end

    # handle general case.
    f1 = t * (delta^2 - t^2)
    f2 = (2t - 1) * (delta^2 - (t - 1)^2 + t * (sigma - t ))
    f3 = -(t - 1) * (delta^2 - (t - 2)^2 + (4t - 3) * (sigma - t + 1))
    f4 = 2(t - 1) * (t - 2) * (sigma - t + 2)
    return (f2 * Crst(r, s, t - 1) + f3 * Crst(r, s, t - 2) + f4 * Crst(r, s, t - 3) ) / f1
end

function laprodexpand(r::Int, s::Int)
    # expand the product of two Laguerre polynomials (with coefficients being 1) into a linear
    # combination of Laguerre polynomials.
    # Lr(x)Ls(x) into sum_{t}C_{rst}Lt(x)
    orders = abs(r - s):(r + s) |> collect
    coeffs = [Crst(r, s, t) for t in orders]
    return orders, coeffs
end

function Base.:*(l1::LaguerrePolynomial{T}, l2::LaguerrePolynomial{S}) where {T,S}
    # take two linear combinations of Laguerre polynomials.
    # and reexpand into a single Laguerre polynomial series.
    lst = []
    R = promote_type(T, S)
    for (r, c1) in l1
        for (s, c2) in l2
            # construct the product
            orders, coeffs = laprodexpand(r, s)
            # convert to type R
            coeffs = R.(coeffs)
            # add this to the list
            la = LaguerrePolynomial(orders, coeffs)
            la = la * c1 * c2
            push!(lst, la)
        end
    end
    # add elements in the list.
    la = lst[1]
    if length(lst) > 1
        for i = 2:length(lst)
            la += lst[i]
        end
    end
    return la
end


"""
    Lennard-Jones Potential
"""
mutable struct LennardJones <: Potential
    ε::Float64
    σ::Float64
end

"""
    LennardJones(params::Dict)

Creates the LennardJones potential.
"""
function LennardJones(params::Dict)
    # Creates the LennardJones model
    ε = params["ε"]
    σ = params["σ"]
    return LennardJones(ε, σ)
end

"""
    potential_energy(p::LennardJones, r::Position, args...)

Calculates LennardJones potential energy.
"""
function potential_energy(p::LennardJones, r::Position, args...)
    return 4.0 * p.ε * ((p.σ / norm(r))^12 - (p.σ / norm(r))^6)
end


# Polynomial mean function

"""
    MeanPoly <: Mean

Polynomial mean function
```math
m(x) = ∑ᵢⱼ βᵢⱼxᵢʲ
```
with polynomial coefficients ``βᵢⱼ`` of shape ``d × D`` where ``d`` is the dimension of
observations and ``D`` is the degree of the polynomial.
"""
mutable struct MeanPoly <: Mean
    "Polynomial coefficients"
    β::Matrix{Float64}
    "Priors for mean parameters"
    priors::Array

    """
        MeanPoly(β::Matrix{Float64})

    Create `MeanPoly` with polynomial coefficients `β`.
    """
    MeanPoly(β::Matrix{Float64}) = new(β, [])
end

function mean(mPoly::MeanPoly, x::AbstractVector)
    dim = length(x)
    deg = size(mPoly.β, 2)
    dim == size(mPoly.β, 1) || throw(ArgumentError("Observations and mean function have inconsistent dimensions"))
    return sum(dot(x.^j, mPoly.β[:,j]) for j in 1:deg)
end

get_params(mPoly::MeanPoly) = vec(mPoly.β)
get_param_names(mPoly::MeanPoly) = get_param_names(vec(mPoly.β), :β)
num_params(mPoly::MeanPoly) = length(mPoly.β)

function set_params!(mPoly::MeanPoly, hyp::AbstractVector)
    length(hyp) == num_params(mPoly) || throw(ArgumentError("Polynomial mean function has $(num_param) parameters"))
    copyto!(mPoly.β, hyp)
end

function grad_mean(mPoly::MeanPoly, x::AbstractVector)
    dim = length(x)
    deg = size(mPoly.β, 2)
    dM_theta = Array{Float64}(undef, dim, deg)
    for i in 1:dim
        for j in 1:deg
            dM_theta[i,j] = x[i].^j
        end
    end
    return vec(dM_theta)
end


# indexing convention for ∂G_ij/∂p^k: (i, j, k)
# indexing convention Γ^k_ij: (i, j, k)
function christoffel_symbols(pn, task::Task{<:TaskMapT{M,N,S}}, C::Chart{I,N}) where {M,N,I,S}
    n = dim(T{N})
    Ginv = inv(metric_chart(pn, task, C))
    ∂G_∂xn_matrix = ForwardDiff.jacobian(pn -> reshape(metric_chart(pn, task, C), Size(n^2)), pn)
    ∂G_∂xn_matrix == (@SMatrix zeros(n^2,n)) && return @SArray zeros(n,n,n)
    ∂G_∂xn = reshape(∂G_∂xn_matrix, Size(n,n,n))
    inds = static(1):static(n)
    Γ = SArray{Tuple{n,n,n},S}([0.5*sum(Tuple(Ginv[k,l]*(∂G_∂xn[j,l,i]+∂G_∂xn[i,l,j]-∂G_∂xn[i,j,l])
        for l in inds)) for i in inds, j in inds, k in inds])
end

function christoffel_symbols(pn, task::Task{<:BaseTaskMap}, CN)
    n, S = dim(codomain_manifold(task)), eltype(pn)
    Ginv = inv(metric_chart(pn, task, CN))
    ∂G_∂xn_matrix = ForwardDiff.jacobian(pn -> reshape(metric_chart(pn, task, CN), Size(n^2)), pn)
    ∂G_∂xn_matrix == (@SMatrix zeros(n^2,n)) && return @SArray zeros(n,n,n)
    ∂G_∂xn = reshape(∂G_∂xn_matrix, Size(n,n,n))
    inds = static(1):static(n)
    Γ = SArray{Tuple{n,n,n},S}([0.5*sum(Tuple(Ginv[k,l]*(∂G_∂xn[j,l,i]+∂G_∂xn[i,l,j]-∂G_∂xn[i,j,l])
        for l in inds)) for i in inds, j in inds, k in inds])
end

function christoffel_symbols_chart_transition(pn1, Γ1, C1::Chart{I,N},
        C2::Chart{J,N}) where {N,I,J}
    n, S = dim(N), eltype(pn1)
    Γ1 == (@SArray zeros(n,n,n)) && return @SArray zeros(n,n,n)
    pn2 = chart_transition(pn1, C1, C2)
    ∂pn1_∂pn2 = chart_transition_jacobian(pn2, C2, C1)
    ∂pn2_∂pn1 = inv(∂pn1_∂pn2)
    Hpn2_pn1 = chart_transition_hessian(pn2, C2, C1)
    inds = static(1):static(n)
    Γ2 = SArray{Tuple{n,n,n},S}([sum(Tuple(∂pn2_∂pn1[k,l]*∂pn1_∂pn2[r,i]*∂pn1_∂pn2[s,j]*Γ1[r,s,l]
        for l in inds, r in inds, s in inds)) + sum(Hpn2_pn1[l,i,j]*∂pn2_∂pn1[k,l] for l in inds)
        for i in inds, j in inds, k in inds])
end


#Dot product using LoopVectorization (real ⋅ real)
"""
    l2inner(a,b)

Computes the inner product in the l2 vector space (dot product) between Real or Complex vectors `a` and `b`. The lenghts of `a` and `b` must match.

See also [`l2norm`](@ref), [`l2dist`](@ref), [`sql2norm`](@ref), [`sql2dist`](@ref).

# Example
```jldoctest
a = [1,2,3]
b = [2,0,4]
l2inner(a,b)

# output

14
```
"""
function l2inner(a::AbstractVector{T}, b::AbstractVector{T}) where {T}
    s = zero(T)
    @turbo for i ∈ eachindex(a,b)
        s += a[i] * b[i]
    end
    return s
end

#Dot product using LoopVectorization (real ⋅ complex)
function l2inner(a::AbstractVector{T}, cb::AbstractVector{Complex{T}}) where {T}
    re = zero(T)
    im = zero(T)
    b = reinterpret(reshape, T, cb)
    @turbo for i ∈ eachindex(a)
        re += a[i] * b[1,i]
        im += a[i] * b[2,i]
    end
    return Complex(re, im)
end

#Dot product using LoopVectorization (real ⋅ complex)
function l2inner(ca::AbstractVector{Complex{T}}, b::AbstractVector{T}) where {T}
    re = zero(T)
    im = zero(T)
    a = reinterpret(reshape, T, ca)
    @turbo for i ∈ eachindex(b)
        re += a[1,i] * b[i]
        im += - (a[2,i] * b[i])
    end
    return Complex(re, im)
end

#Dot product using LoopVectorization (complex ⋅ complex)
function l2inner(ca::AbstractVector{Complex{T}}, cb::AbstractVector{Complex{T}}) where {T}
    re = zero(T)
    im = zero(T)
    a = reinterpret(reshape, T, ca)
    b = reinterpret(reshape, T, cb)
    @turbo for i ∈ axes(a,2) #Conjugate(a) × b
        re += (a[1,i] * b[1,i]) + (a[2,i] * b[2,i])
        im += (a[1,i] * b[2,i]) - (a[2,i] * b[1,i])
    end
    return Complex(re, im)
end

#L2 Norm square using LoopVectorization (real)
"""
    l2inner(a)

Computes the inner product in the l2 vector space (dot product) of the Real or Complex vectors `a` with itself. i.e., the Squared l2 norm of `a`. This is identical to calling `sql2norm(a)`.
"""
function l2inner(a::AbstractVector{T}) where {T}
    s = zero(T)
    @turbo for i ∈ eachindex(a)
        s += a[i] * a[i]
    end
    return s
end

"""
    sql2norm(a)

Compute the square of the l2 norm of a real valued vector `a`. This is identical to calling `l2inner(a)`.
"""
sql2norm(a::AbstractVector{T}) where {T} = l2inner(a)

"""
    l2norm(a)

Compute the l2 norm of a real valued vector `a`.
"""
l2norm(a::AbstractVector{T}) where {T} = sqrt(l2inner(a))

#L2 Distance square using LoopVectorization (real)
"""
    sql2dist(a,b)

Compute the square of the l2 distance between two real vectors `a` and `b`. The lengths of `a` and `b` must match.
"""
function sql2dist(a::AbstractVector{T}, b::AbstractVector{T}) where {T}
    s = zero(T)
    @turbo for i ∈ eachindex(a,b)
        t = a[i] - b[i]
        s += t * t
    end
    return s
end

"""
    l2dist(a,b)

Compute the l2 distance between two real vectors `a` and `b`. The lengths of `a` and `b` must match.
"""
l2dist(a::AbstractVector{T}, b::AbstractVector{T}) where {T} = sqrt(sql2dist(a,b))

#L1 Norm of real vectors
"""
    l1norm(a)

Compute the l1 norm of a real valued vector `a`.
"""
function l1norm(a::AbstractVector{T}) where {T}
    s = zero(T)
    @turbo for i ∈ eachindex(a)
        s += abs(a[i])
    end
    return s
end

#L1 distance between real vectors
"""
    l1dist(a,b)

Compute the l1 distance between two real vectors `a` and `b`. The lengths of `a` and `b` must match.
"""
 function l1dist(a::AbstractVector{T}, b::AbstractVector{T}) where {T}
    s = zero(T)
    @turbo for i ∈ eachindex(a,b)
        t = a[i] - b[i]
        s += abs(t)
    end
    return s
 end


#Some useful functions

#Define the sigmoid function & its derivative
sigmoid(x)  = 1.0 / (1.0 + exp(-x));
dsigmoid(x)  = (1.0 / (1.0 + exp(-x))^2)*exp(-x);


function iterate(m::Integer)
    while m != 1 && m != 89
        s = 0
        while m > 0 # compute sum of squares of digits
            m, d = divrem(m, 10)
            s += d ^ 2
        end
        m = s
    end
    return m
end
itercount(k::Integer) = count(x -> iterate(x) == 89, 1:k)


#   Unit examples of the FourierAnalysis Package for julia language
#   v 0.0.1 - last update 24th of September 2019
#
#   MIT License
#   Copyright (c) 2019, Marco Congedo, CNRS, Grenobe, France:
#   https://sites.google.com/site/marcocongedo/home

# ? CONTENTS :
#   This example show how to use Goertzel's algorithms.

#   ~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~  #
#                                                                             #
#   ~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~  #

########## Test goertzel algorithms #############
using FourierAnalysis
sr, t, f, a = 128, 128, 5, 10
v=sinusoidal(a, f, sr, t, 0)
c=goertzel(v, f, sr, t) # should be 0+aim
d=goertzel2(v, f, sr, t) # should be 0+aim
abs2(c) # power, should be a²
abs2(d) # power, should be a²

ftrue=f+0.15
v=sinusoidal(a, ftrue, sr, t, 0 )
c=goertzel(v, f, sr, t) # should be 0+aim
d=goertzel2(v, ftrue, sr, t) # should be 0+aim
abs2(c) # power, should be a²
abs2(d) # power, should be a²
#################################################


"""
Approximates the arc length of a line segment by treating the curve as a
sequence of linear lines and summing their lengths.

Arguments:
 - f: function that returns the arc
 - x_start: starting x value
 - x_end: ending x_value
 - steps: steps to take for accurace, more the steps greater the accuracy
"""
function line_length(f, x_start, x_end, steps=100)
	x1 = x_start
	fx1 = f(x1)
	len = 0.0

	for step in 1:steps
		x2 = ((x_end - x_start) / steps) + x1
		fx2 = f(x2)
		len += hypot(x2 - x1, fx2 - fx1)

		x1 = x2
		fx1 = fx2
	end

	return len
end


# additional math functions & functors

sqr(x::Number) = x * x
rcp(x::Number) = one(x) / x

rsqrt(x::Number) = one(x) / sqrt(x)
rcbrt(x::Real) = one(x) / cbrt(x)

xlogx(x::AbstractFloat) = x > zero(x) ? x * log(x) : zero(x)
xlogx(x::Real) = xlogx(float(x))

xlogy{T<:AbstractFloat}(x::T, y::T) = x > zero(T) ? x * log(y) : zero(x)
xlogy{T<:Real}(x::T, y::T) = xlogy(float(x), float(y))
xlogy(x::Real, y::Real) = xlogy(promote(x, y)...)

logistic(x::AbstractFloat) = rcp(one(x) + exp(-x))
logistic(x::Real) = logistic(float(x))

logit(x::AbstractFloat) = log(x / (one(x) - x))
logit(x::Real) = logit(float(x))

softplus(x::AbstractFloat) = x <= 0 ? log1p(exp(x)) : x + log1p(exp(-x))
softplus(x::Real) = softplus(float(x))

invsoftplus(x::AbstractFloat) = log(exp(x) - one(x))
invsoftplus(x::Real) = invsoftplus(float(x))

logsumexp{T<:AbstractFloat}(x::T, y::T) = x > y ? x + log1p(exp(y - x)) : y + log1p(exp(x - y))
logsumexp{T<:Real}(x::T, y::T) = logsumexp(float(x), float(y))
logsumexp(x::Real, y::Real) = logsumexp(promote(x, y)...)

@vectorize_1arg Number sqr
@vectorize_1arg Number rcp
@vectorize_1arg Real rsqrt
@vectorize_1arg Real rcbrt

@vectorize_1arg Real xlogx
@vectorize_2arg Real xlogy
@vectorize_1arg Real logistic
@vectorize_1arg Real logit
@vectorize_1arg Real softplus
@vectorize_1arg Real invsoftplus

const sigmoid = logistic


# psf-gauss.jl

export psf_gauss

"""
    psf_gauss( ; nx, nx_psf, fwhm_start, fwhm_end, fwhm, T)

Create depth-dependent Gaussian PSFs
having specified full-width half-maximum (FHWM) values.

# Options
- 'nx::Int = 128'
- 'nx_psf::Int = 11' (should be odd)
- 'fwhm_start::Real = 1'
- 'fwhm_end::Real = 4'
- 'fwhm::AbstractVector{<:Real} = LinRange(fwhm_start, fwhm_end, nx)'
- 'T::DataType == Float32'

Returned `psf` is `[nx_psf, nx_psf, nx]` where each PSF sums to 1.
"""
function psf_gauss( ;
    nx::Int = 128,
    nx_psf::Int = 11,
    fwhm_start::Real = 1,
    fwhm_end::Real = 4,
    fwhm::AbstractVector{<:Real} = LinRange(fwhm_start, fwhm_end, nx),
    T::DataType = Float32,
)
    isodd(nx_psf) || @warn("even nx_psf = $nx_psf ?")
    psf = zeros(T, nx_psf, nx_psf, nx)

    for iy in 1:nx # depth-dependent blur
        r = (-(nx_psf-1)÷2):((nx_psf-1)÷2)
        σ = fwhm[iy] / sqrt(log(256))
        r2 = abs2.(r / σ)
        tmp = @. exp(-π * (r2 + r2'))
        psf[:,:,iy] = tmp / sum(tmp)
    end
    return psf
end


"""
    Fourier

A functor struct representing the Fourier basis.
"""
struct Fourier end

"""
    fourier_basis(k,x)

Evaluate the `k`-th Fourier basis function at `x`.
"""
function fourier_basis(k,x)
    cos((div(k,2)*x - iseven(k)/2)*π)
end

fourier_basis(k,x::AbstractVector) = fourier_basis.(k,x)

(::Fourier)(k,x) = fourier_basis(k,x)