## Banded Cholesky decomposition

struct BandedCholesky{T} <: Factorization{T}
    data::BandedMatrix{T}  # symmetric banded matrix
end

# conversion
convert(::Type{BandedCholesky{T}}, B::BandedCholesky{S}) where {T<:Number, S<:Number} =
    BandedCholesky{T}(convert(BandedMatrix{T}, B.data))

# size of the parent array
@inline size(A::BandedCholesky) = size(A.data)
@inline size(A::BandedCholesky, k::Integer) = size(A.data, k)
@inline bandwidth(A::BandedCholesky, k) = bandwidth(A.data, k)

# Cholesky factorisation.
function cholesky!(A::Symmetric{T,<:BandedMatrix}) where {T<:Number}
    P = parent(A)
    pbtrf!('U', size(A, 1), bandwidth(A), bandeddata(parent(P)))
    BandedCholesky{T}(P)
end
cholesky(A::Symmetric{<:Any, <:BandedMatrix}) = cholesky!(copy(A))
cholesky(F::BandedCholesky) = F # no op

function getindex(F::BandedCholesky, d::Symbol)
    d == :U && return F.data # UpperTriangular(F.data)
    d == :L && return F.data' # LowerTriangular(F.data)
    throw(KeyError(d))
end

## Utilities

# check if matrix is square before solution of linear system or before converting
checksquare(A::BandedCholesky) = checksquare(A.data)

## Conversion/Promotion

# Converts A and b to the narrowest blas type
for typ in [BandedCholesky]
    @eval function _convert_to_blas_type(A::$typ, B::AbstractVecOrMat{T}) where {T<:Number}
        TS = _promote_to_blas_type(eltype(A), eltype(B))
        AA = convert($typ{TS}, A)
        BB = convert(Array{TS, ndims(B)}, B)
        AA, BB # one of these two might make a copy
    end
end


using MathOptInterface
const MOI = MathOptInterface

@testset "NaiveAllDiff (two binary vars)" begin
    optimizer = SCIP.Optimizer(display_verblevel=0)
    atol, rtol = 1e-6, 1e-6

    # add two binary variables: x, y
    x, y = MOI.add_variables(optimizer, 2)
    MOI.add_constraint(optimizer, MOI.SingleVariable(x), MOI.ZeroOne())
    MOI.add_constraint(optimizer, MOI.SingleVariable(y), MOI.ZeroOne())

    # maximize 2x + y
    MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
            MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], [x, y]), 0.0))
    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE)

    # add constraint handler with constraint all-diff(x, y)
    alldiffch = NaiveAllDiff.NADCH(optimizer)
    SCIP.include_conshdlr(optimizer, alldiffch; needs_constraints=true)

    alldiffcons = NaiveAllDiff.NADCons([x, y])
    cr = SCIP.add_constraint(optimizer, alldiffch, alldiffcons)

    # solve problem and query result
    MOI.optimize!(optimizer)
    @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.OPTIMAL
    @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT

    @test MOI.get(optimizer, MOI.ObjectiveValue()) ≈ 2.0 atol=atol rtol=rtol
    @test MOI.get(optimizer, MOI.VariablePrimal(), x) ≈ 1.0 atol=atol rtol=rtol
    @test MOI.get(optimizer, MOI.VariablePrimal(), y) ≈ 0.0 atol=atol rtol=rtol
end

@testset "NaiveAllDiff (3 bin.vars, 2 pairwise conss)" begin
    optimizer = SCIP.Optimizer(display_verblevel=0)
    atol, rtol = 1e-6, 1e-6

    # add three binary variables
    x, y, z = MOI.add_variables(optimizer, 3)
    MOI.add_constraint(optimizer, MOI.SingleVariable(x), MOI.ZeroOne())
    MOI.add_constraint(optimizer, MOI.SingleVariable(y), MOI.ZeroOne())
    MOI.add_constraint(optimizer, MOI.SingleVariable(z), MOI.ZeroOne())

    # maximize 2x + 3y + 2z
    MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
            MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 3.0, 2.0],
                                                           [x, y, z]), 0.0))
    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE)

    # add constraint handler with constraints all-diff(x, y), all-diff(y, x)
    alldiffch = NaiveAllDiff.NADCH(optimizer)
    SCIP.include_conshdlr(optimizer, alldiffch; needs_constraints=true)

    SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([x, y]))
    SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([y, z]))

    # solve problem and query result
    MOI.optimize!(optimizer)
    @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.OPTIMAL
    @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT

    @test MOI.get(optimizer, MOI.ObjectiveValue()) ≈ 4.0 atol=atol rtol=rtol
    @test MOI.get(optimizer, MOI.VariablePrimal(), x) ≈ 1.0 atol=atol rtol=rtol
    @test MOI.get(optimizer, MOI.VariablePrimal(), y) ≈ 0.0 atol=atol rtol=rtol
    @test MOI.get(optimizer, MOI.VariablePrimal(), z) ≈ 1.0 atol=atol rtol=rtol
end

@testset "NaiveAllDiff (3 bin.vars, 3 pairwise conss)" begin
    optimizer = SCIP.Optimizer(display_verblevel=0)
    atol, rtol = 1e-6, 1e-6

    # add three binary variables
    x, y, z = MOI.add_variables(optimizer, 3)
    MOI.add_constraint(optimizer, MOI.SingleVariable(x), MOI.ZeroOne())
    MOI.add_constraint(optimizer, MOI.SingleVariable(y), MOI.ZeroOne())
    MOI.add_constraint(optimizer, MOI.SingleVariable(z), MOI.ZeroOne())

    # maximize 2x + 3y + 2z
    MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
            MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 3.0, 2.0],
                                                           [x, y, z]), 0.0))
    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE)

    # add constraint handler with constraints all-diff(x, y, z)
    alldiffch = NaiveAllDiff.NADCH(optimizer)
    SCIP.include_conshdlr(optimizer, alldiffch; needs_constraints=true)

    SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([x, y]))
    SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([x, z]))
    SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([y, z]))

    # solve problem and query result
    MOI.optimize!(optimizer)
    @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.INFEASIBLE
    @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.NO_SOLUTION

    @test MOI.get(optimizer, MOI.NodeCount()) >= 8  # complete enumeration
end

@testset "NaiveAllDiff (3 bin.vars, 1 cons with all)" begin
    optimizer = SCIP.Optimizer(display_verblevel=0)
    atol, rtol = 1e-6, 1e-6

    # add three binary variables
    x, y, z = MOI.add_variables(optimizer, 3)
    MOI.add_constraint(optimizer, MOI.SingleVariable(x), MOI.ZeroOne())
    MOI.add_constraint(optimizer, MOI.SingleVariable(y), MOI.ZeroOne())
    MOI.add_constraint(optimizer, MOI.SingleVariable(z), MOI.ZeroOne())

    # maximize 2x + 3y + 2z
    MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
            MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 3.0, 2.0],
                                                           [x, y, z]), 0.0))
    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE)

    # add constraint handler with constraints all-diff(x, y, z)
    alldiffch = NaiveAllDiff.NADCH(optimizer)
    SCIP.include_conshdlr(optimizer, alldiffch; needs_constraints=true)

    SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([x, y, z]))

    # solve problem and query result
    MOI.optimize!(optimizer)
    @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.INFEASIBLE
    @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.NO_SOLUTION

    @test MOI.get(optimizer, MOI.NodeCount()) >= 8  # complete enumeration
end

@testset "NaiveAllDiff (3 int.vars, 1 cons with all)" begin
    optimizer = SCIP.Optimizer(display_verblevel=0)
    atol, rtol = 1e-6, 1e-6

    # add three integer variables, in {0, 1, 2}
    x, y, z = MOI.add_variables(optimizer, 3)
    for v in [x, y, z]
        MOI.add_constraint(optimizer, MOI.SingleVariable(v), MOI.Integer())
        MOI.add_constraint(optimizer, MOI.SingleVariable(v), MOI.Interval(0.0, 2.0))
    end

    # maximize 2x + y
    MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
            MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0],
                                                           [x, y]), 0.0))
    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE)

    # add constraint handler with constraints all-diff(x, y, z)
    alldiffch = NaiveAllDiff.NADCH(optimizer)
    SCIP.include_conshdlr(optimizer, alldiffch; needs_constraints=true)

    SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([x, y, z]))

    # solve problem and query result
    MOI.optimize!(optimizer)
    @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.OPTIMAL
    @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT

    @test MOI.get(optimizer, MOI.ObjectiveValue()) ≈ 5.0 atol=atol rtol=rtol
    @test MOI.get(optimizer, MOI.VariablePrimal(), x) ≈ 2.0 atol=atol rtol=rtol
    @test MOI.get(optimizer, MOI.VariablePrimal(), y) ≈ 1.0 atol=atol rtol=rtol
    @test MOI.get(optimizer, MOI.VariablePrimal(), z) ≈ 0.0 atol=atol rtol=rtol
end

@testset "NoGoodCounter (2 binary vars)" begin
    optimizer = SCIP.Optimizer(display_verblevel=0,
                               misc_allowdualreds=SCIP.FALSE)
    atol, rtol = 1e-6, 1e-6

    # add binary variables
    x, y = MOI.add_variables(optimizer, 2)
    MOI.add_constraint(optimizer, MOI.SingleVariable(x), MOI.ZeroOne())
    MOI.add_constraint(optimizer, MOI.SingleVariable(y), MOI.ZeroOne())

    # maximize 2x + y
    MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
            MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0],
                                                           [x, y]), 0.0))
    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE)

    # add constraint handler with constraints
    counter = NoGoodCounter.Counter(optimizer, [x, y])
    SCIP.include_conshdlr(optimizer, counter; needs_constraints=false)

    # solve problem and query result
    MOI.optimize!(optimizer)
    @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.INFEASIBLE
    @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.NO_SOLUTION

    @test length(counter.solutions) == 4
end


using ImageTransformations: center
using Compat

"""
    mask_circle!(::AbstractMatrix, npix; fill=0)
    mask_circle!(::AbstractArray, npix; fill=0)

In-place version of [`mask_circle`](@ref)
"""
function mask_circle!(arr::AbstractMatrix, npix; fill = 0)
    y, x = axes(arr)
    yc, xc = center(arr)
    d = @. sqrt((x' - xc)^2 + (y - yc)^2)
    @. arr[d < npix] = fill
    return arr
end

function mask_circle!(cube::AbstractArray{T, 3}, npix; fill=0) where T
    @inbounds for i in axes(cube, 1)
        slice = @view cube[i, :, :]
        mask_circle!(slice, npix; fill=fill)
    end
    return cube
end

"""
    mask_circle(::AbstractMatrix, npix; fill=0)
    mask_circle(::AbstractArray, npix; fill=0)

Mask the inner-circle of an image with radius `npix` with value `fill`. Note that the input type must be compatible with the fill value's type. If the input is a cube it will mask each frame individually.

# See Also
[`mask_circle!`](@ref)
"""
mask_circle(arr::AbstractMatrix, npix; fill = 0) = mask_circle!(deepcopy(arr), npix, fill = fill)
mask_circle(cube::AbstractArray{T, 3}, npix; fill = 0) where T = mask_circle!(deepcopy(cube), npix, fill=fill)

"""
    mask_annulus!(::AbstractMatrix, npix_in, npix_out; fill=NaN)

In-place version of [`mask_annulus`](@ref)
"""
function mask_annulus!(arr::AbstractMatrix, npix_in, npix_out; fill = 0)
    y, x = axes(arr)
    yc, xc = center(arr)
    d = @. sqrt((x' - xc)^2 + (y - yc)^2)
    @. arr[npix_in ≤ d < npix_out] = fill
    return arr
end

"""
    mask_annulus(::AbstractMatrix, npix_in, npix_out; fill=0)

Mask an annular region of an image with inner-radius `npix_in`, outer-radius `npix_out` with value `fill`.
Note that the input type must be compatible with the fill value's type.

# See Also
[`mask_annulus!`](@ref)
"""
mask_annulus(arr::AbstractMatrix, npix_in, npix_out; fill = 0) = mask_annulus!(deepcopy(arr), npix_in, npix_out, fill = fill)

"""
    get_annulus_segments(data, inner_radius, width, [nsegments];
                         theta_init=0, scale_factor=1, mode=:index)

Returns indices or values in segments of a centered annulus.

### Modes
* `:mask` - returns a positive boolean mask for indexing
* `:value` - returns the data values indexed with the boolean mask
* `:apply` - returns the input data weighted by the boolean mask
"""
function get_annulus_segments(data, inner_radius, width, nsegments; theta_init=0, scale_factor=1, mode=:mask)
    cy, cx = center(data)
    ys, xs = axes(data)
    dist = @. sqrt((xs' - cx)^2 + (ys - cy)^2)
    ϕ_stride = deg2rad(ceil(Int, 360 / nsegments))
    ϕ = @. atan(ys - cy, xs - cx)
    ϕ_rot = mod2pi.(ϕ)
    outer_radius = @. inner_radius + scale_factor * width

    masks = similar(data, Matrix{Bool}, nsegments)
    for i in 1:nsegments
        ϕ_start = deg2rad(theta_init) + (i - 1) * ϕ_stride
        ϕ_end = ϕ_start + ϕ_stride

        if ϕ_start < 2π && ϕ_end > 2π
            mask = @. (dist ≥ inner_radius) & (dist < outer_radius) & (ϕ_rot ≥ ϕ_start) & (ϕ_rot ≤ 2π) | (dist ≥ inner_radius) & (dist < outer_radius) & (ϕ_rot ≥ 0) & (ϕ_rot < ϕ_end - 2π)
        elseif ϕ_start ≥ 2π && ϕ_end > 2π
            mask = @. (dist ≥ inner_radius) & (dist < outer_radius) & (ϕ_rot ≥ ϕ_start - 2π) & (ϕ_rot < ϕ_end - 2π)
        else
            mask = @. (dist ≥ inner_radius) & (dist < outer_radius) & (ϕ_rot ≥ ϕ_start) & (ϕ_rot < ϕ_end)
        end
        @inbounds masks[i] = mask
    end

    return _convert_mask.(Val(mode), masks, (data,))
end

@compat get_annulus_segments(data, inner_radius, width; theta_init=0, scale_factor=1, mode=:mask) = get_annulus_segments(data, inner_radius, width, 1; theta_init=theta_init, scale_factor=scale_factor, mode=mode) |> only

_convert_mask(::Val{:mask}, mask, data) = mask
_convert_mask(::Val{:value}, mask, data) = data[mask]
_convert_mask(::Val{:apply}, mask, data) = data .* mask


function findExtrema(signal::AbstractArray)
    maxima  = [x.I[1] for x in Images.findlocalmaxima(signal, 1)]
    maxvals = signal[maxima]

    minima  = [x.I[1] for x in Images.findlocalminima(signal, 1)]
    minvals = signal[minima]

    ## make sure at length extrema > 1
    if isempty(maxvals)
        mm,tt = findmax(tempy)
        push!(maxima,tt)
        push!(maxvals,mm)
    end

    if isempty(minvals)
        mm,tt = findmin(tempy)
        push!(minima,tt)
        push!(minvals,mm)
    end

    return maxima,minima,maxvals,minvals
end


using ParameterInference
using NonstationaryProcesses
using Plots
using Catch22
fourseas!()

# * Define a path in parameter space for an AR system
N = 1000000
a = sineWave((pa = (N, 0.6, 0, 0))...)
plot(0:N, a)
b = sineWave((pb = (N, 0.6, -N÷4, 0))...)
plot!(0:N, b)

S = arSim(parameter_profiles = (sineWave, sineWave),
          parameters = (pa, pb),
          tmax = N,
          transient_t0 = 0,
          t0 = 0,
          X0 = [0.0]) # Specifying 1 initial condition returns a univariate time series.

@userplot FeatureSpaceImage
@recipe function f(g::FeatureSpaceImage; var=1, lowdim=principalcomponents, nwindows=50, colors=[:cornflowerblue, :crimson])
    @assert g.args[1] isa Process
    S = g.args[1]
    if length(g.args) == 2
        f = g.args[2]
    else
        f = catch22
    end
    layout := (1, 2)
    p = parameterseries(S)
    t = times(S)
    x = timeseries(S, var)
    X, _ = slidingWindow(x, length(t)÷nwindows)
    F = f(X)
    if size(F, 1) > 2 # Do PCA
        m = F |> Array |> lowdim
        F = embed(m, F, 1:2)
    end
    aspect_ratio --> :equal
    arrow --> arrow(:closed, :head, 30.0, 20.0)
    size --> (800, 400)
    bottom_margin --> 0.5Plots.cm
    left_margin --> 0.5Plots.cm
    narrows = 4
    for nn = 1:narrows
        Nn = length(t)÷narrows
        if nn == narrows
            nx = Nn*(nn-1)+1:length(t)
        else
            nx = Nn*(nn-1)+1:Nn*nn
        end
        @series begin
            subplot := 1
            title := "Parameter space"
            seriescolor --> colors[1]
            titlefontsize --> 16
            xguide --> "\$\\theta_1\$"
            yguide --> "\$\\theta_2\$"
            linealpha --> 0.75
            seriestype --> :path
            markersize --> 2
            markercolor --> colors[1]
            x := p[1, nx]
            y := p[2, nx]
            xlims := [minimum(p[1, :])-0.2, maximum(p[1, :])+0.2]
            ylims := [minimum(p[2, :])-0.2, maximum(p[2, :])+0.2]
            ()
        end
        Nn = size(F, 2)÷narrows
        if nn == narrows
            nx = Nn*(nn-1)+1:size(F, 2)
        else
            nx = Nn*(nn-1)+1:Nn*nn+1
        end
        @series begin
            subplot := 2
            aspect_ratio := :auto
            seriescolor --> colors[2]
            seriestype --> :path
            linealpha --> 0.75
            x := F[1, nx]
            y := F[2, nx]
            ()
        end
    end
    @series begin
        subplot := 2
        aspect_ratio := :auto
        arrow := nothing
        title := "Feature space"
        seriescolor --> colors[2]
        titlefontsize --> 16
        xguide --> "\$f_1\$"
        yguide --> "\$f_2\$"
        markeralpha --> 0.75
        seriestype --> :scatter
        linealpha --> 0.5
        markersize --> 2.5
        markerstrokewidth --> 0.01
        x := F[1, :]
        y := F[2, :]
        xlims := [minimum(F[1, :])-0.2, maximum(F[1, :])+0.2]
        ylims := [minimum( F[2, :])-0.2, maximum( F[2, :])+0.2]
        ()
    end
end

featurespaceimage(S, AC[1:2], colors=[:black, :black]);
savefig("../Figures/$(rand(UInt16)).svg");
l = length(timeseries(S, 1))
n = round(Int, 2*8/3)
plot(vec(timeseries(S, 1))[n*l÷8+1:n*l÷8+251], color=:black);
savefig("../Figures/$(rand(UInt16)).svg");

SS = arSim(parameter_profiles = (constant,),
          parameters = (0.97),
          tmax = 250,
          transient_t0 = 0,
          t0 = 0,
          X0 = [0.0])
plot(SS)
savefig("../Figures/$(rand(UInt16)).svg");

SS = arSim(parameter_profiles = (constant,),
          parameters = (0.03),
          tmax = 250,
          transient_t0 = 0,
          t0 = 0,
          X0 = [0.0])
plot(SS)
savefig("../Figures/$(rand(UInt16)).svg");

SSS = updateparam(ikedaSim(), 4, 0.6)
plot(timeseries(SSS, 1)[1:150])
savefig("../Figures/$(rand(UInt16)).svg");

SSS = updateparam(ikedaSim(), 1, 25)
plot(timeseries(SSS, 1)[1:150])
savefig("../Figures/$(rand(UInt16)).svg");


"""
    module ILog2

This module provides `ilog2`.
"""
module ILog2

export ilog2, checkispow2

const IntBits  = Union{Int8, Int16, Int32, Int64, Int128,
                       UInt8, UInt16, UInt32, UInt64, UInt128}

"""
    ilog2(x, RoundUp)

Return the smallest `m` such that `2^m >= n`.
"""
ilog2(x, ::typeof(RoundUp)) = ispow2(x) ? ilog2(x) : ilog2(x) + 1
ilog2(x, ::typeof(RoundDown)) = ilog2(x)

"""
    msbindex(::Type{T})
    T is an Integer bits type

Return one less than the number of bits in T
"""
@generated function msbindex(::Type{T}) where {T<:Integer}
    return sizeof(T)*8 - 1
end

msbindex(::Type{BigInt}) = throw(ArgumentError("Expected an integer type with fixed number of bits"))

"""
    ilog2(n::Real)

Compute the largest `m` such that `2^m <= n`.

!!! note

    `float(2^53 - 1)` is the largest `n::Float64` for which `eps(n) ≤ 1`.
    For larger `n::Float64`, "the largest `m` such that `2^m <= n`" is ambiguous, although
    `ilog2` may return a number. For large enough `n::Float64`, `ilog2` will
    throw an `InexactError`. These cautionary statements do not apply for `n::Integer`.
"""
function ilog2(n::T) where {T<:IntBits}
    n > zero(T) && return msbindex(T) - leading_zeros(n)
    throw(DomainError(n))
end
ilog2(n::BigInt) = Base.GMP.MPZ.sizeinbase(n, 2) - 1
ilog2(x::Real) = ilog2(convert(Integer, floor(x)))

# This is several times slower than the other methods. But none of the standard bitstype integers,
# nor `BigInt`, dispatch to this method.
ilog2(n::Integer) = convert(typeof(n), floor(log(2,n)))

"""
    checkispow2(n::Number)

Return base-2 logarithm of `n` if `n` is a power of two.
Otherwise throw a `DomainError`.
"""
function checkispow2(n::Number)
    if ! ispow2(n)
        throw(DomainError(n, "$n is not a power of two."))
    end
    return ilog2(n)
end

end # module ILog2


function find_largest_α(Q_m::SortedDict{T,T}, pval::T) where T <: Real
    q_sum = Q_sum(Q_m);
    largest_k = nothing;
    for (k,v) in Q_m
        if q_sum ≥ pval
            largest_k = k;
        else
            return k
        end
        q_sum -= v;
    end
    return largest_k
end

function pval_w_Qm(Qm::SortedDict{T,T}, α::Real) where T <: Real
    pval = 0;
    for (k,v) in Qm
        if k ≥ α
            pval += v;
        end
    end
    return pval
end

function find_δ(Q_m::SortedDict{T,T}, pval_ϵ::Real, pval::Real) where T <: Real
    q_sum_plus_pval_ϵ = Q_sum(Q_m)+pval_ϵ;
    largest_δ = nothing;
    for (k,v) in Q_m
        if q_sum_plus_pval_ϵ ≥ pval
            largest_δ = k;
        else
            return k
        end
        q_sum_plus_pval_ϵ -= v;
    end
    return largest_δ
end

"""
    pval2score(pwm, pval, ϵ=1e-1, k=10, bg=[.25,.25,.25,.25])
Returns the highest score(M,pval) of a `pwm` such that p-value is greater or equal to `pval`.

Input:
* `pwm`: a 4 x m matrix
* `pval`: a p-value; e.g. pval = 1e-3
* `ϵ`: initial granularity  (optional)
* `k`: Refinement parameter (optional)
* `bg`: multinomial background (optional)

Output
* `α`: the highest score-threshold
"""
function pvalue2score(pwm::Matrix{T}, pval::Real, ϵ=1e-2, k=10, bg=[.25,.25,.25,.25]) where T <: Real
    @assert 0 ≤ pval ≤ 1 "pvalue must be in [0,1]"
    @assert size(pwm,1) == 4 "The input matrix must have 4 and only 4 rows"
    pwm_double64 = Double64.(pwm);
    pval_double64 = Double64(pval);
    bg_double64 = Double64.(bg);
    mpwm = min_score_range(pwm_double64);
    m = size(pwm, 2);
    pwm_ϵ = round_pwm(mpwm, ϵ);
    E = calc_E(mpwm, pwm_ϵ);
    Q = create_Q(m);
    Q = score_distribution(pwm_ϵ,worst_score(pwm_ϵ),Inf,bg_double64);
    α = find_largest_α(Q[m], pval_double64);

    @inbounds while !(pval_w_Qm(Q[m], α-E) == pval_w_Qm(Q[m], α))
        # println("err: ", pval_w_Qm(Q[m], α-E) - pval_w_Qm(Q[m], α));
        ϵ = ϵ/k;
        pwm_ϵ = round_pwm(mpwm, ϵ);
        E = calc_E(mpwm, pwm_ϵ);
        Q = create_Q(m);
        Q = score_distribution(pwm_ϵ,α-E,α+E,bg_double64);
        #=
        note:
        Sometimes the score range is simply
        too small and hence the score_distribution subroutine
        cannot find any scores within the range [α-E, α+E].
        This happens when the error of the round matrix, E, is very
        small (see definition 3 in Touzet and Varre's paper:
        https://almob.biomedcentral.com/articles/10.1186/1748-7188-2-15)
        When this happens we return α.
        =#
        if isempty(Q[m])
            # α = α + E;
            # break;
            return α
        end
        pval_ϵ = fast_pvalue(pwm_ϵ,α+E);
        δ = find_δ(Q[m],pval_ϵ,pval);
        α = δ;
    end

    return α;
end


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/NodeNumbering.jl/blob/master/LICENSE

"""
    calculate_bandwidth(G::Dict{Int, Vector{Int}})

Calculate bandwidth of graph ``G``. The bandwidth of the matrix is number ``k``
such that ``A_{ij} = 0`` if ``|i-j| > k``.

# Examples

```jldoctest
julia> G = Dict(
           1 => [3, 8, 9],
           2 => [3, 8, 7],
           3 => [1, 2],
           4 => [8, 9],
           5 => [7, 8],
           6 => [2, 7],
           7 => [5, 2, 6],
           8 => [1, 2, 4, 5],
           9 => [1, 4]);

julia> calculate_bandwidth(G)
17

```

# References

* Wikipedia contributors. "Band matrix." Wikipedia, The Free Encyclopedia. Wikipedia, The Free Encyclopedia, 28 Apr. 2017. Web. 15 Jul. 2017. https://en.wikipedia.org/wiki/Band_matrix.
"""
function calculate_bandwidth(G::Dict{Int, Vector{Int}})
    return 2*maximum([maximum(j-g) for (j,g) in G]) + 1
end


module Metrics
# Module for DL Metrics

export mae, mse, msle, male, r2_score, adjusted_r2_score

export binary_accuracy, confusion_matrix, categorical_accuracy, sparse_categorical, top_k_categorical, top_k_sparse_categorical, precision, recall, sensitivity, detection_rate, f_beta_score, specificity, false_alarm_rate, cohen_kappa, statsfromTFPN, classwise_stats, global_stats

export IoU, PSNR

export bleu_score, rouge, rouge_l_summary_level

export ranking_stats_k, avg_precision

export report_stats

using StatsBase, DataFrames
using DataStructures: OrderedDict

include("./CV_Metrics/CVMetrics.jl")
include("./NLP_Metrics/NLPMetrics.jl")
include("Classification.jl")
include("Regression.jl")
include("Ranking_n_Statistical.jl")
include("utils.jl")

end


export QuadAdaptive,
    Q_quadgk,
    Q_hcubature,
    BestRule,
    Q_GaussLegendre,
    Q_GaussJacobi,
    Q_GaussLaguerre,
    Q_GaussHermite

abstract type QuadratureStrategy end

abstract type AdaptiveStrategy{T} <: QuadratureStrategy end

prectype(::Type{<:AdaptiveStrategy{T}}) where {T} = prectype(T)

default_atol() = default_atol(Float64)
default_rtol() = default_rtol(Float64)

default_atol(::Type{T}) where {T} = zero(T)
default_rtol(::Type{T}) where {T} = sqrt(eps(T))
default_rtol(x) = default_rtol(typeof(x))

default_maxevals() = 10^4

"An adaptive quadrature strategy"
struct QuadAdaptive{T} <: AdaptiveStrategy{T}
    atol    ::  T
    rtol    ::  T
    maxevals    ::  Int
end

QuadAdaptive(; atol = default_atol(), rtol = default_rtol(atol), maxevals = default_maxevals()) = QuadAdaptive(atol, rtol, maxevals)
QuadAdaptive(atol, rtol, maxevals) = QuadAdaptive(promote(atol,rtol)..., maxevals)
QuadAdaptive(atol::T, rtol::T, maxevals) where {T} = QuadAdaptive{T}(atol, rtol, maxevals)

QuadAdaptive{T}(atol = default_atol(T), rtol = default_rtol(atol)) where {T} =
    QuadAdaptive{T}(atol, rtol, default_maxevals())

"Adaptive quadrature using quadgk"
struct Q_quadgk{T} <: AdaptiveStrategy{T}
    atol    ::  T
    rtol    ::  T
    maxevals    ::  Int
end

Q_quadgk(; atol = default_atol(), rtol = default_rtol(atol), maxevals = default_maxevals()) = Q_quadgk(atol, rtol, maxevals)
Q_quadgk(atol, rtol, maxevals) = Q_quadgk(promote(atol,rtol)..., maxevals)
Q_quadgk(atol::T, rtol::T, maxevals) where {T} = Q_quadgk{T}(atol, rtol, maxevals)

Q_quadgk{T}(atol = default_atol(T), rtol = default_rtol(atol)) where {T} =
    Q_quadgk{T}(atol, rtol, default_maxevals())

Q_quadgk(qs::QuadAdaptive{T}) where {T} = Q_quadgk{T}(qs.atol, qs.rtol, qs.maxevals)

"Adaptive quadrature using hcubature"
struct Q_hcubature{T} <: AdaptiveStrategy{T}
    atol    ::  T
    rtol    ::  T
    maxevals    ::  Int
end

Q_hcubature(; atol = default_atol(), rtol = default_rtol(atol), maxevals = default_maxevals()) =
    Q_hcubature(atol, rtol, maxevals)
Q_hcubature(atol, rtol, maxevals) = Q_hcubature(promote(atol,rtol)..., maxevals)
Q_hcubature(atol::T, rtol::T, maxevals) where {T} = Q_hcubature{T}(atol, rtol, maxevals)

Q_hcubature{T}(atol = default_atol(T), rtol = default_rtol(atol)) where {T} =
    Q_hcubature{T}(atol, rtol, default_maxevals())

Q_hcubature(qs::QuadAdaptive{T}) where {T} = Q_hcubature{T}(qs.atol, qs.rtol, qs.maxevals)

"Use a quadrature rule adapted to the measure."
struct BestRule <: QuadratureStrategy
    n   ::  Int
end

"A quadrature rule associated with a domain."
abstract type DomainRule{T} <: QuadratureStrategy end

points(q::DomainRule) = q.x
weights(q::DomainRule) = q.w

abstract type IntervalRule{T} <: DomainRule{T} end

"Apply a quadrature rule defined on `[-1,1]`."
struct ChebyshevIntervalRule{T} <: IntervalRule{T}
    x       ::  Vector{T}
    w       ::  Vector{T}
end
domain(q::ChebyshevIntervalRule{T}) where {T} = ChebyshevInterval{T}()

"Apply a fixed quadrature rule defined on `[0,1]`."
struct UnitIntervalRule{T} <: IntervalRule{T}
    x       ::  Vector{T}
    w       ::  Vector{T}
end
domain(q::UnitIntervalRule{T}) where {T} = UnitInterval{T}()

"Apply a fixed quadrature rule defined on the halfline `[0,∞)`."
struct HalfLineRule{T} <: DomainRule{T}
    x       ::  Vector{T}
    w       ::  Vector{T}
end
domain(q::HalfLineRule{T}) where {T} = HalfLine{T}()

"Apply a fixed quadrature rule defined on the real line."
struct RealLineRule{T} <: DomainRule{T}
    x       ::  Vector{T}
    w       ::  Vector{T}
end
domain(q::RealLineRule{T}) where {T} = FullSpace{T}()

struct GenericDomainRule{T} <: DomainRule{T}
    domain  ::  Domain{T}
    x       ::  Vector{T}
    w       ::  Vector{T}
end
domain(q::GenericDomainRule) = q.domain

Q_GaussLegendre(args...) = ChebyshevIntervalRule(gausslegendre(args...)...)
Q_GaussJacobi(args...) = ChebyshevIntervalRule(gaussjacobi(args...)...)
Q_GaussLaguerre(args...) = HalfLineRule(gausslaguerre(args...)...)
Q_GaussHermite(args...) = RealLineRule(gausshermite(args...)...)

"""
Turn the interval rule into a composite quadrature rule with `L` segments on
the same interval.
"""
function composite_rule(L::Int, rule::ChebyshevIntervalRule{T}) where {T}
    x = points(rule)
    w = weights(rule)
    grid = range(-one(T), one(T), length=L+1)
    h = step(grid)
    composite_x = vcat([grid[i] .+ (x .+ 1)*h/2 for i in 1:L]...)
    composite_w = vcat([w*h/2 for i in 1:L]...)
    ChebyshevIntervalRule(composite_x, composite_w)
end

composite_rule(L::Int, n::Int, ::Type{T} = Float64) where {T} =
    composite_rule(L, Q_GaussLegendre(T, n))

"""
Construct a graded quadrature rule towards the left of the unit interval.

The rule is a composite quadrature rule on intervals of the form
`σ^(k+1):σ^k`. The number of points on interval `i` is `⌈n*i*μ⌉`,
where `μ` is an optional parameter.
"""
function graded_rule_left(σ, n, μ = 1, δ = 100eps(σ), nmax = 30)
    k = floor(Int, log(δ)/log(σ))
    graded_mesh = σ.^(k:-1:0)
    T = typeof(σ)
    x = T[]
    w = T[]
    for i in 1:k
        q = Q_GaussLegendre(T, min(ceil(Int, n*i*μ),nmax))
        ai = graded_mesh[i]
        bi = graded_mesh[i+1]
        h = bi-ai
        x = vcat(x, ai .+ (points(q) .+ 1)*h/2)
        w = vcat(w, weights(q)*h/2)
    end
    UnitIntervalRule(x, w)
end

"""
Construct a graded quadrature rule towards the right of the unit interval.

See: `graded_rule_left`.
"""
function graded_rule_right(σ, n, args...)
    Q = graded_rule_left(σ, n, args...)
    x = points(Q)
    w = weights(Q)
    xrev = reverse(1 .- x)
    wrev = reverse(w)
    UnitIntervalRule(xrev, wrev)
end


include("../examples/lorenz.jl")
using JLD
using LinearAlgebra
function p_y_g_x(a, σ_o)
        term =  log(1/sqrt(2π)/σ_o)
        return sum(term .- 0.5*a.*a/σ_o/σ_o)
end
function obs(x, σ_o)
    return x .+ σ_o*randn(d)
end
function obs1(x, σ_o)
	return [x[3] + σ_o*randn()]
end

function resample(x, w)
    # multinomial resampling
    cdfw = cumsum(w)
    new_pts = similar(x)
	N_p = size(w)[1]
    for j = 1:N_p
        r = rand()
        for i = 1:N_p
            if cdfw[i] >= r
                new_pts[:,j] .= x[:,i]
                break
            end
        end
    end
    return new_pts
end

function sir(y,x_t,x_ip,Δ,obs_fun,σ_o,σ_d,N_thr=10)
    K = size(y)[2]
    N_p = size(x_ip)[2]

    N_y = K*Δ
	@show N_y
    rmse_trj = zeros(K)
    N_runup = 3000

	w = SharedArray{Float64}(N_p)
	w .= 1.0/N_p
	x = SharedArray{Float64}(d,N_p)
	t = @distributed for i=1:N_p
		x[:,i] = rand(d)
		for k = 1:N_runup
			x[:,i] = next(x[:,i],s)
		end
	end
	wait(t)

	mean_x = zeros(d,K)
    j = 1
	count = 0
    for k = 1:N_y
        if k % Δ == 0
			filter_mean = sum(x,dims=2)/N_p
			rmse_trj[j] = norm(filter_mean - x_t[:,k])/sqrt(d)

			t = @distributed for i = 1: N_p
                logwi = log(w[i]) + p_y_g_x(y[:,j] .- obs_fun(x[:,i],σ_o), σ_o)
                w[i] = exp(logwi)
			end
            w ./= sum(w)
            N_eff = 1.0/sum(w.*w)
            if (N_eff < N_thr)
				count = count + 1
			    x .= resample(x, w)
		    end
			j = j+1
		end
        for i = 1:N_p
            x[:,i] .= next(x[:,i],σ_d)
        end
    end
	@show "Resampling was triggered ", count, " times out of ", N_y
    return rmse_trj
end
"""
    assimilate(K, Np, σ_o, σ_d,
				   Δ, Nth)

Perform `K` data assimilation steps using an SIR algorithm with `Np` particles. Other inputs:

    1. `σ_o`: std of Gaussian observation noise
	2. `σ_d`: std of Gaussian dynamics noise, which is added to every component and at every timestep.
	3. `Δ`: inter-observation number of timesteps, e.g., if `Δ = 5,` observations are assumed available at timestep `0,5,10,...,5(K-1)`.
	4. `Nth`: number of particles below which to resample
	5. `obs`: observation map, e.g., `obs(x) = x[3]`

Output:

`mean_rmse`: Summary scalar performance metric:

# Examples
```julia-repl
julia> rmse_avg = assimilate(500, 1000, 0.1, 0.1, 1, 10, obs)
```
"""
function assimilate(K, Np, σ_o, σ_d,
				   Δ, Nth, obsfun)
	Ny = K*Δ
	ytest = obsfun(rand(d),σ_o)
	dy = size(ytest)[1]
    x_true = ones(d,Ny)
    x0_true = rand(d)
    Nrunup = 2000
    for i = 1:Nrunup
        x0_true = next(x0_true, 0.0)
    end
    x_true[:,1] .= x0_true
    y = zeros(dy, K)
	y[:,1] .= obsfun(x0_true, σ_o)
	k = 1
    for i = 2:Ny
        x_true[:,i] = next(x_true[:,i-1], σ_d)
		if i % Δ == 0
        	y[:,k] .= obsfun(x_true[:,i], σ_o)
			k = k+1
		end
    end
    # store trajectory of w and x.
    rmse_trj = sir(y, x_true, Δ, obsfun, σ_o, σ_d, Nth)
	mean_rmse = sum(rmse_trj)/K
    return mean_rmse
end


# using CSV, DataFrames

# struct WShape

#     d::Float64
#     tw::Float64
#     bf::Float64
#     tf::Float64
#     kdes::Float64
#     k1::Float64
#     A::Float64
#     Ix::Float64
#     Iy::Float64
#     J::Float64
#     Cw::Float64
#     Zx::Float64
#     Zy::Float64
#     Wno::Float64

# end

# function AISC(shape_name)

#     filename = string(@__DIR__, "/aisc-shapes-database-v15.0.csv")

#     data = CSV.read(filename, DataFrame, header=true)

#     shape_row = findfirst(==(shape_name), data.AISC_Manual_Label)

#     section_type = data[shape_row, :Type]

#     if section_type == "W"

#         d = parse(Float64, data[shape_row, :d])
#         tw = parse(Float64, data[shape_row, :tw])
#         bf = parse(Float64, data[shape_row, :bf])
#         tf = parse(Float64, data[shape_row, :tf])
#         kdes = parse(Float64, data[shape_row, :kdes])

#         #get k1 from AISC table, it is in fraction format
#         k1 = data[shape_row, :k1]
#         index = findfirst("/", k1)

#         whole = Int(data[shape_row, :k1][1] - '0')

#         if isempty(index) == false
#             top_fraction = parse(Float64, data[shape_row, :k1][index[1]-2:index[1]-1])
#             bottom_fraction = parse(Float64, data[shape_row, :k1][index[1]+1:index[1]+2])
#         else
#             top_fraction = 0.0
#             bottom_fraction = 0.0
#         end

#         k1 = whole + top_fraction/bottom_fraction

#         A = data[shape_row, :A]
#         Ix = data[shape_row, :Ix]
#         Iy = data[shape_row, :Iy]
#         J = parse(Float64, data[shape_row, :J])
#         Cw = parse(Float64, data[shape_row, :Cw])
#         Zx = data[shape_row, :Zx]
#         Zy = data[shape_row, :Zy]
#         Wno = parse(Float64, data[shape_row, :Wno])

#         section = WShape(d, tw, bf, tf, kdes,k1, A, Ix, Iy, J, Cw, Zx, Zy, Wno)

#     end

#     return section

# end

using StructuresKit

shape_name = "W14X90"

section = CrossSection.AISC(shape_name)


using ConstraintSolver

CS = ConstraintSolver
include("../../test/sudoku_fcts.jl")

function from_file(filename, sep='\n')
    s = open(filename) do file
        read(file, String)
    end
    str_sudokus = split(strip(s), sep)
    grids = AbstractArray[]
    for str_sudoku in str_sudokus
        str_sudoku = replace(str_sudoku, "."=>"0")
        one_line_grid = parse.(Int, split(str_sudoku,""))
        grid = reshape(one_line_grid, 9, 9)
        push!(grids, grid)
    end
    return grids
end

function solve_all(grids; benchmark=false, single_times=true)
    ct = time()
    grids = grids
    for (i,grid) in enumerate(grids)
        com = CS.init()

        com_grid = create_sudoku_grid!(com, grid)
        add_sudoku_constr!(com, com_grid)

        if single_times
            GC.enable(false)
            t = time()
            status = solve!(com);
            t = time()-t
            GC.enable(true)
            println(i-1,", ", t)
        else
            GC.enable(false)
            status = solve!(com);
            GC.enable(true)
        end
        if !benchmark
            @show com.info
            println("Status: ", status)
            @assert fulfills_sudoku_constr(com_grid)
        end
    end
    println("")
    tt = time()-ct
    println("total time: ", tt)
    println("avg: ", tt/length(grids))
end

function solve_one(grid)
    com = CS.init()

    CS.build_search_space!(com, grid,[1,2,3,4,5,6,7,8,9],0)
    add_sudoku_constr!(com, grid)

    status = solve!(com; backtrack=false);
    return com
end

function main(; benchmark=false, single_times=true)
    solve_all(from_file("benchmark/sudoku/data/top95.txt"); benchmark=benchmark, single_times=single_times)
    # solve_all(from_file("hardest.txt"), "hardest")
end


using ArgParse

s=ArgParseSettings()
@add_arg_table s begin
    "--part2"
        help="Do part 2"
        action=:store_true
end
parsed_args = parse_args(ARGS, s)

function part1(numbers)
    lastSeen = typemax(Int64)
    increases = 0
    for value in numbers
        if value > lastSeen
            increases +=1
        end
        lastSeen = value
    end
    increases
end

function part2(numbers)
    increases = 0
    for idx in 1:(Base.length(numbers)-3)
        if sum(numbers[idx:idx+2]) < sum(numbers[idx+1:idx+3])
            increases += 1
        end
    end
    increases
end

numbers = [parse(Int64, line) for line in eachline("input/day_1.txt")]
if parsed_args["part2"]
    answer = part2(numbers)
else
    answer = part1(numbers)
end
println("The answer is: $answer")


import Laplacians.condNumber
import Laplacians.sddmWrapLap

using StaticArrays

#=

A variations of the approxChol Laplacian solver
by Rasmus Kyng and Daniel A. Spielman.
It solves systems in non-singular BDD (Block Diagonally Dominant) Matrices.
This algorithm is an implementation of an approximate edge-by-edge elimination
algorithm inspired by the Approximate Gaussian Elimination algorithm of
Kyng and Sachdeva.

TODO:

- get rid of mat to analyze, and just build it in to llmatp
- Check on op norm, where faster and how call fastest.

 - Figure out what to do with singular Matrices
 - Make work with full matrices.

 - Test on approximate potential orths
 - Remove logging

=#

"""
    params = AC_Params(queue_forward, stag_test)

queue_forward=true if we use the forward queue (the original)
stag_test is a parameter for PCG, default 5.
"""
mutable struct AC_Params
    queue_forward::Bool
    stag_test::Integer
    useop::Bool
    perm::Bool
end

AC_Params() = AC_Params(true, 5, true, false)
AC_Params(b::Bool) = AC_Params(b, 5, true, false)
AC_Params(qf::Bool, uop::Bool) = AC_Params(qf, 5, uop, false)

#=
  For small matrices, the following is faster than "opnorm".
=#
_ournorm(x::StaticArrays.SArray) = sqrt(maximum(eigvals(x*x')))
ournorm(x::Real) = abs(x)
ournorm(mat::SMatrix{1,1}) = abs(mat[1])
ournorm(mat::SMatrix{2,2}) = _ournorm(mat)
ournorm(mat::SMatrix{3,3}) = _ournorm(mat)
ournorm(mat::SMatrix{4,4}) = _ournorm(mat)
ournorm(mat::SMatrix{5,5}) = _ournorm(mat)
ournorm(mat::SMatrix{6,6}) = _ournorm(mat)
ournorm(mat) = opnorm(mat)

function LLmata(a::SparseMatrixCSC{Tval,Tind}) where {Tind,Tval}
    n = size(a,1)
    m = nnz(a)

    degs = zeros(Tind,n)
    ptrs = copy(a.colptr)

    cols = a.colptr[1:n]
    llelems = Vector{LLa{Tind,Tval}}(undef, m)

    for i in 1:n
        degs[i] = a.colptr[i+1] - a.colptr[i]

        for ind in a.colptr[i]:(a.colptr[i+1]-1)
            j = a.rowval[ind]

            if j < i
                v = a.nzval[ind]

                if ptrs[i] < a.colptr[i+1]-1
                    llelems[ptrs[i]] = LLa(j, v, ptrs[i]+1, ptrs[j])
                else
                    llelems[ptrs[i]] = LLa(j, v, zero(Tind), ptrs[j])
                end

                if ptrs[j] < a.colptr[j+1]-1
                    llelems[ptrs[j]] = LLa(i, v, ptrs[j]+1, ptrs[i])
                else
                    llelems[ptrs[j]] = LLa(i, v, zero(Tind), ptrs[i])
                end

                ptrs[j] += 1
                ptrs[i] += 1
            end
        end

    end

    return LLmata{Tind,Tval}(n, degs, cols, llelems)
end

function print_ll_col(llmat::LLmata, i::Int)
    next = llmat.cols[i]
    while next != 0
      ll = llmat.lles[next]
      println("col $i, row $(ll.row) : $(ll.val)")

      next = ll.next
    end
end

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

The approximate factorization

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

function get_ll_col(llmat::LLmata{Tind,Tval},
    i,
    colspace::Vector{LLcol{Tind,Tval}}) where {Tind,Tval}

      ptr = llmat.cols[i]
      len = 0
      while ptr != 0

          if llmat.lles[ptr].val > zero(Tval)
              len = len+1

              item = LLcol(llmat.lles[ptr].row, ptr, llmat.lles[ptr].val)
              if (len > length(colspace))
                  push!(colspace,item)
              else
                  colspace[len] = item
              end
          end

          ptr = llmat.lles[ptr].next
      end

      return len
  end

#=
Add together entries of the column that are in the same row.
Decrease the degrees of vertices when their entries are added.
Returns the number of nonzero entries in the column after the compression, `ptr`.
=#

function compress_col!(llmat::LLmata{Tind,Tval},
    colspace::Vector{LLcol{Tind,Tval}},
    len::Tind,
    pq::acPQ{Tind},
    col::Int,
    compressions) where {Tind,Tval}

    o1 = Base.Order.ord(isless, x->x.row, false, Base.Order.Forward)

    sort!(colspace, one(len), len, QuickSort, o1)

    c = colspace

    ptr = 0
    currow = c[1].row
    curval = c[1].val
    curptr = c[1].ptr

    for i in 2:len

        if c[i].row != currow

            ptr = ptr+1
            c[ptr] = LLcol(currow, curptr, curval)

            currow = c[i].row
            curval = c[i].val
            curptr = c[i].ptr

        else

            curval = curval + c[i].val
            push!(compressions, Compression(col ,curptr, c[i].ptr))

            zeroval(llmat, llmat.lles[c[i].ptr].reverse)
            pqDec!(pq, currow)
        end

    end

    # emit the last row

    ptr = ptr+1
    c[ptr] = LLcol(currow, curptr, curval)

    o2 = Base.Order.ord(isless, x->x.val, false, Base.Order.Forward)
    sort!(colspace, one(ptr), ptr, QuickSort, o2)

    return ptr
end

const order_forward = Base.Order.ord(isless, identity, false, Base.Order.Forward)

"""
    a = mat_to_analyze(B::BlockCSC)

Produces an adjacency matrix of a graph whose edge weights are the norms
of matrices in B.  This could probably be accelerated.
"""
function mat_to_analyze(B::BlockCSC)

    mat = B.mat

    a = SparseMatrixCSC(mat.m, mat.n, copy(mat.colptr), copy(mat.rowval), ournorm.(mat.nzval))
    for i in 1:a.n
        a[i,i] = 0.0
    end
    dropzeros!(a)

    return a
end

function zeroval(llmat::LLmata{Tind,Tval}, revj::Tind) where {Tind, Tval}
    llmat.lles[revj] = LLa(llmat.lles[revj].row,
            zero(Tval),
            llmat.lles[revj].next ,
            llmat.lles[revj].reverse )
end

#=

Create edge (k,j), using the memory for (i,j)
=#
function rotate_edges(llmat::LLmata,k,j,ptr,revj,newEdgeVal)
    llmat.lles[revj] = LLa(k, newEdgeVal, llmat.lles[revj].next, ptr)

    # fix row j in col k
    khead = llmat.cols[k]
    llmat.cols[k] = ptr

    llmat.lles[ptr] = LLa(j, newEdgeVal, khead, revj)

end

function compute_elim(a::LLmata{Tind,Tval}; params = AC_Params()) where {Tind,Tval}

    #logstart(fn)

    n = a.n

    ijks = Vector{IJKop2{Tind,Tval}}(undef,0)

    #if params.queue_forward
        pq = acPQ(a.degs)
    #else
    #    pq = acPQ_rev(a.degs)
    #end

    colspace = Vector{LLcol{Tind,Tval}}(undef,n)
    cumspace = Vector{Tval}(undef,n)

    # elim_rank[i] is the iter in which vertex i is eliminated
    elim_rank = Vector{Tind}(undef,n)
    order_tmp = Vector{Tind}(undef, n)

    it = 1

    # DAS: might be computable from other terms, in which case can remove.
    diag_tmp = Vector{Tval}(undef,n)
    diag_adjusts = zeros(Tval, n)

    compressions = Vector{Compression}()

#    @inbounds while it < n
    while it < n

        i = pqPop!(pq)
        elim_rank[i] = it

        it = it + 1

        elim_vertex(a, i, ijks, colspace, cumspace,
        diag_tmp, diag_adjusts, order_tmp, pq, compressions, params)

    end

    last_col = pqPop!(pq)
    elim_rank[last_col] = n

    return ijks, diag_adjusts, elim_rank, last_col, compressions
end

function elim_vertex(llmat::LLmata{Tind,Tval},
    i::Int,
    ijks::Vector{IJKop2{Tind,Tval}},
    colspace::Vector{LLcol{Tind,Tval}},
    cumspace::Vector{Tval},
    diag_tmp::Vector{Tval},
    diag_adjusts::Vector{Tval},
    order_tmp::Vector{Tind},
    pq,
    compressions::Vector{Compression},
    params::AC_Params) where {Tind,Tval}

    len = get_ll_col(llmat, i, colspace)
    if len == 0
        return
    end

    len = compress_col!(llmat, colspace, len, pq, i, compressions)
    if len == 0
        return
    end

    #println("$(i): $(len)")

    # csum is the weighted degree of a vertex.
    # cumspace holds the cumulative sums of weights of edges, so that we can sample
    csum = zero(Tval)
    for ii in 1:len
        csum = csum + colspace[ii].val
        cumspace[ii] = csum
    end

    # we store adjustments to diagonals locally in diag_tmp, and then write them to
    # diagonal adjusts later.  This line sets the default adjusts, for when there are no incoming
    # edges to the vertex.
    for hoff in 2:len
        diag_tmp[hoff] = -(cumspace[hoff-1]) * colspace[hoff].val / csum
    end

    # Here we choose all the edges to be sampled.
    # This could be done in bulk, maybe faster, or in line with the next loop.
    for joffset in 1:(len-1)
        r = rand() * (csum - cumspace[joffset]) + cumspace[joffset]
        order_tmp[joffset] = searchsortedfirst(cumspace,r,joffset+1,len,order_forward)
    end

    # loop over the edges attached to vertex i, doing eliminations.
    for joffset in 1:(len-1)

        # are eliminating edge (i,j), and creating edge (j,k)

        ll = colspace[joffset]

        j = ll.row
        revj = llmat.lles[ll.ptr].reverse

        # (j,k) is the edge added.
        koff = order_tmp[joffset]
        k = colspace[koff].row

        pqInc!(pq, k)

        # this is the factor by which we multiply row i to elim row j
        op = - ll.val / csum

        # put in the new edge (j, k)
        oldEdgeVal = colspace[koff].val

        # this is what we really need to know for the factorization:
        # the probability that we chose this edge (j, k)
        probadd = oldEdgeVal / (csum-cumspace[joffset])

        # the following, commented out, form is equivalent
        # newEdgeVal = op * oldEdgeVal  / probadd
        newEdgeVal = (csum-cumspace[joffset]) * ll.val / csum

        # Compensate diagonal k for having sampled this edge
        diag_tmp[koff] += newEdgeVal

        rotate_edges(llmat,k,j,ll.ptr,revj,newEdgeVal)

        push!(ijks, IJKop2(i,j,k, probadd, ll.ptr, colspace[koff].ptr, revj))

    end # for joffset

    # adjust diagonals from sampling
    # takes a lot of time: all cache misses
    for hoff in 2:len
        h = colspace[hoff].row
        diag_adjusts[h] += diag_tmp[hoff]
    end

    # For the degre 1 case - last edge - just adjust diagonal

    ll = colspace[len]
    j = ll.row
    revj = llmat.lles[ll.ptr].reverse

    #= should firgure out why wanted it < n
    if it < n
        pqDec!(pq, j)
    end
    =#

    pqDec!(pq, j)

    # zero out that edge for the other side.
    zeroval(llmat, revj)

    # zero in k position indicates no edge
    push!(ijks, IJKop2(i,j,0, 1.0, ll.ptr, revj, 0))

end

function bdd_setup(B::BlockCSC{D,Tv,L}, elim_rank) where {D, Tv, L}
    mat = B.mat

    n = size(mat,1)
    cnt = count_nonzero_diags(mat)

    m = nnz(mat) - cnt

    matstore = Vector{StaticArrays.SArray{Tuple{D,D},Tv,2,L}}(undef, m)
    matstore = zeros(StaticArrays.SArray{Tuple{D,D},Tv,2,L}, m)
    dgs = Vector{StaticArrays.SArray{Tuple{D,D},Tv,2,L}}(undef, n)

    diag_free_ind = 0

    for i in 1:n

        for ind in mat.colptr[i]:(mat.colptr[i+1]-1)
            j = mat.rowval[ind]

            if i == j
                dgs[i] = mat.nzval[ind]
            else
                diag_free_ind += 1

                # SOMETHING IS WRONG IF NEED TO COMMENT THIS OUT!
                #if elim_rank[i] < elim_rank[j]
                    matstore[diag_free_ind] = mat.nzval[ind]
                #end
            end
        end
    end

    return matstore, dgs
end

"""
This makes a BlockCSC with structural zeros and the elements of bdd that we will use
in the elimination.  It allows us to do the factorizations just once.
"""
function prepare_triangular_factor(bdd::BlockCSC{D,Tv,L}, ijks, elim_rank) where {D,Tv,L}
    n = size(bdd,1)
    m = length(ijks)

    ti = Vector{Int}(undef, m+n)
    tj = Vector{Int}(undef, m+n)
    tv = zeros(typeof(bdd[1,1]), m+n)
    ptr = 1
    i = 0
    for it in 1:m
        ijk = ijks[it]
        if ijk.i != i
            i = ijk.i
            ti[ptr] = i
            tj[ptr] = i
            #tv[ptr] = la[i,i]
            ptr += 1
        end
        ti[ptr] = ijk.i
        tj[ptr] = ijk.j
        ptr += 1
    end
    lst = argmax(elim_rank)
    ti[ptr] = lst
    tj[ptr] = lst
    bdd_tri = BlockCSC(sparse(tj,ti,tv,n,n))

    aj, ai, av = findnz(bdd.mat)
    for it in 1:length(ai)
        if elim_rank[ai[it]] <= elim_rank[aj[it]]
            bdd_tri[aj[it], ai[it]] = av[it]
        #else
        #    bdd_tri[ai[it], aj[it]] = av[it]
        end
    end

    return bdd_tri
end

function factor_bdd_ijks_plt(ijks, diag_adjusts,
    elim_rank,
    matstore::Vector{StaticArrays.SArray{Tuple{D,D},Tv,2,L}}, dgs, comps) where {D,Tv,L}

    Tblock = StaticArrays.SArray{Tuple{D,D},Tv,2,L}

    n = length(dgs)
    colptr = Vector{Int}(undef, n + 1)
    rowval = Vector{Int}(undef, length(ijks) + n)
    nzval = Vector{Tblock}(undef, length(ijks) + n)
    colptr_ind = 1
    rowval_ind = 1

    E = one(Tblock)

    diaginv = E

    # true when are first entry in col.  Would be better to have a cscish ijks, and skip this.
    flag = true

    comps_ind = 1

    for it in 1:length(ijks)
        ijk = ijks[it]

        if flag

            while (comps_ind <= length(comps)) && (comps[comps_ind].col == ijk.i)
                matstore[comps[comps_ind].into] += matstore[comps[comps_ind].from]
                comps_ind += 1
            end

            dgs[ijk.i] += E*diag_adjusts[ijk.i]

            # place the diagonal entry
            colptr[colptr_ind] = rowval_ind
            colptr_ind += 1
            rowval[rowval_ind] = ijk.i
            nzval[rowval_ind] = dgs[ijk.i]
            rowval_ind += 1

            diaginv = inv(dgs[ijk.i])

            flag = false
        end

        op = - matstore[ijk.ptr_ji] * diaginv

        dgs[ijk.j] += op * (matstore[ijk.ptr_ji])'

       # push out an off-diagonal entry
       rowval[rowval_ind] = ijk.j

       nzval[rowval_ind] = matstore[ijk.ptr_ji]
       rowval_ind += 1

        if ijk.k != 0 # if not a degree 1 elim: again better to use cscish ijks.

            if elim_rank[ijk.j] < elim_rank[ijk.k]
                matstore[ijk.ptr_kj] = (op * ( matstore[ijk.ptr_ki])')' / ijk.prob
            else
                matstore[ijk.ptr_ji] = op * ( matstore[ijk.ptr_ki])'/ ijk.prob
            end

        else

            flag = true
        end
    end

    lst = argmax(elim_rank)

    colptr[colptr_ind] = rowval_ind
    rowval[rowval_ind] = lst

    nzval[rowval_ind] = dgs[lst] + E*diag_adjusts[lst]

    colptr[colptr_ind+1] = rowval_ind+1

    return PermLTCSC(colptr, rowval, nzval)

end

# this will be removed from the main code line
function AC_BDD_precon(bdd; verbose=false, params = AC_Params())

    t1 = time()
    t0 = t1

    a = mat_to_analyze(bdd)
    if verbose
        println("Time to construct mat to analyze ", time()-t1)
        t1 = time()
    end

    ijks, diag_adjusts, elim_rank, last_col, comps = compute_elim(LLmata(a))
    if verbose
        println("Time to compute elimination structure ", time()-t1)
        t1 = time()
    end

    matstore, dgs = bdd_setup(bdd, elim_rank)
    if verbose
        println("Time to prepare for elimination ", time()-t1)
        t1 = time()
    end

    ltcsc = factor_bdd_ijks_plt(ijks, diag_adjusts, elim_rank, matstore, dgs, comps)
    if verbose
        println("Time to eliminate ", time()-t1)
        t1 = time()
    end

    F(b) = solver(ltcsc, b)

    if verbose
        println("Total solver build time: ", time()-t0)
        println("Ratio of operator edges to original edges: ", 2 * length(ltcsc.nzval) / nnz(bdd))
    end

    return F
end

function solver(B, ltcsc;
    pcg_params = PCG_params())

    F!(b) = solver!(ltcsc, b)

    #=
    f1(b;tol=tol_,maxits=maxits_, maxtime=maxtime_,
        verbose=verbose_,
        pcgIts=pcgIts_) =
    pcg(Bp, b[elim_order], F, tol=tol, maxits=maxits, maxtime=maxtime,
        pcgIts=pcgIts,
        verbose=verbose,
        stag_test = params.stag_test)[elim_rank]
=#

    f1(b;tol=pcg_params.tol,
        maxits=pcg_params.maxits,
        maxtime=pcg_params.maxtime,
        verbose=pcg_params.verbose,
        pcgIts=pcg_params.pcgIts) =
    pcg_inplace(B, b, F!,
        tol=tol,
        maxits=maxits,
        maxtime=maxtime,
        pcgIts=pcgIts,
        verbose=verbose,
        stag_test = pcg_params.stag_test)

    return f1
end

function permed_solver(B, ltcsc, elim_rank::Vector{Int};
    pcg_params = PCG_params())

    elim_order = invperm(elim_rank)
    Bp = BlockCSC(B.mat[elim_order, elim_order])

    # note: the following does not copy, but directs pointer
    ltcsc_p = LTCSC(ltcsc.colptr, elim_rank[ltcsc.rowval], ltcsc.nzval)

    F(b) = solver(ltcsc_p, b)

    #=
    f1(b;tol=tol_,maxits=maxits_, maxtime=maxtime_,
        verbose=verbose_,
        pcgIts=pcgIts_) =
    pcg(Bp, b[elim_order], F, tol=tol, maxits=maxits, maxtime=maxtime,
        pcgIts=pcgIts,
        verbose=verbose,
        stag_test = params.stag_test)[elim_rank]
=#

    f1(b; tol=pcg_params.tol,
        maxits=pcg_params.maxits,
        maxtime=pcg_params.maxtime,
        verbose=pcg_params.verbose,
        pcgIts=pcg_params.pcgIts) =
    pcg_perm(elim_rank, Bp, copy(b), F,
        PCG_params(verbose, tol, maxits, maxtime, pcgIts, pcg_params.stag_test )
        )

    return f1
end

function pcg_perm(perm,
    mat::BlockCSC{D,Tv,L},
    b::Vector{SArray{Tuple{D},Tv,1,D}},
    pre::Function,
    pcg_params::PCG_params)  where {D,Tv,L}

    bp = b[perm]
    x = similar(b)

    x[perm] = pcg(mat, bp, pre, tol=pcg_params.tol,
        maxits=pcg_params.maxits,
        maxtime=pcg_params.maxtime,
        pcgIts=pcg_params.pcgIts,
        verbose=pcg_params.verbose,
        stag_test = pcg_params.stag_test)

    return x

end

"""
    solver = approxChoBDD(bdd); x = solver(b);
    solver = approxCholBDD(bdd; tol::Real=1e-6, maxits=1000, maxtime=Inf, verbose=false, pcgIts=Int[])

A heuristic by Daniel Spielman inspired by the linear system solver in https://arxiv.org/abs/1605.02353 by Rasmus Kyng and Sushant Sachdeva.  Whereas that paper eliminates vertices one at a time, this eliminates edges one at a time.  It is probably possible to analyze it.
It solves systems of equations in nonsingular Block Diagonally Dominant Matrices.
ADD NOTES ON HOW TO CONSTRUCT THESE.

For more info, see http://danspielman.github.io/Laplacians.jl/latest/usingSolvers/index.html
"""
function approxCholBDD(bdd;
    tol=1e-6,
    maxits=1000,
    maxtime=Inf,
    verbose=false,
    pcgIts=Int[],
    params = AC_Params())

    pcg_params = PCG_params(verbose,
        tol, maxits, maxtime, pcgIts,
        params.stag_test)

    tol_ =tol
    maxits_ =maxits
    maxtime_ =maxtime
    verbose_ =verbose
    pcgIts_ =pcgIts

    t1 = time()
    t0 = t1

    #M = sparse(bdd)

    a = mat_to_analyze(bdd)
    if verbose
        println("Time to construct mat to analyze ", time()-t1)
        t1 = time()
    end

    ijks, diag_adjusts, elim_rank, last_col, comps = compute_elim(LLmata(a))
    if verbose
        println("Time to compute elimination structure ", time()-t1)
        t1 = time()
    end

    matstore, dgs = bdd_setup(bdd, elim_rank)
    if verbose
        println("Time to prepare for elimination ", time()-t1)
        t1 = time()
    end

    ltcsc = factor_bdd_ijks_plt(ijks, diag_adjusts, elim_rank, matstore, dgs, comps)
    if verbose
        println("Time to eliminate ", time()-t1)
        t1 = time()
    end

    permute_outside = params.perm

    if permute_outside

        f1 = permed_solver(bdd, ltcsc, elim_rank,
            pcg_params = pcg_params)

        if verbose
            println("Time to permute ", time()-t1)
            t1 = time()
        end

    else

        f1 = solver(bdd,  ltcsc,
            pcg_params = pcg_params)

    end

    if verbose
        println("Total solver build time: ", time()-t0)
        println("Ratio of operator edges to original edges: ", 2 * length(ltcsc.nzval) / nnz(bdd))
    end

    return f1
end


State = Int64
Action = Int64
DPTable = Array{Dict{State, Float64}, 1}

StatesAvailable = Dict(
    1 => [1],
    2 => [2, 3, 4],
    3 => [5, 6],
    4 => [7]
)

ActionsAvailble = Dict(
    (1, 1) => [2, 3, 4],
    (2, 2) => [5],
    (2, 3) => [5, 6],
    (2, 4) => [5, 6],
    (3, 5) => [7],
    (3, 6) => [7],
    (4, 7) => [7]
)

Transitions = Dict(
    (1, 2) => (2, 7),
    (1, 3) => (3, 8),
    (1, 4) => (4, 5),
    (2, 5) => (5, 12),
    (3, 5) => (5, 8),
    (3, 6) => (6, 9),
    (4, 5) => (5, 7),
    (4, 6) => (6, 13),
    (5, 7) => (7, 9),
    (6, 7) => (7, 6)
#    (7, 7) => (7, 0)
)

EndState = Dict(7 => 0.0)

function state_func(state)
    return StatesAvailable[state]
end

function actions_func(stage, state)
    return ActionsAvailble[(stage, state)]
end

function transitions(state, action)
    return Transitions[(state, action)]
end

function is_better(minimize::Bool, x::Float64, y::Float64)
    if minimize
        return x < y
    end
    return x > y

end

function compute_solution_by_br(current_stage::Int64,
        minimize::Bool,
        table::DPTable,
        state_func,
        actions_func,
        transition)
    if(current_stage == 0)
        return reverse(table)
    end
    factor = 1

    if !minimize
        factor = -1
    end

    states = state_func(current_stage)
    table_next = table[end]
    table_curr = Dict{State, Float64}()
    for state ∈ states
        table_curr[state] = factor * Inf
        actions = actions_func(current_stage, state)
        for action ∈ actions
            transition = transitions(state, action)
            new_state = transition[1]
            val = transition[2]
            if new_state ∈ keys(table_next)
                value_of_action = table_next[new_state] + val
                if is_better(minimize, value_of_action, table_curr[state])
                    table_curr[state] = value_of_action
                end
            end
        end
    end
    push!(table, table_curr)
    return compute_solution_by_br(current_stage - 1, minimize,
        table, state_func, actions_func, transition)
end

table = [Dict(7 => 0.0)]
compute_solution_by_br(3, true, table, state_func, actions_func, transitions)


using OrdinaryDiffEq, ModelingToolkit, Test, SparseArrays

N = 3
xyd_brusselator = range(0, stop = 1, length = N)
brusselator_f(x, y, t) = (((x - 0.3)^2 + (y - 0.6)^2) <= 0.1^2) * (t >= 1.1) * 5.0
limit(a, N) = ModelingToolkit.ifelse(a == N + 1, 1, ModelingToolkit.ifelse(a == 0, N, a))
function brusselator_2d_loop(du, u, p, t)
    A, B, alpha, dx = p
    alpha = alpha / dx^2
    @inbounds for I in CartesianIndices((N, N))
        i, j = Tuple(I)
        x, y = xyd_brusselator[I[1]], xyd_brusselator[I[2]]
        ip1, im1, jp1, jm1 = limit(i + 1, N), limit(i - 1, N), limit(j + 1, N),
                             limit(j - 1, N)
        du[i, j, 1] = alpha * (u[im1, j, 1] + u[ip1, j, 1] + u[i, jp1, 1] + u[i, jm1, 1] -
                       4u[i, j, 1]) +
                      B + u[i, j, 1]^2 * u[i, j, 2] - (A + 1) * u[i, j, 1] +
                      brusselator_f(x, y, t)
        du[i, j, 2] = alpha * (u[im1, j, 2] + u[ip1, j, 2] + u[i, jp1, 2] + u[i, jm1, 2] -
                       4u[i, j, 2]) +
                      A * u[i, j, 1] - u[i, j, 1]^2 * u[i, j, 2]
    end
end

# Test with tuple parameters
p = (3.4, 1.0, 10.0, step(xyd_brusselator))

function init_brusselator_2d(xyd)
    N = length(xyd)
    u = zeros(N, N, 2)
    for I in CartesianIndices((N, N))
        x = xyd[I[1]]
        y = xyd[I[2]]
        u[I, 1] = 22 * (y * (1 - y))^(3 / 2)
        u[I, 2] = 27 * (x * (1 - x))^(3 / 2)
    end
    u
end

u0 = init_brusselator_2d(xyd_brusselator)
prob_ode_brusselator_2d = ODEProblem(brusselator_2d_loop,
                                     u0, (0.0, 11.5), p)
sys = modelingtoolkitize(prob_ode_brusselator_2d)

# test sparse jacobian pattern only.
prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = false)
JP = prob.f.jac_prototype
@test findnz(Symbolics.jacobian_sparsity(map(x -> x.rhs, equations(sys)), states(sys)))[1:2] ==
      findnz(JP)[1:2]

# test sparse jacobian
prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true)
@test findnz(calculate_jacobian(sys, sparse = true))[1:2] ==
      findnz(prob.f.jac_prototype)[1:2]

# test when not sparse
prob = ODEProblem(sys, u0, (0, 11.5), sparse = false, jac = true)
@test prob.f.jac_prototype == nothing

prob = ODEProblem(sys, u0, (0, 11.5), sparse = false, jac = false)
@test prob.f.jac_prototype == nothing

# test when u0 is nothing
f = DiffEqBase.ODEFunction(sys, u0 = nothing, sparse = true, jac = true)
@test findnz(f.jac_prototype)[1:2] == findnz(JP)[1:2]
@test eltype(f.jac_prototype) == Float64

f = DiffEqBase.ODEFunction(sys, u0 = nothing, sparse = true, jac = false)
@test findnz(f.jac_prototype)[1:2] == findnz(JP)[1:2]
@test eltype(f.jac_prototype) == Float64

# test when u0 is not Float64
u0 = similar(init_brusselator_2d(xyd_brusselator), Float32)
prob_ode_brusselator_2d = ODEProblem(brusselator_2d_loop,
                                     u0, (0.0, 11.5), p)
sys = modelingtoolkitize(prob_ode_brusselator_2d)

prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = false)
@test eltype(prob.f.jac_prototype) == Float32

prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true)
@test eltype(prob.f.jac_prototype) == Float32


export IAAFT

include("../utils/powerspectrum.jl")
include("../utils/interpolation.jl")
export IAAFT

"""
    IAAFT(M = 100, tol = 1e-6, W = 75)

An iteratively adjusted amplitude-adjusted-fourier-transform surrogate[^SchreiberSchmitz1996].

IAAFT surrogates have the same linear correlation, or periodogram, and also
preserves the amplitude distribution of the original data, but are improved relative
to AAFT through iterative adjustment (which runs for a maximum of `M` steps).
During the iterative adjustment, the periodograms of the original signal and the
surrogate are coarse-grained and the powers are averaged over `W` equal-width
frequency bins. The iteration procedure ends when the relative deviation
between the periodograms is less than `tol` (or when `M` is reached).

IAAFT, just as AAFT, can be used to test the null hypothesis that the data
come from a monotonic nonlinear transformation of a linear Gaussian process.

[^SchreiberSchmitz1996]: T. Schreiber; A. Schmitz (1996). "Improved Surrogate Data for Nonlinearity Tests". [Phys. Rev. Lett. 77 (4)](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.77.635)
"""
struct IAAFT <: Surrogate
    M::Int
    tol::Real
    W::Int

    function IAAFT(;M::Int = 100, tol::Real = 1e-6, W::Int = 75)
        new(M, tol, W)
    end
end

Base.show(io::IO, x::IAAFT) = print(io, "IAAFT(M = $(x.M), tol = $(x.tol), W = $(x.W))")

function interpolated_spectrum(spectrum, n)
    intp_spectrum = zeros(n)
    interpolated_spectrum!(intp_spectrum, spectrum, n)
end

function interpolated_spectrum!(intp_spectrum, spectrum, n)
    t = 1:length(spectrum)
    r = getrange(t, n)
    iₓ = itp(spectrum)
    intp_spectrum .= iₓ.(r)
    return intp_spectrum
end

function surrogenerator(x, method::IAAFT, rng = Random.default_rng())
    m = mean(x)
    x_sorted = sort(x)
    forward = plan_rfft(x)
    inverse = plan_irfft(forward * x, length(x))
    𝓕 = forward * x
    r = abs.(𝓕)
    ϕ = abs.(𝓕)
    ix = zeros(Int, length(x))

    # Periodograms
    𝓕p = prepare_spectrum(x, forward)
    xpower = zeros(length(𝓕)); powerspectrum!(𝓕p, xpower, x, forward)
    spower = copy(xpower)

    # Binned periodograms
    xpowerᵦ = interpolated_spectrum(xpower, method.W)
    spowerᵦ = interpolated_spectrum(spower, method.W)

    init = (
        forward = forward,
        inverse = inverse,
        𝓕 = 𝓕,
        𝓕p = 𝓕p,
        r = r,
        ϕ = ϕ,
        m = m,
        x_sorted = x_sorted,
        xpower = xpower,
        spower = spower,
        xpowerᵦ = xpowerᵦ,
        spowerᵦ = spowerᵦ,
        ix = ix,
    )

    return SurrogateGenerator(method, x, similar(x), init, rng)
end
using LinearAlgebra
function (sg::SurrogateGenerator{<:IAAFT})()
    init_fields = (:forward, :inverse, :𝓕, :𝓕p, :r, :ϕ, :m, :x_sorted, :xpower, :spower, :xpowerᵦ, :spowerᵦ, :ix)
    forward, inverse, 𝓕, 𝓕p, r, ϕ, m, x_sorted, xpower, spower, xpowerᵦ, spowerᵦ, ix = getfield.(Ref(sg.init), init_fields)

    x, s, rng = sg.x, sg.s, sg.rng
    M, W = sg.method.M, sg.method.W
    tol = sg.method.tol

    # Surrogate starts out as a random permutation of `x`
    n = length(x)
    s .= x[sample(rng, 1:n, n)]

    sum_old, sum_new = 0.0, 0.0
    iter = 1
    while iter <= M
        mul!(𝓕, forward, s)
        ϕ .= angle.(𝓕)
        𝓕 .= r .* exp.(ϕ .* 1im)

        # TODO: Unfortunately, we can't simply do ldiv! here to avoid allocations.
        # But, although FFTW does not yet have irfft!, it will probably have.
        # See https://github.com/JuliaMath/FFTW.jl/pull/222.
        # Once that PR is merged, we should replace the following line with
        # the in-place version.
        ######################################################################
        s .= inverse * 𝓕
        sortperm!(ix, s)
        s[ix] .= x_sorted

        powerspectrum!(𝓕p, spower, s, forward)
        interpolated_spectrum!(spowerᵦ, spower, W)
        if iter == 1
            sum_old = sum((xpowerᵦ .- xpowerᵦ) .^ 2) / sum(xpowerᵦ .^ 2)
        else
            sum_new = sum((xpowerᵦ .- spowerᵦ) .^ 2) / sum(xpowerᵦ .^ 2)
            if abs(sum_old - sum_new) < tol
                iter = M + 1
            else
                sum_old = sum_new
            end
        end

        iter += 1
    end

    return s
end


function mmek(;
  S=301, E=120, SE=0, P=0,
  k1=0.00166, k2=0.0001, k3=0.1)
  # Initialize network
  m = Network("Michaelis-Menten")

  # Species Definitions
  m <= Species("S",  S)
  m <= Species("E",  E)
  m <= Species("SE", SE)
  m <= Species("P",  P)

  # Reaction Definitions
  m <= Reaction("binding",      k1, "S + E --> SE")
  m <= Reaction("dissociation", k2, "SE --> S + E")
  m <= Reaction("conversion",   k3, "SE --> E + P")

  return m
end

model = mmek()


"""
    aggstat(X::Union{AbstractMatrix, AbstractVector}; group, fun = mean)
Compute the mean (or other statistic) of each column of `X`, by group.
* `X` : A matrix (n, p) or vector (n).
* `group` : A vector (n) defining the groups.
* `fun` : Function to compute.
"""
function aggstat(X::Union{AbstractMatrix, AbstractVector}; group,
        fun = mean)
    X = ensure_mat(X)
    group = vec(group)
    q = size(X, 2)
    ztab = tab(group)
    lev = ztab.keys
    nlev = length(lev)
    ni = collect(values(ztab))
    zX = similar(X, nlev, q)
    for i in 1:nlev
        s = group .== lev[i]
        zX[i, :] .= vec(fun(vrow(X, s), dims = 1))
    end
    (X = zX, lev = lev, ni = ni)
end

"""
    aggstat(X::DataFrame; var_nam, group_nam, fun = mean)
Compute the mean (or other statistic) of selected columns of `X`, by group.
* `X` : A dataframe.
* `var_nam` : Names (vector) of the variables to summarize.
* `group_nam` : Names (vector) of the group variables to consider.
* `fun` : Function given the statistic to compute.

Variables defined in `var_nam` and `group_nam` must be columns of `X`.
"""
function aggstat(X::DataFrame; var_nam, group_nam, fun = mean)
    gdf = groupby(X, group_nam)
    combine(gdf, var_nam .=> fun, renamecols = false)
end

"""
    center(X, v)
Center each column of `X`.
* `X` : Data.
* `v` : Centering factors.
"""
function center(X, v)
    M = copy(X)
    center!(M, v)
    M
end

function center!(X, v)
    p = size(X, 2)
    @inbounds for j = 1:p
        X[:, j] .= vcol(X, j) .- v[j]
    end
end

"""
    checkdupl(X; digits = 3)
Finding replicated rows in a dataset.
* `X` : A dataset.
* `digits` : Nb. digits used to round `X`.

## Examples
```julia
X = rand(5, 3)
Z = vcat(X, X[1:3, :])
checkdupl(X)
checkdupl(Z)

M = hcat(X, fill(missing, 5))
Z = vcat(M, M[1:3, :])
checkdupl(M)
checkdupl(Z)
```
"""
function checkdupl(X; digits = 3)
    X = round.(ensure_mat(X), digits = digits)
    n = size(X, 1)
    rownum1 = []
    rownum2 = []
    @inbounds for i = 1:n
        @inbounds for j = 1:n
            res = isequal(vrow(X, i), vrow(X, j))
            if res
                push!(rownum1, i)
                push!(rownum2, j)
            end
        end
    end
    u = findall(rownum1 .!= rownum2)
    res = DataFrame((rownum1 = rownum1[u], rownum2 = rownum2[u]))
    k = length(u)
    if k > 0
        res = res[1:Int64(k / 2), :]
    end
    res
end

"""
    checkmiss(X)
Finding rows with missing data in a dataset.
* `X` : A dataset.

## Examples
```julia
X = rand(5, 4)
zX = hcat(rand(2, 3), fill(missing, 2))
Z = vcat(X, zX)
checkmiss(X)
checkmiss(Z)
```
"""
function checkmiss(X)
    X = ensure_mat(X)
    z = vec(sum(ismissing.(X); dims = 2))
    u = findall(z .> 0)
    DataFrame((rownum = u, nmissing = z[u]))
end

"""
    colmeans(X)
    colmeans(X, w)
Compute the mean of each column of `X`.
* `X` : Data.
* `w` : Weights of the observations.

Return a vector.

For a true weighted mean, `w` must preliminary be normalized to sum to 1.
"""
colmeans(X) = vec(Statistics.mean(X; dims = 1))
colmeans(X, w) = vec(w' * ensure_mat(X))

"""
    colvars(X)
    colvars(X, w)
Compute the (uncorrected) variance of each column of `X`.
* `X` : Data.
* `w` : Weights of the observations.

Return a vector.

**Note:** For a true weighted variance, `w` must preliminary be normalized to sum to 1.
"""
colvars(X) = vec(Statistics.var(X; corrected = false, dims = 1))
function colvars(X, w)
    p = size(X, 2)
    z = colmeans(X, w)
    @inbounds for j = 1:p
        z[j] = dot(view(w, :), (vcol(X, j) .- z[j]).^2)
    end
    z
end
colstds(X) = sqrt.(colvars(X))
colstds(X, w) = sqrt.(colvars(X, w))

"""
    dummy(y)
Examples
≡≡≡≡≡≡≡≡≡≡
y = ["d", "a", "b", "c", "b", "c"]
#y =  rand(1:3, 7)
dummy(y)
"""
function dummy(y)
    ztab = tab(y)
    lev = ztab.keys
    nlev = length(lev)
    ni = collect(values(ztab))
    Y = BitArray(undef, length(y), nlev)
    for i = 1:nlev
        Y[:, i] = y .== lev[i]
    end
    Y = Float64.(Y)
    (Y = Y, lev = lev, ni = ni)
end

function dummy2(y)
    z = tab(y)
    lev = z.keys
    nlev = length(lev)
    ni = collect(values(tab(y)))
    res = list(nlev, BitVector)
    for i = 1:nlev
        res[i] = y .== lev[i]
    end
    Y = reduce(hcat, res)
    (Y = Y, lev = lev, ni = ni)
end

"""
    ensure_df(X)
Reshape `X` to a dataframe if necessary.
"""
ensure_df(X::DataFrame) = X
ensure_df(X::AbstractVector) = DataFrame([X], :auto)
ensure_df(X::AbstractMatrix) = DataFrame(X, :auto)

"""
    ensure_mat(X)
Reshape `X` to a matrix if necessary.
"""
ensure_mat(X::AbstractMatrix) = X
ensure_mat(X::AbstractVector) = reshape(X, :, 1)
ensure_mat(X::Number) = reshape([X], 1, 1)
ensure_mat(X::LinearAlgebra.Adjoint) = Matrix(X)
ensure_mat(X::DataFrame) = Matrix(X)

"""
    findmax_cla(x, weights = nothing)
Find the most occurent level in `x`.
"""
function findmax_cla(x, weights = nothing)
    isnothing(weights) ? weights = ones(length(x)) : nothing
    res = aggstat(weights; group = x, fun = sum)
    res.lev[argmax(res.res)]   # if equal, argmax takes the first
end

"""
    iqr(x)
Compute the interquartile interval (IQR).
"""
iqr(x) = quantile(x, .75) - quantile(x, .25)

"""
    list(n::Integer)
Create a Vector{Any}(nothing, n).

`isnothing(object, i)` can be used to check if cell i is empty.

## Examples
```julia
list(5)
```
"""
list(n::Integer) = Vector{Any}(nothing, n)

"""
    list(n::Integer, type)
Create a Vector{type}(undef, n).

`isassigned(object, i)` can be used to check if cell i is empty.

## Examples
```julia
list(5, Float64)
list(5, Array{Float64})
list(5, Matrix{Float64})
```
"""
list(n::Integer, type) = Vector{type}(undef, n)

"""
    mad(x)
Compute the median absolute deviation (MAD),
adjusted by a factor (1.4826) for asymptotically normal consistency.
"""
mad(x) = 1.4826 * median(abs.(x .- median(x)))

"""
    matcov(X)
    matcov(X, w)
Compute the (uncorrected) covariance matrix of `X`.
* `X` : Data.
* `w` : Weights of the observations.

Uncorrected covariance matrix of the columns of `X`.

**Note:** For true weighted covariances, `w` must preliminary be normalized to sum to 1.
"""
matcov(X) = Statistics.cov(ensure_mat(X); corrected = false)

function matcov(X, w)
    X = ensure_mat(X)
    xmeans = colmeans(X, w)
    X = center(X, xmeans)
    z = Diagonal(sqrt.(w)) * X
    z' * z
end

"""
    mweights(w)
Return a vector of weights that sums to 1.
"""
mweights(w) = w / sum(w)

"""
    pnames(x)
Return the names of the elements of `x`.
"""
pnames(x) = propertynames(x)

"""
    psize(x)
Return the type and size of `x`.
"""
function  psize(x)
    println(typeof(x))
    println(size(x))
end

"""
    recodcat2num(x; start = 1)
Recode a categorical variable to a numeric variable
* `x` : Variable to recode.
* `start` : Numeric value that will be set to the first category.

The codes correspond to the sorted categories.

## Examples
```julia
x = ["b", "a", "b"]
zx = recodcat2num(x)
[x zx]
recodcat2num(x; start = 0)
recodcat2num([25, 1, 25])
```
"""
function recodcat2num(x; start = 1)
    z = dummy(x).Y
    ncla = size(z, 2)
    u = z .* collect(start:(start + ncla - 1))'
    u = sum(u; dims = 2)  ;
    u = vec(u)
end

"""
    recodnum2cla(x, q)
Recode a continuous variable to classes
* `x` : Variable to recode.
* `q` : Values separating the classes.
## Examples
```julia
x = [collect(1:10); 8.1 ; 3.1]
q = [3; 8] ;
zx = recodnum2cla(x, q)
[x zx]
probs = [.33; .66]
q = Statistics.quantile(x, probs)
zx = recodnum2cla(x, q) ;
[x zx]
```
"""
function recodnum2cla(x, q)
    zx = similar(x)
    q = sort(q)
    for i = 1:length(x)
        k = 1
        for j = 1:length(q)
            x[i] > q[j] ? k = k + 1 : nothing
        end
        zx[i] = k
    end
    zx
end

"""
    replacebylev(x, lev)
Replaces the elements of x
by levels of corresponding order.

* `x` : Vector of values to replace.
* `lev` : Vector containing the levels.

The number of levels in `x` and `lev` must be the same.
Before replacement, the levels in `x` and `lev` are sorted.

## Examples
```julia
x = [10, 4, 3, 3, 4, 4]
lev = ["B"; "C"; "AA"]
[x replacebylev(x, lev)]
[string.(x) replacebylev(string.(x), lev)]

x = [10, 4, 3, 3, 4, 4]
lev = [3; 0; -1]
[x replacebylev(x, lev)]
```
"""
function replacebylev(x, lev)
    m = length(x)
    lev = sort(lev)
    nlev = length(lev)
    x_lev = tab(x).keys
    v = similar(lev, m)
    @inbounds for i = 1:nlev
        u = findall(x .== x_lev[i])
        v[u] .= lev[i]
    end
    v
end

"""
    rmcol(X, s)
Remove the columns of `X` having indexes `s`.
## Examples
```julia
X = rand(5, 3) ;
rmcol(X, 1:2)
rmcol(X, [1, 3])
```
"""
rmcol(X::AbstractMatrix, s::Union{Vector, Number}) = X[:, setdiff(1:end, Int64.(s))]
rmcol(x::Vector, s::Union{Vector, Number}) = x[setdiff(1:end, Int64.(s))]

"""
    rmrow(X, s)
Remove the rows of `X` having indexes `s`.
## Examples
```julia
X = rand(5, 2) ;
rmrow(X, 2:3)
rmrow(X, [1, 4])
```
"""
rmrow(X::AbstractMatrix, s::Union{Vector, Number}) = X[setdiff(1:end, Int64.(s)), :]
rmrow(x::Vector, s::Union{Vector, Number}) = x[setdiff(1:end, Int64.(s))]

"""
    scale(X, v)
Scale each column of `X`.
* `X` : Data.
* `v` : Scaling factors.
"""
function scale(X, v)
    M = copy(X)
    scale!(M, v)
    M
end

function scale!(X, v)
    p = size(X, 2)
    @inbounds for j = 1:p
        X[:, j] .= vcol(X, j) ./ v[j]
    end
end

# Below: Much slower and requires more memories
scale2(X, v) = mapslices(function f(x) ; x ./ v ; end, X, dims = 2)

"""
    sourcedir(path)
Include all the files contained in a directory.
"""
function sourcedir(path)
    z = readdir(path)  ## List of files in path
    n = length(z)
    for i in 1:n
        include(string(path, "/", z[i]))
    end
end

"""
    summ(X; digits = 3)
Summarize a dataset (or a variable).
"""
function summ(X; digits = 3)
    X = ensure_df(X)
    res = DataFrames.describe(X, :mean, :min, :max, :nmissing) ;
    insertcols!(res, 5, :n => size(X, 1) .- res.nmissing)
    for j = 2:4
        z = vcol(res, j)
        s = findall(isa.(z, Float64))
        res[s, j] .= round.(res[s, j], digits = digits)
        end
    (res = res, ntot = size(X, 1))
end

"""
    summ(X, group; digits = 1)
Summarize a dataset (or a variable), by group.
* `X` : Dataset (n, p) or (n,).
* `group` : A vector (n,) defing the groups.
"""
function summ(X, group; digits = 1)
    zgroup = sort(unique(group))
    for i = 1:length(zgroup)
        u = findall(group .== zgroup[i])
        z = X[u, :]
        res = summ(z).res
        println("Group: ", zgroup[i])
        println(res)
        println("") ; println("")
        #println(repeat("-", 70))
    end
end

"""
    tab(x)
Univariate tabulation.
* `x` : Univariate class membership.

In the output, the levels in `x` are sorted.
Levels and values can be get by `tab(x).keys` and `tab(x).vals`.
"""
function tab(x)
    x = vec(x)
    sort(StatsBase.countmap(x))
end

"""
    tabnum(x)
Univariate tabulation (only integer classes).
* `x` : Univariate class membership.

In the output, the levels in `x` are sorted.
"""
function tabnum(x)
    x = vec(x)
    lev = sort(unique(x))
    cnt = StatsBase.counts(x)
    cnt = cnt[cnt .> 0]
    (cnt = cnt, lev = lev)
end

"""
    vrow(X::Matrix, j)
    vrow(x::Vector, i)
    vcol(X::Matrix, j)
View of the i-th row(s) or j-th column(s) of a matrix `X`,
or of the i-th element(s) of vector `x`.
"""
vrow(X, i) = view(X, i, :)
vrow(x::Vector, i) = view(x, i)
vcol(X, j) = view(X, :, j)
vcol(x::Vector, i) = view(x, i)


## There are many Poly class methods
## Instances of Poly can be created via:
## @syms x y z
## p = poly(x^2 -x - 1)
##

const SymPoly = Sym

Base.divrem(p::Sym, q::Sym) = _sympy_meth(:div, p, q)
Base.div(p::Sym, q::Sym) = divrem(p,q)[1]
Base.rem(p::Sym, q::Sym) = divrem(p,q)[2]

## rename these, their use is special to polynomials, so we prefix
## Renamed polynomial methods.

## The functions `div`, `rem, `divrem` had slightly different meaning in
## Julia than SymPy, where they are polynomial methods. Here we rename
## them by prefixing with "poly". We do the same for `roots`, so as to
## not conflict with the `roots` function from `Polynomials.jl`.

## [This isn't really needed. We do keep the `polyroots`, as `roots` does not live in base.]

"""

Polynomial division. Renamed from `div` in SymPy to avoid confusion with Julia's `div`. Also, same as `divrem`.

"""
polydiv(ex::Sym, args...; kwargs...) = sympy_meth(:div, ex, args...; kwargs...)

"""

Polynomial division remainer.

"""
polyrem(ex::Sym, args...; kwargs...) = rem(ex, args...; kwargs...)

"""

Polynomial division with remainder. Renamed from `divrem` in SymPy to avoid confusion with Julia's `divrem`

"""
polydivrem(ex::Sym, args...; kwargs...) = _sympy_meth(:div, ex, args...; kwargs...)

"""

Find roots of a polynomial. Renamed from `roots` in
SymPy to avoid confusion with the `roots` function of `Polynomials`

"""
polyroots(ex::Sym, args...; kwargs...) = sympy_meth(:roots, ex, args...; kwargs...)
export polydiv, polyrem, polydivrem, polyroots

polynomial_sympy_methods = (
                            :groebner,
                            :solve_poly_system,
                            :resultant,
                            :cancel,
                            :apart, :together,
                            :poly,
                            :poly_from_expr,
                            :degree_list,
                            :pdiv, :prem, :pquo, :pexquo,
                            :quo, :exquo,
                            :half_gcdex, :gcdex,
                            :invert,
                            :subresultants,
                            :discriminant,
                            :terms_gcd,
                            :cofactors,
                            :gcd_list,
                            :lcm_list,
                            :monic,
                            :content,
                            :compose, :decompose,
                            :sturm,
                            :gff_list, :gff,
                            :sqf_norm, :sqf_part, :sqf_list, :sqf,
                            :factor_list,
                            :intervals,
                            :refine_root,
                            :count_roots,
                            :real_roots,
                            :nroots,
                            :ground_roots,
                            :reduced,
                            :is_zero_dimensional,
                            :symmetrize,
                            :horner,
                            :viete,
                            :construct_domain,
                            :minimal_polynomial,
                            :minpoly, :primitive_element,
                            :field_isomorphism, :to_number_field
#                            :numer#, :denom
#                            :roots ## conflict with Roots.roots and functionality provided by solve
                            )

degree(x::Sym;kwargs...) = _sympy_meth(:degree, x; kwargs...)
export degree

denom(x::Sym) = _sympy_meth(:denom, x)
numer(x::Sym) = _sympy_meth(:numer, x)
export denom, numer
Base.denominator(x::SymbolicObject) = denom(x)
Base.numerator(x::SymbolicObject) = numer(x)

polynomial_sympy_methods_base = (:expand,
                                 :factor #,
                                    #:trunc
                                    )

## SymPy Poly class

"""

Constructor for polynomials. This allows certain polynomial-specifec
methods to be called, such as `coeffs`. There is also `poly` that does
the same thing. (We keep `Poly`, as it is named like a constructor.)

Examples:
```
p = Poly(x^2 + 2x + 1, x)  #  a poly in x over Z
coeffs(p)  # [1, 2, 1]
p = Poly(a*x^2 + b*x + c, x)  # must specify variable that polynomial is over.
coeffs(p)  ## [a,b,c]
```

"""
Poly(x::Sym) = sympy."Poly"(x)
Poly(x::Sym, args...; kwargs...) = sympy_meth(:Poly, x, args...; kwargs...)
export Poly

## Poly class methods that aren't sympy methods
## e.g., must be called as obj.method(...), but not method(obj, ...)
## not coeffs(x^2 -1), rather poly(x^2 - 1)  |> coeffs
## or poly(a*x^2 + b*x + c, x) |> coeffs to specify a specific variable
polynomial_instance_methods = (:EC, :ET, :LC, :LM, :LT, :TC, ## no .abs()
                               :add_ground,
                               :all_coeffs,
                              :all_monoms, :all_roots, :all_terms,
                              :as_dict, :as_expr, :as_list,
                              :clear_denoms,
                              :deflate,
                              :exclude,
                              :l1_norm,
                              :lift,
                              :ltrim,
                              :max_norm,
                              :monoms,
                              :mul_ground,
                              :neg,
                              :nth, :nth_power_roots_poly,
                              :per,
                              :rat_clear_denoms,
                              :retract,
                              :root,
                              :set_domain, :set_modulus,
                              :sqr, :sub_ground,
                              :to_exact, :to_field, :to_ring, :total_degree,
                              :unify
)

"""

    coeffs(p, [x])

Return coefficients, `[a0, a1, ..., an]`, of a polynomial `p = a0 + a1*x + ... + an*x^n` in `D[x]`.

Note: SymPy has the `all_coeffs` function to do this for objects of class `Poly`. The SymPy `coeffs` function returns the non-zero values. Use `sympy"coeffs"` for this functionality.

This function returns the coefficients starting with the constant and
includes zeros, when present. This matches the order of the `coeffs`
function of the `Polynomials` package. This is implemented as
`reverse(all_coeffs(Poly(p,[x])))`.

The expression is converted to a object of class `Poly`. This requires a choices of symbol after which the expression is viewed as a univariate polynomial over a domain. If no symbol is passed in, a heuristic is used, for convenience: when only one free symbol is present it will be used; if a symbol "x" is present it will be used; otherwise the first symbol returned by `free_symbols` will be used. This will clearly fail for constant polynomials.

Examples:
```
@vars x a b c
ex = 2x^2 + 3x + 4
coeffs(ex)  # [4, 3, 2], same as `coeffs(ex, x)`

ex = a*x^2 + b*x + c
coeffs(ex)  # [c, b, a], same as `coeffs(ex, x)`

## may need to be explicit if `x` not present:
@vars y
ex = a*y^2 + b*y + c
coeffs(ex)  # not [c,b,a] as `c` is first free symbol
coeffs(ex, y)  # [c, b, a]
```
"""
function coeffs(p::Sym, x::Sym)
    q = Poly(p, x)
    reverse(object_meth(q, :all_coeffs))
end
function coeffs(p::Sym)
    vars = free_symbols(p)
    length(vars) == 0 && DomainError("No free variables specified")
    if length(vars) == 1
        return coeffs(p, vars[1])
    else
        for var in vars
            if "x" == string(var)
                return coeffs(p, var)
            end
        end
        return coeffs(p, vars[1])
    end
end
export coeffs

polynomial_predicates = (
                         :is_cyclotomic,
                         :is_ground,
                         :is_homogeneous,
                         :is_irreducible,
                         :is_linear,
                         :is_monic,
                         :is_monomial,
                         :is_multivariate,
                         :is_one,
#                        :is_primitive,
                         :is_quadratic,
                         :is_sqf,
                         :is_univariate,
                         :is_zero)

is_primitive(x::Sym) = x.is_primitive
export is_primitive

##
"""

    interpolate([xs], ys, x)

Find interpolating polynomial in the variable x through the points (xs[1], ys[1]), ..., (xs[n], ys[n])

Examples
```jltest
@vars x
interpolate(sin.(1:4), x)          # uses xs = 1:length(ys(
interpolate(1:4, sin.(1:4), x)     # xs, ys
interpolate([(1,2), (2,3), (3,2)], x)  # [(x1,y1), ..., (xn, yn)]
```
"""
interpolate(xsys::AbstractVector, x) = _sympy_meth(:interpolate, xsys, x)
interpolate(xs::AbstractVector, ys::AbstractVector, x::Sym) = interpolate(collect(zip(xs, ys)), x)
export interpolate


# by lazarusA # HIDE
# using GLMakie # HIDE
using CairoMakie
let
    xs = LinRange(-3, 3, 20)
    ys = LinRange(-3, 3, 20)
    us = [x + y for x in xs, y in ys]
    vs = [y - x for x in xs, y in ys]
    strength = vec(sqrt.(us .^2 .+ vs .^2))

    fig = Figure(resolution = (550, 450), fontsize = 18, font = "sans")
    ax = Axis(fig, xlabel = "x", ylabel = "y", aspect = 1)
    arrows!(ax, xs, ys, us, vs, arrowsize = 10, lengthscale = 0.1,
        arrowcolor = strength, linecolor = strength, colormap = :plasma)
    cbar = Colorbar(fig, limits =(minimum(strength),maximum(strength)),
    nsteps =100, colormap = :plasma, ticksize=15, width = 15, tickalign=1)
    limits!(ax, -3,3,-3,3)
    fig[1,1] = ax
    fig[1,2] = cbar
    fig
    #save("arrows.png", fig, px_per_unit = 2)
    save(joinpath(@__DIR__, "output", "arrows.png"), fig, px_per_unit = 2) # HIDE
end
using Pkg # HIDE
Pkg.status("CairoMakie") # HIDE


# ---
# jupyter:
#   jupytext:
#     formats: ipynb,jl:light
#     text_representation:
#       extension: .jl
#       format_name: light
#       format_version: '1.4'
#       jupytext_version: 1.2.4
#   kernelspec:
#     display_name: Julia 1.1.0
#     language: julia
#     name: julia-1.1
# ---

# # This notebook
#
# - [x] read gridded output + `float_trajectory*data` => `uvetc` dictionnary.
# - [x] recompute `u,v` from gridded output
# - [x] compare with `u,v` from `float_traj*data`
# - [x] solve for float trajectory with `DifferentialEquations.jl`
#
# _Notes:_ For documentation see <https://gaelforget.github.io/MeshArrays.jl/stable/>, <https://docs.juliadiffeq.org/latest/solvers/ode_solve.html> and <https://en.wikipedia.org/wiki/Displacement_(vector)>

# ## 1. import software

using IndividualDisplacements, MeshArrays, DifferentialEquations, Plots
p=dirname(pathof(IndividualDisplacements))
include(joinpath(p,"PlotIndDisp.jl"))

# ## 2. reload trajectories from `MITgcm/pkg/flt`

dirIn="flt_example/"
prec=Float32
df=IndividualDisplacements.ReadDisplacements(dirIn,prec)
PyPlot.figure(); PlotBasic(df,300,100000.0)

# ## 3. Read gridded variables via `MeshArrays.jl`
#
# Put grid variables in a dictionary.
#
# _Note:_ `myread` function deals with tiled files from `flt_example/`.

# +
import IndividualDisplacements: myread

mygrid=gcmgrid("flt_example/","ll",1,[(80,42)], [80 42], Float32, read, write)
nr=8

GridVariables=Dict("XC" => myread(mygrid.path*"XC",MeshArray(mygrid,Float32)),
"YC" => myread(mygrid.path*"YC",MeshArray(mygrid,Float32)),
"XG" => myread(mygrid.path*"XG",MeshArray(mygrid,Float32)),
"YG" => myread(mygrid.path*"YG",MeshArray(mygrid,Float32)),
"dx" => 5000.0);
# -

# Put velocity fields in in another dictionary and merge the two dictionaries.

# +
t0=0.0 #approximation / simplification
t1=18001.0*3600.0

u0=myread(mygrid.path*"U.0000000001",MeshArray(mygrid,Float32,nr))
u1=myread(mygrid.path*"U.0000018001",MeshArray(mygrid,Float32,nr))
v0=myread(mygrid.path*"V.0000000001",MeshArray(mygrid,Float32,nr))
v1=myread(mygrid.path*"V.0000018001",MeshArray(mygrid,Float32,nr))

kk=3 #3 to match -1406.25 in pkg/flt output
u0=u0[:,kk]; u1=u1[:,kk];
v0=v0[:,kk]; v1=v1[:,kk];

u0=u0./GridVariables["dx"]
u1=u1./GridVariables["dx"]
v0=v0./GridVariables["dx"]
v1=v1./GridVariables["dx"]

uvt = Dict("u0" => u0, "u1" => u1, "v0" => v0, "v1" => v1, "t0" => t0, "t1" => t1)

uvetc=merge(uvt,GridVariables);
# -

# ## 4. Visualize velocity fields

# +
mskW=myread(mygrid.path*"hFacW",MeshArray(mygrid,Float32,nr))
mskW=1.0 .+ 0.0 * mask(mskW[:,kk],NaN,0.0)
mskS=myread(mygrid.path*"hFacS",MeshArray(mygrid,Float32,nr))
mskS=1.0 .+ 0.0 * mask(mskS[:,kk],NaN,0.0)

msk=Dict("mskW" => mskW, "mskS" => mskS)

uvetc=merge(uvetc,msk);
# -

heatmap(mskW[1,1].*u0[1,1],title="U at the start")

heatmap(mskS[1,1].*v0[1,1],title="V at the start")

heatmap(mskW[1,1].*u1[1,1]-u0[1,1],title="U end - U start")

# ## 5. Visualize trajectories from `MITgcm/pkg/flt`
#
# Select one trajectory

tmp=df[df.ID .== 200, :]
tmp[1:4,:]

# Super-impose trajectory over velocity field (first for u ...)

PyPlot.contourf(GridVariables["XG"].f[1], GridVariables["YC"].f[1], mskW.f[1].*u0.f[1])
PyPlot.plot(tmp[:,:lon],tmp[:,:lat],color="r")
colorbar()

# Super-impose trajectory over velocity field (... then for v)

PyPlot.contourf(GridVariables["XG"].f[1], GridVariables["YC"].f[1], mskS.f[1].*v0.f[1])
PyPlot.plot(tmp[:,:lon],tmp[:,:lat],color="r")
colorbar()

# ## 6. Recompute displacements from gridded flow fields

# +
comp_vel=IndividualDisplacements.VelComp
get_vel=IndividualDisplacements.VelCopy

uInit=[tmp[1,:lon];tmp[1,:lat]]./uvetc["dx"]
nSteps=Int32(tmp[end,:time]/3600)-2
du=fill(0.0,2);
# -

# Visualize and compare with actual grid point values -- jumps on the tangential component are expected with linear scheme:

tmpu=fill(0.0,100)
tmpv=fill(0.0,100)
tmpx=fill(0.0,100)
for i=1:100
    tmpx[i]=500.0 *i./uvetc["dx"]
    comp_vel(du,[tmpx[i];0.499./uvetc["dx"]],uvetc,0.0)
    tmpu[i]=du[1]
    tmpv[i]=du[2]
end
Plots.plot(tmpx,tmpu)
Plots.plot!(uvetc["XG"].f[1][1:10,1]./uvetc["dx"],uvetc["u0"].f[1][1:10,1],marker=".")
Plots.plot!(tmpx,tmpv)
Plots.plot!(uvetc["XG"].f[1][1:10,1]./uvetc["dx"],uvetc["v0"].f[1][1:10,1],marker=".")

tmpu=fill(0.0,100)
tmpv=fill(0.0,100)
tmpy=fill(0.0,100)
for i=1:100
    tmpy[i]=500.0 *i./uvetc["dx"]
    comp_vel(du,[0.499./uvetc["dx"];tmpy[i]],uvetc,0.0)
    tmpu[i]=du[1]
    tmpv[i]=du[2]
end
Plots.plot(tmpx,tmpu)
Plots.plot!(uvetc["YG"].f[1][1,1:10]./uvetc["dx"],uvetc["u0"].f[1][1,1:10],marker=".")
Plots.plot!(tmpx,tmpv)
Plots.plot!(uvetc["YG"].f[1][1,1:10]./uvetc["dx"],uvetc["v0"].f[1][1,1:10],marker=".")

# Compare recomputed velocities with those from `pkg/flt`

nSteps=2998
tmpu=fill(0.0,nSteps); tmpv=fill(0.0,nSteps);
tmpx=fill(0.0,nSteps); tmpy=fill(0.0,nSteps);
refu=fill(0.0,nSteps); refv=fill(0.0,nSteps);
for i=1:nSteps
    get_vel(du,[tmp[i,:lon],tmp[i,:lat]],tmp,tmp[i,:time])
    refu[i]=du[1]./uvetc["dx"]
    refv[i]=du[2]./uvetc["dx"]
    comp_vel(du,[tmp[i,:lon],tmp[i,:lat]]./uvetc["dx"],uvetc,tmp[i,:time])
    tmpu[i]=du[1]
    tmpv[i]=du[2]
end
#
Plots.plot(tmpu)
Plots.plot!(tmpv)
Plots.plot!(refu)
Plots.plot!(refv)

# ## 6. Recompute trajectories from gridded flow fields
#
# Solve through time using `DifferentialEquations.jl`

using DifferentialEquations
tspan = (0.0,nSteps*3600.0)
#prob = ODEProblem(get_vel,uInit,tspan,tmp)
prob = ODEProblem(comp_vel,uInit,tspan,uvetc)
sol = solve(prob,Tsit5(),reltol=1e-8,abstol=1e-8)
sol[1:4]

# Compare recomputed trajectories with those from `pkg/flt`

# +
ref=transpose([tmp[1:nSteps,:lon] tmp[1:nSteps,:lat]])
maxLon=80*5.e3
maxLat=42*5.e3
show(size(ref))
for i=1:nSteps-1
    ref[1,i+1]-ref[1,i]>maxLon/2 ? ref[1,i+1:end]-=fill(maxLon,(nSteps-i)) : nothing
    ref[1,i+1]-ref[1,i]<-maxLon/2 ? ref[1,i+1:end]+=fill(maxLon,(nSteps-i)) : nothing
    ref[2,i+1]-ref[2,i]>maxLat/2 ? ref[2,i+1:end]-=fill(maxLat,(nSteps-i)) : nothing
    ref[2,i+1]-ref[2,i]<-maxLat/2 ? ref[2,i+1:end]+=fill(maxLat,(nSteps-i)) : nothing
end
ref=ref./uvetc["dx"]

using Plots
Plots.plot(sol[1,:],sol[2,:],linewidth=5,title="Using Recomputed Velocities",
     xaxis="lon",yaxis="lat",label="Julia Solution") # legend=false
Plots.plot!(ref[1,:],ref[2,:],lw=3,ls=:dash,label="MITgcm Solution")
# -


### Compute acceptance or rejection rate of MCChain

function acceptance(c::MCChain; lags::Range=1:size(c.samples, 1), reject::Bool=false)
  rlen = length(lags)
  @assert lags[end] <= size(c.samples, 1) "Range of acceptance rate not within post-burnin range of MCmc chain"

  if reject
    return (rlen-sum(c.diagnostics["accept"][lags]))*100/rlen
  else
    return sum(c.diagnostics["accept"][lags])*100/rlen
  end
end

### describe() provides summary statistics for MCChain objects

describe(c::MCChain) = describe(STDOUT, c)

function describe(io, c::MCChain)
  nsamples, npars = size(c.samples)
  for i in 1:npars

    col = c.samples[:, i]
    println(io, "Parameter $i")

    if sum(isnan(col)) != 0
      println(col, "Monte Carlo chain for parameter $i contains NaNs, which are not supported.")
      return
    end

    qs = quantile(col, [0, 1])
    varimse = mcvar_imse(col)
    variid = var(col)/nsamples
    mcerror = sqrt(varimse)
    ss = nsamples*variid./varimse
    act = varimse./variid

    statNames = ["Min", "Mean", "Max", "MC Error", "ESS", "AC Time"]
    statVals = [qs[1], mean(col), qs[2], mcerror, ss, act]
    for i = 1:6
        println(io, string(rpad(statNames[i], 10, " "), " ", string(statVals[i])))
    end

    println(io, )
  end
end


using JuMP
using DecisionProgramming
using RobustDecisionProgramming

function partial_expected_value(q, s_I, model, S, C, X, U⁺, z, k, hard_lower_bound, fixed)
    X′ = copy(X)
    X′[k].data[s_I..., :] = q
    P = DefaultPathProbability(C, X′)
    π_s = PathProbabilityVariables(model, z, S, P; hard_lower_bound=hard_lower_bound, fixed=fixed)
    pev = expected_value(model, π_s, U⁺)
    return pev
end

function minimum_partial_expected_value(dev, I_j, s_I, model, S, C, X, U⁺, z, k, hard_lower_bound)
    Q = polyhedral_uncertainty_set(dev)
    fixed = Dict{Node, State}(i=>s_i for (i, s_i) in zip(I_j, s_I))
    pevs = [partial_expected_value(q, s_I, model, S, C, X, U⁺, z, k, hard_lower_bound, fixed) for q in Q]
    x = @variable(model)
    for pev in pevs
        @constraint(model, x ≤ pev)
    end
    return (x=x, pevs=pevs, Q=Q, s_I=s_I)
end

"""Minimum expected value over discrete polyhedral uncertainty set."""
function min_expected_value(devs, model::Model, S::States, C::Vector{ChanceNode}, X, U⁺, z::DecisionVariables, k::Int; hard_lower_bound::Bool=false)
    mpevs = [minimum_partial_expected_value(dev, C[k].I_j, s_I, model, S, C, X, U⁺, z, k, hard_lower_bound) for (s_I, dev) in devs]
    min_ev = sum(mpev.x for mpev in mpevs)
    return (min_ev, mpevs)
end

"""Find distribution from uncertainty set that minimizes the partial expected value."""
function min_distribution(pevs, Q)
    return Q[argmin([value(pev) for pev in pevs])]
end

"""Consruct the probabilities that minimize the expected value."""
function min_probabilities(mpevs, X, k)
    X′ = copy(X)
    for (_, pevs, Q, s_I) in mpevs
        X′[k].data[s_I..., :] = min_distribution(pevs, Q)
    end
    return X′
end


export exp_J2!,
       exp_J1!

"""
    exp_J2!(state::Vector{T},
            basis::Vector{Int},
            α::AbstractMatrix{T}) where {T<:Number}

Apply the exponential operator e^{∑_pq α_pq n_p n_q} to `state`.
"""
function exp_J2!(state::Vector{T},
                 basis::Vector{Int},
                 α::AbstractMatrix{T}) where {T<:Number}
    # Deduce the number of sites from the highest basis state.
    L = 0
    max_bs = basis[end]
    for i=0:31 # Arbitrary very high number.
        if (max_bs>>i)&1 == 1
            L = i+1
        end
    end

    for (ibs, bs) in enumerate(basis)
        c = zero(T)
        for i=0:L-1
            (bs>>i)&1 != 1 && continue
            c += α[i+1, i+1]
            for j=i+1:L-1
                if (bs>>j)&1 == 1
                    c += α[i+1, j+1] + α[j+1, i+1]
                end
            end
        end
        state[ibs] *= exp(c)
    end
    return state
end

"""
    exp_J1!(state::Vector{T},
            basis::Vector{Int},
            α::AbstractMatrix{T}) where {T<:Number}

Apply the exponential operator e^{∑_p α_p n_p} to `state`.
"""
function exp_J1!(state::Vector{T},
                 basis::Vector{Int},
                 α::Vector{T}) where {T<:Number}
    # Deduce the number of sites from the highest basis state.
    L = 0
    max_bs = basis[end]
    for i=0:31 # Arbitrary very high number.
        if (max_bs>>i)&1 == 1
            L = i+1
        end
    end

    for (ibs, bs) in enumerate(basis)
        c = zero(T)
        for i=0:L-1
            if (bs>>i)&1 == 1
                c += α[i+1]
            end
        end
        state[ibs] *= exp(c)
    end
    return state
end


module DifferentialGeometries

include("implementation.jl")

export AbstractCoordinateSystem, CoordinateSystem
export AbstractScalar, Scalar
export Covariant, Contravariant
export ToCartesian, FromCartesian
export CovariantVector, ContravariantVector
export ∇, ∂, grad, div, ∇o, curl, ∇x, J, dV, dl, gᵢⱼ, gⁱʲ, J⃗
export invert

end