# functions for derivative matrices
function stencil1d(m::Int)

    ic = Int(ceil(m/2.0))
    M = zeros((m, m))
    for i=1:m
        for j=1:m
            M[i, j] = (i - ic)^(j-1) / factorial(j-1)
        end
    end
    return inv(M)
end

# NOTE: Minv is the output of running stencil1d()
function regular_derivative_1d(N, n, Minv)

    D = sparse(zeros(Float64, (N, N)))
    stencil = Minv[n+1,:]
    m = length(stencil)
    ic = Int(ceil(m/2.0))

    for i=1:m
        k = i - ic
        vec = stencil[i] * ones(N - abs(k))
        D += spdiagm(k => vec)
    end

    return D
end

function derivative_matrix(nx, ny, Mxinv, Myinv, grd::Grid)

    Dx = regular_derivative_1d(grd._Nx, nx, Mxinv)
    Dy = regular_derivative_1d(grd._Ny, ny, Myinv)

    D = kron(Dy, Dx)
    P = grd.Proj
    Pinv = transpose(grd.Proj)
    D2d = P * D * Pinv / ((grd.dx)^nx *(grd.dy)^ny)

    return kron(sparse(I, grd.Nz, grd.Nz), D2d)
end

function line_average(points::Matrix{Float64}, mx::Int64, my::Int64, MinvT::Matrix{Float64}, grd::Grid)

    N = size(points)[2]
    is = ones(Int64, N*mx*my)
    js = Int64[]
    dats = Float64[]

    for i=1:N
        row_dat, row_j = interpolation_row(points[:,i]..., mx, my, MinvT, grd)
        js = [js; row_j]
        dats = [dats; row_dat]
    end

    return sparse(is, js, dats, 1, grd._Nx*grd._Ny) * transpose(grd.Proj) / N
end


using Symbolics, Test
import Symbolics: latex

@testset "LaTeX rendering" begin
    @sym x y z

    Symbolics.abbreviate_power_of_negative_unity(true)
    @test latex((-1)^x) == ("(-)^{x}", 1)

    Symbolics.abbreviate_power_of_negative_unity(false)
    @test latex((-1)^x) == ("\\left(-1\\right)^{x}", 1)

    Symbolics.abbreviate_power_of_negative_unity(true)
    f=sin(x^3)
    @test latex(f) == ("\\sin\\left(x^{3}\\right)", 1)
    g′=x/(-z)
    @test latex(g′) == ("\\frac{x}{-z}", 0)
    g=x/-sin(z)
    @test latex(g) == ("\\frac{x}{-\\left(\\sin{z}\\right)}", 1)
    @test latex(x/((x+y+z)^4)) == ("\\frac{x}{\\left(x+y+z\\right)^{4}}", 1)
    @test latex(x/(sin(z)^4)) == ("\\frac{x}{\\sin^{4}z}", 0)
    @test latex(x/(sin(y+z)^4)) == ("\\frac{x}{\\sin^{4}\\left(y+z\\right)}", 1)
    @test latex(x/(sin(y+z))) == ("\\frac{x}{\\sin\\left(y+z\\right)}", 1)
    @test latex(x/(sin(z)^(y+x))) == ("\\frac{x}{\\sin^{y+x}z}", 0)
    @test latex(x + im) == ("x+\\mathrm{i}", 0)
    h=cos(f+g)
    @test latex(h) == ("\\cos\\left[\\sin\\left(x^{3}\\right)+\\frac{x}{-\\left(\\sin{z}\\right)}\\right]", 2)

    Symbolics.advance_delimiters(false)
    @test latex(cos((-1)^x+sin(h))) == ("\\cos\\left((-)^{x}+\\sin\\left(\\cos\\left(\\sin\\left(x^{3}\\right)+\\frac{x}{-\\left(\\sin{z}\\right)}\\right)\\right)\\right)", 0)

    Symbolics.advance_delimiters(true)
    @test latex(cos((-1)^x+sin(h))) == ("\\cos\\left((-)^{x}+\\sin\\left\\{\\cos\\left[\\sin\\left(x^{3}\\right)+\\frac{x}{-\\left(\\sin{z}\\right)}\\right]\\right\\}\\right)", 4)
    @test latex(cos((-1)^x+f)) == ("\\cos\\left[(-)^{x}+\\sin\\left(x^{3}\\right)\\right]", 2)

    @test latex(cos((-1)^(2x+y*(z+cos(x)))+sin(h))) == ("\\cos\\left((-)^{2x+y\\left(z+\\cos{x}\\right)}+\\sin\\left\\{\\cos\\left[\\sin\\left(x^{3}\\right)+\\frac{x}{-\\left(\\sin{z}\\right)}\\right]\\right\\}\\right)", 4)
    @test latex(cos((-1)^(2x+y*(z+cos(x)))+f)) == ("\\cos\\left[(-)^{2x+y\\left(z+\\cos{x}\\right)}+\\sin\\left(x^{3}\\right)\\right]", 2)
    @test latex(x-y-z+x^(2y+4z*(y+x))) == ("-\\left(y+z\\right)+x+x^{2y+4z\\left(y+x\\right)}", 1)
    @test latex(asinh(x+sin(y))) == ("\\operatorname{arcsinh}\\left(x+\\sin{y}\\right)", 1)
    @test latex(log10(x)+log(x)+log2(x)) == ("\\log_{10}{x}+\\ln{x}+\\log_2{x}", 0)
    @test latex(√x) == ("\\sqrt{x}", 0)
    @test latex(√(x+y)) == ("\\sqrt{x+y}", 0)
    @test latex(sin(x/y)) == ("\\sin\\left(\\frac{x}{y}\\right)", 1)
    @test latex(cos(sin(x/y))) == ("\\cos\\left[\\sin\\left(\\frac{x}{y}\\right)\\right]", 2)
    @test latex(sin(-x)) == ("\\sin\\left(-x\\right)", 1)
    @test latex(exp(-x/y)) == ("\\exp\\left(-\\frac{x}{y}\\right)", 1)
    @test latex(exp(x/-y)) == ("\\exp\\left(\\frac{x}{-y}\\right)", 1)

    @test latex(1e-17x) == ("1.0\\times10^{-17}x",0)
end


for v in [0,1,2,3//2,3//1,1.5,1.5im,1im]
    scal(v)
end

param(:g)'

Pr"ω_i,j,3"

σx(:i)*σy(:i)
σz(3)*σy(2)

a(:m)*adag(:n)*adag(3)*a(3)*adag(3)*a(:n)*adag(:m)

σy(1)'
σy(:m)'
(adag(1)*a(:n))' == adag(:n)*a(1)
a(1) * (σy(1) * a(1))'

let
    tmp = ∑(:i,adag(:i)*a(:i))
    adag(:n)*tmp
    a(:n)*tmp
    σz(:n)*tmp
    tmp*scal(2)
    tmp*param(:g,:n)
    tmp*a(:n)
    tmp*adag(:n)
    tmp*(adag(:n)*adag(:n))
    tmp*(adag(:n)*a(:n))
end
∑(:i,σx(:i))*σy(:j)
∑(:i,σx(:i))*σy(:a)
∑(:i,σx(:i)*σy(:a))

comm(σx(5),σy(3))
comm(σx(5),σx(5))
comm(σx(1),σz(1))
scal(2)*comm(σx(5),σy(5))
comm(σy(5),comm(σx(5),σy(5)))

comm(scal(2//5)*param(:h)*σx(5),scal(3)*param(:g)*σy(5))

σx(1)*σy(1)
σx(3)*σy(2)*σy(2)

σx(5)*σy(5)*σz(5)
σx(5)*σy(5)*σx(5)

param(:g,2)*param(:g,1)*param(:g,'c',3)*param(:b)*param(:a)*param(:d,'c',3)*param(:f,'r',1)

a(2)*adag(2)*adag(3)*σy(1)*adag(2)*adag(4)*σz(1)*a(2)*adag(3)

adag(2)*a(2)*adag(2)*σy(1) + scal(2)*param(:g,'c',1)

scal(-1//2) + σp(1) * σm(1)

σp(2)
σm(2)
σp(1)*σm(1)

comm(σp(1),σp(1))
comm(σm(:n),σp(:n))
comm(a(2),adag(:m))

comm(a(1),adag(1)*a(1))
comm(a(1),adag(1)*a(2)*a(1))
comm(a(1),a(2)*adag(1)*a(1)*adag(2))

ascorr(scal(3))
ascorr(a(2))
ascorr(scal(3)*a(2))

ascorr(a(2)*a(2))
ascorr(scal(2)*a(1)*a(2)*a(3))
ascorr(a(1)*a(2)*a(3)*a(4))
ascorr(adag(2)*a(1)*σz(1))

ascorr(param(:g,'r')*adag(3)*a(2)*a(2))
ascorr(scal(-1)*param(:g,'r',1)*σz(1))
ascorr(adag(:n)*σy(:k)*a(:m)*adag(:m))

let
    H = ∑(:i,param(:ω,'r',:i)*adag(:i)*a(:i)) + ∑(:j,scal(1//2)*param(:ωe,'r',:j)*σz(:j)) +
        ∑(:i,∑(:j,param(:g,'r',(:i,:j))*(adag(:i)+a(:i))*σx(:j)))
    comm(adag(:n)*a(:m),H)
    comm(adag(:n)*a(:n),H)
    for op in [a(:n),adag(:n),σx(:k),σy(:k),σz(:k)]
        latex(op),latex(comm(op,H))
    end
    for op in [adag(:n)*σz(:k),a(:n)*σz(:k)]
        latex(op),latex(comm(op,H))
    end

    for op in [adag(:n)*σz(:k),a(:n)*σz(:k)]
        for x = 1:5
            op = comm(op,H)
        end
    end

    ExpVal(H)
    #ascorr(H)
end

vacA(Avac(σp(1)*σm(1)))
vacA(Avac(σm(1)*σp(1)))

for i2 = (:n,:m)
    stateop = scal(1/√2)*adag(:n)*adag(:n)*adag(:n) + scal(1/√2)*adag(i2)
    for A in [scal(1),adag(:n)*a(:n),adag(:n)*adag(:n)*a(:n)*a(:n)]
        latex(A),latex(vacExpVal(A,stateop))
    end
end


struct EllipticCurve
    a::BigInt
    b::BigInt
    n::BigInt
end

struct EllipticPoint
    x::BigInt
    y::BigInt
end

function isInfinityPoint(P::EllipticPoint)
    return P.x == -1 && P.y == -1
end

function double(P::EllipticPoint, E::EllipticCurve)::EllipticPoint
    if isInfinityPoint(P)
        return P
    end

    g::BigInt, inv::BigInt, _ = gcdx(2 * P.y, E.n)
    # if g != 1
    #     return g
    # elseif g == E.n
    #     return InfinityPoint()
    # end

    lambda::BigInt = mod((3 * P.x^2 + E.a) * inv, E.n)
    new_x::BigInt = mod(lambda^2 - 2 * P.x, E.n)
    new_y::BigInt = mod(lambda * (P.x - new_x) - P.y, E.n)

    return EllipticPoint(new_x, new_y)
end

function add(P::EllipticPoint, Q::EllipticPoint, E::EllipticCurve)::EllipticPoint
    if isInfinityPoint(P)
        return Q
    elseif isInfinityPoint(Q)
        return P
    end

    if P.x == Q.x && P.y == Q.y
        return double(P, E)
    end

    @time delta_x = mod(Q.x - P.x, E.n)
    @time g::BigInt, inv::BigInt, _ = gcdx(delta_x, E.n)
    # if 1 < g < E.n
    #     return g
    # elseif g == E.n
    #     return InfinityPoint()
    # end

    @time delta_y = mod(Q.y - P.y, E.n)
    @time lambda = mod(delta_y * inv, E.n)
    @time new_x = mod(lambda^2 - P.x - Q.x, E.n)
    @time new_y = mod(lambda * (P.x - new_x) - P.y, E.n)

    return EllipticPoint(new_x, new_y)
end

function InfinityPoint()
    return EllipticPoint(-1, -1)
end

function multiply(k::BigInt, P::EllipticPoint, E::EllipticCurve)::EllipticPoint
    Q = InfinityPoint()

    @inbounds for b in string(k, base = 2)
        Q = double(Q, E)
        # if typeof(Q) != EllipticPoint
        #     return Q
        # end
        if b == '1'
            Q = add(Q, P, E)
            # if typeof(Q) != EllipticPoint
            #     return Q
            # end
        end
    end
    return Q
end

function test_add()

    p = big"115792089210356248762697446949407573530086143415290314195533631308867097853951"
    a = big"-3"
    b = big"41058363725152142129326129780047268409114441015993725554835256314039467401291"
    E = EllipticCurve(a, b, p)

    gx = big"48439561293906451759052585252797914202762949526041747995844080717082404635286"
    gy = big"36134250956749795798585127919587881956611106672985015071877198253568414405109"
    P = EllipticPoint(gx, gy)

    P = add(P, P, E)
    P = add(P, P, E)
    P = add(P, P, E)
    # for i=1:100000
    #     P = add(P, P, E)
    # end
end

function test_mult()

    p = big"115792089210356248762697446949407573530086143415290314195533631308867097853951"
    a = big"-3"
    b = big"41058363725152142129326129780047268409114441015993725554835256314039467401291"
    E = EllipticCurve(a, b, p)

    gx = big"48439561293906451759052585252797914202762949526041747995844080717082404635286"
    gy = big"36134250956749795798585127919587881956611106672985015071877198253568414405109"
    P = EllipticPoint(gx, gy)

    for i=1:100000
        P = multiply(big"123456", P, E)
    end
end

# initial
# 1.211539 seconds (5.45 M allocations: 153.242 MiB, 11.24% gc time)
# 21.950572 seconds (112.61 M allocations: 2.961 GiB, 12.85% gc time)

@time test_add()
# @time test_mult()


"""
LowRankTransform
```
    P = rand(10,5)
    tr = LowRankTransform(P)
```
Apply the low-rank projection realised by the matrix `P`
The second dimension of `P` must match the number of features of the target.
"""
struct LowRankTransform{T<:AbstractMatrix{<:Real}} <: Transform
    proj::T
end

function set!(t::LowRankTransform{<:AbstractMatrix{T}},M::AbstractMatrix{T}) where {T<:Real}
    @assert size(t) == size(M) "Size of the given matrix $(size(M)) and the projection matrix $(size(t)) are not the same"
    t.proj .= M
end

params(t::LowRankTransform) = t.proj

Base.size(tr::LowRankTransform,i::Int) = size(tr.proj,i)
Base.size(tr::LowRankTransform) = size(tr.proj) #  TODO Add test

function transform(t::LowRankTransform,X::AbstractMatrix{<:Real},obsdim::Int=defaultobs)
    @boundscheck size(t,2) != size(X,feature_dim(obsdim)) ?
        throw(DimensionMismatch("The projection matrix has size $(size(t)) and cannot be used on X with dimensions $(size(X))")) : nothing
    @inbounds _transform(t,X,obsdim)
end

function transform(t::LowRankTransform,x::AbstractVector{<:Real},obsdim::Int=defaultobs) #TODO Add test
    @assert size(t,2) == length(x) "Vector has wrong dimensions $(length(x)) compared to projection matrix"
    t.proj*x
end

_transform(t::LowRankTransform,X::AbstractVecOrMat{<:Real},obsdim::Int=defaultobs) = obsdim == 2 ? t.proj * X : X * t.proj'


module test_suite_applications

using DomainSets, BasisFunctions, FrameFun
using Test

FE = FrameFun
BA = BasisFunctions

## Settings

# Test fourier extensions for all parameters
Extensive = false

# Show matrix vector product timings
const show_mv_times = false
total_mv_allocs = 0
total_mv_time = 0.0

########
# Auxiliary functions
########

function delimit(s::AbstractString)
    println()
    println("############")
    println("# ",s)
    println("############")
end

#######
# Testing -- Accuracy
#######

function test_differential_equations_1d()
    @testset "diff 1D dirichlet" for solverstyle in (AZStyle(), DirectStyle())
        B = Fourier(101) → -1..1
        Dom = Interval(-0.5,0.5)
        # Set up Boundary conditions
        BC = DirichletBC(x->0)
        # Set up Differential equation
        f(x) = x
        Diff = differentiation(B)*differentiation(B)
        DE = DiffEquation(B, Dom, Diff, f, (BC,))
        # Actually solve the differential equation
        F = solve(DE, solverstyle=solverstyle, directsolver=:qr)
        sol(x) = x^3/6 - x/24
        error = abserror(sol,F)
        @test (error < sqrt(eps(real(codomaintype(B))))*10)
        error = abserror(f,F'')
        @test (error < sqrt(eps(real(codomaintype(B))))*100)
    end
    @testset "diff 1D mixed" for solverstyle in (AZStyle(), DirectStyle())
        B = Fourier(101) → -2..2
        Dom = Interval(-1.0,1.0)
        # Set up Boundary conditions
        BC1 = DirichletBC(x->0, Interval(-2.0,0.0))
        BC2 = NeumannBC(x->0, Interval(-2.0,0.0))
        # Set up Differential equation
        f(x) = x
        Diff = differentiation(B)*differentiation(B)
        DE = DiffEquation(B, Dom, Diff, f, (BC1,BC2))
        # Actually solve the differential equation
        F = solve(DE, solverstyle=solverstyle, directsolver=:qr)
        sol(x) = x^3/6 - x/2 -1/3
        error = abserror(sol,F)
        @test (error < sqrt(eps(real(codomaintype(B))))*10)
        error = abserror(f,F'')
        @test (error < sqrt(eps(real(codomaintype(B))))*100)
    end
end

function test_differential_equations_2d()
    # @testset "diff 2D dirichlet" for solverstyle in (AZStyle(), DirectStyle())
    @testset "diff 2D dirichlet" for solverstyle in (AZStyle(),)
        B = (Fourier(11) → -1..1)^2
        Dom = disk(0.8)
        # Set up Boundary conditions
        df = (x,y)->x-y
        BC = DirichletBC(df,DomainSets.euclideanspace(Val{2}()))
        # Set up Differential equation
        f(x,y) = 0
        Diff = differentiation(B,(2,0))+differentiation(B,(0,2))
        DE = DiffEquation(B, Dom, Diff, f, (BC,))
        # Actually solve the differential equation
        F = solve(DE, solverstyle=solverstyle, directsolver=:qr)
        error = abserror(df,F)
        @test (error < 0.3)
        error = abserror(f,∂x(∂x(F))+∂y(∂y(F)))
        @test (error < 0.3)
    end
    # @testset "diff 2D Neumann" for solverstyle in (AZStyle(), DirectStyle())
    # @testset "diff 2D Neumann" for solverstyle in (AZStyle(),)
    #     B = (Fourier(11) → -1..1)^2
    #     Dom = disk(0.8)
    #     # Set up Boundary conditions
    #     df = (x,y)->x-y
    #     BC = NeumannBC(df,DomainSets.euclideanspace(Val{2}()))
    #     # Set up Differential equation
    #     f(x,y) = 0
    #     Diff = differentiation(B,(2,0))+differentiation(B,(0,2))
    #     DE = DiffEquation(B, Dom, Diff, f, (BC,))
    #     # Actually solve the differential equation
    #     F = solve(DE, solverstyle=solverstyle, directsolver=:qr)
    #     # We should find a way to check the boundary condition more easily
    #     berror = sum(abs.(element(operator(DE),2,1)*coefficients(Diff*F)-element(FrameFun.rhs(DE),2)))/length(element(FrameFun.rhs(DE),2))
    #     @test berror < 0.6
    #     error = abserror(f,∂x(∂x(F))+∂y(∂y(F)))
    #     @test (error < 0.3)
    # end
end

# We just test the accuracy, not the smoothing properties
function test_smoothing_1d()
    @testset "Smoothing $(name(Basis(10)))" for Basis in (ChebyshevT,)
        basis = Basis(101)→ -1.0..1.0
        dom = -0.5..0.5
        f = exp
        fscale(dict, i) = 10.0^-4+i+i^2+i^3
        F = Fun(f, basis, dom; solverstyle = AZSmoothStyle(), scaling=fscale)
        @test (abserror(f,F) < 100*sqrt(eps(Float64)))
    end
end

function test_smoothing_2d()
    @testset "Smoothing $(name(Basis(10)))" for Basis in (ChebyshevT,)
        basis = (Basis(20)→(-1.0..1.0))⊗(Basis(20)→(-1.0..1.0))
        dom = disk(0.5)
        f = (x,y) -> exp(x*y)
        fscale(dict, I) = 10.0^-4+100*(I[1]^2+I[2]^2)
        F = Fun(f, basis, dom; solverstyle = AZSmoothStyle(), scaling=fscale)
        @test (abserror(f,F) < 100*sqrt(sqrt(eps(Float64))))
    end
end

test_smoothing_1d()

test_smoothing_2d()

test_differential_equations_1d()

test_differential_equations_2d()

if show_mv_times
    println("Total bytes in MV products:\t$total_mv_allocs")
    println("Total time in MV products:\t$total_mv_time")
end

end


using Revise

using DataFrames, Statistics, Distributions, StatsBase,
  LinearAlgebra, BenchmarkTools, Random, Optim, Zygote

Random.seed!(11)

# a simple AR(1) model
struct AR1{Tϕ<:Real, Tb<:Real, Tμ<:Real, Tσ²<:Union{Nothing,Real}}
  ϕ::Tϕ
  b::Tb
  μ::Tμ
  σ²::Tσ²
end

AR1(;ϕ, μ=missing, b=missing, σ²=nothing) = AR1(
  ϕ, coalesce(b, μ*(1-ϕ)), coalesce(μ, b/(1-ϕ)), σ²)

#runs a simple linear regression
function olsar1(x::Vector{T}) where T
  xc = @view x[2:end]
  Lx = @view x[1:(end-1)]

  ϕ = cov(xc, Lx)/(var(Lx))
  b = mean(xc) - ϕ * mean(Lx)

  @assert b ≈ mean(xc) - ϕ * mean(Lx)
  @assert ϕ ≈ cov(xc .- mean(xc), Lx .- mean(Lx))/var(
    Lx .- mean(Lx))

  return AR1(;ϕ,b)
end

function mlear1(x; tol=10^-10)

  #start by centering
  N=length(x)
  μ = mean(x)
  x̃ = @view(x[2:end]) .- μ
  Lx̃ = @view(x[1:(end-1)]) .- μ

  x₁ = x[1]
  #computes the next iteration of ϕ given a previous ϕ
  SS(ϕ) = (x₁^2*(1-ϕ^2) + sum((x̃ .- ϕ*Lx̃).^2))
  function objϕ(ϕ)
    σ² = SS(ϕ)/N
    return (sum((x̃ .- ϕ * Lx̃) .* Lx̃) - (σ²/(1-ϕ^2) - x₁^2)*ϕ)^2
  end

  ϕ = optimize(objϕ, -1.0, 1.0, Brent()) |> Optim.minimizer

  return AR1(;ϕ, μ, σ² = SS(ϕ)/N)
end

function mle2ar1(x; tol=10^-10)

  #start by centering
  N=length(x)
  μ = mean(x)
  x̃ = @view(x[2:end]) .- μ
  Lx̃ = @view(x[1:(end-1)]) .- μ

  x₁ = x[1]

  l(ϕ, σ²) = (-1/2*log(σ²/(1-ϕ^2))
    - (1-ϕ^2)/(2*σ²)*x₁^2
    - (N-1)/2*log(σ²)
    - 1/(2*σ²)*sum((x̃-ϕ*Lx̃).^2)) * -1

  l(Φ) = l(Φ[1], Φ[2])

  ϕ₀ = cov(x̃, Lx̃)/var(Lx̃) #start by guessing the least squares solution
  ε₀= x̃ - ϕ₀*Lx̃
  σ²₀ = var(ε₀)

  ϕ, σ² = optimize(l,  [-1.0,0.0], [1.0,Inf], [ϕ₀, σ²₀], Fminbox(LBFGS()),
    autodiff = :forward) |> Optim.minimizer

  return AR1(;ϕ, μ, σ²)
end

function mle3ar1(x; tol=10^-10)

  #start by centering
  N=length(x)
  x₁ = x[1]
  xc = @view(x[2:end])
  Lx = @view(x[1:(end-1)])

  #=l(ϕ, σ², μ) = (-N/2*log(σ²)
    + 1/2*log(1-ϕ^2)
    - (x₁-μ)^2*(1-ϕ^2)/(2*σ²)
    - sum((xc .- ϕ .* Lx .- μ*(1-ϕ)).^2)/(2*σ²))*-1=#

  x₁ = x[1]
  #computes the next iteration of ϕ given a previous ϕ
  cμ(ϕ) = (x₁*(1+ϕ)+sum(xc-Lx .* ϕ))/(N*(1-ϕ)+2ϕ)
  cσ²(ϕ,μ) = ((x₁-μ)^2*(1-ϕ^2) + sum((xc .- ϕ*Lx .- μ*(1-ϕ)).^2))/N
  function objϕ(ϕ)
    μ = cμ(ϕ)
    σ² = cσ²(ϕ,μ)
    #return (sum((x .- ϕ * Lx) .* Lx) - (σ²/(1-ϕ^2) - x₁^2))^2
    #the below is just proportional to the derivitive of the likelihood wrt ϕ
    err = -ϕ*σ² + (x₁-μ)^2*ϕ*(1-ϕ^2) + sum((xc .- ϕ * Lx .- μ*(1-ϕ)) .* (Lx .- μ))*(1-ϕ^2)
    return err*err
  end

  ϕ = optimize(objϕ, -1.0, 1.0, Brent(), abs_tol=tol) |> Optim.minimizer
  μ = cμ(ϕ)
  σ² = cσ²(ϕ,μ)
  return AR1(;ϕ, μ, σ²)
end

function mle4ar1(x; tol=10^-10)

  #start by centering
  N=length(x)

  x₁ = x[1]
  xc = @view(x[2:end])
  Lx = @view(x[1:(end-1)])

  l(ϕ, σ², μ) = (-N/2*log(2*π*σ²)
    + 1/2*log(1-ϕ^2)
    - (x₁-μ)^2*(1-ϕ^2)/(2*σ²)
    - sum((xc .- ϕ .* Lx .- μ*(1-ϕ)).^2)/(2*σ²))*-1

  l(Φ) = l(Φ[1], Φ[2], Φ[3])

  ϕ₀ = cov(xc, Lx)/var(Lx) #start by guessing the least squares solution
  b₀ = mean(xc)-ϕ₀*mean(Lx)
  μ₀ = b₀/(1-ϕ₀)
  ε₀= xc .- ϕ₀ .* Lx .- b₀
  σ²₀ = var(ε₀)

  ϕ, σ², μ = optimize(l,  [-1.0,0.0,-Inf], [1.0,Inf,Inf], [ϕ₀, σ²₀, μ₀], Fminbox(LBFGS()),
    autodiff = :forward) |> Optim.minimizer

  return AR1(;ϕ, μ, σ²)
end

#below doesn't work
#=
function spm(x)
  μ = mean(x)
  x̃ = @view(x[2:end]) .- μ
  Lx̃ = @view(x[1:(end-1)]) .- μ

  r = SARIMA(x,) |> StateSpaceModels.fit! |> results

  ϕ=r[1]

=#
function checkmath(x)
  N=length(x)
  x₁ = x[1]
  xc = @view(x[2:end])
  Lx = @view(x[1:(end-1)])

  ϕ₀ = 0.7
  μ₀ = 2.7
  σ²₀ = 0.6

  L(ϕ, σ², μ) =  (1-ϕ^2)^(1/2)/σ²^(1/2)*pdf(Normal(), (x₁-μ)/(σ²/(1-ϕ^2))^(1/2))*prod(
    [1/σ²^(1/2)*pdf(Normal(), (x[n]-ϕ*x[n-1]-μ*(1-ϕ))/σ²^(1/2)) for n ∈ 2:N])

  l(ϕ, σ², μ) = (-N/2*log(2*π*σ²)
    + 1/2*log(1-ϕ^2)
    - (x₁-μ)^2*(1-ϕ^2)/(2*σ²)
    - sum((xc .- ϕ .* Lx .- μ*(1-ϕ)).^2)/(2*σ²))

  @assert log(L(ϕ₀, σ²₀, μ₀)) ≈ l(ϕ₀, σ²₀, μ₀) "log(L(ϕ, σ², μ)): $(log(L(ϕ₀, σ²₀, μ₀)))
    l(ϕ, σ², μ): $(l(ϕ₀, σ²₀, μ₀))"

  #check the derivitive wrt to mu as well as the conditional
  lμ(ϕ,σ²=σ²₀, μ=μ₀) = (x₁-μ)*(1-ϕ^2)/σ²+sum((xc .- ϕ .* Lx .- μ*(1-ϕ))*(1-ϕ))/σ²
  lμz(ϕ, μ=μ₀) = gradient(μ->l(ϕ, σ²₀, μ), μ)[1]
  @assert lμ(ϕ₀) ≈ lμz(ϕ₀) "lμ(ϕ): $(lμ(ϕ₀))
    lμz(ϕ): $(lμz(ϕ₀)))"

  cμ(ϕ) = (x₁*(1+ϕ)+sum(xc-Lx .* ϕ))/(N*(1-ϕ)+2ϕ)
  @assert 1 + lμz(ϕ₀, cμ(ϕ₀)) ≈ 1.0 "cμ(ϕ₀): 2$(cμ(ϕ₀))
    lμz(ϕ₀, cμ(ϕ₀)): $(lμz(ϕ₀, cμ(ϕ₀)))"

  #now verify the derivitive and conditional wrt sigma^2
  lσ²(ϕ=ϕ₀, σ²=σ²₀, μ=μ₀) = (-N/(2σ²)
    + (x₁-μ)^2*(1-ϕ^2)/(2σ²^2)
    + sum((xc .- ϕ .* Lx .- μ*(1-ϕ)).^2)/(2σ²^2))
  lσ²z(ϕ=ϕ₀, σ²=σ²₀, μ=μ₀) = gradient(σ²->l(ϕ, σ², μ), σ²)[1]
  lσ²n() =  (l(ϕ₀, σ²₀+1e-6, μ₀)-l(ϕ₀, σ²₀, μ₀))/1e-6
  @assert lσ²() ≈ lσ²z() "lσ²(...): $(lσ²())
    lσ²z(...): $(lσ²z())"

  cσ²(ϕ,μ=μ₀) = ((x₁-μ)^2*(1-ϕ^2) + sum((xc .- ϕ*Lx .- μ*(1-ϕ)).^2))/N
  @assert 1 + lσ²z(ϕ₀, cσ²(ϕ₀)) ≈ 1.0 "cσ²(ϕ₀): 2$(cσ²(ϕ₀))
    lσ²z(ϕ₀, cσ²(ϕ₀)): $(lσ²z(ϕ₀, cσ²(ϕ₀)))"

  #finally, verify the derivitive wrt ϕ
  lϕ(ϕ=ϕ₀, σ²=σ²₀, μ=μ₀) = (-ϕ/(1-ϕ^2) +
    (x₁-μ)^2*ϕ/σ² +
    sum((xc .- ϕ * Lx .- μ*(1-ϕ)) .* (Lx .- μ))/σ²)
  lϕz(ϕ=ϕ₀, σ²=σ²₀, μ=μ₀) = gradient(ϕ->l(ϕ, σ², μ), ϕ)[1]
  @assert lϕ() ≈ lϕz() "lϕ(...): $(lϕ())
    lϕz(...): $(lϕz())"

  cϕ(σ²=σ²₀,μ=μ₀) = optimize(ϕ->lϕ(ϕ)^2, -1.0, 1.0, Brent(), abs_tol=10^-14) |> Optim.minimizer
  @assert isapprox(lϕz(cϕ()), 0.0,atol=1e-5) "cϕ(ϕ₀): 2$(cϕ())
    lϕz(cϕ()): $(lϕz(cϕ()))"
  #lμ()
end

function testar1(;N)
  ϕ = 0.8
  ε = rand(Normal(), N) + rand(N)
  x = missings(Float64, N)
  x[1] = ε[1]

  global x = x
  for t ∈ 2:N
    x[t] = ϕ*x[t-1] + ε[t]
  end
  @info "ϕ=$ϕ, b=$(mean(ε)), μ=$(mean(x))"

  checkmath(x)

  estolsar1 = @btime olsar1($x)
  @info "ols conditional: $estolsar1"

  estmlear1 = @btime mlear1($x)
  @info "mle: $estmlear1"

  estmle2ar1 = @btime mle2ar1($x)
  @info "mle2: $estmle2ar1"

  #=estmle3ar1 = @btime mle3ar1($x)
  @info "mle3: $estmle3ar1"

  estmle4ar1 = @btime mle4ar1($x)
  @info "mle4: $estmle4ar1"=#
end

@time testar1(N=250)


export Basis1d, Element1d

type Basis1d
    Q::Int
    z::Array{Float64,1}
    w::Array{Float64,1}
    D::Array{Float64,2}

    function Basis1d(q::Int)
        Q = q
        z = zeros_glj(Q, T=Float64)
        w = weights_glj(z)
        D = diffmat_glj(z)
        new(Q, z, w, D)
    end
end

type Element1d

    basis::Basis1d
    Q::Int
    dx::Float64
    x::Array{Float64,1}
    dzdx::Float64
    jac::Float64
    mass::Array{Float64,1}

    function Element1d(b::Basis1d, xnodes::Array{Float64,1})
        basis = b
        Q = b.Q
        dx = xnodes[2] - xnodes[1]
        x = (1-b.z)/2 * xnodes[1] + (1+b.z)/2 * xnodes[2]
        dzdx = 2 / dx
        jac = dx / 2
        mass = jac * basis.w
        new(basis, Q, dx, x, dzdx, jac, mass)
    end

end

using Base.LinAlg.BLAS

function deriv_z(elem::Element1d, f::Array{Float64,1})
    elem.basis.D * f
end

function deriv_x(elem::Element1d, f::Array{Float64,1})
    elem.dzdx * deriv_z(elem, f)
end

function convective(elem::Element1d, f::Array{Float64,1}, u::Array{Float64,1})

    udf = deriv_x(elem, f)
    for i::Int = 1:elem.Q
        udf[i] *= u[i]
    end
    return udf
end

function integrate(elem::Element1d, f::Array{Float64,1})

    dot(elem.mass, f)
end

function laplace_matrix(elem::Element1d)

    local Q::Int = elem.Q, i::Int, j::Int

end


using BenchmarkTools

"""target area: x=217..240, y=-126..-69"""
function day17(tx1=217, ty1=-126, tx2=240, ty2=-69)
    part = [0, 0]
    maxtuple, yfail = Tuple{Int,Int,Int}[], min(ty1, ty2)
    for i in isqrt(2tx1):tx2, j in ty1:(-ty1 - 1)
        x, y, xvel, yvel, maxy, success = 0, 0, i, j, 0, false
        for t in 1:1000
            x += xvel
            y += yvel
            y > maxy && (maxy = y)
            xvel -= sign(xvel)
            yvel -= 1
            if tx1 <= x <= tx2 && ty1 <= y <= ty2
                if !success
                    push!(maxtuple, (maxy, i, j))
                    success = true
                end
            end
            y < yfail && break
        end
    end
    part[1] = maximum([first(t) for t in maxtuple])
    part[2] = length(maxtuple)
    return part
end

part = day17()
println("Part 1: ", part[1])
println("Part 2: ", part[2])

@btime day17()


using Test
using TightBindingLattice

@testset "irrepdatabase" begin
    # D10 group
    @testset "find" begin
        group1 = FiniteGroup([
            1  2  3  4  5  6  7  8  9  10  11  12  13  14  15  16  17  18  19  20;
            2  3  4  5  6  7  8  9  10  1  20  11  12  13  14  15  16  17  18  19;
            3  4  5  6  7  8  9  10  1  2  19  20  11  12  13  14  15  16  17  18;
            4  5  6  7  8  9  10  1  2  3  18  19  20  11  12  13  14  15  16  17;
            5  6  7  8  9  10  1  2  3  4  17  18  19  20  11  12  13  14  15  16;
            6  7  8  9  10  1  2  3  4  5  16  17  18  19  20  11  12  13  14  15;
            7  8  9  10  1  2  3  4  5  6  15  16  17  18  19  20  11  12  13  14;
            8  9  10  1  2  3  4  5  6  7  14  15  16  17  18  19  20  11  12  13;
            9  10  1  2  3  4  5  6  7  8  13  14  15  16  17  18  19  20  11  12;
            10  1  2  3  4  5  6  7  8  9  12  13  14  15  16  17  18  19  20  11;
            11  12  13  14  15  16  17  18  19  20  1  2  3  4  5  6  7  8  9  10;
            12  13  14  15  16  17  18  19  20  11  10  1  2  3  4  5  6  7  8  9;
            13  14  15  16  17  18  19  20  11  12  9  10  1  2  3  4  5  6  7  8;
            14  15  16  17  18  19  20  11  12  13  8  9  10  1  2  3  4  5  6  7;
            15  16  17  18  19  20  11  12  13  14  7  8  9  10  1  2  3  4  5  6;
            16  17  18  19  20  11  12  13  14  15  6  7  8  9  10  1  2  3  4  5;
            17  18  19  20  11  12  13  14  15  16  5  6  7  8  9  10  1  2  3  4;
            18  19  20  11  12  13  14  15  16  17  4  5  6  7  8  9  10  1  2  3;
            19  20  11  12  13  14  15  16  17  18  3  4  5  6  7  8  9  10  1  2;
            20  11  12  13  14  15  16  17  18  19  2  3  4  5  6  7  8  9  10  1;
        ])
        @test isnothing(IrrepDatabase.find(group1))
        group2 = FiniteGroup([1 2; 2 1])
        (irrep_data, elmap) = IrrepDatabase.find(group2)
        @test irrep_data.group == group2
        @test irrep_data.irreps == [[ones(ComplexF64, 1, 1), ones(ComplexF64, 1, 1)],
                                    [ones(ComplexF64, 1, 1), -ones(ComplexF64, 1, 1)]]
        @test irrep_data.conjugacy_classes == [[1], [2]]
        @test irrep_data.character_table == [1 1; 1 -1]
        @test elmap == [1,2]
    end

    @testset "find_irrep" begin
        (irrep, matrep_new, ϕ) = IrrepDatabase.find_irrep([ [1 0; 0 1], [1 0; 0 -1]])
        @test irrep.group == FiniteGroup([1 2; 2 1])
    end
end


# old versions of the code

"""
`Metropolis` - Sample Ising on a N×N square lattice with spin flip proposals
and Metropolis rates

### Fields
* `x0` - Initial of the lattice
* `β` - Inverse temperature
* `niters` - Number of iterations to run
### Optional Fields
* `J = 1.0` - Coupling constant>0
* `nsave_iters = 1` - Number of iterations between saves
"""
function Metropolis(x0, β, niters; J=1.0, nsave_iters=1)

    N = size(x0)[1];
    x = copy(x0);
    xp = copy(x0);

    x_trajectory = typeof(x0)[];

    E = Energy(x, J = J);

    𝒩 = [zeros(MVector{2,Int8}) for _ in 1:4];

    for n in 1:niters
        i = rand(1:N);
        j = rand(1:N);
        xp[i,j] *= -1;

        # compute local energy contribution
        e = 0.0;
        GetNeighbors!(𝒩, i, j, N);
        for (k,l) in 𝒩
            e += - 0.5 * J * xp[i,j] * xp[k,l];
        end

        # double once for double counting of bonds and again for the sign change
        ΔE = 2 * 2 * e;

        a = min(1, exp(-β * ΔE))
        ζ = rand();
        if ζ < a
            x[i,j] *= -1;
            E += ΔE;
        else
            # restore state if rejected
            xp[i,j] *= -1;
        end
        if(mod(n,nsave_iters) == 0)
            push!(x_trajectory,copy(x));
        end
    end

    return x_trajectory
end

"""
`Metropolis!` - Sample Ising on a N×N square lattice with spin flip proposals
and Metropolis rates.  This is the in place version.

### Fields
* `x` - State of the lattice
* `β` - Inverse temperature
* `niters` - Number of iterations to run
### Optional Fields
* 'J=1.0' - Coupling constant>0
"""
function Metropolis!(x, β, niters; J=1.0)

    N = size(x)[1];
    xp = copy(x)

    E = Energy(x, J = J);

    𝒩 = [zeros(MVector{2,Int8}) for _ in 1:4];

    for n in 1:niters
        i = rand(1:N);
        j = rand(1:N);
        xp[i,j] *= -1;

        # compute local energy contribution
        e = 0.0;
        GetNeighbors!(𝒩, i, j, N);
        for (k,l) in 𝒩
            e += - 0.5 * J * xp[i,j] * xp[k,l];
        end

        # double once for double counting of bonds and again for the sign change
        ΔE = 2 * 2 * e;

        a = min(1, exp(-β * ΔE))
        ζ = rand();
        if ζ < a
            x[i,j] *= -1;
            E += ΔE;
        else
            # restore state if rejected
            xp[i,j] *= -1;
        end
    end
    x
end

"""
`Wolff` - Sample Ising on a N×N square lattice with the Wolff cluster algorithm

### Fields
* `x0` - Initial state of the lattice
* `β` - Inverse temperature
* `niters` - Number of iterations to run
### Optional Fields
* 'J = 1.0' - Coupling constant>0
* `nsave_iters = 1` - Number of iterations between saves
"""
function Wolff(x0, β, niters; J = 1.0, nsave_iters=1)

    p = 1 - exp(-2 * β * J);

    N = size(x0)[1];
    x = copy(x0);

    x_trajectory = typeof(x0)[];

    for n in 1:niters
        i = rand(1:N);
        j = rand(1:N);

        D = x[i,j];
        x[i,j] *=-1;

        k,l = mod1(i+1,N), j;
        if (x[k,l]==D)
            if (rand() < p)
                x[k,l] *=-1;
                FlipNeighbors!(x, k, l, p, D, N)
            end
        end

        k,l = mod1(i-1,N), j;
        if (x[k,l]==D)
            if (rand() < p)
                x[k,l] *=-1;
                FlipNeighbors!(x, k, l, p, D, N)
            end
        end

        k,l = i, mod1(j+1,N);
        if (x[k,l]==D)
            if (rand() < p)
                x[k,l] *=-1;
                FlipNeighbors!(x, k, l, p, D, N)
            end
        end

        k,l = i, mod1(j-1,N);
        if (x[k,l]==D)
            if (rand() < p)
                x[k,l] *=-1;
                FlipNeighbors!(x, k, l, p, D, N)
            end
        end
        if(mod(n,nsave_iters) == 0)
            push!(x_trajectory,copy(x));
        end
    end

    return x_trajectory
end

"""
`Wolff!` - Sample Ising on a N×N square lattice with the Wolff cluster
algorithm.  In place version.

### Fields
* `x` - State of the lattice
* `β` - Inverse temperature
* `niters` - Number of iterations to run
### Optional Fields
* 'J=1.0' - Coupling constant>0
"""
function Wolff!(x, β, niters; J = 1.0)

    p = 1 - exp(-2 * β * J);

    N = size(x)[1];

    for n in 1:niters
        i = rand(1:N);
        j = rand(1:N);

        D = x[i,j];
        x[i,j] *=-1;

        k,l = mod1(i+1,N), j;
        if (x[k,l]==D)
            if (rand() < p)
                x[k,l] *=-1;
                FlipNeighbors!(x, k, l, p, D, N)
            end
        end

        k,l = mod1(i-1,N), j;
        if (x[k,l]==D)
            if (rand() < p)
                x[k,l] *=-1;
                FlipNeighbors!(x, k, l, p, D, N)
            end
        end

        k,l = i, mod1(j+1,N);
        if (x[k,l]==D)
            if (rand() < p)
                x[k,l] *=-1;
                FlipNeighbors!(x, k, l, p, D, N)
            end
        end

        k,l = i, mod1(j-1,N);
        if (x[k,l]==D)
            if (rand() < p)
                x[k,l] *=-1;
                FlipNeighbors!(x, k, l, p, D, N)
            end
        end

    end

    x
end


type Conv2d <: Layer{Float32}
    name::Symbol
    W::Array{Float32, 4} # filter_h × filter_w × in_c × out_c
    b::Vector{Float32} # out_c
    strides::Tuple{Int, Int} # width, height

    parent::Array{Float32, 3} # h × w × in_c
    child::Array{Float32, 3}  # out_h × out_w × out_c
end
get_name(a::Conv2d) = a.name
output(a::Conv2d) = a.child
function add_node!(net::ForwardNet, ::Type{Conv2d},
    name::Symbol,
    parent_index::Int,
    strides::Tuple{Int, Int},
    filter_h::Int,
    filter_w::Int,
    out_c::Int)

    parent = output(net[parent_index])::Array{Float32, 3}
    h, w, in_c = size(parent)

    W = Array{Float32}(filter_h, filter_w, in_c, out_c)
    b = Array{Float32}(out_c)

    # FOR VALID: (padding is always zero)
    # out_w = ceil(float(w - filter_w + 1) / float(strides[2]))
    # out_h = ceil(float(h - filter_h + 1) / float(strides[1]))

    # FOR SAME:
    out_w  =ceil(Int, w / strides[2])
    out_h = ceil(Int, h / strides[1])
    child = Array{Float32}(out_h, out_w, out_c)

    node = Conv2d(name, W, b, strides, parent, child)
    add_node!(net, node, parent_index)
end
function forward!(a::Conv2d)

    h_in = size(a.parent, 1)
    h_out = size(a.child, 1)
    w_in = size(a.parent, 2)
    w_out = size(a.child, 2)
    hh, ww, f_in, f_out = size(a.W)

    s₁, s₂ = a.strides
    pad_h = (s₁ * (h_out - 1) + hh - h_in) / 2
    pad_w = (s₂ * (w_out - 1) + ww - w_in) / 2

    pad_h_lo = floor(Int, pad_h)
    pad_h_hi = ceil(Int, pad_h)
    pad_w_lo = floor(Int, pad_w)
    pad_w_hi = ceil(Int, pad_w)

    # println("parent_size: ", size(a.parent))
    # println("child_size:  ", size(a.child))

    # println("padding: ", pad_h_lo, "  ", pad_h_hi)
    # println("         ", pad_w_lo, "  ", pad_w_hi)

    i_out = 1
    for i_out in 1 : size(a.child, 1)
        i_in_lo = (i_out-1)*s₁ - pad_h_lo + 1
        i_in_hi = i_in_lo + hh - 1

        h_in_lo = 1
        if i_in_lo < 1
            h_in_lo += 1-i_in_lo
            i_in_lo = 1
        end
        h_in_hi = hh
        if i_in_hi > h_in
            h_in_hi -= i_in_hi - h_in
            i_in_hi = h_in
        end

        for j_out in 1 : size(a.child, 2)

            j_in_lo = (j_out-1)*s₂ - pad_w_lo + 1
            j_in_hi = j_in_lo + ww - 1

            # println("j_in_lo: ", j_in_lo)
            # println("j_in_hi: ", j_in_hi)

            w_in_lo = 1
            if j_in_lo < 1
                w_in_lo += 1-j_in_lo
                j_in_lo = 1
            end
            w_in_hi = ww
            if j_in_hi > w_in
                w_in_hi -= j_in_hi - w_in
                j_in_hi = w_in
            end

            # println("j_in_lo: ", j_in_lo)
            # println("j_in_hi: ", j_in_hi)
            # println("w_in_lo: ", w_in_lo)
            # println("w_in_hi: ", w_in_hi)

            for k_out in 1 : f_out

                # println("parent: ", i_in_lo:i_in_hi, ", ", j_in_lo:j_in_hi, ", ", 1:f_in)
                # println("W:      ", h_in_lo:h_in_hi, ", ", w_in_lo,w_in_hi, ", ", 1:f_in, ", ", k_out)

                a.child[i_out,j_out,k_out] = a.b[k_out]

                for (i, i_in) in enumerate(i_in_lo:i_in_hi)
                    h₁ = h_in_lo + i - 1
                    for (j, j_in) in enumerate(j_in_lo:j_in_hi)
                        w₁ = w_in_lo + j - 1
                        for k_in in 1 : f_in
                            W_contrib = a.W[h₁,w₁,k_in,k_out]
                            p_contrib = a.parent[i_in,j_in,k_in]
                            a.child[i_out,j_out,k_out] += W_contrib*p_contrib
                        end
                    end
                end
            end
        end
    end

    a
end
function restore!(a::Conv2d, filename_W::String, filename_b::String)

    filter_h, filter_w, in_c, out_c = size(a.W)

    vec = open(read_binary_vec, filename_W)
    a.W[:] = convert_to_column_major_array(vec, (filter_h, filter_w, in_c, out_c))

    copy!(a.b, open(read_binary_vec, filename_b))

    a
end


###############################################################################
#
# ?
#
###############################################################################
"""
    expint(x::FT) where {FT<:AbstractFloat}

# TODO add function description
"""
function expint(
            x::FT
) where {FT<:AbstractFloat}
    pn = Polynomial(FT[ 8.267661952366478e+00,
                       -7.773807325735529e-01,
                       -3.012432892762715e-01,
                       -7.811863559248197e-02,
                       -1.019573529845792e-02,
                       -6.973790859534190e-04,
                       -2.569498322115933e-05,
                       -4.819538452140960e-07,
                       -3.602693626336023e-09]);
    egamma = FT(0.57721566490153286061);
    polyv  = pn(real(x));
    if abs(imag(x)) <= polyv
        #initialization
        xk = x;
        yk = -egamma - log(xk);
        j = 1;
        pterm = xk;
        term = xk;
        while abs(term) > (eps(yk))
            yk = yk + term;
            j = j + 1;
            pterm = -xk.*pterm/j;
            term = pterm/j;
        end # end of the while loop
        y = yk;
    else
        n = FT(1);
        xk = x;
        am2 = FT(0)
        bm2 = FT(1)
        am1 = FT(1)
        bm1 = xk
        f = am1 / bm1;
        oldf = Inf;
        j = FT(2);

        while abs(f-oldf) > (100*eps(FT)*abs(f))
            alpha = n-1+(j/2); # note: beta= 1
            #calculate A(j), B(j), and f(j)
            a = am1 + alpha * am2;
            b = bm1 + alpha * bm2;

            # save new normalized variables for next pass through the loop
            #  note: normalization to avoid overflow or underflow
            am2 = am1 / b;
            bm2 = bm1 / b;
            am1 = a / b;
            bm1 = FT(1);

            f = am1;
            j = j+1;

            # calculate the coefficients for j odd
            alpha = (j-1)/2;
            beta = xk;
            a = beta * am1 + alpha * am2;
            b = beta * bm1 + alpha * bm2;
            am2 = am1 / b;
            bm2 = bm1 / b;
            am1 = a / b;
            bm1 = 1;
            oldf = f;
            f = am1;
            #println(typeof(xk), typeof(f), typeof(alpha))
            j = j+1;
        end

        y= exp(-xk) * f - 1im*FT(pi)*((real(xk)<0)&(imag(xk)==0));
    end

    return y
end


## Turing Complete Interpreter
### A Very Tiny Turing Complete Programming Language defined with denotational semantics

# semantica dalle dispense degano
using Metatheory

import Base.ImmutableDict
Mem = Dict{Symbol,Union{Bool,Int}}

read_mem = @theory v σ begin
  (v::Symbol, σ::Mem) => if v == :skip
    σ
  else
    σ[v]
  end
end

@testset "Reading Memory" begin
  ex = :((x), $(Mem(:x => 2)))
  @test true == areequal(read_mem, ex, 2)
end

arithm_rules = @theory a b σ begin
  (a + b, σ::Mem) --> (a, σ) + (b, σ)
  (a * b, σ::Mem) --> (a, σ) * (b, σ)
  (a - b, σ::Mem) --> (a, σ) - (b, σ)
  (a::Int, σ::Mem) --> a
  (a::Int + b::Int) => a + b
  (a::Int * b::Int) => a * b
  (a::Int - b::Int) => a - b
end

@testset "Arithmetic" begin
  @test areequal(read_mem ∪ arithm_rules, :((2 + 3), $(Mem())), 5)
end

# don't need to access memory
bool_rules = @theory a b σ begin
  (a < b, σ::Mem) --> (a, σ) < (b, σ)
  (a ∨ b, σ::Mem) --> (a, σ) ∨ (b, σ)
  (a ∧ b, σ::Mem) --> (a, σ) ∧ (b, σ)
  (¬(a), σ::Mem) --> ¬((a, σ))

  (a::Bool, σ::Mem)   => a
  (¬a::Bool)          => !a
  (a::Bool ∨ b::Bool) => (a || b)
  (a::Bool ∧ b::Bool) => (a && b)
  (a::Int < b::Int)   => (a < b)
end

t = read_mem ∪ arithm_rules ∪ bool_rules

@testset "Booleans" begin
  @test areequal(t, :((false ∨ false), $(Mem())), false)

  exx = :((false ∨ false) ∨ ¬(false ∨ false), $(Mem(:x => 2)))
  g = EGraph(exx)
  saturate!(g, t)
  ex = extract!(g, astsize)
  @test ex == true
  params = SaturationParams(timeout = 12)
  @test areequal(t, exx, true; params = params)

  @test areequal(t, :((2 < 3) ∧ (3 < 4), $(Mem(:x => 2))), true)
  @test areequal(t, :((2 < x) ∨ ¬(3 < 4), $(Mem(:x => 2))), false)
  @test areequal(t, :((2 < x) ∨ ¬(3 < 4), $(Mem(:x => 4))), true)
end

if_rules = @theory guard t f σ begin
  (
    if guard
      t
    end
  ) --> (
    if guard
      t
    else
      :skip
    end
  )
  (if guard
    t
  else
    f
  end, σ::Mem) --> (if (guard, σ)
    t
  else
    f
  end, σ)
  (if true
    t
  else
    f
  end, σ::Mem) --> (t, σ)
  (if false
    t
  else
    f
  end, σ::Mem) --> (f, σ)
end

if_language = read_mem ∪ arithm_rules ∪ bool_rules ∪ if_rules

@testset "If Semantics" begin
  @test areequal(if_language, 2, :(if true
    x
  else
    0
  end, $(Mem(:x => 2))))
  @test areequal(if_language, 0, :(if false
    x
  else
    0
  end, $(Mem(:x => 2))))
  @test areequal(if_language, 2, :(if ¬(false)
    x
  else
    0
  end, $(Mem(:x => 2))))
  params = SaturationParams(timeout = 10)
  @test areequal(if_language, 0, :(if ¬(2 < x)
    x
  else
    0
  end, $(Mem(:x => 3))); params = params)
end

while_rules = @theory a b σ begin
  (:skip, σ::Mem) --> σ
  ((a; b), σ::Mem) --> ((a, σ); b)
  (a::Int; b) --> b
  (a::Bool; b) --> b
  (σ::Mem; b) --> (b, σ)
  (while a
    b
  end, σ::Mem) --> (if a
    (b;
    while a
      b
    end)
  else
    :skip
  end, σ)
end

write_mem = @theory sym val σ begin
  (sym::Symbol = val, σ::Mem) --> (sym = (val, σ), σ)
  (sym::Symbol = val::Int, σ::Mem) => merge(σ, Dict(sym => val))
end

while_language = if_language ∪ write_mem ∪ while_rules;

@testset "While Semantics" begin
  exx = :((x = 3), $(Mem(:x => 2)))
  g = EGraph(exx)
  saturate!(g, while_language)
  ex = extract!(g, astsize)

  @test areequal(while_language, Mem(:x => 3), exx)

  exx = :((x = 4; x = x + 1), $(Mem(:x => 3)))
  g = EGraph(exx)
  saturate!(g, while_language)
  ex = extract!(g, astsize)

  params = SaturationParams(timeout = 10)
  @test areequal(while_language, Mem(:x => 5), exx; params = params)

  params = SaturationParams(timeout = 14)
  exx = :((
    if x < 10
      x = x + 1
    else
      skip
    end
  ), $(Mem(:x => 3)))
  @test areequal(while_language, Mem(:x => 4), exx; params = params)

  exx = :((while x < 10
    x = x + 1
  end;
  x), $(Mem(:x => 3)))
  g = EGraph(exx)
  params = SaturationParams(timeout = 100, scheduler = Schedulers.SimpleScheduler)
  saturate!(g, while_language, params)
  @test 10 == extract!(g, astsize)
end


using Plots
plotlyjs()
include("FoggyMountainObj.jl")

n = 200
x = range(-20., stop=20., length=n)
y = x

##
p1 = surface(x,y,(x,y)->-FoggyMountainObj(x,y))
savefig("FoggySurface.png")
gui()

##
p2 = contour(x,y,(x,y)->-FoggyMountainObj(x,y))
gui()
savefig("FoggyContour.png")


export LaurentPolynomial

"""
    LaurentPolynomial{T,X}(coeffs::AbstractVector, [m::Integer = 0], [var = :x])

A [Laurent](https://en.wikipedia.org/wiki/Laurent_polynomial) polynomial is of the form `a_{m}x^m + ... + a_{n}x^n` where `m,n` are  integers (not necessarily positive) with ` m <= n`.

The `coeffs` specify `a_{m}, a_{m-1}, ..., a_{n}`.
The argument `m` represents the lowest exponent of the variable in the series, and is taken to be zero by default.

Laurent polynomials and standard basis polynomials promote to  Laurent polynomials. Laurent polynomials may be  converted to a standard basis  polynomial when `m >= 0`
.

Integration will fail if there is a `x⁻¹` term in the polynomial.

!!! note
    `LaurentPolynomial` is not axis-aware by default, and it treats `coeffs` simply as a
    list of coefficients with the first index always corresponding to the constant term.
    In order to use the axis of `coeffs` as the exponents of the variable `var`,
    set `m` to `firstindex(coeff)` in the constructor.

# Examples:
```jldoctest laurent
julia> using Polynomials

julia> P = LaurentPolynomial
LaurentPolynomial

julia> p = P([1,1,1],  -1)
LaurentPolynomial(x⁻¹ + 1 + x)

julia> q = P([1,1,1])
LaurentPolynomial(1 + x + x²)

julia> pp = Polynomial([1,1,1])
Polynomial(1 + x + x^2)

julia> p + q
LaurentPolynomial(x⁻¹ + 2 + 2*x + x²)

julia> p * q
LaurentPolynomial(x⁻¹ + 2 + 3*x + 2*x² + x³)

julia> p * pp
LaurentPolynomial(x⁻¹ + 2 + 3*x + 2*x² + x³)

julia> pp - q
LaurentPolynomial(0)

julia> derivative(p)
LaurentPolynomial(-x⁻² + 1)

julia> integrate(q)
LaurentPolynomial(1.0*x + 0.5*x² + 0.3333333333333333*x³)

julia> integrate(p)  # x⁻¹  term is an issue
ERROR: ArgumentError: Can't integrate Laurent  polynomial with  `x⁻¹` term

julia> integrate(P([1,1,1], -5))
LaurentPolynomial(-0.25*x⁻⁴ - 0.3333333333333333*x⁻³ - 0.5*x⁻²)

julia> x⁻¹ = inv(variable(LaurentPolynomial)) # `inv` defined on monomials
LaurentPolynomial(1.0*x⁻¹)

julia> p = Polynomial([1,2,3])
Polynomial(1 + 2*x + 3*x^2)

julia> x = variable()
Polynomial(x)

julia> x^degree(p) * p(x⁻¹) # reverses  coefficients
LaurentPolynomial(3.0 + 2.0*x + 1.0*x²)
```
"""
struct LaurentPolynomial{T <: Number, X} <: StandardBasisPolynomial{T, X}
    coeffs::Vector{T}
    m::Base.RefValue{Int}
    n::Base.RefValue{Int}
    function LaurentPolynomial{T,X}(coeffs::AbstractVector{S},
                                    m::Union{Int, Nothing}=nothing) where {T <: Number, X, S}

        fnz = findfirst(!iszero, coeffs)
        fnz == nothing && return  new{T,X}(zeros(T,1), Ref(0), Ref(0))
        lnz = findlast(!iszero, coeffs)
        if Base.has_offset_axes(coeffs)
            # if present, use axes
            cs = convert(Vector{T}, coeffs[fnz:lnz])
            return new{T,X}(cs, Ref(fnz), Ref(lnz))
        else

            c = convert(Vector{T}, coeffs[fnz:lnz])

            m′ = fnz - 1 + (m == nothing ? 0 : m)
            n = m′ + (lnz-fnz)

            (n - m′ + 1  == length(c)) || throw(ArgumentError("Lengths do not match"))
            new{T,X}(c, Ref(m′),  Ref(n))
        end
    end
end

@register LaurentPolynomial

## constructors
function LaurentPolynomial{T}(coeffs::AbstractVector{S}, m::Int, var::SymbolLike=:x) where {
    T <: Number, S <: Number}
    LaurentPolynomial{T,Symbol(var)}(T.(coeffs), m)
end

function LaurentPolynomial{T}(coeffs::AbstractVector{T}, var::SymbolLike=:x) where {
    T <: Number}
    LaurentPolynomial{T, Symbol(var)}(coeffs, 0)
end

function LaurentPolynomial(coeffs::AbstractVector{T}, m::Int, var::SymbolLike=:x) where {T <: Number}
    LaurentPolynomial{T, Symbol(var)}(coeffs, m)
end

##
## conversion
##

# LaurentPolynomial is a wider collection than other standard basis polynomials.
Base.promote_rule(::Type{P},::Type{Q}) where {T, X, P <: LaurentPolynomial{T,X}, S, Q <: StandardBasisPolynomial{S, X}} = LaurentPolynomial{promote_type(T, S), X}

Base.promote_rule(::Type{Q},::Type{P}) where {T, X, P <: LaurentPolynomial{T,X}, S, Q <: StandardBasisPolynomial{S,X}} =
    LaurentPolynomial{promote_type(T, S),X}

function Base.convert(P::Type{<:Polynomial}, q::LaurentPolynomial)
    m,n = (extrema∘degreerange)(q)
    m < 0 && throw(ArgumentError("Can't convert a Laurent polynomial with m < 0"))
    P([q[i] for i  in 0:n], indeterminate(q))
end

# need to add p.m[], so abstract.jl method isn't sufficent
function Base.convert(::Type{P}, p::LaurentPolynomial) where {P<:LaurentPolynomial}
    S′ = _eltype(P)
    Y′ = _indeterminate(P)
    S = S′ == nothing ? eltype(p) : S′
    Y = Y′ == nothing ? indeterminate(p) : Y′
    isconstant(p) && return LaurentPolynomial{S,Y}(constantterm(p))
    LaurentPolynomial{S,Y}(p.coeffs, p.m[])
end

function Base.convert(::Type{P}, q::StandardBasisPolynomial{S}) where {T, P <:LaurentPolynomial{T},S}
    v′ = _indeterminate(P)
    X = v′ == nothing ? indeterminate(q) : v′
    ⟒(P){T,X}([q[i] for i in 0:degree(q)], 0)
end

##
## generic functions
##

function Base.inv(p::LaurentPolynomial{T, X}) where {T, X}
    m,n =  (extrema∘degreerange)(p)
    m != n && throw(ArgumentError("Only monomials can be inverted"))
    cs = [1/p for p in p.coeffs]
    LaurentPolynomial{eltype(cs), X}(cs, -m)
end

##
## changes to common.jl mostly as the range in the type is different
##
Base.:(==)(p1::LaurentPolynomial, p2::LaurentPolynomial) =
    check_same_variable(p1, p2) && (degreerange(chop!(p1)) == degreerange(chop!(p2))) && (coeffs(p1) == coeffs(p2))
Base.hash(p::LaurentPolynomial, h::UInt) = hash(indeterminate(p), hash(degreerange(p), hash(coeffs(p), h)))

isconstant(p::LaurentPolynomial) = iszero(lastindex(p)) && iszero(firstindex(p))

basis(P::Type{<:LaurentPolynomial{T,X}}, n::Int) where {T,X} = LaurentPolynomial{T,X}(ones(T,1), n)

# like that in common, only return zero if idx < firstindex(p)
function Base.getindex(p::LaurentPolynomial{T}, idx::Int) where {T}
    m,M = firstindex(p), lastindex(p)
    m <= idx <= M || return zero(T)
    p.coeffs[idx-m+1]
 end

# extend if out of bounds
function Base.setindex!(p::LaurentPolynomial{T}, value::Number, idx::Int) where {T}

    m,n = (extrema ∘ degreerange)(p)
    if idx > n
        append!(p.coeffs, zeros(T, idx-n))
        n = idx
        p.n[] = n
    elseif idx < m
        prepend!(p.coeffs, zeros(T, m-idx))
        m = idx
        p.m[] = m
    end

    i = idx - m + 1
    p.coeffs[i] = value

    return p

end

Base.firstindex(p::LaurentPolynomial) = p.m[]
Base.lastindex(p::LaurentPolynomial) = p.n[]
Base.eachindex(p::LaurentPolynomial) = degreerange(p)
degreerange(p::LaurentPolynomial) = firstindex(p):lastindex(p)

_convert(p::P, as) where {T,X,P <: LaurentPolynomial{T,X}} = ⟒(P)(as, firstindex(p), X)

## chop!
# trim  from *both* ends
function chop!(p::LaurentPolynomial{T};
               rtol::Real = Base.rtoldefault(real(T)),
               atol::Real = 0,) where {T}

    m0,n0 = m,n = (extrema ∘ degreerange)(p)
    for k in n:-1:m
        if isapprox(p[k], zero(T); rtol = rtol, atol = atol)
            n -= 1
        else
            break
        end
    end
    for k in m:n-1
        if isapprox(p[k], zero(T); rtol = rtol, atol = atol)
            m += 1
        else
            break
        end
    end

    cs = copy(coeffs(p))
    rng = m-m0+1:n-m0+1
    resize!(p.coeffs, length(rng))
    p.coeffs[:] = cs[rng]
    isempty(p.coeffs) && push!(p.coeffs,zero(T))
    p.m[], p.n[] = m, max(m,n)

    p

end

# use unicode exponents. XXX modify printexponent to always use these?
function showterm(io::IO, ::Type{<:LaurentPolynomial}, pj::T, var, j, first::Bool, mimetype) where {T}
    if iszero(pj) return false end
    pj = printsign(io, pj, first, mimetype)
    if !(pj == one(T) && !(showone(T) || j == 0))
        printcoefficient(io, pj, j, mimetype)
    end
    printproductsign(io, pj, j, mimetype)
    iszero(j) && return  true
    print(io, var)
    j ==1  && return  true
    unicode_exponent(io, j)
    return true
end

##
## ---- Conjugation has different defintions
##

"""
    conj(p)

This satisfies `conj(p(x)) = conj(p)(conj(x)) = p̄(conj(x))` or `p̄(x) = (conj ∘ p ∘ conj)(x)`

Examples

```jldoctest laurent
julia> using Polynomials;

julia> z = variable(LaurentPolynomial, :z)
LaurentPolynomial(1.0*z)

julia> p = LaurentPolynomial([im, 1+im, 2 + im], -1, :z)
LaurentPolynomial(im*z⁻¹ + 1 + im + (2 + im)z)

julia> conj(p)(conj(z)) ≈ conj(p(z))
true

julia> conj(p)(z) ≈ (conj ∘ p ∘ conj)(z)
true
```
"""
LinearAlgebra.conj(p::P) where {P <: LaurentPolynomial} = map(conj, p)

"""
    paraconj(p)

[cf.](https://ccrma.stanford.edu/~jos/filters/Paraunitary_FiltersC_3.html)

Call `p̂ = paraconj(p)` and `p̄` = conj(p)`, then this satisfies
`conj(p(z)) = p̂(1/conj(z))` or `p̂(z) = p̄(1/z) = (conj ∘ p ∘ conj ∘ inf)(z)`.

Examples:

```jldoctest laurent
julia> using Polynomials;

julia> z = variable(LaurentPolynomial, :z)
LaurentPolynomial(z)

julia> h = LaurentPolynomial([1,1], -1:0, :z)
LaurentPolynomial(z⁻¹ + 1)

julia> Polynomials.paraconj(h)(z) ≈ 1 + z ≈ LaurentPolynomial([1,1], 0:1, :z)
true

julia> h = LaurentPolynomial([3,2im,1], -2:0, :z)
LaurentPolynomial(3*z⁻² + 2im*z⁻¹ + 1)

julia> Polynomials.paraconj(h)(z) ≈ 1 - 2im*z + 3z^2 ≈ LaurentPolynomial([1, -2im, 3], 0:2, :z)
true

julia> Polynomials.paraconj(h)(z) ≈ (conj ∘ h ∘ conj ∘ inv)(z)
true
"""
function paraconj(p::LaurentPolynomial)
    cs = p.coeffs
    ds = adjoint.(cs)
    n = degree(p)
    LaurentPolynomial(reverse(ds), -n, indeterminate(p))
end

"""
    cconj(p)

Conjugation of a polynomial with respect to the imaginary axis.

The `cconj` of a polynomial, `p̃`, conjugates the coefficients and applies `s -> -s`. That is `cconj(p)(s) = conj(p)(-s)`.

This satisfies for *imaginary* `s`: `conj(p(s)) = p̃(s) = (conj ∘ p)(s) = cconj(p)(s) `

[ref](https://github.com/hurak/PolynomialEquations.jl#symmetrix-conjugate-equation-continuous-time-case)

Examples:
```jldoctest laurent
julia> using Polynomials;

julia> s = 2im
0 + 2im

julia> p = LaurentPolynomial([im,-1, -im, 1], 1, :s)
LaurentPolynomial(im*s - s² - im*s³ + s⁴)

julia> Polynomials.cconj(p)(s) ≈ conj(p(s))
true

julia> a = LaurentPolynomial([-0.12, -0.29, 1],:s)
LaurentPolynomial(-0.12 - 0.29*s + 1.0*s²)

julia> b = LaurentPolynomial([1.86, -0.34, -1.14, -0.21, 1.19, -1.12],:s)
LaurentPolynomial(1.86 - 0.34*s - 1.14*s² - 0.21*s³ + 1.19*s⁴ - 1.12*s⁵)

julia> x = LaurentPolynomial([-15.5, 50.0096551724139, 1.19], :s)
LaurentPolynomial(-15.5 + 50.0096551724139*s + 1.19*s²)

julia> Polynomials.cconj(a) * x + a * Polynomials.cconj(x) ≈ b + Polynomials.cconj(b)
true
```

"""
function cconj(p::LaurentPolynomial)
    ps = conj.(coeffs(p))
    m,n = (extrema ∘ degreerange)(p)
    for i in m:n
        if isodd(i)
            ps[i+1-m] *= -1
        end
    end
    LaurentPolynomial(ps, m, indeterminate(p))
end

##
## ----
##

# evaluation uses `evalpoly`
function evalpoly(x::S, p::LaurentPolynomial{T}) where {T,S}
    xᵐ = firstindex(p) < 0 ? inv(x)^(abs(firstindex(p))) : (x/1)^firstindex(p) # make type stable
    return EvalPoly.evalpoly(x, p.coeffs) * xᵐ
end

# scalar operations
# needed as standard-basis defn. assumes basis 1, x, x², ...
function Base.:+(p::LaurentPolynomial{T,X}, c::S) where {T, X, S <: Number}
    R = promote_type(T,S)
    q = LaurentPolynomial{R,X}(p.coeffs, firstindex(p))
    q[0] += c
    q
end

##
## Poly + and  *
##
function Base.:+(p1::P, p2::P) where {T,X,P<:LaurentPolynomial{T,X}}

    isconstant(p1) && return constantterm(p1) + p2
    isconstant(p2) && return p1 + constantterm(p2)

    m1,n1 = (extrema ∘ degreerange)(p1)
    m2,n2 = (extrema ∘ degreerange)(p2)
    m,n = min(m1,m2), max(n1, n2)

    as = zeros(T, length(m:n))
    for i in m:n
        as[1 + i-m] = p1[i] + p2[i]
    end

    q = P(as, m)
    chop!(q)

    return q

end

function Base.:*(p1::P, p2::P) where {T,X,P<:LaurentPolynomial{T,X}}

    m1,n1 = (extrema ∘ degreerange)(p1)
    m2,n2 = (extrema ∘ degreerange)(p2)
    m,n = m1 + m2, n1+n2

    as = zeros(T, length(m:n))
    for i in eachindex(p1)
        p1ᵢ = p1[i]
        for j in eachindex(p2)
            as[1 + i+j - m] = muladd(p1ᵢ, p2[j], as[1 + i + j - m])
        end
    end

    p = P(as, m)
    chop!(p)

    return p
end

##
## roots
##
"""
    roots(p)

Compute the roots of the Laurent polynomial `p`.

The roots of a function (Laurent polynomial in this case) `a(z)` are the values of `z` for which the function vanishes. A Laurent polynomial ``a(z) = a_m z^m + a_{m+1} z^{m+1} + ... + a_{-1} z^{-1} + a_0 + a_1 z + ... + a_{n-1} z^{n-1} + a_n z^n`` can equivalently be viewed as a rational function with a multiple singularity (pole) at the origin. The roots are then the roots of the numerator polynomial. For example, ``a(z) = 1/z + 2 + z`` can be written as ``a(z) = (1+2z+z^2) / z`` and the roots of `a` are the roots of ``1+2z+z^2``.

# Example

```julia
julia> using Polynomials;

julia> p = LaurentPolynomial([24,10,-15,0,1],-2:1,:z)
LaurentPolynomial(24*z⁻² + 10*z⁻¹ - 15 + z²)

julia> roots(a)
4-element Array{Float64,1}:
 -3.999999999999999
 -0.9999999999999994
  1.9999999999999998
  2.9999999999999982
```
"""
function  roots(p::P; kwargs...)  where  {T, X, P <: LaurentPolynomial{T, X}}
    c = coeffs(p)
    r = degreerange(p)
    d = r[end] - min(0, r[1]) + 1    # Length of the coefficient vector, taking into consideration
                                     # the case when the lower degree is strictly positive
                                     # (like p=3z^2).
    z = zeros(T, d)                  # Reserves space for the coefficient vector.
    z[max(0, r[1]) + 1:end] = c      # Leaves the coeffs of the lower powers as zeros.
    a = Polynomial{T,X}(z)           # The root is then the root of the numerator polynomial.
    return roots(a; kwargs...)
end

##
## d/dx, ∫
##
function derivative(p::P, order::Integer = 1) where {T, X, P<:LaurentPolynomial{T,X}}

    order < 0 && throw(ArgumentError("Order of derivative must be non-negative"))
    order == 0 && return p

    hasnan(p) && return ⟒(P)(T[NaN], 0, X)

    m,n = (extrema ∘ degreerange)(p)
    m = m - order
    n = n - order
    as =  zeros(T, length(m:n))

    for k in eachindex(p)
        idx = 1 + k - order - m
        if 0 ≤ k ≤ order - 1
            as[idx] = zero(T)
        else
            as[idx] = reduce(*, (k - order + 1):k, init = p[k])
        end
    end

    chop!(LaurentPolynomial{T,X}(as, m))

end

function integrate(p::P) where {T, X, P<: LaurentPolynomial{T, X}}

    !iszero(p[-1])  && throw(ArgumentError("Can't integrate Laurent  polynomial with  `x⁻¹` term"))
    R = eltype(one(T)/1)
    Q = ⟒(P){R, X}

    if hasnan(p)
        return Q([NaN],0)
    end

    m,n = (extrema ∘ degreerange)(p)
    if  n < 0
        n = 0
    else
        n += 1
    end
    if m < 0
        m += 1
    else
        m = 0
    end
    as = zeros(R,  length(m:n))

    for k in eachindex(p)
        as[1 + k+1-m]  =  p[k]/(k+1)
    end

    return Q(as, m)

end

function Base.gcd(p::LaurentPolynomial{T,X}, q::LaurentPolynomial{T,Y}, args...; kwargs...) where {T,X,Y}
    mp, Mp = (extrema ∘ degreerange)(p)
    mq, Mq = (extrema ∘ degreerange)(q)
    if mp < 0 || mq < 0
        throw(ArgumentError("GCD is not defined when there are `x⁻ⁿ` terms"))
    end

    degree(p) == 0 && return iszero(p) ? q : one(q)
    degree(q) == 0 && return iszero(q) ? p : one(p)
    assert_same_variable(p,q)

    pp, qq = convert(Polynomial, p), convert(Polynomial, q)
    u = gcd(pp, qq, args..., kwargs...)
    return LaurentPolynomial(coeffs(u), X)
end


function f!(resid, x)
    for i in eachindex(x)
        resid[i] = sin(x[i]) + x[i]^3
    end
end
x = ones(5)
@test Sundials.kinsol(f!, x, linear_solver = :Band, jac_upper = 0, jac_lower = 0) ==
      Sundials.kinsol(f!, x)


function seat_to_bin(seat_str::String, letters::Tuple{String,String})::String
    for (i, letter) in enumerate(letters)
        seat_str = replace(seat_str, letter => i - 1)
    end
    return seat_str
end

function parse_ticket(ticket_str::String)::Tuple{Int,Int}
    row = parse(Int, seat_to_bin(ticket_str[1:7], ("F", "B")), base = 2)
    seat = parse(Int, seat_to_bin(ticket_str[8:10], ("L", "R")), base = 2)
    return (row, seat)
end

function seat_id(ticket_str::String)::Int
    (row, seat) = parse_ticket(ticket_str)
    return 8 * row + seat
end

function solve_one()
    return maximum(map(seat_id, readlines("data/day_five.txt")))
end

function solve_two()
    ids = map(seat_id, readlines("data/day_five.txt"))
    expected_tot = (length(ids) + 1) * (maximum(ids) + minimum(ids)) / 2
    return expected_tot - sum(ids)
end

println(solve_one())
println(solve_two())


include("../Happy-Number/HN.jl")

function addDigit(n::Int) ::Int
    n<0&& return -1
    n in range(0,stop=9) && return n
    n >= 10 && addDigit(sum(eachBit(n)))
end


#######################################
###### Monomials and polynomials ######
#######################################

#  An SoSMonom represents a monomial: it maps each symbol to its exponent.
#  Exponents should always be strictly positive (never 0).
typealias SoSMonom Dict{Symbol,Int64}

#  An SoSPoly maps each monomial to its coefficient.
typealias SoSPoly Dict{SoSMonom,Float64}

#    there should be a faster option here that manages iterators, a la mergesort
macro dictplus(a, b)
    quote
        # A, including overlap with B
        map = Dict([ k => v + get(b,k,0) for (k,v) in a ])
        # B \ A
        for (k,v) in b
            if !haskey(a,k)
                map[k] = v
            end
        end
        map
    end
end

#  Multiplication of monomials.
*(a::SoSMonom,b::SoSMonom) = @dictplus(a,b)
+(a::SoSPoly,b::SoSPoly) = @dictplus(a,b)

#  Addition in-place of polynomials
function addpoly!(a::SoSPoly, b::SoSPoly)
    for (k,v) in b
        a[k] = get(a,k,0.0) + v
    end
end

#  Multiplication of polynomials -- just the naive method
function *(a::SoSPoly, b::SoSPoly)
    map = SoSPoly()
    for (k1,v1) in a
        for (k2,v2) in b
            kp = k1 * k2
            map[kp] = get(map,kp,0) + v1 * v2
        end
    end
    return map
end

#  Degree of monomial / polynomial
deg(m :: SoSMonom) = sum(values(m))
deg(p :: SoSPoly) = (length(p) == 0) ? 0 : maximum([deg(k) for (k,v) in p])

#  Enumerate all monomials of degree d.
#    Julia knows about combinations (without replacement), and we
#    want to choose variables with replacement. We simulate this
#    via (v+d-1 choose d).
monoms(varr :: Array{Symbol}, d :: Int64) =
    [ combToMonom(varr,co) for co in combinations(1:(length(varr)+d-1),d) ] :: Array{SoSMonom,1}
monoms(d :: Int64) = (d == 0) ? [SoSMonom()] : Array{SoSMonom,1}() # edge case

function combToMonom(varr :: Array{Symbol}, co :: Array{Int64})
    m = Dict{Symbol,Int64}() :: SoSMonom
    shift = 0
    for i in co
        k = varr[i+shift]
        m[k] = 1 + get(m,k,0);
        shift -= 1;
    end
    m
end

# Find all decompositions of mon into two monomials (m1,m2), each of degree
# at most degmax.
# XXX currently unused
function decomp(mon :: SoSMonom, degmax :: Int64)
    # maybe there's room for improvement here ...
    list = Tuple{SoSMonom,SoSMonom}[]
    varr = [k for (k,v) in mon]

    # for each candidate m1, see if its complement m2 is sensible
    mondeg = deg(mon)
    dmin = max(0,mondeg-degmax)
    dmax = min(mondeg,degmax)
    for deg in dmin:dmax
        for m1 in monoms(varr,deg)

            # construct the complement
            m2 = Dict([ k => (v-get(m1,k,0)) for (k,v) in mon ])

            # test for being sensible (ie non-negative exponents), and remove any zero entries
            if(length(m2) > 0 && minimum(values(m2)) < 0)
                continue
            end

            # remove any zero entries
            filter!((k,v)->(v!=0), m2)

            push!(list, (m1,m2))
        end
    end

    list
end


using Base.Test
using QuantumOptics

fockbasis = FockBasis(40)
spinbasis = SpinBasis(1//2)

alpha = 0.5
a = full(destroy(fockbasis))
at = full(create(fockbasis))
n = full(number(fockbasis))

sx = full(sigmax(spinbasis))
sy = full(sigmay(spinbasis))
sz = full(sigmaz(spinbasis))
sp = full(sigmap(spinbasis))
sm = full(sigmam(spinbasis))

xket = coherentstate(fockbasis, alpha)
xbra = dagger(xket)
op1 = DenseOperator(spinbasis, GenericBasis([3]), [1 1 1; 1 1 1])
op2 = DenseOperator(GenericBasis([3]), spinbasis, [1 1; 1 1; 1 1])
I = dense_identity(fockbasis)

# Test creation
@test_throws DimensionMismatch DenseOperator(spinbasis, [1 1 1; 1 1 1])
@test_throws DimensionMismatch DenseOperator(spinbasis, FockBasis(3), [1 1; 1 1; 1 1])
@test_approx_eq 0. maximum(abs((dagger(op1)-op2).data))

# Test addition
@test_approx_eq 0. tracedistance(sx, sp + sm)
@test_throws bases.IncompatibleBases op1+op2

# Test substraction
@test_approx_eq 0. tracedistance(sy, -1im*(sp - sm))
@test_throws bases.IncompatibleBases op1-op2

# Test multiplication
@test_approx_eq 0. norm(I*xket - xket)
@test_approx_eq 0. norm(xbra*I - xbra)
@test_approx_eq alpha norm(a*xket)
@test_approx_eq alpha norm(xbra*at)
@test_approx_eq_eps 0. tracedistance(n, at*a) 1e-13
@test_approx_eq_eps 0. norm((5. * at)*xket - 5 * (at*xket)) 1e-13
@test_approx_eq_eps 0. norm((at * 5.)*xket - (at*xket) * 5) 1e-13
@test_throws bases.IncompatibleBases a*op1

# Test division
@test_approx_eq_eps 0. norm((at/5.)*xket - (at*xket)/5) 1e-13

# Test trace and normalize
op = DenseOperator(GenericBasis([3]), [1 3 2;5 2 2;-1 2 5])
@test_approx_eq 8. trace(op)
op_normalized = normalize(op)
@test_approx_eq 8. trace(op)
@test_approx_eq 1. trace(op_normalized)
op_ = normalize!(op)
@test op_ === op
@test_approx_eq 1. trace(op)

# Test operator exponential
b = GenericBasis([3])
op = DenseOperator(GenericBasis([3]), [1 3 2;5 2 2;-1 2 5])
op = op + dagger(op)
op /= norm(op.data)
v = SubspaceBasis(b, eigenstates_hermitian(op))
P = projector(v, b)
op_diag = P*op*dagger(P)
op_diag_exp = DenseOperator(v, diagm(exp(diag(op_diag.data))))
@test_approx_eq_eps 0. tracedistance(expm(op), dagger(P)*op_diag_exp*P) 1e-13

# Test identity function
@test full(I) == dense_identity(a)

# Test gemv implementation
result_ket = deepcopy(xket)
operators.gemv!(complex(1.0), at, xket, complex(0.), result_ket)
@test_approx_eq 0. norm(result_ket-at*xket)

result_bra = deepcopy(xbra)
operators.gemv!(complex(1.0), xbra, at, complex(0.), result_bra)
@test_approx_eq 0. norm(result_bra-xbra*at)


module Signals

using Statistics
include("DSP.jl")

"""

Output xN after adding noise to x

SNR:: in decibel scale
"""
function addnoise!(xN, x, SNR)

	# store zero mean version of x in xN
	xavg=0.0
	for i in eachindex(x)
		xavg += x[i]
	end
	xavg /= length(x)

	for i in eachindex(x)
		xN[i] = x[i] - xavg
	end

	σx=var(xN)

	σxN=sqrt(σx^2*inv(10^(SNR/10.)))

	# factor to be multiplied to each scalar
	α=sqrt(σxN)
	for i in eachindex(x)
		xN[i] = x[i] + α*randn()
	end
end

"""
Construct Toy Green's functions
Decaying peaks, control number of events, and their positions, depending on bfrac and efrac.
* `afrac` : control amplitude of peaks
"""
function toy_reflec_green!(x; bfrac=0.0, c=1.0, afrac=1.0)
	nt=size(x,1)
	nr=size(x,2)
	c=c*sqrt((nr^2)/(nt-1))

	it0=1+round(Int,bfrac*nt)
	for ir in 1:nr
		it=round(Int, it0+(ir*ir)*inv(c)*inv(c))
		if(it<=nt)
			x[it, ir]=1.0*afrac
		end
	end
	return x
end

function toy_direct_green!(x; bfrac=0.0, c=1.0, afrac=1.0)
	nt=size(x,1)
	nr=size(x,2)
	c=c*((nr)/(nt-1))

	it0=1+round(Int,bfrac*nt)
	for ir in 1:nr
		it=round(Int, it0+(ir)*inv(c))
		if(it<=nt)
			x[it, ir]=1.0*afrac
		end
	end
	return x

end

#=

	a=1+round(Int,bfrac*nt)
	b=nt-round(Int,efrac*nt)
	(b-a<nevents+2) && error("not enough samples")

	mult_shift=zeros(Int64,nevents+1)
	itvec=zeros(Int64,nevents+1)
	for ir in 1:nr
		itvec[:]=0
		for ie in 1:nevents
			if(ie==1)
				# first event, direct arrival
				itvec[ie]=rand(DiscreteUniform(a,b-nevents-1))
			else
				# all other events after first event
				itvec[ie]=rand(DiscreteUniform(itvec[1]+1,b-1))
				if(mult_shift[ie]==0)
					mult_shift[ie]=rand(DiscreteUniform(1,b-1-itvec[1]))
				end

			end
			sgn=1.
			# randomly choose sign for all other events
			if(ie≠1)
				sgn=(rand(Bernoulli())==1) ? 1. : -1.
			end
			x[itvec[ie],ir]=sgn*afrac[ie]
		end
	end
	return x
end

=#

end # module


import Base: @kwdef

_parse_seed(seed::String) = hex2bytes(seed)
_parse_seed(seed::Vector{UInt8}) = seed

_parse_bits(x::String, N::Int) = hex2bits(x)[end - N + 1:end]
_parse_bits(x::BitVector, m::Int) = x
_parse_bits(x::Nothing, m::Int) = x

_parse_int(x::String) = parse(BigInt, x, base=16)
_parse_int(x::Integer) = BigInt(x)
_parse_int(::Nothing) = nothing

function spec(::Type{P}; kwargs...) where P <: AbstractPoint

    n = hasmethod(order, Tuple{Type{P}}) ? order(P) : nothing
    h = hasmethod(cofactor, Tuple{Type{P}}) ? cofactor(P) : nothing

    EQ = eq(P)
    F = field(P)

    return spec(EQ, F; n, h, kwargs...)
end

spec(p::P) where P <: AbstractPoint = spec(P; Gx=value(gx(p)), Gy=value(gy(p)))

spec(::Type{ECGroup{P}}) where P = spec(P)
spec(g::ECGroup) = spec(g.x)

@kwdef struct ECP <: Spec
    p::BigInt
    n::Union{BigInt, Nothing} = nothing
    a::BigInt = -3
    b::BigInt
    Gx::Union{BigInt, Nothing} = nothing
    Gy::Union{BigInt, Nothing} = nothing

    function ECP(p, n, a, b, Gx, Gy)

        _a = mod(_parse_int(a), p) # taking mod as conventually a=-3
        _b = _parse_int(b)
        _Gx = _parse_int(Gx)
        _Gy = _parse_int(Gy)

        #return ECP(p, n, _a, _b, _Gx, _Gy)
        return new(p, n, _a, _b, _Gx, _Gy)
    end
end

order(curve::ECP) = curve.n
generator(curve::ECP) = (curve.Gx, curve.Gy)

modulus(curve::ECP) = curve.p

# I could always add a field for equation to be used

function spec(::Type{EQ}, ::Type{F}; n=nothing, h=nothing, Gx=nothing, Gy=nothing) where {EQ <: Weierstrass, F <: PrimeField}

    _a = a(EQ)
    _b = b(EQ)

    p = modulus(F)

    return ECP(p, n, _a, _b, Gx, Gy) # I will need to add H in the end here
end

Base.:(==)(x::ECP, y::ECP) = x.p == y.p && x.n == y.n && x.a == y.a && x.b == y.b && x.Gx == y.Gx && x.Gy == y.Gy

abstract type EC2N <: Spec end

order(curve::EC2N) = curve.n
generator(curve::EC2N) = (curve.Gx, curve.Gy)

a(curve::EC2N) = curve.a
b(curve::EC2N) = curve.b

@kwdef struct Koblitz_GNB <: EC2N
    m::Int
    T::Int
    n::BigInt
    a::Int
    Gx::BitVector
    Gy::BitVector

    function Koblitz_GNB(m::Int, T::Int, n::BigInt, a::Int, Gx::BitVector, Gy::BitVector)

        @assert a in [0, 1]
        @assert length(Gx) == length(Gy) == m

        new(m, T, n, a, Gx, Gy)
    end
end

function Koblitz_GNB(m, T, n, a, Gx, Gy)

    _m = convert(Int, m)
    _T = convert(Int, T)
    _a = convert(Int, a)
    _n = convert(BigInt, n)
    _Gx = _parse_bits(Gx, _m)
    _Gy = _parse_bits(Gy, _m)

    return Koblitz_GNB(_m, _T, _n, _a, _Gx, _Gy)
end

function b(curve::Koblitz_GNB)

    (; m) = curve

    _b = 1
    b′ = _parse_bits_gnb(_b, m)

    return b′
end

@kwdef struct Koblitz_PB <: EC2N
    f::Vector{Int}
    n::BigInt
    a::Int
    Gx::BitVector
    Gy::BitVector

    function Koblitz_PB(f::Vector{Int}, n::BigInt, a::Int, Gx::BitVector, Gy::BitVector)

        m = maximum(f)

        @assert a in [0, 1]
        @assert length(Gx) == length(Gy) == m

        new(f, n, a, Gx, Gy)
    end
end

function Koblitz_PB(f, n, a, Gx, Gy)

    _f = convert(Vector{Int}, f)

    m = maximum(_f)

    _n = convert(BigInt, n)
    _a = convert(Int, a)
    _Gx = _parse_bits(Gx, m)
    _Gy = _parse_bits(Gy, m)

    return Koblitz_PB(_f, _n, _a, _Gx, _Gy)
end

function b(curve::Koblitz_PB)

    (; f) = curve

    m = maximum(f)

    _b = 1
    b′ = _parse_bits_pb(_b, m)

    return b′
end

function _int2bits_gnb(x::Int, m::Int)
    if x == 0
        return BitVector(0 for i in 1:m)
    elseif x == 1
        return BitVector(1 for i in 1:m)
    else
        error("Conversion of $x not possible")
    end
end

_parse_bits_gnb(x::BitVector, m::Int) = x
_parse_bits_gnb(x::String, m::Int) = _parse_bits(x, m)
_parse_bits_gnb(x::Int, m::Int) = _int2bits_gnb(x, m)

#abstract type BinaryCurveSpec <: EllipticCurveSpec end

@kwdef struct BEC_GNB <: EC2N  #BinaryCurveSpec
    m::Int
    T::Int
    n::BigInt
    a::BitVector
    b::BitVector
    Gx::BitVector
    Gy::BitVector

    function BEC_GNB(m::Int, T::Int, n::BigInt, a::BitVector, b::BitVector, Gx::BitVector, Gy::BitVector)

        @assert m == length(a) == length(b) == length(Gx) == length(Gy)

        new(m, T, n, a, b, Gx, Gy)
    end
end

function BEC_GNB(m, T, n, a, b, Gx, Gy)

    _m = convert(Int, m)
    _T = convert(Int, T)
    _n = convert(BigInt, n)
    _a = _parse_bits_gnb(a, _m)
    _b = _parse_bits_gnb(b, _m)
    _Gx = _parse_bits(Gx, _m)
    _Gy = _parse_bits(Gy, _m)

    return BEC_GNB(_m, _T, _n, _a, _b, _Gx, _Gy)
end

function BEC_GNB(curve::Koblitz_GNB)

    (; m, T, n, a, Gx, Gy) = curve

    _b = b(curve)

    return BEC_GNB(m, T, n, a, _b, Gx, Gy)
end

function _int2bits_pb(x::Int, m::Int)
    if x == 0
        return BitVector(0 for i in 1:m)
    elseif x == 1
        return BitVector(((0 for i in 1:m-1)..., 1))
    else
        error("Conversion of $x not possible")
    end
end

_parse_bits_pb(x::BitVector, m::Int) = x
_parse_bits_pb(x::String, m::Int) = _parse_bits(x, m)
_parse_bits_pb(x::Int, m::Int) = _int2bits_pb(x, m)

@kwdef struct BEC_PB <: EC2N  #BinaryCurveSpec
    f::Vector{Int}
    n::BigInt
    a::BitVector
    b::BitVector
    Gx::BitVector
    Gy::BitVector

    function BEC_PB(f::Vector{Int}, n::BigInt, a::BitVector, b::BitVector, Gx::BitVector, Gy::BitVector)

        m = maximum(f)

        @assert length(a) == length(b) == length(Gx) == length(Gy) == m

        new(f, n, a, b, Gx, Gy)
    end
end

function BEC_PB(f, n, a, b, Gx, Gy)

    _f = convert(Vector{Int}, f)
    _m = maximum(_f)
    _n = convert(BigInt, n)

    _a = _parse_bits_pb(a, _m)
    _b = _parse_bits_pb(b, _m)
    _Gx = _parse_bits(Gx, _m)
    _Gy = _parse_bits(Gy, _m)

    return BEC_PB(_f, _n, _a, _b, _Gx, _Gy)
end

function BEC_PB(curve::Koblitz_PB)

    (; f, n, a, Gx, Gy) = curve

    _b = b(curve)

    return BEC_PB(f, n, a, _b, Gx, Gy)
end

### Methods for making a concrete AffinePoint type from given elliptic curve spec

function specialize(::Type{AffinePoint{Weierstrass, F}}, curve::ECP) where F <: PrimeField

    (; p, a, b) = curve

    P = AffinePoint{specialize(Weierstrass, a, b), specialize(F, p)}

    return P
end

specialize(::Type{AffinePoint}, spec::ECP) = specialize(AffinePoint{Weierstrass, FP}, spec)
#specialize(::Type{ECPoint}, spec::ECP; name = nothing) = specialize(ECPoint{AffinePoint}, spec; name)

function specialize(::Type{F}, curve::BEC_PB) where F <: BinaryField
    (; f) = curve
    return specialize(F, f)
end

function specialize(::Type{F}, curve::BEC_GNB) where F <: BinaryField
    (; m, T) = curve
    return specialize(F, m, T)
end

specialize(::Type{BinaryCurve}, curve::EC2N) = specialize(BinaryCurve, a(curve), b(curve))

function specialize(::Type{AffinePoint{BinaryCurve, F}}, curve) where F <: BinaryField
    P = AffinePoint{specialize(BinaryCurve, curve), specialize(F, curve)}
    return P
end

specialize(::Type{AffinePoint}, curve::BEC_GNB) = specialize(AffinePoint{BinaryCurve, F2GNB}, curve)
specialize(::Type{AffinePoint}, curve::BEC_PB) = specialize(AffinePoint{BinaryCurve, F2PB}, curve)

specialize(::Type{AffinePoint{BinaryCurve, F}}, curve::Koblitz_GNB) where F <: BinaryField = specialize(AffinePoint{BinaryCurve, F}, BEC_GNB(curve))
specialize(::Type{AffinePoint{BinaryCurve, F}}, curve::Koblitz_PB) where F <: BinaryField = specialize(AffinePoint{BinaryCurve, F}, BEC_PB(curve))

################################ Constants #################

############### Prime curves #############

#
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 = BEC_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 = BEC_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 = BEC_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 = BEC_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 = BEC_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 = BEC_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 = BEC_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 = BEC_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 = BEC_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 = BEC_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"
)


module BSplines

using IterTools
# using OffsetArrays

export BSpline, calc, gen_matrix, predict

struct BSpline
    p
    n
    t
end

function BSpline(p::Int, t::AbstractArray{T}) where T <: Real
    # number of knots t[i]
    n = size(t, 1)

    # extend knots by p on both sides of t
    tt = [repeat(t[1:1], p)..., t..., repeat(t[end:end], p)...]

    BSpline(p, n, tt)
end

function calc(b::BSpline, i::Int, p::Int, x::T) where T <: Real
    if (i + p + 1 > b.n + 2 * b.p) || (i < 1) || !(b.t[i] <= x <= b.t[i+p+1])
        return zero(T)
    elseif p === b.p && i === b.n + p - 1 && x == b.t[end]
        return one(T)
    end

    if p === 0
        if (i + 1 <= b.n + b.p) && (b.t[i] <= x < b.t[i+1])
            one(T)
        else
            zero(T)
        end
    else
        b1 = calc(b, i, p - 1, x)
        a1 = b1 === zero(T) ? zero(T) : (x - b.t[i]) / (b.t[i + p] - b.t[i])

        b2 = calc(b, i + 1, p - 1, x)
        a2 = b2 === zero(T) ? zero(T) : (b.t[i + p + 1] - x) / (b.t[i + p + 1] - b.t[i + 1])

        a1 * b1 + a2 * b2
    end
end

calc(b::BSpline, i::Int, x::T) where T <: Real = calc(b, i, b.p, x)

function gen_matrix(b::BSpline, xs::AbstractArray{T}) where T <: Real
    [
        calc(b, i, x) for x in xs, i in 1:(b.n + b.p - 1)
    ]
end

function predict(b::BSpline, w::AbstractArray{T}, x::T) where T <: Real
    parts = partition(b.t, 2, 1) |> collect
    k = findfirst(i -> first(i) <= x < last(i), parts)
    if isnothing(k)
        return zero(T)
    end
    sum(w[i] * calc(b, i, b.p, x) for i in (k - b.p):k)
end

predict(b::BSpline, x::T) where T <: Real = predict(b, ones(T, b.n + b.p - 1), x)

end  # module


include("../../matplotlib_settings.jl")

set_config(:tab_compensated_summation, false)

using ChargedParticleDynamics.GuidingCenter4d: plot_integral_error, plot_poincare_loop, plot_poincare_trajectories

const coll_title = "Guiding Center 4D Fast 1st Poincaré Integral Invariant"

XMIN = 1.5
XMAX = 2.0
YMIN = -0.25
YMAX = +0.25

include("../guiding_center_4d_poincare_invariant_1st.jl")


"Parse a tree grid from a string."
function parsetreegrid(s)
    lines = split(s, "\n", keepempty=false)
    width = length(lines[1])
    height = length(lines)
    grid = zeros(Int8, height, width)
    for (i, line) in enumerate(lines)
        for (j, c) in enumerate(line)
            if c == '#'
                grid[i, j] = 1
            end
        end
    end
    grid
end

function walktreegrid(grid, right, down)
    count = 0
    pos = (1,1)
    sz = size(grid)
    while pos[1] <= sz[1]
        if grid[pos...] == 1
            count += 1
        end
        pos = (pos[1]+down, pos[2]+right)
        if pos[2] > sz[2]
            pos = (pos[1], pos[2] % sz[2])
        end
    end
    count
end

day03part1(s) = s |> parsetreegrid |> x->walktreegrid(x, 3, 1)

function day03part2(s)
    grid = parsetreegrid(s)
    dirs = [(1,1), (3,1), (5,1), (7,1), (1,2)]
    mapreduce(x->walktreegrid(grid, x...), *, dirs)
end

function day03(input)
    s = read(input, String)
    p1 = day03part1(s)
    p2 = day03part2(s)
    (p1, p2)
end

#=

s = """
..##.......
#...#...#..
.#....#..#.
..#.#...#.#
.#...##..#.
..#.##.....
.#.#.#....#
.#........#
#.##...#...
#...##....#
.#..#...#.#
"""
grid = parsetreegrid(s)

walktreegrid(grid, (1,1)...)

input = read("day03/input.txt", String)
parsetreegrid(input)

day03part1(input)
day03part2(input)

=#


using Rubin
using Tests
using Elliptic
using HypergeometricFunctions
using Polylogarithms
using SpecialFunctions

@test integrate((a+a*csc(x))^-1*cos(x)^4, x) == :(-1//3*a^-1*cos(x)^3+-1//8*x*a^-1+-1//8*a^-1*cos(x)*sin(x)+1//4*a^-1*cos(x)^3*sin(x))
@test integrate((a+a*csc(x))^-1*cos(x)^3, x) == :((1/2)*a^-1*sin(x)^2+-1//3*a^-1*sin(x)^3)
@test integrate((a+a*csc(x))^-1*cos(x)^2, x) == :(-1*a^-1*cos(x)+-1//2*x*a^-1+(1/2)*a^-1*cos(x)*sin(x))
@test integrate((a+a*csc(x))^-1*cos(x), x) == :(a^-1*sin(x)+-1*a^-1*log(1+sin(x)))
@test integrate((a+a*csc(x))^-1*sec(x), x) == :((1/2)*a^-1*sec(x)^2+(1/2)*a^-1*arctanh(sin(x))+-1//2*a^-1*sec(x)*tan(x))
@test integrate((a+a*csc(x))^-1*sec(x)^2, x) == :(-1//3*a^-1*tan(x)^3+1//3*a^-1*sec(x)^3)
@test integrate((a+a*csc(x))^-1*sec(x)^3, x) == :(1//4*a^-1*sec(x)^4+1//8*a^-1*arctanh(sin(x))+-1//4*a^-1*sec(x)^3*tan(x)+1//8*a^-1*sec(x)*tan(x))
@test integrate((a+a*csc(x))^-1*sec(x)^4, x) == :(-1//3*a^-1*tan(x)^3+-1//5*a^-1*tan(x)^5+1//5*a^-1*sec(x)^5)
@test integrate((a+b*csc(x))^-1*cos(x)^4, x) == :(-1//8*a^-4*(8*b*(a^2+-1*b^2)+-1*a*(-4*b^2+3*a^2)*sin(x))*cos(x)+-1//12*a^-2*cos(x)^3*(4b+-3*a*sin(x))+1//8*x*a^-5*(3*a^4+8*b^4+-12*a^2*b^2)+2*b*a^-5*(a^2+-1*b^2)^3//2*arctanh((a^2+-1*b^2)^-1//2*(a+b*tan((1/2)*x))))
@test integrate((a+b*csc(x))^-1*cos(x)^3, x) == :(-1//3*a^-1*sin(x)^3+a^-3*(a^2+-1*b^2)*sin(x)+(1/2)*b*a^-2*sin(x)^2+-1*b*a^-4*(a^2+-1*b^2)*log(b+a*sin(x)))
@test integrate((a+b*csc(x))^-1*cos(x)^2, x) == :((1/2)*x*a^-3*(a^2+-2*b^2)+-1//2*a^-2*(2b+-1*a*sin(x))*cos(x)+2*b*a^-3*(a^2+-1*b^2)^(1/2)*arctanh((a^2+-1*b^2)^-1//2*(a+b*tan((1/2)*x))))
@test integrate((a+b*csc(x))^-1*cos(x), x) == :(a^-1*sin(x)+-1*b*a^-2*log(b+a*sin(x)))
@test integrate((a+b*csc(x))^-1*sec(x), x) == :((-2b+2a)^-1*log(1+sin(x))+-1*(2a+2b)^-1*log(1+-1*sin(x))+-1*b*(a^2+-1*b^2)^-1*log(b+a*sin(x)))
@test integrate((a+b*csc(x))^-1*sec(x)^2, x) == :(-1*(a^2+-1*b^2)^-1*(b+-1*a*sin(x))*sec(x)+2*a*b*(a^2+-1*b^2)^-3//2*arctanh((a^2+-1*b^2)^-1//2*(a+b*tan((1/2)*x))))
@test integrate((a+b*csc(x))^-1*sec(x)^3, x) == :(-1*(-2*b^2+2*a^2)^-1*sec(x)^2*(b+-1*a*sin(x))+-1//4*a*(a+b)^-2*log(1+-1*sin(x))+1//4*a*(a+-1b)^-2*log(1+sin(x))+-1*b*a^2*(a^2+-1*b^2)^-2*log(b+a*sin(x)))
@test integrate((a+b*csc(x))^-1*sec(x)^4, x) == :(-1*(-3*b^2+3*a^2)^-1*sec(x)^3*(b+-1*a*sin(x))+-1//3*(a^2+-1*b^2)^-2*(3*b*a^2+-1*a*(b^2+2*a^2)*sin(x))*sec(x)+2*b*a^3*(a^2+-1*b^2)^-5//2*arctanh((a^2+-1*b^2)^-1//2*(a+b*tan((1/2)*x))))

test/4 Trig functions/4.6 Cosecant/4.6.1.3 (d cos)^n (a+b csc)^m.jl


using NLSolvers
function f∇f!(∇f, x)
   if !(∇f==nothing)
       if ( x[1]^2 + x[2]^2 == 0 )
           dtdx1 = 0;
           dtdx2 = 0;
       else
           dtdx1 = - x[2] / ( 2 * pi * ( x[1]^2 + x[2]^2 ) );
           dtdx2 =   x[1] / ( 2 * pi * ( x[1]^2 + x[2]^2 ) );
       end
       ∇f[1] = -2000.0*(x[3]-10.0*theta(x))*dtdx1 +
           200.0*(sqrt(x[1]^2+x[2]^2)-1)*x[1]/sqrt( x[1]^2+x[2]^2 );
       ∇f[2] = -2000.0*(x[3]-10.0*theta(x))*dtdx2 +
           200.0*(sqrt(x[1]^2+x[2]^2)-1)*x[2]/sqrt( x[1]^2+x[2]^2 );
       ∇f[3] =  200.0*(x[3]-10.0*theta(x)) + 2.0*x[3];
   end

   fx = f(x)
   return ∇f==nothing ? fx : (fx, ∇f)
end

f(x) = 100.0 * ((x[3] - 10.0 * theta(x))^2 + (sqrt(x[1]^2 + x[2]^2) - 1.0)^2) + x[3]^2
function theta(x)
  if x[1] > 0
      return atan(x[2] / x[1]) / (2.0 * pi)
  else
      return (pi + atan(x[2] / x[1])) / (2.0 * pi)
  end
end

obj! = OnceDiffed(f∇f!)

minimize!(obj!, -rand(3)*9 .- 3, NLSolvers.NelderMead())
V = [[1.0,1.0,1.0], [0.0,1.0,1.0],[0.40,0.0,0.0],[-1.0,2.0,.03]]
F = f∇f!.(nothing, V)
splx = NLSolvers.ValuedSimplex(V, F)
minimize!(obj!, splx, NLSolvers.NelderMead())
@profiler minimize!(obj!, splx, NLSolvers.NelderMead(); itermax=1500)

function powell(∇f, x)
    fx = (x[1] + 10.0 * x[2])^2 + 5.0 * (x[3] - x[4])^2 +
        (x[2] - 2.0 * x[3])^4 + 10.0 * (x[1] - x[4])^4

    if !isa(∇f, Nothing)
        ∇f[1] = 2.0 * (x[1] + 10.0 * x[2]) + 40.0 * (x[1] - x[4])^3
        ∇f[2] = 20.0 * (x[1] + 10.0 * x[2]) + 4.0 * (x[2] - 2.0 * x[3])^3
        ∇f[3] = 10.0 * (x[3] - x[4]) - 8.0 * (x[2] - 2.0 * x[3])^3
        ∇f[4] = -10.0 * (x[3] - x[4]) - 40.0 * (x[1] - x[4])^3
        return ∇f, fx
    end
    return fx
end

function powell_optim(F, ∇f, x)
    if !isa(F, Nothing)
        fx = (x[1] + 10.0 * x[2])^2 + 5.0 * (x[3] - x[4])^2 +
            (x[2] - 2.0 * x[3])^4 + 10.0 * (x[1] - x[4])^4
    end
    if !isa(∇f, Nothing)
        ∇f[1] = 2.0 * (x[1] + 10.0 * x[2]) + 40.0 * (x[1] - x[4])^3
        ∇f[2] = 20.0 * (x[1] + 10.0 * x[2]) + 4.0 * (x[2] - 2.0 * x[3])^3
        ∇f[3] = 10.0 * (x[3] - x[4]) - 8.0 * (x[2] - 2.0 * x[3])^3
        ∇f[4] = -10.0 * (x[3] - x[4]) - 40.0 * (x[1] - x[4])^3
        if !isa(F, Nothing)
            return fx
        else
            return Nothing
        end
    end
    return fx
end
obj_powell = OnceDiffed(powell)
# Define vertices in V
x0 = [1.0,1.0,1.0,1.0]
V = [copy(x0)]
for i = 1:4
    push!(V, x0+39*rand(4))
end
V
F = obj_powell.(V)
splx = NLSolvers.ValuedSimplex(V, F)
@time minimize!(obj_powell, splx, NLSolvers.NelderMead(); itermax=3000)
@time minimize!(obj_powell, copy(x0), NLSolvers.NelderMead(); itermax=3000)
@allocated minimize!(obj_powell, splx, NLSolvers.NelderMead(); itermax=3000)
@profiler minimize!(obj_powell, copy(x0), NLSolvers.NelderMead(); itermax=3000)

using Optim
res = optimize(Optim.only_fg!(powell_optim), copy(x0), NelderMead())
res = optimize(Optim.only_fg!(powell_optim), copy(x0), Optim.GradientDescent())
@time optimize(Optim.only_fg!(powell_optim), x0, NelderMead(), Optim.Options(g_tol=0, iterations =3000))

function extros!(storage, x::AbstractArray)
   n = length(x)
   jodd = 1:2:n-1
   jeven = 2:2:n
   xt = similar(x)
   @. xt[jodd] = 10.0 * (x[jeven] - x[jodd]^2)
   @. xt[jeven] = 1.0 - x[jodd]

   if !isa(storage, Nothing)
       @. storage[jodd] = -20.0 * x[jodd] * xt[jodd] - xt[jeven]
       @. storage[jeven] = 10.0 * xt[jodd]
       return 0.5*sum(abs2, xt), storage
   end
   return 0.5*sum(abs2, xt)
end
x0=rand(300)
V = [copy(x0)]
for i = 1:300
    push!(V, x0+39*rand(length(x0)))
end
V
extros_obj! = OnceDiffed(extros!)
F = extros_obj!.(V)
splx = NLSolvers.ValuedSimplex(V, F)
@time minimize!(extros_obj!, splx, NLSolvers.NelderMead(); itermax=3000)


using Distributions, Plots; pyplot()

N = 10^5
nMin, nStep, nMax = 10, 10, 200
nn = Int(nMax/nStep)
sampleSizes = nMin:nStep:nMax
trueB = 5
trueDist = Uniform(-2, trueB)

MLEest(data) = maximum(data)
MMest(data)  = mean(data) + sqrt(3)*std(data)

res = Dict{Symbol,Array{Float64}}(
    ((sym) -> sym => Array{Float64}(undef,nn)).(
        [:MSeMLE,:MSeMM, :VarMLE,:VarMM,:BiasMLE,:BiasMM]))

for (i, n) in enumerate(sampleSizes)
    mleEst, mmEst = Array{Float64}(undef, N), Array{Float64}(undef, N)
    for j in 1:N
        sample    = rand(trueDist,n)
        mleEst[j] = MLEest(sample)
        mmEst[j]  = MMest(sample)
    end
    meanMLE, meanMM = mean(mleEst), mean(mmEst)
    varMLE, varMM = var(mleEst), var(mmEst)

    res[:MSeMLE][i] = varMLE + (meanMLE - trueB)^2
    res[:MSeMM][i] = varMM + (meanMM - trueB)^2
    res[:VarMLE][i] = varMLE
    res[:VarMM][i] = varMM
    res[:BiasMLE][i] = meanMLE - trueB
    res[:BiasMM][i] = meanMM - trueB
end

p1 = scatter(sampleSizes, [res[:MSeMLE] res[:MSeMM]], c=[:blue :red],
    label=["Mean sq.err (MLE)" "Mean sq.err (MM)"])
p2 = scatter(sampleSizes, [res[:VarMLE] res[:VarMM]], c=[:blue :red],
    label=["Variance (MLE)" "Variance (MM)"])
p3 = scatter(sampleSizes, [res[:BiasMLE] res[:BiasMM]], c=[:blue :red],
    label=["Bias (MLE)" "Bias (MM)"])

plot(p1, p2, p3, ms=10, shape=:xcross, xlabel="n",
    layout=(1,3), size=(1200, 400))