"""
    dnx(n::T, f::Basic, v::Basic) where {T <: Unsigned}

Computes the n-th derivative of the the function f with respect to the variable v.
This function re implements SymEngine's own `diff` function using an early quitting strategy.

# Input
- `n::T`: The order of the derivative.
- `f::Basic`: The expression to compute the derivative.
- `v::Basic`: The variable with respect to which the derivative will be computed.

# Output
A `SymEngine.Basic` object with the derived expression.
"""
function dnx(n::T, f::Basic, v::Basic) where {T <: Unsigned}
    b0 = Basic(0)
    zt = zero(T)
    ot = one(T)

    f == b0 && return b0
    n == zt && return f
    n == ot && return SymEngine.diff(f, v)
    n > 1 && return dnx(n - ot, SymEngine.diff(f, v), v)
end

"""
    dnx(p::AnalyticAIMProblem{N,T}, n::N, f::Function, v::Function) where {N <: Unsigned, T <: Number}

Computes the n-th derivative of the AIM expressions with respect to ODE's variable.
This function is only a thin wrapper around SymEngine's own `diff` function.
It works as a barrier function that produces a type stable `Basic` result.

# Input
- `p::AnalyticAIMProblem`: The problem data with the expressions to compute the derivative.
- `n::Unsigned`: The order of the derivative.
- `f::Function`: The actual expression to compute the derivative. Either λ0 or S0.
- `v::Function`: The variable with respec to which the derivative will be computed. Either get_ODEvar or get_ODEeigen.

# Output
A `SymEngine.Basic` object with the derived expression.
"""
function dnx(p::AnalyticAIMProblem{N,T}, n::N, f::Function, v::Function) where {N <: Unsigned, T <: Number}
    return convert(SymEngine.Basic, dnx(n, f(p), v(p)))
end

"""
    computePolynomialFactors(p::QuadraticEigenvalueProblem{N,T}, n::N, f::Function) where {N <: Unsigned, T <: Number}

Create a second order `Polynomial` object in the variable `ω` by computing derivatives of `λ0` or `S0`.

# Input
- `p::QuadraticEigenvalueProblem`: The problem data with the expressions to compute the derivative.
- `n::Unsigned`: The order of the derivative.
- `f::Function`: the function to extract the polynomial from. Either λ0 or S0.

# Output
An object of type `Polynomial{T}` containing the polynomial resulting from the derivation of the expression.
"""
function computePolynomialFactors(p::QuadraticEigenvalueProblem{N,T}, n::N, f::Function) where {N <: Unsigned, T <: Number}
    SYB = SymEngine.Basic

    # Compute the derivative at x0 and expand it to obtain a polynomial in ω
    step1::SYB = convert(SYB, subs(dnx(p, n, f, get_ODEvar), get_ODEvar(p) => get_x0(p)))
    step2::SYB = convert(SYB, expand(step1))

    # Extract the factors of the polynomial in ω
    ω = get_ODEeigen(p)

    p0::T = convert(T, SymEngine.N(coeff(step2, ω, Basic(0))))
    p1::T = convert(T, SymEngine.N(coeff(step2, ω, Basic(1))))
    p2::T = convert(T, SymEngine.N(coeff(step2, ω, Basic(2))))

    poly = Polynomial{T}([p0, p1, p2])

    return poly
end

"""
    createPoly(p::QuadraticEigenvalueProblem{N,T}, n::N, f::Function)

Compute the n-th coefficient of the Taylor expansion around x0 for the functions `λ0` or `S0`

# Input
- `p::QuadraticEigenvalueProblem`: The problem data with the expressions to compute the derivative.
- `n::Unsigned`: The order of the derivative.
- `f::Function`: the function to extract the polynomial from. Either λ0 or S0.

# Output
An object of type `Polynomial{T}` containing the polynomial Taylor coefficient.
"""
function createPoly(p::QuadraticEigenvalueProblem{N,T}, n::N, f::Function) where {N <: Unsigned, T <: Number}
    computePolynomialFactors(p, n, f)/factorial(n)
end

function createPoly(p::QuadraticEigenvalueProblem{N,T}, n::N, f::Function) where {N <: Unsigned, T <: BigFloat}
    computePolynomialFactors(p, n, f)/factorial(big(n))
end

function createPoly(p::QuadraticEigenvalueProblem{N,T}, n::N, f::Function) where {N <: Unsigned, T <: Complex{BigFloat}}
    computePolynomialFactors(p, n, f)/factorial(big(n))
end


export simple_eval, getfield_vec, abserror, relerror

abserror(sol, refsol) = abs(sol-refsol)
relerror(sol, refsol) = abs((sol-refsol)./refsol)
abserror(tr::TestResults) = abserror(tr.solution[tr.testrun.tc.refsolinds],
                                     tr.testrun.tc.refsol)
relerror(tr::TestResults) = relerror(tr.solution[tr.testrun.tc.refsolinds],
                                     tr.testrun.tc.refsol)

@doc """Calculates the scd error estimate from Mazzia & Magherini p.II-ii.
        It is the minimum significant correct digits in the solution.
        (Note this is does not work if any component of refsol==0)""" ->
calc_error_scd(sol, refsol) = -log10(norm(relerror(sol,refsol), Inf))

@doc """Same as two argument call but use absolute error instead
        of relative error at absinds""" ->
function calc_error_scd(sol, refsol, absinds)
    out = relerror(sol,refsol)
    out[absinds] = abserror(sol[absinds],refsol[absinds])
    return -log10(norm(out, Inf))
end
calc_error_scd(sol, tr::TestRun) = calc_error_scd(sol[tr.tc.refsolinds],
                                                 tr.tc.refsol,
                                                 tr.tc.scd_absinds)
calc_error_scd(res::TestResults) = calc_error_scd(res.yend[res.testrun.tc.refsolinds],
                                                 res.testrun.tc.refsol,
                                                 res.testrun.tc.scd_absinds)

@doc """Calculates the mescd error estimate from Mazzia & Magherini p.II-ii.
        Mixed significant digit of the approximate solution.  (Very similar to
        the scd error estimate, except when refsol is close to zero?)
        """ ->
function calc_error_mescd(sol, refsol, abstol, reltol)
    -log10( norm( abserror(sol,refsol)./(abstol./reltol + refsol) ,Inf))
end
calc_error_mescd(sol, tr::TestRun) = calc_error_mescd(sol[tr.tc.refsolinds],
                                                      tr.tc.refsol,
                                                      tr.abstol, tr.reltol)
calc_error_mescd(res::TestResults) = calc_error_mescd(res.yend[res.testrun.tc.refsolinds],
                                                     res.testrun.tc.refsol,
                                                     res.testrun.abstol, res.testrun.reltol)

function simple_eval{N,T}(tend::T, yend::Vector{T}, stats, walltime, mem, gc_time, tr::TestRun{N,T})
    tc = tr.tc
    if tend!=tc.tspan[2]
        st = "Integration did not run to specified end time.  tend=$tend but should be $(tc.tspan[2])."
        warn(st)
        e = ErrorException(st)
        return simple_eval(e, tr)
    end
    scd = calc_error_scd(yend, tr)
    #scd = scd<0 ? NaN : scd # return NaN if significant digits less than 0
    if isadaptive(tr)
        mescd = calc_error_mescd(yend, tr)
    else
        mescd = calc_error_mescd(yend[tc.refsolinds], tc.refsol, 1, 1) #
    end
    #mescd = mescd<0 ? NaN : scd # return NaN if significant digits less than 0

    return TestResults(tr, tend, yend, stats..., walltime, mem, gc_time, scd, mescd, nothing)
end
function simple_eval{N,T}(e::Exception, tr::TestRun{N,T})
    return TestResults(tr, tr.tc.tspan[1], T[], -1, -1, -1, -1, -1,     NaN,  -1,    NaN, NaN, NaN, e)
end

@doc """For getting metrics (or any field out of) out of an Vector filled with
        one type having fields""" ->
function getfield_vec(trs::Vector, field::Symbol)
    tr = trs[1]
    out = zeros(typeof(tr.(field)), length(trs))
    for (i,tr) in enumerate(trs)
        out[i] = tr.(field)
    end
    out
end


export meantoecc, benchmark_kepler

function benchmark_kepler(times::Int)
    r = [8.59072560e+02, -4.13720368e+03, 5.29556871e+03]
    v = [7.37289205e+00, 2.08223573e+00, 4.39999794e-01]
    mu = 3.986004418e5
    el = elements(r, v, mu)
    worst = -Inf
    best = Inf
    total = 0.0
    meantoecc(pi/2, el[2])
    for i = 1:times
        gc_enable(false)
        t = @elapsed meantoecc(pi/2, el[2])
        gc_enable(true)
        if t > worst
            worst = t
        end
        if t < best
            best = t
        end
        total += t
    end
    println("[$(total/times),$best,$worst]")
end

function period(a::Float64, mu::Float64)
    return 2pi*sqrt(a^3/mu)
end

function newton(x0::Float64, func::Function, derivative::Function, maxiter::Int=50, tol::Float64=sqrt(eps()))
    p0 = x0
    for i = 1:maxiter
        p = p0 - func(p0)/derivative(p0)
        if abs(p - p0) < tol
            return p
        end
        p0 = p
    end
    error("Not converged.")
end

function meantoecc(M::Float64, ecc::Float64)
    kepler(E::Float64) = E - ecc*sin(E) - M
    kepler_der(E::Float64) = 1 - ecc*cos(E)
    return newton(M, kepler, kepler_der)
end


@testset "Testing lss.jl" begin

    rough_kwargs = Dict(:atol => 1e-7, :rtol => 1e-7)

    # set up
    A = .95
    C = .05
    G = 1.
    H = 0
    mu_0 = [.75;]
    Sigma_0 = fill(0.000001, 1, 1)

    ss = LSS(A, C, G, H, mu_0)
    ss1 = LSS(A, C, G, H, mu_0, Sigma_0)

    vals = stationary_distributions(ss, max_iter=1000, tol=1e-9)

    @testset "test stationarity" begin
        vals = stationary_distributions(ss, max_iter=1000, tol=1e-9)
        ssmux, ssmuy, sssigx, sssigy = vals

        @test isapprox(ssmux, ssmuy; rough_kwargs...)
        @test isapprox(sssigx, sssigy; rough_kwargs...)
        @test isapprox(ssmux, [0.0]; rough_kwargs...)
        @test isapprox(sssigx, ss.C.^2 ./ (1 - ss.A .^2); rough_kwargs...)
    end

    @testset "test replicate" begin
        xval1, yval1 = replicate(ss, 100, 5000)
        xval2, yval2 = replicate(ss; t=100, num_reps=5000)
        xval3, yval3 = replicate(ss1; t=100, num_reps=5000)

        for (x, y)  in [(xval1, yval1), (xval2, yval2), (xval3, yval3)]
            @test isapprox(x , y; rough_kwargs...)
            @test abs(mean(x)) <= 0.05
        end

    end

    @testset "test convergence error" begin
        @test_throws ErrorException stationary_distributions(ss; max_iter=1, tol=eps())
    end

    @testset "test geometric_sums" begin
        β = 0.98
        xs = rand(10)
        for x in xs
            gsum_x, gsum_y = QuantEcon.geometric_sums(ss, β, [x])
            @test isapprox(gsum_x, ([x/(1-β *ss.A[1])]))
            @test isapprox(gsum_y, ([ss.G[1]*x/(1-β *ss.A[1])]))
        end
    end

    @testset "test constructors" begin
        # kwarg version
        other_ss = LSS(A, C, G; H=H, mu_0=[mu_0;])
        for nm in fieldnames(ss)
            @test getfield(ss, nm) == getfield(other_ss, nm)
        end
    end

    @testset "test moment iterator" begin
        m = QuantEcon.LSSMoments(ss)

        # never done
        @test !done(m, 1)

        # start should give us mu_0, Sigma_0
        @test start(m) == (ss.mu_0, ss.Sigma_0)
    end

    @testset "test positive semi-dfinite covariance" begin

        # set up
        A = [1.0      0.0       0.0 0.0;
             10.0     0.9       0.0 0.0;
             0.0      1.0       0.0 0.0;
             68.9655  -0.689655 0.0 1.0]
        C = [0.0;
             1.0;
             0.0;
             0.0]
        G = [0.0     1.0       0.0  0.0;
             65.5172 0.344828  0.0  -0.05]
        H = [0.6     1.3;
             -5.8    0.1]
        mu_0 = [1.0;
                99.9999;
                99.9999;
                0.0]
        Sigma_0 = [0.0  0.0      0.0      0.0;
                   0.0  5.26316  4.73684  0.0;
                   0.0  4.73684  5.26316  0.0;
                   0.0  0.0      0.0      0.0]

        lss_psd = LSS(A, C, G, H, mu_0, Sigma_0)

        @test isapprox(lss_psd.dist.Sigma,
                    lss_psd.dist.Q*lss_psd.dist.Q')

        @test size(rand(lss_psd.dist,10)) == (4,10)
    end

    @testset "test stability checks: unstable systems" begin

        phi_0, phi_1, phi_2 = 1.1, 1.8, -1.8

        A = [1.0   0.0   0
            phi_0 phi_1 phi_2
            0.0   1.0   0.0]
        C = zeros(3, 1)
        G = [0.0 1.0 0.0]

        lss = LSS(A, C, G)
        lss2 = LSS(A[2:end, 2:end], C[2:end, :], G[:, 2:end])   # system without constant
        lss_vec = [lss, lss2]

        for sys in lss_vec
            @test is_stable(sys) == false
            @test_throws ErrorException stationary_distributions(sys)
            @test_throws ErrorException geometric_sums(sys, 0.97, rand(3))
        end

    end

    @testset "test stability checks: stable systems" begin

        phi_0, phi_1, phi_2 = 1.1, 0.8, -0.8

        A = [1.0   0.0   0
            phi_0 phi_1 phi_2
            0.0   1.0   0.0]
        C = zeros(3, 1)
        G = [0.0 1.0 0.0]

        lss = LSS(A, C, G)
        lss2 = LSS(A[2:end, 2:end], C[2:end, :], G[:, 2:end])   # system without constant
        lss_vec = [lss, lss2]

        for sys in lss_vec
            @test is_stable(sys) == true
        end

    end

end  # @testset


using graphtheoryufrj
using Base.Test

include("ratton_first_assignment.jl")
include("ratton_second_assignment.jl")
"""
First assignment: Basics of a graph package (Modeling, BFS, DFS...)
"""
FLAG_RATTON_ASSIGNMENT01 = false
infile_name = "../assets/as_graph.txt"
outfile_name = "parents_q4.txt"
initial_vertex = [1 5 10 50 100 500 1000 5000 10000 25000]

if FLAG_RATTON_ASSIGNMENT01
	# Test the memory representation of the structures of the graph.
	@test test_one(infile_name) == "TEST_ONE_SUCESS"

	# Time to perform 10 bfs on the graph.
	@test test_two(infile_name, initial_vertex) == "TEST_TWO_SUCESS"

	# Time to perform 10 dfs on the graph.
	@test test_three(infile_name, initial_vertex) == "TEST_THREE_SUCESS"

	# Find the parents of given nodes.
	@test test_four(infile_name, outfile_name) == "TEST_FOUR_SUCESS"

	#
	@test test_five(infile_name) == "TEST_FIVE_SUCESS"

	# Empirical distribuiton test.
	@test test_six(infile_name) == "TEST_SIX_SUCESS"

	#
	@test test_seven(infile_name) == "TEST_SEVEN_SUCESS"
end

"""
First assignment: Extending the graph package (Dijkstra, Floyd-Warshall...)
"""
FLAG_RATTON_ASSIGNMENT02 = false
algorithm = prim_mst
initial_vertex_mst = 20
infile_name = ["../assets/grafo_1.txt",
	"../assets/grafo_2.txt",
	"../assets/grafo_3.txt",
	"../assets/grafo_4.txt",
	"../assets/grafo_5.txt"]

if FLAG_RATTON_ASSIGNMENT02
	for i in infile_name
		#
		@test test_one(i) == "TEST_ONE_SUCESS"

		#
		@test test_two(i, algorithm, initial_vertex_mst) == "TEST_TWO_SUCESS"

		#
		@test test_three(i) == "TEST_THREE_SUCESS"

	end

	#
	@test test_colab_one() == "TEST_COLAB_ONE_SUCESS"

	#
	@test test_colab_two() == "TEST_COLAB_TWO_SUCESS"

end


## utility functions

## https://discourse.julialang.org/t/efficient-tuple-concatenation/5398/7
@inline tuplejoin(x) = x
@inline tuplejoin(x, y) = (x..., y...)
@inline tuplejoin(x, y, z...) = (x..., tuplejoin(y, z...)...)

# powmod(g, q, f) but q can be BigInt,,,
# compute a^n mod m
function _powermod(a, n::S, m) where {S<:Integer}
    ## basically powermod in intfuncs.jl with wider type signatures
    n < 0 && throw(DomainError())
    n == 0 && return one(m)

    _,b = divrem(a,m)
    iszero(b) && return b

    t = prevpow(2,n)::S

    local r = one(a)
    while true
        if n >= t
            _,r = divrem(r * b, m)
            n -= t
        end
        t >>>= 1
        t <= 0 && break
        _,r = divrem(r * r, m)
    end
    r
end


using PtFEM
using Test

ProjDir = @__DIR__

#=
Compare formulas at:
http://www.awc.org/pdf/codes-standards/publications/design-aids/AWC-DA6-BeamFormulas-0710.pdf
=#

data = Dict(
  # Frame(nels, nn, ndim, nst, nip, finite_element(nod, nodof))
  :struc_el => Frame(20, 21, 3, 1, 1, Line(2, 3)),
  :properties => [1.0e3 1.0e3 1.0e3 3.0e5;],
  :x_coords => range(0, stop=4, length=21),
  :y_coords => zeros(21),
  :z_coords => zeros(21),
  :g_num => [
    collect(1:20)';
    collect(2:21)'],
  :support => [
    (1, [0 0 0 0 0 0]),
    (21, [0 0 0 0 0 0]),
    ],
  :loaded_nodes => [
      ( 1, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      ( 2, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      ( 3, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      ( 4, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      ( 5, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      ( 6, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      ( 7, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      ( 8, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      ( 9, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (10, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (11, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (12, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (13, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (14, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (15, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (16, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (17, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (18, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (19, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (20, [0.0 -1.0 0.0 0.0 0.0 0.0]),
      (21, [0.0 -1.0 0.0 0.0 0.0 0.0])
    ]
)

data |> display
println()

m, dis_df, fm_df = p44(data)

println("Displacements:")
m.displacements |> display
println()

println("Actions:")
m.actions |> display
println()

println("y displacements:")
m.displacements[2,:] |> display
println()

println("y moment actions:")
m.actions[12,:] |> display
println()

if VERSION.minor == 6
  using Plots
  gr(size=(400,600))

  p = Vector{Plots.Plot{Plots.GRBackend}}(undef, 3)
  p[1] = plot(m.x_coords, m.displacements[2,:], ylim=(-0.004, 0.001), lab="Displacement",
   xlabel="x [m]", ylabel="deflection [m]", color=:red)
  p[2] = plot(m.actions[2,:], lab="Shear force", ylim=(-10, 10), xlabel="element",
    ylabel="shear force [N]", palette=:greens,fill=(0,:auto),α=0.6)
  p[3] = plot(m.actions[12,:], lab="Moment", ylim=(-7, 7), xlabel="element",
    ylabel="moment [Nm]", palette=:grays,fill=(0,:auto),α=0.6)

  plot(p..., layout=(3, 1))
  savefig(ProjDir*"/figure-23.png")
  #=
  plot!()
  gui()
  =#
end

if VERSION.minor > 5

  # See figure 24 in above reference (Δmax):
  @eval @test m.displacements[2,11] ≈ -10000 * 4^3 / (192 * 1.0e6) atol=10.0*eps()

  # See figure 24 in above reference (Mmax):
  @eval @test m.actions[12,10] ≈ (10000 * 4 / 8) atol=10.0*eps()
end


using MITgcmTools, ClimateModels, MeshArrays, OceanStateEstimation
using Test

MITgcm_download()

@testset "MITgcmTools.jl" begin

    #format conversions
    (γ,Γ)=MeshArrays.GridOfOnes("CubeSphere",30,30)
    @test isa(convert2gcmfaces(Γ.XC),Array)
    @test isa(convert2array(Γ.XC),Array)

    #physical oceanography
    (ρP,ρI,ρR) = SeaWaterDensity(3.,35.5,3000.)
    @test isapprox(ρI,1041.83267, rtol=1e-6)

    D=collect(0.0:1.0:500.0); tmp=(1.0.-tanh.(5*(-1 .+ 2/D[end]*D)));
    T=2.0 .+ 8.0*tmp; S=34.0 .+ 0.5*tmp;
    (ρP,ρI,ρR) = SeaWaterDensity(T,S,D);
    mld=MixedLayerDepth(T,S,D,"BM");
    @test isapprox(mld,134.0)

    #running and reading in verification experiments
    exps=verification_experiments()
    @test isa(exps,Array)

    MC=MITgcm_config(configuration="advect_xy")
    setup(MC)

    fil=joinpath(MC.folder,string(MC.ID),"run","data")
    nml=read(fil,MITgcm_namelist())
    write(fil*"_new",nml)

    MITgcmTools.parse_param("1.0")
    MITgcmTools.parse_param(".TRUE.")
    MITgcmTools.parse_param(".false.")
    MITgcmTools.parse_param("10")

    @test isa(nml,MITgcm_namelist)
    @test nml.groups[1]==:PARM01
    @test nml.params[1][:implicitFreeSurface]

    #

    myexp="advect_cs"
    iexp=findall([exps[i].configuration==myexp for i in 1:length(exps)])[1]
    MC=exps[iexp]

    @test clean(MC)=="no task left in pipeline"
    @test build(MC)
    @test build(MC,"--allow-skip")
    @test compile(MC)
    @test setup(MC)

    push!(MC.status,("setup" => "ended"))

    launch(MC)
    pth=joinpath(MC.folder,string(MC.ID),"run")
    tmp=read_mdsio(pth,"XC.001.001")
    @test isa(tmp,Array)
    tmp=read_mdsio(pth,"XC")
    @test isa(tmp,Array)

    scan_rundir(pth)
    Γ=GridLoad_mdsio(MC)
    @test isa(Γ,NamedTuple)

    myexp="MLAdjust"
    iexp=findall([exps[i].configuration==myexp for i in 1:length(exps)])[1]
    MC=exps[iexp]
    setup(MC)
    build(MC,"--allow-skip")
    launch(MC)
    if isdir(joinpath(MC.folder,string(MC.ID),"run","mnc_test_0001"))
        Γ=GridLoad_mnc(MC)
    else
        Γ=GridLoad_mdsio(MC)
    end
    @test isa(Γ,NamedTuple)

    #read / write functions

    fil=joinpath(pth,"available_diagnostics.log")
    read_available_diagnostics("ETAN";filename=fil)

    readcube(xx::Array,x::MeshArray) = read(cube2compact(xx),x)
    function readcube(fil::String,x::MeshArray)
        p=dirname(fil)*"/"
        b=basename(fil)[1:end-5]
        xx=read_mdsio(p,b)
        read(cube2compact(xx),x)
    end
    writecube(x::MeshArray) = compact2cube(write(x))
    writecube(fil::String,x::MeshArray) = write(fil::String,x::MeshArray)

    γ=gcmgrid(pth,"CubeSphere",6,fill((32, 32),6), [192 32], Float64, readcube, writecube)
    Γ = GridLoad(γ)
    tmp1=writecube(Γ.XC)
    tmp2=readcube(tmp1,Γ.XC)

    @test isa(tmp2,MeshArray)

    ##

    γ=GridSpec("LatLonCap",MeshArrays.GRID_LLC90)
    findtiles(30,30,γ)
    findtiles(30,30,"LatLonCap",MeshArrays.GRID_LLC90)
    read_meta(MeshArrays.GRID_LLC90,"XC.meta")

    fil=joinpath(MeshArrays.GRID_LLC90,"XC.data")
    tmp1=read_bin(fil,γ)
    tmp2=convert2gcmfaces(tmp1)
    tmp3=convert2array(tmp1)
    tmp4=convert2array(tmp3,γ)
    read_bin(fil,γ.ioPrec,γ)
    read_bin(tmp2,tmp1)
    read_bin(tmp2,γ)

    function get_ecco_variable_if_needed(v::String)
        p=dirname(pathof(OceanStateEstimation))
        lst=joinpath(p,"../examples/nctiles_climatology.csv")
        pth=ECCOclim_path
        !isdir(pth*v) ? get_from_dataverse(lst,v,pth) : nothing
    end

    get_ecco_variable_if_needed("ETAN")
    tmp=read_nctiles(joinpath(ECCOclim_path,"ETAN/ETAN"),"ETAN",γ,I=(:,:,1))

    @test isa(tmp,MeshArray)

    ##

    MC=MITgcm_config(configuration="flt_example")
    tmp=testreport(MC)
    pth=MITgcm_path[1]*"verification/flt_example/run/"
    tmp=read_flt(pth,Float32)

    @test isa(tmp[1,1],Number)

end


abstract type ETSModel end
import Optim: optimize
"""
SES
Description:
    - Contains parameters and data to apply Simple Exponential Smoothing (SES)
    - Subtype of ETSModel, for future proofing
Constuctor:
    - Vector{AbstractFloat} y: contains observations for forecasting
    - Integer h: Stores the number of steps after time T to forecast (T = latest time in the observed data)
        - Default = 10, must be > 0
    - Union{Nothing, Float64} alpha: smoothing parameter [1]
        - 0 < alpha < 1
        - If no value is specified, an optimal value will be chosen using LBFGS.
    - Union{Nothing, Float64} init_level: initial level value [1]
        -Inf < init_level < Inf
        - If no value is specified, an optimal value will be chosen uisng LBFGS.

EXAMPLE USAGE:
    observations = [1.0, 2.0, 3.0]
    # Inputted values will be used if they are specified. Otherwise, they will be computed.
    mdl = SES(observations, h = 5)
    mdl = SES(observations, alpha = 0.4)
    mdl = SES(observations, alpha = 0.25, init_level = 500.0)
    mdl = SES(observations, h = 15, alpha = 0.3, init_level = 750.0)

"""
mutable struct SES <: ETSModel
    y::Vector{AbstractFloat}
    h::Integer
    alpha::Union{Nothing, Float64}
    init_level::Union{Nothing, Float64}
    function SES(y = []; h = 5, alpha = nothing, init_level = nothing) # Constuctor
        # Input Cleaning
        if length(y) <= 0
            error("The input array is empty.")
        end
        if h <= 0
            error("The number of prediction steps must be positive")
        end

        cleanParams_(alpha, "alpha")

        return new(y, h, alpha, init_level)
    end
end

"""
makeForecast_()
Description:
    - Helper function
    - computes a forecast with Simple Exponential Smoothing (SES) [1]
Parameters
    - model: SES struct (defined above)
    - Note: when makeForecast_ is called, all parameters have been calibrated.
Returns:
    A tuple containing the following:
    - Vector{Float64} forecast: contains fitted values
    - SSE::Float64 SSE: the Sum of the Squares (error term)
        - used for parameter optimzation
References:
    [1] Hyndman, R.J., & Athanasopoulos, G. (2019) *Forecasting:
        principles and practice*, 3rd edition, OTexts: Melbourne,
        Australia. OTexts.com/fpp3. Accessed on April 19th 2020.
"""
function makeForecast_(model::SES) # Return vector of fitted values of length h + SSE
    h = model.h
    alpha = model.alpha
    init_level = model.init_level
    y = model.y
    data_size = length(y)

    lvls = zeros(data_size + 1) # Stores smoothed values
    forecast = zeros(data_size + h) # Stores fitted values
    lvls[1] = init_level # Set l_0
    forecast[1] = init_level
    SSE = 0.0 # Compute Sum of Squared Errors

    for i in 2:(length(lvls)) # Compute l_t's
        lvls[i] = alpha * y[i-1] + (1 - alpha) * lvls[i-1];
        forecast[i] = lvls[i]
        SSE += (forecast[i-1] - y[i-1])^2
    end

    for i in 1:h # Set forecasted values
        forecast[data_size + i] = lvls[end]
    end

    return forecast, SSE
end

"""
SSE_()
Description:
    - Returns the Sum of Squared Errors for a given alpha and init_level.
Parameters:
    - model: SES struct (defined above)
    - alpha: value of alpha used for computation
    - init_level: value of init_level used for computation
    - verbose: if verbose > 0, then the parameters will be printed as the are optimized.
Returns:
    - Float64 res: the Sum of Squared Errors for the given alpha and init_level values.
"""
function SSE_(model::SES; alpha = nothing, init_level = nothing, verbose = 0) # Helper function for optimization
    model.alpha = alpha
    model.init_level = init_level

    res = makeForecast_(model)[2]
    if verbose > 0
        println("Error: $res, Alpha: $(model.alpha), L0: $(model.init_level)")
    end

    return res
end
"""
cleanParams_()
Description:
    Throws an error if a given parameters is out of the specified bounds.
Parameters:
    - param: value of parameter to be checked
    - name: name of parameter to be checked
    - lb: lower bound of parameter, default = 0.0
    - ub: upper bound of parameter, defualt = 1.0
Note: if param is not of type Nothing, then param must be in (0.0, 1.0)
Returns:
    - nothing
"""
function cleanParams_(param::Union{Nothing, Float64}, name::String, lb::Float64 = 0.0, ub::Float64 = 1.0)
    if !isnothing(param) && (param < lb || param > ub)
        error(name * " needs to be in the range ($lb, $ub)")
    end
end
"""
fit!(model::ETSModel)
Description:
    fits an ETSModel object
    - computes initial values of parameters: 'alpha' and 'init_value'
    - modifies the 'model' directly.
    - after fit! is called, predict(model) is ready to be called
Parameters:
    - model: SES struct
    - v: verbosity, default = 0. if v > 0, verbose will be used.
Computation of model parameters:
'alpha' and 'init_value' are computed using LBFGS
Returns:
    - nothing
"""
function fit(model::SES; v=0) # Set alpha + init_level
    # Creating a copy of the user inputted params for the model as model.alpha and model.init_level will be updated later
    base_alpha = model.alpha
    base_init_level = model.init_level

    # Model to return to ensure that input model is not modified
    retModel = deepcopy(model)

    if (isnothing(retModel.alpha))
        @warn "Since no value was entered for 'alpha', it will be chosen"
    end
    if (isnothing(retModel.init_level))
        # model.init_level = compute_init_heuristic_(model.y)
        @warn "Since no value was entered for 'init_level', it will be chosen"
    end

    # Optimize predict(model) - directly updates model
    f(x) = SSE_(retModel,
                alpha=(isnothing(base_alpha) ? x[1] : base_alpha),
                init_level=(isnothing(base_init_level) ? x[2] : base_init_level),
                verbose=v)

    if isnothing(retModel.alpha) || isnothing(retModel.init_level) # if atleast one value needs to be optimized
        # lower and upper limits for alpha and init_level
        lower = [0.0, -Inf]
        upper = [1.0, Inf]
        # Initial values for alpha and init_level
        init_inputs = [0.0, model.y[1]]
        res = optimize(f, lower, upper, init_inputs)
        if v > 0
            println(res)
        end
    end

    return retModel
end
"""
predict()
Description:
    - Exposed function
    - computes a forecast with Simple Exponential Smoothing (SES) [1]
    - calls makeForecast_ a helper function, which returns a vector of fitted values and SSE error term.
Parameters
    - model: SES struct (defined above)
    - Note: when predict is called, all parameters have been calibrated.
Returns:
    A tuple containing the following:
    - Vector{Float64} forecast: contains fitted values
References:
    [1] Hyndman, R.J., & Athanasopoulos, G. (2019) *Forecasting:
        principles and practice*, 3rd edition, OTexts: Melbourne,
        Australia. OTexts.com/fpp3. Accessed on April 19th 2020.
"""
function predict(model::ETSModel)
    return makeForecast_(model)[1]
end

### Holt's Method - implementing Holt's linear trend method
mutable struct Holt <: ETSModel
    y::Vector{AbstractFloat}
    h::Integer
    alpha::Union{Nothing, Float64}
    beta::Union{Nothing, Float64}
    phi::Union{Nothing, Float64}
    init_level::Union{Nothing, Float64}
    init_trend::Union{Nothing, Float64}
    damped::Bool
    function Holt(y = []; h = 5, alpha = nothing, beta = nothing, phi = nothing, init_level = nothing, init_trend = nothing, damped = false) # Constuctor
        # Input Cleaning
        if length(y) <= 0
            error("The input array is empty.")
        end
        if h <= 0
            error("The number of prediction steps must be positive")
        end

        cleanParams_(alpha, "alpha")
        cleanParams_(beta, "beta")
        cleanParams_(phi, "phi", 0.80, 0.98)

        return new(y, h, alpha, beta, phi, init_level, init_trend)
    end
end

function makeForecast_(model::Holt) # Return vector of fitted values of length h + SSE
    h = model.h
    alpha = model.alpha
    beta = model.beta
    phi = model.phi
    init_level = model.init_level
    init_trend = model.init_trend
    y = model.y
    data_size = length(y)

    lvls = zeros(data_size + 1) # Stores level values, indices (0, T)
    trends = zeros(data_size + 1) # Stores trend values, indices (0, T)
    forecast = zeros(data_size + h) # Stores forecasted values, yhat, indices (1, T+h)

    lvls[1] = init_level # Set l_0
    trends[1] = init_trend # Set b_0

    SSE = 0.0 # Compute Sum of Squared Errors

    for i in 2:(length(lvls)) # Compute l_t's
        lvls[i] = alpha * y[i-1] + (1 - alpha) * (lvls[i-1] + trends[i-1]) # level equation
        trends[i] = beta * (lvls[i] - lvls[i-1]) + (1 - beta) * trends[i-1] # trned equation
        forecast[i-1] = lvls[i-1] + 1 * trends[i-1] # one-step ahead forecast
        SSE += (forecast[i-1] - y[i-1])^2
    end

    for i in 1:h # Set forecasted values
        forecast[data_size + i] = lvls[end] + i * trends[end]
    end

    return forecast, SSE
end

function SSE_(model::Holt; alpha = nothing, beta = nothing, phi = nothing, init_level = nothing, init_trend = nothing, verbose = 0) # Helper function for optimization
    model.alpha = alpha
    model.beta = beta
    model.phi = phi
    model.init_level = init_level
    model.init_trend = init_trend

    res = makeForecast_(model)[2]
    if verbose > 0
        println("Error: $res, Alpha: $(model.alpha), Beta*: $(model.beta), Phi: $(model.phi), L0: $(model.init_level), BO:$(model.init_trend)")
    end

    return res
end

function fit(model::Holt; v=0) # Set alpha + init_level
    # Creating a copy of the user inputted params for the model as model.alpha and model.init_level will be updated later
    base_alpha = model.alpha
    base_beta = model.beta
    base_phi = model.phi
    base_init_level = model.init_level
    base_init_trend = model.init_trend

    # copy of model to return to ensure that input model is not modified
    retModel = deepcopy(model)

    if (isnothing(retModel.alpha))
        @warn "Since no value was entered for 'alpha', it will be chosen"
    end
    if (isnothing(retModel.beta))
        @warn "Since no value was entered for 'beta', it will be chosen"
    end
    if (isnothing(retModel.init_level))
        @warn "Since no value was entered for 'init_level', it will be chosen"
    end
    if (isnothing(retModel.init_trend))
        @warn "Since no value was entered for 'init_trend', it will be chosen"
    end

    if damped
        print("The Damped Trend Method will be used.")
        if (isnothing(retModel.phi))
            @warn "Since no value was entered for `phi`, it will be chosen"
        end
    else
        if (!isnothing(retModel.phi))
            @warn "Since a value was entered for `phi`, damping will occur despite 'damped' being set to false"
        else
            base_phi = 1 # No damping is equivalent to setting the damping parameter to 1
        end
    end

    # Optimize makeForecast_(model) - directly updates model
    f(x) = SSE_(retModel,
                alpha=(isnothing(base_alpha) ? x[1] : base_alpha),
                beta=(isnothing(base_beta) ? x[2] : base_beta),
                phi=(isnothing(base_phi) ? x[3] : base_phi),
                init_level=(isnothing(base_init_level) ? x[4] : base_init_level),
                init_trend=(isnothing(base_init_trend) ? x[5] : base_init_trend),
                verbose=v)

    if isnothing(retModel.alpha) || isnothing(retModel.init_level) || isnothing(retModel.init_trend) || isnothing(retModel.beta) || (damped && isnothing(retModel.phi))# if at least one value needs to be optimized
        # lower and upper limits for alpha, beta, phi, init_level, init_trend
        lower = [0.0, 0.0, 0.8, -Inf, -Inf]
        upper = [1.0, 1.0, 0.98, Inf, Inf]

        # Initial values for alpha, beta, init_level, init_trend (optimization starting points)
        first_l0 = model.y[1]
        first_b0 = model.y[1]
        first_alpha = 0.0
        first_beta = 0.0
        first_phi = 0.9

        # Optimize the SSE of the model through the function "f"
        init_inputs = [first_alpha, first_beta, first_phi, first_l0, first_b0]
        res = optimize(f, lower, upper, init_inputs)

        if v > 0
            println(res)
        end
    end

    return retModel
end

### HoltWinters 's Method - implementing the HoltWinters Additive method
mutable struct HoltWinters <: ETSModel
    y::Vector{AbstractFloat}
    h::Integer
    alpha::Union{Nothing, Float64}
    beta::Union{Nothing, Float64}
    gamma::Union{Nothing, Float64}
    m::Integer
    init_level::Union{Nothing, Float64}
    init_trend::Union{Nothing, Float64}
    init_season::Vector{AbstractFloat}
    function HoltWinters(y = []; h = 5, alpha = nothing, beta = nothing, gamma = nothing, m, init_level = nothing, init_trend = nothing, init_season) # Constuctor
        # Input Cleaning
        if length(y) <= 0
            error("The input array is empty.")
        end
        if h <= 0
            error("The number of prediction steps must be positive")
        end
        if isnothing(m)
            error("You must provide a seasonal period 'm'")
        end
        if m <= 1
            error("'m' must be an integer greater than 1")
        end
        if length(init_season) != m
            error("The length of 'init_season' must be equal to `m`")
        end

        cleanParams_(alpha, "alpha")
        cleanParams_(beta, "beta")
        cleanParams_(gamma, "gamma")

        return new(y, h, alpha, beta, gamma, m, init_level, init_trend, init_season)
    end
end

function makeForecast_(model::HoltWinters) # Return vector of fitted values of length h + SSE
    h = model.h
    alpha = model.alpha
    beta = model.beta
    gamma = model.gamma
    m = model.m
    init_level = model.init_level
    init_trend = model.init_trend
    init_season = model.init_season
    y = model.y
    data_size = length(y)

    lvls = zeros(data_size + 1) # Stores level values, indices (0, T)
    trends = zeros(data_size + 1) # Stores trend values, indices (0, T)
    seasons = zeros(data_size + m) # Stores seasonal values
    forecast = zeros(data_size + h) # Stores forecasted values, yhat, indices (1, T+h)

    lvls[1] = init_level # Set l_0
    trends[1] = init_trend # Set b_0
    seasons[1:m] = init_season # Set s_0 ... s_m-1

    SSE = 0.0 # Compute Sum of Squared Errors
    k = trunc(Integer, (h-1)/m)

    for i in 2:(length(lvls)) # Compute l_t's
        season_idx = i + m - 1 # to start updating values after index m

        lvls[i] = alpha * (y[i-1] - seasons[season_idx - m]) + (1 - alpha) * (lvls[i-1] + trends[i-1]) # level equation
        trends[i] = beta * (lvls[i] - lvls[i-1]) + (1 - beta) * trends[i-1] # trend equation
        seasons[season_idx] = gamma * (y[i-1] - lvls[i-1] - trends[i-1]) + (1 - gamma) * seasons[season_idx - m] # season equation
        forecast[i-1] = lvls[i-1] + 1 * trends[i-1] + season[season_idx + 1 - m * (k+1)] # one-step ahead forecast
        SSE += (forecast[i-1] - y[i-1])^2
    end

    for i in 1:h # Set forecasted values
        forecast[data_size + i] = lvls[end] + i * trends[end + i - m * (k+1)]
    end

    return forecast, SSE
end

function SSE_(model::HoltWinters; alpha = nothing, beta = nothing, gamma = nothing, init_level = nothing, init_trend = nothing, verbose = 0) # Helper function for optimization
    model.alpha = alpha
    model.beta = beta
    model.gamma = gamma
    model.init_level = init_level
    model.init_trend = init_trend

    res = makeForecast_(model)[2]
    if verbose > 0
        println("Error: $res, Alpha: $(model.alpha), Beta*: $(model.beta), Gamma: $(model.gamma), L0: $(model.init_level), BO:$(model.init_trend), S0:$(model.init_season)")
    end

    return res
end

function fit(model::HoltWinters; v=0) # Set alpha + init_level
    # Creating a copy of the user inputted params for the model as model.alpha and model.init_level will be updated later
    base_alpha = model.alpha
    base_beta = model.beta
    base_gamma = model.gamma
    base_init_level = model.init_level
    base_init_trend = model.init_trend

    # copy of model to return to ensure that input model is not modified
    retModel = deepcopy(model)

    if (isnothing(retModel.alpha))
        @warn "Since no value was entered for 'alpha', it will be chosen"
    end
    if (isnothing(retModel.beta))
        @warn "Since no value was entered for 'beta', it will be chosen"
    end
    if (isnothing(retModel.gamma))
        @warn "Since no value was entered for 'gamma', it will be chosen"
    end
    if (isnothing(retModel.init_level))
        @warn "Since no value was entered for 'init_level', it will be chosen"
    end
    if (isnothing(retModel.init_trend))
        @warn "Since no value was entered for 'init_trend', it will be chosen"
    end

    # Optimize makeForecast_(model) - directly updates model
    f(x) = SSE_(retModel,
                alpha=(isnothing(base_alpha) ? x[1] : base_alpha),
                beta=(isnothing(base_beta) ? x[2] : base_beta),
                gamma=(isnothing(base_gamma) ? x[3] : base_gamma),
                init_level=(isnothing(base_init_level) ? x[4] : base_init_level),
                init_trend=(isnothing(base_init_trend) ? x[5] : base_init_trend),
                verbose=v)

    if isnothing(retModel.alpha) || isnothing(retModel.beta) || (isnothing(retModel.gamma)) || isnothing(retModel.init_level) || isnothing(retModel.init_trend) # if at least one value needs to be optimized
        # lower and upper limits for alpha, beta, gamma, init_level, init_trend
        lower = [0.0, 0.0, 0.0, -Inf, -Inf]
        upper = [1.0, 1.0, 1.0, Inf, Inf]

        # Initial values for alpha, beta, init_level, init_trend (optimization starting points)
        first_l0 = model.y[1]
        first_b0 = model.y[1]
        first_alpha = 0.0
        first_beta = 0.0
        first_gamma = 0.0

        # Optimize the SSE of the model through the function "f"
        init_inputs = [first_alpha, first_beta, first_gamma, first_l0, first_b0]
        res = optimize(f, lower, upper, init_inputs)

        if v > 0
            println(res)
        end
    end

    return retModel
end


#
#--------------------------------------#
export restrict, restrictUpper, restrictLower
#--------------------------------------#
"""
 R * u = u[2:end-1]
 R'* u = [0,u,0]
"""
function restrict(n::Integer)
    Id = sparse(I,n,n)
    R  = Id[2:n-1,:]
    return R
end
"""
 R * u = u[2:end]
 R'* u = [0,u]
"""
function restrictUpper(n::Integer)
    Id = sparse(I,n,n)
    R  = Id[2:n,:]
    return R
end
"""
 R * u = u[1:end-1]
 R'* u = [u,0]
"""
function restrictLower(n::Integer)
    Id = sparse(I,n,n)
    R  = Id[1:n-1,:]
    return R
end
#--------------------------------------#
export Tdg
#--------------------------------------#
"""
 Adjointable tridiagonal functor
"""
struct Tdg
    dl::AbstractVector # lower diag
    dd::AbstractVector # main  diag
    du::AbstractVector # upper diag
end

Flux.@functor Tdg

function (T::Tdg)(u::AbstractArray)
    n  = length(T.dd)

    Ru,Rl = Zygote.ignore() do
        Ru = restrictUpper(n)
        Rl = restrictLower(n)
        return Ru,Rl
    end

    v  = u .* T.dd              # main  diag
    v += Rl' * (T.du .* (Ru*u)) # upper diag
    v += Ru' * (T.dl .* (Rl*u)) # lower diag

    return v
end

Base.:*(T::Tdg,u::AbstractArray) = T(u)
Base.:adjoint(T::Tdg) = Tdg(T.du,T.dd,T.dl)
#--------------------------------------#
export lsq
#--------------------------------------#
function lsq(u,A)  # target data, linear function

    x = A \ u

#   AA = Matrix{Float32}(A)
#   uu = vec(u')
#   x,stat = Krylov.cgls(AA,uu)

    return x
end
#--------------------------------------#
#


export BPoint, cartesian, cross, angle_vector, project, angle_scalar, ×, unit, norm_angles, dim_count

struct BPoint
    ρ::Float64
    angles::Array{Float64, 1}

    function BPoint(length, angles::T...) where T <: Number
        new(Float64(length), Float64[Float64(angle) for angle in angles])
    end

    BPoint(length, angles::Array{Float64, 1}) = new(Float64(length), angles)
    BPoint(n::Number) = new(Float64(n), [0])

    "Convert Cartesian to Spherical"
    function BPoint(coordinates::Vector{Float64})
        ρ::Float64 = 0
        for coord in coordinates
            ρ += coord ^ 2
        end

        partialp = ρ
        ρ = sqrt(ρ)

        addPI = coordinates[end] < 0
        deleteat!(coordinates, length(coordinates))

        for i in 1:length(coordinates)
            value = acos(coordinates[i] / sqrt(partialp))
            partialp -= coordinates[i] ^ 2
            coordinates[i] = value
        end

        if addPI
            coordinates[end] = 2 * pi - coordinates[end]
        end

        BPoint(ρ, coordinates)
    end

    Base.length(p::BPoint) = p.ρ

    Base.:+(p::BPoint, n::Number)::BPoint = BPoint(p.ρ + n, p.angles)
    Base.:-(p::BPoint, n::Number)::BPoint = BPoint(p.ρ - n, p.angles)
    Base.:*(p::BPoint, n::Number)::BPoint = BPoint(p.ρ * n, p.angles)
    Base.:/(p::BPoint, n::Number)::BPoint = BPoint(p.ρ / n, p.angles)
    Base.:^(p::BPoint, pow::Number)::BPoint = BPoint(p.ρ ^ pow, p.angles)

    Base.:+(n::Number, p::BPoint)::BPoint = BPoint(p.ρ + n, p.angles)
    Base.:-(n::Number, p::BPoint)::BPoint = BPoint(n - p.ρ, p.angles)
    Base.:*(n::Number, p::BPoint)::BPoint = BPoint(p.ρ * n, p.angles)
    Base.:/(n::Number, p::BPoint)::BPoint = BPoint(n / p.ρ, p.angles)
    Base.:^(n::Number, pow::BPoint)::BPoint = BPoint(pow ^ p.ρ, p.angles)

    function Base.:+(p1::BPoint, p2::BPoint)::BPoint
        angles = norm_angles(p1, p2)
        return BPoint(p1.ρ + p2.ρ, angles)
    end

    function Base.:*(p1::BPoint, p2::BPoint)::BPoint
        angles = norm_angles(p1, p2)
        return BPoint(p1.ρ * p2.ρ, angles)
    end

    function Base.:/(p1::BPoint, p2::BPoint)::BPoint
        angles = norm_angles(p1, p2)
        return BPoint(p1.ρ / p2.ρ, angles)
    end

    function Base.:-(p1::BPoint, p2::BPoint)::BPoint
        angles = norm_angles(p1, p2)
        return BPoint(p1.ρ - p2.ρ, angles)
    end

end

dim_count(p)::Int64 = length(p.angles)

"Project p1 onto p2"
project(p1::BPoint, p2::BPoint)::BPoint = BPoint(p1.ρ * p2.ρ * cos(angle_scalar(p1, p2)), p2.angles)

"Convert to Cartesian"
function cartesian(p)::Array{Float64, 1}
     coordinates = zeros(Float64, dim_count(p) + 1)
     coordinates[1] = p.ρ * cos(p.angles[1])

     sin_product = p.ρ * sin(p.angles[1])
     for i in 2:(length(coordinates) - 1)
         coordinates[i] = sin_product * cos(p.angles[i])
         sin_product *= sin_product * sin(p.angles[i])
     end

     coordinates[end] = sin_product
     coordinates
end

"Angle Number Between Numbers"
function angle_vector(p1::BPoint, p2::BPoint)::Array{Float64, 1}
    check(p1, p2)
    angles = zeros(Float64, dim_count(p1))
    for i in 1:dim_count(p1)
        angles[i] = p1.angles[i] - p2.angles[i]
    end
    angles
end

angle_scalar(p1::BPoint, p2::BPoint)::Float64 = sum(angle_vector(p1, p2))

"Unit Number"
unit(p::BPoint)::BPoint = BPoint(1, p.angles)
unit(angles::Float64...)::BPoint = BPoint(1, collect(angles))
unit(angles::AbstractArray{Float64})::BPoint = BPoint(1, angles)

function check(p1::BPoint, p2::BPoint)
    if dim_count(p1) != dim_count(p2)
        throw(DimensionMismatch("Dimension Mismatch between $p1 and $p2"))
    end
end

"Normalized Angles"
function norm_angles(p1::BPoint, p2::BPoint)::Array{Float64, 1}
    check(p1, p2)
    angles = zeros(Float64, dim_count(p1))
    for i in 1:dim_count(p1)
        angles[i] = (p1.ρ * p1.angles[i] + p2.ρ * p2.angles[i]) / (p1.ρ + p2.ρ)
    end
    angles
end

"Cross Product"
function ×(p1::BPoint, p2::BPoint)::BPoint
    check(p1, p2)
    angle_add = angle_scalar(p1, p2) > pi ? true : false
    anglev = angle_vector(p1, p2)
    for i in 1:dim_count(p1)
        anglev[i] = angle_add ? anglev[i] - (pi/2) : anglev[i] + (pi/2)
    end

    #TODO Change to be area of parallelpid
    return BPoint(p1.ρ * p2.ρ, anglev)
end

cross(p1::BPoint, p2::BPoint)::BPoint = p1 × p2


using
  FourierFlows.KuramotoSivashinsky,
  PyPlot

nx = 512
Lx = 32π
dt = 0.1
nt = 2000
prob = Problem(nx=nx, Lx=Lx, dt=dt, stepper="ETDRK4")

x = prob.grid.x
u0 = @. cos(x/16) * (1 + sin(x/16))
set_u!(prob, u0)

T = [ dt*j for i=1:nx, j=1:nt ]
X = [ x[i] for i=1:nx, j=1:nt ]
u = zeros(nx, nt)

for i = 1:nt
  stepforward!(prob)
  updatevars!(prob)
  u[:, i] .= prob.vars.u
end

close("all")
fig, ax = subplots(figsize=(6, 8))
pcolormesh(X/2π, T, u)
ax[:tick_params](bottom=false, left=false, labelbottom=false, labelleft=false)
show()


function plan_spatial_ft(x::AbstractArray{<: Number, N}, dims::Integer) where N
	# x[spatial dimensions..., other dimensions...]
	# Pay attention that they have to be applied in pairs! Otherwise scaling
	FFT = plan_fft(x, 1:dims)
	FFTH = inv(FFT)
	shape = size(x)
	restore_shape(x) = reshape(x, shape) # Note that x is Vector
	F = LinearMap{ComplexF64}(
		x -> begin
			x = restore_shape(x)
			y = FFT * x
			vec(y)
		end,
		y -> let
			y = restore_shape(y)
			x = FFTH * y
			vec(x)
		end,
		prod(shape)
	)
	return F
end

function plan_spatial_ft(
	kx::AbstractArray{<: Number},
	ky::AbstractArray{<: Number},
	shape::NTuple{3, Integer}
) where N
	# Scaling?

	# Get dims
	num_frequencies = length(kx)
	@assert length(ky) == num_frequencies
	spatial_dims = [shape[1], shape[2]]
	spatial_length = prod(spatial_dims)
	other_dims = shape[3]

	# Plan NUFFT
	forward_plan = finufft_makeplan(
		2, # type
		spatial_dims,
		-1, # iflag
		other_dims, # ntrans
		1e-8 # eps
	)
	backward_plan = finufft_makeplan(
		1, # type
		spatial_dims,
		1, # iflag
		other_dims, # ntrans
		1e-8 # eps
	)
	for plan in (forward_plan, backward_plan)
		# This should go into the finufft package
		finalizer(plan) do plan
			if finufft_destroy!(plan) != 0
				error("Could not destroy finufft plan")
			end
		end
	end
	normalisation = 1 / spatial_length
	finufft_setpts!(forward_plan, kx, ky)
	finufft_setpts!(backward_plan, kx, ky)

	F = LinearMap{ComplexF64}(
		x -> begin
			x = reshape(x, shape)
			y = finufft_exec(forward_plan, x)
			vec(y)
		end,
		y -> let
			y = reshape(y, num_frequencies, other_dims)
			x = finufft_exec(backward_plan, y) * normalisation
			vec(x)
		end,
		num_frequencies * other_dims, # in
		spatial_length * other_dims # out
	)

	return F
end


using BenchmarkTools
using BasicBSpline
using GeometryBasics
using Random

const SUITE = BenchmarkGroup()

Random.seed!(42)

p = 3
k = Knots(rand(100)) + (p+1) * Knots(0,1)
P = FastBSplineSpace(p,k)
i = 1
t = 0.02

SUITE["fast-bsplinebasis"] = @benchmarkable bsplinebasis(i,P,t)

p1 = 2
k1 = Knots(rand(3)) + (p1 + 1) * Knots([0, 1])
P1 = FastBSplineSpace(p1, k1)
n1 = dim(P1)
a = [Point(i1, rand()) for i1 in 1:n1]
M = BSplineCurve([P1], a)

p1′ = p1 + 1
k1′ = k1 + unique(k1) + Knots(rand(2))
P1′ = FastBSplineSpace(p1′, k1′)
M′ = refinement(M, [P1′])

SUITE["refinement"] = @benchmarkable refinement(M, [P1′])
SUITE["fitting_I"] = @benchmarkable fittingcontrolpoints(M, [P1′])
SUITE["fitting_R"] = @benchmarkable fittingcontrolpoints(M, [P1′], domain=:R)


@testset "Isaac" begin

println("Basic correctness tests.")

# TODO: add tests determining what happens on failure!!!!

@testset "dirder" begin
   @info("testing `dirder`")
   for (f, df, init, randinit, name) in tests
      for i = 1:10
         x = randinit()
         w = rand(length(x)) .- 0.5; w /= norm(w)
         err = norm( df(x) * w - Isaac.dirder(x, w, f, f(x)) )
         print_tf(@test err < 1e-6)
      end
   end
   println()
end

@testset "dgmres" begin
   @info("testing dgmres")
   for (f, df, init, randinit, name) in tests
      x = randinit()
      fx = f(x)
      for tol in (1e-1, 1e-2, 1e-3)
         maxiter = 100
         u, error, total_iters = dgmres(fx, f, x, tol, maxiter)
         err = norm(Isaac.dirder(x, u, f, fx) - (-fx))
         print_tf(@test err / norm(fx) < tol)
         # TODO: This next test seems to fail quite often
         # @test abs(error[end] - err) < 1e-7
         print_tf(@test total_iters < maxiter)
      end
   end
   println()
end

@testset "nsoli" begin
   println("testing nsoli")
   for (f, df, init, randinit, name) in tests
      for i = 1:10
         x0 = randinit()
         atol = 1e-5
         maxit = 100
         x, it_hist, ierr, x_hist = nsoli(x0, f;
                                       atol = atol, rtol = 0.0, maxit = maxit)
         if norm(f(x)) > atol
            print_tf(@test ierr != 0)
            print_tf(@test length(x_hist) >= maxit)
         else
            print_tf(@test norm(f(x)) <= atol)
         end
      end
   end
   println()
end

end


@testset "Number fields" begin
  include("NumField/Elem.jl")
  include("NumField/Hilbert.jl")
  include("NumField/NfRel.jl")
  include("NumField/NfAbs.jl")
  include("NumField/QQ.jl")
  include("NumField/NonSimpleNumField.jl")
  include("NumField/CM.jl")
  include("NumField/Embedded.jl")
end


# here we keep code related to the lattice itself
# note that for the time being I will mostly specialize to 2 dimensions, later I will add code
# for 3 and 4 dimensions. This is being done to avoid splatting.

# TODO should probably implement specialized spacetime vectors using StaticArrays
# TODO fix spacetime indexing scheme

Base.getindex(lat::AbstractLattice, idx::Tuple) = idx
Base.getindex(lat::AbstractLattice, idx...) = getindex(lat, idx)

"""
    fieldsites(::Type{T}, lat::AbstractLattice)

Creates an uninitialized array with elements of type T for every poin on the lattice.
"""
function fieldsites(::Type{T}, lat::AbstractLattice{d}) where {T, d}
    Array{T, d}((lat.L for i ∈ 1:(d-1))..., lat.T)
end
export fieldsites

nsites(l::AbstractLattice{d}) where d = l.T*(l.L^(d-1))
export nsites

# this is an implementation that doesn't involve splatting
function Base.sub2ind(M::Tuple, m::Tuple)
    1 + sum((m[n]-1)*prod(M[1:(n-1)]) for n ∈ 1:length(M))
end

size(lat::AbstractLattice{d}) where d = tuple((lat.L for i ∈ 1:(d-1))..., lat.T)

sizenocheck(lat::AbstractLattice{d}, idx::Integer) where d = (idx == d) ? lat.T : lat.L

"""
    eachspacetimeindex(lat::AbstractLattice)

Return an iterator over every spacetime index in the lattice `lat`.  Spacetime indices will be
in the form of `SVector`s.
"""
function eachspacetimeindex(lat::AbstractLattice{d}) where d
    dims = size(lat)
    (SVector{d}(ind2sub(dims , x)) for x ∈ 1:nsites(lat))
end
export eachspacetimeindex

function size(lat::AbstractLattice{d}, idx::Integer) where d
    if idx == d
        return lat.T
    elseif 1 ≤ idx < d
        return lat.D
    else
        ArgumentError("Lattice only has $d spacetime dimensions.")
    end
end

"""
    LatticeToroidal <: AbstractLattice

A type for storing parameters of a simple Euclidean rectilinear lattice with D spatial dimensions
and toroidal topology in all directions.
"""
struct LatticeToroidal{d} <: AbstractLattice{d}
    a::Float64  # lattice spacing in GeV-1
    L::Int  # lattice length in units of a
    T::Int  # lattice duration in units of a
end
export LatticeToroidal

LatticeToroidal(d::Int, a::Float64, L::Integer, T::Integer) = LatticeToroidal{d}(a, L, T)
LatticeToroidal(d::Int, a::Float64, L::Integer) = LatticeToroidal(d, a, L, L)

# helper function for index computers
@inline function _loopedidx(i::Integer, L::Integer)
    # pretty sure this has to be different for negative and positive?
    # Note to self: think in terms of 0-based indexing, otherwise confusing as fuck
    if i < 1
        L + (i % L)
    else
        ((i-1) % L) + 1
    end
end

# these duplications are to avoid splatting
# WARNING right now these don't check lattice dimensions
# for rank-2
function Base.getindex(lat::LatticeToroidal, i::Integer, j::Integer)
    _loopedidx(i, lat.L), _loopedidx(j, lat.T)
end
# for rank-3
function Base.getindex(lat::LatticeToroidal, i::Integer, j::Integer, k::Integer)
    _loopedidx(i, lat.L), _loopedidx(j, lat.L), _loopedidx(k, lat.T)
end
# for rank-4
function Base.getindex(lat::LatticeToroidal, i::Integer, j::Integer, k::Integer, l::Integer)
    _loopedidx(i, lat.L), _loopedidx(j, lat.L), _loopedidx(k, lat.L), _loopedidx(l, lat.L)
end
Base.getindex(lat::LatticeToroidal, idx...) = getindex(lat, idx)
Base.getindex(lat::LatticeToroidal, idx::Tuple) = getindex(lat, idx...)
Base.getindex(lat::LatticeToroidal, idx::SVector) = getindex(lat, Tuple(idx))

# TODO compiler will not know size of these!
spacetimebasisvec(μ::Integer, N::Integer) = SVector{N}([i ≠ μ ? 0 : 1 for i ∈ 1:N])
spacetimebasisvec(μ::Integer, lat::AbstractLattice{d}) where d = spacetimebasisvec(μ, d)
export spacetimebasisvec

# TODO this is probably expensive and shitty
inc(x::SVector{N}, μ::Integer) where N = SVector{N}([i ≠ μ ? x[i] : x[i]+1 for i ∈ 1:N])
Base.dec(x::SVector{N}, μ::Integer) where N = SVector{N}([i ≠ μ ? x[i] : x[i]-1 for i ∈ 1:N])
export inc


using Distributed
@everywhere using MCPhylo

@everywhere extensions = quote
  using Distributions
  import Distributions: location, scale, skewness, kurtosis, minimum, maximum,
         quantile

  struct GK <: ContinuousUnivariateDistribution
    A::Float64
    B::Float64
    g::Float64
    k::Float64
    c::Float64

    function GK(A::Real, B::Real, g::Real, k::Real, c::Real)
      ## check args
      0.0 <= c < 1.0 || throw(ArgumentError("c must be in [0, 1)"))

      ## create distribution
      new(A, B, g, k, c)
    end

    GK(A::Real, B::Real, g::Real, k::Real) = GK(A, B, g, k, 0.8)
  end

  ## Parameters
  location(d::GK) = d.A
  scale(d::GK) = d.B
  skewness(d::GK) = d.g
  kurtosis(d::GK) = d.k
  asymmetry(d::GK) = d.c

  minimum(d::GK) = -Inf
  maximum(d::GK) = Inf

  function quantile(d::GK, p::Float64)
    z = quantile(Normal(), p)
    z2gk(d, z)
  end

  function z2gk(d::GK, z::Float64)
    term1 = exp(-skewness(d) * z)
    term2 = (1.0 + asymmetry(d) * (1.0 - term1) / (1.0 + term1))
    term3 = (1.0 + z^2)^kurtosis(d)
    location(d) + scale(d) * z * term2 * term3
  end
end

@everywhere eval(extensions)

d = GK(3, 1, 2, 0.5)
x = rand(d, 1000)

allingham = Dict{Symbol, Any}(
  :x => x
)

model = Model(
  x = Stochastic(1, (A, B, g, k) -> GK(A, B, g, k), false),
  A = Stochastic(() -> Uniform(0, 10)),
  B = Stochastic(() -> Uniform(0, 10)),
  g = Stochastic(() -> Uniform(0, 10)),
  k = Stochastic(() -> Uniform(0, 10))
)

inits = [
  Dict{Symbol, Any}(:x => x, :A => 3.5, :B => 0.5,
                    :g => 2.0, :k => 0.5),
  Dict{Symbol, Any}(:x => x, :A => median(x),
                    :B => sqrt(var(x)),
                    :g => 1.0, :k => 1.0),
  Dict{Symbol, Any}(:x => x, :A => median(x),
                    :B => diff(quantile(x, [0.25, 0.75]))[1],
                    :g => mean((x .- mean(x)).^3) / (var(x)^(3 / 2)),
                    :k => rand())
]

sigma1 = 0.05
sigma2 = 0.5
stats = x -> quantile(x, [0.1, 0.25, 0.5, 0.75, 0.9])
epsilon = 0.1

scheme = [ABC([:A, :B, :k],
              sigma1, stats, epsilon, maxdraw=50, decay=0.75, randeps=true),
          ABC(:g, sigma2, stats, epsilon, maxdraw=50, decay=0.75)]
setsamplers!(model, scheme)

sim = mcmc(model, allingham, inits, 10000, burnin=2500, chains=3)
describe(sim)
p = plot(sim)
draw(p, filename="gk")


export row_swap!, row_mul!, row_add!, ref!, rref!,
       row_swap,  row_mul,  row_add,  ref,  rref

# Call f on a copy of A and return the (modified) copy
function with_copy(f, A, args...; kwargs...)
  B = copy(A)
  f(B, args...; kwargs...)
  return B
end

"""
    row_swap!(A, i, j)

Swap rows `i` and `j` in matrix `A`.

Returns a copy of the two rows in the new order.

# Example

```jldoctest
julia> A = reshape(1:12, 3, 4) |> collect
3×4 Matrix{Int64}:
 1  4  7  10
 2  5  8  11
 3  6  9  12

julia> row_swap!(A, 1, 3)
2×4 Matrix{Int64}:
 3  6  9  12
 1  4  7  10

julia> A
3×4 Matrix{Int64}:
 3  6  9  12
 2  5  8  11
 1  4  7  10
```
"""
row_swap!(A, i, j) = A[[i,j], :] = A[[j,i], :]

"""
    row_swap(A, i, j)

Create a matrix equal to `A` but with rows `i` and `j` swapped.

# Example

```jldoctest
julia> A = reshape(1:12, 3, 4) |> collect
3×4 Matrix{Int64}:
 1  4  7  10
 2  5  8  11
 3  6  9  12

julia> row_swap(A, 1, 3)
3×4 Matrix{Int64}:
 3  6  9  12
 2  5  8  11
 1  4  7  10
```
"""
row_swap(A, i, j) = with_copy(row_swap!, A, i, j)

"""
    row_mul!(A, i => λ)

Multiply row `i` of matrix `A` by a factor `λ`.

Returns a copy of the new row `i`.

# Example

```jldoctest
julia> A = reshape(1:12, 3, 4) |> collect
3×4 Matrix{Int64}:
 1  4  7  10
 2  5  8  11
 3  6  9  12

julia> row_mul!(A, 2 => 100)
4-element Vector{Int64}:
  200
  500
  800
 1100

julia> A
3×4 Matrix{Int64}:
   1    4    7    10
 200  500  800  1100
   3    6    9    12
```
"""
row_mul!(A, (i, factor)) = A[i, :] *= factor

"""
    row_mul(A, i => factor)

Create a matrix equal to `A` but with row `i` multiplied by `λ`.

# Example

```jldoctest
julia> A = reshape(1:12, 3, 4) |> collect
3×4 Matrix{Int64}:
 1  4  7  10
 2  5  8  11
 3  6  9  12

julia> row_mul(A, 2 => 100)
3×4 Matrix{Int64}:
   1    4    7    10
 200  500  800  1100
   3    6    9    12
```
"""
row_mul(A, (i, factor)) = with_copy(row_mul!, A, i => factor)

"""
    row_add!(A, i => λ => j)

Add row `i` multiplied by `λ` to row `j` of matrix `A`.

Returns a copy of the new row `j`.

# Example

```jldoctest
julia> A = [i//j for i in 1:3, j in 1:4]
3×4 Matrix{Rational{Int64}}:
 1//1  1//2  1//3  1//4
 2//1  1//1  2//3  1//2
 3//1  3//2  1//1  3//4

julia> row_add!(A, 1 => -3 => 3)
4-element Vector{Rational{Int64}}:
 0//1
 0//1
 0//1
 0//1

julia> A
3×4 Matrix{Rational{Int64}}:
 1//1  1//2  1//3  1//4
 2//1  1//1  2//3  1//2
 0//1  0//1  0//1  0//1
```
"""
row_add!(A, (i, (λ, j))) = A[j, :] += λ * A[i, :]

"""
    row_add(A, i => λ => j)

Create a matrix equal to `A` but with `λ` times row `i` added to row `j`.

# Example

```jldoctest
julia> A = [i//j for i in 1:3, j in 1:4]
3×4 Matrix{Rational{Int64}}:
 1//1  1//2  1//3  1//4
 2//1  1//1  2//3  1//2
 3//1  3//2  1//1  3//4

julia> row_add(A, 1 => -3 => 3)
3×4 Matrix{Rational{Int64}}:
 1//1  1//2  1//3  1//4
 2//1  1//1  2//3  1//2
 0//1  0//1  0//1  0//1
```
"""
row_add(A, (i, (λ, j))) = with_copy(row_add!, A, i => λ => j)

"""
    ref_pass!(A)

Perform one pass of the row-echelon-form algorithm.

Returns the column index of the pivot for this pass, or `nothing` if `A` is all
zeros.
"""
function ref_pass!(A)
  # Find first non-zero column
  col = findfirst(!iszero, collect(eachcol(A)))
  isnothing(col) && return nothing

  # Move pivot to first row if not there already
  if A[1, col] == 0
    row = findfirst(!iszero, @view A[:, col])
    row_swap!(A, 1, row)
  end

  # Use row operations to bring zeros under the pivot
  for row in 2:size(A, 1)
    if A[row, col] != 0
      row_add!(A, 1 => -A[row, col]/A[1, col] => row)
    end
  end

  return col
end

"""
    log(A, txt)

Write matrix `A` with title text `txt` to standard output.
"""
function log(A, txt)
  println(txt)
  println(repeat('-', length(txt)))
  show(stdout, MIME("text/plain"), A)
  println()
  println()
end

"""
    ref!(A; show_steps=false)

Put `A` in row echelon form.

Returns the `(row, column)` indices of the pivots.

# Example

```jldoctest
julia> A = Float64[mod(i+j,3) for i in 1:4, j in 1:6]
4×6 Matrix{Float64}:
 2.0  0.0  1.0  2.0  0.0  1.0
 0.0  1.0  2.0  0.0  1.0  2.0
 1.0  2.0  0.0  1.0  2.0  0.0
 2.0  0.0  1.0  2.0  0.0  1.0

julia> ref!(A)
3-element Vector{Tuple{Int64, Int64}}:
 (1, 1)
 (2, 2)
 (3, 3)

julia> A
4×6 Matrix{Float64}:
 2.0  0.0   1.0  2.0  0.0   1.0
 0.0  1.0   2.0  0.0  1.0   2.0
 0.0  0.0  -4.5  0.0  0.0  -4.5
 0.0  0.0   0.0  0.0  0.0   0.0
```
"""
function ref!(A; show_steps=false)
  show_steps && log(A, "Start")
  pivots = Tuple{Int,Int}[]

  # Iterate on the rows, each time doing one pass and "hiding" another row.
  for startrow = 1:size(A, 1)
    col = ref_pass!(@view A[startrow:end, :])

    # No pivot found -> nothing to do
    col == nothing && break

    # Store the position of the pivot
    push!(pivots, (startrow, col))
    show_steps && log(A, "Pivot $(pivots[end])")
  end
  return pivots
end

"""
    ref(A; show_steps=false)

Compute a row echelon form of `A`.

Returns the `(row, column)` indices of the pivots.

# Example

```jldoctest
julia> A = Float64[mod(i+j,3) for i in 1:4, j in 1:6]
4×6 Matrix{Float64}:
 2.0  0.0  1.0  2.0  0.0  1.0
 0.0  1.0  2.0  0.0  1.0  2.0
 1.0  2.0  0.0  1.0  2.0  0.0
 2.0  0.0  1.0  2.0  0.0  1.0

julia> ref(A)
4×6 Matrix{Float64}:
 2.0  0.0   1.0  2.0  0.0   1.0
 0.0  1.0   2.0  0.0  1.0   2.0
 0.0  0.0  -4.5  0.0  0.0  -4.5
 0.0  0.0   0.0  0.0  0.0   0.0
```
"""
ref(A; show_steps=false) = with_copy(ref!, A; show_steps)

"""
    rref!(A; show_steps)

Put `A` in the reduced row echelon form.

Returns the `(row, column)` indices of the pivots.

# Example

```jldoctest
julia> A = Float64[mod(i+j,3) for i in 1:4, j in 1:6]
4×6 Matrix{Float64}:
 2.0  0.0  1.0  2.0  0.0  1.0
 0.0  1.0  2.0  0.0  1.0  2.0
 1.0  2.0  0.0  1.0  2.0  0.0
 2.0  0.0  1.0  2.0  0.0  1.0

julia> rref!(A)
3-element Vector{Tuple{Int64, Int64}}:
 (1, 1)
 (2, 2)
 (3, 3)

julia> A
4×6 Matrix{Float64}:
  1.0   0.0  0.0   1.0   0.0  0.0
  0.0   1.0  0.0   0.0   1.0  0.0
 -0.0  -0.0  1.0  -0.0  -0.0  1.0
  0.0   0.0  0.0   0.0   0.0  0.0
```
"""
function rref!(A; show_steps=false)
  # First calculate non-reduced form
  pivots = ref!(A; show_steps)

  # Backward phase: iterate from last to first pivot
  for (row, col) in reverse(pivots)
    # Multiply row to have pivot = 1
    pivot = A[row, col]
    if pivot != 1
        row_mul!(A, row => inv(pivot))
        show_steps && log(A, "Reduce $((row,col)) $pivot -> 1")
    end

    # Ensure all zeros above the pivot
    if !all(iszero, A[1:row-1, col])
        for i in 1:row-1
            row_add!(A, row => -A[i, col] => i)
        end
        show_steps && log(A, "Reduce above $((row,col))")
    end
  end
  return pivots
end

"""
    rref(A; show_steps)

Compute the reduced row echelon form of `A`.

# Example

```jldoctest
julia> A = Float64[mod(i+j,3) for i in 1:4, j in 1:6]
4×6 Matrix{Float64}:
 2.0  0.0  1.0  2.0  0.0  1.0
 0.0  1.0  2.0  0.0  1.0  2.0
 1.0  2.0  0.0  1.0  2.0  0.0
 2.0  0.0  1.0  2.0  0.0  1.0

julia> rref(A)
4×6 Matrix{Float64}:
  1.0   0.0  0.0   1.0   0.0  0.0
  0.0   1.0  0.0   0.0   1.0  0.0
 -0.0  -0.0  1.0  -0.0  -0.0  1.0
  0.0   0.0  0.0   0.0   0.0  0.0
```
"""
rref(A; show_steps=false) = with_copy(rref!, A; show_steps)


module day3

const ⨸ = mod1
readInput() = reduce(hcat, map.(x -> x == '.' ? 0 : 1, collect.(readlines(joinpath(@__DIR__, "input.txt")))))

function part1(input, slope = (3, 1))
    width, height = size(input)
    nbTrees = 0
    pos = (1, 1)
    while pos[2] <= height
        nbTrees += input[pos...]
        pos = pos .+ slope
        pos = (pos[1] ⨸ width, pos[2])
    end
    nbTrees
end

part2(input) = reduce(*, (part1(input, slope) for slope in ((1,1), (3,1), (5,1), (7,1), (1,2))))

end


function update_hangartner_temp_vars!(u::Matrix{Int}, X::Matrix{Int}, t::Int)
    d = size(X, 2)

    for j in 1:d
        u[X[t, j], j] += 1
    end
end

function hangartner_inner(Y::AbstractMatrix, m::Int)
    ## setup temp vars
    n, d = size(Y)

    # Count for each category in each chain
    u = zeros(Int, m, d)
    v = zeros(Int, m, d)

    for t in 1:n
        # fill out temp vars
        update_hangartner_temp_vars!(u, Y, t)
    end
    phia, chi_stat, m_tot = weiss_sub(u, v, n)

    return (n * sum(chi_stat), m_tot)
end

"""
    weiss(X::AbstractMatrix)

Assess the convergence of the MCMC chains with the Weiss procedure.

It computes ``\\frac{X^2}{c}`` and evaluates a p-value from the ``\\chi^2`` distribution with ``(|R| − 1)(s − 1)`` degrees of freedom.
"""
function weiss(X::AbstractMatrix)
    ## number of iterations, number of chains
    n, d = size(X)

    ## mapping of values to integers
    v_dict = Dict{eltype(X),Int}()

    ## max unique categories
    mc = map(c -> length(unique(X[:, c])), 1:d)
    m = length(unique(X))

    ## counter of number of unique values in each chain
    r0 = 0

    ## Count for each category in each chain
    u = zeros(Int, m, d)

    ## Number of times a category did not transition in each chain
    v = zeros(Int, m, d)

    for t in 1:n
        for c in 1:d
            if !(X[t, c] in keys(v_dict))
                r0 += 1
                v_dict[X[t, c]] = r0
            end
            idx1 = v_dict[X[t, c]]
            u[idx1, c] += 1

            if t > 1
                if X[t - 1, c] == X[t, c]
                    v[idx1, c] += 1
                end
            end
        end
    end
    phia, chi_stat, m_tot = weiss_sub(u, v, n)
    ca = (1 + phia) / (1 - phia)
    stat = (n / ca) * sum(chi_stat)
    pval = NaN
    if ((m_tot - 1) * (d - 1)) >= 1
        pval = 1 - Distributions.cdf(Distributions.Chisq((m_tot - 1) * (d - 1)), stat)
    end

    return (stat, m_tot, pval, ca)
end

function weiss_sub(u::Matrix{Int}, v::Matrix{Int}, t::Int)
    m, d = size(u)
    nt = 0.0
    dt = 0.0
    m_tot = 0

    mp = zeros(Float64, m, d)
    ma = zeros(Float64, m)
    phia = 0.0
    ca = 0.0
    df = 0.0

    chi_stat = zeros(Float64, d)

    for j in 1:m
        p1 = 0.0
        p2 = 0.0
        for l in 1:d
            #aggregate
            p1 += v[j, l] / (d * (t - 1))
            p2 += u[j, l] / (d * t)

            #per chain
            mp[j, l] = u[j, l] / t
            ma[j] += u[j, l] / (d * t)
        end
        nt += p1
        dt += p2^2

        if ma[j] > 0
            m_tot += 1
            for l in 1:d
                chi_stat[l] += (mp[j, l] - ma[j])^2 / ma[j]
            end
        end
    end
    phia = 1.0 + (1.0 / t) - ((1 - nt) / (1 - dt))
    phia = min(max(phia, 0.0), 1.0 - eps())
    return (phia, chi_stat, m_tot)
end

function update_billingsley_temp_vars!(f::Array{Int,3}, X::Matrix{Int}, t::Int)
    d = size(X, 2)
    for j in 1:d
        if t > 1
            f[X[t - 1, j], X[t, j], j] += 1
        end
    end
end

function billingsley_sub(f::Array{Int,3})
    df = 0.0
    stat = 0.0

    m, d = size(f)[2:3]

    # marginal transitions, i.e.
    # number of transitions from each category
    mf = mapslices(sum, f; dims=[2])

    # For each category, number of chains for which
    # that category was present
    A = vec(mapslices((x) -> sum(x .> 0), mf; dims=[3]))

    # For each category, number of categories it
    # transitioned to
    B = vec(mapslices((x) -> sum(x .> 0), mapslices(sum, f; dims=[3]); dims=[2]))

    # transition probabilities in each chain
    P = f ./ mf

    # transition probabilities
    mP = (mapslices(sum, f; dims=[3]) ./ mapslices(sum, mf; dims=[3]))
    mP = reshape(mP, size(mP)[1:2])

    idx = findall((A .* B) .> 0)
    for j in idx
        #df for billingsley
        df += (A[j] - 1) * (B[j] - 1)

        #billingsley
        for k in idx
            if (mP[j, k] > 0.0)
                for l in 1:d
                    if mf[j, 1, l] > 0 && isfinite(P[j, k, l])
                        stat += mf[j, 1, l] * (P[j, k, l] - mP[j, k])^2 / mP[j, k]
                    end
                end
            end
        end
    end
    return (stat, df, mP)
end

function bd_inner(Y::AbstractMatrix, m::Int)
    num_iters, num_chains = size(Y)
    # Transition matrix for each chain
    f = zeros(Int, m, m, num_chains)

    for t in 1:num_iters
        # fill out temp vars
        update_billingsley_temp_vars!(f, Y, t)
    end
    return billingsley_sub(f)
end

function simulate_NDARMA(
    N::Int, p::Int, q::Int, prob::Vector{Float64}, phi::Vector{Float64}
)
    sampler1 = Distributions.sampler(Distributions.Multinomial(1, phi))
    sampler2 = Distributions.sampler(Distributions.Categorical(prob))

    alphabeta = Vector{Int}(undef, length(phi))
    alphabeta_p = @view(alphabeta[1:p])
    alphabeta_q = @view(alphabeta[(end - q):end])
    eps = Vector{Int}(undef, q + 1)
    X = Vector{Int}(undef, N)
    Random.rand!(sampler2, @view(X[1:p]))
    for t in (p + 1):N
        Random.rand!(sampler1, alphabeta)
        Random.rand!(sampler2, eps)
        X[t] =
            LinearAlgebra.dot(alphabeta_p, @view(X[(t - p):(t - 1)])) +
            LinearAlgebra.dot(alphabeta_q, eps)
    end

    return X
end

function simulate_MC(N::Int, P::Matrix{Float64})
    X = zeros(Int, N)
    n, m = size(P)
    X[1] = StatsBase.sample(1:n)
    for i in 2:N
        X[i] = StatsBase.wsample(1:n, vec(P[X[i - 1], :]))
    end
    return X
end

function diag_all(
    X::AbstractMatrix, method::Symbol, nsim::Int, start_iter::Int, step_size::Int
)

    ## number of iterations, number of chains
    n, d = size(X)

    ## mapping of values to integers
    v_dict = Dict{eltype(X),Int}()

    ## max unique categories
    mc = map(c -> length(unique(X[:, c])), 1:d)
    m = length(unique(X))

    ## counter of number of unique values in each chain
    r0 = 0

    ## Count for each category in each chain
    u = zeros(Int, m, d)

    ## Number of times a category did not transition in each chain
    v = zeros(Int, m, d)

    ## transition matrix for each chain
    f = zeros(Int, m, m, d)

    length_result = length(start_iter:step_size:n)
    result = (
        stat=Vector{Float64}(undef, length_result),
        df=Vector{Float64}(undef, length_result),
        pvalue=Vector{Float64}(undef, length_result),
    )
    result_iter = 1
    for t in 1:n
        for c in 1:d
            if !(X[t, c] in keys(v_dict))
                r0 += 1
                v_dict[X[t, c]] = r0
            end
            idx1 = v_dict[X[t, c]]
            u[idx1, c] += 1

            if t > 1
                idx2 = v_dict[X[t - 1, c]]
                f[idx1, idx2, c] += 1

                if X[t - 1, c] == X[t, c]
                    v[idx1, c] += 1
                end
            end
        end

        if ((t >= start_iter) && (((t - start_iter) % step_size) == 0))
            phia, chi_stat, m_tot = weiss_sub(u, v, t)
            hot_stat, df, mP = billingsley_sub(f)

            phat = mapslices(sum, u; dims=[2])[:, 1] / sum(mapslices(sum, u; dims=[2]))
            ca = (1 + phia) / (1 - phia)
            stat = NaN
            pval = NaN
            df0 = NaN

            if method == :hangartner
                stat = t * sum(chi_stat)
                df0 = (m - 1) * (d - 1)
                if m > 1 && !isnan(stat)
                    pval =
                        1 - Distributions.cdf(Distributions.Chisq((m - 1) * (d - 1)), stat)
                end
            elseif method == :weiss
                stat = (t / ca) * sum(chi_stat)
                df0 = (m - 1) * (d - 1)
                pval = NaN
                if m > 1 && !isnan(stat)
                    pval =
                        1 - Distributions.cdf(Distributions.Chisq((m - 1) * (d - 1)), stat)
                end
            elseif method == :DARBOOT
                stat = t * sum(chi_stat)
                bstats = zeros(Float64, nsim)
                for b in 1:nsim
                    Y = reduce(
                        hcat,
                        [simulate_NDARMA(t, 1, 0, phat, [phia, 1 - phia]) for j in 1:d],
                    )
                    s = hangartner_inner(Y, m)[1]
                    bstats[b] = s
                end
                non_nan_bstats = filter(!isnan, bstats)
                df0 = Statistics.mean(non_nan_bstats)
                pval = Statistics.mean(stat <= x for x in non_nan_bstats)
            elseif method == :MCBOOT
                bstats = zeros(Float64, nsim)
                for b in 1:nsim
                    Y = reduce(hcat, [simulate_MC(t, mP) for j in 1:d])
                    s = hangartner_inner(Y, m)[1]
                    bstats[b] = s
                end
                non_nan_bstats = filter(!isnan, bstats)
                df0 = Statistics.mean(non_nan_bstats)
                pval = Statistics.mean(stat <= x for x in non_nan_bstats)
            elseif method == :billingsley
                stat = hot_stat
                df0 = df
                if df > 0 && !isnan(hot_stat)
                    pval = 1 - Distributions.cdf(Distributions.Chisq(df), hot_stat)
                end
            elseif method == :billingsleyBOOT
                stat = hot_stat
                bstats = zeros(Float64, nsim)
                for b in 1:nsim
                    Y = reduce(hcat, [simulate_MC(t, mP) for j in 1:d])
                    (s, sd) = bd_inner(Y, m)[1:2]
                    bstats[b] = s / sd
                end
                non_nan_bstats = filter(!isnan, bstats)
                df0 = Statistics.mean(non_nan_bstats)
                statodf = stat / df
                pval = Statistics.mean(statodf <= x for x in non_nan_bstats)
            else
                error("Unexpected")
            end
            result.stat[result_iter] = stat
            result.df[result_iter] = df0
            result.pvalue[result_iter] = pval
            result_iter += 1
        end
    end
    return result
end

function discretediag_sub(
    c::AbstractArray{<:Real,3},
    frac::Real,
    method::Symbol,
    nsim::Int,
    start_iter::Int,
    step_size::Int,
)
    num_iters, num_vars, num_chains = size(c)

    ## Between-chain diagnostic
    length_results = length(start_iter:step_size:num_iters)
    plot_vals_stat = Matrix{Float64}(undef, length_results, num_vars)
    plot_vals_pval = Matrix{Float64}(undef, length_results, num_vars)
    between_chain_vals = (
        stat=Vector{Float64}(undef, num_vars),
        df=Vector{Float64}(undef, num_vars),
        pvalue=Vector{Float64}(undef, num_vars),
    )
    for j in 1:num_vars
        X = convert(AbstractMatrix{Int}, c[:, j, :])
        result = diag_all(X, method, nsim, start_iter, step_size)

        plot_vals_stat[:, j] .= result.stat ./ result.df
        plot_vals_pval[:, j] .= result.pvalue

        between_chain_vals.stat[j] = result.stat[end]
        between_chain_vals.df[j] = result.df[end]
        between_chain_vals.pvalue[j] = result.pvalue[end]
    end

    ## Within-chain diagnostic
    within_chain_vals = (
        stat=Matrix{Float64}(undef, num_vars, num_chains),
        df=Matrix{Float64}(undef, num_vars, num_chains),
        pvalue=Matrix{Float64}(undef, num_vars, num_chains),
    )
    for k in 1:num_chains
        for j in 1:num_vars
            x = convert(AbstractVector{Int}, c[:, j, k])

            idx1 = 1:round(Int, frac * num_iters)
            idx2 = round(Int, num_iters - frac * num_iters + 1):num_iters
            x1 = x[idx1]
            x2 = x[idx2]
            n_min = min(length(x1), length(x2))
            Y = [x1[1:n_min] x2[(end - n_min + 1):end]]

            result = diag_all(Y, method, nsim, n_min, step_size)
            within_chain_vals.stat[j, k] = result.stat[end]
            within_chain_vals.df[j, k] = result.df[end]
            within_chain_vals.pvalue[j, k] = result.pvalue[end]
        end
    end

    return between_chain_vals, within_chain_vals, plot_vals_stat, plot_vals_pval
end

"""
    discretediag(chains::AbstractArray{<:Real,3}; frac=0.3, method=:weiss, nsim=1_000)

Compute discrete diagnostic where `method` can be one of `:weiss`, `:hangartner`,
`:DARBOOT`, `:MCBOOT`, `:billinsgley`, and `:billingsleyBOOT`.
"""
function discretediag(
    chains::AbstractArray{<:Real,3}; frac::Real=0.3, method::Symbol=:weiss, nsim::Int=1000
)
    valid_methods = (:weiss, :hangartner, :DARBOOT, :MCBOOT, :billingsley, :billingsleyBOOT)
    method in valid_methods || throw(
        ArgumentError("`method` must be one of :" * join(valid_methods, ", :", " and :")),
    )
    0 < frac < 1 || throw(ArgumentError("`frac` must be in (0,1)"))

    num_iters = size(chains, 1)
    between_chain_vals, within_chain_vals, _, _ = discretediag_sub(
        chains, frac, method, nsim, num_iters, num_iters
    )

    return between_chain_vals, within_chain_vals
end


using OrdinaryDiffEq, DiffEqProblemLibrary, Base.Test
prob = deepcopy(prob_ode_2Dlinear)
prob.tspan = (1.0,0.0)

sol = solve(prob,DP5(),dt=-1/4)

if !is_windows()
  sol2 = solve(prob,dopri5(),dt=-1/4)
  @test sol.t ≈ sol2.t
end

sol = solve(prob,DP5(),dt=-1/4,tstops=[0.5])
@test sol.t == [1.0,.75,.5,0]

sol = solve(prob,RK4(),dt=-1/4)
@test sol.t == [1.00,.75,.5,.25,0]

sol = solve(prob,RK4(),dt=-1/4,tstops=[0.5,0.33])
@test sol.t ≈ [1.00,.75,.5,.33,0.08,0]


# ----------------------------------------------------------------------------------- #
# Copyright (c) 2019 Varnerlab
# Robert Frederick School of Chemical and Biomolecular Engineering
# Cornell University, Ithaca NY 14850
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# ----------------------------------------------------------------------------------- #

# ----------------------------------------------------------------------------------- #
# estimate_steady_state: estimates the steady-state vector for the dynamic system
#
# Type: GRN-JULIA
# Version: 1.0
#
# Input arguments:
# data_dictionary  - Data dictionary instance (holds model parameters)
#
# Return arguments:
# XSS - steady state vector
# ----------------------------------------------------------------------------------- #
function estimate_steady_state(data_dictionary)

    # Get the initial conditions -
    initial_condition_array = data_dictionary["initial_condition_array"];

    # calculate the steady-state soln -
    steady_state_prob = SteadyStateProblem(Balances, initial_condition_array, data_dictionary)
    steady_state_soln  = solve(steady_state_prob, DynamicSS(AutoTsit5(Rosenbrock23(autodiff=false));abstol=1e-8,reltol=1e-6,tspan=Inf))

    # return -
    return abs.(steady_state_soln.u)
end

# ----------------------------------------------------------------------------------- #
# AdjBalances: Evaluates adjoint model equations given time, state, parameter_index,
# and the data_dictionary.
#
# Type: GRN-JULIA
# Version: 1.0
#
# Input arguments:
# t  - current time
# dx - derivative vector
# x  - state array
# data_dictionary  - Data dictionary instance (holds model parameters)
#
# Return arguments:
# N/A - derivative array at current time step is filled in the function
# ----------------------------------------------------------------------------------- #
function AdjBalances(dx,x,data_dictionary,t)

    # look up the number of states -
    number_of_states = data_dictionary["number_of_states"]

    # grab the parameter index -
    parameter_index = data_dictionary["parameter_index"]

    # partition that state -
    state_array = x[1:number_of_states]
    sensitivity_array = x[(number_of_states+1):end]

    # call the balances -
    dxdt_array = calculate_balances(t,state_array,data_dictionary)

    # Calculate the sensitivity states -
    local_data_dictionary = deepcopy(data_dictionary)
    JM = calculate_jacobian(t,state_array,local_data_dictionary)
    BM = calculate_bmatrix(t,state_array,local_data_dictionary)

    # calulate the sensitivity state -
    dsdt_array = JM*sensitivity_array+BM[:,parameter_index]
    r_array = [dxdt_array ; dsdt_array]

    # package -
    number_of_total_states = length(r_array)
    for index = 1:number_of_total_states
        dx[index] = r_array[index]
    end
end

# ----------------------------------------------------------------------------------- #
# calculate_balances: Evaluates model equations given time, state and the data_dictionary.
# Type: GRN-JULIA
# Version: 1.0
#
# Input arguments:
# t  - current time
# x  - state array
# data_dictionary  - Data dictionary instance (holds model parameters)
#
# Return arguments:
# dxdt - derivative array at current time step
# ----------------------------------------------------------------------------------- #
function calculate_balances(t,x,data_dictionary)

    # get the structure arrays from the data_dictionary -
    AM = data_dictionary["dilution_degradation_matrix"]
    SM = data_dictionary["stoichiometric_matrix"]

    # what is my system size?
    number_of_states = data_dictionary["number_of_states"]

    # calculate the kinetics array -
    kinetics_array = calculate_txtl_kinetics_array(t,x,data_dictionary)

    # calculate the control array -
    transcription_control_array = calculate_transcription_control_array(t,x,data_dictionary)
    translation_control_array = calculate_translation_control_array(t,x,data_dictionary)

    # control_array is the transcription and translation control arrays -
    control_array = [transcription_control_array ; translation_control_array]

    # modfiy the kinetics -
    rV = kinetics_array.*control_array

    # remove last n states -
    x_main = x[1:12]

    # compute the dxdt vector -
    dxdt = AM*x_main+SM*rV

    # extra states -
    dxdt[13] = -(log(2)/4.0)*x[13]

    # return -
    return dxdt
end

# ----------------------------------------------------------------------------------- #
# Balances: Evaluates model equations given time, state and the data_dictionary.
# Type: GRN-JULIA
# Version: 1.0
#
# Input arguments:
# dx - derivative array
# t  - current time
# x  - state array
# data_dictionary  - Data dictionary instance (holds model parameters)
#
# Return arguments:
# N/A - dx is filled in the function and returned to the caller
# ----------------------------------------------------------------------------------- #
function Balances(dx,x,data_dictionary,t)

    # get the structure arrays from the data_dictionary -
    AM = data_dictionary["dilution_degradation_matrix"]
    SM = data_dictionary["stoichiometric_matrix"]
    half_life_translation = data_dictionary["half_life_translation_capacity"]

    # what is my system size?
    number_of_states = data_dictionary["number_of_states"]

    # calculate the kinetics array -
    kinetics_array = calculate_txtl_kinetics_array(t,x,data_dictionary)

    # calculate the control array -
    transcription_control_array = calculate_transcription_control_array(t,x,data_dictionary)
    translation_control_array = calculate_translation_control_array(t,x,data_dictionary)

    # control_array is the transcription and translation control arrays -
    control_array = [transcription_control_array ; translation_control_array]

    # modfiy the kinetics -
    rV = kinetics_array.*control_array

    # remove last n states -
    x_main = x[1:12]

    # compute the dxdt vector -
    dxdt = AM*x_main+SM*rV

    # add extra term for ssrA tag -
    #N = 1.26
    #dxdt[9] = dxdt[9] - (log(2)/12.0)*x[9]^N
    #dxdt[10] = dxdt[10] - (log(2)/8.0)*x[10]^N

    # package -
    for index = 1:number_of_states
        dx[index] = dxdt[index]
    end

    # extra states -
    # 8.0 w/o extra term
    # 6.0 => T5
    #dx[13] = -(log(2)/8.0)*x[13]
    dx[13] = -(log(2)/half_life_translation)*x[13]
end