""" Code to compute Mie scattering parameters based on MieScatter julia
package

https://github.com/dronir/MieScatter.jl

"""

module MieParams
using FastGaussQuadrature

export compute

# Unfortunately the julia package manager does not really work for dependencies
# on unregistered packages.  And it seems that nothing really works for
# sub-packages so I could also not use them.  This was the only half-satisfactory
# way.
include("MieScatter/src/MieScatter.jl")
using .MieScatter

include("constants.jl")
const co = constants

"""
    g, ω₀, Qext = compute(r, λ, m; order=1000)

Compute the parameters `g`, `ω₀`, `Qext` for given radius of spherical particle,
`r`, wavelength `λ` and refractive index m (generally a complex number).
"""
function compute(r::Number, λ, m; order=1000)
    μ, w = gausslegendre(order)
    theta = acos.(μ)

    S, _, _, _ = compute_mie(size_parameter(r, λ), m, theta)

    # For some reason compute_mie returns Qext and Qback only when we ask
    # for forward and / or backward scattering
    _, Qsca, Qext, Qback = compute_mie(size_parameter(r, λ), m, [0, π])

    ω₀ = Qsca / Qext
    g = sum(μ .* w .* S[:, 1]) / sum(w .* S[:, 1])

    g, ω₀, Qext
end

function compute(r::AbstractVector, λ, m; kw...)
    w = compute.(r, λ, m; kw...)

    g, ω0, Qext = [[itm[i] for itm in w] for i in 1:3]
    g, ω0, Qext
end

end


const ADD = 0
const MUL = 1

function evaluate(line::String, i::Int, linelength::Int)::Tuple{Int,Int}
    res::Vector{Int} = [0]
    op::Int = 0
    while i <= linelength
        if line[i] == ' '
            i += 1
        elseif line[i] == '*'
            op = MUL
            i += 2
        elseif line[i] == '+'
            op = ADD
            i += 2
        elseif line[i] == '('
            (tmp, i) = evaluate(line, i + 1, linelength)
            if op == ADD
                res[end] += tmp
            elseif op == MUL
                push!(res, tmp)
            end
        elseif line[i] == ')'
            return (prod(res), i + 1)
        else
            tmp = parse(Int, line[i])
            if op == ADD
                res[end] += tmp
            elseif op == MUL
                push!(res, tmp)
            end
            i += 1
        end
    end
    return (prod(res), i)
end

function run(s)
    counter::Int = 0
    for line in eachline(IOBuffer(s))
        (res, _) = evaluate(line, 1, lastindex(line))
        counter += res
    end
    return counter
end

#########################################

function main()
    res, time, memory = @timed run(ARGS[1])
    println("_duration:$(time * 1000)")
    println(res)
end

main()


using LyapunovExponents
using Plots
demo = solve!(LyapunovExponents.lozi_map(); progress=1)
report(demo)
plt = plot(demo)


@symbol_func function G_CS(cur_reactor::AbstractReactor)
  cur_G = h_CS(cur_reactor)

  cur_G *= G_LO(cur_reactor)

  cur_G *= safe_symbol(cur_reactor, :R_0) ^ 3

  cur_G *= cur_reactor.T_bar

  cur_G ^= (1/2)

  cur_G
end


function φmin(u::Point{d},v::Point{d}) where d
    sq = sqrt.(u.^2+v.^2)
    # p = (u+v-sq)/2
    # J_u = (1 .- u./sq)/2
    # J_v = (1 .- v./sq)/2
    p =    SVector{d}( (v[i]<Inf ? (u[i]+v[i]-sq[i])/2 : u[i]) for i=1:d )
    J_u =  SVector{d}( (v[i]<Inf ? (1.0 - u[i]./sq[i])/2 : 1) for i=1:d )
    J_v =  SVector{d}( (v[i]<Inf ? (1.0 - v[i]./sq[i])/2 : 0) for i=1:d )

    return p, SDiagonal(J_u), SDiagonal(J_v)
end

function φmax(u::Point{d},v::Point{d}) where d
    sq = sqrt.(u.^2+v.^2)
    # p = (u+v+sq)/2
    # J_u = (1 .+ u./sq)/2
    # J_v = (1 .+ v./sq)/2

    p =    SVector{d}( (v[i]>-Inf ? (u[i]+v[i]+sq[i])/2 : u[i]) for i=1:d )
    J_u =  SVector{d}( (v[i]>-Inf ? (1.0 + u[i]./sq[i])/2 : 1) for i=1:d )
    J_v =  SVector{d}( (v[i]>-Inf ? (1.0 + v[i]./sq[i])/2 : 0) for i=1:d )

    return p, SDiagonal(J_u), SDiagonal(J_v)
end

function PhiPhi(f::Point{d}, x::Point{d}, a::Point{d}, b::Point{d}) where d
    y, y_f, y_x = φmin(f,x-a)

    z, z_y, z_x = φmax(y,x-b)
    return z, (z_y*y_f), (z_y*y_x + z_x)
end

function PhiPhi0(f::Point{d}, x::Point{d}, a::Point{d}, b::Point{d}) where d
    y = φmin(f,x-a)[1]
    z = φmax(y,x-b)[1]
    return z
end

function PhiPhi(f::Point{d}, D::SMatrix{d,d,Float64,q}, x::Point{d}, a::Point{d}, b::Point{d}) where d where q
    z, z_f, z_x = PhiPhi(f,x,a,b)
    z, z_f*D + z_x
end

function PhiPhi!(F::Vector{Vector{Point{d}}},
                 X::Vector{Vector{Point{d}}},
                 A::Vector{Vector{Point{d}}},
                 B::Vector{Vector{Point{d}}},
                 D::Vector{v},
                 J::Matrix{w}) where v<:AbstractVector{SMatrix{d,d,Float64,q}} where w<:AbstractVector{SMatrix{d,d,Float64,q}} where d where q

    n_m, n_M = size(J)
    N = length(F[1])
    for i=1:n_m
        for n=1:N
            f = F[i][n]
            x = X[i][n]
            a = A[i][n]
            b = B[i][n]
            z, z_f, z_x = PhiPhi(f,x,a,b)
            F[i][n] = z
            D[i][n] = z_f*D[i][n] + z_x
            for j=1:n_M
                J[i,j][n] = z_f*J[i,j][n]
            end
        end
    end
end

function PhiPhi!(F::Vector{<:AbstractVector{Point{d}}},
                 X::Vector{<:AbstractVector{Point{d}}},
                 A::Vector{<:AbstractVector{Point{d}}},
                 B::Vector{<:AbstractVector{Point{d}}},
                 J::Matrix{w}) where w<:AbstractVector{SMatrix{d,d,Float64,q}} where d where q

    n_m, n_M = size(J)
    N = length(F[1])
    for i=1:n_m
        for n=1:N
            f = F[i][n]
            x = X[i][n]
            a = A[i][n]
            b = B[i][n]
            z, z_f, z_x = PhiPhi(f,x,a,b)
            F[i][n] = z
            for j=1:n_M
                J[i,j][n] = z_f*J[i,j][n]
            end
        end
    end
end

function PhiPhi!(F::Vector{Vector{Point{d}}},
                X::Vector{Vector{Point{d}}},
                A::Vector{Vector{Point{d}}},
                B::Vector{Vector{Point{d}}},
                D::Vector{v}) where v<:AbstractVector{SMatrix{d,d,Float64,q}} where d where q

    n_m = size(D,1)
    N = length(F[1])
    for i=1:n_m
        for n=1:N
            f = F[i][n]
            x = X[i][n]
            a = A[i][n]
            b = B[i][n]
            z, z_f, z_x = PhiPhi(f,x,a,b)
            F[i][n] = z
            D[i][n] = z_f*D[i][n] + z_x
        end
    end
end

function PhiPhi(F::Vector{Vector{Point{d}}},X::Vector{Vector{Point{d}}},A::Vector{Vector{Point{d}}},B::Vector{Vector{Point{d}}},D::Vector{Vector{SMatrix{d,d,Float64,q}}}, J::Matrix{Vector{SMatrix{d,d,Float64,q}}}) where d where q
    FF = deepcopy(F)
    DD = deepcopy(D)
    JJ = deepcopy(J)
    PhiPhi!(FF,X,A,B,DD,JJ)
    return FF,DD,JJ
end


# TODO: Check.
function logprior(state::State, prior::Prior)
  lp = sum(logpdf.(prior.mu, state.mu))
  lp += sum(logpdf.(prior.omega, state.omega))
  lp += sum(logpdf.(prior.psi, state.psi))
  lp += sum(logpdf.(prior.nu, state.nu))
  lp += logpdf(prior.gamma, state.gammaC)
  lp += logpdf(prior.gamma, state.gammaT)
  lp += logpdf(prior.eta, state.etaC)
  lp += logpdf(prior.eta, state.etaT)

  # lambda prior
  etaT_star = state.beta ? state.etaT : state.etaC
  lp += sum(log.(state.etaC[state.lambdaC]))
  lp += sum(log.(etaT_star[state.lambdaT]))

  # v prior
  nuC = state.nu[state.lambdaC]
  nuT = state.nu[state.lambdaT]
  lp += sum(gammalogpdf.(nuC/2, 2 ./ nuC, state.vC))
  lp += sum(gammalogpdf.(nuT/2, 2 ./ nuT, state.vT))

  # zeta prior
  lp += sum(logpdf(truncated(Normal(0, 1/sqrt(state.vC[n])), 0, Inf), state.zetaC[n])
            for n in eachindex(state.zetaC))
  lp += sum(logpdf(truncated(Normal(0, 1/sqrt(state.vT[n])), 0, Inf), state.zetaT[n])
            for n in eachindex(state.zetaT))
  return lp
end

function marginal_logprior(state::State, prior::Prior)
  lp = sum(logpdf.(prior.mu, state.mu))
  lp += sum(logpdf.(prior.omega, state.omega))
  lp += sum(logpdf.(prior.psi, state.psi))
  lp += sum(logpdf.(prior.nu, state.nu))
  lp += logpdf(prior.gamma, state.gammaC)
  lp += logpdf(prior.eta, state.etaC)
  if state.beta
    lp += logpdf(prior.gamma, state.gammaT)
    lp += logpdf(prior.eta, state.etaT)
  end
  return lp
end


#include("../benchmark.jl")

using JuMP, OnePhase
#using Ipopt

function create_tax_problem(na::Int64)
    A = 1:na;

    mu = ones(na);
    lambda = ones(na)

    wmin = 1;
    wmax = wmin + na - 1;
    w = [wmin + ((wmax-wmin)/(na-1))*(i-1) for i = 1:na]

    m = Model()
    @variable(m, c[i in A] >= 0.0)# for (i,j) in T)
    @variable(m, y[i in A] >= 0.0)# for (i,j) in T )

    @NLobjective(m, Min, -sum( lambda[i] * (log(c[i]) - (y[i]/w[i])^(mu[i]+1) / (mu[i]+1)) for i in A) )

    # Incentive
    for i in A
      for p in A
        if i != p
          @NLconstraint(m,(log(c[i]) - (y[i]/w[i])^(mu[i]+1) / (mu[i]+1))
        - (log(c[p]) - (y[p]/w[i])^(mu[i]+1) / (mu[i]+1)) >= 0.0)
        end
      end
    end

    @NLconstraint(m, sum(lambda[i] * (y[i] - c[i]) for i in A)  >= 0.0);

    return m
end

############
m = create_tax_problem(50);
@show m

nlp_raw = MathProgNLPModel(m);
my_pars = Class_parameters();
iter = one_phase_solve(nlp_raw, my_pars);

using Ipopt
setsolver(m,IpoptSolver())
solve(m)

############
m = create_tax_problem(100);
@show m

nlp_raw = MathProgNLPModel(m);
my_pars = Class_parameters();
iter = one_phase_solve(nlp_raw, my_pars);

m = create_tax_problem(250);
@show m

nlp_raw = MathProgNLPModel(m);
my_pars = Class_parameters();
iter = one_phase_solve(nlp_raw, my_pars);

using Ipopt
setsolver(m,IpoptSolver())
solve(m)

############
m = create_tax_problem(500);
@show m

nlp_raw = MathProgNLPModel(m);
my_pars = Class_parameters();
my_par.max_time = 60.0 * 60.0 * 10.0
iter = one_phase_solve(nlp_raw, my_pars);

using Ipopt
setsolver(m,IpoptSolver())
solve(m)

#=
m = create_tax_problem(800);
@show m

nlp_raw = MathProgNLPModel(m);
my_pars = Class_parameters();
my_par.max_time = 60.0 * 60.0 * 10.0
iter = one_phase_solve(nlp_raw, my_pars);
=#
#=
using Ipopt
setsolver(m,IpoptSolver())
solve(m)
=#

#=srand(1)
m = create_tax_problem(5)
one_phase_solve(m)

srand(1)
m = create_tax_problem(10)
one_phase_solve(m)

srand(1)
m = create_tax_problem(20)
one_phase_solve(m)=#
#=
ORG_STDOUT = STDOUT
file = open("test.txt", "w")
redirect_stdout(file)

srand(1)
m = create_tax_problem(30)
one_phase_solve(m)

STDOUT = ORG_STDOUT

close(file)
=#

#srand(1)
#m = create_tax_problem(40)
#one_phase_solve(m)

#=srand(1)
m = create_tax_problem(5)
status = solve(m)

srand(1)
m = create_tax_problem(10)
status = solve(m)

srand(1)
m = create_tax_problem(20)
status = solve(m)=#

#srand(1)
#m = create_tax_problem(40)
#status = solve(m)

#finalize(nlp_raw)


using luteos
using SymPy

# let # limit scope

Ps = [P1(), P2(), P3()]         # Range of polynomial order
Ns = [7, 13]#[9, 17, 33] # Range of grid size

dim = 3

# Material
mat = Material( ν=0.33, E=1.0, dim=3 )

## Set up problem
#   Get functions for exact solution
function ExactSol3D( Cstiff )

  dim = 3

  @syms x1 x2 x3

  u1 = 10 * (x2 - x2.^2) .* (x3 - x3.^2) .* sin.(pi*x1) .* (1 - x1) .* (1 - x2/2) .* (1 - x3/4)
  u2 =  2 * (x1 - x1.^2) .* (x3 - x3.^2) .* sin.(pi*x2) .* (1 - x2) .* (1 - x1/2) .* (1 - x3/4)
  u3 =  5 * (x1 - x1.^2) .* (x2 - x2.^2) .* cos.(pi*x3) .* (1 - x3/2) .* (1 - x1/2) .* (1 - x2/2)

  ∂u1∂x1 = diff( u1, x1 )
  ∂u1∂x2 = diff( u1, x2 )
  ∂u1∂x3 = diff( u1, x3 )
  ∂u2∂x1 = diff( u2, x1 )
  ∂u2∂x2 = diff( u2, x2 )
  ∂u2∂x3 = diff( u2, x3 )
  ∂u3∂x1 = diff( u3, x1 )
  ∂u3∂x2 = diff( u3, x2 )
  ∂u3∂x3 = diff( u3, x3 )

  ∇u = [ ∂u1∂x1 ∂u2∂x1 ∂u3∂x1; ∂u1∂x2 ∂u2∂x2 ∂u3∂x2; ∂u1∂x3 ∂u2∂x3 ∂u3∂x3 ]

  ϵ = 1/2 * ( ∇u + ∇u.' )

  σ = fill( 0*x1, dim, dim )

  for ii in 1:dim, jj in 1:dim, kk in 1:dim, ll in 1:dim
    σ[ii,jj] += (Cstiff[ii,jj,kk,ll] * ϵ[kk,ll])[1,1,1,1]
  end

  F  = fill( 0*x1, dim )

  ∇σ = fill( 0*x1, dim, dim, dim )

  for ii in 1:dim, jj in 1:dim
    ∇σ[ii,jj,1] = diff( σ[ii,jj], x1 )
    ∇σ[ii,jj,2] = diff( σ[ii,jj], x2 )
    ∇σ[ii,jj,3] = diff( σ[ii,jj], x3 )
  end

  for ii in 1:dim, jj in 1:dim
    F[ii] -= ∇σ[ii,jj,jj]
  end

  u1func_org = lambdify( u1, [x1, x2, x3] )
  u2func_org = lambdify( u2, [x1, x2, x3] )
  u3func_org = lambdify( u3, [x1, x2, x3] )

  F1func_org  = lambdify( F[1], [x1, x2, x3] )
  F2func_org  = lambdify( F[2], [x1, x2, x3] )
  F3func_org  = lambdify( F[3], [x1, x2, x3] )

  σ11func_org = lambdify( σ[1,1], [x1, x2, x3] )
  σ12func_org = lambdify( σ[1,2], [x1, x2, x3] )
  σ13func_org = lambdify( σ[1,3], [x1, x2, x3] )
  σ22func_org = lambdify( σ[2,2], [x1, x2, x3] )
  σ23func_org = lambdify( σ[2,3], [x1, x2, x3] )
  σ33func_org = lambdify( σ[3,3], [x1, x2, x3] )

  ϵ11func_org = lambdify( ϵ[1,1], [x1, x2, x3] )
  ϵ12func_org = lambdify( ϵ[1,2], [x1, x2, x3] )
  ϵ13func_org = lambdify( ϵ[1,3], [x1, x2, x3] )
  ϵ22func_org = lambdify( ϵ[2,2], [x1, x2, x3] )
  ϵ23func_org = lambdify( ϵ[2,3], [x1, x2, x3] )
  ϵ33func_org = lambdify( ϵ[3,3], [x1, x2, x3] )

  # make sure you can call it with matrices
  u1func = x -> u1func_org.(x[:,1], x[:,2], x[:,3])
  u2func = x -> u2func_org.(x[:,1], x[:,2], x[:,3])
  u3func = x -> u3func_org.(x[:,1], x[:,2], x[:,3])

  F1func = x -> F1func_org.(x[:,1], x[:,2], x[:,3])
  F2func = x -> F2func_org.(x[:,1], x[:,2], x[:,3])
  F3func = x -> F3func_org.(x[:,1], x[:,2], x[:,3])

  σ11func = x -> σ11func_org.(x[:,1], x[:,2], x[:,3])
  σ12func = x -> σ12func_org.(x[:,1], x[:,2], x[:,3])
  σ13func = x -> σ13func_org.(x[:,1], x[:,2], x[:,3])
  σ22func = x -> σ22func_org.(x[:,1], x[:,2], x[:,3])
  σ23func = x -> σ23func_org.(x[:,1], x[:,2], x[:,3])
  σ33func = x -> σ33func_org.(x[:,1], x[:,2], x[:,3])

  ϵ11func = x -> ϵ11func_org.(x[:,1], x[:,2], x[:,3])
  ϵ12func = x -> ϵ12func_org.(x[:,1], x[:,2], x[:,3])
  ϵ13func = x -> ϵ13func_org.(x[:,1], x[:,2], x[:,3])
  ϵ22func = x -> ϵ22func_org.(x[:,1], x[:,2], x[:,3])
  ϵ23func = x -> ϵ23func_org.(x[:,1], x[:,2], x[:,3])
  ϵ33func = x -> ϵ33func_org.(x[:,1], x[:,2], x[:,3])

  return( u1func, u2func, u3func,
          F1func, F2func, F3func,
          σ11func, σ12func, σ13func, σ22func, σ23func, σ33func,
          ϵ11func, ϵ12func, ϵ13func, ϵ22func, ϵ23func, ϵ33func )

end

( u1func, u2func, u3func,
  F1func, F2func, F3func,
  σ11func, σ12func, σ13func, σ22func, σ23func, σ33func,
  ϵ11func, ϵ12func, ϵ13func, ϵ22func, ϵ23func, ϵ33func ) = ExactSol3D( mat.Cstiff )

#   Setup boundary conditions
function funcB( p::Array{Float64} )
  return fill(0.0, size(p,1), 3)
end
bctype = [2,1,2,1,1,1]
funcTop = (p) -> [ σ13func(p)  σ23func(p)  σ33func(p)]
funcBot = (p) -> [-σ13func(p) -σ23func(p) -σ33func(p)]
#   Setup source function
source = (p) -> [F1func(p) F2func(p) F3func(p)]

## Compute error for different polynomial orders and grid sizes
#   Initialize arrays
Err_uh1 = fill( 0.0, length(Ps), length(Ns) )
Err_uh2 = fill( 0.0, length(Ps), length(Ns) )
Err_uh3 = fill( 0.0, length(Ps), length(Ns) )
Err_σh1 = fill( 0.0, length(Ps), length(Ns) )
Err_σh2 = fill( 0.0, length(Ps), length(Ns) )
Err_σh3 = fill( 0.0, length(Ps), length(Ns) )
Err_σh5 = fill( 0.0, length(Ps), length(Ns) )
Err_σh6 = fill( 0.0, length(Ps), length(Ns) )
Err_σh9 = fill( 0.0, length(Ps), length(Ns) )
# Err_ϵh1 = fill( 0.0, length(Ps), length(Ns) )
# Err_ϵh2 = fill( 0.0, length(Ps), length(Ns) )
# Err_ϵh3 = fill( 0.0, length(Ps), length(Ns) )
# Err_ϵh5 = fill( 0.0, length(Ps), length(Ns) )
# Err_ϵh6 = fill( 0.0, length(Ps), length(Ns) )
# Err_ϵh9 = fill( 0.0, length(Ps), length(Ns) )

#   Loop over polynomial order and grid size
for ii in 1:length(Ps), jj in 1:length(Ns)

  P = Ps[ii]; N = Ns[jj]

  @printf( " %6i %6i\n", P.p, N )

  mesh   = Mesh3D( "cube", P, N = N)
  master = Master3D( P )

  prob = Elas( @sprintf("Reg %i %i", P.p, N), mat, source, bctype, false, [funcBot, funcB, funcTop, funcB, funcB, funcB] )

  (uhath, uh, σh) = hdgSolve( master, mesh, prob )

  #   Initialize arrays
  err_uh = fill( 0.0, dim )
  err_σh = fill( 0.0, dim^2 )
  # err_ϵh = fill( 0.0, dim^2 )

  # preallocate
  jcwd = fill( 0.0, size(master.∇ϕ,2), size(master.∇ϕ,2) )
  ∂ξ∂x = fill( 0.0, size(master.∇ϕ,2), dim^2 )
  ∂x∂ξ = fill( 0.0, size(master.∇ϕ,2), dim^2 )

  for kk in 1:size(mesh.t,1)

    # Compute Jacobians
    luteos.compJacob!( master, mesh.nodes[:,:,kk], ∂ξ∂x, jcwd, ∂x∂ξ )

    # u
    Δuh1       = master.ϕ' * ( uh[:,1,kk] - u1func( mesh.nodes[:,:,kk] ) )
    Δuh2       = master.ϕ' * ( uh[:,2,kk] - u2func( mesh.nodes[:,:,kk] ) )
    Δuh3       = master.ϕ' * ( uh[:,3,kk] - u3func( mesh.nodes[:,:,kk] ) )
    err_uh[1] += Δuh1' * jcwd * Δuh1
    err_uh[2] += Δuh2' * jcwd * Δuh2
    err_uh[3] += Δuh3' * jcwd * Δuh3

    # σ
    Δσh1       = master.ϕ' * ( σh[:,1,kk] - σ11func( mesh.nodes[:,:,kk] ) )
    Δσh2       = master.ϕ' * ( σh[:,2,kk] - σ12func( mesh.nodes[:,:,kk] ) )
    Δσh3       = master.ϕ' * ( σh[:,3,kk] - σ13func( mesh.nodes[:,:,kk] ) )
    Δσh5       = master.ϕ' * ( σh[:,5,kk] - σ22func( mesh.nodes[:,:,kk] ) )
    Δσh6       = master.ϕ' * ( σh[:,6,kk] - σ23func( mesh.nodes[:,:,kk] ) )
    Δσh9       = master.ϕ' * ( σh[:,9,kk] - σ33func( mesh.nodes[:,:,kk] ) )
    err_σh[1] += Δσh1' * jcwd * Δσh1
    err_σh[2] += Δσh2' * jcwd * Δσh2
    err_σh[3] += Δσh3' * jcwd * Δσh3
    err_σh[5] += Δσh5' * jcwd * Δσh5
    err_σh[6] += Δσh6' * jcwd * Δσh6
    err_σh[9] += Δσh9' * jcwd * Δσh9

    # # ϵ
    # Δϵh1       = master.ϕ' * ( ϵh[:,1,kk] - ϵ11func( mesh.nodes[:,:,kk] ) )
    # Δϵh2       = master.ϕ' * ( ϵh[:,2,kk] - ϵ12func( mesh.nodes[:,:,kk] ) )
    # Δϵh3       = master.ϕ' * ( ϵh[:,3,kk] - ϵ13func( mesh.nodes[:,:,kk] ) )
    # Δϵh5       = master.ϕ' * ( ϵh[:,5,kk] - ϵ22func( mesh.nodes[:,:,kk] ) )
    # Δϵh6       = master.ϕ' * ( ϵh[:,6,kk] - ϵ23func( mesh.nodes[:,:,kk] ) )
    # Δϵh9       = master.ϕ' * ( ϵh[:,9,kk] - ϵ33func( mesh.nodes[:,:,kk] ) )
    # err_ϵh[1] += Δϵh1' * jcwd * Δϵh1
    # err_ϵh[2] += Δϵh2' * jcwd * Δϵh2
    # err_ϵh[3] += Δϵh3' * jcwd * Δϵh3
    # err_ϵh[5] += Δϵh5' * jcwd * Δϵh5
    # err_ϵh[6] += Δϵh6' * jcwd * Δϵh6
    # err_ϵh[9] += Δϵh9' * jcwd * Δϵh9

  end

  Err_uh1[ii,jj] = sqrt(err_uh[1])
  Err_uh2[ii,jj] = sqrt(err_uh[2])
  Err_uh3[ii,jj] = sqrt(err_uh[3])

  Err_σh1[ii,jj] = sqrt(err_σh[1])
  Err_σh2[ii,jj] = sqrt(err_σh[2])
  Err_σh3[ii,jj] = sqrt(err_σh[3])
  Err_σh5[ii,jj] = sqrt(err_σh[5])
  Err_σh6[ii,jj] = sqrt(err_σh[6])
  Err_σh9[ii,jj] = sqrt(err_σh[9])

  # Err_ϵh1[ii,jj] = sqrt(err_ϵh[1])
  # Err_ϵh2[ii,jj] = sqrt(err_ϵh[2])
  # Err_ϵh3[ii,jj] = sqrt(err_ϵh[3])
  # Err_ϵh5[ii,jj] = sqrt(err_ϵh[5])
  # Err_ϵh6[ii,jj] = sqrt(err_ϵh[6])
  # Err_ϵh9[ii,jj] = sqrt(err_ϵh[9])

end

# Compute convergence rates
h = 1 ./ ( Ns - 1 )
conv_uh1 = (log.( Err_uh1[:,end-1]) - log.( Err_uh1[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
conv_uh2 = (log.( Err_uh2[:,end-1]) - log.( Err_uh2[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
conv_uh3 = (log.( Err_uh3[:,end-1]) - log.( Err_uh3[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));

conv_σh1 = (log.( Err_σh1[:,end-1]) - log.( Err_σh1[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
conv_σh2 = (log.( Err_σh2[:,end-1]) - log.( Err_σh2[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
conv_σh3 = (log.( Err_σh3[:,end-1]) - log.( Err_σh3[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
conv_σh5 = (log.( Err_σh5[:,end-1]) - log.( Err_σh5[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
conv_σh6 = (log.( Err_σh6[:,end-1]) - log.( Err_σh6[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
conv_σh9 = (log.( Err_σh9[:,end-1]) - log.( Err_σh9[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));

# conv_ϵh1 = (log.( Err_ϵh1[:,end-1]) - log.( Err_ϵh1[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
# conv_ϵh2 = (log.( Err_ϵh2[:,end-1]) - log.( Err_ϵh2[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
# conv_ϵh3 = (log.( Err_ϵh3[:,end-1]) - log.( Err_ϵh3[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
# conv_ϵh5 = (log.( Err_ϵh5[:,end-1]) - log.( Err_ϵh5[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
# conv_ϵh6 = (log.( Err_ϵh6[:,end-1]) - log.( Err_ϵh6[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));
# conv_ϵh9 = (log.( Err_ϵh9[:,end-1]) - log.( Err_ϵh9[:,end] ) ) / (log.( h[end-1]) - log.( h[end] ));

# Output to terminal
@printf("\n")
@printf("   Convergence rates for 3D Dirichlet + Neumann problem\n\n")
@printf("   ------------------------------------------\n\n")
@printf( "P    ")
for jj in 1:size(Ps,1)
  @printf( " %6i", Ps[jj].p )
end
@printf( "\n" )

#   u
@printf( "u₁   ")
for jj in 1:size(Ps,1)
  @printf( " %6.4f", conv_uh1[jj] )
end
@printf( "\n" )
@printf( "u₂   ")
for jj in 1:size(Ps,1)
  @printf( " %6.4f", conv_uh2[jj] )
end
@printf( "\n" )
@printf( "u₃   ")
for jj in 1:size(Ps,1)
  @printf( " %6.4f", conv_uh3[jj] )
end
@printf( "\n" )

#   σ
@printf( "σ₁₁  ")
for jj in 1:length(Ps)
  @printf( " %6.4f", conv_σh1[jj] )
end
@printf( "\n" )
@printf( "σ₁₂  ")
for jj in 1:length(Ps)
  @printf( " %6.4f", conv_σh2[jj] )
end
@printf( "\n" )
@printf( "σ₁₃  ")
for jj in 1:length(Ps)
  @printf( " %6.4f", conv_σh3[jj] )
end
@printf( "\n" )
@printf( "σ₂₂  ")
for jj in 1:length(Ps)
  @printf( " %6.4f", conv_σh5[jj] )
end
@printf( "\n" )
@printf( "σ₂₃  ")
for jj in 1:length(Ps)
  @printf( " %6.4f", conv_σh6[jj] )
end
@printf( "\n" )
@printf( "σ₃₃  ")
for jj in 1:length(Ps)
  @printf( " %6.4f", conv_σh9[jj] )
end
@printf( "\n" )

# #   ϵ
# @printf( "ϵ₁₁  ")
# for jj in 1:length(Ps)
#   @printf( " %6.4f", conv_ϵh1[jj] )
# end
# @printf( "\n" )
# @printf( "ϵ₁₂  ")
# for jj in 1:length(Ps)
#   @printf( " %6.4f", conv_ϵh2[jj] )
# end
# @printf( "\n" )
# @printf( "ϵ₁₃  ")
# for jj in 1:length(Ps)
#   @printf( " %6.4f", conv_ϵh3[jj] )
# end
# @printf( "\n" )
# @printf( "ϵ₂₂  ")
# for jj in 1:length(Ps)
#   @printf( " %6.4f", conv_ϵh5[jj] )
# end
# @printf( "\n" )
# @printf( "ϵ₂₃  ")
# for jj in 1:length(Ps)
#   @printf( " %6.4f", conv_ϵh6[jj] )
# end
# @printf( "\n" )
# @printf( "ϵ₃₃  ")
# for jj in 1:length(Ps)
#   @printf( " %6.4f", conv_ϵh9[jj] )
# end
# @printf( "\n" )

open("errors_Elas_Neumann3D.dat", "w") do f
  @printf(f, "P \t N \t E_uh1 \t E_uh2 \t E_uh3 \t E_σh1 \t E_σh2 \t E_σh3 \t E_σh5 \t E_σh6 \t E_σh9 \n")#\t E_ϵh1 \t E_ϵh2 \t E_ϵh3 \t E_ϵh5 \t E_ϵh6 \t E_ϵh9 \n")#\t E_J\n")
  for ii in 1:length(Ps), jj in 1:length(Ns)
    @printf(f, "%i \t %i \t %16.15e \t %16.15e \t %16.15e \t %16.15e \t %16.15e \t %16.15e \t %16.15e \t %16.15e \t %16.15e \n", #\t %16.15e \t %16.15e \t %16.15e \t %16.15e \t %16.15e \t %16.15e\n",
      Ps[ii].p, Ns[jj], Err_uh1[ii,jj], Err_uh2[ii,jj], Err_uh3[ii,jj],
      Err_σh1[ii,jj], Err_σh2[ii,jj], Err_σh3[ii,jj], Err_σh5[ii,jj], Err_σh6[ii,jj], Err_σh9[ii,jj] )#,
      # Err_ϵh1[ii,jj], Err_ϵh2[ii,jj], Err_ϵh3[ii,jj], Err_ϵh5[ii,jj], Err_ϵh6[ii,jj], Err_ϵh9[ii,jj] )
  end
end

# end # limit scope


export array_qudiff, prepare_init_state, bval, aval

"""
    Based on : arxiv.org/abs/1010.2745v2

    x' = Ax + b

    * A - input matrix.
    * b - input vector.
    * x - inital vector
    * N - dimension of b (as a power of 2).
    * h - step size.
    * tspan - time span.

    * array_qudiff(N_t,N,h,A) - generates matrix for k-step solver
    * prepare_init_state(b,x,h,N_t) - generates inital states

    LDEMSAlgHHL
    * step - step for multistep method
    * α - coefficients for xₙ
    * β - coefficent for xₙ'
"""

function bval(g::Function,alg::LDEMSAlgHHL,t,h)
    b = zero(g(1))
    for i in 1:(alg.step)
        b += alg.β[i]*g(t-(i-1)*h)
    end
    return b
end

function aval(g::Function,alg::LDEMSAlgHHL,t,h)
    sz, = size(g(1))
    A = Array{ComplexF64}(undef,sz,(alg.step + 1)*sz)
    i_mat = Matrix{Float64}(I, size(g(1)))
    A[1:sz,sz*(alg.step) + 1:sz*(alg.step + 1)] = i_mat
    for i in 1:alg.step
        A[1:sz,sz*(i - 1) + 1: sz*i] = -1*(alg.α[alg.step - i + 1]*i_mat + h*alg.β[alg.step - i + 1]*g(t - (alg.step - i)*h))
    end
    return A
end

function prepare_init_state(g::Function,alg::LDEMSAlgHHL,tspan::NTuple{2, Float64},x::Vector,h::Float64)
    N_t = round(Int, (tspan[2] - tspan[1])/h + 1) #number of time steps
    N = nextpow(2,2*N_t + 1) # To ensure we have a power of 2 dimension for matrix
    sz, = size(g(1))
    init_state = zeros(ComplexF64,2*(N)*sz)
    #inital value
    init_state[1:sz] = x
    for i in 2:N_t
        b = bval(alg,h*(i - 1) + tspan[1],h) do t g(t) end
        init_state[Int(sz*(i - 1) + 1):Int(sz*(i))] = h*b
    end
    return init_state
end

function array_qudiff(g::Function,alg::LDEMSAlgHHL,tspan::NTuple{2, Float64},h::Float64)
    sz, = size(g(1))
    i_mat = Matrix{Float64}(I, size(g(1)))
    N_t = round(Int, (tspan[2] - tspan[1])/h + 1) #number of time steps
    N = nextpow(2,2*N_t + 1) # To ensure we have a power of 2 dimension for matrix
    A_ = zeros(ComplexF64, N*sz, N*sz)
    # Generates First two rows
    @inbounds A_[1:sz, 1:sz] = i_mat
    @inbounds A_[sz + 1:2*sz, 1:sz] = -1*(i_mat + h*g(tspan[1]))
    @inbounds A_[sz + 1:2*sz,sz+1:sz*2] = i_mat
    #Generates additional rows based on k - step
    for i in 3:alg.step
        @inbounds A_[sz*(i - 1) + 1:sz*i, sz*(i - 3) + 1:sz*i] = aval(QuAB2(),(i-2)*h + tspan[1],h) do t g(t) end
    end
    for i in alg.step + 1:N_t
        @inbounds A_[sz*(i - 1) + 1:sz*(i), sz*(i - alg.step - 1) + 1:sz*i] = aval(alg,(i - 2)*h + tspan[1],h) do t g(t) end
    end
    #Generates half mirroring matrix
    for i in N_t + 1:N
        @inbounds A_[sz*(i - 1) + 1:sz*(i), sz*(i - 2) + 1:sz*(i - 1)] = -1*i_mat
        @inbounds A_[sz*(i - 1) + 1:sz*(i), sz*(i - 1) + 1:sz*i] = i_mat
    end
    A_ = [zero(A_) A_;A_' zero(A_)]
    return A_
end

function DiffEqBase.solve(prob::QuLDEProblem{uType,tType,isinplace, F, P}, alg::LDEMSAlgHHL; dt = (prob.tspan[2]-prob.tspan[1])/100, kwargs...) where {uType,tType,isinplace, F, P}
    A = prob.A
    b = prob.b
    tspan = prob.tspan
    x = prob.u0
    nreg = alg.nreg

    matx = array_qudiff(alg, tspan, dt) do t A(t) end
    initstate = prepare_init_state(alg, tspan, x, dt) do t b(t) end
    λ = maximum(eigvals(matx))
    C_value = minimum(eigvals(matx) .|> abs)*0.01;
    matx = 1/(λ*2)*matx
    initstate = initstate*1/(2*λ) |> normalize!
    res = hhlsolve(matx,initstate, nreg, C_value)
    res = res/λ
    return res
end;


# These routines should be added to CartesianGrids

import CartesianGrids: cross!

"""
    cross!(C::VectorData,A::ScalarData/VectorData,B::VectorData/ScalarData) -> VectorData

Compute the cross product between the point data `A` and `B`, one of which
is scalar data and treated as an out-of-plane component of a vector, while
the other is in-plane vector data, and return the result as vector data `C`.
"""
function cross!(C::VectorData{N},A::ScalarData{N},B::VectorData{N}) where {N}
    @. C.u = -A*B.v
    @. C.v = A*B.u
    return C
end

function cross!(C::VectorData{N},A::VectorData{N},B::ScalarData{N}) where {N}
    @. C.u = A.v*B
    @. C.v = -A.u*B
    return C
end


module TestReparametrization
include("preamble.jl")

using StaticArrays: SVector

using Bifurcations.Examples.Reparametrization: reparametrize, forward, backward
using Bifurcations.Examples.Bautin

ode0 = reparametrize(Bautin.ode)
ode1 = reparametrize(Bautin.ode; seed=1, extra=SVector(-1.0))
ode2 = reparametrize(Bautin.ode; seed=2, extra=SVector(-1.0, +1.0))

@testset begin
    x1 = [0.1, 0.1]
    y1 = forward(x1, ode0.p)
    x2 = backward(y1, ode0.p)
    y2 = forward(x2, ode0.p)
    @test x1 ≈ x2
    @test y1 ≈ y2

    x1 = [0.1, 0.1, 0.1]
    y1 = forward(x1, ode1.p)
    x2 = backward(y1, ode1.p)
    y2 = forward(x2, ode1.p)
    @test x1 ≈ x2
    @test y1 ≈ y2

    x1 = [0.1, 0.1, 0.1, 0.1]
    y1 = forward(x1, ode2.p)
    x2 = backward(y1, ode2.p)
    y2 = forward(x2, ode2.p)
    @test x1 ≈ x2
    @test y1 ≈ y2
end

@testset "Random test dim=$dim" for (dim, ode) in [(2, ode0),
                                                   (3, ode1),
                                                   (4, ode2)]
    for seed in 1:10
        rng = MersenneTwister(seed)
        x1 = randn(rng, dim)
        y1 = forward(x1, ode.p)
        x2 = backward(y1, ode.p)
        y2 = forward(x2, ode.p)
        @test x1 ≈ x2
        @test y1 ≈ y2
    end
end

end  # module


module CirculantCov

using Dierckx: Spline1D
using ApproxFun: Fun, Jacobi
using FastTransforms: jac2cheb
using FFTW: plan_fft
using LinearAlgebra: mul!

# J
# ==================================================

"""
Jperm(ℓ::Int, n::Int) return the column number in the J matrix U^2
where U is unitary FFT. The J matrix looks like this:

|1   0|
|  / 1|
| / / |
|0 1  |

"""
function Jperm end

function Jperm(ℓ::Int, n::Int)
    @assert 1 <= ℓ <= n
    ℓ==1 ? 1 : n - ℓ + 2
end

# geometric methods with angles and geodesics and periodize
# ==================================================

function periodize(f::Vector{T}, freq_mult::Int) where {T}
    n = length(f)
    nfm = n÷freq_mult
    @assert nfm == n//freq_mult
    f′ = sum( circshift(f, k*nfm) for k=0:freq_mult-1)
    f′[1:nfm]
end

in_0_2π(φ) = rem2pi(φ, RoundDown)

counterclock_Δφ(φstart, φstop) = in_0_2π(φstop - φstart)

function fullcircle(φ::AbstractVector)
    Δφpix  = counterclock_Δφ(φ[1], φ[2])
    Δφspan = counterclock_Δφ(φ[1], φ[end]) + Δφpix
    # The extra Δφpix makes Δφspan measure the (angular) distance between the
    # left boundary of the starting pixel and the right boundary of the ending pixel

    @assert div(2π, Δφspan, RoundNearest) ≈ 2π / Δφspan
    freq_mult = Int(div(2π, Δφspan, RoundNearest))

    nφ2π = length(φ)*freq_mult
    φ2π  = @. in_0_2π(φ[1] + 2π * (0:nφ2π-1) / nφ2π)

    return φ2π, freq_mult
end

"""
# slated for removal ...
`fraccircle(∂φstart, ∂φstop, nφ)` specifies a uniform grid on a
contiguous interval of azumimuth. `∂φstart` begins the interval. Moving counter
clockwise (looking down from the north pole) to `∂φstop`.
Only integer fractions are allowed and both `∂φstart`, `∂φstop` must be `≥ 0`.

Note: `(∂φstart, ∂φstop) = (5.3, 1.0) ≡ (5.3, 1.0 + 2π)`
"""
# function fraccircle(∂φstart, ∂φstop, nφ)
#     ∂φstart′, ∂φstop′ = in_0_2π(∂φstart), in_0_2π(∂φstop)
#     Δφspan = counterclock_Δφ(∂φstart′, ∂φstop′)
#     φ  = @. in_0_2π(∂φstart′ + Δφspan * (0:nφ-1) / nφ)
#     return φ
# end

"""
`geoβ(θ₁, θ₂, φ₁, φ₂)` -> Geodesic between two spherical points at
(θ₁, φ₁) and (θ₂, φ₂).
"""
function geoβ(θ₁, θ₂, φ₁, φ₂)
    sΔθ½, sΔφ½ = sin((θ₁ - θ₂)/2), sin((φ₁ - φ₂)/2)
    2asin(√(sΔθ½^2 + sin(θ₁)*sin(θ₂) * sΔφ½^2))
end

"""
`cosgeoβ(θ₁, θ₂, φ₁, φ₂)` -> Cosine of the geodesic between two spherical points
at (θ₁, φ₁) and (θ₂, φ₂).
"""
function cosgeoβ(θ₁, θ₂, φ₁, φ₂)
    cos(θ₁-θ₂) - sin(θ₁)*sin(θ₂)*(1-cos(φ₁-φ₂))/2
end

"""
`βsingrid(ngrid, period)` creats a grid of points on [0,period) ⊂ [0,π)
with extra density near the endpoints
"""
βsingrid(ngrid, period)   = shift_scale_sin.(range(0,π,length=ngrid), period)

"""
`shift_scale_sin(β,period)` internal function used in `βsingrid`
"""
shift_scale_sin(β,period) = period * (sin(π*β/period - π/2) + 1) / 2

# Methods for generating some useful polar grids
# ================================================

function θ_healpix_j_Nside(j_Nside)
    0 < j_Nside < 1  ? acos(1-abs2(j_Nside)/3)      :
    1 ≤ j_Nside ≤ 3  ? acos(2*(2-j_Nside)/3)        :
    3 < j_Nside < 4  ? acos(-(1-abs2(4-j_Nside)/3)) :
    error("argument ∉ (0,4)")
end

θ_healpix(Nside) = θ_healpix_j_Nside.((1:4Nside-1)/Nside)

function θ_grid(;θspan::Tuple{T,T}, N::Int, type=:equiθ) where T<:Real
    @assert N > 0
    @assert 0 < θspan[1] < θspan[2] < π

    if type==:equiθ
        θgrid″ = range(θspan[1], θspan[2], length=N+2)
    elseif type==:equicosθ
        θgrid″ = acos.(range(cos(θspan[2]), cos(θspan[1]), length=N+2)[end:-1:1])
    elseif type==:healpix
        @warn """
            When `type` argument is set to `:healpix` the parameter `N` corresponds
            to Healpix `Nside`, _not_ the number of θ grid points within the interval
            specified by `θspan` as it does when `type ∈ {:equiθ, :equicosθ}`.
            """
        θgrid′ = θ_healpix(N)
        θgrid″ = θgrid′[θspan[1] .≤ θgrid′ .≤ θspan[2]]
    else
        error("`type` argument variable is not a valid option. Choose from `type ∈ {:equiθ, :equicosθ, :healpix}`")
    end

    # θgrid″ subsets θgrid′ to be within θspan
    # δ½south″ and δ½north″ are the arclength midpoints to the adjacent pixel
    δ½south″ = (circshift(θgrid″,-1)  .- θgrid″) ./ 2
    δ½north″ = (θgrid″ .- circshift(θgrid″,1)) ./ 2

    # now restrict to the interior of the range of θgrid″
    θ       = θgrid″[2:end-1]
    δ½south = δ½south″[2:end-1]
    δ½north = δ½north″[2:end-1]

    # These are the pixel boundaries along polar
    # so length(θ∂) == length(θ)+1
    θ∂ = vcat(θ[1] .- δ½north[1], θ .+ δ½south)

    return θ, θ∂
end

"""
`φ_grid(;φspan::Tuple{T,T}, N::Int)` specifies a uniform grid on a
contiguous interval of azumimuth. `∂φstart` begins the interval. Moving counter
clockwise (looking down from the north pole) to `∂φstop`.
Only integer fractions are allowed and both `∂φstart`, `∂φstop` must be `≥ 0`.

Note: `(∂φstart, ∂φstop) = (5.3, 1.0) ≡ (5.3, 1.0 + 2π)`
"""
function φ_grid(;φspan::Tuple{T,T}, N::Int) where T<:Real
    ∂φstart′, ∂φstop′ = in_0_2π(φspan[1]), in_0_2π(φspan[2])
    Δφspan = counterclock_Δφ(∂φstart′, ∂φstop′)
    φ∂  = @. in_0_2π(∂φstart′ + Δφspan * (0:N) / N)
    Δφ  = Δφspan / N
    φ   = φ∂[1:end-1] .+ Δφ / 2
    return φ, φ∂
end

# these are the generic versions ...
# you just need to define types for Γθ₁θ₂φ₁φ⃗, Cθ₁θ₂φ₁φ⃗
# for dispatch
# ==================================================

# Note: the reason we restrict to φ::AbstractVector is that Spline1D's are optimized for
# Union{Vector, Number} so in general it is better to broadcast
# via via whole columns

# overload this
function γθ₁θ₂φ⃗_ξθ₁θ₂φ⃗(θ₁::Real, θ₂::Real, φ::AbstractVector, Γθ₁θ₂φ₁φ⃗, Cθ₁θ₂φ₁φ⃗)

    φ2π, freq_mult = fullcircle(φ)
    covPP̄  = Γθ₁θ₂φ₁φ⃗(θ₁, θ₂, φ2π[1], φ2π)
    covPP  = Cθ₁θ₂φ₁φ⃗(θ₁, θ₂, φ2π[1], φ2π)
    covPP̄′ = periodize(covPP̄, freq_mult)
    covPP′ = periodize(covPP, freq_mult)

    return covPP̄′, covPP′
end

# overload this
function γθ₁θ₂φ⃗(θ₁::Real, θ₂::Real, φ::AbstractVector, Γθ₁θ₂φ₁φ⃗)

    φ2π, freq_mult = fullcircle(φ)
    covPP̄  = Γθ₁θ₂φ₁φ⃗(θ₁, θ₂, φ2π[1], φ2π)
    covPP̄′ = periodize(covPP̄, freq_mult)

    return covPP̄′
end

# behavior comes directly from γθ₁θ₂φ⃗_ξθ₁θ₂φ⃗
function γθ₁θ₂ℓ⃗_ξθ₁θ₂ℓ⃗(
    θ₁::Real, θ₂::Real, φ::AbstractVector, Γθ₁θ₂φ₁φ⃗, Cθ₁θ₂φ₁φ⃗,
    planFFT = FFTW.plan_fft(Vector{ComplexF64}(undef,length(φ))),
    γstorage = Vector{ComplexF64}(undef,length(φ)),
    ξstorage = Vector{ComplexF64}(undef,length(φ)),
    )

    γ₁₂φ⃗, ξ₁₂φ⃗ =  γθ₁θ₂φ⃗_ξθ₁θ₂φ⃗(θ₁, θ₂, φ, Γθ₁θ₂φ₁φ⃗, Cθ₁θ₂φ₁φ⃗)

    mul!(γstorage, planFFT, γ₁₂φ⃗)
    mul!(ξstorage, planFFT, ξ₁₂φ⃗)

    return γstorage, ξstorage
end

# behavior comes directly from γθ₁θ₂φ⃗_ξθ₁θ₂φ⃗
function γθ₁θ₂ℓ⃗(
    θ₁::Real, θ₂::Real, φ::AbstractVector, Γθ₁θ₂φ₁φ⃗,
    planFFT  = FFTW.plan_fft(Vector{ComplexF64}(undef,length(φ))),
    γstorage = Vector{ComplexF64}(undef,length(φ)),
    )

    γ₁₂φ⃗ = γθ₁θ₂φ⃗(θ₁, θ₂, φ, Γθ₁θ₂φ₁φ⃗)
    mul!(γstorage, planFFT, γ₁₂φ⃗)

    return γstorage
end

# structs Γθ₁θ₂φ₁φ⃗_CMBpol and Cθ₁θ₂φ₁φ⃗_CMBpol
# =====================================================

"""
IAU uses rotation around outward normal to the sphere => Q + iU is spin (+2)
"""
struct Γθ₁θ₂φ₁φ⃗_CMBpol
    IAU::Bool
    premult_spln::Spline1D
end

struct Cθ₁θ₂φ₁φ⃗_CMBpol
    IAU::Bool
    premult_spln::Spline1D
end

# Constructor for both Γ and C
function ΓCθ₁θ₂φ₁φ⃗_CMBpol(
        ℓ, eeℓ, bbℓ;
        IAU = false,
        ngrid::Int = 100_000,
        βgrid = βsingrid(ngrid, π),
    )
    @assert ℓ[1] == 0
    @assert ℓ[2] == 1
    @assert IAU == false # TODO remove this an impliment the spin(+2) version
    nℓ = @. (2ℓ+1)/(4π)
    ## ↓ starts at 2 since the Jacobi expansion goes like J^(a,b)_{ℓ-2}
    j2⁺2ℓ = (@. (eeℓ + bbℓ) * nℓ)[2:end]
    j2⁻2ℓ = (@. (eeℓ - bbℓ) * nℓ)[2:end]
    ## ↓  TODO: check the a,b swap
    f2⁺2  = ((a,b,jℓ)=(0,4,j2⁺2ℓ);  Fun(Jacobi(b,a),jℓ))
    f2⁻2  = ((a,b,jℓ)=(4,0,j2⁻2ℓ);  Fun(Jacobi(b,a),jℓ))
    # !! pre-canceled out cos β½ and sin β½ in the denom
    covPP̄ = x-> f2⁺2(cos(x))
    covPP = x-> f2⁻2(cos(x))
    β2covPP̄ = Spline1D(βgrid, covPP̄.(βgrid), k=3)
    β2covPP = Spline1D(βgrid, covPP.(βgrid), k=3)
    Γθ₁θ₂φ₁φ⃗_CMBpol(IAU, β2covPP̄), Cθ₁θ₂φ₁φ⃗_CMBpol(IAU, β2covPP)
end

# Hook into method γθ₁θ₂φ⃗_ξθ₁θ₂φ⃗
function γθ₁θ₂φ⃗_ξθ₁θ₂φ⃗(
    θ₁::Real, θ₂::Real, φ::AbstractVector,
    Γθ₁θ₂φ₁φ⃗::Γθ₁θ₂φ₁φ⃗_CMBpol,
    Cθ₁θ₂φ₁φ⃗::Cθ₁θ₂φ₁φ⃗_CMBpol,
    )

    φ2π, freq_mult = fullcircle(φ)
    β      = geoβ.(θ₁, θ₂, φ2π[1], φ2π)
    covPP̄  = Γθ₁θ₂φ₁φ⃗.premult_spln(β) .* multPP̄.(θ₁, θ₂, φ2π[1], φ2π)
    covPP  = Cθ₁θ₂φ₁φ⃗.premult_spln(β) .* multPP.(θ₁, θ₂, φ2π[1], φ2π)
    covPP̄′ = periodize(covPP̄, freq_mult)
    covPP′ = periodize(covPP, freq_mult)

    return covPP̄′, covPP′
end

# for isotropic spin 0
# =======

struct Γθ₁θ₂φ₁φ⃗_Iso
    spln::Spline1D
end

# constructor
function Γθ₁θ₂φ₁φ⃗_Iso(
        ℓ, ttℓ;
        ngrid::Int = 100_000,
        βgrid = βsingrid(ngrid,π),
    )
    @assert ℓ[1] == 0
    @assert ℓ[2] == 1
    nℓ = @. (2ℓ+1)/(4π)
    j0⁺0tℓ = @. ttℓ * nℓ
    f0⁺0t = ((a,b,jℓ)=(0,0,j0⁺0tℓ); Fun(Jacobi(b,a),jℓ))
    covtt = x-> f0⁺0t(cos(x))
    β2covtt = Spline1D(βgrid, covtt.(βgrid), k=3)
    Γθ₁θ₂φ₁φ⃗_Iso(β2covtt)
end

# Hook into method γθ₁θ₂φ⃗
function γθ₁θ₂φ⃗(
    θ₁::Real, θ₂::Real, φ::AbstractVector,
    Γθ₁θ₂φ₁φ⃗::Γθ₁θ₂φ₁φ⃗_Iso,
    )

    φ2π, freq_mult = fullcircle(φ)
    β      = geoβ.(θ₁, θ₂, φ2π[1], φ2π)
    covPP̄  = Γθ₁θ₂φ₁φ⃗.spln(β)
    covPP̄′ = periodize(covPP̄, freq_mult)

    return complex(covPP̄′)
end

# Multipliers needed to convert the isotropic parts to full polarization cov
# =====================================================

function sincosΔθpθΔφ(θ₁, θ₂, φ₁, φ₂)
    𝓅θ½ = (θ₁ + θ₂)/2
    Δθ½ = (θ₁ - θ₂)/2
    Δφ½ = (φ₁ - φ₂)/2
    s𝓅θ½, c𝓅θ½ = sincos(𝓅θ½)
    sΔθ½, cΔθ½ = sincos(Δθ½)
    sΔφ½, cΔφ½ = sincos(Δφ½)
    return sΔθ½, sΔφ½, cΔθ½, cΔφ½, s𝓅θ½, c𝓅θ½
end

function multPP̄(θ₁, θ₂, φ₁, φ₂)
    sΔθ½, sΔφ½, cΔθ½, cΔφ½, s𝓅θ½, c𝓅θ½ = sincosΔθpθΔφ(θ₁, θ₂, φ₁, φ₂)
    return complex(sΔφ½ * c𝓅θ½,   cΔφ½ * cΔθ½)^4
end

function multPP(θ₁, θ₂, φ₁, φ₂)
    sΔθ½, sΔφ½, cΔθ½, cΔφ½, s𝓅θ½, c𝓅θ½ = sincosΔθpθΔφ(θ₁, θ₂, φ₁, φ₂)
    return complex(sΔφ½ * s𝓅θ½, - cΔφ½ * sΔθ½)^4
end

## multII(θ₁, θ₂, φ₁, φ₂) = 1

Q1Q2(covPP̄, covPP) = ( real(covPP̄) + real(covPP) ) / 2

U1U2(covPP̄, covPP) = ( real(covPP̄) - real(covPP) ) / 2

Q1U2(covPP̄, covPP) = ( imag(covPP̄) + imag(covPP) ) / 2

U1Q2(covPP̄, covPP) = (- imag(covPP̄) + imag(covPP) ) / 2

#
# ==================================================

"""
`cheb2spherecov(β, θs)` compute auto-covariance function at
angular distances `θs` from chebyshev expansion.
"""
function cheb2spherecov(β, θs)
    cθ = zero(θs)
    n  = length(β)
    for i ∈ eachindex(cθ)
        for (k,kp1) ∈ zip(0:n-1, 1:n)
            @inbounds cθ[i] += cos(k * θs[i]) * β[kp1]
        end
    end
    return cθ
end

"""
`spec2spherecov(cl, θs)` compute auto-covariance function at
angular distances `θs` from spherical spectral density `cl`.
Note: `cl` is assumed to be a vector whose values correspond to
multipoles `l = [0,1,2 ... ,lmax]`
"""
function spec2spherecov(cl, θs)
    lmax = length(cl)-1
    l    = 0:lmax
    j00l = @. cl * (2l + 1) / (4π)
    β    = jac2cheb(j00l, 0, 0)
    cheb2spherecov(β, θs)
end

# These are slated for removal ...
# ==================================================

struct βcovSpin2
    covPP̄_premult_spln::Spline1D
    covPP_premult_spln::Spline1D
end

struct βcovSpin0
    covII_premult_spln::Spline1D
end

function βcovSpin2(
        ℓ, eeℓ, bbℓ;
        ngrid::Int = 100_000,
        βgrid = βsingrid(ngrid, π),
    )
    @assert ℓ[1] == 0
    @assert ℓ[2] == 1
    nℓ = @. (2ℓ+1)/(4π)
    ## ↓ starts at 2 since the Jacobi expansion goes like J^(a,b)_{ℓ-2}
    j2⁺2ℓ = (@. (eeℓ + bbℓ) * nℓ)[2:end]
    j2⁻2ℓ = (@. (eeℓ - bbℓ) * nℓ)[2:end]
    ## ↓  TODO: check the a,b swap
    f2⁺2  = ((a,b,jℓ)=(0,4,j2⁺2ℓ);  Fun(Jacobi(b,a),jℓ))
    f2⁻2  = ((a,b,jℓ)=(4,0,j2⁻2ℓ);  Fun(Jacobi(b,a),jℓ))
    # !! pre-canceled out cos β½ and sin β½ in the denom
    covPP̄ = x-> f2⁺2(cos(x))
    covPP = x-> f2⁻2(cos(x))
    β2covPP̄ = Spline1D(βgrid, covPP̄.(βgrid), k=3)
    β2covPP = Spline1D(βgrid, covPP.(βgrid), k=3)
    βcovSpin2(β2covPP̄, β2covPP)
end

function βcovSpin0(
        ℓ, ttℓ;
        ngrid::Int = 100_000,
        βgrid = βsingrid(ngrid,π),
    )
    @assert ℓ[1] == 0
    @assert ℓ[2] == 1
    nℓ = @. (2ℓ+1)/(4π)
    j0⁺0tℓ = @. ttℓ * nℓ
    f0⁺0t = ((a,b,jℓ)=(0,0,j0⁺0tℓ); Fun(Jacobi(b,a),jℓ))
    covtt = x-> f0⁺0t(cos(x))
    β2covtt = Spline1D(βgrid, covtt.(βgrid), k=3)
    βcovSpin0(β2covtt)
end

function (covP::βcovSpin2)(β::Matrix)
    rtnPP̄ = similar(β)
    rtnPP = similar(β)
    for (col, cβ) ∈ enumerate(eachcol(β))
        rtnPP̄[:,col] = covP.covPP̄_premult_spln(cβ)
        rtnPP[:,col] = covP.covPP_premult_spln(cβ)
    end
    return complex(rtnPP̄), complex(rtnPP)
end
function (covP::βcovSpin2)(β::Union{Vector, Number})
    rtnPP̄ = covP.covPP̄_premult_spln(β)
    rtnPP = covP.covPP_premult_spln(β)
    return complex(rtnPP̄), complex(rtnPP)
end

# Note: the reason we have different methods for Matrix vrs
# Union{Vector, Number} is that Spline1D's are optimized for
# Union{Vector, Number} so in general it is better to broadcast
# via via whole columns

# Also note: the only reason we make the return argument complex
# is that is the eltype the planned FFT will expect.

function (covP::βcovSpin0)(β::Matrix)
    rtn = similar(β)
    for (col, cβ) ∈ enumerate(eachcol(β))
        rtn[:,col] = covP.covII_premult_spln(cβ)
    end
    return complex(rtn)
end
function (covP::βcovSpin0)(β::Union{Vector, Number})
    return complex(covP.covII_premult_spln(β))
end

end


import Base: ==, *, /, \, isless, show, invperm

################################################################################

function arrayEquals{T}( x :: Array{T}, y :: Array{T} )
  n = length(x)
  if length(y) != n
    return false
  end
  for i = 1:n
    if x[i] != y[i]
      return false
    end
  end
  return true
end

function arrayCompare{T}( x :: Array{T}, y :: Array{T} )
  n = length(x)
  if length(y) != n
    return false
  end
  for i = 1:n
    if x[i] < y[i]
      return -1
    elseif x[i] > y[i]
      return 1
    end
  end
  return 0
end

################################################################################

# arr[i] == j  means what was in position j went to position i
type Perm
  arr :: Array{Int8}
end

function one( Perm, n :: Int )
  return Perm(1:n)
end

function ==( x :: Perm, y :: Perm )
  return arrayEquals( x.arr, y.arr )
end

function equalUpTo( x :: Perm, y :: Perm, n :: Int )
  for i = 1:n
    if x.arr[i] != y.arr[i]
      return false
    end
  end
  return true
end

function constUpTo( x :: Perm, n :: Int)
  return arrayEquals( 1:n, x[1:n] )
end

function numDifferent(x :: Perm)
  ret = 0
  for i = 1:length(x.arr)
    if x.arr[i] != i
      ret += 1
    end
  end
  return ret
end

# Do x, then do y
function *( x :: Perm, y :: Perm)
  a = copy( x.arr )
  permute!( a, y.arr )
  return Perm( a )
end
# Reverse x, then do y
function \( x :: Perm, y :: Perm )
  a = invperm( x.arr )
  permute!( a, y.arr )
  return Perm( a )
end
function square( x :: Perm )
  return x * x
end

function invperm( p :: Perm )
  return Perm( invperm(p.arr) )
end

function isless{T}(arr1 :: Array{T}, arr2 :: Array{T})
  for i = 1:length(arr1)
    if arr1[i] < arr2[i]
      return true
    end
    if arr1[i] > arr2[i]
      return false
    end
  end
  return false
end

function isless(x :: Perm, y :: Perm)
  for i = 1:length(x.arr)
    if x.arr[i] < y.arr[i]
      return true
    elseif x.arr[i] > y.arr[i]
      return false
    end
  end
  return false
end

################################################################################

function permFromCycle(n :: Int, arr :: Array{Int})
  p = one(Perm)
  q = copy(arr)
  mergeSort!(q)
  for i = 1:length(arr)
    p.arr[q[i]] = arr[i]
  end
  return p
end

################################################################################

function isodd( p :: Perm )
  n = length(p.arr)
  seen = Array{Bool}(n)
  ret = false
  for i = 1:n
    if seen[i]
      continue
    end
    if p.arr[i] == i
      seen[i] = true
      continue
    end
    #Start a cycle
    cycLengthOdd = true
    cycat = i
    while !seen[cycat]
      seen[cycat] = true
      cycat = p.arr[cycat]
      cycLengthOdd = !cycLengthOdd
    end
    if cycLengthOdd
      ret = !ret
    end
  end
  return ret
end
function iseven( p :: Perm )
  return !isodd(p)
end

################################################################################

function mergePartial!{T}(arr :: Array{T}, a :: Int, m :: Int, b :: Int)
  aa = Array{T}(b-a+1)
  i = a
  j = m+1
  k = 1
  while i <= m && j <= b
    if isless(arr[i], arr[j])
      aa[k] = arr[i]
      i += 1
    else
      aa[k] = arr[j]
      j += 1
    end
    k += 1
  end
  while i <= m
    aa[k] = arr[i]
    i += 1
    k += 1
  end
  while j <= b
    aa[k] = arr[j]
    j += 1
    k += 1
  end
  for x = a:b
    arr[x] = aa[x-a+1]
  end
  return arr
end

function mergeSortPartial!{T}(arr :: Array{T}, a :: Int, b :: Int)
  if a == b
    return arr
  end
  if b-a == 1
    if ! isless(arr[a], arr[b])
      c = arr[a]
      arr[a] = arr[b]
      arr[b] = c
    end
    return arr
  end
  m = div(b-a+1,2) + a - 1
  mergeSortPartial!(arr, a, m)
  mergeSortPartial!(arr, m+1, b)
  mergePartial!(arr, a, m, b)
end

function mergeSort!{T}(arr :: Array{T})
  if length(arr) == 1
    return arr
  end
  return mergeSortPartial!(arr, Int(1), length(arr))
end

################################################################################
# CenterPerm assumes the centers are matched up already,
# and describes how they will be shuffled around by a permutation,
# while keeping in mind the pieces' interchangeability.
# Each 4-tuple in the array (there are 6 of them) should be sorted ascendingly
# since we don't care about the ordering.

numCenterPiecesPerFace = 4
numCenterPieces = 24
type CenterPerm
  arr :: Array{Int8}
end

function mkNullCenterPerm()
  return CenterPerm(collect(1:numCenterPieces))
end

function ==( a :: CenterPerm, b :: CenterPerm )
  return arrayEquals( a, b )
end

function *( a :: CenterPerm, b :: CenterPerm )
  c = CenterPerm(copy(a))
  permute!(c,b)
  for i = 1:4:24
    mergeSortPartial!(c,i,i+3)
  end
end

################################################################################

type Position
  du :: Int8
  fb :: Int8
  lr :: Int8
  kind :: Int8 # 1 for side, 2 for center, 3 for corner
  code :: Int8 # its number in the permutation arrays
end

function allPositions()
  ret = Array{Position}(4,4,4)
  sidecode = 1
  centercode = 1
  cornercode = 1
  for du = 1:4
    duinner = (du==2 || du==3)
    for fb = 1:4
      fbinner = (fb==2 || fb==3)
      for lr = 1:4
        lrinner = (lr==2 || lr==3)
        if (lrinner && duinner && fbinner)
          continue
        end
        if (lrinner || duinner || fbinner)
          if ((lrinner && duinner) || (lrinner && fbinner) || (duinner && fbinner))
            # center
            ret[du,fb,lr] = Position(du,fb,lr,2,centercode)
            centercode += 1
          else
            # side
            ret[du,fb,lr] = Position(du,fb,lr,1,sidecode)
            sidecode += 1
          end
        else
          # corner
          ret[du,fb,lr] = Position(du,fb,lr,3,cornercode)
          cornercode += 1
        end
      end
    end
  end
  return ret
end

################################################################################

type Rotation
  axis :: Int8 # 1 for Bottom, 2 for Front, 3 for Right.
  layers :: Array{Int8} # 1 for outer layer, 2 for first inner layer, etc.
  direction :: Int8 # 1 for CW, 2 for 180, 3 for CCW.
  sidePerm :: Perm
  centerPerm :: Perm
  cornerPerm :: Perm
end

function ==(x :: Rotation, y :: Rotation)
  return x.axis == y.axis &&
    arrayEquals(x.layers,y.layers) &&
    x.direction == y.direction
end

function invrot( r :: Rotation )
  return Rotation(
    r.axis,
    r.layers,
    4 - r.direction,
    invperm(r.sidePerm),
    invperm(r.centerPerm),
    invperm(r.cornerPerm))
end

# rotate one point around an axis.
function rotatePointCW( du :: Int8, fb :: Int8, lr :: Int8, axis :: Int8 )
  if axis==1
    return ( du, lr, Int8(5)-fb )
  elseif axis==2
    return ( Int8(5)-lr, fb, du )
  else # if axis==3
    return ( fb, Int8(5)-du, lr )
  end
end
function rotatePoint( du :: Int8, fb :: Int8, lr :: Int8, axis :: Int8, dir :: Int8 )
  (a,b,c) = (du,fb,lr)
  for i = 1:dir
    (a,b,c) = rotatePointCW(a,b,c,axis)
  end
  return (a,b,c)
end

function mkRotation( allPos :: Array{Position}, axis :: Int8, layer :: Int8, dir :: Int8 )
  numPieces = (layer==1 || layer==4) ? 16 : 12
  sidePermArr = collect(1:24)
  centerPermArr = collect(1:24)
  cornerPermArr = collect(1:8)

  poss = Array{Position}(numPieces)
  k = 1
  for i = 1:4
    for j = 1:4
      if (i==1 || i==4 || j==1 || j==4 || layer==1 || layer==4)
        if axis==1
          poss[k] = allPos[layer,i,j]
        elseif axis==2
          poss[k] = allPos[i,layer,j]
        else
          poss[k] = allPos[i,j,layer]
        end
        k += 1
      end
    end
  end

  for i = 1:numPieces
    (newdu,newfb,newlr) = rotatePoint( poss[i].du, poss[i].fb, poss[i].lr, axis, dir )
    newpos = allPos[newdu,newfb,newlr]
    if newpos.kind==1
      sidePermArr[newpos.code] = poss[i].code
    elseif newpos.kind==2
      centerPermArr[newpos.code] = poss[i].code
    else
      cornerPermArr[newpos.code] = poss[i].code
    end
  end

  return Rotation(axis, [layer], dir, Perm(sidePermArr), Perm(centerPermArr), Perm(cornerPermArr))
end

function combineRotations( a :: Rotation, b :: Rotation )
  if a.axis != b.axis
    return -1
  elseif a.direction != b.direction
    return -2
  elseif length(a.layers) != 1 && length(b.layers) != 1
    return -3
  end
  layers = Array{Int8}(0)
  append!(layers, a.layers)
  append!(layers, b.layers)
  return Rotation(a.axis, layers, a.direction, a.sidePerm*b.sidePerm, a.centerPerm*b.centerPerm, a.cornerPerm*b.cornerPerm)
end

function rotationSequenceMakesSense( a :: Rotation, b :: Rotation )
  return a.axis != b.axis
  if a.axis == b.axis
    if a.direction==b.direction
      if length(a.layers)==1 && length(b.layers)==1
        if a.layers[1]*b.layers[1] == 2 # 1&2
          return false
        elseif a.layers[1]*b.layers[1] == 12 # 3&4
          return false
        end
      end
    end
    if arrayEquals(a.layers, b.layers)
      return false
    elseif arrayEquals(a.layers, [Int8(1),Int8(2)]) && arrayEquals(b.layers, [Int8(3),Int8(4)])
      return false
    elseif arrayEquals(b.layers, [Int8(1),Int8(2)]) && arrayEquals(a.layers, [Int8(3),Int8(4)])
      return false
    elseif a.direction == b.direction
      # doing 1 CW then 2 CW is stupid.
      if arrayEquals(a.layers, [Int8(1)]) && arrayEquals(a.layers, [Int8(2)])
        return false
      elseif arrayEquals(a.layers, [Int8(2)]) && arrayEquals(a.layers, [Int8(1)])
        return false
      elseif arrayEquals(a.layers, [Int8(4)]) && arrayEquals(a.layers, [Int8(3)])
        return false
      elseif arrayEquals(a.layers, [Int8(3)]) && arrayEquals(a.layers, [Int8(4)])
        return false
      elseif length(a.layers) + length(b.layers) >= 3 && ((a.layers[1] <= 2 && b.layers[1] >= 3) || (b.layers[1] <= 2 && a.layers[1] >= 3))
        return false
      end
    end
    if a.direction == (4 - b.direction)
      # doing 1 CW then 12 CCW is stupid for example.
      if length(a.layers)==1
        for i = 1:length(b.layers)
          if b.layers[i] == a.layers[1]
            return false
          end
        end
      end
      if length(b.layers)==1
        for i = 1:length(a.layers)
          if a.layers[i] == b.layers[1]
            return false
          end
        end
      end
    end
  end
  return true
end

function rotationTripleMakesSense( a :: Rotation, b :: Rotation, c :: Rotation )
  return a.axis != b.axis && b.axis != c.axis
  if a.axis == b.axis && a.axis == c.axis
    return false
  end
  return true
end

function allRotations()

  allPos = allPositions()

  ret = Array{Rotation}(3*7*3)
  i = 1
  for ax = 1:3
    for dir = 1:3
      for lay = 2:4
        ret[i] = mkRotation( allPos, Int8(ax), Int8(lay), Int8(dir) )
        i += 1
      end
      ret[i] = combineRotations( ret[i-3], ret[i-2] )
      i += 1
      ret[i] = combineRotations( ret[i-4], ret[i-2] )
      i += 1
      ret[i] = combineRotations( ret[i-4], ret[i-3] )
      i += 1
      ret[i] = combineRotations( ret[i-6], ret[i-1] )
      i += 1
    end
  end

  return ret

end

################################################################################

type Algorithm
  rotations :: Array{Rotation}
  sidePerm :: Perm
  centerPerm :: Perm
  cornerPerm :: Perm
  #sortingCode :: BigInt
end

function mkAlgorithm(rots :: Array{Rotation})
  s = rots[1].sidePerm
  ce = rots[1].centerPerm
  co = rots[1].cornerPerm
  for i = 2:length(rots)
    s = s * rots[i].sidePerm
    ce = ce * rots[i].centerPerm
    co = co * rots[i].cornerPerm
  end
  return Algorithm(rots, s, ce, co)
end

function sortCodifyAlgorithm!( a :: Algorithm )
  factor = BigInt(1)
  index = 32
  ret = BigInt(0)
  for i in 24:-1:17
    ret += factor * (a.sidePerm.arr[i]-1)
    #index += 1
    factor *= index
  end
  for i in 8:-1:1
    ret += factor * (a.cornerPerm.arr[i]-1)
    #index += 1
    factor *= index
  end
  for i in 16:-1:1
    ret += factor * (a.sidePerm.arr[i]-1)
    #index += 1
    factor *= index
  end
  #a.sortingCode = ret
end
function sortCodifyAlgorithms!( algs :: Array{Algorithm} )
  for i = 1:length(algs)
    sortCodifyAlgorithm!(algs[i])
  end
end

function oneRotationAlgorithm( rot :: Rotation )
  return mkAlgorithm( [rot] )
end

function isless(a1 :: Algorithm, a2 :: Algorithm)
  #return isless(a1.sidePerm, a2.sidePerm)

  #return a1.sortingCode < a2.sortingCode

  # now sort by first 16 side piece destinations, then by corners, then by last 8 side pieces
  c = arrayCompare(a1.sidePerm.arr[1:16], a2.sidePerm.arr[1:16])
  if c < 0
    return true
  elseif c > 0
    return false
  end
  c = arrayCompare(a1.cornerPerm.arr, a2.cornerPerm.arr)
  if c < 0
    return true
  elseif c > 0
    return false
  end
  c = arrayCompare(a1.sidePerm.arr[17:24], a2.sidePerm.arr[17:24])
  return c < 0
end

function invalg( a :: Algorithm )
  n = length(a.rotations)
  rots = Array{Rotation}( n )
  for i = 1:n
    rots[n+1-i] = invrot( a.rotations[i] )
  end
  return Algorithm(
    rots,
    invperm(a.sidePerm),
    invperm(a.centerPerm),
    invperm(a.cornerPerm) )
end

function *(a1 :: Algorithm, a2 :: Algorithm)
  rots = Array{Rotation}( length(a1.rotations) + length(a2.rotations) )
  for i = 1:length(a1.rotations)
    rots[i] = a1.rotations[i]
  end
  for i = 1:length(a2.rotations)
    rots[i + length(a1.rotations)] = a2.rotations[i]
  end
  return Algorithm(
    rots,
    a1.sidePerm * a2.sidePerm,
    a1.centerPerm * a2.centerPerm,
    a1.cornerPerm * a2.cornerPerm )
end

function isNotSandwichedAlgorithm( a :: Algorithm )
  if length(a.rotations)>2
    ri = a.rotations[1]
    rf = a.rotations[length(a.rotations)]
    if ri.axis==1 && rf.axis==1 && ri.direction == 4-rf.direction
      rilayers = Array{Int8}(0)
      if ri.layers[length(ri.layers)] == Int8(4)
        append!(rilayers,ri.layers[1:(length(ri.layers)-1)])
      else
        append!(rilayers,ri.layers)
      end
      rflayers = Array{Int8}(0)
      if rf.layers[length(rf.layers)] == Int8(4)
        append!(rflayers,rf.layers[1:(length(rf.layers)-1)])
      else
        append!(rflayers,rf.layers)
      end
      if arrayEquals(rilayers, rflayers)
        return false
      end
    end
  end
  return true
end

################################################################################

function show(r :: Rotation)
  layers = r.layers
  axis = r.axis
  dir = r.direction
  if (r.layers[1]>2)
    layers = Array{Int8}(length(r.layers))
    for i = 1:length(r.layers)
      layers[i] = 5 - r.layers[length(layers)+1-i]
    end
    axis = -axis
    dir = 4-dir
  end

  for i = (length(layers)+1):4
    print(" ")
  end
  for i = 1:length(layers)
    print(layers[i])
  end

  if axis==1
    print(" D ")
  elseif axis==2
    print(" F ")
  elseif axis==3
    print(" L ")
  elseif axis==-1
    print(" U ")
  elseif axis==-2
    print(" B ")
  else # if axis==-3
    print(" R ")
  end
  if dir==1
    println("CW")
  elseif dir==2
    println("CW Twice")
  else
    println("CCW")
  end
end
function show(alg :: Algorithm)
  for i = 1:length(alg.rotations)
    show(alg.rotations[i])
  end
  println()
end
function show(algs :: Array{Algorithm})
  println()
  for i = 1:length(algs)
    println("  Algorithm ", i)
    println("---------------")
    show(algs[i])
    println()
  end
end

################################################################################

function allAlgs(moves :: Array{Rotation}, numMoves)
  if numMoves==1
    retalgs = Array{Algorithm}(length(moves))
    for i = 1:length(moves)
      retalgs[i] = oneRotationAlgorithm(moves[i])
    end
    return retalgs
  end
  subsequents = allAlgs(moves, numMoves-1)
  ret = Array{Algorithm}( 0 )
  for m = 1:length(moves)
    oneMoveAlg = oneRotationAlgorithm( moves[m] )
    for s = 1:length(subsequents)
      if rotationSequenceMakesSense( oneMoveAlg.rotations[1], subsequents[s].rotations[1] )
        if length(subsequents[s].rotations)<2 || rotationTripleMakesSense( oneMoveAlg.rotations[1], subsequents[s].rotations[1], subsequents[s].rotations[2] )
          append!(ret, [oneMoveAlg * subsequents[s]])
        end
      end
    end
  end
  i = 1
  for m = (length(moves)*length(subsequents) + 1):length(ret)
    ret[m] = subsequents[i]
    i += 1
  end
  return ret
end

topRot = mkRotation( allPositions(), Int8(1), Int8(4), Int8(1) )

function appendOneWaySelfCross(fun, isok, firstisok, src, i, j, dest)
  temparr = Array{typeof(fun(src[i],src[j]))}( (j-i+1)*(j-i+1) )
  #println("temparr from i=",i," to j=",j)
  m = 1
  for k = i:j
    a = src[k]
    if firstisok(a)
      for l = (k+1):j
        #if l==k
        #  continue
        #end
        c = fun( a, src[l] )
        if isok(c)
          temparr[m] = c
          m += 1
        end
      end
    end
  end
  #filter!( isok, temparr[1:(m-1)] )
  append!( dest, temparr[1:(m-1)] )
end
function funForAppendOneWaySelfCross(a :: Algorithm, b :: Algorithm)
  return invalg(a) * b
end

function algsCounteractEachOther( a :: Algorithm, b :: Algorithm, numConstSides, numConstCorners)
  # return true iff a.sortingCode and b.sortingCode have the same first 20 digits
  #if abs(a.sortingCode - b.sortingCode) >= BigInt(32)^12
  #  return false
  #end
  return equalUpTo(a.sidePerm, b.sidePerm, numConstSides) && equalUpTo(a.cornerPerm, b.cornerPerm, numConstCorners)
end

function isrelevantalg( a :: Algorithm )
  n = length(a.rotations)
  lastRot = a.rotations[n]
  # if last rotation is the top
  if lastRot.axis == 1 && arrayEquals(lastRot.layers, [Int8(4)])
    return false
  elseif !isNotSandwichedAlgorithm(a)
    return false
  elseif isstupidalg(topRot, a)
    return false
  elseif isredundantalg(a)
    return false
  # filter out algs that mess up the centers
  elseif !keepsCenters(a)
    return false
  # filter out algs that mess up the corners
  elseif !keepsBottomCorners(a)
    return false
  end

  return true
end

function isrelevantstartofalg( halfAlg :: Algorithm )

  n = length(halfAlg.rotations)
  firstrot = halfAlg.rotations[n]

  # filter out algs that start with R or L
  # (because you can do the same by starting with F)
  if firstrot.axis == 3
    return false
  # filter out algs that start with the back layer
  # (because you can do the same by starting with F)
  elseif firstrot.axis == 2 && firstrot.layers[1] >= 3
    return false
  # filter out algs that start with CCW
  # (because you can mirror them)
  elseif firstrot.direction == 1
    return false
  # filter out algs that start with rotating the top
  elseif firstrot.axis == 1 && arrayEquals(firstrot.layers, [Int8(4)])
    return false
  end

  return true
end

function bucketFunc( a :: Algorithm, level )
  if level > 16
    return a.cornerPerm.arr[level-16]
  else
    return a.sidePerm.arr[level]
  end
end

function bucketSortPartial!(arr :: Array{Algorithm}, i, j, numBuckets, maxLevel, level, mergeSortAfter)
  temparr = Array{Algorithm}(numBuckets, length(arr))
  temparrSizes = Array{Int32}(numBuckets)
  for k = 1:numBuckets
    temparrSizes[k] = 0
  end
  for k = i:j
    b = bucketFunc(arr[k], level)
    temparrSizes[b] += 1
    temparr[ b, temparrSizes[b] ] = arr[k]
  end
  k = i
  for b = 1:numBuckets
    for l = 1:temparrSizes[b]
      arr[k] = temparr[b,l]
      k += 1
    end
  end
  if level<maxLevel
    k = i
    for b = 1:numBuckets
      bucketSortPartial!(arr, k, k + temparrSizes[b]-1, numBuckets, maxLevel, level+1, mergeSortAfter)
      k += temparrSizes[b]
    end
  elseif mergeSortAfter
    k = i
    for b = 1:numBuckets
      if temparrSizes[b]>1
        mergeSortPartial!(arr, k, k + temparrSizes[b]-1)
      end
      k += temparrSizes[b]
    end
  end
end

function bucketSort!( arr :: Array{Algorithm}, numBuckets, maxLevel, mergeSortAfter)
  bucketSortPartial!( arr, 1, length(arr), numBuckets, maxLevel, 1, mergeSortAfter)
end

# algorithms for a given layer.
function layeredAlgs(moves :: Array{Rotation}, numMovesHalved, layer :: Int)
  # numConstSides is number is side pieces (from bottom up) that should remain in place
  numConstSides = layer==2 ? 8 : layer==3 ? 12 : 16
  numConstCorners = 4
  permSize = length( moves[1].sidePerm.arr )
  halfAlgs = allAlgs(moves, numMovesHalved)
  println("done halfAlgs, the number of them is ", length(halfAlgs), ".")

  #sortCodifyAlgorithms!(halfAlgs)
  #println("And now they're sort-codified.")

  numConst = numConstSides+numConstCorners
  numBuckets = 24
  bucketSort!( halfAlgs, numBuckets, 1, true )
  println("And now the halfAlgs are sorted.")

  nHalf = length(halfAlgs)

  #println(sum(halfAlgsGroupedSizes))
  #println(nHalf)

  ret = Array{Algorithm}(0)
  n = nHalf
  i = 1
  while i < n
    if algsCounteractEachOther(halfAlgs[i], halfAlgs[i+1], numConstSides, numConstCorners)
      j = i+1
      while j < n && algsCounteractEachOther(halfAlgs[i], halfAlgs[j+1], numConstSides, numConstCorners)
        j += 1
      end
      #println("Found a group! bucket=",bucket," i=",i," j=",j)
      appendOneWaySelfCross(funForAppendOneWaySelfCross, isrelevantalg, isrelevantstartofalg, halfAlgs, i, j, ret)
      i = j+1
    else
      i += 1
    end
  end

  return ret
end

################################################################################

function isstupidalg(rot :: Rotation, a :: Algorithm)
  ap = a.sidePerm
  p = rot.sidePerm
  for i = 1:4
    if ap==p
      return true
    end
    p = p * rot.sidePerm
  end
  return false
end

function algContainsNoStupidity( a :: Algorithm )
  for i = 1:(length(a.rotations)-1)
    if !rotationSequenceMakesSense(a.rotations[i], a.rotations[i+1])
      return false
    elseif i <= (length(a.rotations)-2)
      if !rotationTripleMakesSense(a.rotations[i], a.rotations[i+1], a.rotations[i+2])
        return false
      end
    end
  end
  return true
end

function theyRotateSameLayer( r1 :: Rotation, r2 :: Rotation )
  if r1.axis != r2.axis
    return false
  elseif !arrayEquals(r1.layers,r2.layers)
    return false
  end
  return true
end

function isredundantalg( alg :: Algorithm )
  for i = 1:(length(alg.rotations)-1)
    if theyRotateSameLayer(alg.rotations[i], alg.rotations[i+1])
      return true
    end
  end
  return false
end

function issideodd( r :: Rotation )
  if r.direction==2
    return false
  end
  ret = false
  for i = 1:length(r.layers)
    if r.layers[i]==2 || r.layers[i]==3
      ret = !ret
    end
  end
  return ret
end
function iscornerodd( r :: Rotation )
  if r.direction==2
    return false
  end
  ret = false
  for i = 1:length(r.layers)
    if r.layers[i]==1 || r.layers[i]==4
      ret = !ret
    end
  end
  return ret
end
function issideodd( a :: Algorithm )
  ret = false
  for i = 1:length(a.rotations)
    if issideodd(a.rotations[i])
      ret = !ret
    end
  end
  return ret
end
function iscornerodd( a :: Algorithm )
  ret = false
  for i = 1:length(a.rotations)
    if iscornerodd(a.rotations[i])
      ret = !ret
    end
  end
  return ret
end

################################################################################

function keepsCenters( alg :: Algorithm )
  csi = [1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6]
  csf = copy(csi)
  permute!(csf, alg.centerPerm.arr)
  return arrayEquals(csi,csf)
end

function keepsBottomCorners( alg :: Algorithm )
  csi = Array{Int8}(8)
  csf = Array{Int8}(8)
  for i = 1:4
    csi[i] = Int8(i)
    csf[i] = Int8(i)
    csi[i + 4] = Int8(5)
    csf[i + 4] = Int8(5)
  end
  permute!(csf, alg.cornerPerm.arr)
  return arrayEquals(csi,csf)
end

################################################################################

function getAlgs4(numMoves)

  allPos = allPositions()
  allRot = allRotations()
  topRot = mkRotation( allPos, Int8(1), Int8(4), Int8(1) )

  x = layeredAlgs(allRot, div(numMoves,2), 4)
  println("Done layeredAlgs! Their number is ", length(x), ".")

  filter!(algContainsNoStupidity, x)

  return x

end

function algs4( numMoves )
  a4 = getAlgs4( numMoves )
  show(a4)
end

################################################################################

# one move may have several different rotations on the same axis,
# so one layer can be CW the other CCW in one move for example.
type AxisMove
  axis :: Int8
  rotations :: Array{Tuple{Int8,Int8}}
  sidePerm :: Perm
  centerPerm :: Perm
  cornerPerm :: Perm
end

################################################################################