using MPI
using Test
using LinearAlgebra
using Random
using StaticArrays
using ClimateMachine
using ClimateMachine.SystemSolvers
using ClimateMachine.MPIStateArrays
using CUDAapi
using Random
using KernelAbstractions
using CuArrays

let
    Random.seed!(1235)
    if CUDAapi.has_cuda_gpu()
        arrays = [Array, CuArray]
    else
        arrays = [Array]
    end
    # Initialize
    ClimateMachine.init()
    T = Float64
    mpicomm = MPI.COMM_WORLD
    # set the error threshold
    err_thresh = 1e-8

    @kernel function multiply_A_kernel!(x, A, y, n1, n2)
        I = @index(Global)
        for i in 1:n1
            tmp = zero(eltype(x))
            for j in 1:n2
                tmp += A[i, j, I] * y[j, I]
            end
            x[i, I] = tmp
        end
    end

    function multiply_by_A!(
        x,
        A,
        y,
        n1,
        n2;
        ndrange = size(x[1, :]),
        cpu_threads = Threads.nthreads(),
        gpu_threads = 256,
    )
        if isa(x, Array)
            kernel! = multiply_A_kernel!(CPU(), cpu_threads)
        else
            kernel! = multiply_A_kernel!(CUDA(), gpu_threads)
        end
        return kernel!(x, A, y, n1, n2, ndrange = ndrange)
    end
    # for defining linear_operator
    function closure_linear_operator_multi!(A, n1, n2, n3)
        function linear_operator!(x, y)
            event = multiply_by_A!(x, A, y, n1, n2, ndrange = n3)
            wait(event)
            return nothing
        end
    end

    # Test 1: Basic Functionality
    for array in arrays
        ArrayType = array
        n = 100  # size of vector space
        ni = 10 # number of independent linear solves
        b = ArrayType(randn(n, ni)) # rhs
        x = ArrayType(randn(n, ni)) # initial guess
        A = ArrayType(randn((n, n, ni)) ./ sqrt(n) .* 1.0)
        for i in 1:n
            A[i, i, :] .+= 1.0
        end
        ss = size(b)[1]
        gmres = BatchedGeneralizedMinimalResidual(
            b,
            ArrayType = ArrayType,
            subspace_size = ss,
        )
        for i in 1:ni
            x[:, i] = A[:, :, i] \ b[:, i]
        end
        sol = copy(x)
        x += ArrayType(randn(n, ni) * 0.01 * maximum(abs.(x)))
        y = copy(x)
        linear_operator! = closure_linear_operator_multi!(A, size(A)...)
        iters = linearsolve!(linear_operator!, gmres, x, b; max_iters = ss)
        linear_operator!(y, x)
        test_name = "(" * string(ArrayType) * ") Basic Test"
        @testset "$test_name" begin
            @test norm(y - b) / norm(b) < err_thresh
        end
    end
    ###
    # Test 2: MPI State Array
    for array in arrays
        ArrayType = array
        Random.seed!(1235)
        n1 = 8
        n2 = 3
        n3 = 10
        mpi_b = MPIStateArray{T}(mpicomm, ArrayType, n1, n2, n3)
        mpi_x = MPIStateArray{T}(mpicomm, ArrayType, n1, n2, n3)
        mpi_A = ArrayType(randn(n1 * n2, n1 * n2, n3))

        # need to make sure that mpi_b and mpi_x are reproducible
        mpi_b.data[:] .= ArrayType(randn(n1 * n2 * n3))
        mpi_x.data[:] .= ArrayType(randn(n1 * n2 * n3))
        mpi_y = copy(mpi_x)

        # for defining linear_operator
        function closure_linear_operator_mpi!(A, n1, n2, n3)
            function linear_operator!(x, y)
                alias_x = reshape(x.data, (n1, n3))
                alias_y = reshape(y.data, (n1, n3))
                event =
                    multiply_by_A!(alias_x, A, alias_y, n1, n2, ndrange = n3)
                wait(event)
                return nothing
            end
        end

        gmres = BatchedGeneralizedMinimalResidual(
            mpi_b,
            ArrayType = ArrayType,
            m = n1 * n2,
            n = n3,
        )

        # Now define the linear operator
        linear_operator! = closure_linear_operator_mpi!(mpi_A, size(mpi_A)...)
        iters = linearsolve!(
            linear_operator!,
            gmres,
            mpi_x,
            mpi_b;
            max_iters = n1 * n2,
        )
        linear_operator!(mpi_y, mpi_x)
        # check one in the batch
        sol = mpi_A[:, :, 1] \ mpi_b.data[:, :, 1][:]
        test_name = "(" * string(ArrayType) * ") MPIStaterray Test"
        @testset "$test_name" begin
            @test norm(mpi_y.data - mpi_b.data) / norm(mpi_b) < err_thresh
            @test norm(sol - mpi_x.data[:, :, 1][:]) < err_thresh
        end
    end

    ###
    # Test 3: Columnwise test (CPU ONLY)
    Random.seed!(1235)
    # ## More Complex Example
    function closure_linear_operator!(A, tup)
        function linear_operator!(y, x)
            alias_x = reshape(x, tup)
            alias_y = reshape(y, tup)
            for i6 in 1:tup[6]
                for i4 in 1:tup[4]
                    for i2 in 1:tup[2]
                        for i1 in 1:tup[1]
                            tmp = alias_x[i1, i2, :, i4, :, i6][:]
                            tmp2 = A[i1, i2, i4, i6] * tmp
                            alias_y[i1, i2, :, i4, :, i6] .=
                                reshape(tmp2, (tup[3], tup[5]))
                        end
                    end
                end
            end
        end
    end

    tup = (3, 4, 7, 6, 5, 2)

    B = [
        randn(tup[3] * tup[5], tup[3] * tup[5])
        for i1 in 1:tup[1], i2 in 1:tup[2], i4 in 1:tup[4], i6 in 1:tup[6]
    ]
    columnwise_A = [
        B[i1, i2, i4, i6] + 10I
        for i1 in 1:tup[1], i2 in 1:tup[2], i4 in 1:tup[4], i6 in 1:tup[6]
    ]
    # taking the inverse of A isn't great, but it is convenient
    columnwise_inv_A = [
        inv(columnwise_A[i1, i2, i4, i6])
        for i1 in 1:tup[1], i2 in 1:tup[2], i4 in 1:tup[4], i6 in 1:tup[6]
    ]
    columnwise_linear_operator! = closure_linear_operator!(columnwise_A, tup)
    columnwise_inverse_linear_operator! =
        closure_linear_operator!(columnwise_inv_A, tup)

    mpi_tup = (tup[1] * tup[2] * tup[3], tup[4], tup[5] * tup[6])
    b = randn(mpi_tup)
    x = copy(b)
    columnwise_inverse_linear_operator!(x, b)
    x += randn((tup[1] * tup[2] * tup[3], tup[4], tup[5] * tup[6])) * 0.1

    reshape_tuple_f = tup
    permute_tuple_f = (5, 3, 1, 4, 2, 6)

    gmres = BatchedGeneralizedMinimalResidual(
        b,
        m = tup[3] * tup[5],
        n = tup[1] * tup[2] * tup[4] * tup[6],
        reshape_tuple_f = reshape_tuple_f,
        permute_tuple_f = permute_tuple_f,
        atol = eps(T),
        rtol = eps(T),
    )

    x_exact = copy(x)
    iters = linearsolve!(
        columnwise_linear_operator!,
        gmres,
        x,
        b,
        max_iters = tup[3] * tup[5],
    )

    @testset "(Array) Columnwise Test" begin
        columnwise_inverse_linear_operator!(x_exact, b)
        @test norm(x - x_exact) / norm(x_exact) < err_thresh
        columnwise_linear_operator!(x_exact, x)
        @test norm(x_exact - b) / norm(b) < err_thresh
    end
end


using Test
using Sirion

function testBackAndForthPressureEnergy()
    d_list = [10.0^n for n in -5:5]
    p_list = [10.0^n for n in -5:5]
    eos = Sirion.IdealGas(5.0/3.0)
    for d in d_list
        for p in p_list
            e = Sirion.calcSpecificThermalEnergy(eos, d, p)
            pr = Sirion.calcPressure(eos, d, e)
            if !(pr ≈ p)
                return false
            end
        end
    end
    return true
end

@test testBackAndForthPressureEnergy()


# This file contains algorithms needed for the solution of
# the parametric Risch differential equation from chapter 7 of the book
#
#    Manuel Bronstein, Symbolic Integration I: Transcendental Functions,
#    2nd ed., Springer 2005.
#

"""
    ParamRdeNormalDenominator(f, gs, D) -> (a, b, Gs, h)

Normal part of the denominator.

Given a field `k`, a derivation `D` on `k[t]`, `f` in `k(t)` which is weakly normalized with respect to `t`,
and `gs=[g₁,...,gₘ]` with `gᵢ` in `k(t)`, return  `a`, `h` in `k[t]`, `b` in `k⟨t⟩` and  `Gs=[G₁,...,Gₘ]`
with `G_i` in `k(t)` such that for any solution `c₁`,...,`cₘ` in `Const(k)` and `y` in `k(t)`
of `D(y)+f*y=∑ᵢcᵢ*gᵢ`, `q=y*h` in `k⟨t⟩` satisfies `a*D(q)+b*q=∑ᵢcᵢ*Gᵢ`.

(Here, `k⟨t⟩` denotes the elements of `k(t)` which are reduced w.r.t. `D`.)

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 219.
"""
function ParamRdeNormalDenominator(f::F, gs::Vector{F}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}, F<:FracElem{P}}
    iscompatible(f, D) && all(iscompatible(g, D) for g in gs) ||
        error("rational functions f and g_i must be in the domain of derivation D")
    # Note: f must be weakly normalized which we do not check. It is recommended
    # to use this function only for rational functions f which were computed by WeakNormalizer.
    (dn, ds) = SplitFactor(denominator(f), D)
    (en, es) = SplitFactor(lcm([denominator(g) for g in gs]), D)
    p = gcd(dn, en)
    h = divexact(gcd(en, derivative(en)), gcd(p, derivative(p)))
    dnh2 = dn*h^2
    dn*h, dn*h*f-dn*D(h), [dnh2*g for g in gs], h
end

"""
    ParamRdeSpecialDenomExp(a, b, gs, D) -> (A, B, Gs, h)

Special part of the denominator - hyperexponential case.

Given a field `k`, a derivation `D` on `k[t]` with `D(t)/t` in `k`, `a` in `k[t]`, `b` in `k⟨t⟩`, `gs=[g₁,...,gₘ]`
with `gᵢ` in `k(t)` , `a≠0` and `gcd(a,t)=1`, return `A`, `B`, `h` in `k[t]`,  `Gs=[G₁,...,Gₘ]` with `Gᵢ` in `k(t)`
such that for any solution `c₁`,...,`cₘ` in `Const(k)` and `q` in `k⟨t⟩`
of `a*D(q)+b*q=∑ᵢcᵢ*gᵢ`, `r=q*h` in `k[t]` satisfies `A*D(r)+B*r=∑ᵢcᵢ*Gᵢ`.

(Here, `k⟨t⟩` denotes the elements of `k(t)` which are reduced w.r.t. `D`.)

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 221.
"""
function ParamRdeSpecialDenomExp(a::P, b::F, gs::Vector{F}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}, F<:FracElem{P}}
    ishyperexponential(D) ||
        error("monomial of derivation D must be hyperexponential")
    iscompatible(a, D) && iscompatible(b, D) && all([iscompatible(g, D) for g in gs]) ||
        error("polynomial a and rational functions b and g_i must be in the domain of derivation D")
    !iszero(a) || error("a must be != 0")
    isreduced(b, D) ||
        error("rational function b must be reduced with respect to derivation D")
    t = gen(parent(a))
    degree(gcd(t, a))==0 || error("gcd(a, t) must be == 1")
    p = t
    nb = iszero(b) ? typemax(Int) : valuation(b, p)
    nc = minimum([iszero(g) ? typemax(Int) : valuation(g, p) for g in gs])
    n = min(0, nc - min(0, nb))
    if nb==0
        α = constant_coefficient(Remainder(-b//a, p))
        w = coeff(MonomialDerivative(D), 1)
        n0, s, z, ρ = ParametricLogarithmicDerivative(α, w, BaseDerivation(D))
        if ρ<0
            error("ParametricLogarithmicDerivative failed")
        end
        if  ρ>0 && n0==1 && !iszero(z)
            n = min(n, s)
        end
    end
    N = max(0, -nb)
    p_power_N = p^N
    p_power_N_minus_n = p^(N-n)
    b1 = (b+n*a*divexact(D(p), p))*p_power_N
    @assert isone(denominator(b1))
    a*p_power_N, numerator(b1), [g*p_power_N_minus_n for g in gs], p^(-n)
end

"""
    ParamRdeSpecialDenomTan(a, b, gs, D) -> (A, B, Gs, h)

Special part of the denominator - hypertangent case.

Given a field `k` not containing `√-1`, a derivation `D` on `k[t]` with `D(t)/(t^2+1)` in `k`,
`a≠0` in `k[t]` with `gcd(a,t^2+1)=1`, `b` in `k⟨t⟩`, and `gs=[g₁,...,gₘ]` with `gᵢ` in `k(t)`,
return  `A`, `B`, `h` in `k[t]` and `Gs=[G₁,...,Gₘ]` with `Gᵢ` in `k(t)`
such that for any solution `c₁`,...,`cₘ` in `Const(k)` and `q` in `k⟨t⟩`
of `a*D(q)+b*q=∑ᵢcᵢ*gᵢ`, `r=q*h` in `k[t]` satisfies `A*D(r)+B*r=∑ᵢcᵢ*Gᵢ`.

(Here, `k⟨t⟩` denotes the elements of `k(t)` which are reduced w.r.t. `D`.)

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 222.
"""
function ParamRdeSpecialDenomTan(a::P, b::F, gs::Vector{F}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}, F<:FracElem{P}}
    !iszero(a) || error("a must be != 0")
    ishypertangent(D) ||
        error("monomial of derivation D must be hypertangent")
    iscompatible(a, D) && iscompatible(b, D) && all([iscompatible(g, D) for g in gs]) ||
        error("polynomial a and rational functions b and g_i must be in the domain of derivation D")
    isreduced(b, D) ||
        error("rational function b must be reduced with respect to derivation D")
    t = gen(parent(a))
    p = t^2+1
    degree(gcd(a, p))==0 || error("gcd(a, t^2+1) must be == 1")
    nb = iszero(b) ? typemax(Int) : valuation(b, p)
    nc = minimum([iszero(g) ? typemax(Int) : valuation(g, p) for g in gs])
    n = min(0, nc - min(0, nb))
    if nb==0
        αI_plus_β = Remainder(-b//a, p)
        α = coeff(αI_plus_β, 1)
        β = coeff(αI_plus_β, 0)
        η = divexact(MonomialDerivative(D), p)
        v, ρ = InFieldDerivative(2*β, D0)
        D0 = BaseDerivation(D)
        if ρ>0 && !iszero(v)
            _, I, D0I = Complexify(parent(η), D0)
            n0, m, v, ρ = ParametricLogarithmicDerivative(α*I+β, 2*η*I, D0I)
            if  ρ>0 && n0==1 && !iszero(v)
                n = min(n, m)
            end
        end
    end
    N = max(0, -nb)
    p_power_N = p^N
    b1 = (b+n*a*divexact(D(p), p))*p_power_N
    @assert isone(denominator(b1))
    a*p_power_N, numerator(b1), [g*p_power_N_minus_n for g in gs], p^(-n)
end

"""
    ParamRdeSpecialDenominator(a, b, gs, D) -> (A, B, Gs, h)

Special part of the denominator.

Given a field `k`, a derivation `D` on `k[t]`, `a≠0` in `k[t]`, `b` in `k⟨t⟩`, `gs=[g₁,...,gₘ]` with
`g₁`,...,`gₘ` in `k(t)`, return  `A`, `B`, `h` in `k[t]` and `Gs=[G₁,...,Gₘ]` with `Gᵢ` in `k(t)`
such that for any solution `c₁`,...,`cₘ` in `Const(k)` and `q` in `k⟨t⟩`
of `a*D(q)+b*q=∑ᵢcᵢ*gᵢ`, `r=q*h` in `k[t]` satisfies `A*D(r)+B*r=∑ᵢcᵢ*Gᵢ`.

(Here, `k⟨t⟩` denotes the elements of `k(t)` which are reduced w.r.t. `D`.)

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 221.
"""
function ParamRdeSpecialDenominator(a::P, b::F, gs::Vector{F}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}, F<:FracElem{P}}
    iscompatible(a, D) && iscompatible(b, D) && all([iscompatible(g, D) for g in gs]) ||
        error("polynomial a and rational functions b and g_i must be in the domain of derivation D")
    !iszero(a) || error("a must be != 0")
    isreduced(b, D) ||
        error("rational function b must be reduced with respect to derivation D")
    if isprimitive(D)
        @assert isone(denominator(b))
        return a, numerator(b), gs, one(b)
    elseif ishyperexponential(D)
        t = gen(parent(a))
        d = gcd(a, t)
        a = divexact(a, d)
        b = b//d
        gs = [g//d for g in gs]
        return ParamRdeSpecialDenomExp(a, b, gs, D)
    elseif ishypertangent(D)
        t = gen(parent(a))
        p = t^2 + 1
        d = gcd(a, p)
        a = divexact(a, d)
        b = b//d
        gs = [g//d for g in gs]
        return ParamRdeSpecialDenomTan(a, b, gs, D)
    else
        H = MonomialDerivative(D)
        throw(NotImplementedError("ParamRdeSpecialDenominator: monomial derivative $H\n@ $(@__FILE__):$(@__LINE__)"))
    end
end

"""
    LinearConstraints(a, b, gs, D) -> (qs, M)

Generate linear constraints on the constants.

Given a field `k`, a derivation `D` on `k[t]`, `a`, `b` in `k[t]` and `gs=[g₁,...,gₘ]` with
`gᵢ` in `k(t)`, return `qs=[q₁,...,qₘ]` with `qᵢ` in `k[t]`
and a matrix `M` with entries in `k` sucht that for any solution
`c₁`,...,`cₘ` in `Const(k)` and `p` in `k[t]` of  `a*D(p)+b*p=∑ᵢcᵢ*gᵢ`,
`x=[c₁,...,cₘ]` is a solution of `M*x=0`, and `p` and the `cᵢ` satisfy
 `a*D(p)+b*p=∑ᵢcᵢ*qᵢ`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 223.
"""
function LinearConstraints(a::P, b::P, gs::Vector{F}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}, F<:FracElem{P}}
    iscompatible(a, D) && iscompatible(b, D) && all([iscompatible(g, D) for g in gs]) ||
        error("polynomials a and b and rational functions g_i must be in the domain of derivation D")
    d = lcm([denominator(g) for g in gs])
    qrs = [divrem(numerator(d*g), d) for g in gs]
    qs = [q for (q, r) in qrs]
    rs = [r for (q, r) in qrs]
    if all([iszero(r) for r in rs])
        N = -1
    else
        N = maximum([degree(r) for r in rs])
    end
    M = [coeff(r, i) for i=0:N, r in rs]
    return qs, M
end

RowEchelon(A::Matrix{T}; cut_zero_rows::Bool=true) where T<:FieldElement = RowEchelon(A, T[], cut_zero_rows=cut_zero_rows)

function RowEchelon(A::Matrix{T}, u::Vector{T}; cut_zero_rows::Bool=true) where T<:FieldElement
    # based on https://github.com/blegat/RowEchelon.jl/blob/master/src/RowEchelon.jl
    # Note: input arguments A, u are changed on output
    nr, nc = size(A)
    uzero = length(u)==0
    i = 1
    j = 1
    while i <= nr && j <= nc
        p = findfirst(x->!iszero(x), A[i:nr,j])
        if p===nothing
            j += 1
        else
            p = p + i - 1
            for k=j:nc
                A[i, k], A[p, k] = A[p, k], A[i, k]
            end
            if !uzero
                u[i], u[p] = u[p], u[i]
            end
            d = A[i,j]
            for k = j:nc
                A[i,k] //= d
            end
            if !uzero
                u[i] //= d
            end
            for k = 1:nr
                if k != i
                    d = A[k,j]
                    for l = j:nc
                        A[k,l] -= d*A[i,l]
                    end
                    if !uzero
                        u[k] -= d*u[i]
                    end
                end
            end
            i += 1
            j += 1
        end
    end
    if cut_zero_rows
        A = A[1:i-1,:]
    end
    if uzero
        return A
    else
        A, u
    end
end

"""
    ConstantSystem(A[, u], D) -> (B[, v])

Generate a system for the constant solutions

Given a field `K`, a derivation `D` on `K` with constant field `C`, a matrix `A`
with entries in `K` and optionally a vector `u` with coefficients in `K`,
return a matrix `B` with entries in `C` and, in the case that `u` is provided,
a vector `v` with coefficients in `K`. If no `u` is provided, or all entries
of the returned `v` are actually elements of `C`, then the solutions in `C`
of `A*x=u` (resp. `A*x=0` if no `u` is provided) are exactly all the solutions
of `B*x=v` (resp. `B*x=0`). Otherwise, if `v` has a nonconstant coefficient,
`A*x=u` has no constant solution.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 225.
"""
ConstantSystem(A::Matrix{T}, D::Derivation) where T<:FieldElement =
    ConstantSystem(A, T[], D)

function ConstantSystem(A::Matrix{T}, u::Vector{T}, D::Derivation) where T<:FieldElement
    # check compatibility
    # check dimensions
    # Note: input arguments A, u are changed on output
    uzero = length(u)==0
    if uzero
        A = RowEchelon(A)
    else
        A, u = RowEchelon(A, u)
    end
    m, n = size(A)
    j = 1
    while j<=n
        i = findfirst(x->!isconstant(x, D), A[:,j])
        if i===nothing
            j += 1
            continue
        end
        # enlarge A by one row and u by one entry
        A = vcat(A, [zero(A[1,1]) for l=1:1, k=1:n])
        if !uzero
            push!(u, zero(A[1,1]))
        end
        Daij = D(A[i,j])
        for k=1:n
            A[m+1, k] =  D(A[i, k])//Daij
        end
        if !uzero
            u[m+1] = D(u[i])//Daij
        end
        for s=1:m
            asj = A[s,j]
            for k=1:n
                A[s, k] -= asj*A[m+1, k]
            end
            if !uzero
                u[s] -= asj*u[m+1]
            end
        end
        j += 1
        m += 1
    end
    # return only nonzero rows
    nz = [i for i=1:size(A,1) if !(all([iszero(A[i,j]) for j=1:size(A,2)]) && (uzero || iszero(u[i])))]
    CT = typeof(zero(constant_field(D))) # ensure B has eltype of constant field, even if it is empty
    B = CT[constantize(A[i,j], D) for i in nz, j=1:n]
    if uzero
        return B
    else
        B, u[nz]
    end
end

"""
    ParamRdeBoundDegreePrim(a, b, qs, D) -> n

Bound on polynomial solutions - primitive case.

Given a field `k`, a derivation `D` on `k[t]` with `D(t)` in `k`, `a`, `b` in `k[t]` with `a≠0`,
and `qs=[q₁,...,qₘ]` with `qᵢ` in `k[t]`, return integer `n`
such that `degree(q)≤n` for any solution  `c₁`,...,`cₘ` in `Const(k)`
and `q` in `k[t]` of  `a*D(q)+b*q=∑ᵢcᵢ*qᵢ`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 228.
"""
function ParamRdeBoundDegreePrim(a::P, b::P, qs::Vector{P}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}}
    isprimitive(D) ||
        error("monomial of derivation D must be primitive")
    iscompatible(a, D) && iscompatible(b, D) && all([iscompatible(q, D) for q in qs]) ||
        error("polynomial a and rational functions b and q_i must be in the domain of derivation D")
    !iszero(a) || error("polynomial a must be nonzero")
    da = degree(a)
    db = degree(b)
    dc = maximum([degree(q) for q in qs])
    if db>da
        n = max(0, dc-db)
    else
        n = max(0, dc-da+1)
    end
    D0 = BaseDerivation(D)
    if db==da-1
        α = -leading_coefficient(b)//leading_coefficient(a)
        z, s0, ρ = LimitedIntegrate(α, leading_coefficient(D), D0)
        if ρ>0 && isrational(s0)
            s = rationalize_over_Int(s0)
            if denominator(s)==1
                n = max(n, numerator(s))
            end
        end
    end
    if db==da
        α = -leading_coefficient(b)//leading_coefficient(a)
        z, ρ = InFieldDerivative(α, D0)
        if ρ>0 && !iszero(z)
            β = -leading_coefficient(a*D0(z)+b*z)//(z*leading_coefficient(a))
            w, s0, ρ = LimitedIntegrate(β, leading_coefficient(D), D0)
            if ρ>0 && isrational(s0)
                m = rationalize_over_Int(s0)
                if denominator(s)==1
                    n = max(n, numerator(s))
                end
            end
        end
    end
    n
end

"""
    ParamRdeBoundDegreeBase(a, b, qs) -> n

Bound on polynomial solutions - base case.

Given a field `k`, `a`, `b` in `k[t]` with `a≠0`, and `qs=[q₁,...,qₘ]` with
`qᵢ` in `k[t]`,  return integer `n`
such that `degree(q)≤n` for any solution  `c₁`,...,`cₘ` in `Const(k)`
and `q` in `k[t]` of `a*(d/dt)(q)+b*q=∑ᵢcᵢ*qᵢ`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 228.
"""
function ParamRdeBoundDegreeBase(a::P, b::P, qs::Vector{P}) where
    {T<:FieldElement, P<:PolyElem{T}}
    !iszero(a) || error("polynomial a must be nonzero")
    da = degree(a)
    db = degree(b)
    dc = maximum([degree(q) for q in qs])
    n = max(0, dc - max(db, da - 1))
    if db==da-1
        s0 = -leading_coefficient(b)//leading_coefficient(a)
        if isrational(s0)
            s = rationalize_over_Int(s0)
            if isone(denominator(s))
                n = max(0, numerator(s), dc - db)
            end
        end
    end
    return n
end

"""
    ParamRdeBoundDegreeExp(a, b, qs, D) -> n

Bound on polynomial solutions - hyperexponential case.

Given a field `k`, a derivation `D` on `k[t]` with `D(t)/t` in `k`, `a`, `b` in `k[t]`,
with `a≠0`, and `qs=[q₁,...,qₘ]` with `qᵢ` in `k[t]` return integer `n`
such that `degree(q)≤n` for any solution  `c₁`,...,`cₘ` in `Const(k)`
and `q` in `k[t]` of  `a*D(q)+b*q=∑ᵢcᵢ*qᵢ`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 229.
"""
function ParamRdeBoundDegreeExp(a::P, b::P, qs::Vector{P}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}}
    ishyperexponential(D) ||
        error("monomial of derivation D must be hyperexponential")
    iscompatible(a, D) && iscompatible(b, D) && all([iscompatible(q, D) for q in qs]) ||
        error("polynomial a and rational functions b and q_i must be in the domain of derivation D")
    !iszero(a) || error("polynomial a must be nonzero")
    da = degree(a)
    db = degree(b)
    dc = maximum([degree(q) for q in qs])
    n = max(0, dc - max(db, da))
    if da==db
        α = -leading_coefficient(b)//leading_coefficient(a)
        w = coeff(MonomialDerivative(D), 1)
        n0, s, z, ρ = ParametricLogarithmicDerivative(α, w, BaseDerivation(D))
        if ρ<0
            error("ParametricLogarithmicDerivative failed")
        end
        if  ρ>0 && n0==1 && !iszero(z)
            n = max(n, s)
        end
    end
    n
end

"""
    ParamRdeBoundDegreeNonLinear(a, b, qs, D) -> n

Bound on polynomial solutions - nonlinear case.

Given a  field `k`, a derivation `D` on `k[t]` with `degree(D(t))≥2` , `a`, `b` in `k[t]` with `a≠0`,
and `qs=[q₁,...,qₘ]` with `qᵢ` in `k[t]`, return integer `n`
such that `degree(q)≤n` for any solution `c₁`,...,`cₘ` in `Const(k)`
and `q` in `k[t]` of `a*D(q)+b*q=∑ᵢcᵢ*qᵢ`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 230.
"""
function ParamRdeBoundDegreeNonLinear(a::P, b::P, qs::Vector{P}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}}
    isnonlinear(D) ||
        error("monomial of derivation D must be nonlinear")
    iscompatible(a, D) && iscompatible(b, D) && all([iscompatible(q, D) for q in qs]) ||
        error("polynomial a and rational functions b and q_i must be in the domain of derivation D")
    !iszero(a) || error("polynomial a must be nonzero")
    da = degree(a)
    db = degree(b)
    dc = maximum([degree(q) for q in qs])
    δ = degree(D)
    λ = leading_coefficient(D)
    n = max(0, dc - max(da + δ - 1, db))
    if db==da+δ-1
        s0 = -leading_coefficient(b)/(λ*leading_coefficient(a))
        if isrational(s0)
            s = rationalize_over_Int(s0)
            if isone(denominator(s))
                n = max(0, numerator(s), dc - db)
            end
        end
    end
    n
end

"""
    ParamRdeBoundDegree(a, b, qs, D) -> n

Bound on polynomial solutions.

Given a  field `k`, a derivation `D` on `k[t]`, `a`, `b` in `k[t]` with `a≠0`,
 and `qs=[q₁,...,qₘ]` with `qᵢ` in `k[t]`, return integer `n`
such that `degree(q)≤n` for any solution `c₁`,...,`cₘ` in `Const(k)`
and `q` in `k[t]` of  `a*D(q)+b*q=∑ᵢcᵢ*qᵢ`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 227.
"""
function ParamRdeBoundDegree(a::P, b::P, qs::Vector{P}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}}
    iscompatible(a, D) && iscompatible(b, D) && all([iscompatible(q, D) for q in qs]) ||
        error("polynomial a and rational functions b and q_i must be in the domain of derivation D")
    !iszero(a) || error("polynomial a must be nonzero")
    if isbasic(D)
        return ParamRdeBoundDegreeBase(a, b, qs)
    elseif isprimitive(D)
        return ParamRdeBoundDegreePrim(a, b, qs, D)
    elseif ishyperexponential(D)
        return ParamRdeBoundDegreeExp(a, b, qs, D)
    elseif isnonlinear(D)
        return ParamRdeBoundDegreeNonLinear(a, b, qs, D)
    else
        H = MonomialDerivative(D)
        throw(NotImplementedError("ParamRdeBoundDegree: monomial derivative $H\n@ $(@__FILE__):$(@__LINE__)"))
    end
end

"""
    ParSPDE(a, b, qs, D, n) -> (A, B, Qs, rs, N)

Parametric Special Polynomial Differential Equation algorithm.

Given a field `k`, a derivation `D` on `k[t]`, `a`, `b` in `k[t]` with `degree(a)>0` and `gcd(a,b)=1`,
`qs=[q₁,...,qₘ]` with `qᵢ` in `k[t]`, and an integer `n` , return `A`, `B` in `k[t]`,
`Qs=[Q₁,...,Qₘ]` with `Qᵢ` in `k[t]`, `rs=[r₁,...,rₘ]` with `rᵢ` in `k[t]`,
and an integer `N` such that for any solution `c₁`,...,`cₘ` in `Const(k)`
and `q` in `k[t]` of degree at most `n` of `a*D(q)+b*q=∑ᵢcᵢ*qᵢ`, `p=(q-∑ᵢcᵢ*rᵢ)/a`
has degree at most `N` and satisfies `A*D(p)+B*p=∑ᵢcᵢ*Qᵢ`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 231.
"""
function ParSPDE(a::P, b::P, qs::Vector{P}, D::Derivation, n::Int) where
    {T<:FieldElement, P<:PolyElem{T}}
    iscompatible(a, D) && iscompatible(b, D) && all([iscompatible(q, D) for q in qs]) ||
        error("polynomial a and rational functions b and q_i must be in the domain of derivation D")
    degree(a)>0 || error("degree(a) must be > 0")
    degree(gcd(a, b))<=0 || error("gcd(a,b) must be 1")
    rzs = [gcdx(b, a, q) for q in qs]
    a, b + D(a), [z-D(r) for (r, z) in rzs], [r for (r, z) in rzs], n-degree(a)
end

"""
    ParamPolyRischDENoCancel1(b, qs, D, n) -> (hs, A)

Parametric polynomial Risch differential equation - no cancellation.

Given a field `k`, a derivation `D` on `k[t]`, `b≠0` in `k[t]`, `qs=[q₁,...,qₘ]` with
`qᵢ` in `k[t]`, and an integer `n` such that either
`D=d/dt` or `degree(b)>max(0, degree(D(t))-1)`, return `hs=[h₁,...,hᵣ]` with
`hᵢ` in `k[t]` and  a matrix `A` with entries in `Const(k)` such that
if `c₁`,...,`cₘ` in `Const(k)` and `q` in `k[t]` satisfy `degree(q)<=n` and
`D(q)+b*q=∑ᵢcᵢ*qᵢ` then `q=∑ⱼdⱼ*hⱼ` where `d₁`,...,`dᵣ` are in `Const(k)` and
`A*[c₁,...,cₘ,d₁,...,dᵣ]=0`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 234.
"""
function ParamPolyRischDENoCancel1(b::P, qs::Vector{P}, D::Derivation, n::Int) where
    {T<:FieldElement, P<:PolyElem{T}}
    # Note: this implementation changes the input parameters qs!
    iscompatible(b, D) && all([iscompatible(q, D) for q in qs]) ||
        error("polynomials a and q_i must be in the domain of derivation D")
    !iszero(b) || error("polynomial b must be nonzero")
    isbasic(D) || degree(b)>max(0, degree(D)-1) ||
        error("either derivation D must be basic or degree(b)>max(0, degree(D)-1)")
    t = gen(parent(b))
    db = degree(b)
    bd = leading_coefficient(b)
    m = length(qs)
    hs = [zero(b) for i=1:m]
    ss = [zero(bd) for i=1:m]
    while n>=0
        for i=1:m
            ss[i] = coeff(qs[i], n + db)//bd
            sitn = ss[i]*t^n
            hs[i] += sitn
            qs[i] -= D(sitn) + b*sitn
        end
        n -= 1
    end
    if all([iszero(q) for q in qs])
        dc = -1
    else
        dc = maximum([degree(q) for q in qs])
    end
    M = [coeff(q, i) for i=0:dc, q in qs]
    A = ConstantSystem(M, BaseDerivation(D))
    C = constant_field(D)
    neq = size(A, 1)
    A = vcat(hcat(A, zeros(C, neq, m)), zeros(C, m, 2*m))
    for i=1:m
        A[i+neq, i] = one(C)
        A[i+neq, m+i] = -one(C)
    end
    hs, A
end

"""
    ParamPolyRischDENoCancel2(b, qs, D, n) -> (hs, A)

Parametric polynomial Risch differential equation - no cancellation.

Given a field `k`, a derivation `D` on `k[t]`, `b` in `k[t]`, `qs=[q₁,...,qₘ]` with
`qᵢ` in `k[t]`, and an integer `n` such that `degree(b)<degree(D(t)-1)` and either
`D=d/dt` or `degree(D(t))≥2`, return `hs=[h₁,...,hᵣ]` with
`hᵢ` in `k[t]` and  a matrix `A` with entries in `Const(k)` such that
if `c₁`,...,`cₘ` in `Const(k)` and `q` in `k[t]` satisfy `degree(q)≤n` and
`D(q)+b*q=∑ᵢcᵢ*qᵢ` then `q=∑ⱼdⱼ*hⱼ` where `d₁`,...,`dᵣ` are in `Const(k)` and
`A*[c₁,...,cₘ,d₁,...,dᵣ]=0`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 238.
"""
function ParamPolyRischDENoCancel2(b::P,  qs::Vector{P}, D::Derivation, n::Int) where
    {T<:FieldElement, P<:PolyElem{T}}
    iscompatible(b, D) && all([iscompatible(q, D) for q in qs]) ||
        error("polynomials a and q_i must be in the domain of derivation D")
    δ = degree(D)
    iszero(b) || degree(b)<δ-1 || error("degree(b)<degree(D)-1 must hold")
    # Note: In Bronstein's book the convention degree(0)=-infinity is used,
    # but in AbstractAlgebra we have degree(0)=-1, so we have to explicitly
    # include the clause iszero(b) in the above condition.
    isbasic(D) || δ>=2 ||
        error("either derivation D must be basic or degree(D)>=2")
    t = gen(parent(b))
    λ = leading_coefficient(D)
    Z  = zero(b)
    Z0 = zero(λ)
    m = length(qs)
    hs = [zero(b) for i=1:m]
    ss = [zero(λ) for i=1:m]
    while n>0
        nλ = n*λ
        for i=1:m
            ss[i] = coeff(qs[i], n + δ -1)//(nλ)
            sitn = ss[i]*t^n
            hs[i] += sitn
            qs[i] -= D(sitn) + b*sitn
        end
        n -= 1
    end
    C = constant_field(D)
    if degree(b)>0
        for i=1:m
            ss[i] = coeff(qs[i], degree(b))//leading_coefficient(b)
            hs[i] += ss[i]
            qs[i] -= D(ss[i]) + b*ss[i]
        end
        if all([iszero(q) for q in qs])
            dc = -1
        else
            dc = maximum([degree(q) for q in qs])
        end
        M = [coeff(q, i) for i=0:dc, q in qs]
        A = ConstantSystem(M, BaseDerivation(D))
        neq = size(A, 1)
        A = vcat(hcat(A, zeros(C, neq, m)), zeros(C, m, 2*m))
        for i=1:m
            A[i+neq, i] = one(C)
            A[i+neq, m+i] = -one(C)
        end
        return hs, A
    else
        D0 = BaseDerivation(D)
        b0 = constant_coefficient(b) # b \in k
        fs, B = ParamRischDE(b0, [constant_coefficient(q) for q in qs], D0)
        if all([iszero(q) for q in qs])
            if all([iszero(D0(f)+b0*f) for f in fs])
                dc = -1
            else
                dc = 0
            end
        else
            dc = maximum([degree(q) for q in qs])
        end
        r = length(fs)
        M = zeros(parent(b0), dc+1, m+r)
        for i=0:dc
            for j = 1:m
                M[i+1,j] = coeff(qs[j], i)
            end
        end
        if dc>=0
            for j=1:r
                M[1,j+m] = -D0(fs[j]) - b0*fs[j]
            end
        end
        A = ConstantSystem(M, D0)
        A = vcat(A, B)
        neq = size(A, 1)
        A = vcat(hcat(A, zeros(C, neq, m)), zeros(C, m, 2*m+r))
        for i=1:m
            A[i+neq, i] = one(C)
            A[i+neq, m+r+i] = -one(C)
        end
        return vcat(fs .+ zero(b), hs), A
    end
end

"""
    ParamPolyRischDECancelLiouvillian(b, qs, D, n) -> (hs, A)

Parametric polynomial Risch differential equation - no cancellation.

Given a field `k`, a derivation `D` on `k[t]` with `D≠d/dt` and `D(t)` in `k`
or `D(t)/t` in `k`, `b` in `k`, `qs=[q₁,...,qₘ]` with
`qᵢ` in `k[t]`, and an integer `n`,   return `hs=[h₁,...,hᵣ]` with
`hᵢ` in `k[t]` and  a matrix `A` with entries in `Const(k)` such that
if `c₁`,...,`cₘ` in `Const(k)` and `q` in `k[t]` satisfy `degree(q)≤n` and
`D(q)+b*q=∑ᵢcᵢ*qᵢ` then `q=∑ⱼdⱼ*hⱼ` where `d₁`,...,`dᵣ` are in `Const(k)` and
`A*[c₁,...,cₘ,d₁,...,dᵣ]=0`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 241.
"""
function ParamPolyRischDECancelLiouvillian(b::T,  qs::Vector{P}, D::Derivation, n::Int) where
    {T<:FieldElement, P<:PolyElem{T}}
    isprimitive(D) || ishyperexponential(D) ||
    error("monomial of derivation D must be primitive or hyperexponential")
    D0 = BaseDerivation(D)
    iscompatible(b, D0) ||
        error("coefficient b must be in the domain of the base derivation of D")
    all([iscompatible(q, D) for q in qs]) ||
        error("polynomials q_i must be in the domain of derivation D")
    m = length(qs)
    C = constant_field(D)
    if iszero(b) && all([iszero(q) for q in qs])
        return P[], zeros(C, 0, m)
    end
    H = MonomialDerivative(D)
    if ishyperexponential(D)
        w = coeff(H, 1)
    else
        w = zero(parent(b))
    end
    t = gen(parent(H))
    first = true
    qns = copy(qs)
    hs = P[]
    while n>=0
        fns, An = ParamRischDE(b + n*w, [coeff(q, n) for q in qns], D0)
        hns = [f*t^n for f in fns]
        qns = vcat(qns, [-(D(h) + b*h) for h in hns]) # 2nd displayed eq. on p. 242 (sign of b wrong there !?!?)
        if first
            A = An
            hs = hns
            first = false
        else
            A = vcat( hcat(A, zeros(C, size(A, 1),   size(An,2) - size(A, 2) )),
                           An)
            hs = vcat(hs, hns)
        end
        n -= 1
    end
    r = length(hs)
    Dhbhs = [D(h) + b*h for h in hs]  # sign of b wrong on  p. 242 !?!?!
    if all([iszero(q) for q in qs]) && all([iszero(Dhbh) for Dhbh in Dhbhs])
        dc = -1
    else
        dc = max( maximum([degree(q) for q in qs]), maximum([degree(Dhbh) for Dhbh in Dhbhs]))
    end
    M = zeros(parent(b), dc+1, m+r)
    for i=0:dc
        for j = 1:m
            M[i+1,j] = coeff(qs[j], i)
        end
        for j=1:r
            M[i+1,j+m] = -coeff(Dhbhs[j], i)
        end
    end
    A0 = ConstantSystem(M, D0)
    A = vcat(A0, A)
    hs, A
end

"""
    ParamPolyRischDE(b, qs, D, n) -> (hs, A)

Parametric polynomial Risch differential equation.

Given a field `k`, a derivation `D` on `k[t]`, `b` in `k[t]`, `qs=[q₁,...,qₘ]` with
`qᵢ` in `k[t]` and an integer `n`, return `hs=[h₁,...,hᵣ]` with
`hᵢ` in `k[t]` and  a matrix `A` with entries in `Const(k)` such that
if `c₁`,...,`cₘ` in `Const(k)` and `q` in `k[t]` satisfy `degree(q)≤n` and
`D(q)+b*q=∑ᵢcᵢ*qᵢ` then `q=∑ⱼdⱼ*hⱼ` where `d₁`,...,`dᵣ` are in `Const(k)` and
`A*[c₁,...,cₘ,d₁,...,dᵣ]=0`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 241.
"""
function ParamPolyRischDE(b::P,  qs::Vector{P}, D::Derivation, n::Int) where
    {T<:FieldElement, P<:PolyElem{T}}
    iscompatible(b, D) || all([iscompatible(q, D) for q in qs]) ||
        error("coefficient b and polynomials q_i must be in the domain of derivation D")
    δ = degree(D)
    if !iszero(b) && (isbasic(D) || degree(b)>max(0, δ-1))
        return ParamPolyRischDENoCancel1(b, qs, D, n)
    elseif (iszero(b) || degree(b)<δ-1) && (isbasic(D) || δ>=2)
        return ParamPolyRischDENoCancel2(b, qs, D, n)
    elseif δ>=2 && degree(b)==δ-1
        if ishypertangent(D)
            throw(NotImplementedError("ParamPolyRischDE: no cancellation, degree(b)==δ-1, hypertangent case\n@ $(@__FILE__):$(@__LINE__)"))
        else
            H = MonomialDerivative(D)
            throw(NotImplementedError("ParamPolyRischDE: no cancellation, degree(b)==δ-1, monomial derivative $H\n@ $(@__FILE__):$(@__LINE__)"))
        end
    elseif isprimitive(D) || ishyperexponential(D)
        @assert δ<=1 && degree(b)<=0 && !isbasic(D)
        return ParamPolyRischDECancelLiouvillian(constant_coefficient(b), qs, D, n)
    elseif ishypertangent(D)
        throw(NotImplementedError("ParamPolyRischDE: hypertangent, cancellation case\n@ $(@__FILE__):$(@__LINE__)"))
    else
        H = MonomialDerivative(D)
        throw(NotImplementedError("ParamPolyRischDE: cancelling case, monomial derivative $H\n@ $(@__FILE__):$(@__LINE__)"))
    end
end

"""
    ParamPolyCoeffsRischDE(a, b, gs, D) -> (hs, A)

Parametric Risch differential equation with polynomial coefficients.

Given a field `k`, a derivation `D` on `k[t]`, `a`, `b` in `k[t]` with `a≠0`, and `gs=[g₁,...,gₘ]` with
`gᵢ` in `k(t)`, return `hs=[h₁,...,hᵣ]` with
`hᵢ` in `k(t)` and  a matrix `A` in reduced row echelon form with entries in `Const(k)` such that
`c₁`,...,`cₘ` in `Const(k)` and `q` in `k[t]` is a solution of
`a*D(q)+b*q=∑ᵢcᵢ*gᵢ` if and only if `q=∑ⱼdⱼ*hⱼ` for `d₁`,...,`dᵣ` in `Const(k)` with
`A*[c₁,...,cₘ,d₁,...,dᵣ]=0`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 222.
"""
function ParamPolyCoeffsRischDE(a::P, b::P, gs::Vector{F}, D::Derivation;
                                n::Int=typemin(Int), exit_if_small_nullspace::Bool=false) where
                                {T<:FieldElement, P<:PolyElem{T}, F<:FracElem{P}}
    iscompatible(a, D) && iscompatible(b, D) && all([iscompatible(g, D) for g in gs]) ||
        error("polynomials a and b and rational functions g_i must be in the domain of derivation D")
    m = length(gs)
    d = gcd(a, b)
    a = divexact(a, d)
    b = divexact(b, d)
    gs = [g//d for g in gs]
    qs, M = LinearConstraints(a, b, gs, D)
    A = ConstantSystem(M, BaseDerivation(D))
    if exit_if_small_nullspace
        A = RowEchelon(A)
        if size(A, 2) - size(A, 1) <= 1
            # rank(nullspace) <= 1
            return F[], A, qs, true
        end
    end
    if n==typemin(Int)
        n = ParamRdeBoundDegree(a, b, qs, D)
    end
    aprod = one(parent(a))
    Rs = zeros(parent(a//one(a)), m)
    while n>=0 && degree(a)>0
        a0 = a
        a, b, qs, rs, n = ParSPDE(a, b, qs, D, n)
        aprod *= a0
        for i=1:m
            Rs[i] = (Rs[i] + rs[i])//a0
        end
        d = gcd(a, b)
        a = divexact(a, d)
        b = divexact(b, d)
        gs = [q//d for q in qs]
        qs, M = LinearConstraints(a, b, gs, D)
        A = vcat(A, ConstantSystem(M, BaseDerivation(D)))
    end
    C = constant_field(D)
    if n<0
        hs = F[]
        A = vcat(A, ConstantSystem(RowEchelon([q//1 for i=1:1, q in qs]), D))
    else
        a = constant_coefficient(a)
        b = divexact(b, a)
        qs = [divexact(q, a) for q in qs]
        hs, A1 = ParamPolyRischDE(b, qs, D, n)
        A = vcat(hcat(A, zeros(C, size(A, 1), length(hs))), A1)
    end
    r = length(hs)
    s = sum([1 for R in Rs if !iszero(R)])
    hs = vcat([aprod*h//1 for h in hs], [aprod*R for R in Rs if !iszero(R)])
    neq = size(A, 1)
    A = vcat(hcat(A, zeros(C, neq, s)), zeros(C, s, m+r+s))
    i1 = 0
    for i=1:m
        if !iszero(Rs[i])
            i1 += 1
            A[neq+i1, i] = one(C)
            A[neq+i1, m+r+i1] = -one(C)
        end
    end
    A = RowEchelon(A)
    if exit_if_small_nullspace
        return hs, A, P[], false
    else
        hs, A
    end
end

"""
    ParamRischDE(f, gs, D) -> (hs, A)

Parametric Risch differential equation.

Given a field `K`, a derivation `D` on `K`, `f` in `K`, `gs=[g₁,...,gₘ]` with
`gᵢ` in `K`, return `hs=[h₁,...,hᵣ]` with
`hᵢ` in `K` and  a matrix `A` reduced row echelon form with entries in `Const(K)` such that
`c₁`,...,`cₘ` in `Const(K)` and `y` in `K` is a solution of
`D(y)+f*y=∑ᵢcᵢ*gᵢ` if and only if `y=∑ⱼdⱼ*hⱼ` for `d₁`,...,`dᵣ` in `Const(K)` with
`A*[c₁,...,cₘ,d₁,...,dᵣ]=0`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.1, p. 217.
"""
function ParamRischDE(f::F, gs::Vector{F}, D::Derivation) where F<:FieldElement
    iszero(D) || error("base case only for null derivations")
    #base case => pure (linear) algebra problem ...
    iscompatible(f, D) && all(iscompatible(g, D) for g in gs) ||
        error("rational functions f and g_i must be in the domain of derivation D")
    m = length(gs)
    C = parent(f)
    if iszero(f)
        hs = [one(parent(f))]
        A = reshape(vcat(gs, zero(parent(f))), (1, m+1))
    else
        A = zeros(C, m , 2*m)
        for i=1:m
            A[i, i] = one(C)
            A[i, m+i] = -one(C)
        end
        hs = [gs[i]//f for i=1:m]
    end
    hs, RowEchelon(A)
end

function ParamRischDE(f::F, gs::Vector{F}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}, F<:FracElem{P}}
    iscompatible(f, D) && all(iscompatible(g, D) for g in gs) ||
        error("rational functions f and g_i must be in the domain of derivation D")

    h0 = WeakNormalizer(f, D)

    f1 = f - D(h0)//h0
    gs = [h0*g for g in gs]
    a, b, gs, h1 = ParamRdeNormalDenominator(f1, gs, D)

    a, b, gs, h2 =  ParamRdeSpecialDenominator(a, b, gs, D)

    hs, A = ParamPolyCoeffsRischDE(a, b, gs, D) # A in reduced echelon form

    h012 = h0*h1*h2
    [h//h012 for h in hs], A
end

"""
    LimitedIntegrateReduce(f, ws, D) -> (a, b, h, N, g, vs)

Reduction to a polynomial problem.

Given a field `k`, a derivation `D` on `k[t]`, `f` in `k(t)` and `ws=[w₁,...,wₘ]` with
`wᵢ` in `k(t)`, return `a`, `b`, `h` in `k[t]`, `g` in `k(t)`, `vs=[v₁,...,vₘ]` with
`vᵢ` in `k(t)` and an integer `N` such that
for any solution `v` in `k(t)`, `c_1`,...,`cₘ` in Const(k) of `f = D(v)+∑ᵢcᵢ*wᵢ`,
`p=v*h` is in `k⟨t⟩`, and `p` and the `c_i` satisfy `a*D(p)+b*p=g+∑ᵢcᵢ*vᵢ`.

(Here, `k⟨t⟩` denotes the elements of `k(t)` which are reduced w.r.t. `D`.)

Furthermore, if `S₁ⁱʳʳ==Sⁱʳʳ`, which is indicated by `is_Sirr1_eq_Sirr(D)==true`,
then `p` is in `k[t]`, and if `t` is nonlinear or Liouvillian over `k`, then `degree(p)≤N`.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.2, p. 248.
"""
function LimitedIntegrateReduce(f::F, ws::Vector{F}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}, F<:FracElem{P}}
    # Note: LimitedIntegrateReduce seems to be the only algorithm in Bronstein's book,
    # where he messed something up. Already in equation (7.32) of Corollary 7.2.1 (p.247)
    # the coefficient of D(p) contains a factor hs too many. This then reproduces in the
    # algoritm.
    iscompatible(f, D) && all(iscompatible(w, D) for w in ws) ||
        error("rational functions f and w_i must be in the domain of derivation D")
    dn, ds = SplitFactor(denominator(f), D)
    eness = [SplitFactor(denominator(w), D) for w in ws]
    c = lcm(vcat(dn, [en for (en, es) in eness]))
    hn = gcd(c, derivative(c))
    a = hn
    b = -D(hn)
    N = 0
    if is_Sirr1_eq_Sirr(D)
       hs = lcm(vcat(ds, [es for (en, es) in eness]))
       h = hn*hs # in Bronsteins's book, wrongly a = hn*hs
       b = -D(hn)-divexact(hn*D(hs),hs)
       μ = min(iszero(f) ? typemax(Int) : valuation_infinity(f), minimum([iszero(w) ? typemax(Int) : valuation_infinity(w) for w in ws]))
       N = degree(hn) + degree(hs) + max(0, 1 - degree(D) - μ)
    end
    hhn = h*hn
    # in Bronsteins'book, wrongly always the factor a*hn instead of h*hn
    a, b, h, N, hhn*f, [-hhn*w for w in ws]
end

function solve_x1_eq_1(A::Matrix{T}) where T<:FieldElement
    # return solution x of A*x = 0 with x[1]==1 if it exists and x=0 otherwise.
    # A must be in  reduced row echelon form
    nr, nc = size(A)
    @assert nr>0 && nc>0
    Z = zero(A[1,1])
    E = one(A[1,1])
    x = fill(Z, nc)
    if iszero(A[1,1])
        x[1] = E
        return x
    elseif (isone(A[1,1]) && (all([iszero(a) for a in A[1,2:nc]])))
        return x
    else
        # A[1,1]==1 && not all other elements in the first row are ==0
        k = findfirst(a->!iszero(a), A[1, 2:nc])+1
        p = [findfirst(a->isone(a), A[i,:]) for i=1:nr]
        x[k] = E
        x[p] = -A*x
        @assert !iszero(x[1])
        x .//= x[1]
        return x
    end
end

"""
    LimitedIntegrate(f, ws, D) -> (v, cs, ρ)

Limited integration problem.

Given a field `k`, a derivation `D` on `k[t]`, `f` in `k(t)`, `ws=[w₁,...,wₘ]` with
`wᵢ` in `k(t)`, return either `ρ=0`, in which case the equation
`f = D(v)+∑ᵢcᵢ*wᵢ` has no solution `v` in `k(t)`, `cs=[c₁,...,cₘ]`, `cᵢ` in `Const(k)`,
or `ρ=1` and a solution `v`, `cs` of that equation.

See [Bronstein's book](https://link.springer.com/book/10.1007/b138171), Section 7.2, p. 245.
"""
function LimitedIntegrate(f::F, w::F, D::Derivation) where F <: FieldElement
    # this is a wrapper for the case length(ws)==1 for convenience
    y, cs, ρ = LimitedIntegrate(f, [w], D)
    if ρ <=0
        return zero(f), zero(constant_field(D)), ρ
    else
        return y, cs[1], 1
    end
end

function LimitedIntegrate(f::F, ws::Vector{F}, D::Derivation) where F <: FieldElement
    # base case f, ws[i] \in constant field, D must be the null derivation
    iscompatible(f, D) && all(iscompatible(w, D) for w in ws) ||
        error("rational functions f and w_i must be in the domain of derivation D")
    iszero(D) || error("base case only for null derivations")
    Z = zero(f)
    m = length(ws)
    A = reshape(vcat(-f, ws), (1, m+1))
    A = RowEchelon(A)
    cs = solve_x1_eq_1(A)
    if isone(cs[1])
        return Z, cs[2:end], 1
    else
        return Z, fill(Z, m), 0
    end
end

function LimitedIntegrate(f::F, ws::Vector{F}, D::Derivation) where
    {T<:FieldElement, P<:PolyElem{T}, F<:FracElem{P}}
    iscompatible(f, D) && all(iscompatible(w, D) for w in ws) ||
        error("rational functions f and w_i must be in the domain of derivation D")
    Z = zero(f)
    no_solution = (Z, F[], 0)
    (a, b, h, n, g, vs) = LimitedIntegrateReduce(f, ws, D)
    if is_Sirr1_eq_Sirr(D)
        gs = vcat(g, vs)
        # Note:  ParamPolyCoeffsRischDE does the following division by gcd(a,b) anyway.
        # But in case of small_nullspace==true we need these *reduced* a, b, so we compute them here.
        d = gcd(a, b)
        a = divexact(a, d)
        b = divexact(b, d)
        gs = [g//d for g in gs]
        hs, A, qs, small_nullspace = ParamPolyCoeffsRischDE(a, b, gs, D, n=n, exit_if_small_nullspace=true)
        # A in reduced echelon form
        if !small_nullspace # regular exit
            cds = solve_x1_eq_1(A) # solution of A*(cs, ds)=0 with cs[1]==1
            if iszero(cds[1])
                return no_solution
            end
            @assert isone(cds[1])
            m = length(gs)
            r = length(hs)
            cs = cds[1:m]
            ds = cds[m+1:m+r]
            y = Z
            for j=1:length(hs)
                y +=  ds[j]*hs[j]
            end
            y = y//h
            return y, cs[2:m], 1
        end
        # premature exit
        rg = size(A, 2)-size(A, 1)
        if rg==0
            return no_solution
        end
        @assert rg==1
        cs = solve_x1_eq_1(A) # solution of A*cs=0 with cs[1]==1
        if iszero(cs[1])
            return no_solution
        end
        m = length(qs)
        c = sum([cs[i]*qs[i] for i=1:m])
        b, c, n, α, β, ρ = SPDE(a, b, c, D, n)
        ρ>=1 || return Z, F[], ρ
        z, ρ = PolyRischDE(b, c, D, n)
        ρ>=1 || return Z, F[], ρ
        return (α*z+β)//h, cs[2:m], 1
    else
        throw(NotImplementedError("LimitedIntegrate: !is_Sirr1_eq_Sirr(D)\n@ $(@__FILE__):$(@__LINE__)"))
    end
end


using LoopFieldCalc

# Define grid on which we compute magnetic field strength.
# This is a 2D grid in the y-z plane
xs = 0.0:0.0
ys = -0.4:0.005:0.4
zs = -0.3:0.005:0.3

# Define number of wire turns per coil
nturns = 10

# Define geometry of inner coil
inner_coil_radius = 0.1
inner_coil_width = 0.15
inner_coil_offset = -0.1
inner_coil_current = 1.0

# Generate loops for inner coil
inner_coil = [
    LoopFieldCalc.CurrentLoop(
        radius = inner_coil_radius,
        current = inner_coil_current / nturns,
        center = LoopFieldCalc.CylindricalPoint(inner_coil_offset + z, 0.0, 0.0)
    )
    for z in LinRange(0.0, inner_coil_width, nturns)
]

# Define geometry for outer coil
outer_coil_radius = 0.25
outer_coil_width = 0.15
outer_coil_offset = -0.15
outer_coil_current = 0.5 * inner_coil_current

# Generate loops for outer coil
outer_coil = [
    LoopFieldCalc.CurrentLoop(
        radius = outer_coil_radius,
        current = outer_coil_current / nturns,
        center = LoopFieldCalc.CylindricalPoint(outer_coil_offset + z, 0.0, 0.0)
    )
    for z in LinRange(0.0, outer_coil_width, nturns)
]

# Combine inner and outer coil loops into a single vector and compute field
loops = [outer_coil; inner_coil]
Bx, By, Bz = LoopFieldCalc.field_on_grid(loops, xs, ys, zs)

# Write output. By reversing z and y and transposing the magnetic field matrices, we
# can rotate the output so that the loop axis is aligned with the x axis
LoopFieldCalc.write_field("examples/example_2.dat", zs, ys, Bz[1, :, :]', By[1, :, :]')


using Distances
using LinearAlgebra
using Ripserer
using StaticArrays
using Test

using Ripserer:
    _landmarks_and_radius,
    _to_integer_coefficients,
    _zero_coboundary_matrix,
    _to_vector,
    _harmonic_smoothing,
    is_cocycle,
    ChainElement

include("../testdatasets.jl")

@testset "Helpers" begin
    @testset "Partition functions" begin
        for fun in (Partition.linear, Partition.quadratic, Partition.exponential)
            for r in range(1, 2; length=5), d in range(0, 2; length=5)
                if r > d
                    @test fun(r, d) > 0
                else
                    @test fun(r, d) == 0
                end
            end
        end
    end

    @testset "Landmark selection" begin
        points = [(sin(t), cos(t)) for t in range(0, 2π; length=41)[1:40]]
        point_matrix = reshape(reinterpret(Float64, points), (2, 40))

        for landmark_selector in (10, 1:4:40, points[1:4:40])
            landmarks, radius = _landmarks_and_radius(
                points, landmark_selector, Euclidean()
            )
            @test length(landmarks) == 10
            @test radius ≈ 0.3128689 atol = 1e-5
            @test eltype(landmarks) <: SVector{2,Float64}
        end
    end

    @testset "Cocycles" begin
        rips = Rips(
            [
                0.0 1.1 2.0 2.0 1.2
                1.1 0.0 1.0 2.0 2.0
                2.0 1.0 0.0 1.0 2.0
                2.0 2.0 1.0 0.0 1.0
                1.2 2.0 2.0 1.0 0.0
            ],
        )

        cocycle = ripserer(rips; modulus=17, reps=true)[2][1].representative
        integral = _to_integer_coefficients(cocycle)
        @test simplex.(integral) == simplex.(cocycle)
        @test coefficient.(integral) == [-1, -1, -1]
        @test _to_vector(rips, integral, Inf) == [0, -1, 0, -1, 0, 0, -1, 0, 0, 0]
        @test _to_vector(rips, integral, 2) == [0, 0, 0, 0, 0, 0, -1, 0, 0, 0]

        @test _zero_coboundary_matrix(rips, Inf) == [
            1 -1 0 0 0
            1 0 -1 0 0
            0 1 -1 0 0
            1 0 0 -1 0
            0 1 0 -1 0
            0 0 1 -1 0
            1 0 0 0 -1
            0 1 0 0 -1
            0 0 1 0 -1
            0 0 0 1 -1
        ]
        @test _zero_coboundary_matrix(rips, 2) == [
            1 -1 0 0 0
            0 0 0 0 0
            0 1 -1 0 0
            0 0 0 0 0
            0 0 0 0 0
            0 0 1 -1 0
            1 0 0 0 -1
            0 0 0 0 0
            0 0 0 0 0
            0 0 0 1 -1
        ]
        coords, harmonic = _harmonic_smoothing(rips, cocycle; time=2)
        @test all(diff(coords) .≈ 0.2)

        # The harmonic interval should have a minimal norm.
        @test norm(harmonic) < norm(_to_vector(rips, integral, 2))
    end
end

@testset "CircularCoordinates" begin
    @testset "landmark selection" begin
        points = torus_points(1024)

        cc1 = CircularCoordinates(points, 200)
        @test length(cc1.landmarks) == 200
        @test allunique(cc1.landmarks)

        cc2 = CircularCoordinates(points, 1:100)
        @test cc2.landmarks == SVector.(points[1:100])

        cc3 = CircularCoordinates(points, points[1:100])
        @test cc3.landmarks == SVector.(points[1:100])

        @test cc2.coordinate_data[1].radius == cc3.coordinate_data[1].radius
    end
    @testset "selecting out_dim" begin
        points = torus_points(1024)

        cc = CircularCoordinates(points, 1:10:1024)
        @test cc.out_dim == 1

        cc = CircularCoordinates(points, 1:10:1024; out_dim=3, warn=false)
        @test cc.out_dim == 1
        @test size(cc(points[1:10])) == (10, 1)
        @test size(cc(points[11:20], 1)) == (10,)

        cc = CircularCoordinates(points, 1:5:1024; out_dim=3, warn=false)
        @test cc.out_dim == 2
        out_both = cc(points[1:10])
        out_first = cc(points[1:10], 1)
        out_second = cc(points[1:10], 2)
        @test out_both[:, 1] == out_first
        @test out_both[:, 2] == out_second

        @test_throws ErrorException CircularCoordinates(points, 1:10)
    end
    @testset "transformation" begin
        ts1 = range(0, 1; length=100)[1:(end - 1)]
        circle = [(sinpi(2t), cospi(2t)) for t in ts1]

        cc = CircularCoordinates(circle, 1:10:100)
        transformed1 = cc(circle, 1)
        transformed1 .-= transformed1[1]
        transformed1 .= Ripserer._mod_z.(transformed1)

        @test transformed1 ≈ ts1 atol = 0.05

        @test cc([(100, 100)])[1] === missing

        ts2 = [0; rand(20)]
        new_points = [(sinpi(2t), cospi(2t)) for t in ts2]
        transformed2 = cc(new_points, 1)
        transformed2 .-= transformed2[1]
        transformed2 .= Ripserer._mod_z.(transformed2)

        @test transformed2 ≈ ts2 atol = 0.05
    end
end


### Day 9
## Explosives in Cyberspace
## Author: Thanasis Georgiou

function decompress(str, rec)
  output = 0
  i = 1
  while i <= length(str)
    # Find first marker
    markerStart = search(str, '(', i)

    # If there are no markers, add the rest of the length and skip
    if markerStart == 0
      output += length(str) - (i - 1)
      break
    else
      # Move to marker start
      output += markerStart - i
      i = markerStart

      # Parse marker
      markerEnd = search(str, ')', markerStart)
      marker = split(str[markerStart + 1:markerEnd - 1], 'x')

      len = parse(Int, marker[1])
      times = parse(Int, marker[2])

      # Extract accoarding to marker
      if rec == true
        output += times * decompress(str[markerEnd + 1:markerEnd + len], true)
      else
        output += times * len
      end

      # Move past this marker's influence
      i = markerEnd + len + 1
    end
  end

  return output
end

# Read file
f = open("input.txt")
input = chomp(readstring(f))
close(f)

# Print output
println("Length 1: $(decompress(input, false))")
println("Length 2: $(decompress(input, true))")


using Plots

using simulator.fresnelltools
using simulator.matrixcore
using simulator.materials.spesifics

save = false

n_1 = SiO2_core_Sellmeier
n_2 = Au
n_3 = Pd
n_4 = l -> 1

d2 = 10e-9
d2s = 0e-9:1e-9:40e-9
d3s = 0e-9:1e-9:60e-9

λs = 900e-9:1e-9:1250e-9
λs_broadband = 400e-9:1e-9:1800e-9
θd = 45
θds = 5:1:80

description = "Palladium on $(round(d2 * 1e9, digits=2)) nm gold"

fig_path = "../rapport/figures/thickness_example/"
fig_token = replace(description, " " => "_")

if save
    gr()
else
    plotly()
end
θ1 = θd * π / 180
θs = θds .* (π / 180)

function system_single(λ::Float64, θ1::Float64, d2::Float64, d3::Float64)
    i_1_2_te, i_1_2_tm, θ2 = FresnellBoundrary(n_1(λ), n_2(λ), θ1)
    bulk_2 = FresnellSlab(n_2(λ), 2*π/λ, d2, θ2)
    i_2_3_te, i_2_3_tm, θ3 = FresnellBoundrary(n_2(λ), n_3(λ), θ2)
    bulk_3 = FresnellSlab(n_3(λ), 2*π/λ, d3, θ3)
    i_3_4_te, i_3_4_tm, _ = FresnellBoundrary(n_3(λ), n_4(λ), θ3)

    system_te = CascadeScattering([i_1_2_te, bulk_2, i_2_3_te, bulk_3, i_3_4_te])
    system_tm = CascadeScattering([i_1_2_tm, bulk_2, i_2_3_tm, bulk_3, i_3_4_tm])

    te_r = abs(system_te[2, 1])^2
    te_t = abs(system_te[1, 1])^2
    tm_r = abs(system_tm[2, 1])^2
    tm_t = abs(system_tm[1, 1])^2

    [te_r, tm_r, te_t, tm_t]
end

function get_min(λs, θ1::Float64, d2::Float64, d3::Float64)
    mins = [2.0, 2, 2, 2]
    λmins = [0.0, 0, 0, 0]

    for λ in λs
        spesifics = system_single(λ, θ1, d2, d3)
        for (i, min, spesific) in zip(1:4, mins, spesifics)
            if spesific <= min
                mins[i] = spesific
                λmins[i] = λ
            end
        end
    end

    mins, λmins
end

println("Settup complete, starting simulation")

# get_min_results = [get_min(λs, θ1, d2, d3_spesific) for d3_spesific in d3s]
# min_r_te = [item[1][1] for item in get_min_results]
# min_r_tm = [item[1][2] for item in get_min_results]
# min_λ_te = [item[2][1] for item in get_min_results]
# min_λ_tm = [item[2][2] for item in get_min_results]

## Plot plasmon minima for plasmon as a function of d3
# fig_r = plot(title="Minimum te and tm reflection for $description at θ = $θd degrees", legend=:topright, xlabel="thickness in nm", ylabel="minimum reflection coefficient", ticks=:native)
# plot!(fig_r, d3s .* 1e9, min_r_te, label="TE reflection")
# plot!(fig_r, d3s .* 1e9, min_r_tm, label="TM reflection")
# display(fig_r)
# if save savefig(fig_r, fig_path * "min_reflection_" * fig_token * ".pdf") end

## Plot frequency minima for plasmon as a function of d3
# fig_λ = plot(title="λ for minimum te and tm reflection for $description at θ = $θd degrees", legend=:topright, xlabel="thickness in nm", ylabel="λ at minimum reflection coefficient", ticks=:native)
# plot!(fig_λ, d3s .* 1e9, min_λ_te, label="TE reflection")
# plot!(fig_λ, d3s .* 1e9, min_λ_tm, label="TM reflection")
# display(fig_λ)
# if save savefig(fig_λ, fig_path * "lambda_min_reflection_" * fig_token * ".pdf") end

## Plot plasmon minima and λ for plasmon as a function of d3 for all d2s
# fig_r_all = plot(title="Minimum tm reflection for $description at θ = $θd degrees", legend=:topright, xlabel="thickness in nm", ylabel="minimum reflection coefficient", ticks=:native)
# fig_λ_all = plot(title="λ for minimum tm reflection for $description at θ = $θd degrees", legend=:topright, xlabel="thickness in nm", ylabel="λ at minimum reflection coefficient", ticks=:native)
# for d2_spesific in d2s
#     get_min_results_current = [get_min(λs, θ1, d2_spesific, d3_spesific) for d3_spesific in d3s]
#     min_r_tm_current = [item[1][2] for item in get_min_results_current]
#     min_λ_tm_current = [item[2][2] for item in get_min_results_current]

#     d2_label = round(d2_spesific * 1e9, digits=2)
#     plot!(fig_r_all, d3s .* 1e9, min_r_tm_current, label="d2 = $d2_label nm")
#     plot!(fig_λ_all, d3s .* 1e9, min_λ_tm_current, label="d2 = $d2_label nm")
# end
# display(fig_r_all)
# if save savefig(fig_r_all, fig_path * "min_reflection_all_" * fig_token * ".pdf") end
# display(fig_λ_all)
# if save savefig(fig_λ_all, fig_path * "lambda_min_reflection_all_" * fig_token * ".pdf") end

## Plot plasmon minima and λ for plasmon as a function of d3 for all θds
# fig_r_all = plot(title="Minimum tm reflection for $description at θ = $θd degrees", legend=:topright, xlabel="thickness in nm", ylabel="minimum reflection coefficient", ticks=:native)
# fig_λ_all = plot(title="λ for minimum tm reflection for $description at θ = $θd degrees", legend=:topright, xlabel="thickness in nm", ylabel="λ at minimum reflection coefficient", ticks=:native)
# for θ1_spesific in θs
#     get_min_results_current = [get_min(λs_broadband, θ1_spesific, d2, d3_spesific) for d3_spesific in d3s]
#     min_r_tm_current = [item[1][2] for item in get_min_results_current]
#     min_λ_tm_current = [item[2][2] for item in get_min_results_current]

#     θ_label = round(θ1_spesific * 180 / π, digits=2)
#     plot!(fig_r_all, d3s .* 1e9, min_r_tm_current, label="θ = $θ_label")
#     plot!(fig_λ_all, d3s .* 1e9, min_λ_tm_current, label="θ = $θ_label")
# end
# display(fig_r_all)
# if save savefig(fig_r_all, fig_path * "min_reflection_angles_" * fig_token * ".pdf") end
# display(fig_λ_all)
# if save savefig(fig_λ_all, fig_path * "lambda_min_reflection_angles_" * fig_token * ".pdf") end

## Plot plasmon spectral response for all thicknesses
# fig_all = plot(title="Wavelength respons for all thicknesses for $description at θ = $θd", legend=:outerright, xlabel="wavelength in nm", ylabel="reflection coefficient", ticks=:native)
# for d in d3s
#     systemdata = reduce(vcat, transpose.(system_single.(λs, θ1, d2, d)))
#     d = round(d * 1e9, digits=2)
#     plot!(fig_all, λs .* 1e9, systemdata[:, 1], label="TE, d = $d")
#     plot!(fig_all, λs .* 1e9, systemdata[:, 2], label="TM, d = $d")
# end
# display(fig_all)
# if save savefig(fig_all, fig_path * "responses_" * fig_token * ".pdf") end

println("Finished without errors")


export product!, product, *, divide, /

"""
    product!(expod, expod1)

    Add the `expod1` degree to `expod` inplace (equivalent to the monomial product)
"""
function product!(expod::Exponent, expod1::Exponent)
    for (var, deg) in expod1
        if !haskey(expod, var)
            expod.expo[var] = Degree(0,0)
        end
        expod.expo[var].explvar += deg.explvar
        expod.expo[var].conjvar += deg.conjvar
        if (expod.expo[var].explvar, expod.expo[var].conjvar) == (0,0)
            delete!(expod, var)
        end
    end
    update_degree!(expod)
    return expod
end

function product!(expod::Exponent, var::Variable)
    if !haskey(expod, var)
        expod.expo[var] = Degree(0,0)
    end
    expod.expo[var].explvar += 1
    if (expod.expo[var].explvar, expod.expo[var].conjvar) == (0,0)
        delete!(expod, var)
    end

    update_degree!(expod)
    return expod
end

function product(exp1::Exponent, exp2::Exponent)
    expod = Exponent()
    product!(expod, exp1)
    product!(expod, exp2)
    return Exponent(expod)
end

## Polynomial
function product(p1::Polynomial, p2::Polynomial)
    p = Polynomial()
    for (expo1, λ1) in p1
        λ1 != 0 || continue
        for (expo2, λ2) in p2
            λ2 != 0 || continue
            expoprod = product(expo1, expo2)
            add_to_dict!(p.poly, expoprod, λ1 * λ2)
        end
    end
    update_degree!(p)
    return p
end

function product(p1::Polynomial, λ::Number)
    if λ == 0
        return Polynomial()
    end

    pdict = SortedDict{Exponent, Number}()
    for (expo, coeff) in p1
        pdict[expo] = coeff*λ
    end
    return Polynomial(pdict)
end

function product(p1::T, p2::U) where T<:AbstractPolynomial where U<:AbstractPolynomial
    return product(convert(Polynomial, p1), convert(Polynomial, p2))
end
# function product(p1::Number, p2::T) where T<:AbstractPolynomial
#     return product(convert(Polynomial, p1), convert(Polynomial, p2))
# end
# function product(p1::T, p2::Number) where T<:AbstractPolynomial
#     return product(convert(Polynomial, p1), convert(Polynomial, p2))
# end
function product(p1::T, p2::Number) where T<:AbstractPolynomial
    return product(convert(Polynomial, p1), p2)
end
function product(p1::Number, p2::T) where T<:AbstractPolynomial
    return product(convert(Polynomial, p2), p1)
end

function *(λ::Number, expo::Exponent)
    return Polynomial(SortedDict{Exponent, Number}(expo=>λ))
end
function *(expo::Exponent, λ::Number)
    return Polynomial(SortedDict{Exponent, Number}(expo=>λ))
end

function *(p1::T, p2::U) where T<:Union{Number, AbstractPolynomial} where U<:Union{Number, AbstractPolynomial}
    debug(LOGGER, "Inefficient implementation\n$(string(stacktrace()))")
    return product(p1, p2)
end

## Division
function divide(p1::Polynomial, p2::Polynomial)
    if length(p2) != 1
        error(LOGGER, "/(::Polynomial, ::Polynomial): Only allowed for monomial divisor ($(length(p2))-monomial polynomial here).")
    end
    expo, λ = collect(p2)[1]
    if expo.degree != Degree(0,0)
        error(LOGGER, "/(::Polynomial, ::Polynomial): Only allowed for constant divisor ($(expo.degree)-degree monomial here).")
    end
    if λ == 0
        error(LOGGER, "/(::Polynomial, ::Polynomial): Only allowed for non null constant divisor.")
    end
    return p1 * (1/λ)
end

function /(p1::T, p2::U) where T<:AbstractPolynomial where U<:AbstractPolynomial
    return divide(convert(Polynomial, p1), convert(Polynomial, p2))
end
function /(p1::Number, p2::T) where T<:AbstractPolynomial
    return divide(convert(Polynomial, p1), convert(Polynomial, p2))
end
function /(p1::T, p2::Number) where T<:AbstractPolynomial
    return divide(convert(Polynomial, p1), convert(Polynomial, p2))
end

## Point
function *(pt1::Point, λ::Number)
    debug(LOGGER, "Inefficient implementation\n$(string(stacktrace()))")
    pt = Point()
    if λ == 0
        return pt
    end
    for (var, val) in pt1
        pt[var] = λ*val
    end
    return pt
end
*(λ::Number, pt1::Point) = pt1*λ


"""
    noise_psd(x::Vector{<:Real}, smoothing, samplerate, nfft, hop=nfft÷2)
    noise_psd(X::Matrix{<:Complex}, smoothing, framerate)

Estimate the noise power-spectral-density of a noisy time-domain signal `x`,
or an STFT `X` using minimum statistics. This is similar to the approach in [1], but assumes a constant noise PSD. To reduce variance the signal will be
smoothed with a gaussian window `smoothing` seconds long. Genrally longer windows will provide a less-biased estimate, but it must be short enough that there will be some regions of the signal where the the smoothing window has none of the target signal in it.

`samplerate` is used for time-domain signals, vs `framerate` for STFTs. You can
convert between them with `framerate = samplerate / hop`. You can set the
samplerate/framerate to 1 to give smoothing in samples of frames, respectively.

The result will be of size `nfft÷2+1` for a time signal `x`, or the same height
as an STFT `X`.

[1] Martin, R., Spectral subtraction based on minimum statistics, 1994
"""
function noise_psd(x::AbstractVector{<:Real},
                   smoothing, samplerate,
                   nfft, hop=nfft÷2)
    noise_psd(stft2(x, nfft, hop), smoothing, samplerate/hop)
end

function noise_psd(X::AbstractMatrix{<:Complex}, smoothing, framerate)
    L = round(Int, smoothing * framerate)
    if iseven(L)
        L += 1
    end
    win = gaussian(L, 0.14)
    win ./= sum(win)
    map(1:size(X, 1)) do band
        # we skip the last STFT frame because it is often lower-power due to
        # being only partially-filled. Note that it would be more correct to
        # actually take the STFT hop and window sizes into account, but in
        # in practice we're often using 50% overlap and this effect shouldn't be
        # that important.
        sm = conv(abs2.(X[band, 1:end-1]), win)
        minimum(sm[L:end-L+1])
    end
end


# Mag
set!(res::MagLike, ::Irrational{:π}) = const_pi!(res)

# Arb
function set!(res::ArbLike, x::Union{MagLike,BigFloat})
    set!(radref(res), UInt64(0))
    set!(midref(res), x)
    return res
end

function set!(res::ArbLike, x::Rational; prec::Integer = precision(res))
    set!(res, numerator(x))
    return div!(res, res, denominator(x), prec = prec)
end

for (irr, suffix) in ((:π, "pi"), (:ℯ, "e"), (:γ, "euler"), (:catalan, "catalan"))
    jlf = Symbol("const_$suffix", "!")
    IrrT = Irrational{irr}
    @eval begin
        set!(res::ArbLike, ::$IrrT; prec::Integer = precision(res)) = $jlf(res, prec = prec)
    end
end

function set!(res::ArbLike, ::Irrational{:φ}; prec::Integer = precision(res))
    set!(res, 5)
    sqrt!(res, res, prec)
    add!(res, res, 1, prec)
    return div!(res, res, 2, prec)
end

# Acb
function set!(res::AcbLike, x::Union{Real,arf_struct,mag_struct})
    set!(realref(res), x)
    set!(imagref(res), 0)
    return res
end

function set!(
    res::AcbLike,
    re::Union{Real,arb_struct,arf_struct,mag_struct},
    im::Union{Real,arb_struct,arf_struct,mag_struct},
)
    set!(realref(res), re)
    set!(imagref(res), im)
    return res
end

set!(res::AcbLike, z::Complex) = set!(res, real(z), imag(z))
set!(res::AcbLike, ::Irrational{:π}) = const_pi!(res)


function dayNumber(inDate::Date)
	return Dates.date2epochdays(inDate)::Integer;
end

excelcostant=693959;

"""
From Excel Number Format to Date

		Date=fromExcelNumberToDate(ExcelNumber)

Where:\n
		ExcelNumber = Integer representing a date in the excel format.

		Date      = Date representing the input in the Julia object format.

# Example
```julia-repl
julia> fromExcelNumberToDate(45000)
2023-03-15
```
"""
function fromExcelNumberToDate(num::Integer)::Date
	return Dates.Dates.epochdays2date(excelcostant+num);
end

"""
Actual Number of days between two dates

		ndays=daysact(Date1,Date2)

Where:\n
		Date1 = Start date.
		Date2 = End date.

		ndays      = Actual Number of days between Start Date and End Date.

# Example
```julia-repl
julia> daysact(Date(1996,10,12),Date(1998,1,10))
455
```
"""
function daysact(Date1::Date,Date2::Date)::Integer
	D1= dayNumber(Date1);
	D2= dayNumber(Date2);
	dayCount=D2-D1;
	return dayCount;
end

function isLastOfFebruary(inDate::Date)::Bool
	return (Dates.isleapyear(Dates.year(inDate))&&(Dates.day(inDate)==29)&&(Dates.month(inDate)==2))||((!Dates.isleapyear(Dates.year(inDate)))&&(Dates.day(inDate)==28)&&(Dates.month(inDate)==2));
end

currMaxImplemented=7;

"""
Fraction of year between two Dates according the following convention

		yfr=yearfrac(Date1,Date2,basis)

Where:\n
		Date1 = Start date.
		Date2 = End date.
		basis = Integer representing the following conventions:
				- 0 = (ACT/ACT)
				- 1 = (30/360 SIA)
				- 2 = (ACT/360)
				- 3 = (ACT/365)
				- 4 = (30/360 PSA)
				- 5 = (30/360 ISDA)
				- 6 = (30E/360)
				- 7 = (ACT/365 JPN)

		yfr      = fraction of year between start and end date according to basis.

# Example
```julia-repl
julia> yearfrac(Date(1996,10,12),Date(1998,1,10),1)
1.2444444444444445
```
"""
function yearfrac(startDate::Date,endDate::Date,convention::Integer)::Real
if (convention<0)
	error("Negative basis are not defined, check the help")
end
if(convention>currMaxImplemented)
	error("Convention not implemented yet")
end
if(startDate>endDate)
	return -yearfrac(endDate,startDate,convention);
elseif(startDate==endDate)
	return 0.0;
end
yearFrac=0.0;
y1=Dates.year(startDate);
m1=Dates.month(startDate);
y2=Dates.year(endDate);
m2=Dates.month(endDate);
d1=Dates.day(startDate);
d2=Dates.day(endDate);
if (convention==0)#(ACT/ACT)
	Nday=daysact(startDate,endDate);
	EndOFYear=Date(1);
	if(isLastOfFebruary(startDate)&&(Dates.day(startDate)==29))
		EndOFYear=Date(Dates.year(startDate)+1,3,1);
	else
		EndOFYear=Date(Dates.year(startDate)+1,Dates.month(startDate),Dates.day(startDate));
	end
	yearFrac=(Nday)/ daysact(startDate,EndOFYear);
elseif(convention==1)  	#(30/360 SIA)
	if(isLastOfFebruary(startDate)&&isLastOfFebruary(endDate))
		d2=30;
	end
	if(isLastOfFebruary(startDate)||Dates.day(startDate)==31)
		d1=30;
	end
	if(d1==30&&d2==31)
		d2=30;
	end
	dy=y2-y1;
	dm=m2-m1;
	dd=d2-d1;
	yearFrac=(360.0*dy+30.0*dm+dd)/360.0;
elseif(convention==2)		#(ACT/360)
	Nday=daysact(startDate,endDate);
	yearFrac=(Nday)/360.0;
elseif(convention==3)		# (ACT/365)
	Nday=daysact(startDate,endDate);
	yearFrac=( Nday)/365.0;
elseif(convention==4)		# (30/360 PSA)
	if((Dates.day(startDate)==31)||isLastOfFebruary(startDate))
		d1=30;
	end
	if((Dates.day(startDate)==30||isLastOfFebruary(startDate))&&Dates.day(endDate)==31)
		d2=30;
	end
	dy=y2-y1;
	dm=m2-m1;
	dd=d2-d1;
	yearFrac=(360.0*dy+30.0*dm+dd)/360.0;

elseif(convention==5)		#(30/360 ISDA)
	y1=Dates.year(startDate);
	m1=Dates.month(startDate);
	y2=Dates.year(endDate);
	m2=Dates.month(endDate);
	if(Dates.day(startDate)<31)
		d1=Dates.day(startDate);
	else
		d1=30;
	end
	if ((Dates.day(endDate)==31)&&(d1>29))
		d2=30;
	else
		d2=Dates.day(endDate);
	end
	yearFrac=(360.0*((y2-y1))+30.0*((m2-m1))+(d2-d1))/360.0;

elseif(convention==6)		#(30E/360)
	if(d1==31)
		d1=30;
	end
	if(d2==31)
		d2=30;
	end
	dy=y2-y1;
	dm=m2-m1;
	dd=d2-d1;
	yearFrac=(360.0*dy+30.0*dm+dd)/360.0;
elseif(convention==7)		#(ACT/365 JPN)
	daydistance = [0;31;59;90;120;151;181;212;243;273;304;334];
	dy=y2-y1;
	dd=d2-d1;
	dayCount=(365.0*dy+daydistance[m2]-daydistance[m1]+dd)
	yearFrac = dayCount/365.0;
end

return yearFrac;

end


using HistogramThresholding, Images, TestImages
using Test

function gaussian_mixture(z;P₁,μ₁,σ₁,P₂,μ₂,σ₂)
    (P₁ / (σ₁*√(2*π))) * exp( -(z-μ₁)^2 / (2*σ₁^2) )  +  (P₂ / (σ₂*√(2*π))) * exp( -(z-μ₂)^2 / (2*σ₂^2) )
end

function surge(t;A,k)
    A*t*exp(-k*t)
end

@testset "HistogramThresholding.jl" begin
    include("otsu.jl")
    include("minimum.jl")
    include("intermodes.jl")
    include("unimodal.jl")
    include("moments.jl")
    include("minimum_error.jl")
end


using LinearAlgebraicRepresentation
Lar = LinearAlgebraicRepresentation
using Plasm

using PyCall
p = pyimport("pyplasm")

geom_0 = hcat([[x] for x=0.:10]...)
topol_0 = [[i,i+1] for i=1:9]
geom_1 = hcat([[0.],[1.],[2.]]...)
topol_1 = [[1,2],[2,3]]

model_0 = (geom_0,topol_0)
model_1 = (geom_1,topol_1)
model_2 = Lar.larModelProduct(model_0, model_1)
model_3 = Lar.larModelProduct(model_2, model_1)

p["VIEW"](Plasm.lar2hpc(model_3...))
V,CV = model_3

hpc = Plasm.hpc_exploded( (V,[CV]) )(1.5,1.5,1.5)
Plasm.view(hpc)

hpc = Plasm.lar2exploded_hpc(model_3[1], model_3[2])(1.5,1.5,1.5)
Plasm.view(hpc)

shape = [10,10,2]
cubes = Lar.larCuboids(shape,true)
V,FV = cubes[1],cubes[2][3]
Plasm.viewexploded(V,FV)()

V,EV = Lar.larCuboids([30])
V1 = hcat( map(u->[u; 0.0], V )...)
W = hcat( map(u->[cos(u*(2π/30)); sin(u*(2π/30))], V )...)
Plasm.viewexploded(V1,EV)()
Plasm.viewexploded(W,EV)()

V,FV = Lar.larCuboids([30,6])
V2 = [2π/30 0; 0 1/6] * V
W = [V2[:,k] for k=1:size(V,2)]
Z = hcat( map(p->let (u,v) = p; [v*cos(u); v*sin(u)] end, W) ...)
Plasm.viewexploded(V2,FV)()
Plasm.viewexploded(Z,FV)()

V,CV = Lar.larCuboids([16,32,1])
V3 = [π/16 0 0; 0 2π/32 0; 0 0 1] * V
W = [V3[:,k]-[π/2,π,1/2] for k=1:size(V3,2)]
Z = hcat( map(p->let (u,v,w) = p; [w*cos(u)*cos(v); w*cos(u)*sin(v); w*sin(u)] end, W) ...)
Plasm.viewexploded(V3,CV)()
Plasm.viewexploded(Z,CV)(2,2,2)


### implements the coveriance matrices as described in Patchwork Kriging for Large-scale Gaussian Process Regression
# https://jmlr.org/papers/v19/17-042.html

function setuppatchGP!(X_parts::Vector{Vector{Vector{T}}},
    θ::KT) where {T,KT}

    N_parts = length(X_parts)

    ### K_XX.
    K_XX_set = Vector{Matrix{T}}(undef, N_parts)

    for n = 1:N_parts

        K_XX_set[n] = constructkernelmatrix(X_parts[n], θ)
    end

    boundary_labels, bb_positive_list, bb_negative_list,
    Xb_positive_list, Xb_negative_list = setupboundaryquantities(X_parts)

    ### K_bb.

    ### K_Xb.

    ### think about fast serach for queryX and queryb.

    return boundary_labels
end

function setupboundaryquantities(X_parts::Vector{Vector{Vector{T}}}) where T

    N_parts = length(X_parts)

    # get boundary labels.
    M = round(Int, N_parts*(N_parts-1)/2)
    boundary_labels = Vector{Tuple{Int,Int}}(undef, M)
    a = 0

    for j = 2:N_parts
        for i = 1:j-1
            a += 1
            boundary_labels[a] = (i,j)
        end
    end
    N_boundaries = length(boundary_labels)

    ### mark part of the non-zero entries of K_bb.
    bb_positive_list = Vector{Tuple{Int,Int}}(undef,0)
    bb_negative_list = Vector{Tuple{Int,Int}}(undef,0)

    #for j = 2:M
        #for i = 1:j-1
    for m1 = 1:N_boundaries
        for m2 = 1:N_boundaries

            k, l = boundary_labels[m1]
            u, v = boundary_labels[m2]

            if (k == u && l != v) || (k != u && l == v)

                push!(bb_positive_list, (m1,m2))

            elseif (k==v && l != u) || (k!=v && l == u)

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

        end
    end

    ### get non-zero entries of K_Xb.

    Xb_positive_list = Vector{Tuple{Int,Int}}(undef,0)
    Xb_negative_list = Vector{Tuple{Int,Int}}(undef,0)

    # number of training data inputs, i.e., excluding patchwork/boundary inputs.
    N_X = sum( length(X_parts[n]) for n = 1:N_parts )

    for m = 1:N_boundaries
        for j = 1:N_parts

            #part_ind = div(i, N_parts) +1
            k, l = boundary_labels[m]

            if k == j
                push!(Xb_positive_list, (m,j))

            elseif l == j
                push!(Xb_positive_list, (m,j))
            end
        end
    end

    return boundary_labels, bb_positive_list, bb_negative_list,
        Xb_positive_list, Xb_negative_list
end
# ####### patchworkGP: kernel matrix contruction.
# boundary_labels, bb_positive_list, bb_negative_list,
# Xb_positive_list, Xb_negative_list = PatchMixtureKriging.setupboundaryquantities(X_parts)

# pause development since the GP inducing-point styled approx. inference could be very slow.
# instead, work on the mixture of GP idea.


# things that don't easily fit elsewhere
import Base.UpperTriangular

zero_like(A::Array) = zeros(A)
zero_like(x::Number) = zero(x)
lB(x::Vector) = sum(lgamma(x)) - lgamma(sum(x))
lB(x::Matrix) = sum(lgamma(x), 1) - lgamma(sum(x, 1))  # columnwise

# define a convert method for Arrays to PDMats (positive definite matrices)
# if the array is not posdef, this will throw an exception
raw(Σ::PDMat) = Σ.mat
raw(Σ::PDSparseMat) = Σ.mat
raw(Σ::PDiagMat) = Σ.diag
raw(Σ::ScalMat) = diag(Σ)

convert{T<:AbstractPDMat}(::Type{T}, P::T) = P
convert{T <: Number}(::Type{PDMat{T, Matrix{T}}}, P::Matrix{T}) = PDMat(P)
convert{T<:AbstractPDMat, S<:AbstractPDMat}(::Type{T}, P::S) = convert(T, raw(P))

flatten(a::Number) = a
flatten(a::Array) = reshape(a, prod(size(a)))
flatten(a::AbstractPDMat) = flatten(UpperTriangular(full(a)))
function flatten(a::UpperTriangular)
    d = size(a, 1)
    l = div(d * (d + 1), 2)
    out = Array(eltype(a), l)
    idx = 1
    for c in 1:d
        for r in 1:d
            if r ≤ c
                out[idx] = a.data[r, c]
                idx += 1
            end
        end
    end
    out
end
function flatten(a::LowerTriangular)
    d = size(a, 1)
    l = div(d * (d + 1), 2)
    out = Array(eltype(a), l)
    idx = 1
    for c in 1:d
        for r in 1:d
            if r ≥ c
                out[idx] = a.data[r, c]
                idx += 1
            end
        end
    end
    out
end

function UpperTriangular(v::AbstractVector)
    l = length(v)
    d = (-1 + Int(sqrt(1 + 8l))) ÷ 2
    A = zeros(eltype(v), d, d)
    idx = 1
    for c in 1:d
        for r in 1:d
            if r ≤ c
                A[r, c] = v[idx]
                idx += 1
            end
        end
    end
    UpperTriangular(A)
end
function LowerTriangular(v::AbstractVector)
    l = length(v)
    d = (-1 + Int(sqrt(1 + 8l))) ÷ 2
    A = zeros(eltype(v), d, d)
    idx = 1
    for c in 1:d
        for r in 1:d
            if r ≥ c
                A[r, c] = v[idx]
                idx += 1
            end
        end
    end
    LowerTriangular(A)
end

"""
Generate a Normal distribution from a vector of unconstrained
parameters. Number of required parameters is
p (μ) + p(p + 1)/2 (Σ) = p(p + 3)/2
"""
normal_from_unconstrained(x::Vector) = Normal(x[1], exp(x[2]))

function mvnormal_from_unconstrained(x::Vector, full=false)
    if full
        # number of parameters: will throw InexactError if not an integer
        p = Int((-3 + sqrt(9 + 8 * length(x)))/2)
        d = MvNormal(x[1:p], constrain(RCovMat(p), x[p+1:end]))
    else
        # return a multivariate normal with diagonal covariance
        p = Int(length(x)/2)
        d = MvNormal(x[1:p], exp(x[p+1:end]))  # μ, σ constructor
    end
    d
end


#   Translation of Matlab version by John Myles White
#   Translation of minpack subroutine cvsrch
#   Dianne O'Leary   July 1991
#
#     **********
#
#     Subroutine cvsrch
#
#     The purpose of cvsrch is to find a step which satisfies
#     a sufficient decrease condition and a curvature condition.
#     The user must provide a subroutine which calculates the
#     function and the gradient.
#
#     At each stage the subroutine updates an interval of
#     uncertainty with endpoints stx and sty. The interval of
#     uncertainty is initially chosen so that it contains a
#     minimizer of the modified function
#
#          f(x + stp * s) - f(x) - f_tol * stp * (gradf(x)' * s).
#
#     If a step is obtained for which the modified function
#     has a nonpositive function value and nonnegative derivative,
#     then the interval of uncertainty is chosen so that it
#     contains a minimizer of f(x + stp * s).
#
#     The algorithm is designed to find a step which satisfies
#     the sufficient decrease condition
#
#           f(x + stp * s) <= f(x) + f_tol * stp * (gradf(x)' * s),
#
#     and the curvature condition
#
#           abs(gradf(x + stp * s)' * s)) <= gtol * abs(gradf(x)' * s).
#
#     If f_tol is less than gtol and if, for example, the function
#     is bounded below, then there is always a step which satisfies
#     both conditions. If no step can be found which satisfies both
#     conditions, then the algorithm usually stops when rounding
#     errors prevent further progress. In this case stp only
#     satisfies the sufficient decrease condition.
#
#     The subroutine statement is
#
#        subroutine cvsrch(df,n,x,f,g,s,stp,f_tol,gtol,x_tol,
#                          stpmin,stpmax,maxfev,info,nfev,wa)
#
#     where
#
#	df is the name of the user-supplied subroutine which
#    calculates the function and the gradient.  df must
#    be declared in an external statement in the user
#    calling program, and should be written as follows.
#
#    function [f,g] = df(n,x) (Matlab)
#                     (10/2010 change in documentation)
#                     (derived from Fortran subroutine df(n,x,f,g))
#    integer n
#    f
#    x(n),g(n)
#
#    Calculate the function at x and
#    return this value in the variable f.
#    Calculate the gradient at x and
#    return this vector in g.
#
#  n is a positive integer input variable set to the number
#	  of variables.
#
#	x is an array of length n. On input it must contain the
#	  base point for the line search. On output it contains
#    x + stp * s.
#
#	f is a variable. On input it must contain the value of f
#    at x. On output it contains the value of f at x + stp * s.
#
#	g is an array of length n. On input it must contain the
#    gradient of f at x. On output it contains the gradient
#    of f at x + stp * s.
#
#	s is an input array of length n which specifies the
#    search direction.
#
#	stp is a nonnegative variable. On input stp contains an
#    initial estimate of a satisfactory step. On output
#    stp contains the final estimate.
#
#  f_tol and gtol are nonnegative input variables. Termination
#    occurs when the sufficient decrease condition and the
#    directional derivative condition are satisfied.
#
#	x_tol is a nonnegative input variable. Termination occurs
#    when the relative width of the interval of uncertainty
#	  is at most x_tol.
#
#	stpmin and stpmax are nonnegative input variables which
#	  specify lower and upper bounds for the step.
#
#	maxfev is a positive integer input variable. Termination
#    occurs when the number of calls to df is at least
#    maxfev by the end of an iteration.
#
#	info is an integer output variable set as follows:
#
#	  info = 0  Improper input parameters.
#
#	  info = 1  The sufficient decrease condition and the
#              directional derivative condition hold.
#
#	  info = 2  Relative width of the interval of uncertainty
#		         is at most x_tol.
#
#	  info = 3  Number of calls to df has reached maxfev.
#
#	  info = 4  The step is at the lower bound stpmin.
#
#	  info = 5  The step is at the upper bound stpmax.
#
#	  info = 6  Rounding errors prevent further progress.
#              There may not be a step which satisfies the
#              sufficient decrease and curvature conditions.
#              Tolerances may be too small.
#
#    nfev is an integer output variable set to the number of
#         calls to df.
#
#     Argonne National Laboratory. MINPACK Project. June 1983
#     Jorge J. More', David J. Thuente
#
#     **********

# Returns x, f, g, stp, info, nfev
# TODO: Decide whether to update x, f, g and info
#       or just return step and nfev and let existing code do its job

function mt_linesearch!{T}(df,
                         x::Vector,
                         s::Vector,
                         new_x::Vector,
                         g::Vector,
                         lsr::LineSearchResults{T},
                         c::Real,
                         mayterminate::Bool;
                         n::Integer = length(x),
                         stp::Real = 1.0,
                         f_tol::Real = 1e-4,
                         gtol::Real = 0.9,
                         x_tol::Real = 1e-8,
                         stpmin::Real = 1e-16,
                         stpmax::Real = 65536.0,
                         maxfev::Integer = 100)

   info = 0
   info_cstep = 1 # Info from step

   # Count function and gradient calls
   f_calls = 0
   g_calls = 0

   f = df.fg!(x, g)
   f_calls += 1
   g_calls += 1

   #
   # Check the input parameters for errors.
   #

   if n <= 0 || stp <= 0.0 || f_tol < 0.0 || gtol < 0.0 ||
      x_tol < 0.0 || stpmin < 0.0 || stpmax < stpmin || maxfev <= 0
      throw(ArgumentError("Invalid parameters to mure_thuente_line_search"))
   end

   #
   # Compute the initial gradient in the search direction
   # and check that s is a descent direction.
   #

   dginit = vecdot(g, s)
   if dginit >= 0.0
      throw(ArgumentError("Search direction is not a direction of descent"))
   end

   #
   # Initialize local variables.
   #

   bracketed = false
   stage1 = true
   nfev = 0
   finit = f
   dgtest = f_tol * dginit
   width = stpmax - stpmin
   width1 = 2 * width
   copy!(new_x, x)
   # Keep this across calls
   # Replace with new_x?

   #
   # The variables stx, fx, dgx contain the values of the step,
   # function, and directional derivative at the best step.
   # The variables sty, fy, dgy contain the value of the step,
   # function, and derivative at the other endpoint of
   # the interval of uncertainty.
   # The variables stp, f, dg contain the values of the step,
   # function, and derivative at the current step.
   #

   stx = 0.0
   fx = finit
   dgx = dginit
   sty = 0.0
   fy = finit
   dgy = dginit

   while true
      #
      # Set the minimum and maximum steps to correspond
      # to the present interval of uncertainty.
      #

      if bracketed
         stmin = min(stx, sty)
         stmax = max(stx, sty)
      else
         stmin = stx
         stmax = stp + 4 * (stp - stx) # Why 4?
      end

      #
      # Force the step to be within the bounds stpmax and stpmin
      #

      stp = max(stp, stpmin)
      stp = min(stp, stpmax)

      #
      # If an unusual termination is to occur then let
      # stp be the lowest point obtained so far.
      #

      if (bracketed && (stp <= stmin || stp >= stmax)) ||
           nfev >= maxfev-1 || info_cstep == 0 ||
           (bracketed && stmax - stmin <= x_tol * stmax)
         stp = stx
      end

      #
      # Evaluate the function and gradient at stp
      # and compute the directional derivative.
      #

      for i in 1:n
         new_x[i] = x[i] + stp * s[i] # TODO: Use x_new here
      end
      f = df.fg!(new_x, g)
      f_calls += 1
      g_calls += 1
      nfev += 1 # This includes calls to f() and g!()
      dg = vecdot(g, s)
      ftest1 = finit + stp * dgtest

      #
      # Test for convergence.
      #

      # What's does info_cstep stand for?

      if (bracketed && (stp <= stmin || stp >= stmax)) || info_cstep == 0
         info = 6
      end
      if stp == stpmax && f <= ftest1 && dg <= dgtest
         info = 5
      end
      if stp == stpmin && (f > ftest1 || dg >= dgtest)
         info = 4
      end
      if nfev >= maxfev
         info = 3
      end
      if bracketed && stmax - stmin <= x_tol * stmax
         info = 2
      end
      if f <= ftest1 && abs(dg) <= -gtol * dginit
         info = 1
      end

      #
      # Check for termination.
      #

      if info != 0
         return stp, f_calls, g_calls
      end

      #
      # In the first stage we seek a step for which the modified
      # function has a nonpositive value and nonnegative derivative.
      #

      if stage1 && f <= ftest1 && dg >= min(f_tol, gtol) * dginit
         stage1 = false
      end

      #
      # A modified function is used to predict the step only if
      # we have not obtained a step for which the modified
      # function has a nonpositive function value and nonnegative
      # derivative, and if a lower function value has been
      # obtained but the decrease is not sufficient.
      #

      if stage1 && f <= fx && f > ftest1
         #
         # Define the modified function and derivative values.
         #
         fm = f - stp * dgtest
         fxm = fx - stx * dgtest
         fym = fy - sty * dgtest
         dgm = dg - dgtest
         dgxm = dgx - dgtest
         dgym = dgy - dgtest
         #
         # Call cstep to update the interval of uncertainty
         # and to compute the new step.
         #
         stx, fxm, dgxm,
         sty, fym, dgym,
         stp, fm, dgm,
         bracketed, info_cstep =
           cstep(stx, fxm, dgxm, sty, fym, dgym,
                 stp, fm, dgm, bracketed, stmin, stmax)
         #
         # Reset the function and gradient values for f.
         #
         fx = fxm + stx * dgtest
         fy = fym + sty * dgtest
         dgx = dgxm + dgtest
         dgy = dgym + dgtest
      else
         #
         # Call cstep to update the interval of uncertainty
         # and to compute the new step.
         #
         stx, fx, dgx,
         sty, fy, dgy,
         stp, f, dg,
         bracketed, info_cstep =
           cstep(stx, fx, dgx, sty, fy, dgy,
                 stp, f, dg, bracketed, stmin, stmax)
      end

      #
      # Force a sufficient decrease in the size of the
      # interval of uncertainty.
      #

      if bracketed
         if abs(sty - stx) >= 0.66 * width1
            stp = stx + 0.5 * (sty - stx)
         end
         width1 = width
         width = abs(sty - stx)
      end
   end # while
end # function


export Dirac

struct Dirac{X} <: AbstractMeasure{X}
    supp :: X
end

Dirac(x::X) = Dirac{X}(x)

eltype(μ::Dirac{X}) = X

function (μ::Dirac{X})(s) where {X}
    μ.supp ∈ s && return 1
    return 0
end

function logdensity(μ::Dirac{X}, x::X)
    μ.supp ∈ s && return 0.0
    return -Inf
end


include("activate.jl")

using LinearAlgebra
using Random
import GLPK
using JSON

n = 200
k = 3000
#k = 500

xpi = rand(n * n);
total = sum(xpi);
# next line needs to be commented out if we use the GLPK variants
xpi = reshape(xpi, n, n)
const xp = xpi # ./ total;

# better for memory consumption as we do coordinate-wise ops

function cf(x, xp)
    return LinearAlgebra.norm(x .- xp)^2 / n^2
end

function cgrad!(storage, x, xp)
    return @. storage = 2 * (x - xp) / n^2
end

# initial direction for first vertex
direction_vec = Vector{Float64}(undef, n * n)
randn!(direction_vec)
direction_mat = reshape(direction_vec, n, n)

lmo = FrankWolfe.BirkhoffPolytopeLMO()
x00 = FrankWolfe.compute_extreme_point(lmo, direction_mat)
target_accuracy = 1e-7

# modify to GLPK variant
# o = GLPK.Optimizer()
# lmo_moi = FrankWolfe.convert_mathopt(lmo, o, dimension=n)
# x00 = FrankWolfe.compute_extreme_point(lmo, direction_vec)

FrankWolfe.benchmark_oracles(
    x -> cf(x, xp),
    (str, x) -> cgrad!(str, x, xp),
    () -> randn(n, n),
    lmo;
    k=100,
)

# vanllia FW

x0 = copy(x00)

x, v, primal, dual_gap, trajectoryFW = FrankWolfe.frank_wolfe(
    x -> cf(x, xp),
    (str, x) -> cgrad!(str, x, xp),
    lmo,
    x0,
    max_iteration=k,
    line_search=FrankWolfe.Adaptive(),
    print_iter=k / 10,
    epsilon=target_accuracy,
    emphasis=FrankWolfe.memory,
    trajectory=true,
    verbose=true,
);

# arbitrary cache

x0 = copy(x00)

x, v, primal, dual_gap, trajectoryLCG = FrankWolfe.lazified_conditional_gradient(
    x -> cf(x, xp),
    (str, x) -> cgrad!(str, x, xp),
    lmo,
    x0,
    max_iteration=k,
    epsilon=target_accuracy,
    line_search=FrankWolfe.Adaptive(),
    print_iter=k / 10,
    emphasis=FrankWolfe.memory,
    trajectory=true,
    verbose=true,
);

# fixed cache size

x0 = copy(x00)

x, v, primal, dual_gap, trajectoryBLCG = FrankWolfe.lazified_conditional_gradient(
    x -> cf(x, xp),
    (str, x) -> cgrad!(str, x, xp),
    lmo,
    x0,
    max_iteration=k,
    line_search=FrankWolfe.Adaptive(),
    print_iter=k / 10,
    epsilon=target_accuracy,
    emphasis=FrankWolfe.memory,
    trajectory=true,
    cache_size=500,
    verbose=true,
);

# AFW run

x0 = copy(x00)

x, v, primal, dual_gap, trajectoryLAFW = FrankWolfe.away_frank_wolfe(
    x -> cf(x, xp),
    (str, x) -> cgrad!(str, x, xp),
    lmo,
    x0,
    max_iteration=k,
    line_search=FrankWolfe.Adaptive(),
    print_iter=k / 10,
    linesearch_tol=1e-9,
    epsilon=target_accuracy,
    emphasis=FrankWolfe.memory,
    lazy=true,
    trajectory=true,
    verbose=true,
);

# BCG run

x0 = copy(x00)

x, v, primal, dual_gap, trajectoryBCG = FrankWolfe.blended_conditional_gradient(
    x -> cf(x, xp),
    (str, x) -> cgrad!(str, x, xp),
    lmo,
    x0,
    max_iteration=k,
    line_search=FrankWolfe.Adaptive(),
    print_iter=k / 10,
    linesearch_tol=1e-9,
    epsilon=target_accuracy,
    emphasis=FrankWolfe.memory,
    trajectory=true,
    verbose=true,
);

# BCG run (reference optimum)

x0 = copy(x00)

x, v, primal, dual_gap, trajectoryBCG_ref = FrankWolfe.blended_conditional_gradient(
    x -> cf(x, xp),
    (str, x) -> cgrad!(str, x, xp),
    lmo,
    x0,
    max_iteration=2 * k,
    line_search=FrankWolfe.Adaptive(),
    print_iter=k / 10,
    epsilon=target_accuracy / 10.0,
    linesearch_tol=1e-9,
    emphasis=FrankWolfe.memory,
    trajectory=true,
    verbose=true,
);

open("lcg_expensive_data.json", "w") do f
    return write(
        f,
        JSON.json((
            FW=trajectoryFW,
            LCG=trajectoryLCG,
            BLCG=trajectoryBLCG,
            LAFW=trajectoryLAFW,
            BCG=trajectoryBCG,
            reference_BCG_primal=primal,
        )),
    )
end

data = [trajectoryFW, trajectoryLCG, trajectoryBLCG, trajectoryLAFW, trajectoryBCG]
label = ["FW", "L-CG", "BL-CG", "L-AFW", "BCG"]
FrankWolfe.plot_trajectories(data, label, xscalelog=true)