text:
using StrideArrays, StaticArrays, LinearAlgebra, BenchmarkTools

# BLAS.set_num_threads(1)

# For laptops that thermally throttle, you can set the `JULIA_SLEEP_BENCH` environment variable for #seconds to sleep before each `@belapsed`
const SLEEPTIME = parse(Float64, get(ENV, "JULIA_SLEEP_BENCH", "0"))
maybe_sleep() = iszero(SLEEPTIME) || sleep(SLEEPTIME)
# BenchmarkTools.DEFAULT_PARAMETERS.samples = 1_000_000
# BenchmarkTools.DEFAULT_PARAMETERS.seconds = 10

matrix_sizes(x::Integer) = (x,x,x)
matrix_sizes(x::NTuple{3}) = x

function runbenches(sr, ::Type{T} = Float64) where {T}
    bench_results = Matrix{T}(undef, length(sr), 5)
    for (i,s) ∈ enumerate(sr)
        M, N, K = matrix_sizes(s)
        if true#s ≤ 20
            Astatic = @SMatrix rand(T, M, K);
            Bstatic = @SMatrix rand(T, K, N);
            maybe_sleep()
            bench_results[i,1] = @belapsed $(Ref(Astatic))[] * $(Ref(Bstatic))[]
            Amutable = MArray(Astatic);
            Bmutable = MArray(Bstatic);
            Cmutable = similar(Amutable);
        else
            bench_results[i,1] = Inf
            Amutable = MMatrix{M,K,T}(undef)
            Bmutable = MMatrix{K,N,T}(undef)
            Cmutable = MMatrix{M,N,T}(undef)
            @inbounds for i ∈ 1:s^2
                Amutable[i] = rand()
                Bmutable[i] = rand()
            end
        end
        maybe_sleep()
        bench_results[i,2] = @belapsed mul!($Cmutable, $Amutable, $Bmutable)
        Afixed = StrideArray(Amutable)
        Bfixed = StrideArray(Bmutable)
        Cfixed = StrideArray{T}(undef, (StaticInt(M),StaticInt(N)))
        maybe_sleep()
        bench_results[i,3] = @belapsed mul!($Cfixed, $Afixed, $Bfixed)
        Cfixed2 = similar(Cfixed);
        Aptr = PtrArray(Afixed); Bptr = PtrArray(Bfixed); Cptr = PtrArray(Cfixed2);
        maybe_sleep()
        GC.@preserve Afixed Bfixed Cfixed2 begin
            bench_results[i,4] = @belapsed mul!($Cptr, $Aptr, $Bptr)
        end
        A = Array(Afixed); B = Array(Bfixed); C = Matrix{T}(undef, M, N);
        maybe_sleep()
        bench_results[i,5] = @belapsed jmul!($C, $A, $B)
        # @show Array(Cmutable) Cfixed Cfixed2 C
        @assert Array(Cmutable) ≈ Cfixed ≈ Cfixed2 ≈ C
        v = @view(bench_results[i,:])'
        @show s, v
    end
    bench_results
end

sizerange = 2:48
br = runbenches(sizerange);
using DataFrames, VegaLite

gflops = @. 2e-9 * (sizerange) ^ 3 / br;

df = DataFrame(gflops);
matmulmethodnames = [:SMatrix, :MMatrix, :StrideArray, :PtrArray, :matmul!];
rename!(df, matmulmethodnames);
df.Size = sizerange

function pick_suffix(desc = "")
    suffix = if StrideArrays.VectorizationBase.AVX512F
        "AVX512"
    elseif StrideArrays.VectorizationBase.AVX2
        "AVX2"
    elseif StrideArrays.VectorizationBase.REGISTER_SIZE == 32
        "AVX"
    else
        "REGSUZE$(StrideArrays.VectorizationBase.REGISTER_SIZE)"
    end
    if desc != ""
        suffix *= '_' * desc
    end
    "$(Sys.CPU_NAME)_$suffix"
end

dfs = stack(df, matmulmethodnames, variable_name = :MatMulType, value_name = :GFLOPS);
p = dfs |> @vlplot(:line, x = :Size, y = :GFLOPS, width = 900, height = 600, color = {:MatMulType});
save(joinpath(pkgdir(StrideArrays), "docs/src/assets/sizedarraybenchmarks_$(pick_suffix()).svg"), p)


text:
using Yao, YaoPlots, Yao.EasyBuild
N = 4
d = 1
ising(nbit, i, j) = rot(kron(nbit, i=>X, j=>X), 0.0)
circuit = dispatch!(variational_circuit(N, d, pair_ring(N), entangler=ising), :zero)

circuit |> vizcircuit(scale=0.7)

ext: jl


text:
using DataFrames
using Arrow
using CSV
using Random
using Tables
using TableOperations

@testset "dtable" begin
    @testset "constructors - Tables.jl compatibility (NamedTuple)" begin
        size = 1_000
        nt = (a = rand(Int, size), b = rand(Int, size))

        dt = DTable(nt, 100)
        @test fetch(dt) == nt
        @test fetch(dt, NamedTuple) == nt
        @test tabletype(dt) == NamedTuple

        # empty dtable case
        dt = DTable((a = [], b = []), 10)
        @test fetch(dt) == NamedTuple()
    end

    @testset "constructors - Tables.jl compatibility (DataFrames)" begin
        size = 1_000
        nt = (a = rand(Int, size), b = rand(Int, size))
        df = DataFrame(nt)

        dt = DTable(df, 100)
        @test fetch(dt) == df
        @test fetch(dt, DataFrame) == df
        @test tabletype(dt) == DataFrame

        dt = DTable(nt, 100; tabletype=DataFrame)
        @test fetch(dt) == df
        @test fetch(dt, DataFrame) == df
        @test tabletype(dt) == DataFrame

        dt = DTable(df, 100; tabletype=NamedTuple)
        @test fetch(dt) == nt
        @test fetch(dt, DataFrame) == df
        @test tabletype(dt) == NamedTuple
    end

    @testset "constructors - Tables.jl compatibility (Arrow)" begin
        size = 1_000
        nt = (a = rand(Int, size), b = rand(Int, size))
        io = IOBuffer()
        Arrow.write(io, nt)
        arr = Arrow.Table(take!(io))

        dt = DTable(arr, 100)
        @test fetch(dt) == nt
        @test fetch(dt, NamedTuple) == nt
        @test tabletype(dt) == NamedTuple # NamedTuple is the type obtained using the sink function of Arrow.Table
    end

    @testset "constructors - Tables.jl compatibility (CSV)" begin
        size = 1_000
        nt = (a = rand(Int, size), b = rand(Int, size))
        io = IOBuffer()
        CSV.write(io, nt)
        dt1 = CSV.read(take!(io), (csv -> DTable(csv, 100)))
        CSV.write(io, nt)
        # or
        dt2 = DTable(CSV.File(take!(io)), 100)
        @test fetch(dt1) == nt
        @test fetch(dt1, NamedTuple) == nt
        @test tabletype(dt1) == NamedTuple
        @test fetch(dt2) == nt
        @test fetch(dt2, NamedTuple) == nt
        @test tabletype(dt2) == NamedTuple
    end

    @testset "constructors - file input" begin
        n = 20
        size = 1000
        ios = [IOBuffer() for _ in 1:n]
        data = [(a = rand(size), b = rand(size)) for _ in 1:n]
        arr = [Arrow.write(ios[idx], data[idx]) for idx in 1:n]

        da = DTable(x -> Arrow.Table(take!(ios[tryparse(Int64, x)])), [string(i) for i in 1:n])
        db = vcat([DataFrame(d) for d in data]...)
        @test fetch(da, DataFrame) == db
        @test Dagger.resolve_tabletype(da) == NamedTuple
        @test da.tabletype === nothing
        tabletype!(da)
        @test da.tabletype === NamedTuple
    end

    @testset "map" begin
        size = 1_000
        nt = (a = rand(size), b = rand(size))

        df = DataFrame(nt)
        for src in [nt, df]
            dt = DTable(src, 100)

            # Single result
            mt = map(x -> (r = x.a + x.b, ), dt)
            mf = map(x -> x.a + x.b, eachrow(df))
            @test fetch(mt).r == mf

            # Two results
            mt = map(x -> (r1 = x.a + x.b, r2 = x.a - x.b), dt)
            mf = combine(df, [:a, :b] => ((a, b) -> a .+ b) => :r1, [:a, :b] => ((a, b) -> a .- b) => :r2)
            @test fetch(mt).r1 == mf.r1
            @test fetch(mt).r2 == mf.r2
        end

        # Map an empty dtable
        dt = DTable((a = [], b = []), 10)
        m = map(x -> (r = x.a + x.b), dt)
        @test fetch(m) == NamedTuple()
    end

    @testset "filter" begin
        size = 1_000
        nt = (a = rand(size), b = rand(size))

        df = DataFrame(nt)

        for src in [nt, df]
            dt = DTable(src, 100)

            dfr = filter(x -> (x.a .> 0.5) .& (x.b .< 0.5), df)
            dtr = filter(x -> (x.a .> 0.5) .& (x.b .< 0.5), dt)
            @test fetch(dtr, DataFrame) == dfr
        end

        # Filter an empty DTable
        dt = DTable((a = [], b = []), 10)
        f = filter(x -> x.a .> 0.5, dt)
        @test fetch(f) == NamedTuple()
    end

    @testset "reduce" begin
        size = 1_000
        nt = (a=rand(Int, size) .% 100, b=rand(Int, size) .% 100)

        df = DataFrame(nt)
        dtdf = DTable(df, 100)
        dtnt = DTable(nt, 100)

        dtdf1 = reduce(+, dtdf, cols=[:a])
        dtnt1 = reduce(+, dtnt, cols=[:a])
        df1 = reduce((x, y) -> x + y.a, eachrow(df); init=0)

        dtdf2 = reduce(+, map(x -> (r = x.a * x.b,), dtdf), cols=[:r])
        dtnt2 = reduce(+, map(x -> (r = x.a * x.b,), dtnt), cols=[:r])
        df2 = reduce((x, y) -> x + y.a * y.b, eachrow(df);init=0)

        dtdf3 = reduce(+, map(x -> (r = x.a + x.b,), dtdf), cols=[:r])
        dtnt3 = reduce(+, map(x -> (r = x.a + x.b,), dtnt), cols=[:r])
        df3 = reduce((x, y) -> x + y.a + y.b, eachrow(df);init=0)

        dtdf4 = reduce(*, dtdf, cols=[:a])
        dtnt4 = reduce(*, dtnt, cols=[:a])
        df4 = reduce((x, y) -> x * y.a, eachrow(df); init=1)

        @test fetch(dtdf1).a == fetch(dtnt1).a == df1
        @test fetch(dtdf2).r == fetch(dtnt2).r == df2
        @test fetch(dtdf3).r == fetch(dtnt3).r == df3
        @test fetch(dtdf4).a == fetch(dtnt4).a == df4

        all_reduce = reduce(+, dtdf)
        df5 = reduce((x, y) -> x + y.b, eachrow(df); init=0)

        @test fetch(all_reduce).a == df1
        @test fetch(all_reduce).b == df5
    end

    @testset "chaining ops" begin
        nt = (a=1:100, b=(1:100))

        d = DTable(nt, 2)
        f1 = filter(x -> iseven.(x.a), d)
        m1 = map(x -> (r = x.a + x.b,), f1)
        f2 = filter(x -> x.r > 50, m1)
        r = reduce(+, f2)
        @test fetch(r).r == 4788
        r = reduce(*, f2, init=BigInt(1))
        @test fetch(r).r == reduce(*, fetch(f2).r, init=BigInt(1))
    end

    @testset "trim" begin
        nt = (a=1:100, b=(1:100))

        d = DTable(nt, 2)
        fd = filter(x -> x.a > 49, d)

        @test length(d.chunks) == length(fd.chunks)
        @test length(trim(fd).chunks) == 26
        trim!(fd)
        @test length(fd.chunks) == 26

        fd2 = filter(x -> x.a > 100, d)
        @test length(d.chunks) == length(fd2.chunks)
        @test length(trim(fd2).chunks) == 0
        trim!(fd2)
        @test length(fd2.chunks) == 0
    end

    @testset "tabletype" begin
        nt = (a=1:100, b=(1:100))
        d = DTable(nt, 2)
        @test tabletype(d) == NamedTuple
        d.tabletype = nothing
        @test Dagger.resolve_tabletype(d) == NamedTuple
        tabletype!(d)
        @test d.tabletype == NamedTuple

        # empty dtable case
        dt = DTable((a = [], b = []), 10)
        @test tabletype(dt) == NamedTuple # fallback in case it can't be found
    end

    @testset "dtable groupby basic" begin
        rng = MersenneTwister(2137)
        charset = collect('a':'d')
        cs1 = shuffle(rng, repeat(charset, inner=4, outer=4))
        cs2 = shuffle(rng, repeat(charset, inner=4, outer=4))

        kwargs_set = [
            (;)
            (chunksize=1,)
            (merge=false,)
            (chunksize=8,)
            (chunksize=16,)
            (chunksize=20,)
        ]

        ######################################################
        # single col groupby
        d = DTable((a=cs1,), 4)

        for kwargs in kwargs_set
            g = Dagger.groupby(d, :a; kwargs...)
            c = Dagger._retrieve.(g.dtable.chunks)
            @test all([all(t.a[1] .== t.a) for t in c])
            @test all(getindex.(getproperty.(c, :a), 1) .∈ Ref(charset))
            @test sort(collect(fetch(d).a)) == sort(collect(fetch(g).a))
        end

        ######################################################
        # multi col groupby
        d = DTable((a=cs1, b=cs2), 4)

        for kwargs in kwargs_set
            g = Dagger.groupby(d, [:a, :b]; kwargs...)
            c = Dagger._retrieve.(g.dtable.chunks)
            @test all([all(t.a[1] .== t.a) for t in c])
            @test all([all(t.b[1] .== t.b) for t in c])
            @test all(getindex.(getproperty.(c, :a), 1) .∈ Ref(charset))
            @test all(getindex.(getproperty.(c, :a), 1) .∈ Ref(charset))
            fd = fetch(d)
            fg = fetch(g)
            @test sort(collect(fd.a)) == sort(collect(fg.a))
            @test sort(collect(fd.b)) == sort(collect(fg.b))
        end

        ######################################################
        # function groupby
        intset = collect(10:29)
        is1 = shuffle(rng, repeat(intset, 4))
        d = DTable((a=is1,), 4)

        f1 = x -> x.a % 10
        f2 = x -> x % 10
        for kwargs in kwargs_set
            g = Dagger.groupby(d, f1)
            c = Dagger._retrieve.(g.dtable.chunks)
            @test all([all(f2(t.a[1]) .== f2.(t.a)) for t in c])
            @test all(getindex.(getproperty.(c, :a), 1) .∈ Ref(intset))
            @test sort(collect(fetch(d).a)) == sort(collect(fetch(g).a))
        end
    end

    @testset "dtable groupby index check" begin
        rng = MersenneTwister(2137)
        charset = collect('a':'d')
        cs1 = shuffle(rng, repeat(charset, inner=4, outer=4))

        d = DTable((a=cs1,), 4)
        g = Dagger.groupby(d, :a)

        for key in keys(g.index)
            chunk_indices = g.index[key]
            chunks = getindex.(Ref(g.dtable.chunks), chunk_indices)
            parts = Dagger._retrieve.(chunks)

            @test all([all(key .== p.a) for p in parts])
        end
    end

    @testset "dtable groupby ops" begin
        rng = MersenneTwister(2137)
        charset = collect('a':'d')
        cs1 = shuffle(rng, repeat(charset, inner=4, outer=4))
        is1 = [3 for _ in 1:length(cs1)]

        d = DTable((a=cs1, b=is1), 4)
        g = Dagger.groupby(d, :a, chunksize=4)

        m = map(x -> (a = x.a, result = x.a + x.b), g)

        for key in keys(m.index)
            chunk_indices = m.index[key]
            chunks = getindex.(Ref(m.dtable.chunks), chunk_indices)
            parts = Dagger._retrieve.(chunks)
            @test all([all((key + 3) .== p.result) for p in parts])
            @test all(fetch(m[key]).a .== key)
        end

        r = reduce(*, g)
        fr = fetch(r)

        for (i, key) in enumerate(fr.a)
            @test fr.result_a[i] == repeat(key, length(cs1) ÷ 4)
            @test fr.result_b[i] == 3 ^ (length(cs1) ÷ 4)
        end

        f = filter(x -> x.a ∈ ['a', 'b'], g)
        @test ['c', 'd'] ∉ fetch(f).a

        t = trim(f)
        @test ['c', 'd'] ∉ collect(keys(t.index))

        trim!(f)
        @test ['c', 'd'] ∉ collect(keys(f.index))
    end

    @testset "tables.jl source" begin
        nt = (a=1:100, b=1:100)

        d1 = DTable(nt, 10) # standard row based constructor

        # partition constructor, check with DTable as input
        d2 = DTable(d1)
        d3 = DTable(Tables.partitioner(identity, [nt for _ in 1:10]))

        @test length(d1.chunks) == length(d2.chunks) == length(d3.chunks)

        @test Tables.getcolumn(d1, 1) == 1:100
        @test Tables.getcolumn(d1, 2) == 1:100
        @test Tables.getcolumn(d1, :a) == 1:100
        @test Tables.getcolumn(d1, :b) == 1:100
        @test Tables.columnnames(d1) == (:a, :b)

        @test Tables.schema(d1).names == (:a, :b)
        @test Tables.schema(d1).types == (Int, Int)

        for c in Tables.columns(d1)
            @test c == 1:100
        end

        @test all([ r.a == r.b == v for (r,v) in zip(collect(Tables.rows(d1)),1:100)])

        # length tests for collect on iterators
        @test length(d1) == 100
        @test length(Tables.rows(d1)) == 100
        @test length(Tables.columns(d1)) == 2
        @test length(Tables.partitions(d1)) == 10

        # GDTable things

        g = Dagger.groupby(d1, r -> r.a % 10, chunksize=3)
        t1 = Tables.columntable(Tables.rows(g))
        @test 1:100 == sort(t1.a) == sort(t1.b)
        t2 = collect(Tables.columns(g))
        @test 1:100 == sort(t2[1]) == sort(t2[2])

        for partition in Tables.partitions(g)
            @test partition isa DTable
            v = Tables.getcolumn(partition, :a)[1]
            @test all([el%10 == v%10 for el in Tables.getcolumn(partition, :a)])
        end
    end
end


text:
# TODO: make work for other dimensions than 2

"""
    uniform_field(gridwidth, gridheight[, scale=0.2, border=true, normalize=true]) -> Array{Float64,3}

Generate a 2D vector field by placing uniformly random generated
displacement vectors on an equally spaced grid of size `gridheight
× gridwidth`.

The resulting array will have 3 dimensions, for which the first
dimension will always have a size of 2, which denotes the y and x
dimension of the vectors.
"""
function uniform_field(gridheight::Int, gridwidth::Int; scale = .2, border = false, normalize = true)
    uniform_field(gridheight, gridwidth, scale, border, normalize)
end

function uniform_field(gridheight::Int, gridwidth::Int, scale, border, normalize)
    A = if !border
        @assert gridwidth > 2 && gridheight > 2
        _2dborder!(safe_rand(2, gridheight, gridwidth), .5)
    else
        @assert gridwidth > 0 && gridheight > 0
        safe_rand(2, gridheight, gridwidth)
    end::Array{Float64,3}
    broadcast!(*, A, A, 2.)
    broadcast!(-, A, A, 1.)
    if normalize
        for d = 1:2
            Ad = view(A, d, :, :)
            broadcast!(*, Ad, Ad, scale / norm(Ad))
        end
    end
    A
end

"""
    gaussian_field(gridwidth, gridheight[, scale=0.2, sigma=2, iterations=1, border=true, normalize=true]) -> Array{Float64,3}

Generate a 2D vector field by placing uniformly random generated
displacement vectors on an equally spaced grid of size
`gridheight × gridwidth`. Additionally, this vector field is
smoothed using a gaussian filter with parameter `sigma`. This
will result in a less chaotic vector field than
[`uniform_field`](@ref) and be much more similar to an elastic
distortion.

The resulting array will have 3 dimensions, for which the first
dimension will always have a size of 2, which denotes the y and x
dimension of the vectors.
"""
function gaussian_field(gridheight::Int, gridwidth::Int; scale = .2, sigma = 2, iterations = 1, border = false, normalize = true)
    gaussian_field(gridheight, gridwidth, scale, sigma, iterations, border, normalize)
end

function gaussian_field(gridheight::Int, gridwidth::Int, scale, sigma, iterations, border, normalize)
    @assert iterations > 0
    A = uniform_field(gridheight, gridwidth, 1., true, false)
    kern = Kernel.gaussian(sigma)
    if !border
        _2dborder!(A, 0.)
        for iter = 1:iterations
            for d = 1:2
                Ad = view(A, d, :, :)
                imfilter!(Ad, Ad, kern)
            end
            _2dborder!(A, 0.)
        end
        A
    else
        for d = 1:2
            Ad = view(A, d, :, :)
            imfilter!(Ad, Ad, kern)
        end
        A
    end
    if normalize
        for d = 1:2
            Ad = view(A, d, :, :)
            broadcast!(*, Ad, Ad, scale / norm(Ad))
        end
    end
    A
end


text:
using Gadfly, Compose

set_default_plot_size(8inch,6inch)

p=plot(y=[1,2,3], Geom.line)
c=Compose.context()
gridstack(Union{Plot,Compose.Context}[p c; c p])


text:
#protocol: http://redis.io/topics/protocol

"`commands` can be any thing that `write` and `sizeof` works properly"
function resp_send(socket::TCPSocket, commands...)
    buf = IOBuffer() # it turns out that buffering can boost the performance by magnititude.
    buf << '*' << length(commands) << CRLF
    for command in commands
        buf << '$' << sizeof(command) << CRLF << command << CRLF
    end
    socket << take!(buf) # sockets are not buffered so no need to flush here
end

function resp_read(socket::TCPSocket)::Union{Int64, String, Bytes, Vector, Nothing}
    magic_byte = socket >> Byte
    line = socket >> readline

    if magic_byte == Byte('+')
        line
    elseif magic_byte == Byte('-')
        RedisException(line) |> throw
    elseif magic_byte == Byte(':')
        parse(Int64, line)
    elseif magic_byte == Byte('$')
        line[1] == '-' && return nothing
        len = parse(Int64, line)
        socket >> (len + 2) |> chomp!
    elseif magic_byte == Byte('*')
        line[1] == '-' && return nothing
        len = parse(Int64, line)
        [resp_read(socket) for i in 1:len]
    else
        ProtocolException("unexpected type $(Char(magic_byte))") |> throw
    end
end


text:
const QUOTE_CACHE = Cache{String,Float64}(Minute(1))

finnhub_token() = get(ENV, "FINNHUB_TOKEN", "")

function commander(c::Client, m::Message, ::Val{:ig})
    @debug "ig_commander called"

    command = extract_command("ig", m.content)
    args = split(command)
    @debug "parse result" command args

    if length(args) == 0 ||
            args[1] ∉ ["start-game", "abandon-game", "quote", "chart",
                "buy", "sell", "rank", "view",
                "abandon-game-really"]
        help_commander(c, m, :ig)
        return
    end

    user = @discord retrieve(c, User, m.author.id)
    try
        ig_execute(c, m, user, Val(Symbol(args[1])), args[2:end])
    catch ex
        if ex isa IgUserError
            discord_reply(c, m, ig_hey(user.username, ex.message))
        else
            discord_reply(c, m, ig_hey(user.username,
                "sorry, looks like you've hit a bug. Please report the problem."))
            @error "Internal error" ex
            Base.showerror(stdout, ex, catch_backtrace())
        end
    end
end

function help_commander(c::Client, m::Message, ::Val{:ig})
    discord_reply(c, m, """
        Play the investment game (ig). US market only for now.
        ```
        ig start-game
        ig abandon-game
        ```
        Research stocks:
        ```
        ig quote <symbol>            - get current price quote
        ig chart <symbol> [period]   - historical price chart
            Period is optional. Examples are: 200d, 36m, or 10y
              for 200 days, 36 months, or 10 years respectively.
        ```
        Manage portfolio:
        ```
        ig view                - view holdings and current market values
        ig buy <n> <symbol>    - buy <n> shares of a stock
        ig sell <n> <symbol>   - sell <n> shares of a stock
        ```
        How are you doing?
        ```
        ig rank [n]            - display top <n> portfolios, defaults to 5.
        ```
        """)
end

"""
    ig_execute

This function executes any of the following commands.

# start-game
Start a new game by giving the player \$1MM USD.

# abandon-game
Abandon current game by wiping out the player's record.

# quote <symbol>
Fetch current market price of a stock.

# chart <symbol> <lookback>
The `lookback` argument is optional. If it is not specified, then it is defaulted
to 1 year. Otherwise, it takes the format of a number followed by `y`, `m`, or `d`
for number of years, months, or days respectively.

# buy <n> <symbol>
Buy `n` shares of a stock at the current market price. Player must have enough
cash to settle the trade.

# sell <n> <symbol>
Sell `n` shares of a stock at the current market price. Player must have
that many shares in the portfolio.

# view
Display detailed information about the portfolio, its holdings, and total market value.

# rank
Display top portfolios with highest market value.
"""
function ig_execute end

function ig_execute(c::Client, m::Message, user::User, ::Val{Symbol("start-game")}, args)
    ig_affirm_non_player(user.id)
    pf = ig_start_new_game(user.id)
    discord_reply(c, m, ig_hey(user.username, "you have \$" * format_amount(pf.cash) *
        " in your shiny new portfolio now! Good luck!"))
end

function ig_execute(c::Client, m::Message, user::User, ::Val{Symbol("abandon-game")}, args)
    ig_affirm_player(user.id)
    discord_reply(c, m, ig_hey(user.username,
        "do you REALLY want to abandon the game and wipe out all of your data? " *
        "If so, type `ig abandon-game-really`."))
end

function ig_execute(c::Client, m::Message, user::User, ::Val{Symbol("abandon-game-really")}, args)
    ig_affirm_player(user.id)
    ig_remove_game(user.id)
    discord_reply(c, m, ig_hey(user.username,
        "your investment game is now over. Play again soon!"))
end

function ig_execute(c::Client, m::Message, user::User, ::Val{:buy}, args)
    ig_affirm_player(user.id)
    length(args) == 2 ||
        throw(IgUserError("Invalid command. Try `ig buy 100 aapl` to buy 100 shares of Apple Inc."))

    symbol = strip(uppercase(args[2]))
    shares = tryparse(Int, args[1])
    shares !== nothing || throw(IgUserError("please enter number of shares as a number: `$shares`"))

    purchase_price = ig_buy(user.id, symbol, shares)
    discord_reply(c, m, ig_hey(user.username, "you have bought $shares shares of $symbol at \$" *
        format_amount(purchase_price)))
end

function ig_execute(c::Client, m::Message, user::User, ::Val{:sell}, args)
    ig_affirm_player(user.id)
    length(args) == 2 ||
        throw(IgUserError("Invalid command. Try `ig sell 100 aapl` to sell 100 shares of Apple Inc."))

    symbol = strip(uppercase(args[2]))
    shares = tryparse(Int, args[1])
    shares !== nothing || throw(IgUserError("please enter number of shares as a number: `$shares`"))

    current_price = ig_sell(user.id, symbol, shares)
    discord_reply(c, m, ig_hey(user.username, "you have sold $shares shares of $symbol at \$" *
        format_amount(current_price)))
end

function ig_execute(c::Client, m::Message, user::User, ::Val{:view}, args)
    ig_affirm_player(user.id)

    df = ig_mark_to_market_portfolio(user.id)
    ig_reformat_view!(df)

    view = length(args) == 1 && args[1] == "simple" ? SimpleView() : PrettyView()
    table = ig_view_table(view, df)
    total_str = format_amount(round(Int, sum(df.amount)))

    discord_reply(c, m, ig_hey(user.username,
        """
        here is your portfolio:
        ```
        $table
        ```
        Total portfolio Value: $total_str
        """))
end

# Shorten colummn headings for better display in Discord
function ig_reformat_view!(df::AbstractDataFrame)
    select!(df, Not(:purchase_price))
    rename!(df, "current_price" => "price", "market_value" => "amount")
end

function ig_execute(c::Client, m::Message, user::User, ::Val{:quote}, args)
    length(args) == 1 ||
        throw(IgUserError("Invalid command. Try `ig quote aapl` to fetch the current price of Apple Inc."))

    symbol = strip(uppercase(args[1]))
    price = ig_get_real_time_quote(symbol)
    discord_reply(c, m, ig_hey(user.username, "the current price of $symbol is " * format_amount(price)))
end

function ig_execute(c::Client, m::Message, user::User, ::Val{:chart}, args)
    1 <= length(args) <= 2 ||
        throw(IgUserError("Invalid command. Try `ig chart aapl` to see a chart for Apple Inc."))

    ch = discord_channel(c, m.channel_id)
    symbol = strip(uppercase(args[1]))
    lookback = length(args) == 1 ? Year(1) : date_period(lowercase(args[2]))
    from_date, to_date = recent_date_range(lookback)
    df = ig_historical_prices(symbol, from_date, to_date)
    filename = ig_chart(symbol, df.Date, df."Adj Close")
    discord_upload_file(c, ch, filename;
        content = ig_hey(user.username, "here is the chart for $symbol for the past $lookback. " *
            "To plot a chart with different time horizon, " *
            "try something like `ig chart $symbol 90d` or `ig chart $symbol 10y`."))
end

function ig_execute(c::Client, m::Message, user::User, ::Val{:rank}, args)
    length(args) <= 1 ||
        throw(IgUserError("Invalid command. Try `ig rank` or `ig rank 10`"))

    n = length(args) == 0 ? 5 : tryparse(Int, args[1])
    n !== nothing || throw(IgUserError("invalid rank argument `$(args[1])`. " *
        "Try `ig rank` or `ig rank 10`"))

    rt = ig_ranking_table(c)
    rt = rt[1:min(n, nrow(rt)), :]  # get top N results
    rt_str = ig_view_table(PrettyView(), rt)

    discord_reply(c, m, ig_hey(user.username, """here's the current ranking:
    ```
    $rt_str
    ```
    """))
end

function ig_ranking_table(c::Client)
    valuations = ig_value_all_portfolios()
    if length(valuations) > 0
        users_dict = retrieve_users(c, [v.id for v in valuations])
        @debug "ig_ranking_table" valuations
        @debug "ig_ranking_table" users_dict
        df = DataFrame(
            rank = 1:length(valuations),
            player = [users_dict[v.id].username for v in valuations],
            portfolio_value = [v.total for v in valuations],
        )
    else
        return DataFrame(player = String[], portfolio_value = Float64[])
    end
end

@mockable function retrieve_users(c::Client, ids::Vector{UInt64})
    futures = retrieve.(Ref(c), User, ids)
    responses = fetch.(futures)
    unknown = User(; id = UInt64(0), username = "Unknown")
    return Dict(k => res.val === nothing ? unknown : res.val for (k, res) in zip(ids, responses))
end

function ig_value_all_portfolios()
    pfs = ig_load_all_portfolios()
    valuations = []
    for (id, pf) in pfs
        @debug "Evaluating portfolio" id pf
        df = ig_mark_to_market!(ig_holdings_data_frame(pf))
        mv = nrow(df) > 0 ? sum(df.market_value) : 0.0
        cash = pf.cash
        total = mv + cash
        push!(valuations, (; id, mv, cash, total))
    end
    sort!(valuations; lt = (x,y) -> x.total < y.total, rev = true)
    # @info "ig_value_all_portfolios result" valuations
    valuations
end

"Format money amount"
format_amount(x::Real) = format(x, commas = true, precision = 2)
format_amount(x::Integer) = format(x, commas = true)

"Pretty table formatters"
decimal_formatter(v, i, j) = v isa Real ? format_amount(v) : v
integer_formatter(v, i, j) = v isa Real ? format_amount(round(Int, v)) : v

"Hey someone"
ig_hey(username::AbstractString, message::AbstractString) = "Hey, " * username * ", " * message

"File location of the game file for a user"
ig_file_path(user_id::UInt64) = joinpath(ig_data_directory(), "$user_id.json")

"Directory of the investment game data files"
ig_data_directory() = joinpath("data", "ig")

"Returns true if user already has a game in progress."
ig_is_player(user_id::UInt64) = isfile(ig_file_path(user_id))

"Start a new game file"
function ig_start_new_game(user_id::UInt64)
    pf = IgPortfolio(1_000_000.00, IgHolding[])
    ig_save_portfolio(user_id, pf)
    return pf
end

"Destroy the existing game file for a user."
ig_remove_game(user_id::UInt64) = rm(ig_file_path(user_id))

"Affirms the user has a game or throw an exception."
function ig_affirm_player(user_id::UInt64)
    ig_is_player(user_id) || throw(IgUserError("you don't have a game yet."))
    return nothing
end

"Affirms the user does not have game or throw an exception."
function ig_affirm_non_player(user_id::UInt64)
    !ig_is_player(user_id) || throw(IgUserError(
        "you already have a game running. Use `ig view` to see your current portfolio."
    ))
    return nothing
end

"Save a user portfolio in the data directory."
function ig_save_portfolio(user_id::UInt64, pf::IgPortfolio)
    @debug "Saving portfolio" user_id
    path = ig_file_path(user_id)
    mkpath(dirname(path)) # ensure directory is there
    write(path, JSON3.write(pf))
    return nothing
end

"Load the portfolio for a single user"
function ig_load_portfolio(user_id::UInt64)
    @debug "Loading portfolio" user_id
    path = ig_file_path(user_id)
    return ig_load_portfolio(path)
end

"Load a single portfolio from game file"
function ig_load_portfolio(path::AbstractString)
    mkpath(dirname(path)) # ensure directory is there
    bytes = read(path)
    return JSON3.read(bytes, IgPortfolio)
end

"Extract user id from the portfolio data file"
function ig_user_id_from_path(path::AbstractString)
    filename = basename(path)
    filename_without_extension = replace(filename, r"\.json$" => "")
    return parse(UInt64, filename_without_extension)
end

"Load all game files"
function ig_load_all_portfolios()
    dir = ig_data_directory()
    files = readdir(dir)
    user_ids = ig_user_id_from_path.(files)
    return Dict(user_id => ig_load_portfolio(joinpath(dir, file))
        for (user_id, file) in zip(user_ids, files))
end

"Fetch the current quote of a stock"
@mockable function ig_get_real_time_quote(symbol::AbstractString)
    @info "$(now()) real time quote: $symbol"
    token = finnhub_token()
    length(token) > 0 || throw(IgSystemError("No market price provider. Please report to admin."))
    symbol = HTTP.escapeuri(symbol)
    response = HTTP.get("https://finnhub.io/api/v1/quote?symbol=$symbol&token=$token")
    data = JSON3.read(response.body)
    current_price = data.c
    current_price > 0 || throw(IgUserError("there is no price for $symbol. Is it a valid stock symbol?"))
    current_price
end

"Fetch quote of a stock, but possibly with a time delay."
function ig_get_quote(symbol::AbstractString)
    return get!(QUOTE_CACHE, symbol) do
        ig_get_real_time_quote(symbol)
    end
end

"Buy stock for a specific user at a specific price."
function ig_buy(
    user_id::UInt64,
    symbol::AbstractString,
    shares::Real,
    current_price::Real = ig_get_real_time_quote(symbol)
)
    @debug "Buying stock" user_id symbol shares
    pf = ig_load_portfolio(user_id)
    cost = shares * current_price
    if pf.cash >= cost
        pf.cash -= cost
        push!(pf.holdings, IgHolding(symbol, shares, current_date(), current_price))
        ig_save_portfolio(user_id, pf)
        return current_price
    end
    throw(IgUserError("you don't have enough cash. " *
        "Buying $shares shares of $symbol will cost you $(format_amount(cost)) " *
        "but you only have $(format_amount(pf.cash))"))
end

"Sell stock for a specific user. Returns executed price."
function ig_sell(
    user_id::UInt64,
    symbol::AbstractString,
    shares::Real,
    current_price::Real = ig_get_real_time_quote(symbol)
)
    @debug "Selling stock" user_id symbol shares
    pf = ig_load_portfolio(user_id)
    pf_new = ig_sell_fifo(pf, symbol, shares, current_price)
    ig_save_portfolio(user_id, pf_new)
    return current_price
end

"Sell stock based upon FIFO accounting scheme. Returns the resulting `IgPortfolio` object."
function ig_sell_fifo(pf::IgPortfolio, symbol::AbstractString, shares::Real, current_price::Real)

    existing_shares = ig_count_shares(pf, symbol)
    if existing_shares == 0
        throw(IgUserError("you do not have $symbol in your portfolio"))
    elseif shares > existing_shares
        existing_shares_str = format_amount(round(Int, existing_shares))
        throw(IgUserError("you cannot sell more than what you own ($existing_shares_str shares)"))
    end

    proceeds = shares * current_price

    # Construct a new IgPortfolio object that contains the resulting portfolio after
    # selling the stock. The following logic does it incrementally but just for documentation
    # purpose an alternative algorithm would be to make a copy and then relief the sold lots.
    holdings = IgHolding[]
    pf_new = IgPortfolio(pf.cash + proceeds, holdings)
    remaining = shares   # keep track of how much to sell
    for h in pf.holdings
        if h.symbol != symbol || remaining == 0
            push!(holdings, h)
        else
            if h.shares > remaining  # relief lot partially
                revised_lot = IgHolding(symbol, h.shares - remaining, h.date, h.purchase_price)
                push!(holdings, revised_lot)
                remaining = 0
            else # relief this lot completely and continue
                remaining -= h.shares
            end
        end
    end
    return pf_new
end

"""
Returns a data frame for the portfolio holdings.
Note that:
1. It does not include cash portion of the portfolio
2. Multiple lots of the same stock will be in different rows

See also: `ig_grouped_holdings`(@ref)
"""
function ig_holdings_data_frame(pf::IgPortfolio)
    return DataFrame(
        symbol = [h.symbol for h in pf.holdings],
        shares = [h.shares for h in pf.holdings],
        purchase_price = [h.purchase_price for h in pf.holdings],
        purchase_date = [h.date for h in pf.holdings],
    )
end

"Returns grouped holdings by symbol with average purchase price"
function ig_grouped_holdings(df::AbstractDataFrame)
    df = combine(groupby(df, :symbol)) do sdf
        shares = sum(sdf.shares)
        weights = sdf.shares / shares
        purchase_price = sum(weights .* sdf.purchase_price)
        (; shares, purchase_price)
    end
    return sort!(df, :symbol)
end

"Return a data frame with the user's portfolio marked to market."
function ig_mark_to_market_portfolio(user_id::UInt64)
    pf = ig_load_portfolio(user_id)
    df = ig_grouped_holdings(ig_holdings_data_frame(pf))
    cash_entry = ig_cash_entry(pf)
    if nrow(df) > 0
        ig_mark_to_market!(df)
        push!(df, cash_entry)
    else
        df = DataFrame([cash_entry])
    end
    return df
end

"Return the portoflio cash as named tuple that can be appended to the portfolio data frame."
function ig_cash_entry(pf::IgPortfolio)
    return (symbol = "CASH:USD", shares = pf.cash,
        purchase_price = 1.0, current_price = 1.0, market_value = pf.cash)
end

"Add columns with current price and market value"
function ig_mark_to_market!(df::AbstractDataFrame)
    df.current_price = [ig_get_quote(s) for s in df.symbol]
    df.market_value = df.shares .* df.current_price
    return df
end

"Format data frame using pretty table"
function ig_view_table(::PrettyView, df::AbstractDataFrame)
    return pretty_table(String, df;
        formatters = integer_formatter, header = names(df))
end

"Return portfolio view as string in a simple format"
function ig_view_table(::SimpleView, df::AbstractDataFrame)
    io = IOBuffer()
    # @show "ig_view_table" df
    for (i, r) in enumerate(eachrow(df))
        if !startswith(r.symbol, "CASH:")
            println(io,
                i, ". ", r.symbol, ": ",
                round(Int, r.shares), " x \$", format_amount(r.price),
                " = \$", format_amount(round(Int, r.amount)))
        else
            println(io,
                i, ". ", r.symbol, " = ", format_amount(round(Int, r.amount)))
        end
    end

    return String(take!(io))
end

"Returns a tuple of two dates by looking back from today's date"
function recent_date_range(lookback::DatePeriod)
    T = current_date()
    to_date = T
    from_date = T - lookback
    return from_date, to_date
end

@mockable current_date() = today()

"""
Return a data frame with historical prices data. Columns include:
- `Date`
- `Open`
- `High`
- `Low`
- `Close`
- `Adj Close`
- `Volume`
"""
function ig_historical_prices(symbol::AbstractString, from_date::Date, to_date::Date)
    from_sec = seconds_since_1970(from_date)
    to_sec = seconds_since_1970(to_date + Day(1))  # apparently, Yahoo is exclusive on this end
    symbol = HTTP.escapeuri(symbol)
    url = "https://query1.finance.yahoo.com/v7/finance/download/$symbol?" *
        "period1=$from_sec&period2=$to_sec&interval=1d&events=history&includeAdjustedClose=true"
    try
        return DataFrame(CSV.File(Downloads.download(url)))
    catch ex
        if ex isa Downloads.RequestError && ex.response.status == 404
            throw(IgUserError("there is no historical prices for $symbol. Is it a valid stock symbol?"))
        else
            rethrow()
        end
    end
end

"Plot a simple price chart"
function ig_chart(symbol::AbstractString, dates::Vector{Date}, values::Vector{<:Real})
    theme(:dark)
    height = 720
    width = height * 16 ÷ 9
    from_date, to_date = extrema(dates)
    last_price_str = format_amount(last(values))
    p = plot(dates, values,
        title = "$symbol Historical Prices ($from_date to $to_date)\nLast price: $last_price_str",
        linewidth = 2,
        size = (width, height),
        legend = nothing,
        tickfontsize = 12,
    )
    filename = tempname() * ".png"
    savefig(p, filename)
    return filename
end

"Find lots for a specific stock in the portfolio. Sort by purchase date."
function ig_find_lots(pf::IgPortfolio, symbol::AbstractString)
    lots = IgHolding[x for x in pf.holdings if x.symbol == symbol]
    return sort(lots, lt = (x,y) -> x.date < y.date)
end

"Return total number of shares for a specific stock in the portfolio."
function ig_count_shares(pf::IgPortfolio, symbol::AbstractString)
    lots = ig_find_lots(pf, symbol)
    return length(lots) > 0 ? round(sum(lot.shares for lot in lots); digits = 0) : 0
    # note that we store shares as Float64 but uses it as Int (for now)
end

seconds_since_1970(d::Date) = (d - Day(719163)).instant.periods.value * 24 * 60 * 60

function date_period(s::AbstractString)
    m = match(r"^(\d+)([ymd])$", s)
    m !== nothing || throw(IgUserError("invalid date period: $s. Try `5y` or `30m`."))
    num = parse(Int, m.captures[1])
    dct = Dict("y" => Year, "m" => Month, "d" => Day)
    return dct[m.captures[2]](num)
end


text:
LONG_SCALE = float(0xFFFFFFFFFFFFFFF)

maybeAppendUnit(unit::Any, appended_unit) = maybeAppendUnit([unit], appended_unit)
function maybeAppendUnit(unit::AbstractArray, appended_unit)
  if length(appended_unit) > 0
    push!(unit, string(appended_unit))
  end
  return unit
end

function getUnit(op::AbstractPlanOutOpRandom, appended_unit="")
  unit = getArgMixed(op, "unit")
  maybeAppendUnit(unit, appended_unit)
end

function getHash(op::AbstractPlanOutOpRandom, i::AbstractPlanOutMapper, appended_unit="")
  if hasArg(op, "full_salt")
    full_salt = getArg(op, "full_salt")
  else
    salt = getArgString(op, "salt")
    experiment_salt = i.salt
    full_salt = "$experiment_salt.$salt"
  end
  unit_str = join(getUnit(op, appended_unit), ".")
  hash_str = "$full_salt.$unit_str"::ASCIIString
  hex = hexdigest("sha1", hash_str)[1:15]
  parse(Int, "0x$hex")
end

function getUniform(op::AbstractPlanOutOpRandom, i::AbstractPlanOutMapper, min_val=0.0, max_val=1.0, appended_unit="")
  zero_to_one = getHash(op, i, appended_unit) / LONG_SCALE
  min_val + (max_val - min_val) * zero_to_one
end

type RandomFloat <: AbstractPlanOutOpRandom
  args::Dict{AbstractString, Any}
end
randomfloat(args::Dict) = RandomFloat(args)

randExecute(op::RandomFloat, i::AbstractPlanOutMapper) = getUniform(op, i, getArgNumeric(op, "min"), getArgNumeric(op, "max"))

type RandomInteger <: AbstractPlanOutOpRandom
  args::Dict{AbstractString, Any}
end
randominteger(args::Dict) = RandomInteger(args)

function randExecute(op::RandomInteger, i::AbstractPlanOutMapper)
  min_val = getArgInt(op, "min")
  max_val = getArgInt(op, "max")

  min_val + getHash(op, i) % (max_val - min_val + 1)
end

type BernoulliTrial <: AbstractPlanOutOpRandom
  args::Dict{AbstractString, Any}
end
bernoullitrial(args::Dict) = BernoulliTrial(args)

function randExecute(op::BernoulliTrial, i::AbstractPlanOutMapper)
  p = getArgNumeric(op, "p")
  rand_val = getUniform(op, i, 0.0, 1.0)
  return Int(rand_val <= p)
end

type BernoulliFilter <: AbstractPlanOutOpRandom
  args::Dict{AbstractString, Any}
end
bernoullifilter(args::Dict) = BernoulliFilter(args)

function randExecute(op::BernoulliFilter, i::AbstractPlanOutMapper)
  p = getArgNumeric(op, "p")
  choices = getArgList(op, "choices")
  ret = []
  for i in choices
    if getUniform(op, i, 0.0, 1.0) <= p
      push!(ret, i)
    end
  end
  return ret
end

type UniformChoice <: AbstractPlanOutOpRandom
  args::Dict{AbstractString, Any}
end
uniformchoice(args::Dict) = UniformChoice(args)

function randExecute(op::UniformChoice, i::AbstractPlanOutMapper)
  choices = getArgList(op, "choices")
  if length(choices) == 0
    return []
  end
  rand_index = (getHash(op, i) % length(choices)) + 1
  return getindex(choices, rand_index)
end

type WeightedChoice <: AbstractPlanOutOpRandom
  args::Dict{AbstractString, Any}
end
weightedchoice(args::Dict) = WeightedChoice(args)

function randExecute(op::WeightedChoice, m::AbstractPlanOutMapper)
  choices = getArgList(op, "choices")
  weights = getArgList(op, "weights")
  if length(choices) == 0
    return []
  end

  cum_weights = Dict(enumerate(weights))
  cum_sum = 0.0
  for (i, j) in cum_weights
    cum_sum += j
    cum_weights[i] = cum_sum
  end
  stop_val = getUniform(op, m, 0.0, cum_sum)
  for (i, j) in cum_weights
    if stop_val <= j
      return choices[i]
    end
  end
end

type Sample <: AbstractPlanOutOpRandom
  args::Dict{AbstractString, Any}
end
sample(args::Dict) = Sample(args)

function randExecute(op::Sample, m::AbstractPlanOutMapper)
  choices = copy(getArgList(op, "choices"))
  if hasArg(op, "draws")
    num_draws = getArgInt(op, "draws")
  else
    num_draws = length(choices)
  end
  for i in reverse(1:length(choices))
    j = (getHash(op, m, i) % (i + 1)) + 1
    tmp = copy(choices[i])
    choices[i] = choices[j]
    choices[j] = tmp
  end
  choices[1:num_draws]
end


text:
##### Mass tendencies

#####
##### First order fluxes
#####

function flux(::Advect{Mass}, atmos, args)
    return args.state.ρu
end

#####
##### Second order fluxes
#####

function flux(::MoistureDiffusion{Mass}, atmos, args)
    @unpack state, aux, t, diffusive = args
    ν, D_t, τ = turbulence_tensors(atmos, state, diffusive, aux, t)
    d_q_tot = (-D_t) .* diffusive.moisture.∇q_tot
    return d_q_tot * state.ρ
end

#####
##### Sources
#####

function source(s::Subsidence{Mass}, m, args)
    @unpack state, aux, diffusive = args
    z = altitude(m, aux)
    w_sub = subsidence_velocity(s, z)
    k̂ = vertical_unit_vector(m, aux)
    return -state.ρ * w_sub * dot(k̂, diffusive.moisture.∇q_tot)
end

function source(s::RemovePrecipitation{Mass}, m, args)
    @unpack state = args
    @unpack ts = args.precomputed
    if has_condensate(ts)
        nt = remove_precipitation_sources(s, m, args)
        return nt.S_ρ_qt
    else
        FT = eltype(state)
        return FT(0)
    end
end

function source(s::WarmRain_1M{Mass}, m, args)
    nt = warm_rain_sources(m, args)
    return nt.S_ρ_qt
end

function source(s::RainSnow_1M{Mass}, m, args)
    nt = rain_snow_sources(m, args)
    return nt.S_ρ_qt
end

max_stars_repo_name: jm-c/CLIMA

max_issues_repo_name: jm-c/CLIMA

max_forks_repo_name: jm-c/CLIMA


text:
using Test
using VsRecorder
using VsRecorder.VsRecorderBase

@testset "VsRecorder.jl" begin
    @test VsRecorder.download_all_ocr_languages()

    include("poke_pastes.jl")
    include("team_preview.jl")
    include("battle_main.jl")
    include("team_view.jl")
end


text:
"""
blanchardkahn()

Blanchard-Kahn method for solving linear rational
expectations models.

"""

function blanchardkahn(Γ::AbstractArray{T};output_all=false) where T

    A = [Γ.A₁₁ Γ.A₁₂ ; Γ.A₂₁ Γ.A₂₂]
    B = [Γ.B₁₁ Γ.B₁₂ ; Γ.B₂₁ Γ.B₂₂]

    F_ = schur(A,B)
    F  = ordschur(F_,abs.(F_.α./F_.β) .< 1) # push large eigs to the bottom

    nx,ny = size(Γ.A₁₂)
    S₁₁,S₁₂,S₂₁,S₂₂ = blocks(F.S,nx)
    T₁₁,T₁₂,T₂₁,T₂₂ = blocks(F.T,nx)
    Qᵀ₁₁,Qᵀ₁₂,Qᵀ₂₁,Qᵀ₂₂ = blocks(F.Q',nx)
    Zᵀ₁₁,Zᵀ₁₂,Zᵀ₂₁,Zᵀ₂₂ = blocks(F.Z',nx)

    V = inv(Zᵀ₂₂)
    N = V*Zᵀ₂₁
    L = V*(S₂₂\(Qᵀ₂₁*Γ.G₁ + Qᵀ₂₂*Γ.G₂))

    β₁⁻¹ = inv(Γ.B₁₁ - Γ.B₁₂*N)
    C  = β₁⁻¹*(Γ.A₁₁ - Γ.A₁₂*N)
    D  = β₁⁻¹*(Γ.G₁ - Γ.A₁₂*L)

    axes = (Axis(
            C=ViewAxis((1:length(C)), ShapedAxis(size(C), NamedTuple())),
            D=ViewAxis(length(C) .+ (1:length(D)), ShapedAxis(size(D), NamedTuple())),
            ),)
    Φ = ComponentArray([vec(C);vec(D)],axes)

    if output_all
        return Φ,N,L,β₁⁻¹,nx,ny
    else
        return Φ
    end
end

function ChainRulesCore.rrule(::typeof(blanchardkahn), Γ)

    Φ,N,L,β₁⁻¹,nx,ny = blanchardkahn(Γ;output_all=true)

    β₁⁻ᵗ = β₁⁻¹'
    ηᵗ   = β₁⁻ᵗ*(Γ.B₂₁' - N'*Γ.B₂₂')
    nε   = size(Φ.D)[2]

    function blanchardkahn_pullback(ΔΦ)

        # If you use or extend this code, please cite
        # Duncan, Alfred (2021) "Reverse Mode Differentiation for DSGE Models,"
        # University of Kent School of Economics Discussion Papers 2108.

        CAΔΦ = ComponentArray(ΔΦ,getaxes(Φ))

        α₂  = Γ.A₁₂'*ηᵗ-Γ.A₂₂'
        β₂  = Γ.B₂₂'-Γ.B₁₂'*ηᵗ
        Ξ   = kron(Φ.C,β₂) + kron(I(nx),α₂)

        β₁⁻ᵗΔC = β₁⁻ᵗ*CAΔΦ.C
        β₁⁻ᵗΔD = β₁⁻ᵗ*CAΔΦ.D
        βCD = β₁⁻ᵗΔC*Φ.C'+β₁⁻ᵗΔD*Φ.D'

        Q   = inv(α₂)*Γ.A₁₂'*β₁⁻ᵗΔD

        w   = -Ξ\vec(Γ.B₁₂'*βCD - Γ.A₁₂'*β₁⁻ᵗΔC + β₂*Q*Φ.D')
        W   =  reshape(w,ny,nx)

        ∂Γ = similar(Γ)
        ∂Γ.A₁₁ = -ηᵗ*W + β₁⁻ᵗΔC
        ∂Γ.A₂₁ =  W
        ∂Γ.A₂₂ = -W*N' - Q*L'
        ∂Γ.A₁₂ = -ηᵗ*∂Γ.A₂₂ - β₁⁻ᵗΔC*N' - β₁⁻ᵗΔD*L'
        ∂Γ.B₂₁ = -W*Φ.C' - Q*Φ.D'
        ∂Γ.B₁₁ = -ηᵗ*∂Γ.B₂₁ - βCD
        ∂Γ.B₁₂ = -∂Γ.B₁₁*N'
        ∂Γ.B₂₂ = -∂Γ.B₂₁*N'
        ∂Γ.G₁  = -ηᵗ*Q+β₁⁻ᵗΔD
        ∂Γ.G₂  =  Q

        return NO_FIELDS, ∂Γ
    end
    return Φ, blanchardkahn_pullback
end

blocks(A::AbstractArray,nx) =
    A[1:nx,1:nx],A[1:nx,nx+1:size(A)[1]],
    A[nx+1:size(A)[1],1:nx],A[nx+1:size(A)[1],nx+1:size(A)[1]]


text:
@testset "Test functionality of System" begin
    sys = create_rts_system()
    summary(devnull, sys)

    generators = collect(get_components(ThermalStandard, sys))
    generator = get_component(ThermalStandard, sys, get_name(generators[1]))
    @test IS.get_uuid(generator) == IS.get_uuid(generators[1])
    @test_throws(IS.ArgumentError, add_component!(sys, generator))

    generators2 = get_components_by_name(ThermalGen, sys, get_name(generators[1]))
    @test length(generators2) == 1
    @test IS.get_uuid(generators2[1]) == IS.get_uuid(generators[1])

    @test isnothing(get_component(ThermalStandard, sys, "not-a-name"))
    @test isempty(get_components_by_name(ThermalGen, sys, "not-a-name"))
    @test_throws(IS.ArgumentError,
                 get_component(ThermalGen, sys, "not-a-name"))
    @test_throws(IS.ArgumentError,
                 get_components_by_name(ThermalStandard, sys, "not-a-name"))

    # Test get_bus* functionality.
    bus_numbers = Vector{Int}()
    for bus in get_components(Bus, sys)
        push!(bus_numbers, bus.number)
        if length(bus_numbers) >= 2
            break
        end
    end

    bus = PowerSystems.get_bus(sys, bus_numbers[1])
    @test bus.number == bus_numbers[1]

    buses = PowerSystems.get_buses(sys, Set(bus_numbers))
    sort!(bus_numbers)
    sort!(buses, by=x -> x.number)
    @test length(bus_numbers) == length(buses)
    for (bus_number, bus) in zip(bus_numbers, buses)
        @test bus_number == bus.number
    end

    initial_times = get_forecast_initial_times(sys)
    @assert length(initial_times) == 1
    initial_time = initial_times[1]

    # Get forecasts with a label and without.
    components = collect(get_components(HydroDispatch, sys))
    @test !isempty(components)
    component = components[1]
    forecast = get_forecast(Deterministic, component, initial_time, "get_rating")
    @test forecast isa Deterministic

    horizon = get_forecasts_horizon(sys)
    @test horizon == 24
    @test get_forecasts_initial_time(sys) == Dates.DateTime("2020-01-01T00:00:00")
    @test get_forecasts_interval(sys) == Dates.Hour(0)
    resolution = get_forecasts_resolution(sys)
    @test resolution == Dates.Hour(1)

    # Actual functionality is tested in InfrastructureSystems.
    @test generate_initial_times(sys, resolution, horizon)[1] == initial_time
    @test generate_initial_times(component, resolution, horizon)[1] == initial_time
    @test are_forecasts_contiguous(sys)
    @test are_forecasts_contiguous(component)

    clear_forecasts!(sys)
    @test length(collect(iterate_forecasts(sys))) == 0
end

@testset "Test handling of bus_numbers" begin
    sys = create_rts_system()

    @test length(sys.bus_numbers) > 0
    buses = get_components(Bus, sys)
    bus_numbers = sort!([get_number(bus) for bus in buses])
    @test bus_numbers == get_bus_numbers(sys)

    # Remove entire type
    remove_components!(Bus, sys)
    @test length(sys.bus_numbers) == 0

    # Remove individually.
    for bus in buses
        add_component!(sys, bus)
    end
    @test length(sys.bus_numbers) > 0
    for bus in buses
        remove_component!(sys, bus)
    end
    @test length(sys.bus_numbers) == 0

    # Remove by name.
    for bus in buses
        add_component!(sys, bus)
    end
    @test length(sys.bus_numbers) > 0
    for bus in buses
        remove_component!(Bus, sys, get_name(bus))
    end
    @test length(sys.bus_numbers) == 0
end

@testset "Test System iterators" begin
    sys = create_rts_system()

    i = 0
    for component in iterate_components(sys)
        i += 1
    end

    components = get_components(Component, sys)
    @test i == length(components)

    initial_times = get_forecast_initial_times(sys)
    unique_initial_times = Set{Dates.DateTime}()
    for forecast in iterate_forecasts(sys)
        push!(unique_initial_times, get_initial_time(forecast))
    end
    @test initial_times == sort!(collect(unique_initial_times))
end

@testset "Test remove_component" begin
    sys = create_rts_system()
    generators = get_components(ThermalStandard, sys)
    initial_length = length(generators)
    @assert initial_length > 0
    gen = collect(generators)[1]

    remove_component!(sys, gen)

    @test isnothing(get_component(typeof(gen), sys, get_name(gen)))
    generators = get_components(typeof(gen), sys)
    @test length(generators) == initial_length - 1

    @test_throws(IS.ArgumentError, remove_component!(sys, gen))

    add_component!(sys, gen)
    remove_component!(typeof(gen), sys, get_name(gen))
    @test isnothing(get_component(typeof(gen), sys, get_name(gen)))

    @assert length(get_components(typeof(gen), sys)) > 0
    remove_components!(typeof(gen), sys)
    @test_throws(IS.ArgumentError, remove_components!(typeof(gen), sys))
end

@testset "Test missing Arc bus" begin
    sys = System(100)
    line = Line(nothing)
    @test_throws(IS.ArgumentError, add_component!(sys, line))
end

@testset "Test exported names" begin
    @test IS.validate_exported_names(PowerSystems)
end


text:
using OpenQuantumTools, Random, Test, OrdinaryDiffEq

H = DenseHamiltonian([(s) -> 1.0], [σi], unit = :ħ)
u0 = PauliVec[1][1]
coupling = ConstantCouplings(["Z"], unit = :ħ)

# test for OhmicBath
bath = Ohmic(1e-4, 4, 16)
annealing = Annealing(H, u0; coupling = coupling, bath = bath)
tf = 10

γ = spectrum(0, bath)

sol = solve_ame(
    annealing,
    tf,
    alg = Tsit5(),
    ω_hint = range(-2, 2, length = 100),
    abstol = 1e-8,
    reltol = 1e-8,
)
@test sol(tf)[1, 2] ≈ exp(-2 * γ * tf) * 0.5 atol = 1e-5 rtol = 1e-5

sol = solve_ame(
    annealing,
    tf,
    alg = Tsit5(),
    one_sided=true,
    ω_hint = range(-2, 2, length = 100),
    abstol = 1e-8,
    reltol = 1e-8,
)
@test sol(tf)[1, 2] ≈ exp(-2 * γ * tf) * 0.5 atol = 1e-5 rtol = 1e-5

tf = 1000
prob = build_ensembles(annealing, tf, :ame, save_positions=(true, true))
Random.seed!(1234)
sol1 = solve(prob, Tsit5(), EnsembleSerial(), trajectories=1, save_everystep=false)[1]

prob = build_ensembles(annealing, tf, :ame)
Random.seed!(1234)
sol2 = solve(prob, Tsit5(), EnsembleSerial(), trajectories=1, save_everystep=false)[1]

@test !(length(sol1) == 2)
@test sol1[end] ≈ sol2[end]

# test suite for hybrid spin-fluctuator and ame
fbath = EnsembleFluctuator([0.1], [0.1])
interactions = InteractionSet(Interaction(coupling, bath), Interaction(coupling, fbath))
annealing = Annealing(H, u0; interactions=interactions)

tf = 1000
prob = build_ensembles(annealing, tf, :ame, save_positions=(true, true))
Random.seed!(1234)
sol1 = solve(prob, Tsit5(), EnsembleSerial(), trajectories=1, save_everystep=false)[1]

prob = build_ensembles(annealing, tf, :ame)
Random.seed!(1234)
sol2 = solve(prob, Tsit5(), EnsembleSerial(), trajectories=1, save_everystep=false)[1]

@test !(length(sol1) == 2)
@test length(sol2) == 2


text:
module API

import PkgMirrors: MIRRORS
import ..Types: PkgMirror
import ..Utils: setcache, getcache, delcache, current, CURRENT
import ..PKG

availables() = collect(keys(MIRRORS))

function setmirror(name::AbstractString, url::AbstractString)
    CURRENT[] = PkgMirror(name, url)
    try
        PKG.activate()
    catch e
        CURRENT[] = nothing
        rethrow(e)
    end
    setcache("$name $url", "current.txt")
    @info "PkgMirror $name activated."
    return CURRENT[]
end

function setmirror(name::AbstractString)
    haskey(MIRRORS, name) || error("Please specify an url for unknown mirror.")
    setmirror(name, MIRRORS[name])
end

function activate(;first=true)
    if activated()
        @warn "Already activated."
        return
    end
    current_mirror = getcache("current.txt")
    if current_mirror !== nothing
        tmp = split(current_mirror, ' ')
        @info "Using saved mirror: $(tmp[1]) ($(tmp[2]))"
        mirror = setmirror(tmp[1], tmp[2])
        return mirror
    elseif first
        @error "Please use `setmirror` for the first time."
    end
    return nothing
end

function deactivate()
    current_mirror = current()
    if current_mirror === nothing
        @warn "No mirror activated."
    else
        PKG.deactivate()
        CURRENT[] = nothing
        @info "PkgMirror $(current_mirror.name) deactivated."
    end
end

function clear()
    activated() && deactivate()
    delcache("current.txt")
    delcache("registries.txt")
    @info "Cache clear."
end

function activated()
    current() !== nothing
end

end


text:
using SMLMData

"""
    connect1DS!(smld::SMLMData.SMLD2D, dataset::Int; maxframegap::Int = 5)

Define the "ideal" frame-connection result for simulated `smld` with one dataset.

# Description
This is a helper function which defines the ideal FC result for a single
dataset.  The user should probably not bother using this, and instead should
call defineidealFC().

# Inputs
- `smld`: SMLMData.SMLD2D with smld.connectID populated to indicate emitter ID.
- `dataset`: Dataset number to be connected.
- `maxframegap`: Maximum frame gap allowed between localizations connected in
                 the "ideal" result.
"""
function connect1DS!(smld::SMLMData.SMLD2D, dataset::Int; maxframegap::Int = 5)
    # Loop through associated localizations and combine them as appropriate.
    currentDS = smld.datasetnum .== dataset
    smld_current = smld[currentDS]
    maxID = maximum(smld.connectID)
    smld_current.connectID .+= maxID # recompressed later
    for ii in unique(smld_current.connectID)
        # Determine which localizations belong to the ii-th emitter.
        currentinds = findall(smld_current.connectID .== ii)

        # Sort these localizations with respect to their frame.
        framenum = smld_current.framenum[currentinds]
        sortinds = sortperm(framenum)
        sortedframes = framenum[sortinds]
        sorted_currentinds = currentinds[sortinds]

        # If all of these localizations are within the framegap, no action is
        # needed (they already share the same connectID).
        framediff = diff(sortedframes)
        if all(framediff .<= maxframegap)
            continue
        end

        # Determine which localizations we can combine.
        for ff = 1:length(framediff)
            if framediff[ff] <= maxframegap
                # Connect these localizations.
                smld_current.connectID[sorted_currentinds[ff+1]] =
                    smld_current.connectID[sorted_currentinds[ff]]
            else
                # Localization ff+1 should be a new blinking event.
                maxID += 1
                smld_current.connectID[sorted_currentinds[ff+1]] = maxID
            end
        end
    end

    # Update smld.connectID for the current dataset.
    smld.connectID[currentDS] = smld_current.connectID

    return smld
end

function connect1DS(smld::SMLMData.SMLD2D, dataset::Int; maxframegap::Int = 5)
    return connect1DS!(deepcopy(smld))
end

"""
    smld, smld_combined = defineidealFC!(smld::SMLMData.SMLD2D;
        maxframegap::Int = 5)

Define the "ideal" frame-connection result for a simulated `smld`.

# Description
This function defines the "ideal" frame connection result from a simulation.
That is to say, for a simulated SMLD2D structure `smld` with `connectID` field
populated to indicate emitter membership of localizations, this function will
generate an "ideal" FC result which combines all blinking events that appeared
with frame gaps less than `maxframegap` of one another.  Note that for very
high duty cycles, multiple blinking events might be mistakingly combined by
this method (i.e., if the emitter blinks back on within `maxframegap` frames
of its previous blink).  Note that localizations are not allowed to be
connected across datasets.

# Inputs
-`smld`: SMLMData.SMLD2D with smld.connectID populated to indicate emitter ID.
-`maxframegap`: Maximum frame gap allowed between localizations connected in
                the "ideal" result.

# Outputs
-`smld_out`: Ideal frame-connection result on input `smld` with localizations
             combined as appropriate.
-`smld_connected`: Copy of the input `smld` with smld_connected.connectID
                   modified to reflect blinking event ID (i.e., `smld_out` is
                   generated as smld_out = combinelocalizations(smld_connected))
"""
function defineidealFC!(smld::SMLMData.SMLD2D; maxframegap::Int = 5)
    # Loop through datasets and combine localizations as appropriate.
    for ii in unique(smld.datasetnum)
        connect1DS!(smld, ii; maxframegap = maxframegap)
    end
    compress_connectID!(smld.connectID)

    return smld, combinelocalizations(smld)
end

function defineidealFC(smld::SMLMData.SMLD2D; maxframegap::Int = 5)
    return defineidealFC!(deepcopy(smld); maxframegap = maxframegap)
end


text:
module NEAT

include("config.jl")

# track count of various global variables & holds refernce to the config
struct Global
    speciesCnt::Int
    chromosomeCnt::Int
    nodeCnt::Int
    innov_number::Int
    innovations::Dict{Tuple{Int,Int},Int}
    cf::Config
    function Global(cf::Config)
        new(0,0,0,0,Dict{(Int,Int),Int},cf) # global dictionary
    end
end

include("genome.jl")
# include("chromosome.jl")
# include("species.jl")
# include("population.jl")
# include("../networks/nn.jl")

# export Input, Output, Hidden

end # module


text:
using Base: fieldindex, RefValue, unsafe_convert
using Base.Libc: calloc, free
using Base.GC: @preserve

using VideoIO: NestedCStruct, nested_wrap, field_ptr, check_ptr_valid, @avptr

struct TestChildStruct
    e::NTuple{4,Int}
    f::Cint
    g::Ptr{Int}
end

struct TestParentStruct
    a::Int
    b::Float64
    c::Ptr{Int}
    d::Ptr{TestChildStruct}
end

# Clang.jl generated structs already has this utility
function Base.getproperty(x::Ptr{<:Union{TestChildStruct,TestParentStruct}}, f::Symbol)
    T = eltype(x)
    fieldpos = fieldindex(T, f)
    field_pointer = convert(Ptr{fieldtype(T, fieldpos)}, x + fieldoffset(T, fieldpos))
    return field_pointer
end

function make_TestChildStruct(; e::NTuple{4,Int} = (1, 2, 3, 4), f::Cint = Cint(-1), g::Ptr{Int} = Ptr{Int}())
    mem = Vector{UInt8}(undef, sizeof(TestChildStruct))
    mem_ptr = pointer(mem)
    @preserve mem begin
        e_arr = unsafe_wrap(Array, convert(Ptr{Int}, mem_ptr), (4,))
        e_arr .= e
        f_ptr = convert(Ptr{Cint}, mem_ptr + fieldoffset(TestChildStruct, 2))
        unsafe_store!(f_ptr, f)
        g_ptr = convert(Ptr{Ptr{Int}}, mem_ptr + fieldoffset(TestChildStruct, 3))
        unsafe_store!(g_ptr, g)
    end
    return mem
end

function make_TestParentStruct(;
    a::Int = -1,
    b::Float64 = 0.0,
    c::Ptr{Int} = Ptr{Int}(),
    d::Ptr{TestChildStruct} = Ptr{TestChildStruct}(),
)
    mem = Vector{UInt8}(undef, sizeof(TestParentStruct))
    mem_ptr = pointer(mem)
    @preserve mem begin
        a_ptr = convert(Ptr{Int}, mem_ptr)
        unsafe_store!(a_ptr, a)
        b_ptr = convert(Ptr{Float64}, mem_ptr + fieldoffset(TestParentStruct, 2))
        unsafe_store!(b_ptr, b)
        c_ptr = convert(Ptr{Ptr{Int}}, mem_ptr + fieldoffset(TestParentStruct, 3))
        unsafe_store!(c_ptr, c)
        d_ptr = convert(Ptr{Ptr{TestChildStruct}}, mem_ptr + fieldoffset(TestParentStruct, 4))
        unsafe_store!(d_ptr, d)
    end
    return mem
end

@testset "Pointer utilities" begin
    child_ptr_null = Ptr{TestChildStruct}()
    e_ptr = field_ptr(child_ptr_null, :e)
    @test e_ptr == child_ptr_null
    @test e_ptr isa Ptr{NTuple{4,Int}}
    f_ptr_pos = child_ptr_null + fieldoffset(TestChildStruct, 2)
    f_ptr = field_ptr(child_ptr_null, :f)
    @test f_ptr == f_ptr_pos
    @test f_ptr isa Ptr{Cint}
    f_ptr_UInt32 = field_ptr(UInt32, child_ptr_null, :f)
    @test f_ptr_UInt32 == f_ptr_pos
    @test f_ptr_UInt32 isa Ptr{UInt32}

    @test check_ptr_valid(C_NULL, false) == false
    @test_throws ErrorException check_ptr_valid(C_NULL)
    @test check_ptr_valid(f_ptr) == true
    @test check_ptr_valid(f_ptr, false) == true
end

@testset "AVPtr" begin
    @testset "NestedCStruct" begin
        g = convert(Ptr{Int}, calloc(4, sizeof(Int)))
        try
            g_arr = unsafe_wrap(Array, g, (4,))
            g_arr .= 1:4
            child_mem = make_TestChildStruct(; g = g)
            child_ptr = convert(Ptr{TestChildStruct}, pointer(child_mem))
            @preserve child_mem begin
                parent_mem = make_TestParentStruct(; c = g, d = child_ptr)
                parent_ptr = convert(Ptr{TestParentStruct}, pointer(parent_mem))
                @preserve parent_mem begin
                    parent_nested_ptr = NestedCStruct(parent_ptr)
                    @testset "NestedCStruct constructors" begin
                        @test parent_nested_ptr isa NestedCStruct{TestParentStruct}
                        inner_ref = getfield(parent_nested_ptr, :data)
                        @test inner_ref isa Ref{Ptr{TestParentStruct}}
                        @test inner_ref[] == parent_ptr
                        void_pointer = convert(Ptr{Nothing}, parent_ptr)
                        parent_nested_ptr_from_null = NestedCStruct{TestParentStruct}(void_pointer)
                        @test parent_nested_ptr_from_null isa NestedCStruct{TestParentStruct}
                        inner_ref_from_null = getfield(parent_nested_ptr_from_null, :data)
                        @test inner_ref_from_null isa Ref{Ptr{TestParentStruct}}
                        @test inner_ref_from_null[] == parent_ptr
                        nested_null_ptr = NestedCStruct(C_NULL)
                        @test nested_null_ptr isa NestedCStruct{Nothing}
                    end

                    @testset "NestedCStruct pointer conversion" begin
                        derived_ptr = unsafe_convert(Ptr{TestParentStruct}, parent_nested_ptr)
                        @test derived_ptr == parent_ptr
                        derived_dptr = unsafe_convert(Ptr{Ptr{TestParentStruct}}, parent_nested_ptr)
                        @test derived_dptr isa Ptr{Ptr{TestParentStruct}}
                        @test unsafe_load(derived_dptr) == parent_ptr
                        derived_null_ptr = unsafe_convert(Ptr{Nothing}, parent_nested_ptr)
                        @test derived_null_ptr isa Ptr{Nothing}
                        @test derived_null_ptr == parent_ptr
                        derived_null_dptr = unsafe_convert(Ptr{Ptr{Nothing}}, parent_nested_ptr)
                        @test derived_null_dptr isa Ptr{Ptr{Nothing}}
                        @test unsafe_load(derived_null_dptr) == parent_ptr
                    end

                    @testset "NestedCStruct field pointers" begin
                        b_field_ptr = convert(Ptr{Float64}, parent_ptr + fieldoffset(TestParentStruct, 2))
                        @test field_ptr(parent_nested_ptr, :b) == b_field_ptr
                        @test field_ptr(Int, parent_nested_ptr, :b) == convert(Ptr{Int}, b_field_ptr)
                    end

                    @testset "NestedCStruct interface" begin
                        @test parent_nested_ptr.a == -1
                        @test parent_nested_ptr.b == 0.0
                        @test parent_nested_ptr.c isa NestedCStruct{Int}
                        @test getfield(parent_nested_ptr.c, :data)[] == g
                        @test parent_nested_ptr.c[1] == 1
                        @test parent_nested_ptr.c[2] == 2
                        @test parent_nested_ptr.c[3] == 3
                        @test parent_nested_ptr.c[4] == 4
                        child_nested_ptr = parent_nested_ptr.d
                        @test child_nested_ptr isa NestedCStruct{TestChildStruct}
                        @test getfield(child_nested_ptr, :data)[] == child_ptr
                        @test child_nested_ptr.e == (1, 2, 3, 4)
                        @test child_nested_ptr.f == Cint(-1)
                        @test getfield(child_nested_ptr.g, :data)[] == g
                    end

                    @testset "NestedCStruct convenience functions" begin
                        @test propertynames(parent_nested_ptr) == (:a, :b, :c, :d, :data)
                    end
                end
            end
        finally
            free(g)
        end
    end
end


text:
using ComoniconTargetExpr
using Documenter

DocMeta.setdocmeta!(ComoniconTargetExpr, :DocTestSetup, :(using ComoniconTargetExpr); recursive=true)

makedocs(;
    modules=[ComoniconTargetExpr],
    authors="Roger-Luo <rogerluo.rl18@gmail.com> and contributors",
    repo="https://github.com/comonicon/ComoniconTargetExpr.jl/blob/{commit}{path}#{line}",
    sitename="ComoniconTargetExpr.jl",
    format=Documenter.HTML(;
        prettyurls=get(ENV, "CI", "false") == "true",
        canonical="https://comonicon.github.io/ComoniconTargetExpr.jl",
        assets=String[],
    ),
    pages=[
        "Home" => "index.md",
    ],
)

deploydocs(;
    repo="github.com/comonicon/ComoniconTargetExpr.jl",
)


text:
using DataStructures: OrderedDict

@testset "config" begin
    @testset "configure" begin
        @testset "default" begin
            c = Cropbox.Config()
            @test Cropbox.configure() == c
            @test Cropbox.configure(()) == c
            @test Cropbox.configure(nothing) == c
        end

        @testset "error" begin
            @test_throws ErrorException Cropbox.configure(missing)
            @test_throws ErrorException Cropbox.configure(0)
            @test_throws ErrorException Cropbox.configure(:a)
            @test_throws ErrorException Cropbox.configure("a")
        end
    end

    @testset "system" begin
        @testset "pair" begin
            c = :S => :a => 1
            C = Cropbox.configure(c)
            @test C[:S][:a] == 1
        end

        @testset "tuple" begin
            c = (:S1 => :a => 1, :S2 => :a => 2)
            C = Cropbox.configure(c)
            @test C[:S1][:a] == 1
            @test C[:S2][:a] == 2
        end

        @testset "vector" begin
            c = [:S1 => :a => 1, :S2 => :a => 2]
            C = Cropbox.configure(c)
            C1 = Cropbox.configure(c[1])
            C2 = Cropbox.configure(c[2])
            @test C == [C1, C2]
        end

        @testset "type" begin
            @system SConfigSystemType begin
                a ~ preserve(parameter)
            end
            c = SConfigSystemType => :a => 1
            C = Cropbox.configure(c)
            @test C[:SConfigSystemType][:a] == 1
        end
    end

    @testset "variable" begin
        @testset "dict" begin
            v = Dict(:a => 1, :b => 2)
            C = Cropbox.configure(:S => v)
            @test C[:S][:a] == 1
            @test C[:S][:b] == 2
        end

        @testset "ordered dict" begin
            v = OrderedDict(:a => 1, :b => 2)
            C = Cropbox.configure(:S => v)
            @test C[:S][:a] == 1
            @test C[:S][:b] == 2
        end

        @testset "tuple" begin
            v = (:a => 1, :b => 2)
            C = Cropbox.configure(:S => v)
            @test C[:S][:a] == 1
            @test C[:S][:b] == 2
        end

        @testset "named tuple" begin
            v = (a=1, b=2)
            C = Cropbox.configure(:S => v)
            @test C[:S][:a] == 1
            @test C[:S][:b] == 2
        end

        @testset "pair" begin
            v = :a => 1
            C = Cropbox.configure(:S => v)
            @test C[:S][:a] == 1
        end
    end

    @testset "value" begin
        @testset "dict" begin
            v = Dict(:a => 1, :b => 2)
            C = Cropbox.configure(:S => :v => v)
            @test C[:S][:v] == v
        end

        @testset "ordered dict" begin
            v = OrderedDict(:a => 1, :b => 2)
            C = Cropbox.configure(:S => :v => v)
            @test C[:S][:v] == v
        end

        @testset "tuple" begin
            v = (:a => 1, :b => 2)
            C = Cropbox.configure(:S => :v => v)
            @test C[:S][:v] == v
        end

        @testset "named tuple" begin
            v = (a=1, b=2)
            C = Cropbox.configure(:S => :v => v)
            @test C[:S][:v] == v
        end

        @testset "pair" begin
            v = :a => 1
            C = Cropbox.configure(:S => :v => v)
            @test C[:S][:v] == v
        end
    end

    @testset "check" begin
        @system SConfigCheck begin
            a ~ preserve(parameter)
            b: bb ~ preserve(parameter)
        end
        @test Cropbox.configure(SConfigCheck => :a => 1) == @config(:SConfigCheck => :a => 1)
        @test_throws ErrorException Cropbox.configure(SConfigCheck => :aa => 2)
        @test Cropbox.configure(SConfigCheck => :b => 3) == @config(:SConfigCheck => :b => 3)
        @test Cropbox.configure(SConfigCheck => :bb => 4) == @config(:SConfigCheck => :bb => 4)
    end

    @testset "merge" begin
        @testset "tuple" begin
            c = (:S1 => (:a => 1, :b => 2), :S2 => (:a => 3, :b => 4))
            C = Cropbox.configure(c)
            @test C[:S1][:a] == 1
            @test C[:S1][:b] == 2
            @test C[:S2][:a] == 3
            @test C[:S2][:b] == 4
        end

        @testset "tuple override" begin
            c = (:S => (:a => 1, :b => 2), :S => :b => 3)
            C = Cropbox.configure(c)
            @test C[:S][:a] == 1
            @test C[:S][:b] == 3
        end

        @testset "config" begin
            C1 = Cropbox.configure(:S1 => :a => 1)
            C2 = Cropbox.configure(:S2 => :a => 2)
            C3 = Cropbox.configure(:S3 => :a => 3)
            C = Cropbox.configure(C1, C2, C3)
            @test C[:S1][:a] == 1
            @test C[:S2][:a] == 2
            @test C[:S3][:a] == 3
        end

        @testset "config override" begin
            C1 = Cropbox.configure(:S => :a => 1)
            C2 = Cropbox.configure(:S => :a => 2)
            C = Cropbox.configure(C1, C2)
            @test C[:S][:a] == 2
        end
    end

    @testset "multiply" begin
        @testset "solo" begin
            p = :S => :a => 1:2
            C = Cropbox.configmultiply(p)
            @test C == Cropbox.configure.([:S => :a => 1, :S => :a => 2])
        end

        @testset "duo" begin
            p1 = :S => :a => 1:2
            p2 = :S => :b => 3:4
            C = Cropbox.configmultiply(p1, p2)
            @test C == Cropbox.configure.([
                :S => (a=1, b=3), :S => (a=1, b=4),
                :S => (a=2, b=3), :S => (a=2, b=4),
            ])
        end

        @testset "trio" begin
            p1 = :S => :a => 1:2
            p2 = :S => :b => 3:4
            p3 = :S => :c => 5:6
            C = Cropbox.configmultiply(p1, p2, p3)
            @test C == Cropbox.configure.([
                :S => (a=1, b=3, c=5), :S => (a=1, b=3, c=6),
                :S => (a=1, b=4, c=5), :S => (a=1, b=4, c=6),
                :S => (a=2, b=3, c=5), :S => (a=2, b=3, c=6),
                :S => (a=2, b=4, c=5), :S => (a=2, b=4, c=6),
            ])
        end

        @testset "base" begin
            b = (:S => :c => 1)
            p1 = :S => :a => 1:2
            p2 = :S => :b => 3:4
            C = Cropbox.configmultiply(p1, p2; base=b)
            @test C == Cropbox.configure.([
                :S => (c=1, a=1, b=3), :S => (c=1, a=1, b=4),
                :S => (c=1, a=2, b=3), :S => (c=1, a=2, b=4),
            ])
        end

        @testset "array" begin
            b = (:S => :c => 1)
            p1 = :S => :a => 1:2
            p2 = :S => :b => 3:4
            p = [p1, p2]
            C1 = Cropbox.configmultiply(p1, p2; base=b)
            C2 = Cropbox.configmultiply(p; base=b)
            @test C1 == C2
        end

        @testset "array single" begin
            p = [:S => :a => 1]
            C = Cropbox.configmultiply(p)
            @test C isa Array && length(C) == 1
            @test C[1] == Cropbox.configure(p[1])
        end

        @testset "array single empty" begin
            p = [()]
            C = Cropbox.configmultiply(p)
            @test C isa Array && length(C) == 1
            @test C[1] == Cropbox.configure(p[1])
        end

        @testset "empty tuple" begin
            p = ()
            C = Cropbox.configmultiply(p)
            @test C isa Array && length(C) == 1
            @test C[1] == Cropbox.configure(p)
        end
    end

    @testset "expand" begin
        @testset "patch" begin
            p = :S => :a => [1, 2]
            C = Cropbox.configexpand(p)
            @test C == Cropbox.configure.([:S => :a => 1, :S => :a => 2])
        end

        @testset "patch with base" begin
            b = :S => :b => 0
            p = :S => :a => [1, 2]
            C = Cropbox.configexpand(p; base=b)
            @test C == Cropbox.configure.([:S => (a=1, b=0), :S => (a=2, b=0)])
        end

        @testset "range" begin
            p = :S => :a => 1:2
            C = Cropbox.configexpand(p)
            @test C == Cropbox.configure.([:S => :a => 1, :S => :a => 2])
        end

        @testset "single" begin
            p = :S => :a => 1
            C = Cropbox.configexpand(p)
            @test C isa Array && length(C) == 1
            @test C[1] == Cropbox.configure(p)
        end

        @testset "empty" begin
            p = ()
            C = Cropbox.configexpand(p)
            @test C isa Array && length(C) == 1
            @test C[1] == Cropbox.configure(p)
        end
    end

    @testset "rebase" begin
        @testset "nonempty configs + nonempty base" begin
            b = (:S => :b => 0)
            C0 = [:S => :a => 1, :S => :a => 2]
            C1 = Cropbox.configrebase(C0; base=b)
            @test C1 == Cropbox.configure.([:S => (a=1, b=0), :S => (a=2, b=0)])
        end

        @testset "nonempty configs + empty base" begin
            C0 = [:S => :a => 1, :S => :a => 2]
            C1 = Cropbox.configrebase(C0)
            @test C1 == Cropbox.configure.(C0)
        end

        @testset "empty configs + nonempty base" begin
            b = :S => :a => 1
            C0 = []
            C1 = Cropbox.configrebase(C0; base=b)
            @test C1 == [Cropbox.configure(b)]
        end

        @testset "empty configs + empty base" begin
            C0 = []
            C1 = Cropbox.configrebase(C0)
            @test C1 == [Cropbox.configure()]
        end

        @testset "single config + single base" begin
            c = :S => :a => 1
            b = :S => :b => 2
            C1 = Cropbox.configrebase(c; base=b)
            C2 = Cropbox.configure(b, c)
            @test C1 isa Array && length(C1) == 1
            @test C1[1] == C2
        end
    end

    @testset "reduce" begin
        @testset "array + single" begin
            a = [:S => :a => 1, :S => :a => 2]
            b = :S => :a => 0
            C1 = Cropbox.configreduce(a, b)
            C2 = Cropbox.configure.([b, b])
            @test C1 == C2
        end

        @testset "single + array" begin
            a = :S => :a => 0
            b = [:S => :a => 1, :S => :a => 2]
            C1 = Cropbox.configreduce(a, b)
            C2 = Cropbox.configure(b)
            @test C1 == C2
        end

        @testset "array + array" begin
            a = [:S => :a => 1, :S => :a => 2]
            b = [:S => :a => 3, :S => :a => 4]
            C1 = Cropbox.configreduce(a, b)
            C2 = Cropbox.configure(b)
            @test C1 == C2
        end

        @testset "single + single" begin
            a = :S => :a => 1
            b = :S => :a => 2
            C1 = Cropbox.configreduce(a, b)
            C2 = Cropbox.configure(b)
            @test C1 == C2
        end
    end

    @testset "macro" begin
        @testset "merge" begin
            a = :S => :a => 1
            b = :S => :b => 2
            C1 = @config a + b
            C2 = Cropbox.configure(a, b)
            @test C1 == C2
        end

        @testset "merge multiple" begin
            a = :S => :a => 1
            b = :S => :b => 2
            c = :S => :c => 3
            C1 = @config a + b + c
            C2 = Cropbox.configure(a, b, c)
            @test C1 == C2
        end

        @testset "rebase" begin
            a = :S => :a => 1
            b = [:S => :b => 1, :S => :b => 2]
            C1 = @config a + b
            C2 = Cropbox.configrebase(b; base=a)
            @test C1 == C2
        end

        @testset "reduce" begin
            a = :S => :a => 0
            b = [:S => :a => 1, :S => :a => 2]
            c1(a, b) = @config a + b
            c2(a, b) = Cropbox.configreduce(a, b)
            @test c1(a, b) == c2(a, b)
            @test c1(b, a) == c2(b, a)
            @test c1(b, b) == c2(b, b)
            @test c1(a, a) == c2(a, a)
        end

        @testset "multiply" begin
            a = :S => :a => [1, 2]
            b = :S => :b => [3, 4]
            C1 = @config a * b
            C2 = Cropbox.configmultiply(a, b)
            @test C1 == C2
        end

        @testset "multiply with base" begin
            a = :S => :a => [1, 2]
            b = :S => :b => [3, 4]
            c = :S => :c => 0
            C1 = @config c + a * b
            C2 = Cropbox.configmultiply(a, b; base=c)
            @test C1 == C2
        end

        @testset "expand" begin
            a = :S => :a => [1, 2]
            C1 = @config !a
            C2 = Cropbox.configexpand(a)
            @test C1 == C2
        end

        @testset "expand with base" begin
            a = :S => :a => [1, 2]
            b = :S => :b => 1
            C1 = @config b + !a
            C2 = Cropbox.configexpand(a; base=b)
            @test C1 == C2
        end

        @testset "single" begin
            a = :S => :a => 1
            C1 = @config a
            C2 = Cropbox.configure(a)
            @test C1 == C2
        end

        @testset "multi" begin
            c1 = :S => :a => 1
            c2 = :S => :b => 2
            c3 = :S => :c => 3
            c = Cropbox.configure(c1, c2, c3)
            C1 = @config(c1, c2, c3)
            C2 = @config((c1, c2, c3))
            C3 = @config (c1, c2, c3)
            C4 = @config c1, c2, c3
            @test C1 == c
            @test C2 == c
            @test C3 == c
            @test C4 == c
        end

        @testset "empty" begin
            c = Cropbox.Config()
            C1 = @config
            C2 = @config()
            C3 = @config ()
            @test C1 == c
            @test C2 == c
            @test C3 == c
        end
    end

    @testset "string" begin
        @testset "basic" begin
            c = "S.a" => 1
            C = Cropbox.configure(c)
            @test C[:S][:a] == 1
        end

        @testset "system" begin
            c = "S" => :a => 1
            C = Cropbox.configure(c)
            @test C[:S][:a] == 1
        end

        @testset "error" begin
            c = "S.a.b" => 1
            @test_throws ErrorException Cropbox.configure(c)
        end
    end

    @testset "calibrate" begin
        c = (:S1 => (:a => 1, :b => 2), :S2 => :c => 3)
        C = Cropbox.configure(c)
        K = Cropbox.parameterkeys(C)
        @test K == [(:S1, :a), (:S1, :b), (:S2, :c)]
        V = Cropbox.parametervalues(C)
        @test V == [1, 2, 3]
        P = Cropbox.parameterzip(K, V)
        @test P == C
        @test P[:S1][:a] == 1
        @test P[:S1][:b] == 2
        @test P[:S2][:c] == 3
    end

    @testset "parameters" begin
        @testset "basic" begin
            @system SConfigParameters begin
                a => 1 ~ preserve(parameter)
            end
            c = Cropbox.parameters(SConfigParameters)
            @test c[:SConfigParameters][:a] == 1
        end

        @testset "unit" begin
            @system SConfigParametersUnit begin
                a => 1 ~ preserve(u"m", parameter)
            end
            c = Cropbox.parameters(SConfigParametersUnit)
            @test c[:SConfigParametersUnit][:a] == 1u"m"
        end

        @testset "missing" begin
            @system SConfigParametersMissing begin
                a => 1 ~ preserve(parameter)
                b(a) => a ~ preserve(parameter)
            end
            c = Cropbox.parameters(SConfigParametersMissing)
            @test c[:SConfigParametersMissing][:a] == 1
            @test c[:SConfigParametersMissing][:b] === missing
        end

        @testset "alias" begin
            @system SConfigParametersAlias begin
                a: aa => 1 ~ preserve(parameter)
            end
            c = Cropbox.parameters(SConfigParametersAlias, alias=true)
            @test_throws KeyError c[:SConfigParametersAlias][:a]
            @test c[:SConfigParametersAlias][:aa] == 1
        end

        @testset "recursive" begin
            @system SConfigParametersRecursiveChild begin
                b => 2 ~ preserve(parameter)
            end
            @system SConfigParametersRecursive begin
                s ~ ::SConfigParametersRecursiveChild
                a => 1 ~ preserve(parameter)
            end
            c = Cropbox.parameters(SConfigParametersRecursive, recursive=true)
            @test haskey(c, :Context)
            @test haskey(c, :Clock)
            @test c[:SConfigParametersRecursive][:a] == 1
            @test c[:SConfigParametersRecursiveChild][:b] == 2
        end

        @testset "exclude" begin
            @system SConfigParametersExclude begin
                a => 1 ~ preserve(parameter)
            end
            X = (Cropbox.Context,)
            c = Cropbox.parameters(SConfigParametersExclude, recursive=true, exclude=X)
            @test !haskey(c, :Context)
            @test !haskey(c, :Clock)
            @test c[:SConfigParametersExclude][:a] == 1
        end
    end

    @testset "option" begin
        @testset "private" begin
            @system SConfigOptionPrivate(Controller) begin
                _a ~ preserve(parameter)
                a ~ preserve(parameter)
                b(_a) => 2a ~ track
                c(a) => 3a ~ track
            end
            c = :SConfigOptionPrivate => (_a = 1, a = 2)
            s = instance(SConfigOptionPrivate, config=c)
            @test s.b' == 2
            @test s.c' == 6
        end
    end
end


text:
using JLD2; using DelimitedFiles; using Printf; using PyPlot
using Statistics

include("../../src/regress.jl")
include("../../src/NbodyGradient/src/ttv.jl")

# Load in the posterior from the transit-timing analysis:
@load "../../data/T1_hmc_total_02212020.jld2"

# Make a larger tmax to forecast to first ~2 years of JWST
# (given the current launch date of October 31, 2021):
tmax = 3000.0

# Boolean that determines if the recompute the posterior transit
# times:
recompute = true

# Set up parameters needed for time-integration:
n = 8; nplanet=7; t0 = 7257.93115525; h = 0.06; rstar =1e12

pname = ["b","c","d","e","f","g","h"]
# Read in orbital elements
elements = readdlm("../../data/elements_noprior_students.txt",',')

# Make an array, tti1,  to hold transit times:
ntt = zeros(Int64,n);
for j=2:n
  ntt[j] = ceil(Int64,tmax/elements[j,2])+1
end

# Run 10^3 realizations of the posterior:
ngrid = 1000
#ngrid = 10
tt1 = zeros(n,maximum(ntt))
tt_grid = zeros(n,maximum(ntt),ngrid)
count1 = zeros(Int64,n)
elements_grid = zeros(n,7,ngrid)

nsamples = size(state_total)[2]
if recompute
# The following code computes the posterior samples.
# However, this takes some time, so these have been saved to a file,
# and can be restored instead.
  for igrid=1:ngrid
    # Select a state from the transit-timing analysis:
    x = state_total[1:35,ceil(Int64,rand()*nsamples)]
    # Insert into the orbital element array:
    for i=2:n, j=1:5
      elements[i,j] = x[(i-2)*5+j]
    end
    @time dq0 = ttv_elements!(n,t0,h,tmax,elements,tt1,count1,0.0,0,0,rstar)
    elements_grid[:,:,igrid] = elements
    tt_grid[:,:,igrid] = tt1
  end
  @save "../../data/T1_timing_posterior.jld2" n nplanet t0 h rstar tt1 count1 ngrid elements_grid tt_grid
else
  # Here is where the timing results are restored:
  @load "../../data/T1_timing_posterior.jld2"
end

# Now, create two tables to save these, one with the measured times
# and posterior, and one with the forecast times:

include("../../src/extract_planet.jl")
data = readdlm("../../data/T1_timings_20191203.txt",',')

# Read in the sources of each of the transit times:
sources = readdlm("../../data/sources.txt",',')

# Now, try to improve the fit to the data:
# First, find the indices that match for each planet:
data_out = zeros(size(data)[1],6)
# Create a timings table:
io = open("timings_table_with_sources.tex","w")
itrans = 1
for ip=1:nplanet
  eobs,tobs,sobs,nobs = extract_planet(data,ip)
  ittv = 1
  for j=1:nobs
    ittv = 1; dt = Inf
    data_out[itrans,1:4] = data[itrans,:]
    # First, match the observation with a transit time:
    for jttv=1:count1[ip+1]
      if abs(tt1[ip+1,jttv]-tobs[j]) < dt
        ittv = jttv
        dt = abs(tt1[ip+1,jttv]-tobs[j])
      end
    end
    # Now compute the mean and standard deviation of the posterior:
    data_out[itrans,5] = mean(tt_grid[ip+1,ittv,:])
    data_out[itrans,6] = std(tt_grid[ip+1,ittv,:])
    dstring = string(pname[ip]," & ",@sprintf("%i",data[itrans,2])," & ",@sprintf("%12.6f",data[itrans,3]), " & ",@sprintf("%9.6f",data[itrans,4])," & ",@sprintf("%12.6f",data_out[itrans,5])," & ",@sprintf("%9.6f",data_out[itrans,6])," &  ",sources[itrans],"  \\cr")
    println(io,dstring)
    println(dstring)
    # Now, create output for timings table:
    global itrans += 1
  end
end
close(io)
writedlm("../../data/times_obs_and_posterior.txt",data_out,',')


text:
# from Posit to Integer
Int64(x::Posit8) = ccall((:p8_to_i64, SoftPositPath), Int64, (Posit8,),x)
Int64(x::Posit16) = ccall((:p16_to_i64, SoftPositPath), Int64, (Posit16,),x)
Int64(x::Posit32) = ccall((:p32_to_i64, SoftPositPath), Int64, (Posit32,),x)

Int32(x::Posit8) = ccall((:p8_to_i32, SoftPositPath), Int32, (Posit8,),x)
Int32(x::Posit16) = ccall((:p16_to_i32, SoftPositPath), Int32, (Posit16,),x)
Int32(x::Posit32) = ccall((:p32_to_i32, SoftPositPath), Int32, (Posit32,),x)

# from Posit_1 to Integer
Int64(x::Posit8_1) = ccall((:pX1_to_i64, SoftPositPath), Int64, (Posit8_1,),x)
Int64(x::Posit16_1) = ccall((:pX1_to_i64, SoftPositPath), Int64, (Posit16_1,),x)
Int64(x::Posit24_1) = ccall((:pX1_to_i64, SoftPositPath), Int64, (Posit24_1,),x)

Int32(x::Posit8_1) = ccall((:pX1_to_i32, SoftPositPath), Int32, (Posit8_1,),x)
Int32(x::Posit16_1) = ccall((:pX1_to_i32, SoftPositPath), Int32, (Posit16_1,),x)
Int32(x::Posit24_1) = ccall((:pX1_to_i32, SoftPositPath), Int32, (Posit24_1,),x)

# from Posit_2 to Integer
Int64(x::Posit8_2) = ccall((:pX2_to_i64, SoftPositPath), Int64, (Posit8_2,),x)
Int64(x::Posit16_2) = ccall((:pX2_to_i64, SoftPositPath), Int64, (Posit16_2,),x)
Int64(x::Posit24_2) = ccall((:pX2_to_i64, SoftPositPath), Int64, (Posit24_2,),x)

Int32(x::Posit8_2) = ccall((:pX2_to_i32, SoftPositPath), Int32, (Posit8_2,),x)
Int32(x::Posit16_2) = ccall((:pX2_to_i32, SoftPositPath), Int32, (Posit16_2,),x)
Int32(x::Posit24_2) = ccall((:pX2_to_i32, SoftPositPath), Int32, (Posit24_2,),x)

# Posit to UInt - this only reinterprets the bitstring and doesn't perform and actual conversion
UInt8(x::Posit8) = reinterpret(UInt8,x)
UInt8(x::Posit8_1) = UInt8(reinterpret(UInt32,x) >> 24)
UInt8(x::Posit8_2) = UInt8(reinterpret(UInt32,x) >> 24)

UInt16(x::Posit16) = reinterpret(UInt16,x)
UInt16(x::Posit16_1) = UInt16(reinterpret(UInt32,x) >> 16)
UInt16(x::Posit16_2) = UInt16(reinterpret(UInt32,x) >> 16)

UInt32(x::Posit32) = reinterpret(UInt32,x)
UInt32(x::PositAll24) = reinterpret(UInt32,x)


text:
using Distributions
using Random
using IntervalSets
using SparseArrays

using CautiousEngine

# Get the absolute path of the experiment dir
EXPERIMENT_DIR = @__DIR__
experiment_type = "synthesis-example"
global_exp_dir = "$EXPERIMENT_DIR/$experiment_type/global"
online_exp_dir = "$EXPERIMENT_DIR/$experiment_type/online"
specification_file = "$EXPERIMENT_DIR/reach-avoid-specification.jl"

#======================================================================================================
0. Define the Unknown Function, Process Noise, and the Full Dynamics Function
======================================================================================================#
# Four control modes defined - g is the unknown function, f is the total (including known) dynamics fcn
v = 0.5
g1 = (x) -> [v + 0.1*sin(x[2]), 0.2*cos(x[1])]*0.5
g2 = (x) -> [-v + 0.1*sin(x[2]), 0.2*cos(x[1])]*0.5
g3 = (x) -> [0.2*cos(x[2]), v + 0.1*sin(x[1])]*0.5
g4 = (x) -> [0.2*cos(x[2]), -v + 0.1*sin(x[1])]*0.5

# Define the zero-mean process noise
σ_proc = 0.01
process_noise_dist = Normal(0., σ_proc)
measurement_noise_dist = nothing                    # Measurement noise is only used in verification, not synthesis

# Full dynamics function with four control modes
f1 = (x) -> x + g1(x) + rand(process_noise_dist, (2,1))
f2 = (x) -> x + g2(x) + rand(process_noise_dist, (2,1))
f3 = (x) -> x + g3(x) + rand(process_noise_dist, (2,1))
f4 = (x) -> x + g4(x) + rand(process_noise_dist, (2,1))

mode_list = [f1, f2, f3, f4]
mode_names = ["mode1", "mode2", "mode3", "mode4"]
unknown_mode_list = [g1, g2, g3, g4]
known_part = (x) -> [x[1], x[2]]                    # Known part of the dynamics function, can only be this currently.

#======================================================================================================
1.  Perform GP regression and construct IMDP for each unknown mode.
======================================================================================================#
exp_dir = global_exp_dir
res_dirs = Array{String,1}()                        # Array that holds the single-mode result directories
X = Dict("x1" => [-2., 2.], "x2" => [-2., 2.])      # Defines the compact safe set
grid_sizes = Dict("x1" => 0.125, "x2" => 0.125)       # Specifies the discretization coarseness in each dimension
safety_dims = ["x1", "x2"]                          # Dimensions that have safety bounds
dependency_dims = Dict("x1" => [1, 1],              # Defines dependencies on other states
                       "x2" => [1, 1])
number_of_datapoints = 1000                           # Number of data points per mode to generate
run_exps_flag = true                                # Set to true to rerun the single-mode constructions
m_opt = -1                                          # Unused
bound_type = "rkhs-tight"                           # GP bound type
epsilon = -1.                                       # Distance bound parameter, set to -1 for automatic determination

for (i, mode) in enumerate(unknown_mode_list)
    system = mode_names[i]                          # Get the mode name
    Lf_bound = 1.0                                  # Lipschitz constant bound for the mode over X
    random_seed = 11 + i                            # Random seed for the random twister
    eta = σ_proc                                    # η parameter set in a naiive manner

    @info system, number_of_datapoints, σ_proc, bound_type, Lf_bound, random_seed

    # Setup experiment parameter structures (defined in CautiousEngine.jl)
    system_params = SystemParameters(system, mode, known_part, measurement_noise_dist, process_noise_dist, Lf_bound, dependency_dims, 2, 2)
    data_params = DataParameters(number_of_datapoints, m_opt, bound_type, σ_proc, epsilon, eta, safety_dims, -1., -1.)
    experiment_params = ExperimentParameters(exp_dir, experiment_type, specification_file, X, grid_sizes, "foo", random_seed, system_params, data_params)

    # If constructing the single-mode systems, run it!
    if run_exps_flag
        CautiousEngine.end_to_end_transition_bounds(experiment_params, reuse_gps_flag=false, reuse_regions_flag=false)
    end

    # Construct the directory where results will be
    res_dir = experiment_params.experiment_directory
    @info "Mode results can be found in $res_dir"
    push!(res_dirs, res_dir)
end

#======================================================================================================
2.  Construct the product IMDP with the DFA, and synthesize a controller.
======================================================================================================#
synth_dir = exp_dir                                 # Specify where the synthesis result should be saved
!isdir(synth_dir) && mkdir(synth_dir)

default_label = "!a∧!b"                             # Default label of states
unsafe_label = "!a∧b"                               # The label of unsafe states
des_extents = [Dict("x1" => -1.0.. 1.0,             # List containing extents corresponding to a label
                    "x2" => -1.0.. 1.0)]
labels_dict = Dict("a∧!b" => des_extents)           # Dictionary with key if label and value of corresponding extents
# Wrapper function
lbl = (state_extent; unsafe=false) -> general_label_fcn(state_extent, default_label, unsafe_label, labels_dict, unsafe=unsafe)
system_tag = "synthesis-example-m$number_of_datapoints" # Tag for this example
sim_points = [[-1.73; 1.73], [0.01; -1.63]]         # Optional, specify points to simulate the resulting policy

# Perform the synthesis procedure. Results found in `synth_dir`
res_mat, res_dir, pimdp = perform_synthesis_from_result_dirs(res_dirs, synth_dir, system_tag, specification_file, lbl;
                            modes=mode_list, add_opt=true, sim_points=sim_points)