# MIT license
# Copyright (c) Microsoft Corporation. All rights reserved.
# See LICENSE in the project root for full license information.

@testset "General" begin
    @testset "QuadraticRoots" begin
        Random.seed!(SEED)
        similarroots(r1, r2, x1, x2) = isapprox([r1, r2], [x1, x2], rtol = 1e-9) || isapprox([r2, r1], [x1, x2], rtol = 1e-9)

        for i in 1:10000
            r1, r2, scale = rand(3) .- 0.5
            a, b, c = scale .* (1, r1 + r2, r1 * r2)
            x1, x2 = quadraticroots(a, b, c)
            @test similarroots(-r1, -r2, x1, x2)
        end

        a, b, c = 1, 0, -1
        x1, x2 = quadraticroots(a, b, c)
        @test similarroots(1, -1, x1, x2)

        a, b, c = 1, 2, 1 # (x+1)(x+1)= x^2 + 2x + 1, double root at one
        x1, x2 = quadraticroots(a, b, c)
        @test similarroots(-1, -1, x1, x2)
    end # testset QuadraticRoots

    @testset "Transform" begin
        Random.seed!(SEED)
        @test isapprox(rotmatd(180, 0, 0), [1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 -1.0], rtol = RTOLERANCE, atol = ATOLERANCE)
        @test isapprox(rotmatd(0.0, 180.0, 0.0), [-1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 -1.0], rtol = RTOLERANCE, atol = ATOLERANCE)
        @test isapprox(rotmatd(0, 0, 180), [-1.0 0.0 0.0; 0.0 -1.0 0.0; 0.0 0.0 1.0], rtol = RTOLERANCE, atol = ATOLERANCE)
        @test isapprox(rotmatd(0, 90, 0), [0.0 0.0 1.0; 0.0 1.0 0.0; -1.0 0.0 0.0], rtol = RTOLERANCE, atol = ATOLERANCE)
        @test isapprox(rotmatd(45, -45, 45), [0.5 -0.8535533905932737 0.1464466094067261; 0.5 0.14644660940672644 -0.8535533905932737; 0.7071067811865475 0.5 0.5], rtol = RTOLERANCE, atol = ATOLERANCE)
        x, y, z = rand(3)
        @test isapprox(rotmatd(x * 180 / π, y * 180 / π, z * 180 / π), rotmat(x, y, z), rtol = RTOLERANCE, atol = ATOLERANCE)

        @test isapprox(Transform(rotmatd(0, 90, 0), SVector(0.0, 0.0, 1.0)) * SVector(1.0, 0.0, 0.0), [0.0, 0.0, 0.0], rtol = RTOLERANCE, atol = ATOLERANCE)

        ta = Transform(rotmatd(0, 90, 0), SVector(0.0, 0.0, 1.0))
        tb = Transform(rotmatd(90, 0, 0), SVector(1.0, 0.0, 0.0))
        @test isapprox(collect(ta * tb), collect(Transform(rotmatd(90, 90, 0), SVector(0.0, 0.0, 0.0))), rtol = RTOLERANCE, atol = ATOLERANCE)

        @test isapprox(collect(ta * inv(ta)), collect(identitytransform()), rtol = RTOLERANCE, atol = ATOLERANCE)
        @test isapprox(collect(tb * inv(tb)), collect(identitytransform()), rtol = RTOLERANCE, atol = ATOLERANCE)
        @test isapprox(collect(inv(ta)), collect(Transform(rotmatd(0, -90, 0), SVector(1.0, 0.0, 0.0))), rtol = RTOLERANCE, atol = ATOLERANCE)

        #verify that matrix and vector{vector} forms of point representations are transformed the same way
        pts = SVector(SVector(1.0,1.0,1.0),SVector(2.0,2.0,2.0),SVector(3.0,3.0,3.0))
        ptsmat = SMatrix{3,3}(1.0,1.0,1.0,2.0,2.0,2.0,3.0,3.0,3.0)
        rottrans = rotationd(20,30,40)
        for i in 1:3
            @test isapprox((rottrans*pts[i]),(rottrans*ptsmat)[:,i])
        end
    end # testset Transform

    @testset "Interval" begin
        pt1 = Intersection(0.3, [4.0, 5.0, 6.0], normalize(rand(3)), 0.4, 0.5, NullInterface())
        pt2 = Intersection(0.5, [1.0, 2.0, 3.0], normalize(rand(3)), 0.2, 0.3, NullInterface())
        @test pt1 < pt2 && pt1 <= pt2 && pt2 > pt1 && pt2 >= pt1 && pt1 != pt2 && pt1 <= pt1
        intvl2 = positivehalfspace(pt1)
        @test α(halfspaceintersection(intvl2)) == α(pt1)

        ### interval intersection
        intersectionat = TestData.intersectionat
        ## interval/interval
        # one in another
        a = Interval(intersectionat(0.1), intersectionat(0.2))
        b = Interval(intersectionat(0.0), intersectionat(0.3))
        @test intervalintersection(a, b) == intervalintersection(b, a) == a
        # overlap
        a = Interval(intersectionat(0.1), intersectionat(0.3))
        b = Interval(intersectionat(0.2), intersectionat(0.4))
        @test intervalintersection(a, b) == intervalintersection(b, a) == Interval(intersectionat(0.2), intersectionat(0.3))
        # no overlap
        a = Interval(intersectionat(0.1), intersectionat(0.2))
        b = Interval(intersectionat(0.3), intersectionat(0.4))
        @test intervalintersection(a, b) == intervalintersection(b, a) isa EmptyInterval
        # start == end
        a = Interval(intersectionat(0.1), intersectionat(0.2))
        b = Interval(intersectionat(0.2), intersectionat(0.3))
        @test intervalintersection(a, b) == intervalintersection(b, a) isa EmptyInterval
        ## interval/du
        # encompass one
        a = Interval(intersectionat(0.0), intersectionat(0.3))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.2)), Interval(intersectionat(0.5), intersectionat(0.6)))
        @test intervalintersection(a, b) == intervalintersection(b, a) == b[1]
        # encompass two
        a = Interval(intersectionat(0.0), intersectionat(0.8))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.2)), Interval(intersectionat(0.5), intersectionat(0.6)))
        @test intervalintersection(a, b) == intervalintersection(b, a) == b
        # overlap one
        a = Interval(intersectionat(0.0), intersectionat(0.2))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.3)), Interval(intersectionat(0.5), intersectionat(0.6)))
        @test intervalintersection(a, b) == intervalintersection(b, a) == Interval(intersectionat(0.1), intersectionat(0.2))
        # overlap two
        a = Interval(intersectionat(0.2), intersectionat(0.5))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.3)), Interval(intersectionat(0.4), intersectionat(0.6)))
        res = intervalintersection(a, b)
        @test res == intervalintersection(b, a) && res[1] == Interval(intersectionat(0.2), intersectionat(0.3)) && res[2] == Interval(intersectionat(0.4), intersectionat(0.5))
        # no overlap
        a = Interval(intersectionat(0.1), intersectionat(0.2))
        b = DisjointUnion(Interval(intersectionat(0.3), intersectionat(0.4)), Interval(intersectionat(0.5), intersectionat(0.6)))
        @test intervalintersection(a, b) == intervalintersection(b, a) isa EmptyInterval
        ## du/du
        # no overlap
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.1)), Interval(intersectionat(0.4), intersectionat(0.5)))
        b = DisjointUnion(Interval(intersectionat(0.2), intersectionat(0.3)), Interval(intersectionat(0.6), intersectionat(0.7)))
        @test intervalintersection(a, b) == intervalintersection(b, a) isa EmptyInterval
        # one in a overlaps one in b
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.2)), Interval(intersectionat(0.4), intersectionat(0.5)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.3)), Interval(intersectionat(0.6), intersectionat(0.7)))
        @test intervalintersection(a, b) == intervalintersection(b, a) == Interval(intersectionat(0.1), intersectionat(0.2))
        # one in a encompasses one in b
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.3)), Interval(intersectionat(0.4), intersectionat(0.5)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.2)), Interval(intersectionat(0.6), intersectionat(0.7)))
        @test intervalintersection(a, b) == intervalintersection(b, a) == b[1]
        # one in a overlaps two in b
        a = DisjointUnion(Interval(intersectionat(0.2), intersectionat(0.5)), Interval(intersectionat(0.7), intersectionat(0.8)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.3)), Interval(intersectionat(0.4), intersectionat(0.6)))
        res = intervalintersection(a, b)
        @test res == intervalintersection(b, a) && res[1] == Interval(intersectionat(0.2), intersectionat(0.3)) && res[2] == Interval(intersectionat(0.4), intersectionat(0.5))
        # one in a encompasses two in b
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.5)), Interval(intersectionat(0.7), intersectionat(0.8)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.2)), Interval(intersectionat(0.3), intersectionat(0.4)))
        @test intervalintersection(a, b) == intervalintersection(b, a) == b
        # each in a overlap one in b
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.2)), Interval(intersectionat(0.4), intersectionat(0.6)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.3)), Interval(intersectionat(0.5), intersectionat(0.7)))
        res = intervalintersection(a, b)
        @test res == intervalintersection(b, a) && res[1] == Interval(intersectionat(0.1), intersectionat(0.2)) && res[2] == Interval(intersectionat(0.5), intersectionat(0.6))
        # each in a encompass one in b
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.3)), Interval(intersectionat(0.4), intersectionat(0.7)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.2)), Interval(intersectionat(0.5), intersectionat(0.6)))
        @test intervalintersection(a, b) == intervalintersection(b, a) == b
        ## empties
        intvl = positivehalfspace(pt1)
        du = intervalcomplement(Interval(pt1, pt2))
        @test intervalintersection(EmptyInterval(), EmptyInterval()) isa EmptyInterval
        @test intervalintersection(EmptyInterval(), intvl) isa EmptyInterval
        @test intervalintersection(intvl, EmptyInterval()) isa EmptyInterval
        @test intervalintersection(EmptyInterval(), du) isa EmptyInterval
        @test intervalintersection(du, EmptyInterval()) isa EmptyInterval

        ### interval union
        ## interval/interval
        # one in another
        a = Interval(intersectionat(0.1), intersectionat(0.2))
        b = Interval(intersectionat(0.0), intersectionat(0.3))
        @test intervalunion(a, b) == intervalunion(b, a) == b
        # overlap
        a = Interval(intersectionat(0.1), intersectionat(0.3))
        b = Interval(intersectionat(0.2), intersectionat(0.4))
        @test intervalunion(a, b) == intervalunion(b, a) == Interval(intersectionat(0.1), intersectionat(0.4))
        # no overlap
        a = Interval(intersectionat(0.1), intersectionat(0.2))
        b = Interval(intersectionat(0.3), intersectionat(0.4))
        res = intervalunion(a, b)
        @test res == intervalunion(b, a) && res[1] == a && res[2] == b
        # start == end
        a = Interval(intersectionat(0.1), intersectionat(0.2))
        b = Interval(intersectionat(0.2), intersectionat(0.3))
        @test intervalunion(a, b) == intervalunion(b, a) == Interval(intersectionat(0.1), intersectionat(0.3))
        ## interval/du
        # encompass one
        a = Interval(intersectionat(0.0), intersectionat(0.3))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.2)), Interval(intersectionat(0.5), intersectionat(0.6)))
        res = intervalunion(a, b)
        @test res == intervalunion(b, a) && res[1] == a && res[2] == b[2]
        # encompass two
        a = Interval(intersectionat(0.0), intersectionat(0.8))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.2)), Interval(intersectionat(0.5), intersectionat(0.6)))
        @test intervalunion(a, b) == intervalunion(b, a) == a
        # overlap one
        a = Interval(intersectionat(0.0), intersectionat(0.2))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.3)), Interval(intersectionat(0.5), intersectionat(0.6)))
        res = intervalunion(a, b)
        @test res == intervalunion(b, a) && res[1] == Interval(intersectionat(0.0), intersectionat(0.3)) && res[2] == b[2]
        # overlap two
        a = Interval(intersectionat(0.2), intersectionat(0.5))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.3)), Interval(intersectionat(0.4), intersectionat(0.6)))
        @test intervalunion(a, b) == intervalunion(b, a) == Interval(intersectionat(0.1), intersectionat(0.6))
        # no overlap
        a = Interval(intersectionat(0.1), intersectionat(0.2))
        b = DisjointUnion(Interval(intersectionat(0.3), intersectionat(0.4)), Interval(intersectionat(0.5), intersectionat(0.6)))
        res = intervalunion(a, b)
        @test res == intervalunion(b, a) && res[1] == a && res[2] == b[1] && res[3] == b[2]
        ## du/du
        # no overlap
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.1)), Interval(intersectionat(0.4), intersectionat(0.5)))
        b = DisjointUnion(Interval(intersectionat(0.2), intersectionat(0.3)), Interval(intersectionat(0.6), intersectionat(0.7)))
        res = intervalunion(a, b)
        @test res == intervalunion(b, a) && res[1] == a[1] && res[2] == b[1] && res[3] == a[2] && res[4] == b[2]
        # one in a overlaps one in b
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.2)), Interval(intersectionat(0.4), intersectionat(0.5)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.3)), Interval(intersectionat(0.6), intersectionat(0.7)))
        res = intervalunion(a, b)
        @test res == intervalunion(b, a) && res[1] == Interval(intersectionat(0.0), intersectionat(0.3)) && res[2] == a[2] && res[3] == b[2]
        # one in a encompasses one in b
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.3)), Interval(intersectionat(0.4), intersectionat(0.5)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.2)), Interval(intersectionat(0.6), intersectionat(0.7)))
        res = intervalunion(a, b)
        @test res == intervalunion(b, a) && res[1] == a[1] && res[2] == a[2] && res[3] == b[2]
        # one in a overlaps two in b
        a = DisjointUnion(Interval(intersectionat(0.2), intersectionat(0.5)), Interval(intersectionat(0.7), intersectionat(0.8)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.3)), Interval(intersectionat(0.4), intersectionat(0.6)))
        res = intervalunion(a, b)
        @test res == intervalunion(b, a) && res[1] == Interval(intersectionat(0.1), intersectionat(0.6)) && res[2] == a[2]
        # one in a encompasses two in b
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.5)), Interval(intersectionat(0.7), intersectionat(0.8)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.2)), Interval(intersectionat(0.3), intersectionat(0.4)))
        @test intervalunion(a, b) == intervalunion(b, a) == a
        # each in a overlap one in b
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.2)), Interval(intersectionat(0.4), intersectionat(0.6)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.3)), Interval(intersectionat(0.5), intersectionat(0.7)))
        res = intervalunion(a, b)
        @test res == intervalunion(b, a) && res[1] == Interval(intersectionat(0.0), intersectionat(0.3)) && res[2] == Interval(intersectionat(0.4), intersectionat(0.7))
        # each in a encompass one in b
        a = DisjointUnion(Interval(intersectionat(0.0), intersectionat(0.3)), Interval(intersectionat(0.4), intersectionat(0.7)))
        b = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.2)), Interval(intersectionat(0.5), intersectionat(0.6)))
        @test intervalunion(a, b) == intervalunion(b, a) == a
        ## empties
        @test intervalunion(EmptyInterval(), EmptyInterval()) isa EmptyInterval
        @test intervalunion(EmptyInterval(), intvl) == intvl
        @test intervalunion(intvl, EmptyInterval()) == intvl
        @test intervalunion(EmptyInterval(), du) == du
        @test intervalunion(du, EmptyInterval()) == du

        # interval complement
        int = intervalcomplement(EmptyInterval())
        @test lower(int) isa RayOrigin && upper(int) isa Infinity

        int = intervalcomplement(rayorigininterval(Infinity()))
        @test int isa EmptyInterval

        int = intervalcomplement(rayorigininterval(pt1))
        @test lower(int) == pt1 && upper(int) isa Infinity

        int = intervalcomplement(positivehalfspace(pt1))
        @test lower(int) isa RayOrigin && upper(int) == pt1

        int = intervalcomplement(Interval(pt1, pt2))
        @test lower(int[1]) isa RayOrigin && upper(int[1]) == pt1 && lower(int[2]) == pt2 && upper(int[2]) isa Infinity

        a = DisjointUnion(Interval(intersectionat(0.1), intersectionat(0.3)), Interval(intersectionat(0.4), intersectionat(0.7)))
        res = intervalcomplement(a)
        @test res[1] == rayorigininterval(intersectionat(0.1)) && res[2] == Interval(intersectionat(0.3), intersectionat(0.4)) && res[3] == positivehalfspace(intersectionat(0.7))

        a = DisjointUnion(rayorigininterval(intersectionat(0.2)), Interval(intersectionat(0.4), intersectionat(0.7)))
        res = intervalcomplement(a)
        @test res[1] == Interval(intersectionat(0.2), intersectionat(0.4)) && res[2] == positivehalfspace(intersectionat(0.7))
    end # testset interval
end # testset General


using Test
using LegendrePolynomials
using OffsetArrays
using HyperDualNumbers
using QuadGK
using Symbolics
using LinearAlgebra

tohyper(x) = Hyper(x, one(x), one(x), zero(x))

@testset "LegendrePolynomialIterator" begin
    iter = LegendrePolynomials.LegendrePolynomialIterator(0)
    @test Base.IteratorSize(iter) === Base.IsInfinite()
    @test eltype(iter) == Float64
    iter = LegendrePolynomials.LegendrePolynomialIterator(0, 1)
    @test Base.IteratorSize(iter) === Base.IsInfinite()
    iter = LegendrePolynomials.LegendrePolynomialIterator(big(0))
    @test eltype(iter) == BigFloat
end

@testset "Pl and collectPl" begin
	x = 0.5
    lmax = 5
    lmin = max(0, lmax - 1)

    @testset "norm" begin
        @testset "standard norm" begin
            P = @inferred collectPl(x, lmax = lmax)
            T = Union{OffsetVector{Float64, Vector{Float64}}, OffsetVector{BigFloat, Vector{BigFloat}}}
            P3 = @inferred collectdnPl(x, lmax = lmax, n = 0)
            @test P[0] ≈ P3[0] ≈ 1
            @test P[1] ≈ P3[1] ≈ x
            @test P[2] ≈ P3[2] ≈ (3x^2 - 1)/2
            @test P[3] ≈ P3[3] ≈ (5x^3 - 3x)/2
            @test P[4] ≈ P3[4] ≈ (35x^4 - 30x^2 + 3)/8
            @test P[5] ≈ P3[5] ≈ (63x^5 - 70x^3 + 15x)/8
        end
    end

    @testset "x = 0" begin
        for l = 1:2:101
            P1 = @inferred Pl(0, l)
            P2 = @inferred Union{BigFloat, Float64} Plm(0, l, 0)
            P3 = @inferred dnPl(0, l, 0)
            @test P1 == P2 == P3 == 0
            P1 = @inferred Pl(0, l, norm = Val(:normalized))
            P2 = @inferred Plm(0, l, 0, norm = Val(:normalized))
            @test P1 == P2 == 0
        end
    end

    @testset "x = ±1" begin
        lmax = 10

        P = collectPl(1, lmax = lmax)
        @test all(==(1), P)
        P2 = collectPlm(1, lmax = lmax, m = 0)
        @test all(≈(1), P2)
        P3 = collectdnPl(1, lmax = lmax, n = 0)
        @test all(==(1), P3)

        P = collectPl(-1, lmax = lmax)
        P2 = collectPlm(-1, lmax = lmax, m = 0)
        P3 = collectdnPl(-1, lmax = lmax, n = 0)
        for l in axes(P, 1)
            @test P[l] ≈ P2[l] ≈ P3[l] ≈ (-1)^l
        end
    end

    @testset "large degree" begin
        l = 200
        x = 0.5
        expval = -big"50516977220606216108218169174406199263421124121643988159364881659548
                    89526732539052701378087401221240954262583383510627"/
                    big"3227812347608635737069898965003764842912132241036529391038324195675
                    80952752105149328705669160017228929487896496593436672"

        @test Pl(x, l) ≈ expval rtol=1e-14
        @test Pl(big(x), big(l)) ≈ expval rtol=1e-75
    end

    @testset "parity" begin
        for x in range(0, 1, length=10), l in 0:4
            @test Pl(-x, l) ≈ (-1)^l * Pl(x, l) atol=1e-12 rtol=1e-8
        end
    end

    @test_throws DomainError collectPl(-2, lmax = lmax)
    @test_throws ArgumentError collectPl(0, lmax = -1)

    @test_throws ArgumentError collectPl(0, lmax=1, norm = Val(:undefined))
end

@testset "Plm and collectPlm" begin
    x = 0.5
    lmax = 5
    lmin = max(0, lmax - 1)

    @testset "double factorial" begin
        @test Plm(0.5, 2, 2) ≈ 9/4
        @test Plm(0.5, 3, 3) ≈ -45/8*sqrt(3)
        @test Plm(0.5, 3, 3) ≈ -45/8*sqrt(3)
        @test Plm(0.5, 10, 0) ≈ -49343/262144
        @test Plm(0.5, 10, 6) ≈ -674999325/8192
        @test Plm(0.5, 10, 10) ≈ 159099165225/1024
    end
    @testset "norm" begin
        @testset "standard norm" begin
            P = @inferred collectPl(x, lmax = lmax)
            T = Union{OffsetVector{Float64, Vector{Float64}}, OffsetVector{BigFloat, Vector{BigFloat}}}
            P2 = @inferred T collectPlm(x, lmax = lmax, m = 0)
            @test P ≈ P2

            Q = @inferred collectPl(x, lmax = lmax, lmin = lmin, norm = Val(:standard))
            Q2 = @inferred T collectPlm(x, lmax = lmax, lmin = lmin, m = 0, norm = Val(:standard))
            @test Q ≈ Q2
            @test @views Q[lmin:lmax] == P[lmin:lmax]
        end

        @testset "normalized" begin
            P = @inferred collectPl(x, lmax = lmax, norm = Val(:normalized))
            Q = @inferred collectPlm(x, lmax = lmax, m = 0, norm = Val(:normalized))
            @test P ≈ Q
            @test P[0] ≈ √(1/2)

            P2 = @inferred collectPl(x, lmax = lmax, lmin = lmin, norm = Val(:normalized))
            Q2 = @inferred collectPlm(x, lmax = lmax, lmin = lmin, m = 0, norm = Val(:normalized))
            @test P2 ≈ Q2
            @test @views P2[lmin:lmax] == P[lmin:lmax]

            for m in 1:3
                Q = @inferred collectPlm(x, lmax = lmax, m = m, norm = Val(:normalized))
                @testset for l in m:lmax
                    @test Q[l] ≈ √((2l+1)/2*(factorial(l-m)/factorial(l+m))) * Plm(x, l, m)
                end
            end
        end

        @testset "schmidtquasi" begin
            P = @inferred collectPl(x, lmax = lmax, norm = Val(:schmidtquasi))
            Q = @inferred collectPlm(x, lmax = lmax, m = 0, norm = Val(:schmidtquasi))
            @test P ≈ Q

            for m in 1:3
                Q = @inferred collectPlm(x, lmax = lmax, m = m, norm = Val(:schmidtquasi))
                @testset for l in m:lmax
                    @test Q[l] ≈ √(2*(factorial(l-m)/factorial(l+m))) * Plm(x, l, m)
                end
            end
        end

        @testset "schmidt" begin
            P = @inferred collectPl(x, lmax = lmax, norm = Val(:schmidt))
            Q = @inferred collectPlm(x, lmax = lmax, m = 0, norm = Val(:schmidt))
            @test P ≈ Q

            for m in 1:3
                Q = @inferred collectPlm(x, lmax = lmax, m = m, norm = Val(:schmidt))
                @testset for l in m:lmax
                    @test Q[l] ≈ √(2*(2l+1)*(factorial(l-m)/factorial(l+m))) * Plm(x, l, m)
                end
            end
        end
    end
    @testset "non-standard integers" begin
        l = 2
        m = 1
        x = 0.5
        p_Int = Plm(x, l, m)
        p_Int8 = Plm(x, Int8(l), Int8(m))
        @test p_Int == p_Int8
        p_BigInt = Plm(big(x), big(l), big(m))
        @test p_BigInt ≈ p_Int rtol=1e-12
    end
    @testset "Condon-Shortley phase" begin
        p1 = Plm(0.5, 2, 1)
        p2 = Plm(0.5, 2, 1, csphase = false)
        @test p1 ≈ -p2
        p1 = Plm(0.5, 2, 2)
        p2 = Plm(0.5, 2, 2, csphase = false)
        @test p1 ≈ p2
        for norm in [:standard, :normalized, :schmidtquasi]
            @test all(Plm(0.5, 3, m, csphase = false, norm = Val(norm)) ≈
                    (-1)^m * Plm(0.5, 3, m, norm = Val(norm)) for m in -3:3)
        end
    end
    @testset "avoid overflow" begin
        l = 200
        m = 200
        x = 0.5
        # copied from Mathematica
        expval = big"2604065338240773053120460757061528950680509925586185542679571264679180
           1169571890502639513862292536574563617492830604806347049485953379217258
           5643579361782965615917473831726956215382240884618736130956683834034044
           2039232933380686854433175974549155182434563952079326518988454706912117
           0695345051735139971477695522848840659242654598631485913523486512584654
           3918383779732853064471286369286966147111339555668732856505146906124873
           14193734385778184852110786895451610689633525907993316650390625"/
           big"1606938044258990275541962092341162602522202993782792835301376"
        @test Plm(x, l, m) ≈ expval rtol=1e-12
        @test Plm(big(x), big(l), m) ≈ expval rtol=1e-70
    end
    @testset "negative order" begin
        x = 0.5
        pm1 = collectPlm(x, lmax = 2, m = -1)
        @test Plm(x, 1, -1) ≈ pm1[1] ≈ √3/4
        @test Plm(x, 2, -1) ≈ pm1[2] ≈ √3/8
        pm2 = collectPlm(x, lmax = 3, m = -2)
        @test Plm(x, 2, -2) ≈ pm2[2] ≈ 3/32
        @test Plm(x, 3, -2) ≈ pm2[3] ≈ 3/64
    end
    @testset "parity" begin
        for x in range(0, 1, length=10), l in 0:4, m in -l:l
            @test Plm(-x, l, m) ≈ (-1)^(l+m) * Plm(x, l, m) atol=1e-12 rtol=1e-8
        end
    end
    @test_throws ArgumentError Plm(0.5, 3, 10)
    @test_throws ArgumentError Plm(0.5, -1, 3)
    @test_throws ArgumentError Plm(0.5, 1, -3)
    @test_throws DomainError Plm(2.5, 1, 3)

    @test_throws ArgumentError collectPlm(0, lmax=1, m=1, norm = Val(:undefined))
end

@testset "dnPl" begin
    @test dnPl(0.5, 3, 10) == 0
    @test dnPl(0.5, 3, 10, zeros(100)) == 0
    @testset "double factorial" begin
        @test dnPl(0.5, 2, 2) == 3
        @test dnPl(0.5, 3, 3) == 15
        @test dnPl(0.5, 3, 3, zeros(1)) == 15
    end
    @test_throws ArgumentError dnPl(0.5, 10, 3, zeros(1))
    @test_throws ArgumentError dnPl(0.5, -1, 3)
    @test_throws ArgumentError dnPl(0.5, 1, -3)
    @test_throws DomainError dnPl(2.5, 1, 3)
end

@testset "collectPl!" begin
    v = zeros(10);
    x = 0.6

    collectPl!(v, x)
    for l in 0:length(v) - 1
        @test v[firstindex(v) + l] == Pl(x, l)
    end

    v .= 0
    lmax = 6
    collectPl!(v, x, lmax = lmax)
    for l in 0:lmax
        @test v[firstindex(v) + l] == Pl(x, l)
    end

    @test_throws DomainError collectPl!(v, -2, lmax = lmax)
    @test_throws ArgumentError collectPl!(v, 0, lmax = -1)
end

@testset "collectPlm!" begin
    v = zeros(10);
    @test_throws DomainError collectPlm!(v, -2, lmax = 3, m = 2)
    @test_throws ArgumentError collectPlm!(v, 0, lmax = -1, m = 3)
    @test_throws ArgumentError collectPlm!(v, 0.5, lmax = 1, m = -3)
end

@testset "collectPlm" begin
    @test_throws DomainError collectPlm(-2, lmax = 3, m = 2)
    @test_throws ArgumentError collectPlm(0, lmax = -1, m = 3)
    @test_throws ArgumentError collectPlm(0.5, lmax = 1, m = -3)
end

@testset "collectdnPl!" begin
    v = zeros(10);
    @test_throws DomainError collectdnPl!(v, -2, lmax = 3, n = 2)
    @test_throws ArgumentError collectdnPl!(v, 0, lmax = -1, n = 3)
    @test_throws ArgumentError collectdnPl!(v, 0.5, lmax = 1, n = -3)
    @test_throws ArgumentError collectdnPl!(zeros(1), 0, lmax = 3, n = 3)
end

@testset "collectdnPl" begin
    @test_throws DomainError collectdnPl(-2, lmax = 3, n = 2)
    @test_throws ArgumentError collectdnPl(0, lmax = -1, n = 3)
    @test_throws ArgumentError collectdnPl(0.5, lmax = 1, n = -3)
end

@testset "dPl/dx" begin
    x = 2rand() - 1
    xh = tohyper(x)
    lmax = 5
    P = eps1.(collectPl(xh, lmax = lmax))
    P2 = collectPlm(x, lmax = lmax, m = 1) / (- sqrt(1 - x^2))
    P3 = collectdnPl(x, lmax = lmax, n = 1)
    @test P[0] == P3[0] == 0
    @test P[1] ≈ P2[1] ≈ P3[1] ≈ 1
    @test P[2] ≈ P2[2] ≈ P3[2] ≈ 3x
    @test P[3] ≈ P2[3] ≈ P3[3] ≈ (-3 + 15x^2)/2
    @test P[4] ≈ P2[4] ≈ P3[4] ≈ 1/8*(-60x + 140x^3)
    @test P[5] ≈ P2[5] ≈ P3[5] ≈ 1/8*(15 - 210x^2 + 315x^4)

    @testset "x = ±1" begin
        lmax = 10
        x = 1
        P = collectPl(tohyper(x), lmax = lmax)
        dP1h = eps1.(P)
        dP1 = collectdnPl(x, lmax = lmax, n = 1)

        for l in 1:lmax
            @test dP1h[l] == dP1[l] == l*(l+1)/2
        end

        x = -1
        P = collectPl(tohyper(x), lmax = lmax)
        dPm1h = eps1.(P)
        dPm1 = collectdnPl(x, lmax = lmax, n = 1)

        for l in 1:lmax
            @test dPm1h[l] == dPm1[l] == (-1)^(l+1) * l*(l+1)/2
        end
    end
end

@testset "d^2Pl/dx^2" begin
    x = 2rand() - 1
    xh = tohyper(x)
    lmax = 5
    P = eps1eps2.(collectPl(xh, lmax = lmax))
    P2 = collectPlm(x, lmax = lmax, m = 2) / (1 - x^2)
    P3 = collectdnPl(x, lmax = lmax, n = 2)

    @test P[0] == P3[0] == 0
    @test P[1] == P3[1] == 0
    @test P[2] ≈ P2[2] ≈ P3[2] ≈ 3
    @test P[3] ≈ P2[3] ≈ P3[3] ≈ 15x
    @test P[4] ≈ P2[4] ≈ P3[4] ≈ 1/8*(-60 + 420x^2)
    @test P[5] ≈ P2[5] ≈ P3[5] ≈ 1/8*(-420x + 1260x^3)
end

@testset "Askey-Gasper" begin
    x = 0.5
    for lmax=1:100:1_000
        @test sum(collectPl(x, lmax = lmax)) >= 0
        @test sum(collectPlm(x, lmax = lmax, m = 0)) >= 0
        @test sum(collectdnPl(x, lmax = lmax, n = 0)) >= 0
    end
end

@testset "norm" begin
    @testset "Pl" begin
        @testset "normalized" begin
            for l in 0:4
                f = x -> Pl(x, l, norm = Val(:normalized))^2
                I, E = quadgk(f, -1, 1, rtol=1e-2)
                @test I ≈ 1 rtol=1e-2 atol=E
            end
        end

        @testset "standard" begin
            for l in 0:4
                f = x -> Pl(x, l, norm = Val(:standard))^2
                I, E = quadgk(f, -1, 1, rtol=1e-2)
                @test I ≈ 2/(2l+1) rtol=1e-2 atol=E
            end
        end

        @testset "schmidtquasi" begin
            for l in 0:4
                f = x -> Pl(x, l, norm = Val(:schmidtquasi))^2
                I, E = quadgk(f, -1, 1, rtol=1e-2)
                @test I ≈ 2/(2l+1) rtol=1e-2 atol=E
            end
        end

        @testset "schmidt" begin
            for l in 0:4
                f = x -> Pl(x, l, norm = Val(:schmidt))^2
                I, E = quadgk(f, -1, 1, rtol=1e-2)
                @test I ≈ 2 rtol=1e-2 atol=E
            end
        end
    end
    @testset "Plm" begin
        @testset "normalized" begin
            @testset for m in -4:4, l in abs(m):4
                f = x -> Plm(x, l, m, norm = Val(:normalized))^2
                I, E = quadgk(f, -1, 1, rtol=1e-2)
                @test I ≈ 1 rtol=1e-2 atol=E
            end
        end

        @testset "schmidtquasi" begin
            @testset for m in -4:4, l in abs(m):4
                f = x -> Plm(x, l, m, norm = Val(:schmidtquasi))^2
                I, E = quadgk(f, -1, 1, rtol=1e-2)
                @test I ≈ 2*(2 - (m==0))/(2l + 1) rtol=1e-2 atol=E
            end
        end

        @testset "schmidt" begin
            @testset for m in -4:4, l in abs(m):4
                f = x -> Plm(x, l, m, norm = Val(:schmidt))^2
                I, E = quadgk(f, -1, 1, rtol=1e-2)
                @test I ≈ 2*(2 - (m==0)) rtol=1e-2 atol=E
            end
        end

        @testset "standard" begin
            for m in -4:4, l in abs(m):4
                f = x -> Plm(x, l, m, norm = Val(:standard))^2
                I, E = quadgk(f, -1, 1, rtol=1e-2)
                @test I ≈ (2/(2l+1)*factorial(l+m)/factorial(l-m)) rtol=1e-2 atol=E
            end
        end
    end
end

@testset "custom types" begin
    @testset "Symbolics" begin
        @variables x
        f_expr = build_function(Pl(x, 3), [x])
        myf = eval(f_expr)
        @test myf(0.4) ≈ Pl(0.4, 3)
        f_expr = build_function(Plm(x, 3, 2), [x])
        myf2 = eval(f_expr)
        @test myf2(0.4) ≈ Plm(0.4, 3, 2)
    end

    @testset "Matrix" begin
        A = reshape([0.1:0.1:0.4;], 2, 2)
        @test Pl(A, 1) == A
        @test Pl(A, 2) ≈ (3A^2 - one(A))/2
        @test Plm(A, 2, 1) ≈ -3A*√(I - A^2)

        D = Diagonal([1.0, 0.5])
        @test Pl(D, 1) == D
        @test Pl(D, 2) ≈ (3D^2 - one(D))/2
        @test Plm(D, 2, 1) ≈ -3D*√(I - D^2)
    end
end


##############################   Born-Mayer  ###################################
# mutable struct BornMayer <: EmpiricalPotential
#     A::Float64
#     ρ::Float64
# end

# function BornMayer()
#     #ToDO
#     return BornMayer(1.0, 1.0)
# end

# function get_trainable_params(bm::BornMayer)
#     return (A = bm.A, ρ = bm.ρ)
# end

# function get_nontrainable_params(lj::BornMayer)
#     p = Parameter{}(())
#     return p
# end

# ##############################   Energy  ###################################

# function potential_energy(r::Vector{<:Real}, p::BornMayer)
#     return p.A * exp(-norm(r) / p.ρ)
# end

# ##############################   Force   ###################################

# function force(r::Vector{<:Real}, p::BornMayer)
#     d = norm(r)
#     return p.A /p.ρ * exp(-d / p.ρ ) .* r ./ d
# end

# ##############################   Gradients  ###################################

# function grad_potential_energy(r::Vector{<:Real}, p::BornMayer)
#     d = norm(r)
#     return (dpdA = exp(-d / p.ρ),
#             dpdρ = p.A * d * exp(-d/p.ρ) / p.ρ^2 )
# end

# function grad_force(r::Vector{<:Real}, p::BornMayer)
#     d = norm(r)
#     return (dfdA = 1.0 /p.ρ * exp(-d / p.ρ ) .* r ./ d,
#             dfdρ = p.A / p.ρ^3 * exp(-d / p.ρ )*(d - p.ρ)  .* r ./ d)
# end

# function grad_virial(r::Vector{<:Real}, p::BornMayer)
#     df = grad_force(r, p)
#     dfdA = df[:dfdA]
#     dfdrho = df[:dfdρ]
#     return (dvdA = dfdA[1]*r[1] + dfdA[2]*r[2] + dfdA[3]*r[3],
#             dvdρ = dfdrho[1]*r[1] + dfdrho[2]*r[2] + dfdrho[3]*r[3])
# end


include("euler/euler.jl")
using .Primes: prime_numbers

function compute()::Int
    for prime ∈ reverse(prime_numbers(7_654_321))
        str_prime = string(prime)
        if sort(parse.(Int, ∪(str_prime))) == collect(1:length(str_prime))
            return prime
        end
    end
end


"""
Pre-computes a nonuniform fast Fourier transform of type `N`.

For best performance, choose the right number of threads by `FFTW.set_num_threads(4)`, for example.
"""
struct NUFFTPlan{N,T,FFT} <: Plan{T}
    U::Matrix{T}
    V::Matrix{T}
    p::FFT
    t::Vector{Int}
    temp::Matrix{T}
    temp2::Matrix{T}
    Ones::Vector{T}
end

"""
Pre-computes a nonuniform fast Fourier transform of type I.
"""
function plan_nufft1(ω::AbstractVector{T}, ϵ::T) where {T<:AbstractFloat}
    N = length(ω)
    ωdN = ω/N
    t = AssignClosestEquispacedFFTpoint(ωdN)
    γ = PerturbationParameter(ωdN, AssignClosestEquispacedGridpoint(ωdN))
    K = FindK(γ, ϵ)
    U = constructU(ωdN, K)
    V = constructV(range(zero(T), stop=N-1, length=N), K)
    p = plan_bfft!(V, 1)
    temp = zeros(Complex{T}, N, K)
    temp2 = zeros(Complex{T}, N, K)
    Ones = ones(Complex{T}, K)

    NUFFTPlan{1, eltype(U), typeof(p)}(U, V, p, t, temp, temp2, Ones)
end

"""
Pre-computes a nonuniform fast Fourier transform of type II.
"""
function plan_nufft2(x::AbstractVector{T}, ϵ::T) where T<:AbstractFloat
    N = length(x)
    t = AssignClosestEquispacedFFTpoint(x)
    γ = PerturbationParameter(x, AssignClosestEquispacedGridpoint(x))
    K = FindK(γ, ϵ)
    U = constructU(x, K)
    V = constructV(range(zero(T), stop=N-1, length=N), K)
    p = plan_fft!(U, 1)
    temp = zeros(Complex{T}, N, K)
    temp2 = zeros(Complex{T}, N, K)
    Ones = ones(Complex{T}, K)

    NUFFTPlan{2, eltype(U), typeof(p)}(U, V, p, t, temp, temp2, Ones)
end

"""
Pre-computes a nonuniform fast Fourier transform of type III.
"""
function plan_nufft3(x::AbstractVector{T}, ω::AbstractVector{T}, ϵ::T) where T<:AbstractFloat
    N = length(x)
    s = AssignClosestEquispacedGridpoint(x)
    t = AssignClosestEquispacedFFTpoint(x)
    γ = PerturbationParameter(x, s)
    K = FindK(γ, ϵ)
    u = constructU(x, K)
    v = constructV(ω, K)

    p = plan_nufft1(ω, ϵ)

    D1 = Diagonal(1 .- (s .- t .+ 1)./N)
    D2 = Diagonal((s .- t .+ 1)./N)
    D3 = Diagonal(exp.(-2 .* im .* T(π) .* ω ))
    U = hcat(D1*u, D2*u)
    V = hcat(v, D3*v)

    temp = zeros(Complex{T}, N, 2K)
    temp2 = zeros(Complex{T}, N, 2K)
    Ones = ones(Complex{T}, 2K)

    NUFFTPlan{3, eltype(U), typeof(p)}(U, V, p, t, temp, temp2, Ones)
end

function (*)(p::NUFFTPlan{N,T}, c::AbstractArray{V}) where {N,T,V}
    mul!(zeros(promote_type(T,V), size(c)), p, c)
end

function mul!(f::AbstractVector{T}, P::NUFFTPlan{1,T}, c::AbstractVector{T}) where {T}
    U, V, p, t, temp, temp2, Ones = P.U, P.V, P.p, P.t, P.temp, P.temp2, P.Ones

    broadcast!(*, temp, c, U)
    conj!(temp)
    fill!(temp2, zero(T))
    recombine_rows!(temp, t, temp2)
    p*temp2
    conj!(temp2)
    broadcast!(*, temp, V, temp2)
    mul!(f, temp, Ones)

    f
end

function mul!(F::Matrix{T}, P::NUFFTPlan{N,T}, C::Matrix{T}) where {N,T}
    for J = 1:size(F, 2)
        mul_col_J!(F, P, C, J)
    end
    F
end

function broadcast_col_J!(f, temp::Matrix, C::Matrix, U::Matrix, J::Int)
    N = size(C, 1)
    COLSHIFT = N*(J-1)
    @inbounds for j = 1:size(temp, 2)
        for i = 1:N
            temp[i,j] = f(C[i+COLSHIFT],U[i,j])
        end
    end
    temp
end

function mul_col_J!(F::Matrix{T}, P::NUFFTPlan{1,T}, C::Matrix{T}, J::Int) where {T}
    U, V, p, t, temp, temp2, Ones = P.U, P.V, P.p, P.t, P.temp, P.temp2, P.Ones

    broadcast_col_J!(*, temp, C, U, J)
    conj!(temp)
    fill!(temp2, zero(T))
    recombine_rows!(temp, t, temp2)
    p*temp2
    conj!(temp2)
    broadcast!(*, temp, V, temp2)
    COLSHIFT = size(C, 1)*(J-1)
    mul_for_col_J!(F, temp, Ones, 1+COLSHIFT, 1)

    F
end

function mul!(f::AbstractVector{T}, P::NUFFTPlan{2,T}, c::AbstractVector{T}) where {T}
    U, V, p, t, temp, temp2, Ones = P.U, P.V, P.p, P.t, P.temp, P.temp2, P.Ones

    broadcast!(*, temp, c, V)
    p*temp
    reindex_temp!(temp, t, temp2)
    broadcast!(*, temp, U, temp2)
    mul!(f, temp, Ones)

    f
end

function mul_col_J!(F::Matrix{T}, P::NUFFTPlan{2,T}, C::Matrix{T}, J::Int) where {T}
    U, V, p, t, temp, temp2, Ones = P.U, P.V, P.p, P.t, P.temp, P.temp2, P.Ones

    broadcast_col_J!(*, temp, C, V, J)
    p*temp
    reindex_temp!(temp, t, temp2)
    broadcast!(*, temp, U, temp2)
    COLSHIFT = size(C, 1)*(J-1)
    mul_for_col_J!(F, temp, Ones, 1+COLSHIFT, 1)

    F
end

function mul!(f::AbstractVector{T}, P::NUFFTPlan{3,T}, c::AbstractVector{T}) where {T}
    U, V, p, t, temp, temp2, Ones = P.U, P.V, P.p, P.t, P.temp, P.temp2, P.Ones

    broadcast!(*, temp2, c, V)
    mul!(temp, p, temp2)
    reindex_temp!(temp, t, temp2)
    broadcast!(*, temp, U, temp2)
    mul!(f, temp, Ones)

    f
end

function mul_col_J!(F::Matrix{T}, P::NUFFTPlan{3,T}, C::Matrix{T}, J::Int) where {T}
    U, V, p, t, temp, temp2, Ones = P.U, P.V, P.p, P.t, P.temp, P.temp2, P.Ones

    broadcast_col_J!(*, temp2, C, V, J)
    mul!(temp, p, temp2)
    reindex_temp!(temp, t, temp2)
    broadcast!(*, temp, U, temp2)
    COLSHIFT = size(C, 1)*(J-1)
    mul_for_col_J!(F, temp, Ones, 1+COLSHIFT, 1)

    F
end

mul_for_col_J!(y::AbstractVecOrMat{T}, A::AbstractMatrix{T}, x::AbstractVecOrMat{T}, istart::Int, jstart::Int) where T =
    mul_for_col_J!(y, A, x, istart, jstart, 1, 1)

function mul_for_col_J!(y::AbstractVecOrMat{T}, A::AbstractMatrix{T}, x::AbstractVecOrMat{T}, istart::Int, jstart::Int, INCX::Int, INCY::Int) where T
    m, n = size(A)
    ishift, jshift = istart-INCY, jstart-INCX
    @inbounds for i = 1:m
        y[ishift+i*INCY] = zero(T)
    end
    @inbounds for j = 1:n
        xj = x[jshift+j*INCX]
        for i = 1:m
            y[ishift+i*INCY] += A[i,j]*xj
        end
    end

    y
end

function reindex_temp!(temp::Matrix{T}, t::Vector{Int}, temp2::Matrix{T}) where {T}
    @inbounds for j = 1:size(temp, 2)
        for i = 1:size(temp, 1)
            temp2[i, j] = temp[t[i], j]
        end
    end
    temp2
end

function recombine_rows!(temp::Matrix{T}, t::Vector{Int}, temp2::Matrix{T}) where {T}
    @inbounds for j = 1:size(temp, 2)
        for i = 1:size(temp, 1)
            temp2[t[i], j] += temp[i, j]
        end
    end
    temp2
end

"""
Computes a nonuniform fast Fourier transform of type I:

```math
f_j = \\sum_{k=0}^{N-1} c_k e^{-2\\pi{\\rm i} \\frac{j}{N} \\omega_k},\\quad{\\rm for}\\quad 0 \\le j \\le N-1.
```
"""
nufft1(c::AbstractVector, ω::AbstractVector{T}, ϵ::T) where {T<:AbstractFloat} = plan_nufft1(ω, ϵ)*c

"""
Computes a nonuniform fast Fourier transform of type II:

```math
f_j = \\sum_{k=0}^{N-1} c_k e^{-2\\pi{\\rm i} x_j k},\\quad{\\rm for}\\quad 0 \\le j \\le N-1.
```
"""
nufft2(c::AbstractVector, x::AbstractVector{T}, ϵ::T) where {T<:AbstractFloat}  = plan_nufft2(x, ϵ)*c

"""
Computes a nonuniform fast Fourier transform of type III:

```math
f_j = \\sum_{k=0}^{N-1} c_k e^{-2\\pi{\\rm i} x_j \\omega_k},\\quad{\\rm for}\\quad 0 \\le j \\le N-1.
```
"""
nufft3(c::AbstractVector, x::AbstractVector{T}, ω::AbstractVector{T}, ϵ::T) where {T<:AbstractFloat} = plan_nufft3(x, ω, ϵ)*c

const nufft = nufft3
const plan_nufft = plan_nufft3

"""
Pre-computes a 2D nonuniform fast Fourier transform.

For best performance, choose the right number of threads by `FFTW.set_num_threads(4)`, for example.
"""
struct NUFFT2DPlan{T,P1,P2} <: Plan{T}
    p1::P1
    p2::P2
    temp::Vector{T}
end

"""
Pre-computes a 2D nonuniform fast Fourier transform of type I-I.
"""
function plan_nufft1(ω::AbstractVector{T}, π::AbstractVector{T}, ϵ::T) where T<:AbstractFloat
    p1 = plan_nufft1(ω, ϵ)
    p2 = plan_nufft1(π, ϵ)
    temp = zeros(Complex{T}, length(π))

    NUFFT2DPlan(p1, p2, temp)
end

"""
Pre-computes a 2D nonuniform fast Fourier transform of type II-II.
"""
function plan_nufft2(x::AbstractVector{T}, y::AbstractVector{T}, ϵ::T) where T<:AbstractFloat
    p1 = plan_nufft2(x, ϵ)
    p2 = plan_nufft2(y, ϵ)
    temp = zeros(Complex{T}, length(y))

    NUFFT2DPlan(p1, p2, temp)
end

function (*)(p::NUFFT2DPlan{T}, C::Matrix{V}) where {T,V}
    mul!(zeros(promote_type(T,V), size(C)), p, C)
end

function mul!(F::Matrix{T}, P::NUFFT2DPlan{T}, C::Matrix{T}) where {T}
    p1, p2, temp = P.p1, P.p2, P.temp

    # Apply 1D x-plan to all columns
    mul!(F, p1, C)

    # Apply 1D y-plan to all rows
    for i = 1:size(C, 1)
        @inbounds for j = 1:size(F, 2) temp[j] = F[i,j] end
        mul!(temp, p2, temp)
        @inbounds for j = 1:size(F, 2) F[i,j] = temp[j] end
    end

    F
end

"""
Computes a 2D nonuniform fast Fourier transform of type I-I:

```math
F_{i,j} = \\sum_{k=0}^{M-1}\\sum_{\\ell=0}^{N-1} C_{k,\\ell} e^{-2\\pi{\\rm i} (\\frac{i}{M} \\omega_k + \\frac{j}{N} \\pi_{\\ell})},\\quad{\\rm for}\\quad 0 \\le i \\le M-1,\\quad 0 \\le j \\le N-1.
```
"""
nufft1(C::Matrix, ω::AbstractVector{T}, π::AbstractVector{T}, ϵ::T) where {T<:AbstractFloat} = plan_nufft1(ω, π, ϵ)*C

"""
Computes a 2D nonuniform fast Fourier transform of type II-II:

```math
F_{i,j} = \\sum_{k=0}^{M-1}\\sum_{\\ell=0}^{N-1} C_{k,\\ell} e^{-2\\pi{\\rm i} (x_i k + y_j \\ell)},\\quad{\\rm for}\\quad 0 \\le i \\le M-1,\\quad 0 \\le j \\le N-1.
```
"""
nufft2(C::Matrix, x::AbstractVector{T}, y::AbstractVector{T}, ϵ::T) where {T<:AbstractFloat} = plan_nufft2(x, y, ϵ)*C

FindK(γ::T, ϵ::T) where {T<:AbstractFloat} = γ ≤ ϵ ? 1 : Int(ceil(5*γ*exp(lambertw(log(10/ϵ)/γ/7))))

(AssignClosestEquispacedGridpoint(x::AbstractVector{T})::AbstractVector{T}) where {T<:AbstractFloat} = round.([Int], size(x, 1)*x)
(AssignClosestEquispacedFFTpoint(x::AbstractVector{T})::Array{Int,1}) where {T<:AbstractFloat} = mod.(round.([Int], size(x, 1)*x), size(x, 1)) .+ 1
(PerturbationParameter(x::AbstractVector{T}, s_vec::AbstractVector{T})::AbstractFloat) where {T<:AbstractFloat} = norm(size(x, 1)*x - s_vec, Inf)

function constructU(x::AbstractVector{T}, K::Int) where {T<:AbstractFloat}
    # Construct a low rank approximation, using Chebyshev expansions
    # for AK = exp(-2*pi*1im*(x[j]-j/N)*k):
    N = length(x)
    s = AssignClosestEquispacedGridpoint(x)
    er = N*x - s
    γ = norm(er, Inf)
    Diagonal(exp.(-im*(π*er)))*ChebyshevP(K-1, er/γ)*Bessel_coeffs(K, γ)
end

function constructV(ω::AbstractVector{T}, K::Int) where {T<:AbstractFloat}
    complex(ChebyshevP(K-1, ω.*(two(T)/length(ω)) .- 1))
end

function Bessel_coeffs(K::Int, γ::T) where {T<:AbstractFloat}
    # Calculate the Chebyshev coefficients of exp(-2*pi*1im*x*y) on [-gam,gam]x[0,1]
    cfs = zeros(Complex{T}, K, K)
    arg = -γ*π/two(T)
    for p = 0:K-1
     	for q = mod(p,2):2:K-1
    		cfs[p+1,q+1] = 4*(1im)^q*besselj((p+q)/2,arg).*besselj((q-p)/2,arg)
    	end
    end
    cfs[1,:] = cfs[1,:]/two(T)
    cfs[:,1] = cfs[:,1]/two(T)
    return cfs
end

function ChebyshevP(n::Int, x::AbstractVector{T}) where T<:AbstractFloat
    # Evaluate Chebyshev polynomials of degree 0,...,n at x:
    N = size(x, 1)
    Tcheb = Matrix{T}(undef, N, n+1)

    # T_0(x) = 1.0
    One = convert(eltype(x),1.0)
    @inbounds for j = 1:N
        Tcheb[j, 1] = One
    end
    # T_1(x) = x
    if ( n > 0 )
        @inbounds for j = 1:N
            Tcheb[j, 2] = x[j]
        end
    end
    # 3-term recurrence relation:
    twoX = 2x
    @inbounds for k = 2:n
        @simd for j = 1:N
            Tcheb[j, k+1] = twoX[j]*Tcheb[j, k] - Tcheb[j, k-1]
        end
    end
    return Tcheb
end


@doc doc"""
    LogCholeskyMetric <: Metric

The Log-Cholesky metric imposes a metric based on the Cholesky decomposition as
introduced by [^Lin2019].

[^Lin2019]:
    > Lin, Zenhua: "Riemannian Geometry of Symmetric Positive Definite Matrices via
    > Cholesky Decomposition", arXiv: [1908.09326](https://arxiv.org/abs/1908.09326).
"""
struct LogCholeskyMetric <: RiemannianMetric end

cholesky_to_spd(l, w) = (l * l', w * l' + l * w')

tangent_cholesky_to_tangent_spd!(l, w) = (w .= w * l' + l * w')

spd_to_cholesky(x, v) = spd_to_cholesky(x, cholesky(x).L, v)

function spd_to_cholesky(x, l, v)
    w = inv(l) * v * inv(transpose(l))
    # strictly lower triangular plus half diagonal
    return (l, l * (LowerTriangular(w) - Diagonal(w) / 2))
end

@doc doc"""
    distance(M::MetricManifold{SymmetricPositiveDefinite,LogCholeskyMetric}, x, y)

Compute the distance on the manifold of [`SymmetricPositiveDefinite`](@ref)
nmatrices, i.e. between two symmetric positive definite matrices `x` and `y`
with respect to the [`LogCholeskyMetric`](@ref). The formula reads

````math
d_{\mathcal P(n)}(x,y) = \sqrt{
 \lVert \lfloor l \rfloor - \lfloor k \rfloor \rVert_{\mathrm{F}}^2
 + \lVert \log(\operatorname{diag}(l)) - \log(\operatorname{diag}(k))\rVert_{\mathrm{F}}^2 }\ \ ,
````

where $l$ and $k$ are the cholesky factors of $x$ and $y$, respectively,
$\lfloor\cdot\rfloor$ denbotes the strictly lower triangular matrix of its argument,
and $\lVert\cdot\rVert_{\mathrm{F}}$ denotes the Frobenius norm.
"""
function distance(
    M::MetricManifold{SymmetricPositiveDefinite{N},LogCholeskyMetric},
    x,
    y,
) where {N}
    return distance(CholeskySpace{N}(), cholesky(x).L, cholesky(y).L)
end

@doc doc"""
    exp(M::MetricManifold{SymmetricPositiveDefinite,LogCholeskyMetric}, x, v)

Compute the exponential map on the [`SymmetricPositiveDefinite`](@ref) `M` with
[`LogCholeskyMetric`](@ref) from `x` into direction `v`. The formula reads

````math
\exp_x v = (\exp_l w)(\exp_l w)^\mathrm{T}
````

where $\exp_lw$ is the exponential map on [`CholeskySpace`](@ref), $l$ is the cholesky
decomposition of $x$, $w = l(l^{-1}vl^{-\mathrm{T}})_\frac{1}{2}$,
and $(\cdot)_\frac{1}{2}$
denotes the lower triangular matrix with the diagonal multiplied by $\frac{1}{2}$.
"""
exp(::MetricManifold{SymmetricPositiveDefinite,LogCholeskyMetric}, ::Any...)

function exp!(
    M::MetricManifold{SymmetricPositiveDefinite{N},LogCholeskyMetric},
    y,
    x,
    v,
) where {N}
    (l, w) = spd_to_cholesky(x, v)
    z = exp(CholeskySpace{N}(), l, w)
    return copyto!(y, z * z')
end

@doc doc"""
    inner(M::MetricManifold{SymmetricPositiveDefinite,LogCholeskyMetric}, x, v, w)

Compute the inner product of two matrices `v`, `w` in the tangent space of `x`
on the [`SymmetricPositiveDefinite`](@ref) manifold `M`, as
a [`MetricManifold`](@ref) with [`LogCholeskyMetric`](@ref). The formula reads

````math
    (v,w)_x = (p_l(w),p_l(v))_l,
````

where the right hand side is the inner product on the [`CholeskySpace`](@ref),
$l$ is the cholesky factor of $x$,
$p_l(w) = l (l^{-1}wl^{-\mathrm{T}})_{\frac{1}{2}}$, and $(\cdot)_\frac{1}{2}$
denotes the lower triangular matrix with the diagonal multiplied by $\frac{1}{2}$
"""
function inner(
    M::MetricManifold{SymmetricPositiveDefinite{N},LogCholeskyMetric},
    x,
    v,
    w,
) where {N}
    (l, vl) = spd_to_cholesky(x, v)
    (l, wl) = spd_to_cholesky(x, l, w)
    return inner(CholeskySpace{N}(), l, vl, wl)
end

@doc doc"""
    log(M::MetricManifold{SymmetricPositiveDefinite,LogCholeskyMetric}, x, y)

Compute the logarithmic map on [`SymmetricPositiveDefinite`](@ref) `M` with
respect to the [`LogCholeskyMetric`](@ref) eminating from `x` to `y`.
The formula can be adapted from the [`CholeskySpace`](@ref) as
````math
\log_x y = lw^{\mathrm{T}} + wl^{\mathrm{T}},
````
where $l$ is the colesky factor of $x$ and $w=\log_lk$ for $k$ the cholesky factor
of $y$ and the just mentioned logarithmic map is the one on [`CholeskySpace`](@ref).
"""
log(::MetricManifold{SymmetricPositiveDefinite,LogCholeskyMetric}, ::Any...)

function log!(
    M::MetricManifold{SymmetricPositiveDefinite{N},LogCholeskyMetric},
    v,
    x,
    y,
) where {N}
    l = cholesky(x).L
    k = cholesky(y).L
    log!(CholeskySpace{N}(), v, l, k)
    return tangent_cholesky_to_tangent_spd!(l, v)
end

@doc doc"""
    vector_transport_to(
        M::MetricManifold{SymmetricPositiveDefinite,LogCholeskyMetric},
        x,
        v,
        y,
        ::ParallelTransport,
    )

Parallely transport the tangent vector `v` at `x` along the geodesic to `y` with respect to
the [`SymmetricPositiveDefinite`](@ref) manifold `M` and [`LogCholeskyMetric`](@ref).
The parallel transport is based on the parallel transport on [`CholeskySpace`](@ref):
Let $l$ and $k$ denote the cholesky factors of `x` and `y`, respectively and
$w = l(l^{-1}vl^{-\mathrm{T}})_\frac{1}{2}$, where $(\cdot)_\frac{1}{2}$ denotes the lower
triangular matrix with the diagonal multiplied by $\frac{1}{2}$. With $u$ the parallel
transport on [`CholeskySpace`](@ref) from $l$ to $k$. The formula hear reads

````math
    \mathcal P_{y\gets x}(v) = ku^{\mathrm{T}} + uk^{\mathrm{T}}.
````
"""
vector_transport_to(
    ::MetricManifold{SymmetricPositiveDefinite,LogCholeskyMetric},
    ::Any,
    ::Any,
    ::Any,
    ::ParallelTransport,
)

function vector_transport_to!(
    M::MetricManifold{SymmetricPositiveDefinite{N},LogCholeskyMetric},
    vto,
    x,
    v,
    y,
    ::ParallelTransport,
) where {N}
    k = cholesky(y).L
    (l, w) = spd_to_cholesky(x, v)
    vector_transport_to!(CholeskySpace{N}(), vto, l, w, k, ParallelTransport())
    return tangent_cholesky_to_tangent_spd!(k, vto)
end


using DifferentialEquations
const g = 9.81
L = 1.0
tspan = (0.0,1.0)
function f(du,u,p,t)
    du[1] = u[3]#vx
    du[2] = u[4]#vy
    du[3] =  0 #ax no forces in this direction
    du[4] = g #ax
end
function bc1!(residual, u, p, t)
    residual[1] = u[end][1] + pi/2 # the solution at the middle of the time span should be -pi/2
    residual[2] = u[end][1] - pi/2 # the solution at the end of the time span should be pi/2
    residual[3] = u[1][2]-1
end
bvp1 = BVProblem(simplependulum!, bc1!, [pi/2,pi/2,0.0], tspan)
sol1 = solve(bvp1, GeneralMIRK4(), dt=0.05)
#plot(sol1)


using LimitedLDLFactorizations
using MatrixMarket

# Warmup
A = sprand(10, 10, .5); A = A * A' + spdiagm(rand(10))
(L, d, α) = lldl(A, memory=5)

# obtain matrix from
# https://www.cise.ufl.edu/research/sparse/matrices/HB/bcsstk09.html
K = MatrixMarket.mmread("bcsstk09.mtx")
# make K lower triangular and extract diagonal here
# so we don't time tril() and diag().
Kdiag = diag(K)
K1 = tril(K, -1)
nnzK1 = nnz(K1)
n = size(K, 1)

@printf("%2s %6s %6s %8s %7s %8s\n",
        "p", "nnz(K)", "nnz(L)", "‖LDL'-K‖", "α", "time")
for p = 0 : 5 : 50
  res, t, b, g, m = @timed lldl(K1, Kdiag, memory=p)
  (L, d, α) = res
  L = L + speye(n)
  @printf("%2d %6d %6d %8.2e %7.1e %8.2e\n",
          p, nnzK1, nnz(L) - n, norm(L*diagm(d)*L' - K, 1) / norm(K, 1), α, t)
end


using DelayDiffEq

import FiniteDiff
import ForwardDiff

using Test

# Hutchinson's equation
f(u, h, p, t) = p[2] * u * (1 - h(p, t - p[1]) / p[3])
h(p, t) = p[4]

@testset "Gradient" begin
  function test(p)
    prob = DDEProblem(f, p[5], h, eltype(p).((0.0, 10.0)), copy(p);
                      constant_lags = (p[1],))
    sol = solve(prob, MethodOfSteps(Tsit5()); abstol = 1e-14, reltol = 1e-14,
                saveat = 1.0)
    sum(sol)
  end

   # without delay length estimation
  p = [1.5, 1.0, 0.5]
  findiff = FiniteDiff.finite_difference_gradient(p -> test(vcat(1, p, p[end])), p)
  fordiff = ForwardDiff.gradient(p -> test(vcat(1, p, p[end])), p)
  @test maximum(abs.(findiff .- fordiff)) < 1e-6

  # with delay length estimation and without discontinuity
  p = [1.0, 1.5, 1.0, 0.5]
  findiff2 = FiniteDiff.finite_difference_gradient(p -> test(vcat(p, p[end])), p)
  fordiff2 = ForwardDiff.gradient(p -> test(vcat(p, p[end])), p)
  @test_broken maximum(abs.(findiff2 .- fordiff2)) < 2

  # with discontinuity and without delay length estimation
  p = [1.5, 1.0, 0.5, 1.0]
  findiff3 = FiniteDiff.finite_difference_gradient(p -> test(vcat(1, p)), p)
  fordiff3 = ForwardDiff.gradient(p -> test(vcat(1, p)),p)
  @test maximum(abs.(findiff3 .- fordiff3)) < 9

  # consistency checks
  @test findiff2[2:end] ≈ findiff
  @test fordiff2[2:end] ≈ fordiff
  @test_broken findiff3[1:(end-1)] ≈ findiff
  @test_broken fordiff3[1:(end-1)] ≈ fordiff
end

@testset "Jacobian" begin
  function test(p)
    prob = DDEProblem(f, p[5], h, eltype(p).((0.0, 10.0)), copy(p);
                      constant_lags = (p[1],))
    sol = solve(prob, MethodOfSteps(Tsit5()); abstol = 1e-14, reltol = 1e-14,
                saveat = 1.0)
    sol.u
  end

  # without delay length estimation
  p = [1.5, 1.0, 0.5]
  findiff = FiniteDiff.finite_difference_jacobian(p -> test(vcat(1, p, p[end])), p)
  fordiff = ForwardDiff.jacobian(p -> test(vcat(1, p, p[end])), p)
  @test maximum(abs.(findiff .- fordiff)) < 2e-6

  # with delay length estimation and without discontinuity
  p = [1.0, 1.5, 1.0, 0.5]
  findiff2 = FiniteDiff.finite_difference_jacobian(p -> test(vcat(p, p[end])), p)
  fordiff2 = ForwardDiff.jacobian(p -> test(vcat(p, p[end])), p)
  @test_broken maximum(abs.(findiff2 .- fordiff2)) < 3

  # with discontinuity and without delay length estimation
  p = [1.5, 1.0, 0.5, 1.0]
  findiff3 = FiniteDiff.finite_difference_jacobian(p -> test(vcat(1, p)), p)
  fordiff3 = ForwardDiff.jacobian(p -> test(vcat(1, p)),p)
  @test maximum(abs.(findiff3 .- fordiff3)) < 1

  # consistency checks
  @test findiff2[:, 2:end] ≈ findiff
  @test fordiff2[:, 2:end] ≈ fordiff
  @test_broken findiff3[:, 1:(end-1)] ≈ findiff
  @test_broken fordiff3[:, 1:(end-1)] ≈ fordiff
end

@testset "Hessian" begin
  function test(p)
    prob = DDEProblem(f, p[5], h, eltype(p).((0.0, 10.0)), copy(p);
                      constant_lags = (p[1],))
    sol = solve(prob, MethodOfSteps(Tsit5()); abstol = 1e-14, reltol = 1e-14,
                saveat = 1.0)
    sum(sol)
  end

  # without delay length estimation and without discontinuity
  p = [1.5, 1.0, 0.5]
  findiff = FiniteDiff.finite_difference_hessian(p -> test(vcat(1, p, p[end])), p)
  fordiff = ForwardDiff.hessian(p -> test(vcat(1, p, p[end])), p)
  @test maximum(abs.(findiff .- fordiff)) < 1e-5

  # with delay length estimation and without discontinuity
  p = [1.0, 1.5, 1.0, 0.5]
  findiff2 = FiniteDiff.finite_difference_hessian(p -> test(vcat(p, p[end])), p)
  fordiff2 = ForwardDiff.hessian(p -> test(vcat(p, p[end])), p)
  @test_broken maximum(abs.(findiff2 .- fordiff2)) < 25

  # with discontinuity and without delay length estimation
  p = [1.5, 1.0, 0.5, 1.0]
  findiff3 = FiniteDiff.finite_difference_hessian(p -> test(vcat(1, p)), p)
  fordiff3 = ForwardDiff.hessian(p -> test(vcat(1, p)),p)
  @test maximum(abs.(findiff3 .- fordiff3)) < 1

  # consistency checks
  @test findiff2[2:end, 2:end] ≈ findiff
  @test fordiff2[2:end, 2:end] ≈ fordiff
  @test_broken findiff3[1:(end-1), 1:(end-1)] ≈ findiff
  @test_broken fordiff3[1:(end-1), 1:(end-1)] ≈ fordiff
end


using FMMLIB2D
using Test

@testset "Laplace FMM 2D" begin
    include("laplace.jl")
end

@testset "Helmholtz FMM 2D" begin
    include("helmholtz.jl")
end

@testset "Complex FMM 2D" begin
    include("complex.jl")
end


module InterfaceSymbolicUtilsModule

using SymbolicUtils
import ..CoreModule: CONST_TYPE, Node, Options
import ..UtilsModule: isgood, isbad, @return_on_false

const SYMBOLIC_UTILS_TYPES = Union{<:Number,SymbolicUtils.Symbolic{<:Number}}

const SUPPORTED_OPS = (cos, sin, exp, cot, tan, csc, sec, +, -, *, /)

function isgood(x::SymbolicUtils.Symbolic)
    return if SymbolicUtils.istree(x)
        all(isgood.([SymbolicUtils.operation(x); SymbolicUtils.arguments(x)]))
    else
        true
    end
end
subs_bad(x) = isgood(x) ? x : Inf

function parse_tree_to_eqs(tree::Node, options::Options, index_functions::Bool=false)
    if tree.degree == 0
        # Return constant if needed
        tree.constant && return subs_bad(tree.val)
        return SymbolicUtils.Sym{LiteralReal}(Symbol("x$(tree.feature)"))
    end
    # Collect the next children
    children = tree.degree >= 2 ? (tree.l, tree.r) : (tree.l,)
    # Get the operation
    op = tree.degree > 1 ? options.binops[tree.op] : options.unaops[tree.op]
    # Create an N tuple of Numbers for each argument
    dtypes = map(x -> Number, 1:(tree.degree))
    #
    if !(op ∈ SUPPORTED_OPS) && index_functions
        op = SymbolicUtils.Sym{(SymbolicUtils.FnType){Tuple{dtypes...},Number}}(Symbol(op))
    end

    return subs_bad(
        op(map(x -> parse_tree_to_eqs(x, options, index_functions), children)...)
    )
end

# For operators which are indexed, we need to convert them back
# using the string:
function convert_to_function(
    x::SymbolicUtils.Sym{SymbolicUtils.FnType{T,Number}}, options::Options
) where {T<:Tuple}
    degree = length(T.types)
    if degree == 1
        ind = findoperation(x.name, options.unaops)
        return options.unaops[ind]
    elseif degree == 2
        ind = findoperation(x.name, options.binops)
        return options.binops[ind]
    else
        throw(AssertionError("Function $(String(x.name)) has degree > 2 !"))
    end
end

# For normal operators, simply return the function itself:
convert_to_function(x, options::Options) = x

# Split equation
function split_eq(
    op, args, options::Options; varMap::Union{Array{String,1},Nothing}=nothing
)
    !(op ∈ (sum, prod, +, *)) && throw(error("Unsupported operation $op in expression!"))
    if Symbol(op) == Symbol(sum)
        ind = findoperation(+, options.binops)
    elseif Symbol(op) == Symbol(prod)
        ind = findoperation(*, options.binops)
    else
        ind = findoperation(op, options.binops)
    end
    return Node(
        ind,
        convert(Node, args[1], options; varMap=varMap),
        convert(Node, op(args[2:end]...), options; varMap=varMap),
    )
end

function findoperation(op, ops)
    for (i, oi) in enumerate(ops)
        Symbol(oi) == Symbol(op) && return i
    end
    throw(error("Operation $(op) in expression not found in operations $(ops)!"))
end

function Base.convert(
    ::typeof(SymbolicUtils.Symbolic),
    tree::Node,
    options::Options;
    varMap::Union{Array{String,1},Nothing}=nothing,
    index_functions::Bool=false,
)
    return node_to_symbolic(tree, options; varMap=varMap, index_functions=index_functions)
end

function Base.convert(
    ::typeof(Node),
    x::Number,
    options::Options;
    varMap::Union{Array{String,1},Nothing}=nothing,
)
    return Node(CONST_TYPE(x))
end

function Base.convert(
    ::typeof(Node),
    expr::SymbolicUtils.Symbolic,
    options::Options;
    varMap::Union{Array{String,1},Nothing}=nothing,
)
    if !SymbolicUtils.istree(expr)
        varMap === nothing && return Node(String(expr.name))
        return Node(String(expr.name), varMap)
    end

    # First, we remove integer powers:
    y, good_return = multiply_powers(expr)
    if good_return
        expr = y
    end

    op = convert_to_function(SymbolicUtils.operation(expr), options)
    args = SymbolicUtils.arguments(expr)

    length(args) > 2 && return split_eq(op, args, options; varMap=varMap)
    ind = if length(args) == 2
        findoperation(op, options.binops)
    else
        findoperation(op, options.unaops)
    end

    return Node(ind, map(x -> convert(Node, x, options; varMap=varMap), args)...)
end

"""
    node_to_symbolic(tree::Node, options::Options;
                varMap::Union{Array{String, 1}, Nothing}=nothing,
                index_functions::Bool=false)

The interface to SymbolicUtils.jl. Passing a tree to this function
will generate a symbolic equation in SymbolicUtils.jl format.

## Arguments

- `tree::Node`: The equation to convert.
- `options::Options`: Options, which contains the operators used in the equation.
- `varMap::Union{Array{String, 1}, Nothing}=nothing`: What variable names to use for
    each feature. Default is [x1, x2, x3, ...].
- `index_functions::Bool=false`: Whether to generate special names for the
    operators, which then allows one to convert back to a `Node` format
    using `symbolic_to_node`.
    (CURRENTLY UNAVAILABLE - See https://github.com/MilesCranmer/SymbolicRegression.jl/pull/84).
"""
function node_to_symbolic(
    tree::Node,
    options::Options;
    varMap::Union{Array{String,1},Nothing}=nothing,
    index_functions::Bool=false,
)
    expr = subs_bad(parse_tree_to_eqs(tree, options, index_functions))
    # Check for NaN and Inf
    @assert isgood(expr) "The recovered equation contains NaN or Inf."
    # Return if no varMap is given
    varMap === nothing && return expr
    # Create a substitution tuple
    subs = Dict(
        [
            SymbolicUtils.Sym{LiteralReal}(Symbol("x$(i)")) =>
                SymbolicUtils.Sym{LiteralReal}(Symbol(varMap[i])) for i in 1:length(varMap)
        ]...,
    )
    return substitute(expr, subs)
end

function symbolic_to_node(
    eqn::T, options::Options; varMap::Union{Array{String,1},Nothing}=nothing
)::Node where {T<:SymbolicUtils.Symbolic}
    return convert(Node, eqn, options; varMap=varMap)
end

# function Base.convert(::typeof(Node), x::Number, options::Options; varMap::Union{Array{String, 1}, Nothing}=nothing)
# function Base.convert(::typeof(Node), expr::SymbolicUtils.Symbolic, options::Options; varMap::Union{Array{String, 1}, Nothing}=nothing)

function multiply_powers(eqn::Number)::Tuple{SYMBOLIC_UTILS_TYPES,Bool}
    return eqn, true
end

function multiply_powers(eqn::SymbolicUtils.Symbolic)::Tuple{SYMBOLIC_UTILS_TYPES,Bool}
    if !SymbolicUtils.istree(eqn)
        return eqn, true
    end
    op = SymbolicUtils.operation(eqn)
    return multiply_powers(eqn, op)
end

function multiply_powers(
    eqn::SymbolicUtils.Symbolic, op::F
)::Tuple{SYMBOLIC_UTILS_TYPES,Bool} where {F}
    args = SymbolicUtils.arguments(eqn)
    nargs = length(args)
    if nargs == 1
        l, complete = multiply_powers(args[1])
        @return_on_false complete eqn
        @return_on_false isgood(l) eqn
        return op(l), true
    elseif op == ^
        l, complete = multiply_powers(args[1])
        @return_on_false complete eqn
        @return_on_false isgood(l) eqn
        n = args[2]
        if typeof(n) <: Int
            if n == 1
                return l, true
            elseif n == -1
                return 1.0 / l, true
            elseif n > 1
                return reduce(*, [l for i in 1:n]), true
            elseif n < -1
                return reduce(/, vcat([1], [l for i in 1:abs(n)])), true
            else
                return 1.0, true
            end
        else
            r, complete2 = multiply_powers(args[2])
            @return_on_false complete2 eqn
            return l^r, true
        end
    elseif nargs == 2
        l, complete = multiply_powers(args[1])
        @return_on_false complete eqn
        @return_on_false isgood(l) eqn
        r, complete2 = multiply_powers(args[2])
        @return_on_false complete2 eqn
        @return_on_false isgood(r) eqn
        return op(l, r), true
    else
        # return mapreduce(multiply_powers, op, args)
        # ## reduce(op, map(multiply_powers, args))
        out = map(multiply_powers, args) #vector of tuples
        for i in 1:size(out, 1)
            @return_on_false out[i][2] eqn
            @return_on_false isgood(out[i][1]) eqn
        end
        cumulator = out[1][1]
        for i in 2:size(out, 1)
            cumulator = op(cumulator, out[i][1])
            @return_on_false isgood(cumulator) eqn
        end
        return cumulator, true
    end
end

end


using Test, LinearMaps, LinearAlgebra, Quaternions

@testset "noncommutative number type" begin
    x = Quaternion.(rand(10), rand(10), rand(10), rand(10))
    v = rand(10)
    A = Quaternion.(rand(10,10), rand(10,10), rand(10,10), rand(10,10))
    γ = Quaternion.(rand(4)...) # "Number"
    α = UniformScaling(γ)
    β = UniformScaling(Quaternion.(rand(4)...))
    L = LinearMap(A)
    @test Array(L) == A
    @test Array(L') == A'
    @test Array(transpose(L)) == transpose(A)
    @test Array(α * L) == α * A
    @test Array(L * α) == A * α
    @test Array(α * L) == α * A
    @test Array(L * α ) == A * α
    @test Array(α * L') == α * A'
    @test Array((α * L')') ≈ (α * A')' ≈ A * conj(α)
    @test L * x ≈ A * x
    @test L' * x ≈ A' * x
    @test α * (L * x) ≈ α * (A * x)
    @test α * L * x ≈ α * A * x
    @test 3L * x ≈ 3A * x
    @test 3L' * x ≈ 3A' * x
    @test α * (3L * x) ≈ α * (3A * x)
    @test (@inferred α * 3L) * x ≈ α * 3A * x
    @test (@inferred 3L * α) * x ≈ 3A * α * x
    @test (α * L') * x ≈ (α * A') * x
    @test (α * L')' * x ≈ (α * A')' * x
    @test (α * L')' * v ≈ (α * A')' * v
    @test Array(@inferred adjoint(α * L * β)) ≈ conj(β) * A' * conj(α)
    @test Array(@inferred transpose(α * L * β)) ≈ β * transpose(A) * α
    J = LinearMap(α, 10)
    @test (β * J) * x ≈ LinearMap(β*α, 10) * x ≈ β*α*x
    @test (J * β) * x ≈ LinearMap(α*β, 10) * x ≈ α*β*x
    M = β.λ * (γ * L * L)
    @test M == β * (γ * L * L)
    @test length(M.maps) == 3
    @test M.maps[end].λ == β.λ * γ
    @test γ * (β * L * L) == γ * (β.λ * L * L) == α * (β.λ * L * L) == α * (β * L * L)
    @test length(M.maps) == 3
    @test M.maps[end].λ == β.λ * γ
    M = (L * L * γ) * β.λ
    @test M == (L * L * γ) * β == (L * L * α) * β == (L * L * α) * β.λ
    @test length(M.maps) == 3
    @test M.maps[1].λ == γ*β.λ

    # exercise non-RealOrComplex scalar operations
    @test Array(γ * (L'*L)) ≈ γ * (A'*A) # CompositeMap
    @test Array((L'*L) * γ) ≈ (A'*A) * γ
    @test Array(-L) == -A
    @test Array(γ \ L) ≈ γ \ A
    @test Array(L / γ) ≈ A / γ
end

@testset "nonassociative number type" begin
    x = Octonion.(rand(10), rand(10), rand(10), rand(10),rand(10), rand(10), rand(10), rand(10))
    v = rand(10)
    A = Octonion.(rand(10,10), rand(10,10), rand(10,10), rand(10,10),rand(10,10), rand(10,10), rand(10,10), rand(10,10))
    α = UniformScaling(Octonion.(rand(8)...))
    β = UniformScaling(Octonion.(rand(8)...))
    L = LinearMap(A)
    @test Array(L) == A
    @test Array(α * L) == α * A
    @test Array(L * α) == A * α
    @test Array(α * L) == α * A
    @test Array(L * α) == A * α
    @test (α * L')' * v ≈ (α * A')' * v
end


abstract type AbstractLeakyDAFESN <: AbstractLeakyESN end

struct dafESN{T, S<:AbstractArray{T}, I, B, F1, F2, N} <: AbstractLeakyDAFESN
    res_size::I
    in_size::I
    out_size::I
    train_data::S
    alpha::T
    nla_type::N
    first_activation::F1
    second_activation::F2
    first_lambda::T
    second_lambda::T
    W::S
    W_in::S
    states::S
    extended_states::B
end

function dafESN(approx_res_size::Int,
        train_data::AbstractArray{T},
        degree::Int,
        radius::T,
        first_lambda::T,
        second_lambda::T;
        first_activation::Any = tanh,
        second_activation::Any = tanh,
        sigma::T = 0.1,
        alpha::T = 1.0,
        nla_type::NonLinearAlgorithm = NLADefault(),
        extended_states::Bool = false) where T<:AbstractFloat

    in_size = size(train_data, 1)
    out_size = size(train_data, 1)
    res_size = Int(floor(approx_res_size/in_size)*in_size)

    W = init_reservoir_givendeg(res_size, radius, degree)
    W_in = init_input_layer(res_size, in_size, sigma)
    states = daf_states_matrix(W, W_in, train_data, alpha,
    first_activation, second_activation, first_lambda, second_lambda, extended_states)

    return dafESN{T, typeof(train_data),
        typeof(res_size),
        typeof(extended_states),
        typeof(first_activation),
        typeof(second_activation),
        typeof(nla_type)}(res_size, in_size, out_size, train_data,
    alpha, nla_type, first_activation, second_activation, first_lambda,
    second_lambda, W, W_in, states, extended_states)
end

#reservoir matrix W given by the user
function dafESN(W::AbstractArray{T},
        train_data::AbstractArray{T},
        first_lambda::T,
        second_lambda::T;
        first_activation::Any = tanh,
        second_activation::Any = tanh,
        sigma::T = 0.1,
        alpha::T = 1.0,
        nla_type::NonLinearAlgorithm = NLADefault(),
        extended_states::Bool = false) where T<:AbstractFloat

    in_size = size(train_data, 1)
    out_size = size(train_data, 1)
    res_size = size(W, 1)

    W_in = init_input_layer(res_size, in_size, sigma)
    states = daf_states_matrix(W, W_in, train_data, alpha,
    first_activation, second_activation, first_lambda, second_lambda, extended_states)

    return dafESN{T, typeof(train_data),
        typeof(res_size),
        typeof(extended_states),
        typeof(first_activation),
        typeof(second_activation),
        typeof(nla_type)}(res_size, in_size, out_size, train_data,
    alpha, nla_type, first_activation, second_activation, first_lambda,
    second_lambda, W, W_in, states, extended_states)
end

#input layer W_in given by the user
function dafESN(approx_res_size::Int,
        train_data::AbstractArray{T},
        degree::Int,
        radius::T,
        first_lambda::T,
        second_lambda::T,
        W_in::AbstractArray{T};
        first_activation::Any = tanh,
        second_activation::Any = tanh,
        alpha::T = 1.0,
        nla_type::NonLinearAlgorithm = NLADefault(),
        extended_states::Bool = false) where T<:AbstractFloat

    in_size = size(train_data, 1)
    out_size = size(train_data, 1)
    res_size = Int(floor(approx_res_size/in_size)*in_size)
    W = init_reservoir_givendeg(res_size, radius, degree)

    if size(W_in, 1) != res_size
        throw(DimensionMismatch(W_in, "size(W_in, 1) must be equal to size(W, 1)"))
    elseif size(W_in, 2) != in_size
        throw(DimensionMismatch(W_in, "size(W_in, 2) must be equal to in_size"))
    end

    states = daf_states_matrix(W, W_in, train_data, alpha,
    first_activation, second_activation, first_lambda, second_lambda, extended_states)

    return dafESN{T, typeof(train_data),
        typeof(res_size),
        typeof(extended_states),
        typeof(first_activation),
        typeof(second_activation),
        typeof(nla_type)}(res_size, in_size, out_size, train_data,
    alpha, nla_type, first_activation, second_activation, first_lambda,
    second_lambda, W, W_in, states, extended_states)
end

#reservoir matrix W and input layer W_in given by the user
"""
    dafESN(W::AbstractArray{T}, train_data::AbstractArray{T}, first_lambda::T,
    second_lambda::T, W_in::AbstractArray{T} [, first_activation::Any,
    second_activation::Any, alpha::T, nla_type::NonLinearAlgorithm, extended_states::Bool])

Build a double activation function ESN struct given the input and reservoir matrices, as described in [1].

[1]: Lun, Shu-Xian, et al. "A novel model of leaky integrator echo state network for time-series prediction." Neurocomputing 159 (2015): 58-66.
"""
function dafESN(W::AbstractArray{T},
        train_data::AbstractArray{T},
        first_lambda::T,
        second_lambda::T,
        W_in::AbstractArray{T};
        first_activation::Any = tanh,
        second_activation::Any = tanh,
        alpha::T = 1.0,
        nla_type::NonLinearAlgorithm = NLADefault(),
        extended_states::Bool = false) where T<:AbstractFloat

    in_size = size(train_data, 1)
    out_size = size(train_data, 1)
    res_size = size(W, 1)

    if size(W_in, 1) != res_size
        throw(DimensionMismatch(W_in, "size(W_in, 1) must be equal to size(W, 1)"))
    elseif size(W_in, 2) != in_size
        throw(DimensionMismatch(W_in, "size(W_in, 2) must be equal to in_size"))
    end

    states = daf_states_matrix(W, W_in, train_data, alpha,
    first_activation, second_activation, first_lambda, second_lambda, extended_states)

    return dafESN{T, typeof(train_data),
        typeof(res_size),
        typeof(extended_states),
        typeof(first_activation),
        typeof(second_activation),
        typeof(nla_type)}(res_size, in_size, out_size, train_data,
    alpha, nla_type, first_activation, second_activation, first_lambda,
    second_lambda, W, W_in, states, extended_states)
end

function daf_states_matrix(W::AbstractArray{Float64},
        W_in::AbstractArray{Float64},
        train_data::AbstractArray{Float64},
        alpha::Float64,
        first_activation::Function,
        second_activation::Function,
        first_lambda::Float64,
        second_lambda::Float64,
        extended_states::Bool)

    train_len = size(train_data, 2)
    res_size = size(W, 1)
    in_size = size(train_data, 1)
    states = zeros(Float64, res_size, train_len)
    for i=1:train_len-1
        states[:, i+1] = double_leaky_fixed_rnn(alpha, first_activation, second_activation, first_lambda, second_lambda, W, W_in, states[:, i], train_data[:, i])
    end

    if extended_states == true
        ext_states = vcat(states, hcat(zeros(Float64, in_size), train_data[:,1:end-1]))
        return ext_states
    else
        return states
    end

    return states
end

"""
    dafESNpredict(esn::AbstractLeakyESN, predict_len::Int, W_out::AbstractArray{Float64})

Return the prediction for a given length of the constructed dafESN struct.
"""
function dafESNpredict(esn::AbstractLeakyDAFESN,
    predict_len::Int,
    W_out::AbstractArray{Float64})

    output = zeros(Float64, esn.in_size, predict_len)
    x = esn.states[:, end]

    if esn.extended_states == false
        for i=1:predict_len
            x_new = nla(esn.nla_type, x)
            out = (W_out*x_new)
            output[:, i] = out
            x = double_leaky_fixed_rnn(esn.alpha, esn.first_activation, esn.second_activation, esn.first_lambda, esn.second_lambda, esn.W, esn.W_in, x, out)
        end
    elseif esn.extended_states == true
        for i=1:predict_len
            x_new = nla(esn.nla_type, x)
            out = (W_out*x_new)
            output[:, i] = out
            x = vcat(double_leaky_fixed_rnn(esn.alpha, esn.first_activation, esn.second_activation, esn.first_lambda, esn.second_lambda, esn.W, esn.W_in, x[1:esn.res_size], out), out)
        end
    end

    return output
end

"""
    dafESNpredict_h_steps(esn::AbstractLeakyESN, predict_len::Int, h_steps::Int,
    test_data::AbstractArray{Float64}, W_out::AbstractArray{Float64})

Return the prediction for h steps ahead of the constructed ESN struct.
"""
function dafESNpredict_h_steps(esn::AbstractLeakyESN,
    predict_len::Int,
    h_steps::Int,
    test_data::AbstractArray{Float64},
    W_out::AbstractArray{Float64})

    output = zeros(Float64, esn.in_size, predict_len)
    x = esn.states[:, end]

    if esn.extended_states == false
        for i=1:predict_len
            x_new = nla(esn.nla_type, x)
            out = (W_out*x_new)
            output[:, i] = out
            if mod(i, h_steps) == 0
                x = double_leaky_fixed_rnn(esn.alpha, esn.first_activation, esn.second_activation, esn.first_lambda, esn.second_lambda, esn.W, esn.W_in, x, test_data[:,i])
            else
                x = double_leaky_fixed_rnn(esn.alpha, esn.first_activation, esn.second_activation, esn.first_lambda, esn.second_lambda, esn.W, esn.W_in, x, out)
            end
        end
    elseif esn.extended_states == true
        for i=1:predict_len
            x_new = nla(esn.nla_type, x)
            out = (W_out*x_new)
            output[:, i] = out
            if mod(i, h_steps) == 0
                x = vcat(double_leaky_fixed_rnn(esn.alpha, esn.first_activation, esn.second_activation, esn.first_lambda, esn.second_lambda, esn.W, esn.W_in, x[1:esn.res_size], test_data[:,i]), test_data[:,i])
            else
                x = vcat(double_leaky_fixed_rnn(esn.alpha, esn.first_activation, esn.second_activation, esn.first_lambda, esn.second_lambda, esn.W, esn.W_in, x[1:esn.res_size], out), out)
            end
        end
    end
    return output
end


"""
    OCEstimator <: Estimator

Compatible estimator minimizing expected MSE for response rate `p` see also:

> Kunzmann K, Kieser M. Point estimation and p‐values in phase II adaptive two‐stage designs with a binary endpoint. Statistics in medicine. 2017 Mar 15;36(6):971-84.
"""
struct OCEstimator{TD<:Design} <: Estimator

  design::TD
  estimates::DataFrames.DataFrame

  function OCEstimator{TD}(
    design::TD,
    solver::TS,
    prior::Function,
    k::Integer
  ) where {TD<:Design,TS<:MathProgBase.AbstractMathProgSolver}

    epsilon = 10.0^(-6.0)
    n1   = interimsamplesize(design)
    nmax = maximum(samplesize(design))
    supp = support(design)
    model = JuMP.Model(solver = solver)
    JuMP.@variable(model, 0.0 <= est[x1 = 0:n1, x2 = 0:(nmax - n1); ispossible(design, x1, x2)] <= 1.0)
    h = 1 / k
    vrho = collect(linspace(0, 1, k + 1))

    function coefs(x1, x2, rho)

      if !ispossible(design, x1, x2)
        return 0.0
      else
        pdf = prior(rho)
        pdf = (pdf > 10.0) | !isfinite(pdf) ? 10 : pdf
        return pdf * rho^(x1 + x2) * (1.0 - rho)^(samplesize(design, x1) - x1 - x2)
      end

    end # coefs

    JuMP.@objective(model, Min,
      sum(binomial(BigInt(n1), BigInt(x1))*binomial(BigInt(samplesize(design, x1) - n1), BigInt(x2))*h/3*(
              coefs(x1, x2, vrho[2*i - 1])*(est[x1, x2] - vrho[2*i - 1])^2 +
            4*coefs(x1, x2,     vrho[2*i])*(est[x1, x2] -     vrho[2*i])^2 +
              coefs(x1, x2, vrho[2*i + 1])*(est[x1, x2] - vrho[2*i + 1])^2) for
        x1 in 0:n1, x2 in 0:(nmax - n1), i in 1:convert(Int64, floor(k/2)) if
        ispossible(design, x1, x2)
      )
    )
    JuMP.@variable(model, 0 <= minREJECT <= 1)
    JuMP.@variable(model, 0 <= maxACCEPT <= 1)
    for i in 1:size(supp, 1)
      x1, x2 = supp[i, :]
      if test(design, x1, x2) == true
        JuMP.@constraint(model, minREJECT - est[x1, x2] <= 0)
      else
        JuMP.@constraint(model, maxACCEPT - est[x1, x2] >= 0)
      end
    end
    JuMP.@constraint(model, minREJECT - maxACCEPT - epsilon >= 0) # smallest estimate of reject must be larger than largest estimate of accept
    for i in size(supp, 1)
      for j in size(supp, 1)
        if i < j
          x1a, x2a = supp[i, :]
          x1b, x2b = supp[j, :]
          if (x1a == x1b) & (x2a >= x2b) # same first stage, larger or equal second stage ~~> larger or equal estimate
            JuMP.@constraint(model, est[x1a, x2a] - est[x1b, x2b] >= 0)
          end
          n2a = samplesize(design, x1a)
          n2a = samplesize(design, x1a)
          if (n2a == interimsamplesize(design)) & (n2b == interimsamplesize(design)) & (x1a >= x1b) # no second stage, larger or equal first stage ~~> larger or equal estimate
            JuMP.@constraint(model, est[x1a, x2a] - est[x1b, x2b] >= 0)
          end
        end
      end
    end
    JuMP.solve(model)
    estimates = Array{Float64}(n1 + 1, nmax - n1 + 1)
    for x1 in 0:n1
      for x2 in 0:(nmax - n1) # brute force, not all possible
        if ispossible(design, x1, x2)
          estimates[x1 + 1, x2 + 1] = JuMP.getvalue(est[x1, x2])
        else
          estimates[x1 + 1, x2 + 1] = convert(Float64, NaN)
        end
      end
    end
    new(design, estimates)

  end # inner constructor

end

"""
    OCEstimator{TS<:MathProgBase.AbstractMathProgSolver}(
      design::Design,
      solver::TS;
      prior::Function = jeffreysprior(design),
      k = 100
    )

Create compatible estimator minimizing expected MSE for response rate `p` see also:

> Kunzmann K, Kieser M. Point estimation and p‐values in phase II adaptive two‐stage designs with a binary endpoint. Statistics in medicine. 2017 Mar 15;36(6):971-84.

# Parameters

| Parameter    | Description |
| -----------: | :---------- |
| design       | Design |
| solver       | MathProgBase solver, must support quadratic expressions |
| prior        | weight function for MSE values at different `p`, must be of form `f(p::Real)::Real` |
| k            | number of equally spaced grid-points for evaluation of MSE and prior |
"""
function OCEstimator(
  design::TD, solver::TS;
  prior::Function = jeffreysprior(design), k::Integer = 100
) where {TD<:Design,TS<:MathProgBase.AbstractMathProgSolver}

  isodd(k) ?
    error("k must be even") : nothing
  return OCEstimator{TD}(design, solver, prior, k)

end # outer constructor

Base.show(io::IO, estimator::OCEstimator) = print("OCEstimator")

function estimate(estimator::OCEstimator, x1::T, x2::T) where {T<:Integer}

  !ispossible(design(estimator), x1, x2) ?
      error("x1/x2 combination not possible under design") : nothing
    return estimator.estimates[x1 + 1, x2 + 1]

end


# brick wall scanning for a single MERA layer with treatment to the tail
function correlation_matrix_to_gmps_brickwall_tailed(
  Λ0::AbstractMatrix{ElT},
  inds::Vector{Int};
  eigval_cutoff::Float64=1e-8,
  maxblocksize::Int=size(Λ0, 1),
) where {ElT<:Number}
  Λ = Hermitian(Λ0)
  N = size(Λ, 1)
  V = Circuit{ElT}([])
  #ns = Vector{real(ElT)}(undef, 2*N)
  err_tot = 0.0
  indsnext = Int[]
  relinds = Int[]
  for i in 1:N
    if i % 2 == 0
      append!(indsnext, inds[i])
      append!(relinds, i)
      continue
    end
    blocksize = 0
    n = 0.0
    err = 0.0
    p = Int[]
    uB = 0.0
    # find the block whose lowest eigenvalue is within torelence
    for blocksize in 1:maxblocksize
      j = min(i + blocksize, N)
      ΛB = deepcopy(Λ[i:j, i:j]) #@view Λ[i:j, i:j] # \LambdaB is still part of Lambda
      nB, uB = eigen(Hermitian(ΛB))
      # sort by -(n * log(n) + (1 - n) * log(1 - n)) in ascending order
      p = sortperm(nB; by=entropy)
      n = nB[p[1]]
      err = min(n, 1 - n)
      err ≤ eigval_cutoff && break
    end
    # keep the node if the err cannot be reduced
    if i + maxblocksize >= N && err > eigval_cutoff
      append!(indsnext, inds[i])
      append!(relinds, i)
      continue
    end
    err_tot += err
    #ns[i] = n # eigenvalue
    v = deepcopy(uB[:, p[1]]) #@view uB[:, p[1]] # eigenvector of the correlation matrix
    g, _ = givens_rotations(v) # convert eigenvector into givens rotation
    shift!(g, i - 1) # shift rotation location
    # In-place version of:
    # V = g * V
    lmul!(g, V)
    #@show g
    Λ = Hermitian(g * Λ * g') #isolate current site i
  end
  return Λ, V, indsnext, relinds
end

# shift givens rotation indexes according to the inds
function shiftByInds!(G::Circuit, inds::Vector{Int})
  for (n, g) in enumerate(G.rotations)
    G.rotations[n] = Givens(inds[g.i1], inds[g.i2], g.c, g.s)
  end
  return G
end

"""
    correlation_matrix_to_gmera(Λ::AbstractMatrix{ElT}; eigval_cutoff::Float64 = 1e-8, maxblocksize::Int = size(Λ0, 1))
Diagonalize a correlation matrix through MERA layers,
output gates and eigenvalues of the correlation matrix
"""
# Combine gates for each MERA layer
function correlation_matrix_to_gmera(
  Λ0::AbstractMatrix{ElT}; eigval_cutoff::Float64=1e-8, maxblocksize::Int=size(Λ0, 1)
) where {ElT<:Number}
  Λ = Hermitian(Λ0)
  N = size(Λ, 1)
  Nnew = N - 1
  inds = collect(1:N)
  V = Circuit{ElT}([])
  Λtemp = deepcopy(Λ)
  layer = 0                                 # layer label of MERA
  while N > Nnew # conditioned on the reduction of nodes
    N = Nnew
    # indsnext: next layer indexes with original matrix labels
    # relinds: next layer indexes with labels from the last layer
    Λr, C, indsnext, relinds = correlation_matrix_to_gmps_brickwall_tailed(
      Λtemp, inds; eigval_cutoff=eigval_cutoff, maxblocksize=maxblocksize
    )
    shiftByInds!(C, inds)    # shift the index back to the original matrix
    inds = indsnext
    Λtemp = deepcopy(Λr[relinds, relinds]) # project to even site for next layer based on keeping indexes relinds
    Nnew = size(Λtemp, 1)
    lmul!(C, V)                           # add vector of givens rotation C into the larger vector V
    #V = C * V
    layer += 1
    #Λ = ITensors.Hermitian(C * Λ * C')
  end
  # gmps for the final layer
  Λr, C = correlation_matrix_to_gmps(
    Λtemp; eigval_cutoff=eigval_cutoff, maxblocksize=maxblocksize
  )
  shiftByInds!(C, inds)
  lmul!(C, V)
  Λ = V * Λ0 * V'
  ns = real.(diag(Λ))
  return ns, V
end

# output the MERA gates and eigenvalues of correlation matrix from WF
function slater_determinant_to_gmera(Φ::AbstractMatrix; kwargs...)
  return correlation_matrix_to_gmera(conj(Φ) * transpose(Φ); kwargs...)
end

# ouput the MPS based on the MERA gates
function correlation_matrix_to_mera(
  s::Vector{<:Index},
  Λ::AbstractMatrix;
  eigval_cutoff::Float64=1e-8,
  maxblocksize::Int=size(Λ, 1),
  kwargs...,
)
  @assert size(Λ, 1) == size(Λ, 2)
  ns, C = correlation_matrix_to_gmera(
    Λ; eigval_cutoff=eigval_cutoff, maxblocksize=maxblocksize
  )
  if all(hastags("Fermion"), s)
    U = [ITensor(s, g) for g in reverse(C.rotations)]
    ψ = MPS(s, n -> round(Int, ns[n]) + 1, U; kwargs...)
  elseif all(hastags("Electron"), s)
    isodd(length(s)) && error(
      "For Electron type, must have even number of sites of alternating up and down spins.",
    )
    N = length(s)
    if isspinful(s)
      error(
        "correlation_matrix_to_mps(Λ::AbstractMatrix) currently only supports spinless Fermions or Electrons that do not conserve Sz. Use correlation_matrix_to_mps(Λ_up::AbstractMatrix, Λ_dn::AbstractMatrix) to use spinful Fermions/Electrons.",
      )
    else
      sf = siteinds("Fermion", 2 * N; conserve_qns=true)
    end
    U = [ITensor(sf, g) for g in reverse(C.rotations)]
    ψf = MPS(sf, n -> round(Int, ns[n]) + 1, U; kwargs...)
    ψ = MPS(N)
    for n in 1:N
      i, j = 2 * n - 1, 2 * n
      C = combiner(sf[i], sf[j])
      c = combinedind(C)
      ψ[n] = ψf[i] * ψf[j] * C
      ψ[n] *= δ(dag(c), s[n])
    end
  else
    error("All sites must be Fermion or Electron type.")
  end
  return ψ
end

function slater_determinant_to_mera(s::Vector{<:Index}, Φ::AbstractMatrix; kwargs...)
  return correlation_matrix_to_mera(s, conj(Φ) * transpose(Φ); kwargs...)
end

# G the circuit from the gates, N is the total number of sites
function UmatFromGates(G::Circuit, N::Int)
  U = Matrix{Float64}(I, N, N)
  n = size(G.rotations, 1)
  for k in 1:n
    rot = G.rotations[k]
    U = rot * U
  end
  return U
end

# compute the energy of the state based on the gates
function EfromGates(H::Matrix{<:Number}, U::Matrix{<:Number})
  Htemp = U * H * U'
  Etot = 0
  N = size(U, 1)
  for i in 1:N
    if Htemp[i, i] < 0.0
      Etot += Htemp[i, i]
    end
  end
  return Etot
end


using MKLsparseinspector
using LinearAlgebra
using SparseArrays
using Test

using LinearAlgebra: BlasInt

@testset "MKLcsr" begin
    spm = sparse(BlasInt.(collect(1:6)), BlasInt[3,2,3,1,3,3], ones(6))
    csrpt = csrptr(spm')
    @test isa(csrpt, Ptr{MKLcsr{Float64}})
    @test SparseMatrixCSC(csrpt) == spm  # this may change
end


pts = invariantize([rand(3) for i = 1:15])
triang = triangulate(pts)

μ_exact = invariantmeasure(pts, TriangulationBinning(), ExactIntersection())
μ_approx = invariantmeasure(pts, TriangulationBinning(), ApproximateIntersection())

@test μ_exact isa TransferOperatorTriangulationExact
@test μ_approx isa TransferOperatorTriangulationApprox


"""
    VFE(fz::FiniteGP)

The "Variational Free Energy" sparse approximation [1], used to construct an
approximate posterior with inducing inputs `fz.x`. See [`posterior(v::VFE,
fx::FiniteGP, y::AbstractVector{<:Real})`](@ref) for a usage example.

[1] - M. K. Titsias. "Variational learning of inducing variables in sparse Gaussian
processes". In: Proceedings of the Twelfth International Conference on Artificial
Intelligence and Statistics. 2009.
"""
struct VFE{Tfz<:FiniteGP}
    fz::Tfz
end

const DTC = VFE

struct ApproxPosteriorGP{Tapprox,Tprior,Tdata} <: AbstractGP
    approx::Tapprox
    prior::Tprior
    data::Tdata
end

"""
    posterior(vfe::VFE, fx::FiniteGP, y::AbstractVector{<:Real})

Compute the optimal approximate posterior [1] over the process `f = fx.f`, given observations `y`
of `f` at `x`, and inducing points `vfe.fz.x`.

```jldoctest
julia> f = GP(Matern52Kernel());

julia> x = randn(1000);

julia> z = range(-5.0, 5.0; length=13);

julia> vfe = VFE(f(z));

julia> y = rand(f(x, 0.1));

julia> post = posterior(vfe, f(x, 0.1), y);

julia> post(z) isa AbstractGPs.FiniteGP
true
```

[1] - M. K. Titsias. "Variational learning of inducing variables in sparse Gaussian
processes". In: Proceedings of the Twelfth International Conference on Artificial
Intelligence and Statistics. 2009.
"""
function posterior(vfe::VFE, fx::FiniteGP, y::AbstractVector{<:Real})
    @assert vfe.fz.f === fx.f

    U_y = _cholesky(_symmetric(fx.Σy)).U
    U = cholesky(_symmetric(cov(vfe.fz))).U

    B_εf = U' \ (U_y' \ cov(fx, vfe.fz))'

    b_y = U_y' \ (y - mean(fx))

    D = B_εf * B_εf' + I
    Λ_ε = cholesky(_symmetric(D))

    m_ε = Λ_ε \ (B_εf * b_y)

    cache = (m_ε=m_ε, Λ_ε=Λ_ε, U=U, α=U \ m_ε, b_y=b_y, B_εf=B_εf, x=fx.x, Σy=fx.Σy)
    return ApproxPosteriorGP(vfe, fx.f, cache)
end

"""
    function update_posterior(
        f_post_approx::ApproxPosteriorGP{<:VFE},
        fx::FiniteGP,
        y::AbstractVector{<:Real}
    )

Update the `ApproxPosteriorGP` given a new set of observations. Here, we retain the same
set of pseudo-points.
"""
function update_posterior(
    f_post_approx::ApproxPosteriorGP{<:VFE}, fx::FiniteGP, y::AbstractVector{<:Real}
)
    @assert f_post_approx.prior === fx.f

    U = f_post_approx.data.U
    z = inducing_points(f_post_approx)

    U_y₂ = _cholesky(_symmetric(fx.Σy)).U

    temp = zeros(size(f_post_approx.data.Σy, 1), size(fx.Σy, 2))
    Σy = [f_post_approx.data.Σy temp; temp' fx.Σy]

    b_y = vcat(f_post_approx.data.b_y, U_y₂ \ (y - mean(fx)))

    B_εf₂ = U' \ (U_y₂' \ cov(fx.f, fx.x, z))'
    B_εf = hcat(f_post_approx.data.B_εf, B_εf₂)

    Λ_ε = f_post_approx.data.Λ_ε

    for col in eachcol(B_εf₂)
        lowrankupdate!(Λ_ε, col)
    end

    m_ε = Λ_ε \ (B_εf * b_y)
    α = U \ m_ε
    x = vcat(f_post_approx.data.x, fx.x)

    cache = (m_ε=m_ε, Λ_ε=Λ_ε, U=U, α=α, z=z, b_y=b_y, B_εf=B_εf, x=x, Σy=Σy)
    return ApproxPosteriorGP(f_post_approx.approx, fx.f, cache)
end

"""
    function update_posterior(
        f_post_approx::ApproxPosteriorGP{<:VFE},
        z::FiniteGP,
    )

Update the `ApproxPosteriorGP` given a new set of pseudo-points to append to the existing
set of pseudo-points.
"""
function update_posterior(f_post_approx::ApproxPosteriorGP{<:VFE}, fz::FiniteGP)
    @assert f_post_approx.prior === fz.f

    z_old = inducing_points(f_post_approx)
    z = fz.x

    U11 = f_post_approx.data.U
    C12 = cov(f_post_approx.prior, z_old, z)
    C22 = _symmetric(cov(f_post_approx.prior, z))
    U = update_chol(Cholesky(U11, 'U', 0), C12, C22).U
    U22 = U[(end - length(z) + 1):end, (end - length(z) + 1):end]
    U12 = U[1:length(z_old), (end - length(z) + 1):end]

    B_εf₁ = f_post_approx.data.B_εf

    Cu1f = cov(f_post_approx.prior, z_old, f_post_approx.data.x)
    Cu2f = cov(f_post_approx.prior, z, f_post_approx.data.x)

    U_y = _cholesky(_symmetric(f_post_approx.data.Σy)).U

    B_εf₂ = U22' \ (Cu2f * inv(U_y) - U12' * B_εf₁)
    B_εf = vcat(B_εf₁, B_εf₂)

    Λ_ε = update_chol(f_post_approx.data.Λ_ε, B_εf₁ * B_εf₂', B_εf₂ * B_εf₂' + I)

    m_ε = Λ_ε \ (B_εf * f_post_approx.data.b_y)

    α = U \ m_ε

    z_new = vcat(z_old, z)
    vfe = f_post_approx.approx
    fz_new = vfe.fz.f(z_new, vfe.fz.Σy)

    cache = (
        m_ε=m_ε,
        Λ_ε=Λ_ε,
        U=U,
        α=α,
        b_y=f_post_approx.data.b_y,
        B_εf=B_εf,
        x=f_post_approx.data.x,
        Σy=f_post_approx.data.Σy,
    )
    return ApproxPosteriorGP(VFE(fz_new), f_post_approx.prior, cache)
end

# AbstractGP interface implementation.

function Statistics.mean(f::ApproxPosteriorGP{<:VFE}, x::AbstractVector)
    return mean(f.prior, x) + cov(f.prior, x, inducing_points(f)) * f.data.α
end

function Statistics.cov(f::ApproxPosteriorGP{<:VFE}, x::AbstractVector)
    A = f.data.U' \ cov(f.prior, inducing_points(f), x)
    return cov(f.prior, x) - At_A(A) + Xt_invA_X(f.data.Λ_ε, A)
end

function Statistics.var(f::ApproxPosteriorGP{<:VFE}, x::AbstractVector)
    A = f.data.U' \ cov(f.prior, inducing_points(f), x)
    return var(f.prior, x) - diag_At_A(A) + diag_Xt_invA_X(f.data.Λ_ε, A)
end

function Statistics.cov(f::ApproxPosteriorGP{<:VFE}, x::AbstractVector, y::AbstractVector)
    A_zx = f.data.U' \ cov(f.prior, inducing_points(f), x)
    A_zy = f.data.U' \ cov(f.prior, inducing_points(f), y)
    return cov(f.prior, x, y) - A_zx'A_zy + Xt_invA_Y(A_zx, f.data.Λ_ε, A_zy)
end

function StatsBase.mean_and_cov(f::ApproxPosteriorGP{<:VFE}, x::AbstractVector)
    A = f.data.U' \ cov(f.prior, inducing_points(f), x)
    m_post = mean(f.prior, x) + A' * f.data.m_ε
    C_post = cov(f.prior, x) - At_A(A) + Xt_invA_X(f.data.Λ_ε, A)
    return m_post, C_post
end

function StatsBase.mean_and_var(f::ApproxPosteriorGP{<:VFE}, x::AbstractVector)
    A = f.data.U' \ cov(f.prior, inducing_points(f), x)
    m_post = mean(f.prior, x) + A' * f.data.m_ε
    c_post = var(f.prior, x) - diag_At_A(A) + diag_Xt_invA_X(f.data.Λ_ε, A)
    return m_post, c_post
end

inducing_points(f::ApproxPosteriorGP{<:VFE}) = f.approx.fz.x

"""
    elbo(vfe::VFE, fx::FiniteGP, y::AbstractVector{<:Real})

The Titsias Evidence Lower BOund (ELBO) [1]. `y` are observations of `fx`, and `v.z`
are inducing points.

```jldoctest
julia> f = GP(Matern52Kernel());

julia> x = randn(1000);

julia> z = range(-5.0, 5.0; length=13);

julia> v = VFE(f(z));

julia> y = rand(f(x, 0.1));

julia> elbo(v, f(x, 0.1), y) < logpdf(f(x, 0.1), y)
true
```

[1] - M. K. Titsias. "Variational learning of inducing variables in sparse Gaussian
processes". In: Proceedings of the Twelfth International Conference on Artificial
Intelligence and Statistics. 2009.
"""
function elbo(vfe::VFE, fx::FiniteGP, y::AbstractVector{<:Real})
    @assert vfe.fz.f === fx.f
    _dtc, A = _compute_intermediates(fx, y, vfe.fz)
    return _dtc - (tr_Cf_invΣy(fx, fx.Σy) - sum(abs2, A)) / 2
end

"""
    dtc(v::VFE, fx::FiniteGP, y::AbstractVector{<:Real})

The Deterministic Training Conditional (DTC) [1]. `y` are observations of `fx`, and `v.z`
are inducing points.

```jldoctest
julia> f = GP(Matern52Kernel());

julia> x = randn(1000);

julia> z = range(-5.0, 5.0; length=256);

julia> v = VFE(f(z));

julia> y = rand(f(x, 0.1));

julia> isapprox(dtc(v, f(x, 0.1), y), logpdf(f(x, 0.1), y); atol=1e-6, rtol=1e-6)
true
```

[1] - M. Seeger, C. K. I. Williams and N. D. Lawrence. "Fast Forward Selection to Speed Up
Sparse Gaussian Process Regression". In: Proceedings of the Ninth International Workshop on
Artificial Intelligence and Statistics. 2003
"""
function dtc(vfe::VFE, fx::FiniteGP, y::AbstractVector{<:Real})
    @assert vfe.fz.f === fx.f
    _dtc, _ = _compute_intermediates(fx, y, vfe.fz)
    return _dtc
end

# Factor out computations common to the `elbo` and `dtc`.
function _compute_intermediates(fx::FiniteGP, y::AbstractVector{<:Real}, fz::FiniteGP)
    consistency_check(fx, y)
    chol_Σy = _cholesky(fx.Σy)

    A = cholesky(_symmetric(cov(fz))).U' \ (chol_Σy.U' \ cov(fx, fz))'
    Λ_ε = cholesky(Symmetric(A * A' + I))
    δ = chol_Σy.U' \ (y - mean(fx))

    tmp = logdet(chol_Σy) + logdet(Λ_ε) + sum(abs2, δ) - sum(abs2, Λ_ε.U' \ (A * δ))
    _dtc = -(length(y) * typeof(tmp)(log2π) + tmp) / 2
    return _dtc, A
end

function consistency_check(fx, y)
    @assert length(fx) == length(y)
end

function tr_Cf_invΣy(f::FiniteGP, Σy::Diagonal)
    return sum(var(f.f, f.x) ./ diag(Σy))
end