#=

Testing of syntheticimages.jl
Hard to test these functions other than visually.  This script simply
runs then all to make sure that they at least run

Set 'disp' = true to display the images
=#

disp = false

using ImagePhaseCongruency, Images, TestImages

if disp
    using PyPlot
end

println("Testing syntheticimages...")

function mypause()
    println("Hit return to continue ")
    a = readline()
    return nothing
end

disp ? PyPlot.set_cmap(PyPlot.ColorMap("gray")) : nothing

#lena = Float64.(testimage("lena_gray"))
# We need the test image to be square for the phase amplitude tests.
testimg = Float64.(Gray.(testimage("lighthouse")))[1:512, 1:512]

sze = 512
img = step2line(sze; nscales=50, ampexponent=-1, ncycles=1.5, phasecycles=0.25)
if disp
    imshow(img); title("step2line() test image")
    mypause()
end

img = circsine(sze; wavelength = 40, nscales = 50, ampexponent = -1,
               offset = 0, p = 2, trim = false)
if disp
    imshow(img); title("circsign() test image")
    mypause()
end

img = starsine(sze; ncycles=10, nscales=50, ampexponent=-1, offset=0)
if disp
    imshow(img); title("starsign() test image")
    mypause()
end

img = noiseonf(sze, 1.5)
if disp
    imshow(img); title("Noise with amplitude spectrum f^{-1.5}")
    mypause()
end

newimg = nophase(testimg)
if disp
    imshow(newimg); title("Testimg with randomized phase")
    mypause()
end

newimg = quantizephase(testimg, 4)
if disp
    imshow(newimg); title("Testimg with phase quantized to 4 levels")
    mypause()
end

(newimg1, newimg2) = swapphase(testimg, testimg')
if disp
    imshow(newimg1); title("Testimg with phase of Testimg transposed")
    mypause()
    imshow(newimg2); title("Testimg transposed with phase of Testimg")
    mypause()
end

nothing


export ImmutablePolynomial

"""
    ImmutablePolynomial{T<:Number}(coeffs::AbstractVector{T}, var=:x)

Construct an immutable (static) polynomial from its coefficients `a`,
lowest order first, optionally in terms of the given variable `x`
where `x` can be a character, symbol, or string.

If ``p = a_n x^n + \\ldots + a_2 x^2 + a_1 x + a_0``, we construct
this through `ImmutablePolynomial((a_0, a_1, ..., a_n))` (assuming
`a_n ≠ 0`). As well, a vector or number can be used for construction.

The usual arithmetic operators are overloaded to work with polynomials
as well as with combinations of polynomials and scalars. However,
operations involving two polynomials of different variables causes an
error, though for `+` and `*` operations, constant polynomials are
treated as having no variable.

As the coefficient size is a compile-time constant, immutable
polynomials can take advantage of faster polynomial evaluation
provided by `evalpoly` from Julia 1.4.

    # Examples

```jldoctest
julia> using  Polynomials

julia> ImmutablePolynomial((1, 0, 3, 4))
ImmutablePolynomial(1 + 3*x^2 + 4*x^3)

julia> ImmutablePolynomial((1, 2, 3), :s)
ImmutablePolynomial(1 + 2*s + 3*s^2)

julia> one(ImmutablePolynomial)
ImmutablePolynomial(1.0)
```

!!! note
    This was modeled after https://github.com/tkoolen/StaticUnivariatePolynomials.jl by @tkoolen.

"""
struct ImmutablePolynomial{T <: Number,  N} <: StandardBasisPolynomial{T}
    coeffs::NTuple{N, T}
    var::Symbol
    function ImmutablePolynomial{T,N}(coeffs::NTuple{N,T}, var::SymbolLike=:x) where {T <: Number,N}
        N == 0 && return new{T,0}(coeffs, Symbol(var))
        iszero(coeffs[end]) &&  throw(ArgumentError("Leading  term must  be  non-zero"))
        new{T,N}(coeffs, Symbol(var))
    end
    function ImmutablePolynomial{T,N}(coeffs::AbstractVector{S}, var::SymbolLike=:x) where {T <: Number, N, S}
        M = findlast(!iszero, coeffs)
        M == N || throw(ArgumentError("Leading  term must  be  non-zero; length of coefficients must be N"))
        cs = NTuple{N,T}(T(c) for c in coeffs[1:N])
        new{T,N}(cs, var)
    end
    function ImmutablePolynomial{T}(coeffs::NTuple{M,S}, var::SymbolLike=:x) where {T, S<: Number, M}
        N = findlast(!iszero, coeffs)
        if N == nothing
            return zero(ImmutablePolynomial{T}, var)
        else
            cs = NTuple{N,T}(coeffs[i] for  i in  1:N)
        end
        new{T,N}(cs, var)
    end
    function ImmutablePolynomial{T}(coeffs::Vector{M}, var::SymbolLike=:x) where {T, M}
        N = findlast(!iszero, coeffs)
        if N == nothing
            return zero(ImmutablePolynomial{T}, var)
        else
            cs = NTuple{N,T}(coeffs[i] for  i in  1:N)
        end
        new{T,N}(cs, var)
    end

end

@register ImmutablePolynomial

function ImmutablePolynomial{T,N}(coeffs::Tuple, var)  where {T,N}
    ImmutablePolynomial{T,N}(NTuple{N,T}(c for c in coeffs), var)
end

function ImmutablePolynomial(coeffs::NTuple{M,T}, var::SymbolLike=:x) where {M, T}
    N = findlast(!iszero, coeffs)
    if N == nothing
        return zero(ImmutablePolynomial, var) # no  type  passed in NTuple{M,T}
    else
        cs = NTuple{N,T}(T(c) for  c in  coeffs[1:N])
        end
    ImmutablePolynomial{T,N}(cs, var)
end

# Convenience; pass tuple to Polynomial
# Not documented, not sure this is a good idea as P(...)::P is not true...
Polynomial(coeffs::NTuple{N,T}, var::SymbolLike = :x) where{N,T} =
    ImmutablePolynomial(coeffs, var)
function Polynomial{T}(coeffs::NTuple{N,S}, var::SymbolLike = :x) where{N,T,S}
    ImmutablePolynomial{N,T}(T.(coeffs), var)
end

##
## ----
##
# overrides from common.jl due to coeffs possibly being padded, coeffs being non mutable, ...

Base.collect(p::P) where {P <: ImmutablePolynomial} = [pᵢ for pᵢ ∈ p]

Base.copy(p::P) where {P <: ImmutablePolynomial} = P(coeffs(p), p.var)

# catch q == 0 case
LinearAlgebra.norm(q::ImmutablePolynomial{T}, p::Real = 2) where {T} = degree(q) == -1 ? zero(T) : norm(coeffs(q), p)

#  zero, one, variable
function Base.zero(P::Type{<:ImmutablePolynomial},var::SymbolLike=:x)
    R = eltype(P)
    ImmutablePolynomial{R,0}(NTuple{0,R}(),var)
end

function  Base.one(P::Type{<:ImmutablePolynomial},var::SymbolLike=:x)
    R = eltype(P)
    ImmutablePolynomial{R,1}(NTuple{1,R}(1),var)
end
function variable(P::Type{<:ImmutablePolynomial},var::SymbolLike=:x)
    R  = eltype(P)
    ImmutablePolynomial{R,2}(NTuple{2,R}((0,1)),var)
end

# degree, isconstant
degree(p::ImmutablePolynomial{T,N}) where {T,N} = N - 1
isconstant(p::ImmutablePolynomial{T,N}) where {T,N}  = N <= 1

function Base.getindex(p::ImmutablePolynomial{T,N}, idx::Int) where {T <: Number,N}
    (idx <  0 || idx > N-1) && return zero(T)
    return p.coeffs[idx + 1]
end

Base.setindex!(p::ImmutablePolynomial, val::Number,  idx::Int) = throw(ArgumentError("Immutable polynomials are immutable"))

for op in [:isequal, :(==)]
    @eval function Base.$op(p1::ImmutablePolynomial{T,N}, p2::ImmutablePolynomial{S,M}) where {T,N,S,M}
        check_same_variable(p1,p2) || return false
        p1s, p2s = coeffs(p1), coeffs(p2)
        (N == M  && $op(p1s,p2s)) &&  return  true
        n1 = findlast(!iszero, p1s) # now trim out zeros
        n2 = findlast(!iszero, p2s)
        (n1 == nothing && n2 == nothing) && return true
        (n1 == nothing || n2  == nothing) && return false
        $op(p1s[1:n1],p2s[1:n2]) &&  return true
        false
    end
end

##
## Change ≈ to handle tuples for coefficients *and* get handling of Inf correct
## right now
## Inf ≈ Inf # true
## [Inf] ≈ [Inf] # true
## P([Inf]) ≈ P([Inf]) # false
## P([Inf]) ≈ Inf # false
## This fixes the last two cases for P=ImmutablePolynomial, and could replace
## isapprox in common.jl
##
## check Inf values (matching Vector ≈ Vector)
## @test P([Inf]) ≈ Inf
## @test !(P([Inf]) ≈ P([-Inf])) # default compares zero(P) to zero(P)
## @test !(P([1,2,Inf]) ≈ P([1,3,Inf])) # default uses truncate with  Inf which clobbers numbers
function Base.isapprox(p1::ImmutablePolynomial{T},
    p2::ImmutablePolynomial{S};
    rtol::Real = (Base.rtoldefault(T, S, 0)),
                       atol::Real = 0,) where {T,S}

    p1, p2 = promote(p1, p2)
    check_same_variable(p1, p2)  || error("p1 and p2 must have same var")

    # copy over from abstractarray.jl
    Δ  = norm(p1-p2)
    if isfinite(Δ)
        return Δ <= max(atol, rtol*max(norm(p1), norm(p2)))
    else
        for i in 0:max(degree(p1), degree(p2))
            isapprox(p1[i], p2[i]; rtol=rtol, atol=atol) || return false
        end
        return true
    end
end

function Base.isapprox(p1::P,
                       n::S;
                       rtol::Real = (Base.rtoldefault(T, S, 0)),
                       atol::Real = 0,) where {T,S, P<:ImmutablePolynomial{T}}
    return isapprox(p1, ⟒(P){T}(n,p1.var))
end

function Base.chop(p::ImmutablePolynomial{T,N};
              rtol::Real = Base.rtoldefault(real(T)),
              atol::Real = 0)  where {T,N}
    cs = coeffs(p)
    for i in N:-1:1
        if !isapprox(cs[i], zero(T), rtol=rtol, atol=atol)
            return ImmutablePolynomial{T,i}(cs[1:i], p.var)
        end
    end
    zero(ImmutablePolynomial{T})
end

function Base.truncate(p::ImmutablePolynomial{T,N};
                       rtol::Real = Base.rtoldefault(real(T)),
                       atol::Real = 0)  where {T,N}
    q = chop(p, rtol=rtol, atol=atol)
    iszero(q) && return  q
    cs = coeffs(q)
    thresh = maximum(abs,cs) * rtol + atol
    ImmutablePolynomial(map(c->abs(c) <= thresh ? zero(T) : c, coeffs(q)), p.var)
end

# no in-place chop! and truncate!
chop!(p::ImmutablePolynomial; kwargs...) =  chop(p; kwargs...)
truncate!(p::ImmutablePolynomial; kwargs...) =  truncate(p; kwargs...)

LinearAlgebra.conj(p::P) where {P <: ImmutablePolynomial} = P(conj([aᵢ for aᵢ in coeffs(p)]))

##
## --------------------
##

(p::ImmutablePolynomial{T,N})(x::S) where {T,N,S} = evalpoly(x, coeffs(p))

function Base.:+(p1::ImmutablePolynomial{T,N}, p2::ImmutablePolynomial{S,M}) where {T,N,S,M}

    R = promote_type(S,T)
    iszero(N) && return ImmutablePolynomial{R}(coeffs(p2), p2.var)
    iszero(M) && return ImmutablePolynomial{R}(coeffs(p1), p1.var)

    isconstant(p1) && p1.var != p2.var && return p2 + p1[0]*one(ImmutablePolynomial{T},p2.var)
    isconstant(p2) && p1.var != p2.var && return p1 + p2[0]*one(ImmutablePolynomial{S},p1.var)

    p1.var != p2.var && error("Polynomials must have same variable")

    if  N == M
        cs = coeffs(p1) .+ coeffs(p2)
        return ImmutablePolynomial(cs, p1.var) # N  unknown, as leading terms can cancel
    elseif N < M
        ⊕(p2,p1)
    else
        ⊕(p1,p2)
    end

end

# assume N > M.
# As N ≠ M, we are assured of size of output (max(N,M)), so we generate the function
@generated function ⊕(p1::ImmutablePolynomial{T,N}, p2::ImmutablePolynomial{S,M}) where {T,N,S,M}
    R = promote_type(T,S)

    exprs = Any[nothing for i = 1:N]
    for i in  1:M
        exprs[i] = :(p1.coeffs[$i] + p2.coeffs[$i])
    end
    for i in M+1:N
        exprs[i] =:(p1.coeffs[$i])
    end

    return quote
        Base.@_inline_meta
        ImmutablePolynomial{$(R),$(N)}(tuple($(exprs...)), p1.var)
    end

end

function Base.:*(p1::ImmutablePolynomial, p2::ImmutablePolynomial)
    isconstant(p1) && return p2 * p1[0]
    isconstant(p2) && return p1 * p2[0]
    p1.var != p2.var && error("Polynomials must have same variable")
    p1 ⊗ p2
end

## Static size of product makes generated functions  a good choice
## from https://github.com/tkoolen/StaticUnivariatePolynomials.jl/blob/master/src/monomial_basis.jl
@generated function ⊗(p1::ImmutablePolynomial{T,N}, p2::ImmutablePolynomial{S,M}) where {T,N,S,M}
    P = M + N - 1
    R = promote_type(T,S)
    exprs = Any[nothing for i = 1 : P]
    for i in 1 : N
        for j in 1 : M
            k = i + j - 1
            if exprs[k] === nothing
                exprs[k] = :(p1.coeffs[$i] * p2.coeffs[$j])
            else
                exprs[k] = :(muladd(p1.coeffs[$i], p2.coeffs[$j], $(exprs[k])))
            end
        end
    end

    return quote
        Base.@_inline_meta
        ImmutablePolynomial{$(R),$(max(0,P))}(tuple($(exprs...)), p1.var)
    end

end

# scalar ops
function Base.:+(p::ImmutablePolynomial{T,N}, c::S) where {T, N, S<:Number}
    R = promote_type(T,S)
    iszero(c) && return ImmutablePolynomial{R,N}(R.(p.coeffs), p.var)
    N == 0 && return ImmutablePolynomial{R,1}((R(c),), p.var)
    N == 1 && return ImmutablePolynomial(R[p[0]+c])

    return p + ImmutablePolynomial{R,1}(R[c],p.var)

    cs = NTuple{N,R}(iszero(i) ? p[i]+c : p[i] for i in 0:N-1)
    return ImmutablePolynomial{R,N}(cs, p.var)
end

function Base.:*(p::ImmutablePolynomial{T,N}, c::S) where {T, N, S <: Number}
    R = promote_type(T,S)
    iszero(c)  && return zero(ImmutablePolynomial{R})
    ImmutablePolynomial{R,N}(p.coeffs .* c, p.var)
end

function Base.:/(p::ImmutablePolynomial{T,N}, c::S) where {T,N,S <: Number}
    R = eltype(one(T)/one(S))
    isinf(c)  && return zero(ImmutablePolynomial{R})
    ImmutablePolynomial{R,N}(p.coeffs ./ c, p.var)
end

Base.:-(p::ImmutablePolynomial{T,N}) where {T,N} = ImmutablePolynomial(NTuple{N,T}(-pi for pi in p.coeffs), p.var)

Base.to_power_type(p::ImmutablePolynomial{T,N}) where {T,N} = p


using Test

using MathOptInterface
const MOI = MathOptInterface
const MOIT = MathOptInterface.Test
const MOIU = MathOptInterface.Utilities
const MOIB = MathOptInterface.Bridges

include("../utilities.jl")

mock = MOIU.MockOptimizer(MOIU.UniversalFallback(MOIU.Model{Float64}()))
config = MOIT.TestConfig()

@testset "NonposToNonneg" begin
    bridged_mock = MOIB.Variable.NonposToNonneg{Float64}(mock)

    @testset "lin2v" begin
        mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [-4, 3, 16, 0],
            (MOI.VectorAffineFunction{Float64}, MOI.Zeros) => [[7, 2, -4]])
        MOIT.lin2vtest(bridged_mock, config)

        @test MOI.get(mock, MOI.NumberOfVariables()) == 4
        @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 4
        vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices())
        y = vis[4]
        @test y.value == -1

        @test MOI.supports(bridged_mock, MOI.VariablePrimalStart(), MOI.VariableIndex)
        @test MOI.supports(bridged_mock, MOI.VariablePrimalStart(), typeof(MOIB.bridge(bridged_mock, y)))
        MOI.set(bridged_mock, MOI.VariablePrimalStart(), y, 1.0)
        x, y_flipped, z, s = MOI.get(mock, MOI.ListOfVariableIndices())
        @test MOI.get(mock, MOI.VariablePrimalStart(), y_flipped) == -1
        @test MOI.get(bridged_mock, MOI.VariablePrimalStart(), y) == 1

        var_names = ["x", "y", "z", "w"]
        MOI.set(mock, MOI.VariableName(), MOI.get(mock, MOI.ListOfVariableIndices()), var_names)
        con_w = MOI.get(mock, MOI.ListOfConstraintIndices{MathOptInterface.VectorOfVariables, MathOptInterface.Zeros}())[1]
        con_yz = MOI.get(mock, MOI.ListOfConstraintIndices{MathOptInterface.VectorOfVariables, MathOptInterface.Nonnegatives}())
        con_ex = MOI.get(mock, MOI.ListOfConstraintIndices{MathOptInterface.VectorAffineFunction{Float64}, MathOptInterface.Zeros}())[1]

        MOI.set(mock, MOI.ConstraintName(), con_w, "cw")
        MOI.set(mock, MOI.ConstraintName(), con_yz[1], "cy")
        MOI.set(mock, MOI.ConstraintName(), con_yz[2], "cz")
        MOI.set(mock, MOI.ConstraintName(), con_ex, "cex")

        MOI.set(bridged_mock, MOI.VariableName(), MOI.get(bridged_mock, MOI.ListOfVariableIndices())[4], "v")
        con_v = MOI.get(bridged_mock, MOI.ListOfConstraintIndices{MathOptInterface.VectorOfVariables, MathOptInterface.Nonpositives}())[1]
        MOI.set(bridged_mock, MOI.ConstraintName(), con_v, "cv")

        @testset "Test Mock model" begin
            s = """
            variables: x, y, z, w
            cw: [w] in MathOptInterface.Zeros(1)
            cy: [y] in MathOptInterface.Nonnegatives(1)
            cz: [z] in MathOptInterface.Nonnegatives(1)
            cex: [1*x + -1*w + 4.0, -1*y + 3.0, 1*x + 1*z + -12.0] in MathOptInterface.Zeros(3)
            minobjective: 3*x + -2*y + -4*z
            """
            model = MOIU.Model{Float64}()
            MOIU.loadfromstring!(model, s)
            MOIU.test_models_equal(mock, model, var_names, ["cw", "cy", "cz", "cex"])
        end

        @testset "Test Bridged model" begin
            s = """
            variables: x, z, w, v
            cv: [v] in MathOptInterface.Nonpositives(1)
            cw: [w] in MathOptInterface.Zeros(1)
            cz: [z] in MathOptInterface.Nonnegatives(1)
            cex: [1*x + -1*w + 4.0, 1*v + 3.0, 1*x + 1*z + -12.0] in MathOptInterface.Zeros(3)
            minobjective: 3*x + 2*v + -4*z
            """
            model = MOIU.Model{Float64}()
            MOIU.loadfromstring!(model, s)
            MOIU.test_models_equal(bridged_mock, model, ["x", "z", "w", "v"], ["cv", "cw", "cz", "cex"])
        end
    end

    @testset "lin4" begin
        MOIU.set_mock_optimize!(mock,
            (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
                mock, MOI.INFEASIBLE, MOI.INFEASIBLE_POINT,
                MOI.INFEASIBILITY_CERTIFICATE)
        )
        MOIT.lin4test(bridged_mock, config)

        @test MOI.get(mock, MOI.NumberOfVariables()) == 1
        @test length(MOI.get(mock, MOI.ListOfVariableIndices())) == 1
        @test first(MOI.get(mock, MOI.ListOfVariableIndices())).value ≥ 0
        @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 1
        vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices())
        @test vis == [MOI.VariableIndex(-1)]
        test_delete_bridged_variable(bridged_mock, vis[1], MOI.Nonpositives, 1, (
            (MOI.VectorOfVariables, MOI.Nonnegatives, 0),
        ))
    end

    @testset "Delete in vector" begin
        MOI.empty!(bridged_mock)
        vis, ci = MOI.add_constrained_variables(bridged_mock, MOI.Nonpositives(4))
        test_delete_bridged_variable(bridged_mock, vis[2], MOI.Nonpositives, 4, (
            (MOI.VectorOfVariables, MOI.Nonnegatives, 0),
        ), used_bridges = 0, used_constraints = 0)
    end
end


# ---
# title: 113. Path Sum II
# id: problem113
# author: zhwang
# date: 2022-01-23
# difficulty: Medium
# categories: Tree, Depth-first Search
# link: <https://leetcode.com/problems/path-sum-ii/description/>
# hidden: true
# ---
#
# Given a binary tree and a sum, find all root-to-leaf paths where each path's
# sum equals the given sum.
#
# **Note:**  A leaf is a node with no children.
#
# **Example:**
#
# Given the below binary tree and `sum = 22`,
#
#
#
#           **5**
#          **/ \**
#         **4   8**
#        **/**   / **\**
#       **11**  13  **4**
#      /  **\**    **/** \
#     7    **2**  **5**   1
#
#
# Return:
#
#
#
#     [
#        [5,4,11,2],
#        [5,8,4,5]
#     ]
#
#
#
## @lc code=start
using LeetCode

function path_sum_113(node::Union{Nothing,TreeNode}, target::Int)::Vector{Vector{Int}}
    isnothing(node) && return Vector{Vector{Int}}[]
    isnothing(node.left) &&
        isnothing(node.right) &&
        return node.val != target ? Vector{Vector{Int}}[] : [[target]]
    paths = path_sum_113(node.left, target - node.val)
    paths = vcat(paths, path_sum_113(node.right, target - node.val))
    for path in paths
        pushfirst!(path, node.val)
    end
    return paths
end

## @lc code=end


"""
GeometricProblems.jl is a collection of ODEs and DAEs with interesting geometric structures.
"""
module GeometricProblems

    include("diagnostics.jl")
    include("plot_recipes.jl")

    include("double_pendulum.jl")
    include("duffing_oscillator.jl")
    include("exponential_growth.jl")
    include("harmonic_oscillator.jl")
    include("kubo_oscillator.jl")
    include("lennard_jones_oscillator.jl")
    include("lorenz_attractor.jl")
    include("lotka_volterra_2d.jl")
    include("lotka_volterra_2d_gauge.jl")
    include("lotka_volterra_2d_singular.jl")
    include("lotka_volterra_2d_symmetric.jl")
    include("lotka_volterra_2d_plots.jl")
    include("lotka_volterra_3d.jl")
    include("lotka_volterra_3d_plots.jl")
    include("lotka_volterra_4d.jl")
    include("lotka_volterra_4d_lagrangian.jl")
    include("lotka_volterra_4d_plots.jl")
    include("massless_charged_particle.jl")
    include("massless_charged_particle_plots.jl")
    include("mathews_lakshmanan_oscillator.jl")
    include("morse_oscillator.jl")
    include("pendulum.jl")
    include("point_vortices.jl")
    include("point_vortices_linear.jl")

end


using TimeSeriesCausality
using Test
using Statistics: mean
using Distributions: MvNormal

@time @testset "granger_estimation.jl" begin
    time_steps = 256 * 1024
    segment_length = 1024
    noise_cov = MvNormal([0.25 0.0; 0.0 0.64])  # uncorrelated cov matrix of noise
    noise = rand(noise_cov, time_steps)'

    # initial values
    signal = zeros(time_steps, 2)
    signal[1:3, :] = rand(3, 2) + noise[1:3, :]

    # simulation
    designer = [0.4 -0.6  0.8  0.0  0.0  0.0;
                0.5  0.9  0.0  0.0  0.0  0.7]

    for t in 4:time_steps
        signal[t, :] = designer * reshape(signal[t-3:t-1, :], :, 1) + noise[t, :]
    end

    covariances = TimeSeriesCausality.est_sig_covs(signal, 3)
    ar_factors, p_error = TimeSeriesCausality.mvar_est(covariances)

    begin
        @test all(isapprox.(ar_factors[:, 1], designer[:, 3], atol = 0.01))
        @test all(isapprox.(ar_factors[:, 2], designer[:, 6], atol = 0.01))
        @test all(isapprox.(ar_factors[:, 3], designer[:, 2], atol = 0.01))
        @test all(isapprox.(ar_factors[:, 4], designer[:, 5], atol = 0.01))
        @test all(isapprox.(ar_factors[:, 5], designer[:, 1], atol = 0.01))
        @test all(isapprox.(ar_factors[:, 6], designer[:, 4], atol = 0.01))
    end

    granger_idx, std_error = granger_est(signal, segment_length; order = 3, method = "jackknife")
    @test granger_idx ≈ 0.59 atol = 0.02
    @test std_error ≈ 0.0035 atol = 0.002

    order_range = 1:7
    best_aic = argmin(granger_aic(signal, segment_length, order_range))
    @test best_aic == 3
    best_bic = argmin(granger_bic(signal, segment_length, order_range))
    @test best_bic == 3
end

@time @testset "utils.jl" begin

    # tests of int ####################################################
    @test TimeSeriesCausality.int(3.14) == 3
    @test TimeSeriesCausality.int(-2.72) == -2
    @test TimeSeriesCausality.int(0.0) == 0

    # tests of dropmean ###############################################
    test_a = rand(13)
    @test TimeSeriesCausality.dropmean(test_a, 1) == mean(test_a; dims = 1)
    test_a = rand(3, 5)
    @test TimeSeriesCausality.dropmean(test_a, 1) == mean(test_a; dims = 1)[1, :]
    test_a = rand(7, 11)
    @test TimeSeriesCausality.dropmean(test_a, 2) == mean(test_a; dims = 2)[:, 1]

    # tests of squeeze ################################################
    test_a = rand(17)
    @test TimeSeriesCausality.squeeze(test_a) == test_a
    test_a = rand(3, 5, 7)
    @test TimeSeriesCausality.squeeze(test_a) == test_a
    test_a = rand(3, 1, 7)
    @test TimeSeriesCausality.squeeze(test_a) == test_a[:, 1, :]

    # tests of detrend ################################################
    # testing the inplace
    test_x = [0:0.01:2.0;]
    zeros_x = zeros(size(test_x))
    @test all(isapprox.(TimeSeriesCausality.detrend!(test_x, 1), zeros_x, atol = 0.001))
    @test all(isapprox.(test_x, zeros_x, atol = 0.001))

    test_x = [0:0.01:2.0;]
    @test all(isapprox.(TimeSeriesCausality.detrend!(test_x, 0), [-1.0:0.01:1.0;], atol = 0.001))
    test_x = [0:0.01:2.0;]
    @test all(isapprox.(TimeSeriesCausality.detrend!(test_x, 1), zeros_x, atol = 0.001))

    test_x = [0:0.01:(2pi);]
    test_y = sin.(test_x)
    @test all(isapprox.(TimeSeriesCausality.detrend!(test_y, 0), test_y, atol = 0.001))
    test_x = [0:0.01:(2pi);]
    test_y = sin.(test_x)
    @test all(isapprox.(TimeSeriesCausality.detrend!(test_y, 1), test_y, atol = 0.001))

    test_x = [0:0.01:(2pi);]
    test_y = cos.(test_x) .+ test_x
    aux_x = [0:0.01:(2pi);] .- pi
    aux_y = cos.(test_x) .+ aux_x
    @test all(isapprox.(TimeSeriesCausality.detrend!(test_y, 0), aux_y, atol = 0.001))
    test_x = [0:0.01:(2pi);]
    test_y = cos.(test_x) .+ test_x
    @test all(isapprox.(TimeSeriesCausality.detrend!(test_y, 1), cos.(test_x), atol = 0.01))

    # tests of hanning_fun ############################################
    # the arrays are from MATLAB `hanning` function.
    hann_12 = [
        0.057271987173395
        0.215967626634422
        0.439731659872338
        0.677302443521268
        0.874255374085551
        0.985470908713026
        0.985470908713026
        0.874255374085551
        0.677302443521268
        0.439731659872338
        0.215967626634422
        0.057271987173395
    ]
    hann_13 = [
        0.049515566048790
        0.188255099070633
        0.388739533021843
        0.611260466978157
        0.811744900929367
        0.950484433951210
        1.000000000000000
        0.950484433951210
        0.811744900929367
        0.611260466978157
        0.388739533021843
        0.188255099070633
        0.049515566048790
    ]
    hanning_window_12 = TimeSeriesCausality.hanning_fun(12)
    hanning_window_13 = TimeSeriesCausality.hanning_fun(13)
    @test all(hanning_window_12 .≈ hann_12)
    @test all(hanning_window_13 .≈ hann_13)
    @test hanning_window_12[1] == hanning_window_12[end]
    @test hanning_window_13[1] == hanning_window_13[end]
    @test hanning_window_12[1] > 0.0
    @test hanning_window_13[1] > 0.0

end
@time @testset "phase_slope_index.jl" begin
    # tests of psi_est ###############################################
    # two random signals
    signal = [[randn(1000000);] [randn(1000000);]]
    psi, _ = psi_est(signal, 100; subave = true, segave = true)
    @test all(isapprox(psi, zeros(2, 2); atol = 0.01))

    # induced causality should always be inferred independent of optional arguments
    for eplen_ in [0, 200], detrend_ in [true, false]
        for segave_ in [true, false], subave_ in [true, false]
            for method_ in ["jackknife", "bootstrap"]
                # channel 2 being exactly channel 1
                ch1_ = randn(1000)
                signal = [[ch1_;] [ch1_;]]
                psi, _ = psi_est(
                    signal,
                    100;
                    method = method_,
                    eplen = eplen_,
                    detrend = detrend_,
                    segave = segave_,
                    subave = subave_,
                )
                @test psi == zeros(2, 2)

                # channel 1 leading channel 2
                ch1_ = randn(100000)
                signal = [[ch1_[2:end];] [ch1_[1:(end-1)];]]
                psi, _ = psi_est(
                    signal,
                    100;
                    method = method_,
                    eplen = eplen_,
                    detrend = detrend_,
                    segave = segave_,
                    subave = subave_,
                )
                @test psi[1, 1] == 0.0 && psi[2, 2] == 0.0
                @test psi[1, 2] > 1.0 && psi[2, 1] + psi[1, 2] == 0.0

                # channel 2 leading channel 1
                ch2_ = randn(100000)
                signal = [[ch2_[1:(end-1)];] [ch2_[2:end];]]
                psi, _ = psi_est(
                    signal,
                    100;
                    method = method_,
                    eplen = eplen_,
                    detrend = detrend_,
                    segave = segave_,
                    subave = subave_,
                )
                @test psi[1, 1] == 0.0 && psi[2, 2] == 0.0
                @test psi[1, 2] < -1.0 && psi[2, 1] + psi[1, 2] == 0.0
            end
        end
    end

    # test freqlist ###################################################
    ch1_ = randn(100000)
    signal = [[ch1_[2:end];] [ch1_[1:(end-1)];]]
    psi_range, _ = psi_est(signal, 100; freqlist = 1:49)
    psi_default, _ = psi_est(signal, 100)  # default is based on seglen
    @test all(psi_range == psi_default)

    # auxiliary function for testing
    """
        sin_sum = v_sin(X, A, Ω, Φ)

    returns an array summed over series of Sin waves with A-amplitudes,
        Ω-frequencies and Φ-phase delay
    """
    v_sin(X, A, Ω, Φ) = sum([a .* sin.(ω .* X .+ ϕ) for (a, ω, ϕ) in zip(A, Ω, Φ)])

    nsamples_ = 200000
    fs_ = 100  # sampling frequency (Hz)
    f_range_ = 12:30  # beta frequency band [12-30] Hz
    Ω_ = f_range_ * 2pi
    X_ = range(0.0, nsamples_ / fs_; length = nsamples_)
    A_ = rand(0.1:0.01:1.0, size(f_range_, 1))
    Φ_ = rand(size(f_range_, 1))
    Y_ = v_sin(X_, A_, Ω_, Φ_)
    Y_ch1, Y_ch2 = Y_[5:end], Y_[1:(end-4)]
    noise_ = randn(size(Y_ch1))
    signal = [Y_ch1 Y_ch2] .+ noise_
    seglen_ = fs_
    psi_full_range, _ = psi_est(signal, seglen_)
    psi_low, _ = psi_est(signal, seglen_; freqlist = 1:11)
    psi_freq, _ = psi_est(signal, seglen_; freqlist = f_range_)
    psi_high, _ = psi_est(signal, seglen_; freqlist = 31:49)
    @test psi_full_range[1, 2] > 1.0  # ch1 leading ch2
    @test psi_freq[1, 2] > psi_full_range[1, 2]  # PSI higher in target frequency range
    @test psi_freq[1, 2] > psi_low[1, 2]  # PSI higher in target frequency range
    @test psi_freq[1, 2] > psi_high[1, 2]  # PSI higher in target frequency range

    # ndims(data) should be 2
    signal = randn(100, 3, 2)
    @test_throws DimensionMismatch psi_est(signal, 100)

    # squeeze
    signal = randn(100, 1, 4)
    @test psi_est(signal, 100)[1] == psi_est(signal[:, 1, :], 100)[1]

    # size(data, 1) >!> seglen
    signal = randn(50, 3)
    @test_throws DimensionMismatch psi_est(signal, 100)

    # if eplen == 0 then no std estimation
    signal = [[randn(1000000);] [randn(1000000);]]
    _, psi_std = psi_est(signal, 100)
    @test all(isnan.(psi_std))

    # if continuous data, subave shall not be true otherwise NaN is returned
    signal = [[randn(1000000);] [randn(1000000);]]
    psi, _ = psi_est(signal, 100; subave = true)
    @test !all(isnan.(psi))

end


@polly function kernel_gemver(alpha, beta, A, u1, v1, u2, v2, w, x, y, z)
    n = size(u1,1)
    for i = 1:n, j = 1:n
        A[i,j] = A[i,j] + u1[i] * v1[j] + u2[i] * v2[j]
    end

    for i = 1:n, j = 1:n
        x[i] = x[i] + beta * A[j,i] * y[j]
    end

    for i = 1:n
        x[i] = x[i] + z[i]
    end

    for i = 1:n, j = 1:n
        w[i] = w[i] + alpha * A[i,j] * x[j]
    end
end

let
    n = 2000

    A = zeros(Float32, n, n)
    u1 = zeros(Float32, n)
    v1 = zeros(Float32, n)
    u2 = zeros(Float32, n)
    v2 = zeros(Float32, n)
    w = zeros(Float32, n)
    x = zeros(Float32, n)
    y = zeros(Float32, n)
    z = zeros(Float32, n)

    alpha = Float32(1.5)
    beta = Float32(1.2)

    fn = Float32(n)

    for i = 1:n
        u1[i] = i;
        u2[i] = ((i+1)/fn)/2.0
        v1[i] = ((i+1)/fn)/4.0
        v2[i] = ((i+1)/fn)/6.0
        y[i] = ((i+1)/fn)/8.0
        z[i] = ((i+1)/fn)/9.0
        x[i] = 0.0
        w[i] = 0.0
        for j = 1:n
            A[i,j] = (i*j % n) / n
        end
    end

    SUITE["gemver"] = @benchmarkable kernel_gemver($alpha, $beta, A, u1, v1, u2, v2, w, x, y, z) setup = (A = copy($A); u1 = copy($u1); v1 = copy($v1); u2 = copy($u2); v2 = copy($v2); w = copy($w); x = copy($x); y = copy($y); z = copy($z))
end


#
# Tests for the Pacanowski-Philander module
#

function test_pp_basic()
    model = PacanowskiPhilander.Model(N=4, L=2)
    model.grid.N == 4
end

function test_pp_diffusion_cosine()
    parameters = PacanowskiPhilander.Parameters(Cκ₀=1.0, Cκ₁=0.0)
    model = PacanowskiPhilander.Model(N=100, L=π/2, parameters=parameters)
    z = model.grid.zc

    c_init(z) = cos(2z)
    c_ans(z, t) = exp(-4t) * c_init(z)

    model.solution.T = c_init

    dt = 1e-3
    iterate!(model, dt)

    # The error tolerance is a bit arbitrary.
    norm(c_ans.(z, time(model)) .- data(model.solution.T)) < model.grid.N*1e-6
end

@testset "Pacanowski-Philander" begin
    @test test_pp_basic()
    @test test_pp_diffusion_cosine()
end


using OrdinaryDiffEq, HyperbolicDiffEq

function compute(K, N, fnum, compute_entropy_var, diss)
    xmin = -1.0
    xmax = +1.0
    tspan = (0., 1.0)
    ode_solver = SSPRK104()

    mesh = UniformPeriodicMesh1D(xmin, xmax, N)
    balance_law = Cubic()
    reconstruction = ModifiedENO(K)
    semidisc = ScalarUniformPeriodicTecno1D(balance_law, mesh, fnum, diss, reconstruction, compute_entropy_var)
    ode = semidiscretise(semidisc, sinpi, tspan)

    solve(ode, ode_solver, dt=1/N, save_everystep=false);
end

let K = 1
    N = 2^10
    fnum = CentralFlux()
    compute_entropy_var = (u,model) -> u^3
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * (wr-wl) # TeCNO
    compute(K, N, fnum, compute_entropy_var, diss)

    fnum = CentralFlux()
    compute_entropy_var = (u,model) -> u^3
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * abs(wr-wl)^(K-1) * (wr-wl) #ELW
    compute(K, N, fnum, compute_entropy_var, diss)

    fnum = EnergyConservativeFlux()
    compute_entropy_var = (u,model) -> u
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * (wr-wl) # TeCNO
    compute(K, N, fnum, compute_entropy_var, diss)

    fnum = EnergyConservativeFlux()
    compute_entropy_var = (u,model) -> u
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * abs(wr-wl)^(K-1) * (wr-wl) #ELW
    compute(K, N, fnum, compute_entropy_var, diss)
end

let K = 2
    N = 2^10
    fnum = CentralFlux()
    compute_entropy_var = (u,model) -> u^3
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * (wr-wl) # TeCNO
    compute(K, N, fnum, compute_entropy_var, diss)

    fnum = CentralFlux()
    compute_entropy_var = (u,model) -> u^3
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * abs(wr-wl)^(K-1) * (wr-wl) #ELW
    compute(K, N, fnum, compute_entropy_var, diss)

    fnum = EnergyConservativeFlux()
    compute_entropy_var = (u,model) -> u
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * (wr-wl) # TeCNO
    compute(K, N, fnum, compute_entropy_var, diss)

    fnum = EnergyConservativeFlux()
    compute_entropy_var = (u,model) -> u
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * abs(wr-wl)^(K-1) * (wr-wl) #ELW
    compute(K, N, fnum, compute_entropy_var, diss)
end

let K = 3
    N = 2^10
    fnum = CentralFlux()
    compute_entropy_var = (u,model) -> u^3
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * (wr-wl) # TeCNO
    compute(K, N, fnum, compute_entropy_var, diss)

    fnum = CentralFlux()
    compute_entropy_var = (u,model) -> u^3
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * abs(wr-wl)^(K-1) * (wr-wl) #ELW
    compute(K, N, fnum, compute_entropy_var, diss)

    fnum = EnergyConservativeFlux()
    compute_entropy_var = (u,model) -> u
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * (wr-wl) # TeCNO
    compute(K, N, fnum, compute_entropy_var, diss)

    fnum = EnergyConservativeFlux()
    compute_entropy_var = (u,model) -> u
    diss = (wl, wr, model) -> -0.5*max_abs_speed(wl, wr, model) * abs(wr-wl)^(K-1) * (wr-wl) #ELW
    compute(K, N, fnum, compute_entropy_var, diss)
end


"""
    LatentGP(f<:GP, lik)

 - `fx` is a `FiniteGP`.
 - `lik` is the log likelihood function which maps sample from f to corresposing
 conditional likelihood distributions.

"""
struct LatentGP{T<:AbstractGPs.FiniteGP, S}
    fx::T
    lik::S
end

function Distributions.rand(rng::AbstractRNG, lgp::LatentGP)
    f = rand(rng, lgp.fx)
    y = rand(rng, lgp.lik(f))
    return (f=f, y=y)
end

"""
    logpdf(lgp::LatentGP, y::NamedTuple{(:f, :y)})

```math
    log p(y, f; x)
```
Returns the joint log density of the gaussian process output `f` and real output `y`.
"""
function Distributions.logpdf(lgp::LatentGP, y::NamedTuple{(:f, :y)})
    return logpdf(lgp.fx, y.f) + logpdf(lgp.lik(y.f), y.y)
end


using Unitful

import PipeLayout: weymouth

@testset "unit consistency in pressure loss" begin
    L = 100.0u"km"
    D = 1.0u"m"
    q = 100.0u"kg/s"
    p_i = 80.0u"bar"
    C = L/D^5*weymouth

    # pi^2 - po^2 = C*q|q|,  pressure should drop
    @test p_i^2 - C*q*abs.(q) <= p_i^2
end


import Ipopt, Hyperopt
using Nonconvex, LinearAlgebra, Test

f(x::AbstractVector) = sqrt(x[2])
g(x::AbstractVector, a, b) = (a*x[1] + b)^3 - x[2]

alg = HyperoptAlg(IpoptAlg())

@testset "Sampler - $spl_name" for (spl_name, spl) in [
    ("RandomSampler", RandomSampler()),
    ("Hyperband", Hyperband(R=100, η=3, inner=RandomSampler())),
    ("LHSampler", LHSampler()),
    ("CLHSampler", CLHSampler()),
    ("GPSampler", GPSampler()),
]
    if spl_name == "Hyperband"
        options = HyperoptOptions(
            sub_options = max_iter -> IpoptOptions(first_order = true, max_iter = max_iter),
            sampler = spl,
        )
    else
        options = HyperoptOptions(
            sub_options = IpoptOptions(first_order = true),
            sampler = spl,
        )
    end
    m = Model(f)
    addvar!(m, [0.0, 0.0], [10.0, 10.0])
    add_ineq_constraint!(m, x -> g(x, 2, 0))
    add_ineq_constraint!(m, x -> g(x, -1, 1))

    r = Nonconvex.optimize(m, alg, [1.234, 2.345], options = options)
    @test abs(r.minimum - sqrt(8/27)) < 1e-6
    @test norm(r.minimizer - [1/3, 8/27]) < 1e-6
end


using Dice
using Dice: num_flips, num_nodes, to_dice_ir

code = @dice begin
    # network reachability example from the dice paper

    function diamond(s1)
        route = flip(0.5)
        s2 = if route s1 else false end
        s3 = if route false else s1 end
        drop = flip(0.0001)
        s2 || (s3 && !drop)
    end

    net = true
    for i=1:3
        net = diamond(net)
    end

    net
end

# BDD analysis
bdd = compile(code)
num_flips(bdd)
num_nodes(bdd)
infer(code, :bdd)

# IR analysis
to_dice_ir(code)
has_dice_binary() && rundice(code)
has_dice_binary() && infer(code, :ocaml)
# TODO FIX to not duplicate flips


struct DFP{T1} <: QuasiNewton{T1}
   approx::T1
end
# function update!(scheme::DFP, B::InverseApprox, s, y)
#    B.A = B.A + s*s'/dot(s, y) - B.A*y*y'*B.A/(y'*B.A*y)
# end
# function update!(scheme::DFP, B::DirectApprox, s, y)
#    B.A = (I - y*s'/dot(y, s))*B.A*(I - s*y'/dot(y, s)) + y*y'/dot(y, s)
# end

function update(H, s, y, scheme::DFP{<:InverseApprox})
    ρ = inv(dot(s, y))
    if ρ > 1e6
        H = H + ρ*s*s' - H*(y*y')*H/(y'*H*y)
    end
    H
end
function update(B, s, y, scheme::DFP{<:DirectApprox})
    ρ = inv(dot(s, y))

    if ρ > 1e6
        C = (I - ρ*y*s')
        B = C*B*C' + ρ*y*y'
    else
        if isa(B, UniformScaling)
            B = B + zero(eltype(s))* y * s'
        end
    end
    B
end
function update!(H, s, y, scheme::DFP{<:InverseApprox})
    ρ = inv(dot(s, y))
    if ρ > 1e6
        H .+= ρ*s*s' - H*(y*y')*H/(y'*H*y)
    end
    H
end
function update!(B, s, y, scheme::DFP{<:DirectApprox})
    ρ = inv(dot(s, y))
    # so right now, we just skip the update if dsy is zero
    # but we might do something else here
    if ρ > 1e6
        C = (I - ρ*y*s')
        B .= C*B*C' + ρ*y*y'
    end
    B
end
update!(A::UniformScaling, s, y , scheme::DFP{<:InverseApprox}) = update(A, s, y, scheme)
update!(A::UniformScaling, s, y , scheme::DFP{<:DirectApprox}) = update(A, s, y, scheme)


module LotkaVolterra

using Iterators
using Match
using ODE

import Base.show

export AbstractLotkaVolterra,
       communitymatrix,intrinsicrate,
       issector,sectors,
       LogisticGrowth,GeneralizedLotkaVolterra,
       rate,specificrate,jacobian,
       somefixedpoint,isfixedpoint,fixedpoint,fixedpoints,
       somefeasible,isfeasible,feasibility,
       somestable,isstable,stability,
       someviable,isviable,viability,
       odeint

include("utils.jl")
include("abstract.jl")
include("logistic.jl")
include("general.jl")

end


__precompile__()
module Shampoo

using JUDI, JOLI, LinearAlgebra, FFTW, SpecialFunctions, DSP

export CumsumOp,DiffOp,HammingOp,FractionalIntegrationOp,FractionalLaplacianOp, MaskOp

# utilities
include("utils/adj_diff_cumsum.jl")
include("utils/auxilary.jl")
include("utils/fgl_deriv.jl")
include("utils/Filter_freq_band.jl")
include("utils/joConvolve.jl")
include("utils/wavelet.jl")

# operators
include("operators/fractional_integration.jl")
include("operators/fractional_laplacian.jl")
include("operators/hamming_operator.jl")
include("operators/mask_op.jl")

# left preconditioners: work in data domain

function CumsumOp(nt::Int,nsrc::Int,nrec::Int;DDT=Float32)
	P = joLinearFunctionFwd_T(nt*nsrc*nrec, nt*nsrc*nrec,
	                             v -> cumsum(v),
	                             w -> adjoint_cumsum(w),
								 DDT,DDT,name="cumsum operator")
end

function DiffOp(nt::Int,nsrc::Int,nrec::Int;order=1,DDT=Float32)
	P = joLinearFunctionFwd_T(nt*nsrc*nrec, nt*nsrc*nrec,
	                             v -> diff(v),
	                             w -> adjoint_diff(w),
								 DDT,DDT,name="diff operator")
	return P
end

function HammingOp(nt::Int,nsrc::Int,nrec::Int;DDT=Float32)
	H = joLinearFunctionFwd_T(nt*nsrc*nrec, nt*nsrc*nrec,
								v -> hamm_op(v),
								w -> hamm_op(w),
								DDT,DDT,name="Hamming operator")
	return H
end

function FractionalIntegrationOp(nt::Int,nsrc::Int,nrec::Int,order::Number;DDT=Float32)
	### Positive order -> integration, Negative order -> derivative
	C = frac_int(nt,order; DDT=DDT)
	P = joLinearFunctionFwd_T(nt*nsrc*nrec, nt*nsrc*nrec,
	                             v -> apply_frac_integral(C,v),
	                             w -> apply_frac_integral(C',w),
								 DDT,DDT,name="Fractional integration operator")
	return P
end

# right preconditioners: work in model domain

function MaskOp(n::Tuple,mask::Array{DDT}) where {DDT}
	P = joLinearFunctionFwd_T(prod(n), prod(n),
								v -> mask_op(v,mask),
								w -> mask_op(w,mask),
								DDT,DDT,name="Mask operator")
	return P
end

function FractionalLaplacianOp(n::Tuple,order::Number;DDT=Float32)
	model = F.model
	info = F.info
	P = laplacian_operator(model::Model,order::Number,info::Info)
	return P
end

end


function update_alpha!(s::State, c::Constants, d::Data, sb_ibp::Bool)
  newShape = shape(c.alpha_prior) + c.K

  if sb_ibp
    newRate = rate(c.alpha_prior) - sum(log.(s.v))
  else
    newRate = rate(c.alpha_prior) - sum(log.(s.v)) / c.K
  end

  post = Gamma(newShape, 1.0 / newRate)
  s.alpha = rand(post)
end


module Fitting

using ConstructionBase: constructorof, setproperties
using LsqFit: curve_fit, coef
using PolynomialRoots: roots
using Polynomials: fit, derivative, coeffs, derivative
using Unitful: AbstractQuantity, NoUnits, ustrip, unit, uconvert

using ..EquationsOfStateOfSolids:
    EquationOfStateOfSolids,
    FiniteStrainParameters,
    Parameters,
    PressureEquation,
    EnergyEquation,
    BulkModulusEquation,
    orderof,
    _ispositive,
    getparam
using ..FiniteStrains: FiniteStrain, ToStrain, FromStrain, Dⁿᵥf, straintype

import Unitful

export linfit, nonlinfit, eosfit

# See https://github.com/JuliaMath/Roots.jl/blob/bf0da62/src/utils.jl#L9-L11
struct ConvergenceFailed
    msg::String
end

struct CriterionNotMet
    msg::String
end

eosfit(eos::EnergyEquation{<:FiniteStrainParameters}, volumes, energies; kwargs...) =
    linfit(eos, volumes, energies; kwargs...)
eosfit(eos, xs, ys; kwargs...) = nonlinfit(eos, xs, ys; kwargs...)

# ================================== Linear fitting ==================================
"""
    linfit(eos::EnergyEquation{<:FiniteStrainParameters}, volumes, energies; kwargs...)

Fit an equation of state using linear algorithms.

# Arguments
- `maxiter::Integer=1000`: .
- `conv_thr::AbstractFloat=1e-12`: .
- `root_thr::AbstractFloat=1e-20`: .
- `verbose::Bool=false`: .

!!! note
    If you want to fit with `BigFloat` data, you need to install
    [`GenericSVD.jl`](https://github.com/JuliaLinearAlgebra/GenericSVD.jl) and `using
    GenericSVD` before fittting!
"""
function linfit(
    eos::EnergyEquation{<:FiniteStrainParameters},
    volumes,
    energies;
    maxiter = 1000,
    conv_thr = 1e-12,
    root_thr = 1e-20,
    verbose = false,
)::FiniteStrainParameters
    deg = orderof(getparam(eos))
    S = straintype(getparam(eos))
    v0 = iszero(getparam(eos).v0) ? volumes[findmin(energies)[2]] : getparam(eos).v0  # Initial v0
    uv, ue = unit(v0), unit(energies[1])
    uvrule = uconvert(unit(volumes[1]), 1 * uv)
    v0 = ustrip(v0)
    volumes = collect(map(x -> ustrip(uv, x), volumes))  # `parent` is needed to unwrap `DimArray`
    energies = collect(map(x -> ustrip(ue, x), energies))
    for i in 1:maxiter  # Self consistent loop
        strains = map(ToStrain{S}(v0), volumes)
        if !(isreal(strains) && isreal(energies))
            throw(DomainError("the strains or the energies are complex!"))
        end
        poly = fit(real(strains), real(energies), deg)
        f0, e0 = _minofmin(poly, root_thr)
        v0_prev, v0 = v0, FromStrain{S}(v0)(f0)  # Record v0 to v0_prev, then update v0
        if abs((v0_prev - v0) / v0_prev) <= conv_thr
            if verbose
                @info "convergence reached after $i steps!"
            end
            fᵥ = map(deg -> Dⁿᵥf(S(), deg, v0)(v0), 1:4)
            e_f = map(deg -> derivative(poly, deg)(f0), 1:4)
            b0, b′0, b″0 = _Dₚb(fᵥ, e_f)
            return _update(
                getparam(eos);
                v0 = v0 * uv,
                b0 = b0(v0) * ue / uv,
                b′0 = b′0(v0),
                b″0 = b″0(v0) * uv / ue,
                e0 = e0 * ue,
            )
        end
    end
    throw(ConvergenceFailed("convergence not reached after $maxiter steps!"))
end

function _islocalmin(x, y)  # `x` & `y` are both real
    y″ₓ = derivative(y, 2)(x)  # Must be real
    return _ispositive(real(y″ₓ))  # If 2nd derivative at x > 0, (x, y(x)) is a local minimum
end

function _localmin(y, root_thr = 1e-20)  # `y` is a polynomial (could be complex)
    y′ = derivative(y, 1)
    pool = roots(coeffs(y′); polish = true, epsilon = root_thr)
    real_roots = real(filter(isreal, pool))  # Complex volumes are meaningless
    if isempty(real_roots)
        throw(CriterionNotMet("no real extrema found! Consider changing `root_thr`!"))  # For some polynomials, could be all complex
    else
        localminima = filter(x -> _islocalmin(x, y), real_roots)
        if isempty(localminima)
            throw(CriterionNotMet("no local minima found!"))
        else
            return localminima
        end
    end
end

# https://stackoverflow.com/a/21367608/3260253
function _minofmin(y, root_thr = 1e-20)  # Find the minimum of the local minima
    localminima = _localmin(y, root_thr)
    y0, i = findmin(map(y, localminima))  # `y0` must be real, or `findmap` will error
    x0 = localminima[i]
    return x0, y0
end

# See Eq. (55) - (57) in Ref. 1.
function _Dₚb(fᵥ, e_f)  # Bulk modulus & its derivatives
    e″ᵥ = _D²ᵥe(fᵥ, e_f)
    e‴ᵥ = _D³ᵥe(fᵥ, e_f)
    b0 = v -> v * e″ᵥ
    b′0 = v -> -v * e‴ᵥ / e″ᵥ - 1
    b″0 = v -> (v * (_D⁴ᵥe(fᵥ, e_f) * e″ᵥ - e‴ᵥ^2) + e‴ᵥ * e″ᵥ) / e″ᵥ^3
    return b0, b′0, b″0  # 3 lazy functions
end

# Energy-volume derivatives, see Eq. (50) - (53) in Ref. 1.
_D¹ᵥe(fᵥ, e_f) = e_f[1] * fᵥ[1]
_D²ᵥe(fᵥ, e_f) = e_f[2] * fᵥ[1]^2 + e_f[1] * fᵥ[2]
_D³ᵥe(fᵥ, e_f) = e_f[3] * fᵥ[1]^3 + 3fᵥ[1] * fᵥ[2] * e_f[2] + e_f[1] * fᵥ[3]
_D⁴ᵥe(fᵥ, e_f) =
    e_f[4] * fᵥ[1]^4 +
    6fᵥ[1]^2 * fᵥ[2] * e_f[3] +
    (4fᵥ[1] * fᵥ[3] + 3fᵥ[3]^2) * e_f[2] +
    e_f[1] * fᵥ[4]

function _update(x::FiniteStrainParameters; kwargs...)
    patch = (; (f => kwargs[f] for f in propertynames(x))...)
    return setproperties(x, patch)
end

# ================================== Nonlinear fitting ==================================
"""
    nonlinfit(eos::EquationOfStateOfSolids, xs, ys; kwargs...)

Fit an equation of state using nonlinear algorithms.

# Arguments
- `xtol::AbstractFloat=1e-16`: .
- `gtol::AbstractFloat=1e-16`: .
- `maxiter::Integer=1000`: .
- `min_step_quality::AbstractFloat=1e-16`: .
- `good_step_quality::AbstractFloat=0.75`: .
- `verbose::Bool=false`: .
"""
function nonlinfit(
    eos::EquationOfStateOfSolids,
    xdata,
    ydata;
    xtol = 1e-16,
    gtol = 1e-16,
    maxiter = 1000,
    min_step_quality = 1e-16,
    good_step_quality = 0.75,
    verbose = false,
)::Parameters
    model = buildmodel(eos)
    p0, xdata, ydata = _preprocess(eos, xdata, ydata)
    fit = curve_fit(  # See https://github.com/JuliaNLSolvers/LsqFit.jl/blob/f687631/src/levenberg_marquardt.jl#L3-L28
        model,
        xdata,
        ydata,
        p0;
        x_tol = xtol,
        g_tol = gtol,
        maxIter = maxiter,
        min_step_quality = min_step_quality,
        good_step_quality = good_step_quality,
        show_trace = verbose,
    )
    if fit.converged
        result = _postprocess(coef(fit), getparam(eos))
        checkresult(result)
        return result
    else
        throw(ConvergenceFailed("convergence not reached after $maxiter steps!"))
    end
end

# Do not export!
buildmodel(eos::EquationOfStateOfSolids{T}) where {T} =
    (x, p) -> constructorof(typeof(eos))(constructorof(T)(p...)).(x)

function checkresult(x::Parameters)  # Do not export!
    if x.v0 <= zero(x.v0) || x.b0 <= zero(x.b0)
        @error "either v0 ($(x.v0)) or b0 ($(x.b0)) is not positive!"
    end
    if PressureEquation(x)(x.v0) >= x.b0 && x isa FiniteStrainParameters
        @warn "consider using higher order EOS!"
    end
end

function _preprocess(eos, xdata, ydata)  # Do not export!
    p = getparam(eos)
    if eos isa EnergyEquation && iszero(p.e0)
        eos = EnergyEquation(setproperties(p; e0 = uconvert(unit(p.e0), minimum(ydata))))  # Energy minimum as e0, `uconvert` is important to keep the unit right!
    end
    return map(_float_collect, _unify(eos, xdata, ydata))  # `xs` & `ys` may not be arrays
end

function _unify(eos, xdata, ydata)  # Unify units of data
    p = getparam(eos)
    uy(eos::EnergyEquation) = unit(p.e0)
    uy(eos::Union{PressureEquation,BulkModulusEquation}) = unit(p.e0) / unit(p.v0)
    return ustrip.(_unormal(p)), ustrip.(unit(p.v0), xdata), ustrip.(uy(eos), ydata)
end

_unormal(p::Parameters) = collect(getfield(p, i) for i in 1:nfields(p))
function _unormal(p::Parameters{<:AbstractQuantity})  # Normalize units of `p`
    up = unit(p.e0) / unit(p.v0)  # Pressure/bulk modulus unit
    return map(fieldnames(typeof(p))) do f
        x = getfield(p, f)
        if f == :b0
            x |> up
        elseif f == :b″0
            x |> up^(-1)
        elseif f == :b‴0
            x |> up^(-2)
        elseif f in (:v0, :b′0, :e0)
            x
        else
            error("unknown field `$f`!")
        end
    end
end

_postprocess(p, p0::Parameters) = constructorof(typeof(p0))(p...)
function _postprocess(p, p0::Parameters{<:AbstractQuantity})
    up = unit(p0.e0) / unit(p0.v0)  # Pressure/bulk modulus unit
    param = map(enumerate(fieldnames(typeof(p0)))) do (i, f)
        x = p[i]
        u = unit(getfield(p0, f))
        if f == :b0
            x * up |> u
        elseif f == :b″0
            x * up^(-1) |> u
        elseif f == :b‴0
            x * up^(-2) |> u
        elseif f in (:v0, :b′0, :e0)
            x * u
        else
            error("unknown field `$f`!")
        end
    end
    return constructorof(typeof(p0))(param...)
end

_float_collect(x) = collect(float.(x))  # Do not export!

_fmap(f, x) = constructorof(typeof(x))((f(getfield(x, i)) for i in 1:nfields(x))...)  # Do not export!

"Convert all elements of a `Parameters` to floating point data types."
Base.float(p::Parameters) = _fmap(float, p)  # Not used here but may be useful

"Test whether all `p`'s elements are numerically equal to some real number."
Base.isreal(p::Parameters) = all(isreal(getfield(p, i)) for i in 1:nfields(p))  # Not used here but may be useful

"Construct a real `Parameters` from the real parts of the elements of p."
Base.real(p::Parameters) = _fmap(real, p)  # Not used here but may be useful

"""
    ustrip(p::Parameters)

Strip units from a `Parameters`.
"""
Unitful.ustrip(p::Parameters) = _fmap(ustrip, p)

end


X = zeros(Int, 12, 12*11*10)

n = 1
for p1 in 1:12
    for p2 in 1:12
        if p2 != p1
            for p3 in 1:12
                if p3 != p1 && p3 != p2
                    X[p1, n] = 1
                    X[p2, n] = 1
                    X[p3, n] = 1
                    n = n + 1
                end
            end
        end
    end
end

Cmaj = [1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]
X_fourier = abs.(fft(X, 1))
Cmaj_fourier = abs.(fft(Cmaj, 1))
Cmaj_fourier = repmat(Cmaj_fourier, 1, 12*11*10)
dists_fourier = sum((X_fourier - Cmaj_fourier).*(X_fourier - Cmaj_fourier), 1)
sorted_dists_fourier, sorting_indices_fourier = sort(vec(dists_fourier))


module FastArithmetic

using Nemo

export monomialToDual

"""
    monomialToDual{T <: RingElem}(a::Array{T,1},P::PolyElem{T})

Convert monomial (canonical) coordinates "a" to dual coordinates
with respect to the trace generated by P.

# Remark
* a and P are over a field k which must be a perfect field ;
* P must be monic and squarefree ;
* k[x]/(P) is not necessarily a field since P is not necessarily irreducible.
"""
function monomialToDual{T <: RingElem}(a::Array{T,1},P::PolyElem{T})

  # We first check if the elements of b and coefficients
  # of P belong to the same field
  k::Nemo.Ring=parent(a[1])
  @assert k==base_ring(P)

  # We set some constants, P is of degree m and belongs to the ring R = k[t]
  m::Int64=degree(P)
  R::Nemo.Ring=parent(P)
  t::PolyElem{T}=gen(R)

  # We set Q to 1/rev(P,m+1) mod t^m
  Q::PolyElem{T}=reverse(P,m+1)
  Q=gcdinv(Q,t^m)[2]

  # Finally we compute b = rev(P'a mod P, m)Q mod t^m
  A::PolyElem{T}=R(a)
  b::PolyElem{T}=(reverse((derivative(P)*A)%P,m)*Q)%(t^m)
  return T[coeff(b,i) for i in 0:(m-1)]
end

export monomialToDual_pre
function monomialToDual_pre{T <: RingElem}(a::Array{T,1},P::PolyElem{T},TP::PolyElem{T})

  # We first check if the elements of b and coefficients
  # of P belong to the same field
  k::Nemo.Ring=parent(a[1])
  @assert k==base_ring(P)

  # We set some constants, P is of degree m and belongs to the ring R = k[t]
  m::Int64=degree(P)
  R::Nemo.Ring=parent(P)
  t::PolyElem{T}=gen(R)

  # Finally we compute b = rev(P'a mod P, m)TP mod t^m
  A::PolyElem{T}=R(a)
  b::PolyElem{T}=(reverse((derivative(P)*A)%P,m)*TP)%(t^m)
  return T[coeff(b,i) for i in 0:(m-1)]
end

export dualToMonomial

"""
    dualToMonomial{T}(b::Array{T,1},P::PolyElem{T})

Convert dual coordinates "b" to monomial coordinates with respect to the trace
generated by P.

# Remark
* a and P are over a field k which must be a perfect field ;
* P must be monic and squarefree ;
* k[x]/(P) is not necessarily a field since P is not necessarily irreducible.
"""
function dualToMonomial{T}(b::Array{T,1},P::PolyElem{T})

  # We first check if the elements of b and coefficients
  # of P belong to the same field
  k::Nemo.Ring=parent(b[1])
  @assert k==base_ring(P)

  # We set some constants, P is of degree m and belongs to the ring R = k[t]
  m::Int64=degree(P)
  R::Nemo.Ring=parent(P)
  t::PolyElem{T}=gen(R)

  # We compute S = 1/P' mod P, c = rev(P, m+1)b, d = cS mod P
  S::PolyElem{T}=gcdinv(derivative(P),P)[2]
  c::PolyElem{T}=(reverse(P,m+1)*R(b))%(t^m)
  c=reverse(c,m)
  d::PolyElem{T}=(c*S)%P
  return T[coeff(d,i) for i in 0:(m-1)]
end

export dualToMonomial_pre
function dualToMonomial_pre{T}(b::Array{T,1},P::PolyElem{T}, S::PolyElem{T})

  # We first check if the elements of b and coefficients
  # of P belong to the same field
  k::Nemo.Ring=parent(b[1])
  @assert k==base_ring(P)

  # We set some constants, P is of degree m and belongs to the ring R = k[t]
  m::Int64=degree(P)
  R::Nemo.Ring=parent(P)
  t::PolyElem{T}=gen(R)

  # We compute S = 1/P' mod P, c = rev(P, m+1)b, d = cS mod P
  c::PolyElem{T}=(reverse(P,m+1)*R(b))%(t^m)
  c=reverse(c,m)
  d::PolyElem{T}=(c*S)%P
  return T[coeff(d,i) for i in 0:(m-1)]
end

export mulT

"""
    mulT{T}(c::Array{T,1},P::PolyElem{T},n::Int64)

The tranposition of the naive algorithm of multiplication by P.

# Arguments
* c::Array{T,1} must have length m (degree of P) + n.

# Remark
* closely linked with the middle product [1] ;
* I don't really see why n is an argument, since we could obtain it by
computing n = length(c) - degree(P).

# References
* [1] : G. Hanrot, M. Quercia, and P. Zimmerman. The middle product algorithm I.
Appl. Algebra Engrg. Comm. Comput., 14(6):415-438, 2004.
"""
function mulT{T}(c::Array{T,1},P::PolyElem{T},n::Int64)

  # We first check if the elements of b and coefficients
  # of P belong to the same field
  m::Int64=degree(P)
  k::Nemo.Ring=base_ring(P)
  @assert k==parent(c[1])

  # We compute R = k[t]
  R::Nemo.Ring=parent(P)
  t::PolyElem{T}=gen(R)

  # And we transpose all the basic operations
  p::Array{T,1}=T[k(coeff(P,j)) for j in 0:m]
  b::Array{T,1}=Array(T,n+1) # array filled with "#undef"
  for i in 1:(n+1)
    b[i]=k(0)
  end
  for i in range(m+n,-1,m+n+1)
    for j in range(min(m,i),-1,min(m,i)-max(0,i-n)+1)
      b[i-j+1]=b[i-j+1]+p[j+1]*c[i+1]
    end
  end
  return R(T[b[i] for i in 1:(n+1)])
end

export mulTmid

"""
    mulTmid{T}(c::Array{T,1},P::PolyElem{T},n::Int64)

The tranposition of the algorithm of multiplication by P. Using middle product.

# Arguments
* c::Array{T,1} must have length m (degree of P) + n + 1 since
  it represents a polynomial in R_{n+m}[X].

# Remark
* the middle product formula is in [1]
* mulTmid(⋅,P,n) : R_{n+m}[X] ⟶ R_n[X]

# References
* [1] : G. Hanrot, M. Quercia, and P. Zimmerman. The middle product algorithm I.
Appl. Algebra Engrg. Comm. Comput., 14(6):415-438, 2004.
"""
function mulTmid{T}(c::Array{T,1},P::PolyElem{T},n::Int64)

  # We first check if the elements of b and coefficients
  # of P belong to the same ring
  m::Int64=degree(P)
  k::Nemo.Ring=base_ring(P)
  @assert k==parent(c[1])

  # And we compute the middle product of P and c
  R::Nemo.Ring=parent(P)
  C::PolyElem{T}=R(c)
  Q::PolyElem{T}=reverse(P,m+1)
  return shift_right(mullow(Q,C,n+m+1),m)
end

export remT

"""
    remT{T}(r::Array{T,1},P::PolyElem{T},n::Int64)

Transposition of the remainder by P algorithm.

An other linear extension algorithm. Take the r first values of a linear
recurring sequence of minimal polynomial P and compute the n first values.
"""
function remT{T}(r::Array{T,1},P::PolyElem{T},n::Int64)

  # We first check if the elements of b and coefficients
  # of P belong to the same ring
  m::Int64=degree(P)
  k::Nemo.Ring=base_ring(P)
  @assert k==parent(r[1])

  # We compute the ring K = k[t]
  K::Nemo.Ring=parent(P)
  t::PolyElem{T}=gen(K)

  # We compute α = 1/rev(P, m+1) mod t^(n-m)
  R::PolyElem{T}=K(T[r[i] for i in 1:m]) # useless creation of a list... memory !
  α::PolyElem{T}=reverse(P,m+1)
  α=gcdinv(α,t^(n-m))[2]

  # We add zeros to match the awaited size
  b::Array{T,1}=copy(r) # copy + push! : cost ?
  while length(b)<n
    push!(b,k(0))
  end

  # We compute the final result
  ans = R-shift_left((mullow(α,mulTmid(b,P,n-m),n-m)),m)
  return T[coeff(ans,j) for j in 0:n-1]
end

export remT_pre
function remT_pre{T}(r::Array{T,1},P::PolyElem{T},n::Int64,TP::PolyElem{T})

  # We first check if the elements of b and coefficients
  # of P belong to the same ring
  m::Int64=degree(P)
  k::Nemo.Ring=base_ring(P)
  @assert k==parent(r[1])

  # We compute the ring K = k[t]
  K::Nemo.Ring=parent(P)
  t::PolyElem{T}=gen(K)

  # We compute α = 1/rev(P, m+1) mod t^(n-m)
  R::PolyElem{T}=K(T[r[i] for i in 1:m]) # useless creation of a list... memory !
  α::PolyElem{T}=TP % t^(n-m)

  # We add zeros to match the awaited size
  b::Array{T,1}=copy(r) # copy + push! : cost ?
  while length(b)<n
    push!(b,k(0))
  end

  # We compute the final result
  ans = R-shift_left((mullow(α,mulTmid(b,P,n-m),n-m)),m)
  return T[coeff(ans,j) for j in 0:n-1]
end

export remTnaif

"""
    remTnaif{T}(r::Array{T,1},P::PolyElem{T},n::Int64)

Linear extension algorithm.

Take the m first elements of a Linear recurring sequence with minimal
polynomial P and compute the n first.
"""
function remTnaif{T}(r::Array{T,1},P::PolyElem{T},n::Int64)
  m::Int64=degree(P)
  p::Array{T,1}=T[coeff(P,j) for j in 0:m]
  b::Array{T,1}=copy(r)
  while length(b)<n
      s=sum([-1*p[j]*b[end-m+j] for j in 1:m])
      push!(b,p[end]^(-1)*s)
  end
  return b
end

export mulModT

"""
    mulModT{T}(P::Array{T,1},Q::PolyElem{T},R::PolyElem{T},n::Int64)

Transposition of the modular multiplication.

# Arguments
* P::Array{T,1} represents a polynomial of degree r - 1, so is of size r (where r=deg(R)) ;
* Q::PolyElem{T} is the fixed polynomial used in mul(⋅,Q) : P ⟼ PQ ;
* R::PolyElem{T} is the polynomial used in the remainder rem(⋅,R) : P ⟼ P mod R ;
* n::Int64 is the degree of the output (at most).

# Remark
* mulModT(⋅,Q,R,n) : R_{r-1}[X] ⟶ R_n[X].
"""
function mulModT{T}(P::Array{T,1},Q::PolyElem{T},R::PolyElem{T},n::Int64)
  q,r=degree(Q),degree(R)
  a = remT(P,R,n+q+1)
  return mulTmid(a,Q,n)
end

export mulModT_pre
function mulModT_pre{T}(P::Array{T,1},Q::PolyElem{T},R::PolyElem{T},n::Int64,TR::PolyElem{T})
  q,r=degree(Q),degree(R)
  a = remT_pre(P,R,n+q+1,TR)
  return mulTmid(a,Q,n)
end

export embed

"""
    embed{T}(b::Array{T,1},P::PolyElem{T},c::Array{T,1},Q::PolyElem{T},r::Int64=0)

Compute the embeding of Π={bc | b ∈ k[x]/(P) , c ∈ k[y]/(Q)} ⊂ k[x,y]/(P,Q) in k[z]/(R).
"""
function embed{T}(b::Array{T,1},P::PolyElem{T},c::Array{T,1},Q::PolyElem{T},r::Int64=0)
  if r==0
    r=length(b)*length(c)
  end
  t::Array{T,1}=remT(b,P,r)
  u::Array{T,1}=remT(c,Q,r)
  return T[t[j]*u[j] for j in 1:r]
end

export berlekampMassey

"""
    berlekampMassey{T <: RingElem}(a::Array{T,1},n::Int64,S=0)

Compute the minimal polynomial of a linear recurring sequence.
"""
function berlekampMassey{T <: RingElem}(a::Array{T,1},n::Int64,S=0)
  if S==0
    k::Nemo.Ring=parent(a[1])
    S::Nemo.Ring,x::PolyElem{T}=PolynomialRing(k,"x")
  else
    x=gen(S)
  end
  m::Int64=2*n-1
  R0::PolyElem{T}=x^(2*n)
  R1::PolyElem{T}=S(reverse(a))
  V0::PolyElem{T}=S(0)
  V1::PolyElem{T}=S(1)
  while n<=degree(R1)
    Q::PolyElem{T},R::PolyElem{T}=divrem(R0,R1)
    V::PolyElem{T}=V0-Q*V1
    V0=V1
    V1=V
    R0=R1
    R1=R
  end
  return V1*lead(V1)^(-1)
end

export computeR

"""
    computeR{T}(P::PolyElem{T},Q::PolyElem{T})

Compute the composed product R of P and Q.
"""
function computeR{T}(P::PolyElem{T},Q::PolyElem{T})
  m::Int64=degree(P)
  n::Int64=degree(Q)
  k::Nemo.Ring=base_ring(P)

  up::Array{T,1}=monomialToDual([k(1)],P)
  uq::Array{T,1}=monomialToDual([k(1)],Q)

  t::Array{T,1}=embed(up,P,uq,Q,2*m*n)

  return berlekampMassey(t,m*n,parent(P))
end

export project

"""
    project{T}(a::Array{T,1},P::PolyElem{T},Q::PolyElem{T})

Compute the section of the embedding k[x]/(P) ⟶ k[z]/(R), where R = P ⊙ Q.
"""
function project{T}(a::Array{T,1},P::PolyElem{T},Q::PolyElem{T})
  n::Int64=degree(Q)
  m::Int64=degree(P)
  c::Array{T,1}=Array(T,n)
  k::Nemo.Ring=base_ring(Q)
  c[1]=k(1)
  for j in 2:n
    c[j]=k(0)
  end
  u::Array{T,1}=remT(c,Q,m*n) # it seems to be the only thing expensive
  K::Nemo.Ring=parent(P)
  d::PolyElem{T}=K([a[j]*u[j] for j in 1:(m*n)])%P
  return T[coeff(d,j) for j in 0:(m-1)]
end

export phi1

"""
    phi1{T}(b::Array{T,2},P::PolyElem{T},Q::PolyElem{T})

Compute the isomorphism Φ : k[x,y]/(P,Q) ⟶ k[z]/(R).

# Arguments
* b::Array{T,2} is a 2d array reprenting (b_ji) where 0 <= i < m and 0 <= j < n, it is
the transpose of the b_ij of the paper because we prefer to extract columns than lines for
types reasons.
"""
function phi1{T}(b::Array{T,2},P::PolyElem{T},Q::PolyElem{T})
  k::Nemo.Ring=parent(b[1,1])
  @assert k==base_ring(Q)
  m::Int64=degree(P)
  n::Int64=degree(Q)
  u::Array{T,1}=remT(monomialToDual(T[k(1)],P),P,m*(n+1)-1)
  a::Array{T,1}=Array(T,m*n)
  for j in 1:m*n
    a[j]=k(0)
  end
  for i in 1:m
    t::Array{T,1}=remT(b[:,i],Q,m*n)
    for j in 1:m*n
      a[j]=a[j]+t[j]*u[i+j-1] # /!\ indices
    end
  end
  return a
end

export phi1_pre
function phi1_pre{T}(b::Array{T,2},P::PolyElem{T},Q::PolyElem{T}, up::Array{T,1})
  k::Nemo.Ring=parent(b[1,1])
  @assert k==base_ring(Q)
  m::Int64=degree(P)
  n::Int64=degree(Q)
  u::Array{T,1}=remT(up,P,m*(n+1)-1)
  a::Array{T,1}=Array(T,m*n)
  for j in 1:m*n
    a[j]=k(0)
  end
  for i in 1:m
    t::Array{T,1}=remT(b[:,i],Q,m*n)
    for j in 1:m*n
      a[j]=a[j]+t[j]*u[i+j-1] # /!\ indices
    end
  end
  return a
end

export inversePhi1

"""
    inversePhi1{T}(a::Array{T,1},P::PolyElem{T},Q::PolyElem{T})

Compute Φ^(-1) : k[z]/(R) ⟶ k[x,y]/(P,Q).

# Output
* b::Array{T,2} is a 2d array reprenting (b_ji) where 0 <= i < m and 0 <= j < n, it is
the transpose of the b_ij of the paper because we prefer to extract columns than lines for
types reasons.
"""
function inversePhi1{T}(a::Array{T,1},P::PolyElem{T},Q::PolyElem{T})
  k::Nemo.Ring=parent(a[1,1])
  @assert k==base_ring(Q)
  K::Nemo.Ring=parent(Q)
  y::PolyElem{T}=gen(K)
  m::Int64=degree(P)
  n::Int64=degree(Q)
  b::Array{T,2}=Array(T,(n,m))
  u::Array{T,1}=remT(monomialToDual(T[k(1)],P),P,m*(n+1)-1)
  for i in m:-1:1
    d::PolyElem{T}=K([a[j]*u[i+j-1] for j in 1:(m*n)])%Q
    for j in 1:n
    b[j,i]=coeff(d,j-1)
    end
  end
  return b
end

export inversePhi1_pre
function inversePhi1_pre{T}(a::Array{T,1},P::PolyElem{T},Q::PolyElem{T}, up::Array{T,1})
  k::Nemo.Ring=parent(a[1,1])
  @assert k==base_ring(Q)
  K::Nemo.Ring=parent(Q)
  y::PolyElem{T}=gen(K)
  m::Int64=degree(P)
  n::Int64=degree(Q)
  b::Array{T,2}=Array(T,(n,m))
  u::Array{T,1}=remT(up,P,m*(n+1)-1)
  for i in m:-1:1
    d::PolyElem{T}=K([a[j]*u[i+j-1] for j in 1:(m*n)])%Q
    for j in 1:n
    b[j,i]=coeff(d,j-1)
    end
  end
  return b
end

export phi2

"""
    phi2{T}(b::Array{T,2},P::PolyElem{T},Q::PolyElem{T})

Compute the isomorphism Φ : k[x,y]/(P,Q) ⟶ k[z]/(R).

# Argument
* This time b::Array{T,2} is the same as the one in the text.
"""
function phi2{T,Y}(b::Array{T,2},P::Y,Q::Y,R::Y)
  k::Nemo.Ring=parent(b[1,1])
  @assert k==base_ring(P)
  K::Nemo.Ring=parent(P)
  z=gen(K)
  m::Int64=degree(P)
  n::Int64=degree(Q)
  N::Int64=n+m-1
  p::Int64=ceil(sqrt(N))
  q::Int64=ceil(N/p)
  y::Array{T,1}=monomialToDual(T[k(0),k(1)],Q)
  up::Array{T,1}=monomialToDual(T[k(1)],P)
  S::Y=K(dualToMonomial(embed(up,P,y,Q),R))
  U::Y=gcdinv(S,R)[2]

  Sprime::Array{Y,1}=Array(Y,q+1)
  Sprime[1]=K(1)

  for i in 2:(q+1)
    Sprime[i]=mulmod(Sprime[i-1],S,R)
  end

  MT::Nemo.Ring=MatrixSpace(K,q,n)
  mt::MatElem=MT()

  for i in 1:q
    c::Array{T,1}=T[coeff(Sprime[i],h) for h in 0:(m*n-1)]
    for j in 1:n
      mt[i,j]=K(c[(j-1)*m+1:j*m])
    end
  end

  MC::Nemo.Ring=MatrixSpace(K,p,q)
  mc::MatElem=MC()

  for i in 1:p
    for j in 1:q
      mc[i,j]=K(T[h+i*q+j-m-2 < 1 ? k(0) : h+i*q+j-m-2 > n ? k(0) : b[h,h+i*q+j-m-2] for h in 1:m])
    end
  end

  Mv::MatElem=mc*mt

  V::Array{Y,1}=Array(Y,p)
  for i in 1:p
    V[i]=sum([shift_left(Mv[i,j],(j-1)*m) for j in 1:n])%R
  end

  a::Y=K()

  for i in p:-1:1
    a=(Sprime[q+1]*a+V[i])%R
  end

  a=mulmod(a,U^(m-1),R)

  return T[coeff(a,i) for i in 0:(m*n-1)]
end

export phi2_pre
function phi2_pre{T,Y}(b::Array{T,2},P::Y,Q::Y,R::Y, up::Array{T,1})
  k::Nemo.Ring=parent(b[1,1])
  @assert k==base_ring(P)
  K::Nemo.Ring=parent(P)
  z=gen(K)
  m::Int64=degree(P)
  n::Int64=degree(Q)
  N::Int64=n+m-1
  p::Int64=ceil(sqrt(N))
  q::Int64=ceil(N/p)
  y::Array{T,1}=monomialToDual(T[k(0),k(1)],Q)
  S::Y=K(dualToMonomial(embed(up,P,y,Q),R))
  U::Y=gcdinv(S,R)[2]

  Sprime::Array{Y,1}=Array(Y,q+1)
  Sprime[1]=K(1)

  for i in 2:(q+1)
    Sprime[i]=mulmod(Sprime[i-1],S,R)
  end

  MT::Nemo.Ring=MatrixSpace(K,q,n)
  mt::MatElem=MT()

  for i in 1:q
    c::Array{T,1}=T[coeff(Sprime[i],h) for h in 0:(m*n-1)]
    for j in 1:n
      mt[i,j]=K(c[(j-1)*m+1:j*m])
    end
  end

  MC::Nemo.Ring=MatrixSpace(K,p,q)
  mc::MatElem=MC()

  for i in 1:p
    for j in 1:q
      mc[i,j]=K(T[h+i*q+j-m-2 < 1 ? k(0) : h+i*q+j-m-2 > n ? k(0) : b[h,h+i*q+j-m-2] for h in 1:m])
    end
  end

  Mv::MatElem=mc*mt

  V::Array{Y,1}=Array(Y,p)
  for i in 1:p
    V[i]=sum([shift_left(Mv[i,j],(j-1)*m) for j in 1:n])%R
  end

  a::Y=K()

  for i in p:-1:1
    a=(Sprime[q+1]*a+V[i])%R
  end

  a=mulmod(a,U^(m-1),R)

  return T[coeff(a,i) for i in 0:(m*n-1)]
end

export inversePhi2

"""
    inversePhi2{T}(a::Array{T,1},P::Y,Q::Y)

Compute Φ^(-1) : k[z]/(R) ⟶ k[x,y]/(P,Q).

# Output
* This time b::Array{T,2} is the same as the one in the text.
"""
function inversePhi2{T,Y}(a::Array{T,1},P::Y,Q::Y,R::Y)
  k::Nemo.Ring=parent(a[1])
  @assert k==base_ring(P)
  K::Nemo.Ring=parent(P)
  m::Int64=degree(P)
  n::Int64=degree(Q)
  N::Int64=n+m-1
  p::Int64=ceil(sqrt(N))
  q::Int64=ceil(N/p)
  y::Array{T,1}=monomialToDual(T[k(0),k(1)],Q)
  up::Array{T,1}=monomialToDual(T[k(1)],P)
  S::Y=K(dualToMonomial(embed(up,P,y,Q),R))
  U::Y=gcdinv(S,R)[2]

  Sprime::Array{Y,1}=Array(Y,q+1)
  Sprime[1]=K(1)

  for i in 2:(q+1)
    Sprime[i]=mulmod(Sprime[i-1],S,R)
  end

  MT::Nemo.Ring=MatrixSpace(K,n,q)
  mt::MatElem=MT()

  for i in 1:q
    c::Array{T,1}=T[coeff(Sprime[i],h) for h in 0:(m*n-1)]
    for j in 1:n
      mt[j,i]=reverse(K(c[(j-1)*m+1:j*m]),m) # reverse in order to do transposed product
    end
  end

  u::Y=powmod(U,m-1,R)
  W::Y=mulTmid(remT(a,R,2*m*n-1),u,m*n-1)
  a=T[coeff(W,j) for j in 0:(m*n-1)]

  V::Array{Array{T,1},1}=Array(Array{T,1},p)
  A::Y=K()

  for i in 1:p
    V[i]=remT(a,R,m*n+m-1)
    A=mulTmid(remT(a,R,2*m*n-1),Sprime[q+1],m*n-1) # That's the thing taking time !!!
    a=T[coeff(A,j) for j in 0:(m*n-1)]
  end

  MV::Nemo.Ring=MatrixSpace(K,p,n)
  mv::MatElem=MV()

  for i in 1:p, j in 1:n
    mv[i,j]=K(V[i][(j-1)*m+1:(j-1)*m+2*m-1])
  end

  mc::MatElem=mv*mt
  cc::Array{Y,1}=Array(Y,p*q)

  for i in 1:p, j in 1:q
    cc[(i-1)*q+j]=shift_right(truncate(mc[i,j],2*m-1),m-1)
  end

  return T[coeff(cc[j-i+m],i-1) for i in 1:m, j in 1:n]
end

export inversePhi2_pre
function inversePhi2_pre{T,Y}(a::Array{T,1},P::Y,Q::Y,R::Y,up::Array{T,1})
  k::Nemo.Ring=parent(a[1])
  @assert k==base_ring(P)
  K::Nemo.Ring=parent(P)
  m::Int64=degree(P)
  n::Int64=degree(Q)
  N::Int64=n+m-1
  p::Int64=ceil(sqrt(N))
  q::Int64=ceil(N/p)
  y::Array{T,1}=monomialToDual(T[k(0),k(1)],Q)
  S::Y=K(dualToMonomial(embed(up,P,y,Q),R))
  U::Y=gcdinv(S,R)[2]

  Sprime::Array{Y,1}=Array(Y,q+1)
  Sprime[1]=K(1)

  for i in 2:(q+1)
    Sprime[i]=mulmod(Sprime[i-1],S,R)
  end

  MT::Nemo.Ring=MatrixSpace(K,n,q)
  mt::MatElem=MT()

  for i in 1:q
    c::Array{T,1}=T[coeff(Sprime[i],h) for h in 0:(m*n-1)]
    for j in 1:n
      mt[j,i]=reverse(K(c[(j-1)*m+1:j*m]),m) # reverse in order to do transposed product
    end
  end

  u::Y=powmod(U,m-1,R)
  W::Y=mulTmid(remT(a,R,2*m*n-1),u,m*n-1)
  a=T[coeff(W,j) for j in 0:(m*n-1)]

  V::Array{Array{T,1},1}=Array(Array{T,1},p)
  A::Y=K()

  for i in 1:p
    V[i]=remT(a,R,m*n+m-1)
    A=mulTmid(remT(a,R,2*m*n-1),Sprime[q+1],m*n-1) # That's the thing taking time !!!
    a=T[coeff(A,j) for j in 0:(m*n-1)]
  end

  MV::Nemo.Ring=MatrixSpace(K,p,n)
  mv::MatElem=MV()

  for i in 1:p, j in 1:n
    mv[i,j]=K(V[i][(j-1)*m+1:(j-1)*m+2*m-1])
  end

  mc::MatElem=mv*mt
  cc::Array{Y,1}=Array(Y,p*q)

  for i in 1:p, j in 1:q
    cc[(i-1)*q+j]=shift_right(truncate(mc[i,j],2*m-1),m-1)
  end

  return T[coeff(cc[j-i+m],i-1) for i in 1:m, j in 1:n]
end

println("FastArithmetic comes with even less warranty\n")

end


"""
    MultivariateGamma(α,θ)

Construct a distribution that correspond to several comonotonous gammas with common shapes α and respectives scales θ[i].
The distribution can then be used through several methods,
following the Distributions.jl standard, mainly to obtain samples.

The code is type stable and handles any <:Real types, given by the parameters.

Note that the supprot of this distribution is a half-line in the d-dimensional space, [0,+∞], with angle given by θ.

# Examples
```julia-repl
julia> dist = MultivariateGamma(2,[3,4,5]);
julia> sample = zeros(Float64,(3,10));
julia> Random.rand!(dist,sample);
```
"""
struct MultivariateGamma{T<:Real, V<:AbstractVector{T}, M<:Distributions.Gamma{T} } <: Distributions.ContinuousMultivariateDistribution where T
    α::T
    θ::V
    Γα::M
end

#### Constructor :
function MultivariateGamma(α::T1,θ::AbstractVector{T2}) where {T1 <: Real, T2<:Real}
     T = Base.promote_eltype([α],θ,[1.0])
     α = T(α)
     θ = T.(θ)
     MultivariateGamma(α,θ,Distributions.Gamma(α,T(1)))
end
MultivariateGamma(α::T1,θ::T2) where {T1<:Real, T2<:Real} = Distributions.Gamma(promote(α,θ)...)

#### eltype, length, support
Base.eltype(d::MultivariateGamma) = typeof(d.α)
Base.length(d::MultivariateGamma) = length(d.θ)
function Distributions.insupport(d::MultivariateGamma,x::AbstractVector{T}) where {T <: Real}
    # for theta that are 0, x must be 0
    # for other theta, x/theta must be a constant.
    j = 0
    for i in 1:length(x)
        if d.θ[i] == T(0)
            if not x[i] == T(0)
                return false
            end
        else
            j = i
        end
    end
    if j == 0
        # then all theta are 0 and all x are 0
        return true
    end
    constant = x[j]/d.θ[j]
    for i in 1:length(x)
        if d.θ[i] != 0
            # for stability issue, we doubled the tolerence of the type. Otherwise even simulations are not inbounds, which causes pdf == 0 on simulated data !
            if !isapprox(x[i]/d.θ[i],constant,atol=2*eps(T),rtol=T(0))
                return false
            end
        end
    end
    return true
end

#### Sampling
struct MGSPL <: Distributions.Sampleable{Distributions.Multivariate,Distributions.Continuous}
    α
    θ
    Γα
end
Distributions.sampler(d::MultivariateGamma) = MGSPL(d.α,d.θ,d.Γα)
Base.length(s::MGSPL) = length(s.θ)
function Distributions._rand!(rng::Distributions.AbstractRNG, s::MGSPL, x::AbstractVector{T}) where T<:Real
    x .= s.θ .* rand(rng,s.Γα) # simulate a gamma(alpha,1) and multiply by theta
end

#### Logpdf
function Distributions._logpdf(d::MultivariateGamma{T}, x::AbstractArray) where T<:Real
    if !insupport(d,x)
        return T(-Inf)
    end
    for i in 1:length(x)
        if d.θ[i] != 0
            return log(Distributions.pdf(d.Γα,x[i]/d.θ[i])/d.θ[i])
        end
    end
    print("we go there")
    return all(x .== T(0)) ? T(0) : T(-Inf) # if all theta are zero, then the distribution is a diract in (0,...,0)
end

mean(d::MultivariateGamma) = d.α .* d.θ
var(d::MultivariateGamma) = d.α .* d.θ .* d.θ
Statistics.cov(d::MultivariateGamma) = d.α .* d.θ[na,:] .* d.θ[:,na]

# # Okay this should be enough to obtain a pdf
#
# dist = MultivariateGamma(2,[3,4,5])
# sample = zeros(Float64,(3,10))
# import Random
# Random.rand!(dist,sample)
# display(sample)
#
#
# display(Distributions.pdf(dist,sample))
# display(Distributions.pdf(Distributions.Gamma(dist.α,dist.θ[1]),sample[1,:]))


using Distributions, DiffEqBayes

println("Starting the test")
@test DiffEqBayes.stan_string(Bernoulli(1)) == "bernoulli(1.0)"
@test DiffEqBayes.stan_string(Bernoulli) == "bernoulli"

@test DiffEqBayes.stan_string(Binomial(5,0.3)) == "binomial(5, 0.3)"
@test DiffEqBayes.stan_string(Binomial) == "binomial"

@test DiffEqBayes.stan_string(BetaBinomial(5,1,1)) == "beta_binomial(5, 1.0, 1.0)"
@test DiffEqBayes.stan_string(BetaBinomial) == "beta_binomial"

@test DiffEqBayes.stan_string(Hypergeometric(5,5,3)) == "hypergeometric(5, 5, 3)"
@test DiffEqBayes.stan_string(Hypergeometric) == "hypergeometric"

@test DiffEqBayes.stan_string(NegativeBinomial(5,0.3)) == "neg_binomial(5.0, 0.3)"
@test DiffEqBayes.stan_string(NegativeBinomial) == "neg_binomial"

@test DiffEqBayes.stan_string(Poisson(5)) == "poisson(5.0)"
@test DiffEqBayes.stan_string(Poisson) == "poisson"

@test DiffEqBayes.stan_string(Normal(0,1)) == "normal(0.0, 1.0)"
@test DiffEqBayes.stan_string(Normal) == "normal"

@test DiffEqBayes.stan_string(TDist(5)) == "student_t(5.0,0,1)"
@test DiffEqBayes.stan_string(TDist) == "student_t"

@test DiffEqBayes.stan_string(Cauchy(0,1)) == "cauchy(0.0, 1.0)"
@test DiffEqBayes.stan_string(Cauchy) == "cauchy"

@test DiffEqBayes.stan_string(Laplace(0,1)) == "double_exponential(0.0, 1.0)"
@test DiffEqBayes.stan_string(Laplace) == "double_exponential"

@test DiffEqBayes.stan_string(Distributions.Logistic(0,1)) == "logistic(0.0, 1.0)"
@test DiffEqBayes.stan_string(Distributions.Logistic) == "logistic"

@test DiffEqBayes.stan_string(Gumbel(0,1)) == "gumbel(0.0, 1.0)"
@test DiffEqBayes.stan_string(Gumbel) == "gumbel"

@test DiffEqBayes.stan_string(LogNormal(0,1)) == "lognormal(0.0, 1.0)"
@test DiffEqBayes.stan_string(LogNormal) == "lognormal"

@test DiffEqBayes.stan_string(Chisq(5)) == "chi_square(5.0)"
@test DiffEqBayes.stan_string(Chisq) == "chi_square"

@test DiffEqBayes.stan_string(Exponential(5)) == "exponential(5.0)"
@test DiffEqBayes.stan_string(Exponential) == "exponential"

@test DiffEqBayes.stan_string(Gamma(2,3)) == "gamma(2.0, 3.0)"
@test DiffEqBayes.stan_string(Gamma) == "gamma"

@test DiffEqBayes.stan_string(InverseGamma(2,3)) == "inv_gamma(2.0, 3.0)"
@test DiffEqBayes.stan_string(InverseGamma) == "inv_gamma"

@test DiffEqBayes.stan_string(Weibull(1,1)) == "weibull(1.0, 1.0)"
@test DiffEqBayes.stan_string(Weibull) == "weibull"

@test DiffEqBayes.stan_string(Frechet(1,1)) =="frechet(1.0, 1.0)"
@test DiffEqBayes.stan_string(Frechet) == "frechet"

@test DiffEqBayes.stan_string(Rayleigh(5)) == "rayleigh(5.0)"
@test DiffEqBayes.stan_string(Rayleigh) == "rayleigh"

@test DiffEqBayes.stan_string(Pareto(2,3)) == "pareto(2.0, 3.0)"
@test DiffEqBayes.stan_string(Pareto) == "pareto"

@test DiffEqBayes.stan_string(GeneralizedPareto(0,1,2)) == "pareto_type_2(0.0, 1.0, 2.0)"
@test DiffEqBayes.stan_string(GeneralizedPareto) == "pareto_type_2"

@test DiffEqBayes.stan_string(Beta(3,3)) == "beta(3.0, 3.0)"
@test DiffEqBayes.stan_string(Beta) == "beta"

@test DiffEqBayes.stan_string(Uniform(1,4)) =="uniform(1.0, 4.0)"
@test DiffEqBayes.stan_string(Uniform) == "uniform"

@test DiffEqBayes.stan_string(VonMises(0,2)) == "von_mises(0.0, 2.0)"
@test DiffEqBayes.stan_string(VonMises) == "von_mises"


#=
Written by Rasmus Kyng.
Based closely on Matlab code by Anup Rao, Sushant Sachdeva & Rasmus Kyng.
Matlab code available at https://github.com/sachdevasushant/Isotonic

Interior point method for computing the Isotonic regression (under Euclidean norm) on arbitrary directed acyclic graphs. The code accompanies the paper 'Fast, Provable Algorithms for Isotonic Regression in all l_p-norms' at NIPS 2015

This repository presently contains code for computing isotonic regression in directed acyclic graphs (DAG).

isotonicIPM takes as input the adjacency matrix ‘A’ of a DAG, followed by a vector ‘v’ giving the initial values on the vertices. v should be a vector of length equal to the number of vertices.

On input ‘A’ and ‘v’, the code computes a vector ‘x’ of same length as v, such that x minimizes the quantity: sum_i (x(i) - v(i))^2 subject to x(i) <= x(j) if (i,j) is an entry in ‘a’.

=#

"""Compute isotonic regression of v with constraints given by A,
and multiplicative error param eps,
using a PD SDD linear system solver, given by argument *solver*."""
function isotonicIPMrelEps(A::SparseMatrixCSC{Tv,Ti},
                           v::Array{Tv,1},
                           eps::Real=0.1,
                           solver=(H -> augTreeSddm(H,tol=1e-1,maxits=1000))) where {Tv,Ti}
    return isotonicIPM(A,v,eps,solver,true)
end

"""Compute isotonic regression of v with constraints given by A,
and additive error param eps,
using a PD SDD linear system solver, given by argument *solver*.
Setting relGapTermination=true means the IPM continues until a multiplicative error of eps is reached."""
function isotonicIPM(A::SparseMatrixCSC{Tv,Ti},
                     v::Array{Tv,1},
                     eps::Tv=0.1,
                     solver=(H -> augTreeSddm(H,tol=1e-1,maxits=1000)),
                     relGapTermination::Bool=false) where {Tv,Ti}
    n = size(A)[1]
    topoOrder = toposort(A)
    permMat = (sparse(I,n,n))[1:n,topoOrder]
    Bt = -dirEdgeVertexMat(A) #sign convention implies all edges should get positive values
    m = size(Bt)[1]

    Theta = m
    eps0 = eps
    beta2 = 10.0

    x = collect(1.0:n) # our initial guess
    x = permMat*x #this seems to be the right direction for permMat

    if notFeasible(x, Bt)
        error("Input graph is not a DAG!")
    end

    eTaStartFn(y) = ( (y-v)'*Bt'*(1 ./ (Bt*y))/norm(y-v)^2 )[1]
    #This seems to be the formula for the minimizer?

    #TODO
    #eTaStartFn(y) = ( (y-v)'*Bt'*(1./(Bt*y))/norm(y-v) )[1]
    #This is the formula Anup used?
    #Seems to give better results?

    eTa1 = eTaStartFn(x)
    #@printf "eTa1 = %f\n" eTa1
    while eTa1<0
        x = 2*x
        eTa1 = eTaStartFn(x)
        #Note: y'*Bt'*(1./(Bt*y)) is always pos when y is isotonic,
        #We can show this loop will complete
        #println("doubling")
    end

    #@printf "eTa1 = %f\n" eTa1

    if eTa1 < 0
        error("bad initialization!")
    end
    eTa2 = Theta/eps0
    mu0 = eTa1

    relGap = Inf
    i=0;
    muTooSmall = mu0 < eTa2

    # numSteps = 0

    while muTooSmall
        F(y) = mu0*norm(y-v)^2 - sum(log.(Bt*y))
        centered = false
        while !centered
            # numSteps += 1
            # if numSteps % 30 == 1
            #     println("So far ", numSteps, " steps were made")
            # end

            (H,xNewton) = l2NewtonStep( Bt,x,v,mu0, solver )

            s = Bt*x
            gradF = 2*mu0*(x-v) - Bt'*(1 ./ s)
            x = backtrackLineSearch(F,gradF,Bt,xNewton,x)
            centMeasure = (xNewton'*H*xNewton)[1]
            centered = centMeasure < 10.0^-2
            i = i+1
        end

        dval = dualVal(Bt,x,v,mu0)
        relGap = (sum((x-v).^2) - dval)/sum((x-v).^2)
        if relGapTermination
            mu0 = mu0*beta2
            if relGap < eps
                break
            end
            #muTooSmall stays false: we only terminate if relGap is good
        else
            #this is the old termination condition:
            #stop if within 1 % relGap or if eta is too large?
            if relGap < 10.0^-2
                break
            end
            muTooSmall = mu0 < eTa2
            if muTooSmall
                mu0 = mu0*beta2
            end
        end
    end

    if notFeasible(x, Bt)
        error("output is not isotonic!")
    end

    return (x,relGap,i)
end

function l2NewtonStep( Bt::SparseMatrixCSC{Tv,Ti},
                       x::Array{Tv,1},
                       v::Array{Tv,1},
                       mu0::Real,
                       solver ) where {Tv,Ti}
    m = size(Bt)[1]
    n = size(Bt)[2]
    d1 = 2 * mu0 * sparse(I,n,n)
    s = Bt * x
    d2 = sparse(1:m, 1:m, s .^ -2, m, m)
    grad = 2 * mu0 * (x - v) - Bt'*(1 ./ s)
    H1 = sparse(Bt'*d2*Bt)

    H = H1 + d1

    invMinEntryH = maximum(abs.(s)) ^ 2

    #println("Maximum eigenvalue of the matrix we are computing: ", eigs(invMinEntryH * H;nev=1,which=:LM,tol=1e-1)[1][1])
    #println("Smallest eigenvalues of the matrix we are computing: ", eigs(invMinEntryH * H;nev=2,which=:SM,tol=1e-1)[1][1])
    F = solver(invMinEntryH * H)
    xNewton = -F(grad)*invMinEntryH

    return (H,xNewton)
end

function dualVal( Bt::SparseMatrixCSC{Tv,Ti},
                  x::Array{Tv,1},
                  v::Array{Tv,1},
                  mu0::Real) where {Tv,Ti}
    s = Bt*x # all pos entries
    f = 1/mu0 * 1 ./ s
    dval = (-1/4 * sum((Bt'*f).^2) - f'*Bt*v)[1]
    return dval
end

function backtrackLineSearch(F,
                      gradF::Array{Tv,1},
                      Bt::SparseMatrixCSC{Tv,Ti},
                      xNewton::Array{Tv,1},
                      x::Array{Tv,1}) where {Tv,Ti}
    #track line search as in Boyd's book
    A0 = 0.01;
    B0 = 0.5;

    t = 1;
    iter = 0;

    xt = x + t * xNewton;

    while (notFeasible(xt, Bt)) || (F(xt) > F(x) + A0 * t * (gradF'*xNewton)[1])
        t = B0 * t;
        iter = iter + 1;
        if iter >= 100
            error("Number of iterations in line search exceeded the limit")
        end
        xt = x + t * xNewton;
    end

    return xt
end

function notFeasible(x::Array{Tv,1},
              Bt::SparseMatrixCSC{Tv,Ti}) where {Tv,Ti}
    return minimum(Bt * x) <= 0 #not feasible if some entry is non-positive
end


"""
    lanczos(;input::AbstractString="", outdir::Union{Nothing,AbstractString}=nothing, scale::AbstractString="ftt", pseudocount::Number=1f0, rowmeanlist::AbstractString="", rowvarlist::AbstractString="",colsumlist::AbstractString="", dim::Number=3, numepoch::Number=10, perm::Bool=false, cper::Number=1f0)

Lanczos method.

Input Arguments
---------
- `input` : Julia Binary file generated by `OnlinePCA.csv2bin` function.
- `outdir` : The directory specified the directory you want to save the result.
- `scale` : {log,ftt,raw}-scaling of the value.
- `pseudocount` : The number specified to avoid NaN by log10(0) and used when `Feature_LogMeans.csv` <log10(mean+pseudocount) value of each feature> is generated.
- `rowmeanlist` : The mean of each row of matrix. The CSV file is generated by `OnlinePCA.sumr` functions.
- `rowvarlist` : The variance of each row of matrix. The CSV file is generated by `OnlinePCA.sumr` functions.
- `colsumlist` : The sum of counts of each columns of matrix. The CSV file is generated by `OnlinePCA.sumr` functions.
- `dim` : The number of dimension of PCA.
- `perm` : Whether the data matrix is shuffled at random.
- `cper` : Count per X (e.g. CPM: Count per million <1e+6>)

Output Arguments
---------
- `W` : Eigen vectors of covariance matrix (No. columns of the data matrix × dim)
- `λ` : Eigen values (dim × dim)
- `V` : Loading vectors of covariance matrix (No. rows of the data matrix × dim)
- `Scores` : Principal component scores
- `ExpVar` : Explained variance by the eigenvectors
- `TotalVar` : Total variance of the data matrix
- stop : Whether the calculation is converged
"""
function lanczos(;input::AbstractString="", outdir::Union{Nothing,AbstractString}=nothing, scale::AbstractString="ftt", pseudocount::Number=1f0, rowmeanlist::AbstractString="", rowvarlist::AbstractString="",colsumlist::AbstractString="", dim::Number=3, numepoch::Number=10, perm::Bool=false, cper::Number=1f0)
    # Initial Setting
    pca = LANCZOS()
    N, M = nm(input)
    pseudocount, W, X, D, rowmeanvec, rowvarvec, colsumvec, N, M, TotalVar = init(input, pseudocount, dim, rowmeanlist, rowvarlist, colsumlist, pca, numepoch, cper, scale)
    T = zeros(Float32, numepoch, numepoch)
    tmpN = zeros(UInt32, 1)
    tmpM = zeros(UInt32, 1)
    x = zeros(UInt32, M)
    normx = zeros(Float32, M)
    # If not 0 the calculation is converged
    stop = 0
    k = 1
    n = 1
    s = 0
    # Each epoch s
    progress = Progress(numepoch)
    while(stop == 0 && k <= numepoch)
        v = zeros(Float32, M)
        open(input) do file
            stream = ZstdDecompressorStream(file)
            read!(stream, tmpN)
            read!(stream, tmpM)
            # Each step n
            while(stop == 0 && n <= N)
                next!(progress)
                # Row vector of data matrix
                read!(stream, x)
                normx = normalizex(x, n, stream, scale, pseudocount, rowmeanlist, rowmeanvec, rowvarlist, rowvarvec, colsumlist, colsumvec, cper)
                if perm
                    normx .= normx[randperm(length(normx))]
                end
                # Power iteration
                v .+= normx * (normx' * W[:,k])
                n += 1
            end
            close(stream)
        end
        # NaN
        checkNaN(W, GD())
        # Check Float32
        @assert W[1,1] isa Float32
        # Ortho-normalization in Lanczos process
        T[k,k] = W[:,k]' * v
        if k == 1
            v = v .- T[k,k] * W[:,k]
        else
            v = v .- s * W[:,k-1] .- T[k,k] * W[:,k]
        end
        s = norm(v)
        if k != numepoch
            T[k+1,k] = s
            T[k,k+1] = s
        end
        W[:,k+1] = v / s
        k += 1
        if n == N + 1
            n = 1
        end
    end

    # Return, W, λ, V
    Y = eigvecs(T[1:numepoch, :])
    X = (W[:,1:numepoch] * Y[:,1:dim])
    out = WλV(X, input, dim, scale, pseudocount, rowmeanlist, rowmeanvec, rowvarlist, rowvarvec, colsumlist, colsumvec, TotalVar, cper)
    out = (out[1], out[2], out[3], out[4], out[5], out[6], stop)
    if outdir isa String
        output(outdir, out, expvar)
    end
    return out
end


module RaviartThomasRefFEsTest

using Test
using Gridap.Polynomials
using Gridap.Fields
using Gridap.TensorValues
using Gridap.Fields: MockField
using Gridap.ReferenceFEs

p = QUAD
D = num_dims(QUAD)
et = Float64
order = 0

reffe = RaviartThomasRefFE(et,p,order)
test_reference_fe(reffe)
@test num_terms(get_prebasis(reffe)) == 4
@test get_order(get_prebasis(reffe)) == 0
@test num_dofs(reffe) == 4
@test get_default_conformity(reffe) == DivConformity()

p = QUAD
D = num_dims(QUAD)
et = Float64
order = 1

reffe = RaviartThomasRefFE(et,p,order)
test_reference_fe(reffe)
@test num_terms(get_prebasis(reffe)) == 12
@test num_dofs(reffe) == 12
@test get_order(get_prebasis(reffe)) == 1

prebasis = get_prebasis(reffe)
dof_basis = get_dof_basis(reffe)

v = VectorValue(3.0,0.0)
field = MockField{D}(v)

cache = dof_cache(dof_basis,field)
r = evaluate_dof!(cache, dof_basis, field)
test_dof(dof_basis,field,r)

cache = dof_cache(dof_basis,prebasis)
r = evaluate_dof!(cache, dof_basis, prebasis)
test_dof(dof_basis,prebasis,r)

end # module