text:
#=
backtrack.jl

    Backtracking line search algorithms based on the Armijo or sufficient
    decrease condition to determine the optimal step length, using either no,
    quadratic, or cubic safeguarded interpolation.

@author: Quint Wiersma <q.wiersma@vu.nl>

@date: 2022/02/02
=#

# Backtracking struct
Base.@kwdef mutable struct BackTrack{Tf, Gf, Ti}
    α::Tf	            # stepsize
	α_init::Tf 	        # initial stepsize
    ρ::Gf = .5		    # contraction factor
    c_1::Tf = 1e-4      # sufficient decrease constant
    order::Ti = 0       # interpolation order
    ρ_hi::Gf = .5       # safeguard upper bound
    ρ_lo::Gf = .1       # safeguard lower bound
    max_iter::Ti = 1000 # max iterations
end

"""
    fixed!(ls, x, x_prev, p, f_prev, ∇f_prev, f)

Backtracking line search wtih a fixed contraction parameter ``ρ`` to find ``α``,
storing the result in `x`.

#### Arguments
  - `ls::BackTrack`             : line search parameters
  - `x_prev::AbstractVector`    : previous state
  - `p::AbstractVector`         : search direction
  - `f_prev::Real`              : previous objective function value
  - `∇f_prev::AbstractVector`   : previous gradient
  - `f::Function`               : ``f(x)``

#### Returns
  - `x::AbstractVector` : updated state
  - `f_new::Real`       : objective function value
"""
function fixed!(ls::BackTrack, x::AbstractVector, x_prev::AbstractVector,
                p::AbstractVector, f_prev::Real, ∇f_prev::AbstractVector,
                f::Function)
    # Unpack
    α= ls.α_init
    c_1= ls.c_1
    ρ= ls.ρ
    max_iter= ls.max_iter

    # Objective function
    x.= x_prev .+ α .* p
    f_new= f(x)

    # Directional derivative
    d= dot(∇f_prev, p)

    # Iteration counter
    iter= 1
    # Backtracking
    while f_new > f_prev + c_1 * α * d && iter < max_iter
        # Contract
        α*= ρ

        # Update objective function
        x.= x_prev .+ α .* p
        f_new= f(x)

        # Update iteration counter
        iter+= 1
    end

    # Store stepsize
    ls.α= α

    return f_new
end

"""
    quad_interp!(ls, x, x_prev, p, f_prev, ∇f_prev, f)

Backtracking line search wtih  quadratic interpolation to find ``α``, storing
the result in `x`.

#### Arguments
  - `ls::BackTrack`             : line search parameters
  - `x_prev::AbstractVector`    : previous state
  - `p::AbstractVector`         : search direction
  - `f_prev::Real`              : previous objective function value
  - `∇f_prev::AbstractVector`   : previous gradient
  - `f::Function`               : ``f(x)``

#### Returns
  - `x::AbstractVector` : updated state
  - `f_new::Real`       : objective function value
"""
function quad_interp!(ls::BackTrack, x::AbstractVector, x_prev::AbstractVector,
                        p::AbstractVector, f_prev::Real, ∇f_prev::AbstractVector,
                        f::Function)
    # Unpack
    α_1= ls.α_init
    c_1= ls.c_1
    ρ_hi, ρ_lo= ls.ρ_hi, ls.ρ_lo
    max_iter= ls.max_iter

    # Objective function
    x.= x_prev .+ α_1 .* p
    f_new= f(x)

    # Directional derivative
    d= dot(∇f_prev, p)

    # Iteration counter
    iter= 1
    # Backtracking
    while f_new > f_prev + c_1 * α_1 * d && iter < max_iter
        # Quadratic interpolation using f(x), ∇f(x)'p, and f(x + α_1 * p)
        α_tmp= -d * α_1^2 * inv(2 * (f_new - f_prev - d * α_1))

        # Safeguard
        α_tmp= min(α_tmp, ρ_hi * α_1)   # avoid too small reductions
        α_1= max(α_tmp, ρ_lo * α_1) # avoid too big reductions

        # Update objective function
        x.= x_prev .+ α_1 .* p
        f_new= f(x)

        # Update iteration counter
        iter+= 1
    end

    # Store stepsize
    ls.α= α_1

    return f_new
end

"""
    cubic_interp!(ls, x, x_prev, p, f_prev, ∇f_prev, f)

Backtracking line search wtih cubic interpolation to find ``α``, storing the
result in `x`.

#### Arguments
  - `ls::BackTrack`             : line search parameters
  - `x_prev::AbstractVector`    : previous state
  - `p::AbstractVector`         : search direction
  - `f_prev::Real`              : previous objective function value
  - `∇f_prev::AbstractVector`   : previous gradient
  - `f::Function`               : ``f(x)``

#### Returns
  - `x::AbstractVector` : updated state
  - `f_new::Real`       : objective function value
"""
function cubic_interp!(ls::BackTrack, x::AbstractVector, x_prev::AbstractVector,
                        p::AbstractVector, f_prev::Real, ∇f_prev::AbstractVector,
                        f::Function)
    # Infer type
    Tα= typeof(ls.α_init)

    # Unpack
    α_2, α_1= ls.α_init, ls.α_init
    c_1= ls.c_1
    ρ_hi, ρ_lo= ls.ρ_hi, ls.ρ_lo
    max_iter= ls.max_iter

    # Objective function values
    x.= x_prev .+ α_2 .* p
    f_new= f(x)
    f_tmp= f_new

    # Directional derivative
    d= dot(∇f_prev, p)

    # Iteration counter
    iter= 1
    # Backtracking
    while f_new > f_prev + c_1 * α_2 * d && iter < max_iter
        if iter == 1
            # Quadratic interpolation using f(x), ∇f(x)'p, and f(x + α_2 * p)
            α_tmp= -d * α_2^2 * inv(2 * (f_new - f_prev - d * α_2))
        else
            # Cubic interpolation using f(x), ∇f(x)'p, f(x + α_1 * p), and f(x + α_2 * p)
            div= inv(α_1^2 * α_2^2 * (α_2 - α_1))
            tmp_2= f_new - f_prev - d * α_2
            tmp_1= f_tmp - f_prev - d * α_1
            a= (α_1^2 * tmp_2  - α_2^2 * tmp_1) * div
            b= (-α_1^3 * tmp_2  + α_2^3 * tmp_1) * div

            if isapprox(a, zero(a), atol=eps(Tα))
                # approximate quadratic interpolation
                α_tmp= -d * inv(2 * b)
            else
                # discriminant
                D= max(b^2 - 3 * a * d, Tα(0))
                # quadratic equation root
                α_tmp= (-b + sqrt(D)) * inv(3 * a)
            end
        end

        # Store stepsize
        α_1= α_2

        # Safeguard
        α_tmp= min(α_tmp, ρ_hi * α_2)   # avoid too small reductions
        α_2= max(α_tmp, ρ_lo * α_2) # avoid too big reductions

        # Update objective function values
        x.= x_prev .+ α_2 .* p
        f_tmp, f_new= f_new, f(x)

        # Update iteration counter
        iter+= 1
    end

    # Store stepsize
    ls.α= α_2

    return f_new
end

"""
    backtrack!(ls, x, x_prev, p, f_prev, ∇f_prev, f)

Backtracking line search to find optimal step size ``α``, storing the result in
`x`.

#### Arguments
  - `ls::BackTrack`             : line search parameters
  - `x_prev::AbstractVector`    : previous state
  - `p::AbstractVector`         : search direction
  - `f_prev::Real`              : previous objective function value
  - `∇f_prev::AbstractVector`   : previous gradient
  - `f::Function`               : ``f(x)``

#### Returns
  - `x::AbstractVector` : updated state
  - `f_new::Real`       : objective function value
"""
function backtrack!(ls::BackTrack, x::AbstractVector, x_prev::AbstractVector,
                    p::AbstractVector, f_prev::Real, ∇f_prev::AbstractVector,
                    f::Function)
    # Check interpolation
    if ls.order == 0
        # Fixed backtracking
        f_new= fixed!(ls, x, x_prev, p, f_prev, ∇f_prev, f)
    elseif ls.order == 2
        # Quadratic interpolation
        f_new= quad_interp!(ls, x, x_prev, p, f_prev, ∇f_prev, f)
    elseif ls.order == 3
        # Cubic interpolation
        f_new= cubic_interp!(ls, x, x_prev, p, f_prev, ∇f_prev, f)
    end

    return f_new
end


text:
# For some reason if these aren't defined here, then they are interpreted as closures
futestkws(x; err = true) = err ? error() : x

fbtestkws(x, y; err = true) = err ? error() : x

sinconj(x) = sin(x)

primalapprox(x) = x

@testset "testers.jl" begin
    @testset "test_scalar" begin
        @testset "Ensure correct rules succeed" begin
            double(x) = 2x
            @scalar_rule(double(x), 2)
            test_scalar(double, 2.1)
        end
        @testset "Ensure incorrect rules caught" begin
            alt_double(x) = 2x
            @scalar_rule(alt_double(x), 3)  # this is wrong, on purpose
            @test fails(()->test_scalar(alt_double, 2.1))
        end
    end

    @testset "unary: identity(x)" begin
        function ChainRulesCore.frule((_, ẏ), ::typeof(identity), x)
            return x, ẏ
        end
        function ChainRulesCore.rrule(::typeof(identity), x)
            function identity_pullback(ȳ)
                return (NO_FIELDS, ȳ)
            end
            return x, identity_pullback
        end
        @testset "frule_test" begin
            frule_test(identity, (randn(), randn()))
            frule_test(identity, (randn(4), randn(4)))
        end
        @testset "rrule_test" begin
            rrule_test(identity, randn(), (randn(), randn()))
            rrule_test(identity, randn(4), (randn(4), randn(4)))
        end
    end

    @testset "Inplace accumulation: identity on Array" begin
        @testset "Correct definitions" begin
            local inplace_used
            function ChainRulesCore.frule((_, ẋ), ::typeof(identity), x::Array)
                ẏ = InplaceableThunk(@thunk(ẋ), ȧ -> (inplace_used=true; ȧ .+= ẋ))
                return identity(x), ẏ
            end
            function ChainRulesCore.rrule(::typeof(identity), x::Array)
                function identity_pullback(ȳ)
                    x̄_ret = InplaceableThunk(@thunk(ȳ), ā -> (inplace_used=true; ā .+= ȳ))
                    return (NO_FIELDS, x̄_ret)
                end
                return identity(x), identity_pullback
            end

            inplace_used = false
            frule_test(identity, (randn(4), randn(4)))
            @test inplace_used  # make sure we are using, and thus testing the add!

            inplace_used = false
            rrule_test(identity, randn(4), (randn(4), randn(4)))
            @test inplace_used  # make sure we are using, and thus testing the add!
        end

        @testset "Incorrect in-place definitions" begin
            my_identity(value) = value  # we will define bad rules on this
            function ChainRulesCore.frule((_, ẋ), ::typeof(my_identity), x::Array)
                # only the in-place part is incorrect
                ẏ = InplaceableThunk(@thunk(ẋ), ȧ -> ȧ .+= 200 .* ẋ)
                return my_identity(x), ẏ
            end
            function ChainRulesCore.rrule(::typeof(my_identity), x::Array)
                x_dims = size(x)
                function my_identity_pullback(ȳ)
                    # only the in-place part is incorrect
                    x̄_ret = InplaceableThunk(@thunk(ȳ), ā -> ā .+= 200 .* ȳ)
                    return (NO_FIELDS, x̄_ret)
                end
                return my_identity(x), my_identity_pullback
            end
            @test fails(()->frule_test(my_identity, (randn(4), randn(4))))
            @test fails(()->rrule_test(my_identity, randn(4), (randn(4), randn(4))))
        end
    end

    @testset "test derivative conjugated in pullback" begin
        ChainRulesCore.frule((_, Δx), ::typeof(sinconj), x) = (sin(x), cos(x) * Δx)

        # define rrule using ChainRulesCore's v0.9.0 convention, conjugating the derivative
        # in the rrule
        function ChainRulesCore.rrule(::typeof(sinconj), x)
            sinconj_pullback(ΔΩ) = (NO_FIELDS, conj(cos(x)) * ΔΩ)
            return sin(x), sinconj_pullback
        end

        rrule_test(sinconj, randn(ComplexF64), (randn(ComplexF64), randn(ComplexF64)))
        test_scalar(sinconj, randn(ComplexF64))
    end

    @testset "binary: fst(x, y)" begin
        fst(x, y) = x
        ChainRulesCore.frule((_, dx, dy), ::typeof(fst), x, y) = (x, dx)
        function ChainRulesCore.rrule(::typeof(fst), x, y)
            function fst_pullback(Δx)
                return (NO_FIELDS, Δx, Zero())
            end
            return x, fst_pullback
        end
        @testset "frule_test" begin
            frule_test(fst, (2, 4.0), (3, 5.0))
            frule_test(fst, (randn(4), randn(4)), (randn(4), randn(4)))
        end
        @testset "rrule_test" begin
            rrule_test(fst, rand(), (2.0, 4.0), (3.0, 5.0))
            rrule_test(fst, randn(4), (randn(4), randn(4)), (randn(4), randn(4)))
        end
    end

    @testset "single input, multiple output" begin
        simo(x) = (x, 2x)
        function ChainRulesCore.rrule(simo, x)
            simo_pullback((a, b)) = (NO_FIELDS, a .+ 2 .* b)
            return simo(x), simo_pullback
        end
        function ChainRulesCore.frule((_, ẋ), simo, x)
            y = simo(x)
            return y, Composite{typeof(y)}(ẋ, 2ẋ)
        end

        @testset "frule_test" begin
            frule_test(simo, (randn(), randn()))  # on scalar
            frule_test(simo, (randn(4), randn(4)))  # on array
        end
        @testset "rrule_test" begin
            # note: we are pulling back tuples (could use Composites here instead)
            rrule_test(simo, (randn(), rand()), (randn(), randn()))  # on scalar
            rrule_test(simo, (randn(4), rand(4)), (randn(4), randn(4))) # on array
        end
    end

    @testset "tuple input: first" begin
        ChainRulesCore.frule((_, dx), ::typeof(first), xs::Tuple) = (first(xs), first(dx))
        function ChainRulesCore.rrule(::typeof(first), x::Tuple)
            function first_pullback(Δx)
                return (NO_FIELDS, Composite{typeof(x)}(Δx, falses(length(x)-1)...))
            end
            return first(x), first_pullback
        end

        CTuple{N} = Composite{NTuple{N, Float64}}  # shorter for testing
        @testset "frule_test" begin
            frule_test(first, ((2.0, 3.0), CTuple{2}(4.0, 5.0)))
            frule_test(first, (Tuple(randn(4)), CTuple{4}(randn(4)...)))
        end
        @testset "rrule_test" begin
            rrule_test(first, 2.0, ((2.0, 3.0), CTuple{2}(4.0, 5.0)))
            rrule_test(first, randn(), (Tuple(randn(4)), CTuple{4}(randn(4)...)))
        end
    end

    @testset "ignoring arguments" begin
        fsymtest(x, s::Symbol) = x
        ChainRulesCore.frule((_, Δx, _), ::typeof(fsymtest), x, s) = (x, Δx)
        function ChainRulesCore.rrule(::typeof(fsymtest), x, s)
            function fsymtest_pullback(Δx)
                return NO_FIELDS, Δx, DoesNotExist()
            end
            return x, fsymtest_pullback
        end

        @testset "frule_test" begin
            frule_test(fsymtest, (randn(), randn()), (:x, nothing))
        end

        @testset "rrule_test" begin
            rrule_test(fsymtest, randn(), (randn(), randn()), (:x, nothing))
        end
    end

    @testset "unary with kwargs: futestkws(x; err)" begin
        function ChainRulesCore.frule((_, ẋ), ::typeof(futestkws), x; err = true)
            return futestkws(x; err = err), ẋ
        end
        function ChainRulesCore.rrule(::typeof(futestkws), x; err = true)
            function futestkws_pullback(Δx)
                return (NO_FIELDS, Δx)
            end
            return futestkws(x; err = err), futestkws_pullback
        end

        # we defined these functions at top of file to throw errors unless we pass `err=false`
        @test_throws ErrorException futestkws(randn())
        @test_throws ErrorException test_scalar(futestkws, randn())
        @test_throws ErrorException frule((nothing, randn()), futestkws, randn())
        @test_throws ErrorException rrule(futestkws, randn())

        @test_throws ErrorException futestkws(randn(4))
        @test_throws ErrorException frule((nothing, randn(4)), futestkws, randn(4))
        @test_throws ErrorException rrule(futestkws, randn(4))

        @testset "scalar_test" begin
            test_scalar(futestkws, randn(); fkwargs=(; err = false))
        end
        @testset "frule_test" begin
            frule_test(futestkws, (randn(), randn()); fkwargs=(; err = false))
            frule_test(futestkws, (randn(4), randn(4)); fkwargs=(; err = false))
        end
        @testset "rrule_test" begin
            rrule_test(futestkws, randn(), (randn(), randn()); fkwargs=(; err = false))
            rrule_test(futestkws, randn(4), (randn(4), randn(4)); fkwargs=(; err = false))
        end
    end

    @testset "binary with kwargs: fbtestkws(x, y; err)" begin
        function ChainRulesCore.frule((_, ẋ, _), ::typeof(fbtestkws), x, y; err = true)
            return fbtestkws(x, y; err = err), ẋ
        end
        function ChainRulesCore.rrule(::typeof(fbtestkws), x, y; err = true)
            function fbtestkws_pullback(Δx)
                return (NO_FIELDS, Δx, Zero())
            end
            return fbtestkws(x, y; err = err), fbtestkws_pullback
        end

        # we defined these functions at top of file to throw errors unless we pass `err=false`
        @test_throws ErrorException fbtestkws(randn(), randn())
        @test_throws ErrorException frule((nothing, randn(), nothing), fbtestkws, randn(), randn())
        @test_throws ErrorException rrule(fbtestkws, randn(), randn())

        @test_throws ErrorException fbtestkws(randn(4), randn(4))
        @test_throws ErrorException frule((nothing, randn(4), nothing), fbtestkws, randn(4), randn(4))
        @test_throws ErrorException rrule(fbtestkws, randn(4), randn(4))

        @testset "frule_test" begin
            frule_test(fbtestkws, (randn(), randn()), (randn(), randn()); fkwargs=(; err = false))
            frule_test(fbtestkws, (randn(4), randn(4)), (randn(4), randn(4)); fkwargs=(; err = false))
        end
        @testset "rrule_test" begin
            rrule_test(fbtestkws, randn(), (randn(), randn()), (randn(), randn()); fkwargs=(; err = false))
            rrule_test(fbtestkws, randn(4), (randn(4), randn(4)), (randn(4), randn(4)); fkwargs=(; err = false))
        end
    end

    @testset "primal can be only approximately equal" begin
        ChainRulesCore.frule((_, Δx), ::typeof(primalapprox), x) = (x + sqrt(eps(x)), Δx)

        function ChainRulesCore.rrule(::typeof(primalapprox), x)
            function primalapprox_pullback(Δx)
                return (NO_FIELDS, Δx)
            end
            return x + sqrt(eps(x)), primalapprox_pullback
        end

        frule_test(primalapprox, (randn(), randn()); atol = 1e-6)
        rrule_test(primalapprox, randn(), (randn(), randn()); atol = 1e-6)
    end

    @testset "TestIterator input" begin
        function iterfun(iter)
            state = iterate(iter)
            state === nothing && error()
            (x, i) = state
            s = x^2
            while true
                state = iterate(iter, i)
                state === nothing && break
                (x, i) = state
                s += x^2
            end
            return s
        end

        function ChainRulesCore.frule((_, Δiter), ::typeof(iterfun), iter)
            iter_Δiter = zip(iter, Δiter)
            state = iterate(iter_Δiter)
            state === nothing && error()
            # for some reason the following line errors if the frule is defined within a testset
            ((x, Δx), i) = state
            return iterfun(iter), sum(2 .* iter.data .* Δiter.data)
            s = x^2
            ∂s = 2 * x * Δx
            while true
                state = iterate(iter_Δiter, i)
                state === nothing && break
                ((x, Δx), i) = state
                s += x^2
                ∂s += 2 * x * Δx
            end
            return s, ∂s
        end

        function ChainRulesCore.rrule(::typeof(iterfun), iter::TestIterator)
            function iterfun_pullback(Δs)
                data = iter.data
                ∂data = (2 * Δs) .* conj.(data)
                ∂iter = TestIterator(
                    ∂data,
                    Base.IteratorSize(iter),
                    Base.IteratorEltype(iter),
                )
                return (NO_FIELDS, ∂iter)
            end
            return iterfun(iter), iterfun_pullback
        end

        # This needs to be in a separate testet to stop the `x` being shared with `iterfun`
        @testset "Testing iterator function" begin
            x = TestIterator(randn(2, 3), Base.SizeUnknown(), Base.EltypeUnknown())
            ẋ = TestIterator(randn(2, 3), Base.SizeUnknown(), Base.EltypeUnknown())
            x̄ = TestIterator(randn(2, 3), Base.SizeUnknown(), Base.EltypeUnknown())

            frule_test(iterfun, (x, ẋ))
            rrule_test(iterfun, randn(), (x, x̄))
        end
    end

    @testset "unhappy path" begin
        @testset "primal wrong" begin
            my_identity1(x) = x
            function ChainRulesCore.frule((_, ẏ), ::typeof(my_identity1), x)
                return 2.5 * x, ẏ
            end
            function ChainRulesCore.rrule(::typeof(my_identity1), x)
                function identity_pullback(ȳ)
                    return (NO_FIELDS, ȳ)
                end
                return 2.5 * x, identity_pullback
            end

            @test fails(()->frule_test(my_identity1, (2.2, 3.3)))
            @test fails(()->rrule_test(my_identity1, 4.1, (2.2, 3.3)))
        end

        @testset "derivative wrong" begin
            my_identity2(x) = x
            function ChainRulesCore.frule((_, ẏ), ::typeof(my_identity2), x)
                return x, 2.7 * ẏ
            end
            function ChainRulesCore.rrule(::typeof(my_identity2), x)
                function identity_pullback(ȳ)
                    return (NO_FIELDS, 31.8 * ȳ)
                end
                return x, identity_pullback
            end

            @test fails(()->frule_test(my_identity2, (2.2, 3.3)))
            @test fails(()->rrule_test(my_identity2, 4.1, (2.2, 3.3)))
        end
    end
end

ext: jl


text:
"""
Standard basic quantum circuit simulator building blocks.

This is a component package for [Yao.jl](https://github.com/QuantumBFS/Yao.jl). It contains the abstract definitions and basic implementation of Yao's circuit building blocks.
"""
module YaoBlocks

using LinearAlgebra

include("utils.jl")
# include("traits.jl")

include("abstract_block.jl")

# concrete blocks
include("routines.jl")
include("primitive/primitive.jl")
include("composite/composite.jl")

include("algebra.jl")
include("blocktools.jl")
include("measure_ops.jl")
include("layout.jl")
include("treeutils/treeutils.jl")

include("autodiff/autodiff.jl")
export AD, Optimise

include("deprecations.jl")

end # YaoBlocks


text:
"""
    eom_ar1(x, p, n) -> Function

Equations of motion for a bivariate, order one autoregressive model, where
x -> y [1].

# References
1. Paluš, M., Krakovská, A., Jakubík, J., & Chvosteková, M. (2018). Causality,
dynamical systems and the arrow of time. Chaos: An Interdisciplinary Journal of
Nonlinear Science, 28(7), 075307. http://doi.org/10.1063/1.5019944
"""
function eom_ar1(x, p, n)
    a₁, b₁, c₁, σ = (p...,)
    x, y = (x...,)
    ξ₁ = rand(Normal(0, σ))
    ξ₂ = rand(Normal(0, σ))

    dx = a₁*x + ξ₁
    dy = b₁*y + c₁*x + ξ₂
    return SVector{2}(dx, dy)
end

function ar1(uᵢ, a₁, b₁, c₁, σ)
    p = [a₁, b₁, c₁, σ]
    return DiscreteDynamicalSystem(eom_ar1, uᵢ, p)
end

"""
    ar1(uᵢ, a₁ = 0.90693, b₁ = 0.40693, c₁ = 0.5, σ = 0.40662) -> DiscreteDynamicalSystem

Initialise a discrete vector autoregressive system where x → y
"""
ar1(;uᵢ = rand(2), a₁ = 0.90693, b₁ = 0.40693, c₁ = 0.5, σ = 0.40662) =
    ar1(uᵢ, a₁, b₁, c₁, σ)


text:
function test_emptyX_Adam()
    X = emptyX_Adam()
    @assert size(X.X) == (22, 1)
    @assert size(X.pit) == (3,)
    @assert isapprox(X.V, 0)
    @assert isapprox(X.ageInDays, 0)
    @assert isapprox(X.alpha1, 0)
    @assert isapprox(X.alpha2, 0)
end

test_emptyX_Adam()

params = make_parameters_adam()

# Test at the ar2pr level.
pr_low = ar2pr_Adam(0.1, params)
@test pr_low < 0.5
pr_high = ar2pr_Adam(0.9, params)
@test pr_high > 0.5


text:
"""
    PoreMatModGO()

Launches the GUI Pluto notebook.
"""
function PoreMatModGO()
    # notebook path
    pmmg_ntbk = joinpath(pathof(PoreMatMod), "..", "PoreMatModGO.jl")
    # run the notebook in Pluto
    Pluto.run(notebook=pmmg_ntbk)
end

"""
    banner()

Displays the ASCII banner for the package using FIGlet
"""
banner() = FIGlet.render("PoreMatMod.jl", FIGlet.availablefonts()[35])


text:
using LGravity
using Test

# @testset "LGravity.jl" begin
#     # Write your tests here.
# end

# I can have two testsets within a testset. i don't usually create tests for type definitions. If I wanted to do that I would use the License
#  richness = lexicalrichness
# @test isa(richness, RichnessBundle)
# @testset "collector.jl" begin
#     @testset "first" begin
#         richness = lexicalrichness
#         @test
#     end
#     @testset "second" begin

#     end
# end


text:
module FranklinContent

using FranklinUtils
using Franklin; const fr = Franklin;
using DataStructures:DefaultDict
using Franklin: convert_md, convert_html, pagevar, globvar, locvar, path, refstring
using Dates: DateFormat, Date
using Memoization
using Gumbo: HTMLElement, getattr

const posts_date_format = DateFormat("mm/dd/yyyy")

macro unimp(fname)
    quote
        function $(esc(fname))()
            throw("unimplemented")
        end
    end
end

@doc "Wrapper for Franklin.locvar."
function hfun_locvar(args)
	name = args[1];
    locvar(Symbol(name))
end

@doc "Text wrapped in an HTML tag with a specified color."
function hfun_color(args)
    txt = args[1]
    color = args[2]
    return "$txt"
end

@doc "Prepend `website_url` to PATH."
@memoize function website_url(path)
    joinpath(globvar(:website_url), path)
end

@doc "Process a franklin markdown page and return the output HTML."
function page_content(loc::String)
    raw = read((path(:folder) * "/" * loc * ".md"), String)
    m = convert_md(raw; isinternal=true)
    # remove all `{{}}` functions
    m = replace(m, r"{{.*?}}" => "")
    convert_html(m)
end

@doc "The page description, otherwise the website description."
function hfun_page_desc()
	desc = locvar(:rss_description)
    if desc === ""
        return globvar(:website_description)
    end
    desc
end

@doc "All the posts files (.md) in the `:posts_path` directory."
function iter_posts()
    posts_list = globvar(:posts_path) |> dirname |> basename |> readdir
    filter!(f -> endswith(f, ".md") && f != "index.md" && !startswith(f, "."), posts_list)
    return posts_list
end

@doc "Convert post date string to date"
@inline function post_date(post)
    @debug @assert post.date !== nothing
    Date(post.date, posts_date_format)
end

@doc "HTML list of recent posts.
arg1: the root path of all the posts
arg2: how many posts to display"
function hfun_recent_posts(m::Vector{String})
    @assert length(m) < 3 "only two arguments allowed for recent posts (the number of recent posts to pull and the path)"
    n = parse(Int64, m[1])
    posts_path = length(m) == 1 ? "posts/" : m[2]
    list = readdir(dirname(posts_path))
    filter!(f -> endswith(f, ".md") && f != "index.md" && !startswith(f, "."), list)
    markdown = ""
    posts = []
    for (_, post) in enumerate(list)
        fi = posts_path * splitext(post)[1]
        # FIXME: https://github.com/tlienart/Franklin.jl/issues/891#issuecomment-932033412
        fr.PAGEVAR_DEPTH[] = 0
        push!(
            posts,
            (
                title = pagevar(fi, :title),
                link = fi,
                date = pagevar(fi, :date),
                description = pagevar(fi, :rss_description),
            ),
        )
    end
    # pull all posts if n <= 0

    n = n >= 0 ? n : length(posts) + 1
    for ele in
        view(sort(posts, by=x -> post_date(x), rev=true), 1:min(length(posts), n))
        markdown *= "* [($(ele.date)) $(ele.title)](../$(ele.link)) -- _$(ele.description)_ \n"
    end

    return fd2html(markdown, internal=true)
end

function tags_sorter(p)
    pvd = pagevar(p, "date")
    if isnothing(pvd)
        return Date(Dates.unix2datetime(stat(p * ".md").ctime))
    end
    return pvd
end

function hfun_taglist_desc(tags::AbstractVector)
    hfun_taglist_desc(tags[1])
end

@doc "All the pages for a particular tag."
function hfun_taglist_desc(tag::Union{Nothing,AbstractString}=nothing)
    if isnothing(tag)
        tag = locvar(:fd_tag)
        if isnothing(tag)
            throw("need a tag")
        end
    end

    c = IOBuffer()
    write(c, "
")

    all_tags = globvar(:fd_tag_pages)
    # tags have yet to be processed
    if isnothing(all_tags)
        all_tags = fr.invert_dict(globvar(:fd_page_tags))
    end
    tag ∉ keys(all_tags) && begin
        @warn "tag: $tag not found"
        return ""
    end
    rpaths = all_tags[tag]
    sort!(rpaths, by=tags_sorter, rev=false)

    for rpath in rpaths
        title = pagevar(rpath, "title")
        if isnothing(title)
            title = "/$rpath/"
        end
        url = get_url(rpath)
        desc = pagevar(rpath, "rss_description")
        write(c, "
$title -- $desc
")
    end
    write(c, "
")
    html = String(take!(c))
    close(c)
    return html
end

@doc "The base font size for tags in the tags cloud (rem)."
const tag_cloud_font_size = 1;

@doc "Tag list to display in the post footer."
function hfun_addtags()
    if is_post()
        c = IOBuffer()
        write(c, "
\nPost Tags:\n")
        for tag in locvar(:tags)
            println(c, "", tag_link(tag), ", ")
        end
        # remove comma at the end
        str = chop(String(take!(c)); tail=10) * "
"
        close(c)
        str
    else
        ""
    end
end

@doc "The HTML link to a page display all the tags."
function tag_link(tag, font_size::Union{Float64,Nothing}=nothing)
    style = ""
    if !isnothing(font_size)
        style = "font-size: $(font_size)rem"
    end
    link = join([globvar(:website_url), globvar(:tag_page_path), tag], "/")
    " $tag "
end

@doc "Tag cloud, tags with font size dependent on the number of posts that use it."
function hfun_tags_cloud()
    tags = DefaultDict{String,Int}(0)
    # count all the tags
    for p in iter_posts()
        fi = "posts/" * splitext(p)[1]
        for t in pagevar(fi, :tags)
            tags[t] += 1
        end
    end
    ordered_tags = [k for k in keys(tags)]
    sort!(ordered_tags)
    # normalize counts
    counts = [tags[t] for t in ordered_tags]
    min, max = extrema(counts)
    sizes = @. ((counts - min) / (max - min)) + 1
    # make html with inline size based on counts
    c = IOBuffer()
    write(c, "
")
    icon = ""
    tag_path = globvar(:tag_page_path)
    for (n, (tag, count)) in enumerate(zip(ordered_tags, counts))
        icon_name = icons_tags[tag]
        if icon_name !== ""
            icon = ""
        else
            icon = ""
        end
        write(
            c,
            " $icon $tag ",
        )
    end
    write(c, "
")
    str = String(take!(c))
    close(c)
    str
end

@doc "The absolute or relative post link from a file path."
function post_link(file_path, code=""; rel=true, amp=false)
    let name = splitext(file_path)[1]
        joinpath(rel ? "/" : globvar(:website_url),
                 amp ? "amp/" : "",
                 code,
                 replace(name, r"(index|404)$" => ""))
    end
end

@doc "HTML for the page title, using `rss_description` for the subtitle."
function hfun_post_title()
    path = locvar(:fd_rpath)
    if (!isnothing(match(r"posts/.+", path)) && path !== "posts/index.html")

        link = post_link(path)
        title = locvar(:title)
        desc = locvar(:rss_description)
        "

$title

                $desc

          "
    else
        ""
    end
end

@doc "A colored (var(--\$color)) star."
function hfun_star(args)
    color = args[1]
    ""
end

@doc "The tag of the current page, none otherwise."
function hfun_tag_title(prefix="Tag: ", default="Tags")
    # NOTE: franklin as {{if else}} and {{isdef}}
    c = IOBuffer()
    write(c, "
")
    let tag = locvar(:fd_tag),
        prefix = tag === "about" || tag === "lightbulbs" ? "" : prefix
        if tag != ""
            write(c, prefix)
            write(c, tag)
        else
            # write(c, locvar(:title))
            write(c, default)
        end
        write(c, "
")
    end
    str = String(take!(c))
    close(c)
    str
end

@doc "The (modified) biographic details from the field `bio.md` located in the :assets franklin folder."
function hfun_insert_bio()
    let bio = read(joinpath(path(:assets), "bio.md"), String) |>
        x -> convert_md(x; isinternal=true)
        replace(bio, "{{bio_link}}" => """
""")
    end
end

@doc "The evaluated `place.html` file located in the :layout franklin folder."
function hfun_about_place()
    joinpath(path(:layout), "place.html") |>
        x -> read(x, String) |>
        fr.convert_html
end

@doc "Check if page is an index page."
function is_index(path)
    !isnothing(match(r".*/index\.(html|md)", path))
end

@doc "Regex expression for posts path"
function posts_path_rgx(posts_path)
    Regex("$(strip(posts_path, '/'))/.+")
end

@doc "Check if page is a post."
function is_post()
    path = locvar(:fd_rpath)
    (!isnothing(match(posts_path_rgx(globvar(:posts_path)), path)) && !is_index(path))
end

@doc "Check if page is a tags page."
function is_tag(tag)
    path = locvar(:fd_rpath)
    !isnothing(match(r"$tag/index.(html|md)", path))
end

@doc "The utteranc.es comments widget (if the page is a post)."
function hfun_addcomments()
    if is_post()
        html_str = """

    """
        return html_str
    else
        ""
    end
end

@doc "Add edited date at appropriate pages."
function hfun_editedpage(tag="lightbulbs")
    if is_post() || is_tag(tag)
        locvar(:fd_mtime)
    else
        ""
    end
end

@doc "Inserts a file relative to the current page."
function hfun_insert_path(args)
    (pwd(), dirname(locvar(:fd_rpath)), args[1]) |> (x) -> joinpath(x...) |> readlines |> join
end

# TODO:remove stylings
@doc "IMG html tag, where the `alt` attribute defaults to the image name."
function hfun_insert_img(args)
    if args[2] === "none"
        ""
    else
        ""
    end
end

@doc "A dict mapping of tag names to font-awesome icons."
icons_tags =
    DefaultDict("",
                Dict(
                    "programming" => "fas fa-code",
                    "about" => "fas fa-wrench",
                    "lightbulbs" => "fas fa-lightbulb",
                    "apps" => "fab fa-android",
                    "crypto" => "fab fa-bitcoin",
                    "guides" => "fas fa-directions",
                    "hosting" => "fas fa-server",
                    "linux" => "fab fa-linux",
                    "mobile" => "fas fa-mobile-alt",
                    "net" => "fas fa-network-wired",
                    "nice-to-haves" => "fas fa-candy-cane",
                    "opinions" => "fas fa-blog",
                    "philosophy" => "fas fa-pen-alt",
                    "poetry" => "fas fa-feather",
                    "shell" => "fas fa-user-ninja",
                    "cooking" => "fas fa-utensils",
                    "games" => "fas fa-gamepad",
                    "software" => "fas fa-code-branch",
                    "stats" => "fas fa-chart-bar",
                    "tech" => "fas fa-pager",
                    "agri" => "fas fa-tree",
                    "things-that-should-not-be published" => "fas fa-comment-dots",
                    "tools" => "fas fa-tools",
                    "trading" => "fas fa-chart-line",
                    "media" => "fas fa-tv"
                ))
@doc "Wraps the `icon_tags` dict."
hfun_icon_tag(tag) = icons_tags[tag]

@doc "Either the article image, or the author image or the website logo."
function image_url()
    locvar(:images;
           default=[globvar(:author_image;
                            default=globvar(:logo))]) |>
                                first |>
                                x -> joinpath(globvar(:website_url), lstrip(x, '/'))
end

@doc "Wrapper to call an arbitrary function as an latex function."
function lx_fun(com::Franklin.LxCom, _)
    args = lxproc(com) |> split
    let f = getfield(Main, Symbol(args[1]))
        length(args) > 1 ? f(args[2:end]...) : f()
    end
end

@doc "A canonical link html element"
function canonical_link_el(url::AbstractString="")
    ln = HTMLElement(:link)
    ln.attributes["rel"] = "canonical"
    ln.attributes["href"] = url
    ln
end

@doc "The canonical url from franklin current (local) page vars."
@inline function canonical_url(;code="", amp=false)
    locvar(:fd_rpath; default="") |> x -> post_link(x, code; rel=false, amp)
end

@doc "The canonical url of a given file path."
@inline function canonical_url(path; code="", amp=false)
    post_link(path, code; rel=false, amp)
end

@doc "The html link tag for the current canonical url."
function hfun_canonical_link()
	"<link rel=\"canonical\" href=\"" *
        canonical_url() *
        "\">"
end

@doc "The html link tag for the canonical url of the current TAG page."
function hfun_canonical_link_tag()
    tag_path = joinpath(locvar(:tag_page_path), locvar(:fd_tag))
	"<link rel=\"canonical\" href=\"" *
        canonical_url(tag_path) *
        "\">"
end

@doc "The html link tag for the canonical url of the given path."
function hfun_canonical_link(args)
	"<link rel=\"canonical\" href=\"" *
        canonical_url(args[1]) *
        "\">"
end

@memoize function feed_name(parts...)
    joinpath(parts..., globvar(:rss_file) * ".xml")
end

@doc "The RSS feed link tag."
function hfun_rss_link()
    "<link rel=\"alternate\" type=\"application/rss+xml\" href=\"" *
         feed_name(globvar(:website_url)) * "\" title=\"$(globvar(:website_title))\">"
end

function hfun_rss_link_tag()
    "<link rel=\"alternate\" type=\"application/rss+xml\" href=\"" *
       feed_name(globvar(:website_url), globvar(:tag_page_path), refstring(locvar(:fd_tag))) *
       "\" title=\"$(globvar(:website_title))\">"
end

function hfun_rss_url()
	(isnothing(locvar(:fd_tag)) &&
        feed_name(globvar(:website_url))) ||
        feed_name(globvar(:website_url), globvar(:tag_page_path))
end

@doc "The html link tag for the AMP url of the current page."
function hfun_amp_link()
    # @show keys(fr.LOCAL_VARS)
	"<link rel=\"amphtml\" href=\"" *
        canonical_url(;amp=true) *
        "\">"
end

@doc "The html link tag for the AMP url of the given path."
function hfun_amp_link(args)
	"<link rel=\"amphtml\" href=\"" *
        canonical_url(args[1] ;amp=true) *
        "\">"
end

@doc "The html link tag for the AMP url of the current TAG path."
function hfun_amp_link_tag()
    tag_path = joinpath(locvar(:tag_page_path), locvar(:fd_tag))
	"<link rel=\"amphtml\" href=\"" *
        canonical_url(tag_path ;amp=true) *
        "\">"
end

@doc "A breadcrumbs list (title, path), where the last element is left to be filled."
function base_crumbs()
	[("Home", globvar(:website_url)),
     ("Posts List", joinpath(globvar(:website_url),
                             globvar(:posts_path))),
     ("", "")]
end

@doc "Breadcrumbs for the current page being evaluated."
function post_crumbs()
    crumbs = base_crumbs()
    crumbs[end] = (locvar(:title; default=""), canonical_url())
    crumbs
end

function tags_crumbs()
    tag_path = joinpath(locvar(:tag_page_path), locvar(:fd_tag))
    [("Home", globvar(:website_url)),
     ("Tags List", joinpath(globvar(:website_url), globvar(:tag_page_path))),
     (locvar(:fd_tag; default=""), canonical_url(tag_path))]
end

function hfun_insertsearch()
    if locvar(:fd_rpath) === "search.md"
        return read(joinpath(fr.path(:layout), "lunr_include.html"), String)
    end
    ""
end

function franklincontent_hfuncs()
    for sym in names(@__MODULE__; all=true)
        if startswith(string(sym), "hfun_")
            @eval export $sym
        end
    end
end

@inline function isldjson(el::HTMLElement)
    getattr(el, "type", "") === "application/ld+json"
end

@inline function islink(el::HTMLElement, rel="canonical")
    getattr(el, "rel", "") === rel
end

function load_amp()
    include(joinpath(dirname(@__FILE__), "amp.jl"))
    @eval export AMP
end

function load_minify()
    include(joinpath(dirname(@__FILE__), "minify.jl"))
    @eval export FranklinMinify
end

function load_yandex()
    include(joinpath(dirname(@__FILE__), "yandex.jl"))
    @eval export Yandex
end

function load_opg()
    include(joinpath(dirname(@__FILE__), "opg.jl"))
    @eval export OPG
end

function load_simkl()
    include(joinpath(dirname(@__FILE__), "simkl.jl"))
    @eval export Simkl
end

export tags_crumbs, post_crumbs, page_content, iter_posts, tag_link, post_link, is_index, is_post, is_tag, lx_fun

include("files.jl")

end # module