using Test
using Qecsim.App
using Qecsim.BasicModels:FiveQubitCode
using Qecsim.GenericModels:BitFlipErrorModel, DepolarizingErrorModel, NaiveDecoder
using Qecsim.Model:Model, ErrorModel, Decoder, DecodeResult, logical_xs, logical_zs
using Qecsim.PauliTools:to_bsf
using JSON
using Random:MersenneTwister

# TODO: profiling / type-stability checks

# test stub error model that generates a fixed given error
struct _FixedErrorModel <: ErrorModel
    error::BitVector
end
Model.label(::_FixedErrorModel) = "fixed"
Model.generate(error_model::_FixedErrorModel, x...) = error_model.error
# test stub decoder that decodes to a fixed given decode result
struct _FixedDecoder <: Decoder
    result::DecodeResult
end
Model.label(::_FixedDecoder) = "fixed"
Model.decode(decoder::_FixedDecoder, x...; kwargs...) = decoder.result
# test stub decoder that decodes to a cycle of given decode results
struct _CycleDecoder <: Decoder
    result_iter  # Iterator{DecodeResult}
    _CycleDecoder(results) = new(Iterators.Stateful(Iterators.Cycle(results)))
end
Model.label(::_CycleDecoder) = "cycle"
Model.decode(decoder::_CycleDecoder, x...; kwargs...) = popfirst!(decoder.result_iter)

# test stub code using duck-typing
struct _DuckCode end
Model.label(::_DuckCode) = "duck"
Model.stabilizers(::_DuckCode) = to_bsf(["XZZXI", "IXZZX", "XIXZZ", "ZXIXZ"])
Model.logical_xs(::_DuckCode) = to_bsf(["XXXXX"])
Model.logical_zs(::_DuckCode) = to_bsf(["ZZZZZ"])
Model.nkd(::_DuckCode) = (5, 1, 3)
# test stub error model using duck-typing
struct _DuckErrorModel end
Model.label(::_DuckErrorModel) = "duck"
Model.generate(::_DuckErrorModel, x...) = to_bsf("IIIII")
# test stub decoder using duck-typing
struct _DuckDecoder end
Model.label(::_DuckDecoder) = "duck"
Model.decode(::_DuckDecoder, x...; kwargs...) = DecodeResult(recovery=to_bsf("IIIII"))

@testset "RunResult" begin
    r0 = RunResult(false, 3, BitVector([0, 1]), nothing)  # reference
    r1 = RunResult(false, 3, BitVector([0, 1]), nothing)  # same
    r2 = RunResult(true, 3, BitVector([0, 1]), nothing)   # different success
    r3 = RunResult(false, 3, BitVector([0, 0]), nothing)  # different logical_commutations
    r4 = RunResult(false, 4, BitVector([0, 1]), nothing)  # different error_weight
    @test r1 == r0 && hash(r1) == hash(r0)
    @test r2 != r0 && hash(r2) != hash(r0)
    @test r3 != r0 && hash(r3) != hash(r0)
    @test r4 != r0 && hash(r4) != hash(r0)
    @test isequal(r1, r0) && hash(r1) == hash(r0)
    @test !isequal(r2, r0) && hash(r2) != hash(r0)
    @test !isequal(r3, r0) && hash(r3) != hash(r0)
    @test !isequal(r4, r0) && hash(r4) != hash(r0)
end

@testset "qec_run_once" begin
    # simulation parameters
    code = FiveQubitCode()
    error_model = DepolarizingErrorModel()
    decoder = NaiveDecoder()
    # float p
    data = qec_run_once(code, error_model, decoder, 0.1)
    @test !data.success || !any(data.logical_commutations)  # success -> zero commutator
    # rational p
    data = qec_run_once(code, error_model, decoder, 1 // 10)
    @test !data.success || !any(data.logical_commutations)  # success -> zero commutator
    # seeded run
    data1 = qec_run_once(code, error_model, decoder, 0.1, MersenneTwister(13))
    data2 = qec_run_once(code, error_model, decoder, 0.1, MersenneTwister(13))
    @test data1 == data2
    # warn: RECOVERY DOES NOT RETURN TO CODESPACE
    error_model = _FixedErrorModel(to_bsf("IIIII"))
    decoder = _FixedDecoder(DecodeResult(recovery=to_bsf("IIIIX")))
    @test_logs (:warn, "RECOVERY DOES NOT RETURN TO CODESPACE") #=
        =# data = qec_run_once(code, error_model, decoder, 0.1)
    @test !data.success
end

@testset "qec_run_once-duck-typing" begin
    # full duck-type simulation
    data = qec_run_once(_DuckCode(), _DuckErrorModel(), _DuckDecoder(), 0.1)
    @test data.success
    @test !any(data.logical_commutations)
    @test data.error_weight == 0
    @test isnothing(data.custom_values)
    # mixed duck-type simulation (no exceptions thrown)
    qec_run_once(_DuckCode(), _DuckErrorModel(), NaiveDecoder(), 0.1)  # no exception
    qec_run_once(_DuckCode(), BitFlipErrorModel(), NaiveDecoder(), 0.1)  # no exception
end

@testset "qec_run_once-override" begin
    # common simulation parameters
    code = FiveQubitCode()
    identity = to_bsf("IIIII")
    error_model = _FixedErrorModel(identity)
    p = 0.1
    # tests with and without overrides
    for (decode_result, expected) in [
        # identity recovery
        (DecodeResult(recovery=identity),
            RunResult(true, 0, BitVector([0, 0]), nothing)),
        # logical_x recovery
        (DecodeResult(recovery=logical_xs(code)[1,:]),
            RunResult(false, 0, BitVector([0, 1]), nothing)),
        # logical_z recovery
        (DecodeResult(recovery=logical_zs(code)[1,:]),
            RunResult(false, 0, BitVector([1, 0]), nothing)),
        # identity but override success=false
        (DecodeResult(success=false, recovery=identity),
            RunResult(false, 0, BitVector([0, 0]), nothing)),
        # identity but override logical_commutations=[1, 1]
        (DecodeResult(recovery=identity, logical_commutations=[1, 1]),
            RunResult(true, 0, BitVector([1, 1]), nothing)),
        # identity but override success=false and logical_commutations=[1, 1]
        (DecodeResult(success=false, recovery=identity, logical_commutations=[1, 1]),
            RunResult(false, 0, BitVector([1, 1]), nothing)),
        # no-recovery but override success=false
        (DecodeResult(success=false),
            RunResult(false, 0, nothing, nothing)),
        # no-recovery but override success=false and logical_commutations=[1, 1]
        (DecodeResult(success=false, logical_commutations=[1, 1]),
            RunResult(false, 0, BitVector([1, 1]), nothing)),
        # no-recovery but override success=false and custom_values=[1, 2]
        (DecodeResult(success=false, custom_values=[1, 2]),
            RunResult(false, 0, nothing, [1, 2])),
        # no-recovery but override success=false and custom_values=[1., 2.]
        (DecodeResult(success=false, custom_values=[1., 2.]),
            RunResult(false, 0, nothing, [1., 2.])),
        # no-recovery but override success=false and custom_values=[1//2, 1//3]
        (DecodeResult(success=false, custom_values=[1 // 2, 1 // 3]),
            RunResult(false, 0, nothing, [1 // 2, 1 // 3])),
    ]
        decoder = _FixedDecoder(decode_result)
        data = qec_run_once(code, error_model, decoder, p)
        @test data == expected
    end
end

@testset "qec_run" begin
    # simulation parameters
    code = FiveQubitCode()
    error_model = DepolarizingErrorModel()
    decoder = NaiveDecoder()
    max_runs = 1000
    for p in [0.25, 1 // 4]  # test with float and rational p
        data = qec_run(code, error_model, decoder, p; max_runs=max_runs)
        # JSON.print(data, 4)
        expected_keys = Set([:code, :n_k_d, :time_steps, :error_model, :decoder,
            :error_probability, :measurement_error_probability, :n_run, :n_success, :n_fail,
            :n_logical_commutations, :custom_totals, :error_weight_total,
            :error_weight_pvar, :logical_failure_rate, :physical_error_rate, :wall_time])
        @test keys(data) == expected_keys
        @test data[:error_probability] == p
        @test typeof(data[:error_probability]) == typeof(p)
        @test data[:n_run] == max_runs
        @test data[:n_success] + data[:n_fail] == data[:n_run]
        @test data[:n_success] >= 0 && data[:n_fail] >= 0
        @test data[:n_fail] <= sum(data[:n_logical_commutations])
        @test data[:logical_failure_rate] == data[:n_fail] / data[:n_run]
        # physical_error_rate
        p_rate = data[:physical_error_rate]
        p_rate_std = sqrt(data[:error_weight_pvar] / (data[:n_k_d][1]^2))
        @test p_rate - p_rate_std < p < p_rate + p_rate_std
        # run count
        data = qec_run(code, error_model, decoder, p)
        @test data[:n_run] == 1
        data = qec_run(code, error_model, decoder, p; max_runs=10)
        @test data[:n_run] == 10
        data = qec_run(code, error_model, decoder, p; max_failures=2)
        @test data[:n_fail] == 2
        data = qec_run(code, error_model, decoder, p; max_runs=10, max_failures=3)
        @test ((data[:n_run] == 10 && data[:n_fail] <= 3)
            || (data[:n_run] <= 10 && data[:n_fail] == 3))
    end
    # seeded run
    data1 = qec_run(code, error_model, decoder, 0.1, 13; max_runs=max_runs)
    data2 = qec_run(code, error_model, decoder, 0.1, 13; max_runs=max_runs)
    delete!(data1, :wall_time)
    delete!(data2, :wall_time)
    @test data1 == data2
end

@testset "qec_run-duck-typing" begin
    # full duck-type simulation
    data = qec_run(_DuckCode(), _DuckErrorModel(), _DuckDecoder(), 0.1; max_runs=10)
    @test data[:n_success] == 10
    @test all(data[:n_logical_commutations] .== 0)
    @test data[:error_weight_total] == 0
    @test isnothing(data[:custom_totals])
    # mixed duck-type simulation (no exceptions thrown)
    qec_run(_DuckCode(), _DuckErrorModel(), NaiveDecoder(), 0.1; max_runs=10)
    qec_run(_DuckCode(), BitFlipErrorModel(), NaiveDecoder(), 0.1; max_runs=10)
end

@testset "qec_run-override" begin
    # common simulation parameters
    code = FiveQubitCode()
    identity = to_bsf("IIIII")
    error_model = _FixedErrorModel(identity)
    p = 0.1
    # tests with and without overrides
    for (decoder, max_runs, expected) in [
        # identity recovery
        (_FixedDecoder(DecodeResult(recovery=identity)), 1,
            Dict(:n_success => 1, :n_fail => 0, :n_logical_commutations => [0, 0])),
        # logical_x recovery
        (_FixedDecoder(DecodeResult(recovery=logical_xs(code)[1,:])), 2,
            Dict(:n_success => 0, :n_fail => 2, :n_logical_commutations => [0, 2])),
        # logical_z recovery
        (_FixedDecoder(DecodeResult(recovery=logical_zs(code)[1,:])), 3,
            Dict(:n_success => 0, :n_fail => 3, :n_logical_commutations => [3, 0])),
        # identity but override success=false
        (_FixedDecoder(DecodeResult(success=false, recovery=identity)), 4,
            Dict(:n_success => 0, :n_fail => 4, :n_logical_commutations => [0, 0])),
        # identity but override logical_commutations=[1, 1]
        (_FixedDecoder(DecodeResult(recovery=identity, logical_commutations=[1, 1])), 5,
            Dict(:n_success => 5, :n_fail => 0, :n_logical_commutations => [5, 5])),
        # identity but override success=false and logical_commutations=[1, 1]
        (_FixedDecoder(DecodeResult(success=false, recovery=identity,
            logical_commutations=[1, 1])), 6,
            Dict(:n_success => 0, :n_fail => 6, :n_logical_commutations => [6, 6])),
        # no-recovery but override success=false
        (_FixedDecoder(DecodeResult(success=false)), 7,
            Dict(:n_success => 0, :n_fail => 7, :n_logical_commutations => nothing)),
        # no-recovery but override success=false and logical_commutations=[1, 1]
        (_FixedDecoder(DecodeResult(success=false, logical_commutations=[1, 1])), 8,
            Dict(:n_success => 0, :n_fail => 8, :n_logical_commutations => [8, 8])),
        # no-recovery but override success=false and custom_values=[1, 1]
        (_FixedDecoder(DecodeResult(success=false, custom_values=[1, 1])), 9,
            Dict(:n_success => 0, :n_fail => 9, :custom_totals => [9, 9])),
        # no-recovery but override success=false and custom_values=[1., 1.]
        (_FixedDecoder(DecodeResult(success=false, custom_values=[1., 1.])), 10,
            Dict(:n_success => 0, :n_fail => 10, :custom_totals => [10., 10.])),
        # no-recovery but override success=false and custom_values=[1//2, 1//3]
        (_FixedDecoder(DecodeResult(success=false, custom_values=[1 // 2, 1 // 3])), 11,
            Dict(:n_success => 0, :n_fail => 11, :custom_totals => [11 // 2, 11 // 3])),
        # no-recovery but override success=false/true and custom_values=[1, 1]/[2, 3]
        (_CycleDecoder([DecodeResult(success=false, custom_values=[1, 1]),
            DecodeResult(success=true, custom_values=[2, 3])]), 12,
            Dict(:n_success => 6, :n_fail => 6, :custom_totals => [18, 24])),
        # no-recovery but override success=false/true and custom_values=[1, [1.]]/[2, [3.]]
        (_CycleDecoder([DecodeResult(success=false, custom_values=[1, [1.]]),
            DecodeResult(success=true, custom_values=[2, [3.]])]), 2,
            Dict(:n_success => 1, :n_fail => 1, :custom_totals => [3, [1., 3.]])),
        # no-recovery but override success=false/true and custom_values=[1,[1 2]]/[2,[3 4]]
        (_CycleDecoder([DecodeResult(success=false, custom_values=[1, [1 2]]),
            DecodeResult(success=true, custom_values=[2, [3 4]])]), 2,
            Dict(:n_success => 1, :n_fail => 1, :custom_totals => [3, [1 2; 3 4]])),
    ]
        data = qec_run(code, error_model, decoder, p; max_runs=max_runs)
        @test issubset(expected, data)
    end
    # test that inconsistent overrides fail
    for (decode_results, expected) in [
        ([DecodeResult(success=true, logical_commutations=nothing),
            DecodeResult(success=true, logical_commutations=[1, 0])], MethodError),
        ([DecodeResult(success=true, logical_commutations=[1, 1, 0]),
            DecodeResult(success=true, logical_commutations=[1, 0])], DimensionMismatch),
        ([DecodeResult(success=true, custom_values=[1, 0]),
            DecodeResult(success=true, custom_values=[1.1, 0.1])], InexactError),
    ]
        decoder = _CycleDecoder(decode_results)
        @test_throws expected qec_run(code, error_model, decoder, p; max_runs=5)
    end
end

@testset "qec_merge-similar" begin
    # similar simulation parameters
    code = FiveQubitCode()
    error_model = DepolarizingErrorModel()
    decoder = NaiveDecoder()
    p = 0.20
    max_runs = 10
    data1 = qec_run(code, error_model, decoder, p; max_runs=max_runs)
    # JSON.print(data1, 4)
    data2 = qec_run(code, error_model, decoder, p; max_runs=max_runs)
    # JSON.print(data2, 4)
    # data1 and data2 have same keys
    @test keys(data1) == keys(data2)
    merged_data_list = qec_merge(data1, data2)
    # JSON.print(merged_data_list, 4)
    # merged_data_list is a vector with one entry
    @test length(merged_data_list) == 1
    merged_data = merged_data_list[1]
    # merged_data drops the error_weight_pvar key
    @test keys(merged_data) ⊆ keys(data1)
    @test setdiff(keys(data1), keys(merged_data)) == Set([:error_weight_pvar])
    # merged_data preserves p and sums max_runs
    @test merged_data[:error_probability] == p
    @test merged_data[:n_run] == max_runs * 2
    # all value types match
    @test all(typeof(merged_data[k]) == typeof(data1[k]) for k in keys(merged_data))
end

@testset "qec_merge-distinct" begin
    # distinct simulation parameters
    code = FiveQubitCode()
    error_model = DepolarizingErrorModel()
    decoder = NaiveDecoder()
    p1, p2 = 0.1, 0.2
    max_runs = 10
    data1 = qec_run(code, error_model, decoder, p1; max_runs=max_runs)
    data2 = qec_run(code, error_model, decoder, p2; max_runs=max_runs)
    # data1 and data2 have same keys
    @test keys(data1) == keys(data2)
    merged_data_list = qec_merge(data1, data2)
    # merged_data_list is a vector with two entries
    @test length(merged_data_list) == 2
    merged_data1 = merged_data_list[1]
    merged_data2 = merged_data_list[2]
    # merged_data preserves all values except :error_weight_pvar
    delete!(data1, :error_weight_pvar)
    @test merged_data1 == data1
    delete!(data2, :error_weight_pvar)
    @test merged_data2 == data2
end

@testset "qec_merge-zero" begin
    # no data to merge
    merged_data_list = qec_merge()
    # merged_data_list is a vector with no entries
    @test length(merged_data_list) == 0
end

@testset "qec_merge-one" begin
    # single simulation
    data = qec_run(FiveQubitCode(), BitFlipErrorModel(), NaiveDecoder(), 0.1; max_runs=10)
    merged_data_list = qec_merge(data)
    # merged_data_list is a vector with one entry
    @test length(merged_data_list) == 1
    merged_data = merged_data_list[1]
    # merged_data preserves all values except :error_weight_pvar
    delete!(data, :error_weight_pvar)
    @test merged_data == data
end

@testset "qec_merge-custom_totals" begin
    # similar simulation parameters
    code = FiveQubitCode()
    error_model = DepolarizingErrorModel()
    decoder = _CycleDecoder([
        DecodeResult(success=false, custom_values=[1, [1]]),
        DecodeResult(success=true, custom_values=[2, [3]])
    ])
    p = 0.20
    max_runs = 3
    data1 = qec_run(code, error_model, decoder, p; max_runs=max_runs)
    # JSON.print(data1, 4)
    data2 = qec_run(code, error_model, decoder, p; max_runs=max_runs)
    # JSON.print(data2, 4)
    merged_data_list = qec_merge(data1, data2)
    # JSON.print(merged_data_list, 4)
    # merged_data_list is a vector with one entry
    @test length(merged_data_list) == 1
    merged_data = merged_data_list[1]
    # check custom_totals as expected
    @test merged_data[:custom_totals] == [9, [1, 3, 1, 3, 1, 3]]
end

@testset "qec_write_read" begin
    # multiple simulation parameters
    code = FiveQubitCode()
    error_model = DepolarizingErrorModel()
    decoder = NaiveDecoder()
    p1, p2 = 0.08, 0.10
    max_runs = 10
    data = []
    push!(data, qec_run(code, error_model, decoder, p1; max_runs=max_runs))
    push!(data, qec_run(code, error_model, decoder, p2; max_runs=max_runs))
    # JSON.print(data, 4)
    # write data
    filename = tempname()
    qec_write(filename, data...)
    # read data
    data_read = qec_read(filename)
    # JSON.print(data_read, 4)
    @test data_read == data
end

@testset "qec_write-existing-file" begin
    # single simulation
    data = qec_run(FiveQubitCode(), BitFlipErrorModel(), NaiveDecoder(), 0.1; max_runs=10)
    # write data
    filename = tempname()
    touch(filename)
    # refuse to overwrite existing file
    @test_throws ErrorException qec_write(filename, data...)
end

@testset "qec_write_read-empty-data" begin
    # write empty data
    filename = tempname()
    qec_write(filename)
    # read empty data
    data_read = qec_read(filename)
    @test data_read == Dict{Symbol,Any}[]
end

@testset "qec_read-invalid-data" begin
    # write invalid data
    filename = tempname()
    touch(filename)
    # fail read invalid data
    @test_throws Exception qec_read(filename)
    # write invalid data
    filename = tempname()
    open(io -> JSON.print(io, Dict(1 => '1')), filename, "w")
    # fail read invalid data
    @test_throws Exception qec_read(filename)
end

@testset "qec_read-qecsim-py-data" begin
    # read data from Python version of Qecsim
    data_py = qec_read(joinpath(@__DIR__, "./App_files/qecsim-py-data.json"))
    # create local data
    data = qec_run(FiveQubitCode(), BitFlipErrorModel(), NaiveDecoder(), 0.1; max_runs=10)
    for d in data_py
        # keys match
        @test keys(d) == keys(data)
        # all value types match
        @test all(typeof(d[k]) == typeof(data[k]) for k in keys(data))
    end
end


using Test
using MDTrajAnalyse

@testset "Cell" begin
    c = CubicCell(1.1)
    a = parsecell(1.1)
    aa = parsecell(1.1,1.1,1.1)
    naa = parsecell(1.2,1.1,1.1)
    @test c == a
    @test c == aa
    @test typeof(aa) != typeof(naa)

    v = VariableCell([CubicCell(2.3), CubicCell(3.4), OrthorombicCell(2.3,2.4,2.5)] )
    @test length(v) == 3
    @test lastindex(v) == 3

    q = iterate(v)

    @test q[1] == v[1]

    @test length(volume(v)) == 3
end

@testset "Trajectorys" begin
    t = Trajectory(rand(3,4,5))
    tt = Trajectory(t.xyz; cell=CubicCell(1.0))
    @test view(t,1) == view(tt,1)
    distances(t, 1,2)
    compute_rdf(tt, 1:1, 2:3)
end


abstract type SimpleSDMSource end

latitudes(::Type{T}) where {T <: SimpleSDMSource} = (-90.0, 90.0)
longitudes(::Type{T}) where {T <: SimpleSDMSource} = (-180.0, 180.0)

struct WorldClim <: SimpleSDMSource
    resolution::AbstractFloat
    function WorldClim(resolution::AbstractFloat)
        resolution ∈ [2.5, 5.0, 10.0] || throw(ArgumentError("The resolution argument ($(resolution)) must be 2.5, 5, or 10"))
        return new(resolution)
    end
end

WorldClim() = WorldClim(10.0)

struct BioClim <: SimpleSDMSource end
longitudes(::Type{BioClim}) = (-180.0001388888, 179.9998611111)
latitudes(::Type{BioClim}) = (-90.0001388888, 83.9998611111)

struct EarthEnv <: SimpleSDMSource
    full::Bool
end

EarthEnv() = EarthEnv(false)
latitudes(::Type{EarthEnv}) = (-56.0, 90.0)
longitudes(::Type{EarthEnv}) = (-180.0, 180.0)


using PGFPlots
using CSV
using Query
using DataFrames

# ld
filename = Pkg.dir("ContinuousPOMDPTreeSearchExperiments", "icaps_2018", "data", "ld_discretization_Tuesday_27_Feb_15_23.csv")
alldata = CSV.read(filename)

aggregated = by(alldata, [:solver, :d]) do df
    r = df[:reward]
    return DataFrame(mean=mean(r), sem=std(r)/sqrt(length(r)))
end

names = Dict("pomcpow"=>"POMCPOW\\textsuperscript{D}",
             "d_pomcp"=>"POMCP\\textsuperscript{D}",
             "d_despot"=>"DESPOT\\textsuperscript{D}")
plots = Plots.Plot[]
for k in ["d_pomcp", "d_despot", "pomcpow"]
    series = @from pt in aggregated begin
        @where pt.solver == k
        @select {pt.d, pt.mean, pt.sem}
        @collect DataFrame
    end
    push!(plots, Plots.Linear(series, x=:d, y=:mean, errorBars=ErrorBars(y=series[:sem]), legendentry=names[k]))
end
ds = sort(unique(aggregated[:d]))
push!(plots, Plots.Linear(ds,
                          fill!(similar(ds), 57.7),
                          errorBars=ErrorBars(y=fill!(similar(ds), 0.5)),
                          legendentry="POMCPOW\\phantom{\\textsuperscript{D}}"))
p = plot(Axis(plots,
              xlabel="Discretization bin size",
              ylabel="Mean accumulated reward",
              xmode="log",
              style="legend style={at={(1.06,0.3)},anchor=south east,font=\\small}"))
save("/tmp/ld_discretization.pdf", p)

# Subhunt

filename = Pkg.dir("ContinuousPOMDPTreeSearchExperiments", "icaps_2018", "data", "subhunt_discretization_Tuesday_27_Feb_11_38.csv")
alldata = CSV.read(filename)

aggregated = by(alldata, [:solver, :binsize]) do df
    r = df[:reward]
    return DataFrame(mean=mean(r), sem=std(r)/sqrt(length(r)))
end

# println(aggregated)

names = Dict("pomcpow"=>"POMCPOW\\textsuperscript{D}",
             "d_pomcp"=>"POMCP\\textsuperscript{D}",
             "d_despot"=>"DESPOT\\textsuperscript{D}")
plots = Plots.Plot[]
for k in ["d_pomcp", "d_despot", "pomcpow"]
    series = @from pt in aggregated begin
        @where pt.solver == k
        @select {pt.binsize, pt.mean, pt.sem}
        @collect DataFrame
    end
    push!(plots, Plots.Linear(series, x=:binsize, y=:mean, errorBars=ErrorBars(y=series[:sem]), legendentry=names[k]))
end
ds = sort(unique(aggregated[:binsize]))
push!(plots, Plots.Linear(ds,
                          fill!(similar(ds), 69.2),
                          errorBars=ErrorBars(y=fill!(similar(ds), 1.3)),
                          legendentry="POMCPOW\\phantom{\\textsuperscript{D}}"))
p = plot(Axis(plots,
              ymin=0,
              xmode="log",
              xlabel="Discretization bin size",
              ylabel="Mean accumulated reward",
              style="legend style={at={(0.01,0.45)},anchor=south west,font=\\small}"))
save("/tmp/subhunt_discretization.pdf", p)


mutable struct BasicDiscUnvParameter <: Parameter{Discrete, Univariate}
  key::Symbol
  index::Integer
  pdf::Union{DiscreteUnivariateDistribution, Void}
  prior::Union{DiscreteUnivariateDistribution, Void}
  setpdf::Union{Function, Void}
  setprior::Union{Function, Void}
  loglikelihood!::Union{Function, Void}
  logprior!::Union{Function, Void}
  logtarget!::Union{Function, Void}
  states::VariableStateVector

  function BasicDiscUnvParameter(
    key::Symbol,
    index::Integer,
    pdf::Union{DiscreteUnivariateDistribution, Void},
    prior::Union{DiscreteUnivariateDistribution, Void},
    setpdf::Union{Function, Void},
    setprior::Union{Function, Void},
    ll::Union{Function, Void},
    lp::Union{Function, Void},
    lt::Union{Function, Void},
    states::VariableStateVector
  )
    instance = new()

    instance.key = key
    instance.index = index
    instance.pdf = pdf
    instance.prior = prior

    args = (setpdf, setprior, ll, lp, lt)
    fnames = fieldnames(BasicDiscUnvParameter)[5:9]

    # Check that all generic functions have correct signature
    for i in 1:5
      if isa(args[i], Function) &&
        isa(args[i], Function) &&
        !method_exists(args[i], (BasicDiscUnvParameterState, VariableStateVector))
        error("$(fnames[i]) has wrong signature")
      end
    end

    # Define setpdf (i = 1) and setprior (i = 2)
    for (i, setter, distribution) in ((1, :setpdf, :pdf), (2, :setprior, :prior))
      setfield!(
        instance,
        setter,
        if isa(args[i], Function)
          (_state::BasicDiscUnvParameterState, _states::VariableStateVector) ->
            setfield!(instance, distribution, args[i](_state, _states))
        else
          nothing
        end
      )
    end

    # Define loglikelihood!
    setfield!(
      instance,
      :loglikelihood!,
      if isa(args[3], Function)
        _state::BasicDiscUnvParameterState -> args[3](_state, instance.states)
      else
        nothing
      end
    )

    # Define logprior!
    setfield!(
      instance,
      :logprior!,
      if isa(args[4], Function)
        _state::BasicDiscUnvParameterState -> args[4](_state, instance.states)
      else
        if (isa(prior, DiscreteUnivariateDistribution) && method_exists(logpdf, (typeof(prior), eltype(prior)))) ||
          isa(args[2], Function)
          _state::BasicDiscUnvParameterState ->
            setfield!(_state, :logprior, logpdf(getfield(instance, :prior), _state.value))
        else
          nothing
        end
      end
    )

    # Define logtarget!
    setfield!(
      instance,
      :logtarget!,
      if isa(args[5], Function)
        _state::BasicDiscUnvParameterState -> args[5](_state, instance.states)
      else
        if isa(args[3], Function) && isa(getfield(instance, :logprior!), Function)
          function (_state::BasicDiscUnvParameterState)
            getfield(instance, :loglikelihood!)(_state)
            getfield(instance, :logprior!)(_state)
            setfield!(_state, :logtarget, getfield(_state, :loglikelihood)+getfield(_state, :logprior))
          end
        elseif (isa(pdf, DiscreteUnivariateDistribution) && method_exists(logpdf, (typeof(pdf), eltype(pdf)))) ||
          isa(args[1], Function)
          _state::BasicDiscUnvParameterState -> setfield!(_state, :logtarget, logpdf(getfield(instance, :pdf), _state.value))
        else
          nothing
        end
      end
    )

    instance
  end
end

BasicDiscUnvParameter(key::Symbol, index::Integer=0; signature::Symbol=:high, args...) =
  BasicDiscUnvParameter(key, Val{signature}, index; args...)

BasicDiscUnvParameter(
  key::Symbol,
  ::Type{Val{:low}},
  index::Integer=0;
  pdf::Union{DiscreteUnivariateDistribution, Void}=nothing,
  prior::Union{DiscreteUnivariateDistribution, Void}=nothing,
  setpdf::Union{Function, Void}=nothing,
  setprior::Union{Function, Void}=nothing,
  loglikelihood::Union{Function, Void}=nothing,
  logprior::Union{Function, Void}=nothing,
  logtarget::Union{Function, Void}=nothing,
  states::VariableStateVector=VariableState[]
) =
  BasicDiscUnvParameter(key, index, pdf, prior, setpdf, setprior, loglikelihood, logprior, logtarget, states)

function BasicDiscUnvParameter(
  key::Symbol,
  ::Type{Val{:high}},
  index::Integer=0;
  pdf::Union{DiscreteUnivariateDistribution, Void}=nothing,
  prior::Union{DiscreteUnivariateDistribution, Void}=nothing,
  setpdf::Union{Function, Void}=nothing,
  setprior::Union{Function, Void}=nothing,
  loglikelihood::Union{Function, Void}=nothing,
  logprior::Union{Function, Void}=nothing,
  logtarget::Union{Function, Void}=nothing,
  states::VariableStateVector=VariableState[],
  nkeys::Integer=0,
  vfarg::Bool=false
)
  inargs = (setpdf, setprior, loglikelihood, logprior, logtarget)

  fnames = fieldnames(BasicDiscUnvParameterState)[2:4]

  if vfarg
    @assert nkeys > 0 "If vfarg=$vfarg, nkeys must be positive, got $nkeys"
  else
    @assert nkeys >= 0 "If vfarg=$vfarg, nkeys must be non-negative, got $nkeys"
  end

  outargs = Union{Function, Void}[nothing for i in 1:5]

  if vfarg
    for i in 1:2
      if isa(inargs[i], Function)
        outargs[i] = (_state::BasicDiscUnvParameterState, _states::VariableStateVector) ->
          inargs[i](Any[s.value for s in _states])
      end
    end

    for i in 3:5
      if isa(inargs[i], Function)
        outargs[i] = (_state::BasicDiscUnvParameterState, _states::VariableStateVector) ->
          setfield!(_state, fnames[i-2], inargs[i](Any[s.value for s in _states]))
      end
    end
  else
    if nkeys > 0
      for i in 1:2
        if isa(inargs[i], Function)
          outargs[i] = (_state::BasicDiscUnvParameterState, _states::VariableStateVector) ->
            inargs[i](_state.value, Any[s.value for s in _states])
        end
      end

      for i in 3:5
        if isa(inargs[i], Function)
          outargs[i] = (_state::BasicDiscUnvParameterState, _states::VariableStateVector) ->
            setfield!(_state, fnames[i-2], inargs[i](_state.value, Any[s.value for s in _states]))
        end
      end
    else
      for i in 1:2
        if isa(inargs[i], Function)
          outargs[i] = (_state::BasicDiscUnvParameterState, _states::VariableStateVector) -> inargs[i](_state.value)
        end
      end

      for i in 3:5
        if isa(inargs[i], Function)
          outargs[i] = (_state::BasicDiscUnvParameterState, _states::VariableStateVector) ->
            setfield!(_state, fnames[i-2], inargs[i](_state.value))
        end
      end
    end
  end

  BasicDiscUnvParameter(key, index, pdf, prior, outargs..., states)
end

value_support(::Type{BasicDiscUnvParameter}) = Discrete
value_support(::BasicDiscUnvParameter) = Discrete

variate_form(::Type{BasicDiscUnvParameter}) = Univariate
variate_form(::BasicDiscUnvParameter) = Univariate

default_state_type(::BasicDiscUnvParameter) = BasicDiscUnvParameterState

default_state(variable::BasicDiscUnvParameter, value::N, outopts::Dict) where {N<:Integer} =
  BasicDiscUnvParameterState(
    value, (haskey(outopts, :diagnostics) && in(:accept, outopts[:diagnostics])) ? [:accept] : Symbol[]
  )

show(io::IO, ::Type{BasicDiscUnvParameter}) = print(io, "BasicDiscUnvParameter")


include("src/TransitRouter.jl")

using .TransitRouter
using .TransitRouter.OSRM
using ArgParse

function main()
    parser = ArgParseSettings()
    @add_arg_table! parser begin
        "--osrm-network", "-n"
            help = "path to a .osrm file to use for transfer finding"
        "--max-transfer-distance", "-t"
            help = "maximum transfer distance (meters)"
            arg_type = Float64
            default = 1000.0
        "output"
            help = "Output file to save network (ends in .trjl)"
            required = true
        "gtfs"
            help = "GTFS input files"
            nargs = '+'
            required = true
    end

    parsed_args = parse_args(parser)

    gtfs::Vector{String} = convert(Vector{String}, parsed_args["gtfs"])::Vector{String}
    output::String = parsed_args["output"]::String

    if haskey(parsed_args, "osrm-network")
        @info "Starting OSRM to route through the street network"
        # TODO don't hardwire mld
        osrm = start_osrm(parsed_args["osrm-network"]::String, "mld")
        network = build_network(gtfs, osrm, parsed_args["max-transfer-distance"])
        stop_osrm!(osrm)
        save_network(network, output)
    else
        network = build_network(gtfs, max_transfer_distance_meters=parsed_args["max-transfer-distance"])
        save_network(network, output)
    end
end

main()


# # Sidewalk

#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/notebooks/sidewalk.ipynb)

# In this notebook, we will be creating a sidewalk environment in which
#  pedestrians can walk along the sidewalk and cross the street as cars pass.

using Parameters
using AutomotiveSimulator
using AutomotiveVisualization
AutomotiveVisualization.colortheme["background"] = colorant"white"; # hide
using Cairo

# Define sidewalk IDs
const TOP = 1
const BOTTOM = 2
#md nothing # hide

# ### Creating the Environment
# Here, we create a new type of environment called SidewalkEnv. It consists of a roadway, crosswalk, and sidewalk. A sidewalk is a Vector of Lanes that run alongside the road.

@with_kw mutable struct SidewalkEnv
    roadway::Roadway
    crosswalk::Lane
    sidewalk::Vector{Lane}
end;
#md nothing # hide

# #### Defining the Sidewalk
# We define the sidewalk's parameters.

## Geometry parameters
roadway_length = 100.
crosswalk_length = 15.
crosswalk_width = 6.0
crosswalk_pos = roadway_length/2
sidewalk_width = 3.0
sidewalk_pos = crosswalk_length/2 - sidewalk_width / 2
#md nothing # hide

# Now we create the sidewalk environment.
# Our environment will consist of:
# * 1-way road with 2 lanes
# * Unsignalized zebra crosswalk perpendicular to the road
# * Sidewalks above and below the road

# Generate straight roadway of length roadway_length with 2 lanes.
# Returns a Roadway type (Array of segments).
# There is already a method to generate a simple straight roadway, which we use here.
roadway = gen_straight_roadway(2, roadway_length)

# Generate the crosswalk.
# Our crosswalk does not have a predefined method for generation, so we define it with a LaneTag and a curve.
n_samples = 2 # for curve generation
crosswalk = Lane(LaneTag(2,1), gen_straight_curve(VecE2(crosswalk_pos, -crosswalk_length/2),
                                                  VecE2(crosswalk_pos, crosswalk_length/2),
                                                   n_samples), width = crosswalk_width)
cw_segment = RoadSegment(2, [crosswalk])
push!(roadway.segments, cw_segment) # Append the crosswalk to the roadway

## Generate the sidewalk
top_sidewalk = Lane(LaneTag(3, TOP), gen_straight_curve(VecE2(0., sidewalk_pos),
                                                      VecE2(roadway_length, sidewalk_pos),
                                                        n_samples), width = sidewalk_width)
bottom_sidewalk = Lane(LaneTag(3, BOTTOM), gen_straight_curve(VecE2(0., -(sidewalk_pos - sidewalk_width)),
                                                          VecE2(roadway_length, -(sidewalk_pos - sidewalk_width)),
                                                            n_samples), width = sidewalk_width)

# Note: we subtract the sidewalk_width from the sidewalk position so that the edge is flush with the road.

sw_segment = RoadSegment(3, [top_sidewalk, bottom_sidewalk])
push!(roadway.segments, sw_segment)

# Initialize crosswalk environment
env = SidewalkEnv(roadway, crosswalk, [top_sidewalk, bottom_sidewalk]);
#md nothing # hide

# Since there is no defined `add_renderable!` method for the crosswalk and the sidewalk, we must define it ourselves.

function AutomotiveVisualization.add_renderable!(rendermodel::RenderModel, env::SidewalkEnv)
    ## Render sidewalk
    for sw in env.sidewalk
        curve = sw.curve
        n = length(curve)
        pts = Array{Float64}(undef, 2, n)
        for (i,pt) in enumerate(curve)
            pts[1,i] = pt.pos.x
            pts[2,i] = pt.pos.y
        end
        add_instruction!(rendermodel, render_line, (pts, colorant"grey", sw.width, Cairo.CAIRO_LINE_CAP_BUTT))
    end

    ## Render roadway
    roadway = gen_straight_roadway(2, roadway_length)
    add_renderable!(rendermodel, roadway)

    ## Render crosswalk
    curve = env.crosswalk.curve
    n = length(curve)
    pts = Array{Float64}(undef, 2, n)
    for (i,pt) in enumerate(curve)
        pts[1,i] = pt.pos.x
        pts[2,i] = pt.pos.y
    end

    ## We can add render instructions from AutomotiveVisualization.
    ## Here we want the crosswalk to appear as a white-striped zebra crossing rather than a road.
    add_instruction!(rendermodel, render_dashed_line, (pts, colorant"white", env.crosswalk.width, 1.0, 1.0, 0.0, Cairo.CAIRO_LINE_CAP_BUTT))

    return rendermodel
end

snapshot = render([env])
#md write("sidewalk.svg", snapshot) # hide
#md # ![sidewalk](sidewalk.svg)

# Now we can define our pedestrian.

# We define its class and the dimensions of its bounding box.
const PEDESTRIAN_DEF = VehicleDef(AgentClass.PEDESTRIAN, 1.0, 1.0)
#md nothing # hide

# We assign models to each agent in the scene.

timestep = 0.1

# Crossing pedestrian definition
ped_init_state = VehicleState(VecSE2(49.0,-3.0,0.), env.sidewalk[BOTTOM], roadway, 1.3)
ped = Entity(ped_init_state, PEDESTRIAN_DEF, :pedestrian)

# Car definition
car_initial_state = VehicleState(VecSE2(0.0, 0., 0.), roadway.segments[1].lanes[1],roadway, 8.0)
car = Entity(car_initial_state, VehicleDef(), :car)

scene = Scene([ped, car])

# Define a model for each entity present in the scene
models = Dict{Symbol, DriverModel}(
    :pedestrian => SidewalkPedestrianModel(
        timestep=timestep, crosswalk=env.crosswalk,
        sw_origin = env.sidewalk[BOTTOM], sw_dest = env.sidewalk[TOP]
    ),
    :car        => LatLonSeparableDriver( # produces LatLonAccels
        ProportionalLaneTracker(), # lateral model
        IntelligentDriverModel(), # longitudinal model
    )
)
#md nothing # hide

### Simulate
# Finally, we simulate and visualize the scene.

using Reel

nticks = 300
scenes = simulate(scene, roadway, models, nticks, timestep)

animation = roll(fps=1.0/timestep, duration=nticks*timestep) do t, dt
    i = Int(floor(t/dt)) + 1
    render([env, scenes[i]])
end;

#md write("sidewalk_animation.gif", animation); # hide
#md # ![sidewalk animation](sidewalk_animation.gif)

# We can use a slider to scroll through each scene in the simulation. This usually takes less time than rendering a video.

#md using Interact
#md using Reel
#md using Blink
#md
#md w = Window()
#md viz = @manipulate for i in 1 : length(scenes)
#md     render([env, scenes[i]])
#md end
#md body!(w, viz)


struct Target{W,T}
    v::NTuple{W,Core.VecElement{T}}
end
@inline Target() = Target(LoopVectorization.SIMDPirates.extract_data(LoopVectorization.vbroadcast(LoopVectorization.pick_vector_width_val(Float64), 0.0)))
@inline Target(::Type{T}) where {T} = Target(LoopVectorization.SIMDPirates.extract_data(LoopVectorization.vbroadcast(LoopVectorization.pick_vector_width_val(T), zero(T))))
@inline Base.:(+)(t::Target, s) = Target(LoopVectorization.addscalar(t.v, s))
@inline Base.:(+)(s, t::Target) = Target(LoopVectorization.addscalar(t.v, s))
@inline Base.:(+)(t::Target{WT,T}, v::LoopVectorization.SVec{WV,T}) where {WT,WV,T} = Target(LoopVectorization.vadd(t.v, LoopVectorization.SIMDPirates.extract_data(v)))
@inline Base.:(+)(t::Target{WT,T}, v::NTuple{WV,Core.VecElement{T}}) where {WT,WV,T} = Target(LoopVectorization.vadd(t.v, v))
@inline Base.:(+)(v::LoopVectorization.SVec{WV,T}, t::Target{WT,T}) where {WT,WV,T} = Target(LoopVectorization.vadd(t.v, LoopVectorization.SIMDPirates.extract_data(v)))
@inline Base.:(+)(v::NTuple{WV,Core.VecElement{T}}, t::Target{WT,T}) where {WT,WV,T} = Target(LoopVectorization.vadd(t.v, v))
@inline Base.:(+)(t₁::Target, t₂::Target) = Target(LoopVectorization.vadd(t₁.v, t₂.v))
@inline Base.sum(t::Target) = LoopVectorization.vsum(t.v)
@inline VectorizationBase.extract_data(t::Target) = t.v


"""
    abstract type MetabolicModel end

A helper supertype that wraps everything usable as a linear-like model for
COBREXA functions.

If you want your model type to work with COBREXA, add the `MetabolicModel` as
its supertype, and implement the accessor functions. Accessors
[`reactions`](@ref), [`metabolites`](@ref), [`stoichiometry`](@ref),
[`bounds`](@ref) and [`objective`](@ref) must be implemented; others are not
mandatory and default to safe "empty" values.
"""
abstract type MetabolicModel end

const SparseMat = SparseMatrixCSC{Float64,Int}
const SparseVec = SparseVector{Float64,Int}
const MatType = AbstractMatrix{Float64}
const VecType = AbstractVector{Float64}
const StringVecType = AbstractVector{String}

"""
    GeneAssociation = Vector{Vector{String}}

An association to genes, represented as a logical formula in a positive
disjunctive normal form (DNF). (The 2nd-level vectors of strings are connected
by "and" to form conjunctions, and the 1st-level vectors of these conjunctions
are connected by "or" to form the DNF.)
"""
const GeneAssociation = Vector{Vector{String}}

"""
    MetaboliteFormula = Dict{String,Int}

Dictionary of atoms and their abundances in a molecule.
"""
const MetaboliteFormula = Dict{String,Int}

"""
    Annotations = Dict{String,Vector{String}}

Dictionary used to store (possible multiple) standardized annotations of
something, such as a [`Metabolite`](@ref) and a [`Reaction`](@ref).

# Example
```
Annotations("PubChem" => ["CID12345", "CID54321"])
```
"""
const Annotations = Dict{String,Vector{String}}

"""
    Notes = Dict{String,Vector{String}}

Free-form notes about something (e.g. a [`Gene`](@ref)), categorized by
"topic".
"""
const Notes = Dict{String,Vector{String}}

_missing_impl_error = (m, a) -> throw(MethodError(m, a))

"""
    reactions(a::MetabolicModel)::Vector{String}

Return a vector of reaction identifiers in a model.
"""
function reactions(a::MetabolicModel)::Vector{String}
    _missing_impl_error(reactions, (a,))
end

"""
    metabolites(a::MetabolicModel)::Vector{String}

Return a vector of metabolite identifiers in a model.
"""
function metabolites(a::MetabolicModel)::Vector{String}
    _missing_impl_error(metabolites, (a,))
end

"""
    n_reactions(a::MetabolicModel)::Int

Get the number of reactions in a model.
"""
function n_reactions(a::MetabolicModel)::Int
    length(reactions(a))
end

"""
    n_metabolites(a::MetabolicModel)::Int

Get the number of metabolites in a model.
"""
function n_metabolites(a::MetabolicModel)::Int
    length(metabolites(a))
end

"""
    stoichiometry(a::MetabolicModel)::SparseMat

Get the sparse stoichiometry matrix of a model.
"""
function stoichiometry(a::MetabolicModel)::SparseMat
    _missing_impl_error(stoichiometry, (a,))
end

"""
    bounds(a::MetabolicModel)::Tuple{SparseVec,SparseVec}

Get the lower and upper flux bounds of a model.
"""
function bounds(a::MetabolicModel)::Tuple{SparseVec,SparseVec}
    _missing_impl_error(bounds, (a,))
end

"""
    balance(a::MetabolicModel)::SparseVec

Get the sparse balance vector of a model (ie. the `b` from `S x = b`).
"""
function balance(a::MetabolicModel)::SparseVec
    return spzeros(n_metabolites(a))
end

"""
    objective(a::MetabolicModel)::SparseVec

Get the objective vector of a model.
"""
function objective(a::MetabolicModel)::SparseVec
    _missing_impl_error(objective, (a,))
end

"""
    coupling(a::MetabolicModel)::SparseMat

Get a matrix of coupling constraint definitions of a model. By default, there
is no coupling in the models.
"""
function coupling(a::MetabolicModel)::SparseMat
    return spzeros(0, n_reactions(a))
end

"""
    n_coupling_constraints(a::MetabolicModel)::Int

Get the number of coupling constraints in a model.
"""
function n_coupling_constraints(a::MetabolicModel)::Int
    size(coupling(a), 1)
end

"""
    coupling_bounds(a::MetabolicModel)::Tuple{SparseVec,SparseVec}

Get the lower and upper bounds for each coupling bound in a model, as specified
by `coupling`. By default, the model does not have any coupling bounds.
"""
function coupling_bounds(a::MetabolicModel)::Tuple{SparseVec,SparseVec}
    return (spzeros(0), spzeros(0))
end

"""
    genes(a::MetabolicModel)::Vector{String}

Return identifiers of all genes contained in the model. By default, there are
no genes.

In SBML, these are usually called "gene products" but we write `genes` for
simplicity.
"""
function genes(a::MetabolicModel)::Vector{String}
    return []
end

"""
    n_genes(a::MetabolicModel)::Int

Return the number of genes in the model (as returned by [`genes`](@ref)). If
you just need the number of the genes, this may be much more efficient than
calling [`genes`](@ref) and measuring the array.
"""
function n_genes(a::MetabolicModel)::Int
    return length(genes(a))
end

"""
    reaction_gene_association(a::MetabolicModel, gene_id::String)::Maybe{GeneAssociation}

Returns the sets of genes that need to be present so that the reaction can work
(technically, a DNF on gene availability, with positive atoms only).

For simplicity, `nothing` may be returned, meaning that the reaction always
takes place. (in DNF, that would be equivalent to returning `[[]]`.)
"""
function reaction_gene_association(
    a::MetabolicModel,
    reaction_id::String,
)::Maybe{GeneAssociation}
    return nothing
end

"""
    metabolite_formula(
        a::MetabolicModel,
        metabolite_id::String,
    )::Maybe{MetaboliteFormula}

Return the formula of metabolite `metabolite_id` in `model`.
Return `nothing` in case the formula is not known or irrelevant.
"""
function metabolite_formula(
    model::MetabolicModel,
    metabolite_id::String,
)::Maybe{MetaboliteFormula}
    return nothing
end

"""
metabolite_charge(model::MetabolicModel, metabolite_id::String)::Maybe{Int}

Return the charge associated with metabolite `metabolite_id` in `model`.
Returns `nothing` if charge not present.
"""
function metabolite_charge(model::MetabolicModel, metabolite_id::String)::Maybe{Int}
    return nothing
end

"""
    reaction_annotations(a::MetabolicModel, reaction_id::String)::Annotations

Return standardized names that may help identifying the reaction. The
dictionary assigns vectors of possible identifiers to identifier system names,
e.g. `"Reactome" => ["reactomeID123"]`.
"""
function reaction_annotations(a::MetabolicModel, reaction_id::String)::Annotations
    return Dict()
end

"""
    metabolite_annotations(a::MetabolicModel, metabolite_id::String)::Annotations

Return standardized names that may help to reliably identify the metabolite. The
dictionary assigns vectors of possible identifiers to identifier system names,
e.g. `"ChEMBL" => ["123"]` or `"PubChem" => ["CID123", "CID654645645"]`.
"""
function metabolite_annotations(a::MetabolicModel, metabolite_id::String)::Annotations
    return Dict()
end

"""
    gene_annotations(a::MetabolicModel, gene_id::String)::Annotations

Return standardized names that identify the corresponding gene or product. The
dictionary assigns vectors of possible identifiers to identifier system names,
e.g. `"PDB" => ["PROT01"]`.
"""
function gene_annotations(a::MetabolicModel, gene_id::String)::Annotations
    return Dict()
end

"""
    reaction_notes(model::MetabolicModel, reaction_id::String)::Notes

Return the notes associated with reaction `reaction_id` in `model`.
"""
function reaction_notes(model::MetabolicModel, reaction_id::String)::Notes
    return Dict()
end

"""
    metabolite_notes(model::MetabolicModel, metabolite_id::String)::Notes

Return the notes associated with metabolite `reaction_id` in `model`.
"""
function metabolite_notes(model::MetabolicModel, metabolite_id::String)::Notes
    return Dict()
end

"""
    gene_notes(model::MetabolicModel, gene_id::String)::Notes

Return the notes associated with the gene `gene_id` in `model`.
"""
function gene_notes(model::MetabolicModel, gene_id::String)::Notes
    return Dict()
end

"""
    metabolite_compartment(model::MetabolicModel, metabolite_id::String)::Maybe{String}

Return the compartment of metabolite `metabolite_id` in `model` if it is assigned. If not,
return `nothing`.
"""
function metabolite_compartment(model::MetabolicModel, metabolite_id::String)::Maybe{String}
    return nothing
end

"""
    reaction_subsystem(model::MetabolicModel, reaction_id::String)::Maybe{String}

Return the subsystem of reaction `reaction_id` in `model` if it is assigned. If not,
return `nothing`.
"""
function reaction_subsystem(model::MetabolicModel, reaction_id::String)::Maybe{String}
    return nothing
end

"""
    reaction_stoichiometry(model::MetaboliteModel, rxn_id::String)::Dict{String, Float64}

Return the reaction equation of reaction with id `rxn_id` in model. The reaction
equation maps metabolite ids to their stoichiometric coefficients.
"""
function reaction_stoichiometry(m::MetabolicModel, rxn_id::String)::Dict{String,Float64}
    mets = metabolites(m)
    Dict(
        mets[k] => v for (k, v) in
        zip(findnz(stoichiometry(m)[:, first(indexin([rxn_id], reactions(m)))])...)
    )
end

"""
    precache!(a::MetabolicModel)::Nothing

Do whatever is feasible to get the model into a state that can be read from
as-quickly-as-possible. This may include e.g. generating helper index
structures and loading delayed parts of the model from disk. The model should
be modified "transparently" in-place. Analysis functions call this right before
applying modifications or converting the model to the optimization model using
[`make_optimization_model`](@ref); usually on the same machine where the
optimizers (and, generally, the core analysis algorithms) will run. The calls
are done in a good hope that the performance will be improved.

By default, it should be safe to do nothing.
"""
function precache!(a::MetabolicModel)::Nothing
    nothing
end


# Module for The King is Dead bot
# This houses all of the functions

function random_pick_from_supply(board::Board)
    supply_array = Array{String}(undef, 0)
    for (f_name, f_num) in board.supply
        append!(supply_array, repeat([f_name], f_num))
    end
    pick = supply_array[rand(1:length(supply_array))]
    return pick
end

function initialise_player_courts!(players::Array{Player}, board::Board)
    for player in players
        while player.court_size < court_size
            pick = random_pick_from_supply(board)
            player.court[pick] += 1
            player.court_size += 1
            board.supply[pick] -= 1
        end
    end
end

function initialise_board!(board::Board)
    # First make the players' courts
    for player in board.players
        while player.court_size < court_size
            pick = random_pick_from_supply(board)
            player.court[pick] += 1
            player.court_size += 1
            board.supply[pick] -= 1
        end
    end
    # Then put followers in home bases
    for region in board.regions
        for (f, starting_region) in faction_names
            if region.name == starting_region
                region.followers[f] += 2
                region.size = 2
                board.supply[f] -= 2 # Reduce the supply
                for r in board.unresolved_regions
                    if r.name == starting_region
                        r.controller = f
                    end
                end
            end
        end
    end
    # Then randomly pick followers from the supply
    for region in board.regions
        while region.size < region_size
            pick = random_pick_from_supply(board)
            region.followers[pick] += 1
            region.size += 1
            board.supply[pick] -= 1
        end
    end
end

function resolve_region!(board::Board, region::String)
    index = 0
    for (i, r) in enumerate(board.unresolved_regions)
        if r.name == region
            # Check which faction controls this region
            max_followers = maximum([v for (k,v) in r.followers])
            controlling_factions = [k for (k,v) in r.followers if v == max_followers]
            if length(controlling_factions) == 1
                r.controller = controlling_factions[1]
            else
                r.controller = "France"
                board.invasions += 1
            end
            r.resolved = true
            # Return all followers to the supply
            for (f_name, f_num) in r.followers
                board.supply[f_name] += f_num
            end
            index = i
        end
    end
    if index == 0
        error("The region to be resolved does not exist in the unresolved regions\n",
              "Tried to resolve ", region)
    end
    push!(board.resolved_regions, board.unresolved_regions[index])
    deleteat!(board.unresolved_regions, index)
end

function resolve_board!(board::Board)
    while length(board.unresolved_regions) > 0
        r = board.unresolved_regions[1]
        resolve_region!(board, r.name)
    end
end

function evaluate_current_board(board::Board)
    evaluation = 0
    player = board.players[board.turn]
    for r in board.resolved_regions
        evaluation += get(player.court, r.controller,
                        minimum([v for (k,v) in player.court]))
    end
    evaluation
end

function evaluate_resolved_board(board::Board)
    board_copy = deepcopy(board)
    resolve_board!(board_copy)
    evaluate_current_board(board_copy)
end

function take_follower!(board::Board, region::String,
                        faction::String)
    for r in board.unresolved_regions
        if r.name == region
            if r.followers[faction] == 0
                error("Trying to take a follower from a region where that faction is not present\n",
                "Tried to take a(n) ", faction, " follower from ", region)
            end
            r.followers[faction] -= 1
            r.size -= 1
            board.players[board.turn].court[faction] += 1
            board.players[board.turn].court_size += 1
        end
    end
end

function take_follower!(board::Board, player::Player,
    p::Pair{String, String})
    take_follower!(board, player, p.first, p.second)
end

function place_follower!(board::Board, region::String, faction::String)
    for r in board.unresolved_regions
        if r.name == region
            if board.supply[faction] == 0
                prinln("Trying to take a follower from the supply when that faction is not present\n",
                "Tried to take a(n) ", faction, " follower from the supply")
                break
            end
            r.followers[faction] += 1
            r.size += 1
            board.supply[faction] -= 1
        end
    end
end

function place_follower!(board::Board, d::Pair{String, String})
    place_follower!(board, d.second, d.first)
end


using Knet: Knet, AutoGrad

module AutoML

using ArgParse

export
    Knet,
    AutoGrad,
    Model,
    Config,
    savemodel,
    loadmodel,
    preprocess,
    preprocess2,
    preparedata,
    build,
    hyperoptimization,
    train,
    partialtrain,
    predictdata,
    crossvalidate,
    getbatches,
    csv2data,
    readtext,
    splitdata,
    house_rentals,
    splice_junction,
    cifar100,
    imdb_movie_review,
    quora_questions,
    default_of_credit,
    prediction_of_return,
    train_house_rentals,
    train_gene_sequences,
    train_cifar_100,
    train_imdb,
    train_quora,
    train_default_of_credit,
    train_prediction_of_return

const DIR = @__DIR__
const SAVEDIR = abspath(joinpath(DIR, "..", "models"))
const DATADIR = abspath(joinpath(DIR, "..", "data"))

include("config.jl")
include("layers.jl")
include("data.jl")
include("utils.jl")
include("model.jl")
include("build.jl")
include("clean.jl")
include("preprocess.jl")
include("hyperopt.jl")
include("train.jl")
include("examples.jl")
include("resnet.jl")
include("bert.jl")

function main(args)
    s = ArgParseSettings()
    s.description = "Automated Machine Learning Tool"

    @add_arg_table s begin
        ("--config"; default=nothing; help="Yaml file for config")
        ("--train"; default=nothing; help="Training dataset file.")
        ("--model_name"; default="model"; help="Model name")
        ("--model_path"; default=nothing; help="Pretrained model path")
        ("--predict"; default=nothing; help="Test dataset file.")
        ("--epochs"; default="1"; help="Number of epochs")
        ("--optimize"; default="true"; help="Hyperparameter optimization")
    end

    isa(args, AbstractString) && (args=split(args))
    println(s.description)
    o = parse_args(args, s; as_symbols=true)

    istrain = o[:train] != nothing
    ispredict = o[:predict] != nothing

    if istrain
        o[:config] == nothing && throw("Config file is missing")

        yamlpath = isabspath(o[:config]) ? o[:config] :
            joinpath(dirname(DIR), o[:config])
        trainpath = isabspath(o[:train]) ? o[:train] :
            joinpath(dirname(DIR), o[:train])
        epochs = parse(Int, o[:epochs])
        optimize = parse(Bool, o[:optimize])
        modelname = o[:model_name]

        inputfeatures, outputfeatures = readyaml(yamlpath)
        model = Model(inputfeatures, outputfeatures; name=modelname)
        model, dtrn = train(model, trainpath;
                            epochs=epochs,
                            showprogress=false,
                            issave=true,
                            optimize=optimize)

        println("Error: ", model.model(dtrn.x, dtrn.y))
        println("Accuracy: ", accuracy(model.model, dtrn))

        return
    elseif ispredict
        o[:model_path] == nothing && throw("Model path is missing")

        modelpath = isabspath(o[:model_path]) ? o[:model_path] :
            joinpath(dirname(DIR), o[:model_path])
        testpath = isabspath(o[:predict]) ? o[:predict] :
            joinpath(dirname(DIR), o[:predict])

        model = loadmodel(modelpath)
        dtst = getbatches(model, testpath; showtime=false)
        println("Error: ", model.model(dtst.x, dtst.y))
        println("Accuracy: ", accuracy(model.model, dtst))
    else
        throw("Use --train or --predict parameters to use package")
    end
end

basename(PROGRAM_FILE) == "AutoML.jl" && main(ARGS)

end


include("../src/BlueMesh7.jl")
using .BlueMesh7

using Random
using DataFrames
using Statistics: mean
using ProgressMeter
using BSON: @load, @save
using Flux
using CUDA
# ■

benchmark = DataFrame()
minutes = 7
ngraphs = 4

nactors = [32, 64, 96]
dims = [(25, 25), (30, 30), (35, 35)]

setups = [(d, n) for d in dims, n in nactors] |> vec
results = DataFrame()

Random.seed!(7)
allgraphs = [generate_positions(; n, dims) for _ = 1:ngraphs, (dims, n) in setups]

function bench(name, benchmark; init, unroll)
    println("Benching $(name)")
    bench_stats = DataFrame()

    @showprogress for (setup, graphs) in zip(setups, eachcol(allgraphs))
        setup_stats = DataFrame()
        spin = Threads.SpinLock()

        Threads.@threads for graph in graphs
            # GIL avoidance for python algorithms
            lock(spin)
            mesh = init(graph)
            unlock(spin)

            Random.seed!(7)
            stats = unroll(mesh, minutes)

            # since when it's not thread safe
            lock(spin)
            push!(setup_stats, stats)
            unlock(spin)
        end

        @assert nrow(setup_stats) == ngraphs
        averages = [setup, colwise(mean, setup_stats)...]
        push!(bench_stats, NamedTuple{(:setup, :pdr, :worstpdr, :delay, :centrality, :packetloss)}(averages))
    end

    bench_stats.algorithm = name

    for row in eachrow(bench_stats)
        push!(benchmark, row)
    end
end

bench("All relays (0)", benchmark,
      init = coordinates -> initialize_mesh(coordinates, ones(Int, length(coordinates))),
      unroll = (mesh, minutes) -> start(mesh; minutes))

bench("Half relays (0)", benchmark,
      init = coordinates -> initialize_mesh(coordinates, rand(0:1, length(coordinates))),
      unroll = (mesh, minutes) -> start(mesh; minutes))

# using PyCall
# pushfirst!(PyVector(pyimport("sys")."path"), "../test/ble-mesh-algorithms/standard-algorithms/")
# greedy = pyimport("greedy_algorithms")
# dominator = pyimport("dominator_algorithm")

# function init_rssi_to_adjacency(coordinates::Vector{NTuple{2, Int}}, Oracle)
#     protomesh = initialize_mesh(coordinates, ones(Int, length(coordinates)))
#     mask = protomesh.rssi_map .> protomesh.scanner_sensitivity + 1e-10

#     adjacency = zeros(Int, size(protomesh.rssi_map))
#     adjacency[mask] .= 1
#     adjacency = adjacency[2:end, 2:end]

#     roles = Oracle(adjacency)
#     initialize_mesh(coordinates, roles)
# end

# bench("Connect (1)", benchmark,
#       init = coordinates -> init_rssi_to_adjacency(coordinates, greedy.greedy_connect),
#       unroll = (mesh, minutes) -> start(mesh; minutes))

# bench("Dominator (1)", benchmark,
#       init = coordinates -> init_rssi_to_adjacency(coordinates, dominator.dominator),
#       unroll = (mesh, minutes) -> start(mesh; minutes))

@load "model.bson" model

function unroll_mc(env, minutes)
    moves = zeros(env.model.n)
    S = get_state(env)

    ps = model(S) |> softmax

    moves = rand.(mapslices(Categorical, ps', dims=2)) |> vec

    env(moves)

    for t = 1:minutes * 60_000
        env(moves, true)
    end

    getstats(env.model)
end

bench("Monte Carlo (3)", benchmark,
      init = (positions) -> BlueMesh7Env(positions),
      unroll = unroll_mc)

# ■

using Dates
datenow = Date(now())
@save "benchmark-$(datenow).bson" benchmark


@testset "rl_unplugged_atari" begin
    ds = rl_unplugged_atari_dataset(
        "Pong",
        1,
        [1, 2];
        shuffle_buffer_size = 10_000,
        tf_reader_bufsize = 1*1024*1024,
        tf_reader_sz = 10_000,
        batch_size = 256,
        n_preallocations = Threads.nthreads() * 12
    )

    @test typeof(ds) <: AbstractChannel

    data_1 = take!(ds)

    frame_size = 84
    stack_size = 4
    batch_size = 256

    @test size(data_1.state) == (frame_size, frame_size, stack_size, batch_size)
    @test size(data_1.next_state) == (frame_size, frame_size, stack_size, batch_size)
    @test size(data_1.action) == (batch_size,)
    @test size(data_1.next_action) == (batch_size,)
    @test size(data_1.reward) == (batch_size,)
    @test size(data_1.terminal) == (batch_size,)
    @test size(data_1.episode_id) == (batch_size,)
    @test size(data_1.episode_return) == (batch_size,)

    @test typeof(data_1.state) == Array{UInt8, 4}
    @test typeof(data_1.next_state) == Array{UInt8, 4}
    @test typeof(data_1.action) == Vector{Int64}
    @test typeof(data_1.next_action) == Vector{Int64}
    @test typeof(data_1.reward) == Vector{Float32}
    @test typeof(data_1.terminal) == Vector{Bool}
    @test typeof(data_1.episode_id) == Vector{Int64}
    @test typeof(data_1.episode_return) == Vector{Float32}

    take!(ds)
    take!(ds)
    data_2 = take!(ds)

    @test data_1.state != data_2.state
    @test data_1.next_state != data_2.next_state
    @test data_1.action != data_2.action
    @test data_1.next_action != data_2.next_action
    @test data_1.reward != data_2.reward
    @test data_1.episode_id != data_2.episode_id
    @test data_1.episode_return != data_2.episode_return

end


"""
Shorthand for `isnothing(optional) ? fallback : optional`
"""
@inline ifnothing(optional, fallback) = isnothing(optional) ? fallback : optional

function alignedbboxnode!(
    suggestedbbox::Node{BBox},
    computedsize::Node{NTuple{2, Optional{Float32}}},
    alignment::Node,
    sizeattrs::Node,
    autosizenode::Node{NTuple{2, Optional{Float32}}})

    finalbbox = Node(BBox(0, 100, 0, 100))

    onany(suggestedbbox, alignment, computedsize) do sbbox, al, csize

        bw = width(sbbox)
        bh = height(sbbox)

        # we only passively retrieve sizeattrs here because if they change
        # they also trigger computedsize, which triggers this node, too
        # we only need to know here if there are relative sizes given, because
        # those can only be computed knowing the suggestedbbox
        widthattr, heightattr = sizeattrs[]

        cwidth, cheight = csize

        w = if isnothing(cwidth)
            @match widthattr begin
                wa::Relative => wa.x * bw
                wa::Nothing => bw
                wa::Auto => if isnothing(autosizenode[][1])
                        # we have no autowidth available anyway
                        # take suggested width
                        bw
                    else
                        # use the width that was auto-computed
                        autosizenode[][1]
                    end
                wa => error("At this point, if computed width is not known,
                widthattr should be a Relative or Nothing, not $wa.")
            end
        else
            cwidth
        end

        h = if isnothing(cheight)
            @match heightattr begin
                ha::Relative => ha.x * bh
                ha::Nothing => bh
                ha::Auto => if isnothing(autosizenode[][2])
                        # we have no autoheight available anyway
                        # take suggested height
                        bh
                    else
                        # use the height that was auto-computed
                        autosizenode[][2]
                    end
                ha => error("At this point, if computed height is not known,
                heightattr should be a Relative or Nothing, not $ha.")
            end
        else
            cheight
        end

        # how much space is left in the bounding box
        rw = bw - w
        rh = bh - h

        xshift = @match al[1] begin
            :left => 0.0f0
            :center => 0.5f0 * rw
            :right => rw
            x::Real => x * rw
            x => error("Invalid horizontal alignment $x (only Real or :left, :center, or :right allowed).")
        end

        yshift = @match al[2] begin
            :bottom => 0.0f0
            :center => 0.5f0 * rh
            :top => rh
            x::Real => x * rh
            x => error("Invalid vertical alignment $x (only Real or :bottom, :center, or :top allowed).")
        end

        # align the final bounding box in the layout bounding box
        l = left(sbbox) + xshift
        b = bottom(sbbox) + yshift
        r = l + w
        t = b + h

        newbbox = BBox(l, r, b, t)
        # if finalbbox[] != newbbox
        #     finalbbox[] = newbbox
        # end
        finalbbox[] = newbbox
    end

    finalbbox
end

function computedsizenode!(sizeattrs, autosizenode::Node{NTuple{2, Optional{Float32}}})

    # set up csizenode with correct type manually
    csizenode = Node{NTuple{2, Optional{Float32}}}((nothing, nothing))

    onany(sizeattrs, autosizenode) do sizeattrs, autosize

        wattr, hattr = sizeattrs
        wauto, hauto = autosize

        wsize = computed_size(wattr, wauto)
        hsize = computed_size(hattr, hauto)

        csizenode[] = (wsize, hsize)
    end

    # trigger first value
    sizeattrs[] = sizeattrs[]

    csizenode
end

function computed_size(sizeattr, autosize)
    ms = @match sizeattr begin
        sa::Nothing => nothing
        sa::Real => sa
        sa::Fixed => sa.x
        sa::Relative => nothing
        sa::Auto => if sa.trydetermine
                # if trydetermine we report the autosize to the layout
                autosize
            else
                # but not if it's false, this allows for single span content
                # not to shrink its column or row, like a small legend next to an
                # axis or a super title over a single axis
                nothing
            end
        sa => error("""
            Invalid size attribute $sizeattr.
            Can only be Nothing, Fixed, Relative, Auto or Real""")
    end
end

function sizenode!(widthattr::Node, heightattr::Node)
    sizeattrs = Node{Tuple{Any, Any}}((widthattr[], heightattr[]))
    onany(widthattr, heightattr) do w, h
        sizeattrs[] = (w, h)
    end
    sizeattrs
end

function sceneareanode!(finalbbox, limits, aspect)

    scenearea = Node(IRect(0, 0, 100, 100))

    onany(finalbbox, limits, aspect) do bbox, limits, aspect

        w = width(bbox)
        h = height(bbox)
        # mw = min(w, maxsize[1])
        # mh = min(h, maxsize[2])
        # as = mw / mh
        as = w / h
        mw, mh = w, h

        if aspect isa AxisAspect
            aspect = aspect.aspect
        elseif aspect isa DataAspect
            aspect = limits.widths[1] / limits.widths[2]
        end

        if !isnothing(aspect)
            if as >= aspect
                # too wide
                mw *= aspect / as
            else
                # too high
                mh *= as / aspect
            end
        end

        restw = w - mw
        resth = h - mh

        # l = left(bbox) + alignment[1] * restw
        # b = bottom(bbox) + alignment[2] * resth
        l = left(bbox) + 0.5f0 * restw
        b = bottom(bbox) + 0.5f0 * resth

        newbbox = BBox(l, l + mw, b, b + mh)

        # only update scene if pixel positions change
        new_scenearea = IRect2D(newbbox)
        if new_scenearea != scenearea[]
            scenearea[] = new_scenearea
        end
    end

    scenearea
end

function create_suggested_bboxnode(n::Nothing)
    Node(BBox(0, 100, 0, 100))
end

function create_suggested_bboxnode(tup::Tuple)
    Node(BBox(tup...))
end

function create_suggested_bboxnode(bbox::AbstractPlotting.Rect2D)
    Node(BBox(bbox))
end

function create_suggested_bboxnode(node::Node{BBox})
    node
end

function roundedrectvertices(rect, cornerradius, cornersegments)
    cr = cornerradius
    csegs = cornersegments

    cr = min(width(rect) / 2, height(rect) / 2, cr)

    # inner corners
    ictl = topleft(rect) .+ Point2(cr, -cr)
    ictr = topright(rect) .+ Point2(-cr, -cr)
    icbl = bottomleft(rect) .+ Point2(cr, cr)
    icbr = bottomright(rect) .+ Point2(-cr, cr)

    # check if corners touch so we can remove one vertex that is doubled
    wtouching = width(rect) / 2 == cr
    htouching = height(rect) / 2 == cr

    cstr = if wtouching
        anglepoint.(Ref(ictr), LinRange(0, pi/2, csegs), cr)
    else
        anglepoint.(Ref(ictr), LinRange(0, pi/2, csegs)[1:end-1], cr)
    end
    cstl = if htouching
        anglepoint.(Ref(ictl), LinRange(pi/2, pi, csegs), cr)
    else
        anglepoint.(Ref(ictl), LinRange(pi/2, pi, csegs)[1:end-1], cr)
    end
    csbl = if wtouching
        anglepoint.(Ref(icbl), LinRange(pi, 3pi/2, csegs), cr)
    else
        anglepoint.(Ref(icbl), LinRange(pi, 3pi/2, csegs)[1:end-1], cr)
    end
    csbr = if htouching
        anglepoint.(Ref(icbr), LinRange(3pi/2, 2pi, csegs), cr)
    else
        anglepoint.(Ref(icbr), LinRange(3pi/2, 2pi, csegs)[1:end-1], cr)
    end
    arr = [cstr; cstl; csbl; csbr]
end

function anglepoint(center::Point2, angle::Real, radius::Real)
    Ref(center) .+ Ref(Point2(cos(angle), sin(angle))) .* radius
end

function enlarge(bbox::BBox, l, r, b, t)
    BBox(left(bbox) - l, right(bbox) + r, bottom(bbox) - b, top(bbox) + t)
end

function center(bbox::BBox)
    Point2f0((right(bbox) + left(bbox)) / 2, (top(bbox) + bottom(bbox)) / 2)
end

"""
Converts a point in fractions of rect dimensions into real coordinates.
"""
function fractionpoint(bbox::BBox, point::T) where T <: Point2
    T(left(bbox) + point[1] * width(bbox), bottom(bbox) + point[2] * height(bbox))
end

function tightlimits!(la::LAxis)
    la.xautolimitmargin = (0, 0)
    la.yautolimitmargin = (0, 0)
    autolimits!(la)
end

function tightlimits!(la::LAxis, sides::Union{Left, Right, Bottom, Top}...)
    for s in sides
        tightlimits!(la, s)
    end
end

function tightlimits!(la::LAxis, ::Left)
    la.xautolimitmargin = Base.setindex(la.xautolimitmargin[], 0.0, 1)
    autolimits!(la)
end

function tightlimits!(la::LAxis, ::Right)
    la.xautolimitmargin = Base.setindex(la.xautolimitmargin[], 0.0, 2)
    autolimits!(la)
end

function tightlimits!(la::LAxis, ::Bottom)
    la.yautolimitmargin = Base.setindex(la.yautolimitmargin[], 0.0, 1)
    autolimits!(la)
end

function tightlimits!(la::LAxis, ::Top)
    la.yautolimitmargin = Base.setindex(la.yautolimitmargin[], 0.0, 2)
    autolimits!(la)
end

function Base.foreach(f::Function, contenttype::Type, layout::GridLayout; recursive = true)
    for c in layout.content
        if recursive && c.content isa GridLayout
            foreach(f, contenttype, c.content)
        elseif c.content isa contenttype
            f(c.content)
        end
    end
end

"""
Swaps or rotates the layout positions of the given elements to their neighbor's.
"""
function swap!(layout_elements...)
    gridcontents = [le.layoutnodes.gridcontent for le in layout_elements]

    # copy relevant fields before gridcontents are mutated
    parents = map(gc -> gc.parent, gridcontents)
    spans = map(gc -> gc.span, gridcontents)
    sides = map(gc -> gc.side, gridcontents)

    for (gc, parent, span, side) in zip(circshift(gridcontents, 1), parents, spans, sides)
        parent[span.rows, span.cols, side] = gc.content
    end
end