text:
module LLVM

using Unicode
using Printf
using Libdl

## discovery

using Libdl

VERSION >= v"0.7.0-DEV.2576" || error("This version of LLVM.jl requires Julia 0.7")

let
    # find LLVM library

    libllvm_paths = filter(Libdl.dllist()) do lib
        occursin(r"LLVM\b", basename(lib))
    end
    if isempty(libllvm_paths)
        error("""
            Cannot find the LLVM library loaded by Julia.
            Please use a version of Julia that has been built with USE_LLVM_SHLIB=1 (like the official binaries).
            If you are, please file an issue and attach the output of `Libdl.dllist()`.""")
    end
    if length(libllvm_paths) > 1
        error("""
            Multiple LLVM libraries loaded by Julia.
            Please file an issue and attach the output of `Libdl.dllist()`.""")
    end
    global const libllvm = first(libllvm_paths)
    Base.include_dependency(libllvm)

    global const libllvm_version = Base.libllvm_version::VersionNumber

    # figure out the supported targets by looking at initialization routines
    lib = Libdl.dlopen(libllvm)
    llvm_targets = [:AArch64, :AMDGPU, :ARC, :ARM, :AVR, :BPF, :Hexagon, :Lanai, :MSP430,
                    :Mips, :NVPTX, :PowerPC, :RISCV, :Sparc, :SystemZ, :WebAssembly, :X86,
                    :XCore]
    global const libllvm_targets = filter(llvm_targets) do target
        sym = Libdl.dlsym_e(lib, Symbol("LLVMInitialize$(target)Target"))
        sym !== nothing
    end
    # TODO: figure out the name of the native target

    @debug "Found LLVM v$libllvm_version at $libllvm with support for $(join(libllvm_targets, ", "))"

    # find appropriate LLVM.jl wrapper

    vercmp_match(a,b)  = a.major==b.major &&  a.minor==b.minor
    vercmp_compat(a,b) = a.major>b.major  || (a.major==b.major && a.minor>=b.minor)

    llvmjl_wrappers_path = joinpath(@__DIR__, "..", "lib")

    llvmjl_wrappers = filter(path->isdir(joinpath(llvmjl_wrappers_path, path)),
                                   readdir(llvmjl_wrappers_path))
    @assert !isempty(llvmjl_wrappers)

    matching_wrappers = filter(wrapper->vercmp_match(libllvm_version,
                                                     VersionNumber(wrapper)),
                                    llvmjl_wrappers)
    global const llvmjl_wrapper = if !isempty(matching_wrappers)
        @assert length(matching_wrappers) == 1
        matching_wrappers[1]
    else
        compatible_wrappers = filter(wrapper->vercmp_compat(libllvm_version,
                                                            VersionNumber(wrapper)),
                                    llvmjl_wrappers)
        isempty(compatible_wrappers) && error("Could not find any compatible wrapper for LLVM $(libllvm_version)")
        last(compatible_wrappers)
    end

    @debug "Using LLVM.jl wrapper for LLVM v$llvmjl_wrapper"

    # backwards-compatible flags

    global const libllvm_system = false

    global const configured = true
end

## source code includes

include("util.jl")

include("base.jl")

module API
using CEnum
using ..LLVM
using ..LLVM: @apicall, libllvm_version
const off_t = Csize_t
libdir = joinpath(@__DIR__, "..", "lib", LLVM.llvmjl_wrapper)
include(joinpath(libdir, "libLLVM_common.jl"))
include(joinpath(libdir, "libLLVM_h.jl"))
include(joinpath(libdir, "..", "libLLVM_extra.jl"))
end

# LLVM API wrappers
include("support.jl")
include("types.jl")
include("passregistry.jl")
include("init.jl")
include("core.jl")
include("linker.jl")
include("irbuilder.jl")
include("analysis.jl")
include("moduleprovider.jl")
include("pass.jl")
include("passmanager.jl")
include("execution.jl")
include("buffer.jl")
include("target.jl")
include("targetmachine.jl")
include("datalayout.jl")
include("ir.jl")
include("bitcode.jl")
include("transform.jl")
include("debuginfo.jl")

include("interop.jl")

include("deprecated.jl")

## initialization

function __init__()
    libllvm_paths = filter(Libdl.dllist()) do lib
        occursin(r"LLVM\b", basename(lib))
    end
    if length(libllvm_paths) > 1
        # NOTE: this still allows switching to a non-USE_LLVM_SHLIB version, but
        #       there's no way to detect that since the new libLLVM is loaded before this...
        cachefile = if VERSION >= v"1.3-"
            Base.compilecache_path(Base.PkgId(LLVM))
        else
            abspath(DEPOT_PATH[1], Base.cache_file_entry(Base.PkgId(LLVM)))
        end
        rm(cachefile)
        error("Your set-up changed, and LLVM.jl needs to be reconfigured. Please load the package again.")
    end

    _install_handlers()
    _install_handlers(GlobalContext())
end

end


text:
@testset "utils" begin

    ex1 = :(foo(x, y=1, z=2))
    ex2 = :(foo(x; y=1, z=2))
    args, kw_args = parse_call_args(ex1)
    @test args == [:x]
    @test kw_args == Dict(:y => 1, :z => 2)

    @test without_keywords(ex1) == :(foo(x))
    @test without_keywords(ex2) == :(foo(x))
    @test with_keywords(:(foo(x)), Dict(:y => 1, :z => 2)) == ex2

    @test parse_call_expr(ex1) == (:foo, [:x], Dict(:y => 1, :z => 2))
    @test parse_call_expr(ex2) == (:foo, [:x], Dict(:y => 1, :z => 2))
end

ext: jl


text:
function uniquearrayrefs(ls::LoopSet)
    uniquerefs = ArrayReferenceMeta[]
    includeinlet = Bool[]
    # for arrayref ∈ ls.refs_aliasing_syms
    for op ∈ operations(ls)
        arrayref = op.ref
        arrayref === NOTAREFERENCE && continue
        notunique = false
        isonlyname = true
        for ref ∈ uniquerefs
            notunique = sameref(arrayref, ref)
            isonlyname &= vptr(arrayref) !== vptr(ref)
            # if they're not the sameref, they may still have the same name
            # if they have different names, they're definitely not sameref
            notunique && break
        end
        if !notunique
            push!(uniquerefs, arrayref)
            push!(includeinlet, isonlyname)
        end
        # any(ref -> sameref(arrayref, ref), uniquerefs) || push!(uniquerefs, arrayref)
        # any(ref -> vptr(ref) === vptr(arrayref), uniquerefs) || push!(uniquerefs, arrayref)
    end
    uniquerefs, includeinlet
end

otherindexunrolled(loopsym::Symbol, ind::Symbol, loopdeps::Vector{Symbol}) = (loopsym !== ind) && (loopsym ∈ loopdeps)
function otherindexunrolled(ls::LoopSet, ind::Symbol, ref::ArrayReferenceMeta)
    us = ls.unrollspecification[]
    u₁sym = names(ls)[us.u₁loopnum]
    u₂sym = us.u₂loopnum > 0 ? names(ls)[us.u₂loopnum] : Symbol("##undefined##")
    otherindexunrolled(u₁sym, ind, loopdependencies(ref)) || otherindexunrolled(u₂sym, ind, loopdependencies(ref))
end
multiple_with_name(n::Symbol, v::Vector{ArrayReferenceMeta}) = sum(ref -> n === vptr(ref), v) > 1
# TODO: DRY between indices_calculated_by_pointer_offsets and use_loop_induct_var
function indices_calculated_by_pointer_offsets(ls::LoopSet, ar::ArrayReferenceMeta)
    indices = getindices(ar)
    ls.isbroadcast[] && return fill(false, length(indices))
    looporder = names(ls)
    offset = isdiscontiguous(ar)
    gespinds = Expr(:tuple)
    out = Vector{Bool}(undef, length(indices))
    li = ar.loopedindex
    # @show ls.vector_width[]
    for i ∈ eachindex(li)
        ii = i + offset
        ind = indices[ii]
        # j = findfirst(isequal(ind), view(indices, offset+1:ii-1))
        # if !isnothing(j)
        #     out[i] = out[j - offset]
        #     continue
        # end
        if (!li[i]) || multiple_with_name(vptr(ar), ls.lssm[].uniquearrayrefs) ||
            (iszero(ls.vector_width[]) && isstaticloop(getloop(ls, ind)))# ||
            # ((ls.align_loops[] > 0) && (first(names(ls)) == ind))
            out[i] = false
        elseif (isone(ii) && (first(looporder) === ind))
            out[i] = otherindexunrolled(ls, ind, ar)
        else
            out[i] = true
        end
    end
    out
end

@generated function set_first_stride(sptr::StridedPointer{T,N,C,B,R}) where {T,N,C,B,R}
    minrank = argmin(R)
    newC = C > 0 ? (C == minrank ? 1 : 0) : C
    newB = C > 0 ? (C == minrank ? B : 0) : B #TODO: confirm correctness
    quote
        $(Expr(:meta,:inline))
        VectorizationBase.StridedPointer{$T,1,$newC,$newB,$(R[minrank],)}(pointer(sptr), (sptr.strd[$minrank],), (Zero(),))
    end
end
set_first_stride(x) = x # cross fingers that this works
@inline onetozeroindexgephack(sptr::AbstractStridedPointer) = gesp(set_first_stride(sptr), (Static{-1}(),)) # go backwords
@inline onetozeroindexgephack(sptr::AbstractStridedPointer{T,1}) where {T} = sptr
# @inline onetozeroindexgephack(sptr::StridedPointer{T,1}) where {T} = sptr
@inline onetozeroindexgephack(x) = x

"""
Returns a vector of length equal to the number of indices.
A value > 0 indicates which loop number that index corresponds to when incrementing the pointer.
A value < 0 indicates that abs(value) is the corresponding loop, and a `loopvalue` will be used.
"""
function use_loop_induct_var!(ls::LoopSet, q::Expr, ar::ArrayReferenceMeta, allarrayrefs::Vector{ArrayReferenceMeta}, includeinlet::Bool)
    us = ls.unrollspecification[]
    li = ar.loopedindex
    looporder = reversenames(ls)
    uliv = Vector{Int}(undef, length(li))
    indices = getindices(ar)
    offset = first(indices) === Symbol("##DISCONTIGUOUSSUBARRAY##")
    if length(indices) != offset + length(li)
        println(ar)
        throw("Length of indices and length of offset do not match!")
    end
    isbroadcast = ls.isbroadcast[]
    gespinds = Expr(:tuple)
    offsetprecalc_descript = Expr(:tuple)
    use_offsetprecalc = false
    for i ∈ eachindex(li)
        ii = i + offset
        ind = indices[ii]
        # j = findfirst(isequal(ind), view(indices, offset+1:ii-1))
        # if !isnothing(j)
        #     j -= offset
        #     push!(gespinds.args, gespinds.args[j])
        #     uliv[i] = uliv[j]
        # else
        if (!li[i])
            uliv[i] = 0
            # push!(gespinds.args, Expr(:call, lv(:Zero)))
            push!(gespinds.args, Expr(:call, Expr(:curly, lv(:Static), 1)))
            push!(offsetprecalc_descript.args, 0)
        elseif isbroadcast ||
            ((isone(ii) && (last(looporder) === ind)) && !(otherindexunrolled(ls, ind, ar)) ||
             multiple_with_name(vptr(ar), allarrayrefs)) ||
             (iszero(ls.vector_width[]) && isstaticloop(getloop(ls, ind)))# ||
             # ((ls.align_loops[] > 0) && (first(names(ls)) == ind))

            # Not doing normal offset indexing
            uliv[i] = -findfirst(isequal(ind), looporder)::Int
            # push!(gespinds.args, Expr(:call, lv(:Zero)))
            push!(gespinds.args, Expr(:call, Expr(:curly, lv(:Static), 1)))

            push!(offsetprecalc_descript.args, 0) # not doing offset indexing, so push 0
        else
            uliv[i] = findfirst(isequal(ind), looporder)::Int
            loop = getloop(ls, ind)
            if loop.startexact
                push!(gespinds.args, Expr(:call, Expr(:curly, lv(:Static), loop.starthint)))
            else
                push!(gespinds.args, loop.startsym)
            end
            # if loop.startexact
            #     push!(gespinds.args, Expr(:call, Expr(:curly, lv(:Static), loop.starthint - 1)))
            # else
            #     push!(gespinds.args, Expr(:call, lv(:staticm1), loop.startsym))
            # end
            if ind === names(ls)[us.vectorizedloopnum]
                push!(offsetprecalc_descript.args, 0)
            elseif (ind === names(ls)[us.u₁loopnum]) & (us.u₁ > 3)
                use_offsetprecalc = true
                push!(offsetprecalc_descript.args, us.u₁)
            elseif (ind === names(ls)[us.u₂loopnum]) & (us.u₂ > 3)
                use_offsetprecalc = true
                push!(offsetprecalc_descript.args, us.u₂)
            else
                push!(offsetprecalc_descript.args, 0)
            end
        end
    end
    if includeinlet
        vptr_ar = if isone(length(li))
            # Workaround for fact that 1-d OffsetArrays are offset when using 1 index, but multi-dim ones are not
            Expr(:call, lv(:onetozeroindexgephack), vptr(ar))
        else
            vptr(ar)
        end
        if use_offsetprecalc
            push!(q.args, Expr(:(=), vptr(ar), Expr(:call, lv(:offsetprecalc), Expr(:call, lv(:gesp), vptr_ar, gespinds), Expr(:call, Expr(:curly, :Val, offsetprecalc_descript)))))
        else
            push!(q.args, Expr(:(=), vptr(ar), Expr(:call, lv(:gesp), vptr_ar, gespinds)))
        end
    end
    uliv
end

# Plan here is that we increment every unique array
function add_loop_start_stop_manager!(ls::LoopSet)
    q = Expr(:block)
    us = ls.unrollspecification[]
    # Presence of an explicit use of a loopinducation var means we should use that, so we look for one
    # TODO: replace first with only once you add Compat as a dep or drop support for older Julia versions
    loopinductvars = map(op -> first(loopdependencies(op)), filter(isloopvalue, operations(ls)))
    # Filtered ArrayReferenceMetas, we must increment each
    arrayrefs, includeinlet = uniquearrayrefs(ls)
    use_livs = map((ar,iil) -> use_loop_induct_var!(ls, q, ar, arrayrefs, iil), arrayrefs, includeinlet)
    # loops, sorted from outer-most to inner-most
    looporder = reversenames(ls)
    # For each loop, we need to choose an induction variable
    nloops = length(looporder)
    # loopstarts = Vector{Vector{ArrayReferenceMeta}}(undef, nloops)
    loopstarts = fill(ArrayReferenceMeta[], nloops)
    terminators = Vector{Int}(undef, nloops) # zero is standard loop induct var
    # loopincrements = Vector{Vector{ArrayReferenceMeta}}(undef, nloops) # Not needed; copy of loopstarts
    # The purpose of the reminds thing here is to pick which of these to use for the terminator
    # We want to pick the one with the fewest outer loops with respect to this one, to minimize
    # the number of redefinitions of max-pointer used for the termination comparison.
    reached_indices = zeros(Int, length(arrayrefs))
    for (i,loopsym) ∈ enumerate(looporder) # iterates from outer to inner
        loopstartᵢ = ArrayReferenceMeta[]; arⱼ = 0; minrem = typemax(Int);
        ric = Tuple{Int,Int}[]
        for j ∈ eachindex(use_livs) # j is array ref number
            for (l,k) ∈ enumerate(use_livs[j])# l is index number, k is loop number
                if k == i
                    push!(loopstartᵢ, arrayrefs[j])
                    push!(ric, ((reached_indices[j] += 1), length(loopstartᵢ)))
                end
            end
        end
        loopstarts[i] = loopstartᵢ
        terminators[i] = if (loopsym ∈ loopinductvars) || (any(r -> any(isequal(-i), r), use_livs)) || iszero(length(loopstartᵢ))
            0
        else
            # @show i, loopsym loopdependencies.(operations(ls)) operations(ls)
            # @assert !iszero(length(loopstartᵢ))
            last(ric[argmin(first.(ric))]) # index corresponds to array ref's position in loopstart
        end
    end
    ls.lssm[] = LoopStartStopManager(
        reverse!(terminators), reverse!(loopstarts), arrayrefs
    )
    q
end
maxsym(ptr, sub) = Symbol(ptr, "##MAX##", sub, "##")
function pointermax(ls::LoopSet, ar::ArrayReferenceMeta, n::Int, sub::Int, isvectorized::Bool)::Expr
    pointermax(ls, ar, n, sub, isvectorized, getloop(ls, names(ls)[n]))
end
function pointermax(ls::LoopSet, ar::ArrayReferenceMeta, n::Int, sub::Int, isvectorized::Bool, loop::Loop)::Expr
    if loop.startexact & loop.stopexact
        return pointermax(ls, ar, n, sub, isvectorized, length(loop))
    end
    looplensym = loop.startexact & isone(loop.starthint) ? loop.stopsym : loop.lensym
    pointermax(ls, ar, n, sub, isvectorized, looplensym)
end
function pointermax_index(ls::LoopSet, ar::ArrayReferenceMeta, n::Int, sub::Int, isvectorized::Bool, stophint::Int)::Tuple{Expr,Int}
    # @unpack u₁loopnum, u₂loopnum, vectorizedloopnum, u₁, u₂ = us
    loopsym = names(ls)[n]
    index = Expr(:tuple)
    found_loop_sym = false
    ind = 0
    for (j,i) ∈ enumerate(getindicesonly(ar))
        if i === loopsym
            ind = j
            if iszero(sub)
                push!(index.args, stophint)
            elseif isvectorized
                if isone(sub)
                    push!(index.args, Expr(:call, lv(:vsub_fast), staticexpr(stophint), VECTORWIDTHSYMBOL))
                else
                    push!(index.args, Expr(:call, lv(:vsub_fast), staticexpr(stophint), Expr(:call, lv(:vmul_fast), VECTORWIDTHSYMBOL, staticexpr(sub))))
                end
            else
                push!(index.args, staticexpr(stophint - sub))
            end
        else
            push!(index.args, Expr(:call, lv(:Zero)))
        end
    end
    @assert ind != 0 "Failed to find $loopsym"
    index, ind
end
function pointermax_index(ls::LoopSet, ar::ArrayReferenceMeta, n::Int, sub::Int, isvectorized::Bool, stopsym)::Tuple{Expr,Int}
    loopsym = names(ls)[n]
    index = Expr(:tuple);
    ind = 0
    for (j,i) ∈ enumerate(getindicesonly(ar))
        if i === loopsym
            ind = j
            if iszero(sub)
                push!(index.args, stopsym)
            elseif isvectorized
                if isone(sub)
                    push!(index.args, Expr(:call, lv(:vsub_fast), stopsym, VECTORWIDTHSYMBOL))
                else
                    push!(index.args, Expr(:call, lv(:vsub_fast), stopsym, Expr(:call, lv(:vmul_fast), VECTORWIDTHSYMBOL, sub)))
                end
            else
                push!(index.args, Expr(:call, lv(:vsub_fast), stopsym, sub))
            end
        else
            push!(index.args, Expr(:call, lv(:Zero)))
        end
    end
    @assert ind != 0 "Failed to find $loopsym"
    index, ind
end
function pointermax(ls::LoopSet, ar::ArrayReferenceMeta, n::Int, sub::Int, isvectorized::Bool, stopsym)::Expr
    index = first(pointermax_index(ls, ar, n, sub, isvectorized, stopsym))
    Expr(:call, lv(:pointerforcomparison), vptr(ar), index)
    # @show ar, loopsym
end

function defpointermax(ls::LoopSet, ar::ArrayReferenceMeta, n::Int, sub::Int, isvectorized::Bool)::Expr
    Expr(:(=), maxsym(vptr(ar), sub), pointermax(ls, ar, n, sub, isvectorized))
end
function offsetindex(dim::Int, ind::Int, scale::Int, isvectorized::Bool)
    index = Expr(:tuple)
    for d ∈ 1:dim
        if d != ind || iszero(scale)
            push!(index.args, Expr(:call, lv(:Zero)))
            continue
        end
        if isvectorized
            if isone(scale)
                push!(index.args, VECTORWIDTHSYMBOL)
            else
                push!(index.args, Expr(:call, lv(:vmul_fast), VECTORWIDTHSYMBOL, staticexpr(scale)))
            end
        else
            push!(index.args, staticexpr(scale))
        end
    end
    index
end
function append_pointer_maxes!(loopstart::Expr, ls::LoopSet, ar::ArrayReferenceMeta, n::Int, submax::Int, isvectorized::Bool, stopindicator)
    if submax < 2
        for sub ∈ 0:submax
            push!(loopstart.args, Expr(:(=), maxsym(vptr(ar), sub), pointermax(ls, ar, n, sub, isvectorized, stopindicator)))
            # push!(loopstart.args, defpointermax(ls, ptrdefs[termind], n, sub, isvectorized, stopindicator))
        end
    else
        index, ind = pointermax_index(ls, ar, n, submax, isvectorized, stopindicator)
        vptr_ar = vptr(ar)
        _pointercompbase = maxsym(vptr_ar, submax)
        pointercompbase = gensym(_pointercompbase)
        push!(loopstart.args, Expr(:(=), pointercompbase, Expr(:call, lv(:gesp), vptr_ar, index)))
        push!(loopstart.args, Expr(:(=), _pointercompbase, Expr(:call, lv(:pointerforcomparison), pointercompbase)))
        dim = length(getindicesonly(ar))
        # OFFSETPRECALCDEF = true
        # if OFFSETPRECALCDEF
        for sub ∈ 0:submax-1
            push!(loopstart.args, Expr(:(=), maxsym(vptr_ar, sub), Expr(:call, lv(:pointerforcomparison), pointercompbase, offsetindex(dim, ind, submax - sub, isvectorized))))
        end
        # else
        #     indexoff = offsetindex(dim, ind, 1, isvectorized)
        #     for sub ∈ submax-1:-1:0
        #         _newpointercompbase = maxsym(vptr_ar, sub)
        #         newpointercompbase = gensym(_pointercompbase)
        #         push!(loopstart.args, Expr(:(=), newpointercompbase, Expr(:call, lv(:gesp), pointercompbase, indexoff)))
        #         push!(loopstart.args, Expr(:(=), _newpointercompbase, Expr(:call, lv(:pointerforcomparison), newpointercompbase)))
        #         _pointercompbase = _newpointercompbase
        #         pointercompbase = newpointercompbase
        #     end
        # end
    end
end
function append_pointer_maxes!(loopstart::Expr, ls::LoopSet, ar::ArrayReferenceMeta, n::Int, submax::Int, isvectorized::Bool)
    loop = getloop(ls, names(ls)[n])
    if loop.startexact & loop.stopexact
        return append_pointer_maxes!(loopstart, ls, ar, n, submax, isvectorized, length(loop))
    end
    looplensym = loop.startexact & isone(loop.starthint) ? loop.stopsym : loop.lensym
    append_pointer_maxes!(loopstart, ls, ar, n, submax, isvectorized, looplensym)
end

function maxunroll(us::UnrollSpecification, n)
    @unpack u₁loopnum, u₂loopnum, u₁, u₂ = us
    if n == u₁loopnum# && u₁ > 1
        u₁
    elseif n == u₂loopnum# && u₂ > 1
        u₂
    else
        1
    end
end

function startloop(ls::LoopSet, us::UnrollSpecification, n::Int, submax = maxunroll(us, n))
    @unpack u₁loopnum, u₂loopnum, vectorizedloopnum, u₁, u₂ = us
    lssm = ls.lssm[]
    termind = lssm.terminators[n]
    ptrdefs = lssm.incrementedptrs[n]
    loopstart = Expr(:block)
    firstloop = n == num_loops(ls)
    for ar ∈ ptrdefs
        ptr = vptr(ar)
        push!(loopstart.args, Expr(:(=), ptr, ptr))
    end
    if iszero(termind)
        loopsym = names(ls)[n]
        push!(loopstart.args, startloop(getloop(ls, loopsym), loopsym))
    else
        isvectorized = n == vectorizedloopnum
        append_pointer_maxes!(loopstart, ls, ptrdefs[termind], n, submax, isvectorized)
    end
    loopstart
end
function offset_ptr(ar::ArrayReferenceMeta, us::UnrollSpecification, loopsym::Symbol, n::Int, UF::Int, offsetinds::Vector{Bool})
    indices = getindices(ar)
    offset = first(indices) === DISCONTIGUOUS
    gespinds = Expr(:tuple)
    li = ar.loopedindex
    for i ∈ eachindex(li)
        ii = i + offset
        ind = indices[ii]
        if !offsetinds[i]
            push!(gespinds.args, Expr(:call, lv(:Zero)))
        elseif ind == loopsym
            incrementloopcounter!(gespinds, us, n, UF)
        else
            push!(gespinds.args, Expr(:call, lv(:Zero)))
        end
        # ind == loopsym && break
    end
    Expr(:(=), vptr(ar), Expr(:call, lv(:gesp), vptr(ar), gespinds))
end
function incrementloopcounter(ls::LoopSet, us::UnrollSpecification, n::Int, UF::Int)
    @unpack u₁loopnum, u₂loopnum, vectorizedloopnum, u₁, u₂ = us
    lssm = ls.lssm[]
    ptrdefs = lssm.incrementedptrs[n]
    looporder = names(ls)
    loopsym = looporder[n]
    q = Expr(:block)
    termind = lssm.terminators[n]
    if iszero(termind) # increment liv
        push!(q.args, incrementloopcounter(us, n, loopsym, UF))
    end
    for (j,ar) ∈ enumerate(ptrdefs)
        offsetinds = indices_calculated_by_pointer_offsets(ls, ar)
        push!(q.args, offset_ptr(ar, us, loopsym, n, UF, offsetinds))
    end
    q
end
function terminatecondition(ls::LoopSet, us::UnrollSpecification, n::Int, inclmask::Bool, UF::Int)
    lssm = ls.lssm[]
    termind = lssm.terminators[n]
    if iszero(termind)
        loop = getloop(ls, names(ls)[n])
        return terminatecondition(loop, us, n, loop.itersymbol, inclmask, UF)
    end

    termar = lssm.incrementedptrs[n][termind]
    ptr = vptr(termar)
    # @show UF, isvectorized(us, n)
    if inclmask && isvectorized(us, n)
        Expr(:call, :<, callpointerforcomparison(ptr), maxsym(ptr, 0))
    else
        Expr(:call, :≤, callpointerforcomparison(ptr), maxsym(ptr, UF))
    end
end


text:
# Use baremodule to shave off a few KB from the serialized `.ji` file
baremodule Tk_jll
using Base
using Base: UUID
import JLLWrappers

JLLWrappers.@generate_main_file_header("Tk")
JLLWrappers.@generate_main_file("Tk", UUID("056b5f69-f28f-5060-ad8a-df43647a2b5c"))
end  # module Tk_jll


text:
# -----------------------------------------------------------------------------
# Integration with AtomsBase
# -----------------------------------------------------------------------------

# Internal type used to represent a MassBody and retain the element metadata
struct ElementMassBody{cType<:Real,mType<:Real} <: Body
    r::SVector{3,cType} # in LENGTH_UNIT
    v::SVector{3,cType} # in VELOCITY_UNIT
    m::mType            # in MASS_UNIT
    symbol::Symbol
    data::Dict{Symbol,Any}
end
function ElementMassBody(r::SVector{3,<:Unitful.Length}, v::SVector{3,<:Unitful.Velocity}, symbol::AtomsBase.AtomId; data...)
    r, v = promote(austrip.(r), austrip.(v))
    ElementMassBody(r, v, austrip(elements[symbol].atomic_mass), Symbol(elements[symbol].symbol), Dict{Symbol,Any}(data...))
end

# Convert AtomsBase Atom to NBodySimulator body
function ElementMassBody(atom::AtomsBase.Atom)
    r, v = promote(austrip.(position(atom)), austrip.(velocity(atom)))
    ElementMassBody(r, v, austrip(atomic_mass(atom)), atomic_symbol(atom), atom.data)
end
# Convert NBodySimulator body to AtomsBase Atom
AtomsBase.Atom(b::ElementMassBody, boundary_conditions::BoundaryConditions) = AtomsBase.Atom(b, b.r, b.v, boundary_conditions)
AtomsBase.Atom(b::ElementMassBody, r::SVector{3,<:Real}, v::SVector{3,<:Real}, boundary_conditions::BoundaryConditions) = AtomsBase.Atom(b, bound_position(r, boundary_conditions) * LENGTH_UNIT, v * VELOCITY_UNIT)
AtomsBase.Atom(b::ElementMassBody, r::SVector{3,<:Unitful.Length}, v::SVector{3,<:Unitful.Velocity}) = AtomsBase.Atom(b.symbol, r, v; b.data...)

# Convert AtomsBase boundary conditions to NBodySimulator boundary conditions
function nbs_boundary_conditions(system::AbstractSystem{3})
    # TODO: support more boundary conditions
    @assert hcat(bounding_box(system)...) == bounding_box(system)[1][1] * I(3)
    @assert all(periodicity(system))
    CubicPeriodicBoundaryConditions(austrip(bounding_box(system)[1][1]))
end
# Convert NBodySimulator boundary conditions to AtomsBase boundary conditions
# TODO: support more boundary conditions
get_boundary_conditions(::CubicPeriodicBoundaryConditions) = @SVector [Periodic(), Periodic(), Periodic()]
# Convert NBodySimulator boundary conditions to AtomsBase bounding box
# TODO: support more boundary conditions
function get_bounding_box(boundary_conditions::CubicPeriodicBoundaryConditions)
    SVector{3}(SVector{3}.(eachrow(boundary_conditions.L * LENGTH_UNIT * I(3))))
end

# Bound a position according to the boundary conditions
# TODO: support more boundary conditions
bound_position(r::SVector{3,<:Real}, boundary_conditions::CubicPeriodicBoundaryConditions) = mod.(r, boundary_conditions.L)


text:
using Base.Test
import WaterData
import WaterData.testhelpers: between, noerror

@testset "Regions and tessellations" begin
    @testset "Bounding boxes" begin
        bb = WaterData.BoundingBox(1,2,0,3)
        @test (1.5, 1.5) ∈ bb
        @test (0, 0) ∉ bb
        infbox = WaterData.BoundingBox(-2.5, 2.5, -Inf, Inf)
        @test (1, 4) ∈ infbox
        @test (-2, 3e9) ∈ infbox
        @test (-8, 0) ∉ infbox
    end

    @testset "Polygon inclusion testing" begin
        xs = [1, 5, 5, 4, 3, 2, 1]
        ys = [1, 1, 3, 2, 3, 2, 3]
        poly = WaterData.Polygon(xs, ys)

        # outside bounding box
        @test (0, 0) ∉ poly
        @test (6, 2) ∉ poly
        # clearly inside, non-pathological
        @test (1.5, 1.5) ∈ poly
        @test (3, 2.5) ∈ poly
        @test (4.5, 1.5) ∈ poly
        # outside, but within bounding box, non-pathological
        @test (2, 2.5) ∉ poly
        @test (4, 2.5) ∉ poly
        # outside, pathogical cases
        @test (0, 1) ∉ poly
        @test (0, 2) ∉ poly
        @test (0, 3) ∉ poly
        @test (6, 1) ∉ poly
        @test (6, 2) ∉ poly
        @test (6, 3) ∉ poly
        # inside, pathological cases
        @test (3, 2) ∈ poly
        @test (1.5, 2) ∈ poly

        # one more to convince myself it works
        xs = [1, 2, 3, 4, 5, 5, 4, 3, 2, 1]
        ys = [3, 3, 4, 3, 3, 2, 2, 1, 2, 2]
        poly = WaterData.Polygon(xs, ys)

        # outside bounding box
        @test (-3, -2) ∉ poly
        # clearly inside, non-pathological
        @test (2.5, 2.5) ∈ poly
        # outside, but within bounding box, non-pathological
        @test (1.5, 3.5) ∉ poly
        # outside, pathogical cases
        @test (1, 4) ∉ poly
        @test (0, 3) ∉ poly
        # inside, pathological cases
        @test (3, 2) ∈ poly
        @test (3, 3) ∈ poly
    end

    @testset "Normalisation functions" begin
        norm12 = WaterData.norm12
        unnorm = WaterData.unnorm
        lognorm12 = WaterData.lognorm12
        unlognorm = WaterData.unlognorm
        min_coord = WaterData.min_coord
        max_coord = WaterData.max_coord

        @testset "Normalisation to (1,2)" begin
            @test norm12(4, 4, 6) == min_coord
            @test norm12(6, 4, 6) == max_coord
            @test norm12(5, 4, 6) ≈ 1.5
            @test norm12(0, 4, 6) < min_coord
            @test norm12(10, 4, 6) > max_coord

            r = collect(linspace(20, 30))
            @test all(norm12(r) .≥ min_coord)
            @test all(norm12(r) .≤ max_coord)

            @test lognorm12(10^1.5, 10^1, 10^2) ≈ 1.5
            @test lognorm12(5, 10, 100) < min_coord
            @test lognorm12(500, 10, 100) > max_coord
            @test lognorm12(10, 10, 100) == min_coord
            @test lognorm12(100, 10, 100) == max_coord

            r2 = collect(logspace(3, 4))
            @test all(lognorm12(r2) .≥ min_coord)
            @test all(lognorm12(r2) .≤ max_coord)
        end

        @testset "De-normalising from (1,2)" begin
            @test unnorm(min_coord, 40, 60) == 40
            @test unnorm(max_coord, 40, 60) == 60
            @test unnorm(norm12(50, 40, 60), 40, 60) ≈ 50
            @test unnorm(0.5, 40, 60) < 40
            @test unnorm(2.5, 40, 60) > 60

            @test unlognorm(min_coord, 10, 100) ≈ 10
            @test unlognorm(max_coord, 10, 100) ≈ 100
            @test unlognorm(lognorm12(50, 10, 100), 10, 100) ≈ 50
            @test unlognorm(0.5, 10, 100) < 10
            @test unlognorm(500, 10, 100) > 500
        end
    end

    @testset "Tessellation and interpolation" begin
        geta = WaterData.geta
        getb = WaterData.getb
        getc = WaterData.getc
        getx = WaterData.getx
        gety = WaterData.gety

        xs = [0, 1, 0, 1]
        ys = [0, 0, 1, 1]
        zs = [0, 1, 1, 2]
        @test WaterData.get_tessellation(xs, ys) |> noerror
        tess = WaterData.get_tessellation(xs, ys)
        @test WaterData.findtriangle(tess, 1.2, 1.2) |> noerror
        @test WaterData.findtriangle(tess, 1.8, 1.8) |> noerror
        tri1 = WaterData.findtriangle(tess, 1.2, 1.2)
        tri2 = WaterData.findtriangle(tess, 1.8, 1.8)
        p1, p2, p3 = geta(tri1), getb(tri1), getc(tri1)
        q1, q2, q3 = geta(tri2), getb(tri2), getc(tri2)

        for f in [getx, gety], p in [p1, p2, p3, q1, q2, q3]
            @test f(p) ∈ (WaterData.min_coord, WaterData.max_coord)
        end

        λ1 = WaterData.barycoords(tri1, 1.2, 1.2)
        @test sum(λ1) ≈ 1
        λ2 = WaterData.barycoords(tri2, 1.8, 1.8)
        @test sum(λ2) ≈ 1

        x1 = [0, 1, 2]
        x2 = [8, 9, 10]
        @test WaterData.lininterp(λ1, x1) |> between(0, 2)
        @test WaterData.lininterp(λ2, x2) |> between(8, 10)
    end

    @testset "Testing for points in a tessellation" begin
        # it's a parallelogram
        tess = WaterData.get_tessellation([0., 1, 1, 2], [0., 0, 1, 1])

        # any point in (1,2)x(1,2) should be inside the bounding box
        @test in(1.5, 1.5, WaterData.BoundingBox(tess))
        @test in(0.5, 1.5, WaterData.BoundingBox(tess)) == false

        # only points actually in the parallelogram should test true here
        @test in(1.1, 1.9, tess) == false  # top left corner
        @test in(1.5, 1.5, tess)    # the middle
        @test in(1.9, 1.1, tess) == false  # bottom right corner
        @test in(1.1, 1.1, tess)    # bottom left corner
        @test in(1.9, 1.9, tess)    # bottom right corner
    end
end


text:
import Base: rand,
             ∈

export ZeroSet,
       linear_map

"""
    ZeroSet{N<:Real} <: AbstractSingleton{N}

Type that represents the zero set, i.e., the set that only contains the origin.

### Fields

- `dim` -- the ambient dimension of this zero set
"""
struct ZeroSet{N<:Real} <: AbstractSingleton{N}
    dim::Int
end

# default constructor of type Float64
ZeroSet(dim::Int) = ZeroSet{Float64}(dim)

# --- AbstractSingleton interface functions ---

"""
    element(S::ZeroSet{N})::Vector{N} where {N<:Real}

Return the element of a zero set.

### Input

- `S` -- zero set

### Output

The element of the zero set, i.e., a zero vector.
"""
function element(S::ZeroSet{N})::Vector{N} where {N<:Real}
    return zeros(N, S.dim)
end

"""
    element(S::ZeroSet{N}, ::Int)::N where {N<:Real}

Return the i-th entry of the element of a zero set.

### Input

- `S` -- zero set
- `i` -- dimension

### Output

The i-th entry of the element of the zero set, i.e., 0.
"""
function element(S::ZeroSet{N}, ::Int)::N where {N<:Real}
    return zero(N)
end

# --- LazySet interface functions ---

"""
    dim(Z::ZeroSet)::Int

Return the ambient dimension of this zero set.

### Input

- `Z` -- a zero set, i.e., a set that only contains the origin

### Output

The ambient dimension of the zero set.
"""
function dim(Z::ZeroSet)::Int
    return Z.dim
end

"""
    σ(d::AbstractVector{N}, Z::ZeroSet{N}) where {N<:Real}

Return the support vector of a zero set.

### Input

- `d` -- direction
- `Z` -- a zero set, i.e., a set that only contains the origin

### Output

The returned value is the origin since it is the only point that belongs to this
set.
"""
function σ(d::AbstractVector{N}, Z::ZeroSet{N}) where {N<:Real}
    @assert length(d) == dim(Z) "the direction has the wrong dimension"
    return element(Z)
end

"""
    ρ(d::AbstractVector{N}, Z::ZeroSet{N}) where {N<:Real}

Evaluate the support function of a zero set in a given direction.

### Input

- `d` -- direction
- `Z` -- a zero set, i.e., a set that only contains the origin

### Output

`0`.
"""
function ρ(d::AbstractVector{N}, Z::ZeroSet{N}) where {N<:Real}
    @assert length(d) == dim(Z) "the direction has the wrong dimension"
    return zero(N)
end

"""
    ∈(x::AbstractVector{N}, Z::ZeroSet{N})::Bool where {N<:Real}

Check whether a given point is contained in a zero set.

### Input

- `x` -- point/vector
- `Z` -- zero set

### Output

`true` iff ``x ∈ Z``.

### Examples

```jldoctest
julia> Z = ZeroSet(2);

julia> ∈([1.0, 0.0], Z)
false
julia> ∈([0.0, 0.0], Z)
true
```
"""
function ∈(x::AbstractVector{N}, Z::ZeroSet{N})::Bool where {N<:Real}
    @assert length(x) == dim(Z)

    zero_N = zero(N)
    return all(i -> x[i] == zero_N, eachindex(x))
end

"""
    rand(::Type{ZeroSet}; [N]::Type{<:Real}=Float64, [dim]::Int=2,
         [rng]::AbstractRNG=GLOBAL_RNG, [seed]::Union{Int, Nothing}=nothing
        )::ZeroSet{N}

Create a zero set (note that there is nothing to randomize).

### Input

- `ZeroSet` -- type for dispatch
- `N`       -- (optional, default: `Float64`) numeric type
- `dim`     -- (optional, default: 2) dimension
- `rng`     -- (optional, default: `GLOBAL_RNG`) random number generator
- `seed`    -- (optional, default: `nothing`) seed for reseeding

### Output

The (only) zero set of the given numeric type and dimension.
"""
function rand(::Type{ZeroSet};
              N::Type{<:Real}=Float64,
              dim::Int=2,
              rng::AbstractRNG=GLOBAL_RNG,
              seed::Union{Int, Nothing}=nothing
             )::ZeroSet{N}
    rng = reseed(rng, seed)
    return ZeroSet{N}(dim)
end

"""
    linear_map(M::AbstractMatrix{N}, Z::ZeroSet{N}) where {N<:Real}

Concrete linear map of a zero set.

### Input

- `M` -- matrix
- `Z` -- zero set

### Output

The zero set whose dimension matches the output dimension of the given matrix.
"""
function linear_map(M::AbstractMatrix{N}, Z::ZeroSet{N}) where {N<:Real}
    @assert dim(Z) == size(M, 2) "a linear map of size $(size(M)) cannot be " *
                                 "applied to a set of dimension $(dim(Z))"

    return ZeroSet(size(M, 1))
end

"""
    translate(Z::ZeroSet{N}, v::AbstractVector{N}) where {N<:Real}

Translate (i.e., shift) a zero set by a given vector.

### Input

- `Z` -- zero set
- `v` -- translation vector

### Output

A singleton containing the vector `v`.
"""
function translate(Z::ZeroSet{N}, v::AbstractVector{N}) where {N<:Real}
    @assert length(v) == dim(Z) "cannot translate a $(dim(Z))-dimensional " *
                                "set by a $(length(v))-dimensional vector"
    return Singleton(v)
end


text:
using PSCOPF

using .PSCOPFFixtures

using Test
using JuMP
using Dates
using DataStructures
using Printf

@testset verbose=true "test_rso_assessment_call" begin

    function create_instance(limit_1, impositions_1, impositions_2,
                            u_wind_1, u_bus_1, u_bus_2,
                            limit::Float64=35.,
                            logs=nothing)
        network = PSCOPFFixtures.network_2buses(limit=limit)
        # Limitables
        PSCOPF.Networks.add_new_generator_to_bus!(network, "bus_1", "wind_1_1", PSCOPF.Networks.LIMITABLE,
                                                0., 200.,
                                                0., 1.,
                                                Dates.Second(0), Dates.Second(0))
        # Pilotables
        PSCOPF.Networks.add_new_generator_to_bus!(network, "bus_1", "prod_1_1", PSCOPF.Networks.PILOTABLE,
                                                0., 200.,
                                                0., 10.,
                                                Dates.Second(0), Dates.Second(0))
        PSCOPF.Networks.add_new_generator_to_bus!(network, "bus_2", "prod_2_1", PSCOPF.Networks.PILOTABLE,
                                                0., 200.,
                                                0., 50.,
                                                Dates.Second(0), Dates.Second(0))

        # Uncertainties
        uncertainties = PSCOPF.Uncertainties()

        #Fixme to args
        assessment_uncertainties = SortedDict("bus_1" => u_bus_1,
                                            "bus_2" => u_bus_2,
                                            "wind_1_1" => u_wind_1
                                            )

        context = PSCOPF.PSCOPFContext(network, TS, PSCOPF.PSCOPF_MODE_1,
                                    SortedDict{String,PSCOPF.GeneratorState}(), #gen_initial_state
                                    uncertainties, assessment_uncertainties, logs)

        #TODO save impositions in UncertainValue
        PSCOPF.set_imposition_definitive_value!(PSCOPF.get_tso_actions(context), "prod_1_1", TS[1], impositions_1[1], impositions_1[2])
        PSCOPF.set_imposition_definitive_value!(PSCOPF.get_tso_actions(context), "prod_2_1", TS[1], impositions_2[1], impositions_2[2])

        PSCOPF.set_limitation_value!(PSCOPF.get_tso_actions(context), "wind_1_1", TS[1], limit_1)

        return context
    end

    TS = [DateTime("2015-01-01T11:00:00")]
    ech = DateTime("2015-01-01T07:00:00")
    next_ech = DateTime("2015-01-01T07:30:00")

    #=
                    bus 1                   bus 2
                            |                      |
        (limitable) wind_1_1|       "1_2"          |
        Pmin=0, Pmax=200    |                      |
        Csta=0, Cprop=1     |                      |
        [50-90]             |----------------------|
                            |         35           |
                            |                      |
        (pilotable) prod_1_1|                      |(pilotable) prod_2_1
        Pmin=20, Pmax=200   |                      | Pmin=20, Pmax=200
        Csta=10k, Cprop=10  |                      | Csta=50k, Cprop=50
        [0-0]               |                      | [20-200]
                            |                      |
               load(bus_1)  |                      |load(bus_2)
        [30-50]             |                      | [20-70]

        #overflow = 10.
        #     75
        #      0 ---45 > 35---> 20
        #     30                70
        # or
        #     75
        #      0 ---45 > 35---> 0
        #     30                50
    =#
    @testset verbose=true "test_possible_rso_violation" begin
        limit_1 = 75.
        impositions_1 = (0., 0.)
        impositions_2 = (20., 200.)
        u_wind_1 = (50, 90)
        u_bus_1 = (30, 50)
        u_bus_2 = (20, 70)

        context = create_instance(limit_1, impositions_1, impositions_2,
                                u_wind_1, u_bus_1, u_bus_2,
                                35.)

        assessment = PSCOPF.RSOAssessment()
        result = PSCOPF.run(assessment, ech, TS, context)

        @test !PSCOPF.is_validated(result)

        @test 10 ≈ value(result.overflow[TS[1]])
    end

    #=
                    bus 1                   bus 2
                            |                      |
        (limitable) wind_1_1|       "1_2"          |
        Pmin=0, Pmax=200    |                      |
        Csta=0, Cprop=1     |                      |
        [50-90]             |----------------------|
                            |         35           |
                            |                      |
        (pilotable) prod_1_1|                      |(pilotable) prod_2_1
        Pmin=20, Pmax=200   |                      | Pmin=20, Pmax=200
        Csta=10k, Cprop=10  |                      | Csta=50k, Cprop=50
        [0-0]               |                      | [20-105]
                            |                      |
               load(bus_1)  |                      |load(bus_2)
        [30-50]             |                      | [20-70]

    =#
    @testset verbose=true "test_no_possible_rso_violation" begin
        limit_1 = 65.
        impositions_1 = (0., 0.)
        impositions_2 = (20., 105.)
        u_wind_1 = (50, 90)
        u_bus_1 = (30, 50)
        u_bus_2 = (20, 70)

        context = create_instance(limit_1, impositions_1, impositions_2,
                                u_wind_1, u_bus_1, u_bus_2,
                                35.,)

        assessment = PSCOPF.RSOAssessment()
        result = PSCOPF.run(assessment, ech, TS, context)

        @test PSCOPF.is_validated(result)
        @test value(result.overflow[TS[1]]) < 1e-09
    end

    #=
                    bus 1                   bus 2
                            |                      |
        (limitable) wind_1_1|       "1_2"          |
        Pmin=0, Pmax=200    |                      |
        Csta=0, Cprop=1     |                      |
        [50-90]             |----------------------|
                            |         35           |
                            |                      |
        (pilotable) prod_1_1|                      |(pilotable) prod_2_1
        Pmin=20, Pmax=200   |                      | Pmin=20, Pmax=200
        Csta=10k, Cprop=10  |                      | Csta=50k, Cprop=50
        [0-0]               |                      | [20-200]
                            |                      |
               load(bus_1)  |                      |load(bus_2)
        [30-50]             |                      | [20-70]

        we can generate a violation in this case if we allow over-production
         50
          0 ----------- 200
        -50             -20
        flow = |0.5 (50 - 50) + -0.5 (20 - 200)| = 90
    =#
    @testset verbose=true "test_no_violation_due_to_overproduction" begin
        limit_1 = 65.
        impositions_1 = (0., 0.)
        impositions_2 = (20., 105.)
        u_wind_1 = (50, 90)
        u_bus_1 = (30, 50)
        u_bus_2 = (20, 70)

        context = create_instance(limit_1, impositions_1, impositions_2,
                                u_wind_1, u_bus_1, u_bus_2,
                                35.,)

        assessment = PSCOPF.RSOAssessment()
        result = PSCOPF.run(assessment, ech, TS, context)

        @test PSCOPF.is_validated(result)
        @test value(result.overflow[TS[1]]) < 1e-09
        @test value(result.overflow[TS[1]]) < (90-35)
    end

    #=
                    bus 1                   bus 2
                            |                      |
        (limitable) wind_1_1|       "1_2"          |
        Pmin=0, Pmax=200    |                      |
        Csta=0, Cprop=1     |                      |
        [50-90]             |----------------------|
                            |         35           |
                            |                      |
        (pilotable) prod_1_1|                      |(pilotable) prod_2_1
        Pmin=20, Pmax=200   |                      | Pmin=20, Pmax=200
        Csta=10k, Cprop=10  |                      | Csta=50k, Cprop=50
        [0-0]               |                      | [20-200]
                            |                      |
               load(bus_1)  |                      |load(bus_2)
        [30-50]             |                      | [20-70]

        #TODO Illustrate the limitation of the current Market model
        #ie of choosing prod in order of proportional cost while having supply == demand
        #with this model we constraint the uncertainties indirectly so that the EOD can be satisfied

          ?
          ? ----------- ??? case where if we choose uncertainties to oblige having ???<20 to get a violation
          ?             ?
    =#
    @testset verbose=true "test_drawbacks_of_current_model" begin
    end

end

max_stars_repo_name: klorel/pscopf

max_issues_repo_name: klorel/pscopf

max_forks_repo_name: klorel/pscopf


text:
using Test
using Pluto.Configuration: CompilerOptions
using Pluto.WorkspaceManager: _merge_notebook_compiler_options
import Pluto: update_save_run!, update_run!, WorkspaceManager, ClientSession, ServerSession, Notebook, Cell, project_relative_path
import Distributed

@testset "Workspace manager" begin
# basic functionality is already tested by the reactivity tests

    @testset "Multiple notebooks" begin

        fakeclientA = ClientSession(:fakeA, nothing)
        fakeclientB = ClientSession(:fakeB, nothing)
        🍭 = ServerSession()
        🍭.options.evaluation.workspace_use_distributed = true
        🍭.connected_clients[fakeclientA.id] = fakeclientA
        🍭.connected_clients[fakeclientB.id] = fakeclientB

        notebookA = Notebook([
            Cell("x = 3")
        ])
        fakeclientA.connected_notebook = notebookA

        notebookB = Notebook([
            Cell("x")
        ])
        fakeclientB.connected_notebook = notebookB

        @test notebookA.path != notebookB.path

        Sys.iswindows() && sleep(.5) # workaround for https://github.com/JuliaLang/julia/issues/39270
        update_save_run!(🍭, notebookA, notebookA.cells[1])
        Sys.iswindows() && sleep(.5) # workaround for https://github.com/JuliaLang/julia/issues/39270
        update_save_run!(🍭, notebookB, notebookB.cells[1])

        @test notebookB.cells[1].errored == true

        Sys.iswindows() && sleep(.5) # workaround for https://github.com/JuliaLang/julia/issues/39270
        WorkspaceManager.unmake_workspace((🍭, notebookA))
        Sys.iswindows() && sleep(.5) # workaround for https://github.com/JuliaLang/julia/issues/39270
        WorkspaceManager.unmake_workspace((🍭, notebookB))
    end
    @testset "Variables with secret names" begin
        fakeclient = ClientSession(:fake, nothing)
        🍭 = ServerSession()
        🍭.options.evaluation.workspace_use_distributed = false
        🍭.connected_clients[fakeclient.id] = fakeclient

        notebook = Notebook([
            Cell("result = 1"),
            Cell("result"),
            Cell("elapsed_ns = 3"),
            Cell("elapsed_ns"),
        ])
        fakeclient.connected_notebook = notebook

        update_save_run!(🍭, notebook, notebook.cells[1:4])
        @test notebook.cells[1].output.body == "1"
        @test notebook.cells[2].output.body == "1"
        @test notebook.cells[3].output.body == "3"
        @test notebook.cells[4].output.body == "3"

        WorkspaceManager.unmake_workspace((🍭, notebook))
    end

    @testset "Notebook environment" begin
        session_options = CompilerOptions()
        notebook = Notebook([Cell("x")])
        notebook.compiler_options = CompilerOptions(;project="test")
        @test _merge_notebook_compiler_options(notebook, session_options).project ==
            joinpath(dirname(notebook.path), "test")

        notebook.compiler_options = CompilerOptions(;project=project_relative_path("test"))
        @test _merge_notebook_compiler_options(notebook, session_options).project ==
            project_relative_path("test")

        session_options = CompilerOptions(;project=project_relative_path("test"))
        notebook.compiler_options = CompilerOptions(;project=project_relative_path("Project.toml"))
        @test _merge_notebook_compiler_options(notebook, session_options).project ==
            project_relative_path("Project.toml")
    end

    Sys.iswindows() || (VERSION < v"1.6.0-a") || @testset "Pluto inside Pluto" begin

        client = ClientSession(:fakeA, nothing)
        🍭 = ServerSession()
        🍭.options.evaluation.workspace_use_distributed = true
        🍭.connected_clients[client.id] = client

        notebook = Notebook([
            Cell("""begin
                import Pkg
                Pkg.activate()
                empty!(LOAD_PATH)
                push!(LOAD_PATH, $(repr(Base.load_path()))...)
                import Pluto
            end"""),
            Cell("""
            s = Pluto.ServerSession()
            """),
            Cell("""
            nb = Pluto.SessionActions.open(s, Pluto.project_relative_path("sample", "Tower of Hanoi.jl"); run_async=false, as_sample=true)"""),
            Cell("length(nb.cells)"),
            Cell(""),
        ])
        client.connected_notebook = notebook

        update_run!(🍭, notebook, notebook.cells)

        @test notebook.cells[1].errored == false
        @test notebook.cells[2].errored == false
        @test notebook.cells[3].errored == false
        @test notebook.cells[4].errored == false
        @test notebook.cells[5].errored == false

        setcode(notebook.cells[5], "length(nb.cells)")
        update_run!(🍭, notebook, notebook.cells[5])
        @test notebook.cells[5].errored == false

        desired_nprocs = Distributed.nprocs() - 1
        setcode(notebook.cells[5], "Pluto.SessionActions.shutdown(s, nb)")
        update_run!(🍭, notebook, notebook.cells[5])
        @test noerror(notebook.cells[5])

        while Distributed.nprocs() != desired_nprocs
            sleep(.1)
        end
        sleep(.1)

        WorkspaceManager.unmake_workspace((🍭, notebook))
    end
end


text:
module Hashids

export encode, decode, encodehex, decodehex

using Base.Checked: mul_with_overflow, add_with_overflow

# const RATIO_SEPARATORS = 3.5
const RATIO_SEPARATORS = 7//2
const RATIO_GUARDS = 12
const DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
const DEFAULT_SEPARATORS = "cfhistuCFHISTU"

_reorder!(list::Vector{Char}, salt::AbstractString) = _reorder!(list, collect(salt))
function _reorder!(list::Vector{Char}, salt)
    len_salt = length(salt)
    len_salt == 0 && return list

    integer_sum = 0
    index = 1
    for i in length(list):-1:2
        integer = Int(salt[index])
        integer_sum += integer
        j = mod1(integer + index + integer_sum, i - 1)
        list[i], list[j] = list[j], list[i]
        index = mod1(index + 1, len_salt)
    end

    list
end

"""
    Hashids.Configuration

Hashids' parameter-configuration.
Be sure to place the instance as the 1st argument of [`encode`](@ref), [`decode`](@ref), [`encodehex`](@ref) and [`decodehex`](@ref).

See also: [`configure`](@ref)
"""
struct Configuration
    salt::String
    min_length::Int
    alphabet::String
    guards::String
    separators::String
    function Configuration(salt::AbstractString, min_length::Int, alphabet::AbstractString)
        separators_list = [c for c = DEFAULT_SEPARATORS if c ∈ alphabet]
        alphabet_list = [c for c = unique(alphabet) if c ∉ separators_list && !isspace(c)]
        len_alphabet = length(alphabet_list)
        len_separators = length(separators_list)
        if len_alphabet + len_separators < 16
            throw(ArgumentError("Alphabet must contain at least 16 unique characters."))
        end
        _reorder!(separators_list, salt)
        min_separators = cld(len_alphabet, RATIO_SEPARATORS)
        number_of_missing_separators = min_separators - len_separators
        if number_of_missing_separators > 0
            append!(separators_list, alphabet_list[1:number_of_missing_separators])
            alphabet_list = alphabet_list[number_of_missing_separators + 1:end]
            len_alphabet = length(alphabet_list)
        end
        _reorder!(alphabet_list, salt)

        num_guards = cld(len_alphabet, RATIO_GUARDS)
        if len_alphabet < 3
            guards_list = separators_list[1:num_guards]
            separators_list = separators_list[num_guards + 1:end]
        else
            guards_list = alphabet_list[1:num_guards]
            alphabet_list = alphabet_list[num_guards + 1:end]
        end

        new(string(salt), max(min_length, 0), String(alphabet_list), String(guards_list), String(separators_list))
    end
end
Configuration(; salt::AbstractString = "", min_length::Int = 0, alphabet::AbstractString = DEFAULT_ALPHABET) = Configuration(salt, min_length, alphabet)

"""
    Hashids.configure()
    Hashids.configure(salt)
    Hashids.configure(salt="", min_length=0, alphabet=DEFAULT_ALPHABET)

Configure Hashids with parameters, and return [`Hashids.Configuration`](@ref) instance.
`Hashids.configure()` returns default-configuration.

# Example
```julia-repl
julia> config = Hashids.configure();

julia> config = Hashids.configure("this is my salt");

julia> config = Hashids.configure(salt="this is another salt", min_length=16, alphabet="abcdefghijklmnopqrstuvwxyz");

```

See also: [`Configuration`](@ref)
"""
configure(salt) = Configuration(string(salt), 0, DEFAULT_ALPHABET)
configure(; kwargs...) = Configuration(; kwargs...)

function _hash(number, alphabet_list)
    alphabet_list[reverse(digits(number, base=length(alphabet_list))) .+ 1]
end

function _checked_muladd(x::T, y::Integer, z::Integer) where {T<:Integer}
    _checked_muladd(promote(x, y, z)...)::Union{T, Nothing}
end
function _checked_muladd(x::T, y::T, z::T) where {T<:Integer}
    xy, overflow = mul_with_overflow(x, y)
    overflow && return nothing
    result, overflow = add_with_overflow(xy, z)
    overflow && return nothing
    result
end
_checked_muladd(x::BigInt, y::Integer, z::Integer) = muladd(x, y, z)

_unhash(hashed, alphabet_list) = _checked_unhash(0, hashed, iterate(hashed), alphabet_list)
function _checked_unhash(number::I, hashed, hashed_status, alphabet_list) where {I <: Integer}
    len_alphabet = length(alphabet_list)
    while !isnothing(hashed_status)
        (character, st) = hashed_status
        position = findfirst(isequal(character), alphabet_list)
        isnothing(position) && return nothing
        _number = _checked_muladd(number, len_alphabet, position - 1)
        isnothing(_number) && return _checked_unhash(widen(number), hashed, hashed_status, alphabet_list)
        number = something(_number)
        hashed_status = iterate(hashed, st)
    end
    return number
end

function _ensure_length!(encoded, min_length, alphabet_list, guards, values_hash)
    len_guards = length(guards)
    guard_index = (values_hash + Int(encoded[1])) % len_guards
    pushfirst!(encoded, guards[guard_index + 1])

    if length(encoded) < min_length
        guard_index = (values_hash + Int(encoded[3])) % len_guards
        push!(encoded, guards[guard_index + 1])
    end

    alphabet_list = copy(alphabet_list)
    split_at = length(alphabet_list) ÷ 2
    while length(encoded) < min_length
        _reorder!(alphabet_list, copy(alphabet_list))
        append!(prepend!(encoded, alphabet_list[split_at+1:end]), alphabet_list[1:split_at])
        excess = length(encoded) - min_length
        if excess > 0
            from_index = excess ÷ 2
            # encoded = encoded[from_index+1:from_index+min_length]
            deleteat!(deleteat!(encoded, from_index+min_length+1:lastindex(encoded)), 1:from_index)
        end
    end
    return encoded
end

"""
    encode(config::Hashids.Configuration, values::Array{<:Integer})
    encode(config::Hashids.Configuration, values::Integer...)

Encode the passed `values` to a hash.

# Example
```julia-repl
julia> encode(Hashids.configure(), 1, 2, 3)
"o2fXhV"

julia> encode(Hashids.configure(), [1, 2, 3])
"o2fXhV"

```
"""
encode(config::Configuration, values::Array{<:Integer}) = encode(config, values...)
function encode(config::Configuration, values::Vararg{Integer})
    alphabet_list = collect(config.alphabet)
    separators_list = collect(config.separators)
    salt_list = collect(config.salt)
    len_alphabet = length(alphabet_list)
    len_separators = length(separators_list)
    len_values = length(values)
    values_hash = sum(x % (i + 99) for (i, x) in enumerate(values))
    lottery = alphabet_list[values_hash % len_alphabet + 1]
    encoded = [lottery]

    for (i, value) in enumerate(values)
        alphabet_salt = [lottery; salt_list; alphabet_list][1:len_alphabet]
        _reorder!(alphabet_list, alphabet_salt)
        last = _hash(value, alphabet_list)
        append!(encoded, last)
        value %= Int(last[1]) + i - 1
        i < len_values && push!(encoded, separators_list[value % len_separators + 1])
    end
    if length(encoded) ≤ config.min_length
        _ensure_length!(encoded, config.min_length, alphabet_list, config.guards, values_hash)
    end
    return String(encoded)
end

"""
    decode(config::Hashids.Configuration, hashid::AbstractString)

Restore a numbers list from the passed `hashid`.

# Example
```julia-repl
julia> decode(Hashids.configure(), "o2fXhV")
3-element Array{Int64,1}:
 1
 2
 3

```
"""
function decode(config::Configuration, hashid::AbstractString)
    parts = split(hashid, in(config.guards))
    _hashid = 2 ≤ length(parts) ≤ 3 ? parts[2] : parts[1]
    length(_hashid) > 0 || return Integer[]

    lottery_char = _hashid[1]
    _hashid = _hashid[nextind(_hashid, 1):end]

    hash_parts = split(_hashid, in(config.separators))

    alphabet_list = collect(config.alphabet)
    len_alphabet = length(alphabet_list)
    salt_list = collect(config.salt)
    numbers = Vector{Integer}(undef, length(hash_parts))
    for (idx, part) in enumerate(hash_parts)
        alphabet_salt = [lottery_char; salt_list; alphabet_list][1:len_alphabet]
        alphabet_list = _reorder!(alphabet_list, alphabet_salt)
        number = _unhash(part, alphabet_list)
        isnothing(number) && return Integer[]
        numbers[idx] = something(number)
    end
    encode(config, numbers) == hashid || return Integer[]
    # return numbers
    # promote to uniform types
    return [numbers...]
end

"""
    encodehex(config::Hashids.Configuration, hex::AbstractString)

Convert a hexadecimal string (e.g. a MongoDB id) to a hashid.

# Example
```julia-repl
julia> encodehex(Hashids.configure(), "abcdef123456")
"kmP69lB3xv"

```
"""
function encodehex(config::Configuration, hex::AbstractString)
    isnothing(match(r"^[0-9a-fA-F]+$", hex)) && return ""
    numbers = [parse(Int64, "1" * hex[i:min(i+11,end)], base=16) for i in 1:12:lastindex(hex)]
    encode(config, numbers)
end

"""
    decodehex(config::Hashids.Configuration, hashid::AbstractString)

Resrtore a hexadecimal string (e.g. a MongoDB id) from the passed `hashid`.

# Example
```julia-repl
julia> decodehex(Hashids.configure(), "kmP69lB3xv")
"abcdef123456"

```
"""
function decodehex(config::Configuration, hashid::AbstractString)
    numbers = decode(config, hashid)
    join(string(number, base=16)[2:end] for number in numbers)
end

# Compatibility
@static if !isdefined(Base, :isnothing)
    isnothing(::Nothing) = true
    isnothing(_any) = false
end

end # module


text:
export Scenes

module Scenes

export
    SceneActions

@enum SceneActions NO_ACTION REPLACE REPLACE_TAKE REPLACE_TAKE_UNREGISTER

import Base.show

using ..Ranger

function Base.show(io::IO, node::Ranger.AbstractScene)
    # repl = nothing
    # if  !is_nil(node.replacement)
    #     repl = node.replacement.base.name
    # else
    #     repl = "Nil"
    # end

    # parent = nothing
    # if is_nil(node.base.parent)
    #     parent = "Nil"
    # else
    #     parent = node.base.parent
    # end
    # print(io,
    #     "'", node.base.name, "' ",
    #     "(", node.base.id, ") ",
    #     "repl:{", repl, "}");
    show(io, node.base)
end

end # Module -------------------------------------------------------

max_stars_repo_name: wdevore/Ranger-Julia-SDL

max_issues_repo_name: wdevore/Ranger-Julia-SDL

max_forks_repo_name: wdevore/Ranger-Julia-SDL


text:
@testset "$TEST $G" begin

g4 = PathDiGraph(5, DG)
d1 = EdgeMap(g4,float([ 0 1 2 3 4; 5 0 6 7 8; 9 10 0 11 12; 13 14 15 0 16; 17 18 19 20 0]))
d2 = EdgeMap(g4,sparse(float([ 0 1 2 3 4; 5 0 6 7 8; 9 10 0 11 12; 13 14 15 0 16; 17 18 19 20 0])))

y = dijkstra_shortest_paths(g4, 2, d1)
z = dijkstra_shortest_paths(g4, 2, d2)

@test y.parents == z.parents == [0, 0, 2, 3, 4]
@test y.dists == z.dists == [Inf, 0, 6, 17, 33]

y = dijkstra_shortest_paths(g4, 2, d1; allpaths=true)
z = dijkstra_shortest_paths(g4, 2, d2; allpaths=true)
@test z.predecessors[3] == y.predecessors[3] == [2]

@test enumerate_paths(z) == enumerate_paths(y)
@test enumerate_paths(z)[4] ==
    enumerate_paths(z,4) ==
    enumerate_paths(y,4) == [2,3,4]

g = PathGraph(5, G)
add_edge!(g,2,4)
d = EdgeMap(g, ones(Int, 5,5))
d[2,3] = 100
z = dijkstra_shortest_paths(g,1,d)
@test z.dists == [0, 1, 3, 2, 3]
@test z.parents == [0, 1, 4, 2, 4]

z2 = shortest_paths(g,1,d)
@test z2.dists == z.dists
@test z2.parents == z.parents

end # testset


text:
module PDFmerger

import Base.Filesystem
using Poppler_jll: pdfunite, pdfinfo

export merge_pdfs, append_pdf!

"""
```
  merge_pdfs(files::Vector{AbstractString}, destination::AbstractString = "merged.pdf";
                    cleanup::Bool = false)
```

Merge all PDF files listed in `files` into a PDF named `destination`. Returns the name
 of the destination file.

## Arguments

- `files`: array of file names to merge
- `destination`: name of the newly created pdf
- `cleanup`: if `true`, all `files` are deleted after merging
"""
function merge_pdfs(files::Vector{T}, destination::AbstractString="merged.pdf";
                    cleanup::Bool = false) where T <: AbstractString
    if destination ∈ files
        # rename existing file
        Filesystem.mv(destination, destination * "_x_")
        files[files .== destination] .=  destination * "_x_"
    end

    # Merge large number of files iteratively, because there
    # is a (OS dependent) limit how many files 'pdfunit' can handel at once.
    # See: https://gitlab.freedesktop.org/poppler/poppler/-/issues/334
    filemax = 200

    k = 1
    for files_part in Base.Iterators.partition(files, filemax)
        if k == 1
            outfile_tmp2 = "_temp_destination_$k"

            pdfunite() do unite
                run(`$unite $files_part $outfile_tmp2`)
            end
        else
            outfile_tmp1 = "_temp_destination_$(k-1)"
            outfile_tmp2 = "_temp_destination_$k"

            pdfunite() do unite
                run(`$unite $outfile_tmp1 $files_part $outfile_tmp2`)
            end
        end
        k += 1
    end

    # rename last file
    Filesystem.mv("_temp_destination_$(k-1)", destination, force=true)

    # remove temp files
    Filesystem.rm(destination * "_x_", force=true)
    Filesystem.rm.("_temp_destination_$(i)" for i in 1:(k-2))
    if cleanup
        Filesystem.rm.(files, force=true)
    end

    destination
end

"""
```
  append_pdf!(file1::AbstractString, file2::AbstractString;
              create::Bool = true, cleanup::Bool = false)
```

Appends the PDF `file2` to PDF `file1`.

## Arguments

- `create`: if `true`, `file1` is created if not existing.
- `cleanup`: if `true`, all `file2` is deleted after appending

## Example

Create a single PDF containing a page for each plot:
```Julia
using Plots

for i in 1:5
    p = plot(rand(33));
    savefig(p, "temp.pdf")
    append_pdf!("allplots.pdf", "temp.pdf", cleanup=true)
end
```
"""
function append_pdf!(file1::AbstractString, file2::AbstractString;
                     create::Bool = true, cleanup::Bool = false)
    if Filesystem.isfile(file1)
        merge_pdfs([file1, file2], file1, cleanup=cleanup)
    else
        create || error("File '$file1' does not exist!")
        if cleanup
            Filesystem.mv(file2, file1)
        else
            Filesystem.cp(file2, file1)
        end
    end
end

"""
Count number of pages
"""
function n_pages(file)

    str = pdfinfo() do info
        read(`$info $file`, String)
    end

    m = match(r"Pages:\s+(?<npages>\d+)", str)
    isnothing(m) && error("Could not extract number of pages from:\n\n $str")
    parse(Int, m[:npages])
end

end


text:
__precompile__()
module NetABM
using LightGraphs, Random, DelimitedFiles, StatsBase, SparseArrays, Distributions

export lectura_uw, lfr_network, Agent, Params, get_coop, set_adapt_agents!, update_coop!, set_coop_effect!,
    set_adapt_sign_agents!

export init_demographics!,
    get_coop!, next_state!, update_coop_infections!, update_coop_distance!
    #  , update_effect_given_distance_coop!, update_effect_given_distance_coop_free_dir!
    #  update_single_effect_distance_coop!, update_single_effect_distance_coop_free_dir!

export initialize_demographics!, set_fixed_coop_agents!, set_coop_agents!, SI_attitude!

export assign_contacts!,
    get_next_state!,
    update_state!,
    update_single_given_distance!,
    update_coop_given_distance!,
    rand_sign

export update_all_agents!,
    get_populations,
    export_parameters,
    update_single_effect_distance!,
    update_effect_given_distance!,
    update_single_effect_distance_coop!,
    update_single_effect_distance_coop_free_dir!,
    update_effect_given_distance_coop!,
    update_effect_given_distance_coop_free_dir!,
    update_effect_given_distance_coop_prior!,
    update_single_effect_distance_coop_prior!,
    update_effect_given_distance_coop_stochastic!,
    update_single_effect_distance_coop_stochastic!,
    update_effect_given_distance_coop_alpha!,
    update_single_effect_distance_alpha!,
    risk_assessment!, update_risk_assessment!,
    risk_up!, update_past!, update_single_neighbors!,
    update_all_neighbors!, update_effect_given_distance_coop_alpha_risk!

"""
    Agent(id, neighs, p_cop, state)
Agent definition for the simulations with parameters:
`id          ::Int64 ` -> ID
`state       ::String ` -> Infection state (S, I, R)
`days        ::Int64 ` -> Days the agent will remain infected
`new_state   ::String ` -> Agent's state after meetings
`num_meets   ::Int64 ` -> Number of meetings in a time step
`recovery_t  ::Int64 ` -> Recovery time
`infection_t ::Int64 ` -> Time of infection
`contacts_t  ::Vector{Int64}` -> Contacts at time t
`age_group   ::Int64 ` -> Agent's age group (demographic)
`degree_t    ::Int64` -> Degree (Number of contacts) at time t
`ϵ           ::Float64` -> Bound of confidence
`p_cop       ::Float64 ` -> Cooperation probability
`attitude    ::String ` -> Risk aversion vs risk tolerant behavior: RA, RT
`coop_effect ::Float64 ` -> The mitigation efficacy of the complaince rules take by the agent
`at_home     ::Bool ` -> Flag to represent Agent is at home
`adapter     ::Bool ` -> Flag to represent Agent is willing to change behavior
`prior_bel   ::Float64 ` -> The mean value of the direction change of adaptive behavior
"""
mutable struct Agent
    id          ::Int64
    state       ::String
    new_state   ::String
    previous    ::Array{String}
    num_meets   ::Int64
    recovery_t  ::Int64
    infection_t ::Int64
    contacts_t  ::Vector{Int64}
    coopf       ::Vector{Int64}
    non_coopf   ::Vector{Int64}
    counter     ::Int64
    age_group   ::Int64
    degree_t    ::Int64
    ϵ           ::Float64
    p_cop       ::Float64
    attitude    ::String
    coop_effect ::Float64
    new_coop_effect ::Float64
    at_home     ::Bool
    adapter     ::Bool
    prior_bel   ::Float64
    # DEFAULT CONSTRUCTOR
    Agent(id) = new(
        id,
        "S",
        "S",
        Vector{String}(),
        1,
        5,
        0,
        Vector{Int64}(),
        Vector{Int64}(),
        Vector{Int64}(),
        0,
        0,
        0,
        0.0,
        1.0,
        "rt",
        0.0,
        0.0,
        false,
        true,
        5.0
    )
end

##=================####==============##

"""
    Params(N, p_link, attack)
Global parameters for the simulation
`N::Int64 `  -> Number of agents in the population
`now_t::Int64 `  -> Current time (iteration)
`p_link::Float64` -> Link probability (Erdos-Renyi)
`p_infected_t0::Float64` -> Initial probability (fraction) of infected agents
`p_coop_agents::Float64` -> Initial probability (fraction) of lockdown agents
`attack_rate::Float64` -> Virus' attack rate
"""
mutable struct Params
    num_agents      ::Int64
    now_t           ::Int64
    μ_recovT_agents ::Int64
    p_link          ::Float64
    p_infected_t0   ::Float64
    μ_cop_agents    ::Float64
    σ_cop_agents    ::Float64
    attack_rate     ::Float64
    repetition      ::Int
    # DEFAULT CONSTRUCTOR
    Params() = new(0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
end

#  include("agents.jl")
include("initialize.jl")
include("interactions.jl")
include("environmental.jl")
include("update_schemes.jl")
include("opinion_dynamics.jl")
include("data_out.jl")
end # module


text:
using JuliaZH
using Test

@testset "check ENV" begin
    @test ENV["REPL_LOCALE"] == "zh_CN"
    @test ENV["JULIA_PKG_SERVER"] == "https://mirrors.bfsu.edu.cn/julia/static"
end


text:
using OrdinaryDiffEq
using Trixi

###############################################################################
# semidiscretization of the compressible Euler equations

equations = ShallowWaterEquations2D(gravity_constant=9.81)

initial_condition = initial_condition_convergence_test # MMS EOC test

###############################################################################
# Get the DG approximation space

volume_flux = (flux_wintermeyer_etal, flux_nonconservative_wintermeyer_etal)
solver = DGSEM(polydeg=3, surface_flux=(flux_lax_friedrichs, flux_nonconservative_fjordholm_etal),
               volume_integral=VolumeIntegralFluxDifferencing(volume_flux))

###############################################################################
# Get the TreeMesh and setup a periodic mesh

coordinates_min = (0.0, 0.0)
coordinates_max = (sqrt(2.0), sqrt(2.0))
mesh = TreeMesh(coordinates_min, coordinates_max,
                initial_refinement_level=3,
                n_cells_max=10_000,
                periodicity=true)

# create the semi discretization object
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition, solver,
                                    source_terms=source_terms_convergence_test)

###############################################################################
# ODE solvers, callbacks etc.

tspan = (0.0, 1.0)
ode = semidiscretize(semi, tspan)

summary_callback = SummaryCallback()

analysis_interval = 500
analysis_callback = AnalysisCallback(semi, interval=analysis_interval)

alive_callback = AliveCallback(analysis_interval=analysis_interval)

save_solution = SaveSolutionCallback(interval=200,
                                     save_initial_solution=true,
                                     save_final_solution=true)

callbacks = CallbackSet(summary_callback, analysis_callback, alive_callback, save_solution)

###############################################################################
# run the simulation

# use a Runge-Kutta method with automatic (error based) time step size control
sol = solve(ode, RDPK3SpFSAL49(), abstol=1.0e-8, reltol=1.0e-8,
            save_everystep=false, callback=callbacks);
summary_callback() # print the timer summary


text:
# create bare repo
test_repo_path = joinpath(pwd(), "TestLibGit2")

function cleanup_repo(path)
    p = abspath(path)
    if isdir(p)
        rm(p, recursive=true)
    end
end

function tmp_repo(body, tmp_path)
   cleanup_repo(tmp_path)
   try
        body()
   catch err
       cleanup_repo(tmp_path)
       rethrow(err)
   end
   cleanup_repo(tmp_path)
end

# ------------------------------------
# Tests adapted from Git2Go Library
# ------------------------------------
context("test creating bare repository") do
    tmp_repo(test_repo_path) do
        init_repo(test_repo_path; bare=true)
        repo = GitRepo(test_repo_path)
        try
            @test isa(repo, GitRepo)
            @test isbare(repo)
            @test isempty(repo)
        finally
            close(repo)
            LibGit2.free!(repo)
        end
    end
end

context("test creating repository") do
    tmp_repo(test_repo_path) do
        init_repo(test_repo_path)
        repo = GitRepo(test_repo_path)
        try
            @test isa(repo, GitRepo)
            @test !isbare(repo)
            @test isempty(repo)
            @test abspath(workdir(repo)) == abspath(test_repo_path)
            @test abspath(path(repo)) == joinpath(test_repo_path, ".git")
            @test isa(GitIndex(repo), GitIndex)
            # empty repo has no head
            @test head(repo) == nothing
            # empty repo has no tags
            @test isempty(tags(repo))
            # empty repo has no commits
            @test isempty(commits(repo))
            # empty repo has no references
            @test isempty(references(repo))

            @test isa(GitConfig(repo), GitConfig)
            @test isa(GitTreeBuilder(repo), GitTreeBuilder)
        finally
            close(repo)
            LibGit2.free!(repo)
        end
    end
end

# -----------------------------------------
# Tests adapted from Ruby's Rugged Library
# -----------------------------------------
sandboxed_test("testrepo.git", "test fails to open repos that dont' exist") do test_repo, path
    @test_throws LibGitError{:OS,:NotFound} GitRepo("fakepath/123")
    @test_throws LibGitError{:OS,:NotFound} GitRepo("test")
end

sandboxed_test("testrepo.git", "test can check if object exist") do test_repo, path
    @test Oid("8496071c1b46c854b31185ea97743be6a8774479") in test_repo
    @test exists(test_repo, Oid("8496071c1b46c854b31185ea97743be6a8774479"))
    @test Oid("1385f264afb75a56a5bec74243be9b367ba4ca08") in test_repo
    @test exists(test_repo, Oid("1385f264afb75a56a5bec74243be9b367ba4ca08"))
    @test !(Oid("ce08fe4884650f067bd5703b6a59a8b3b3c99a09") in test_repo)
    @test !(exists(test_repo, Oid("ce08fe4884650f067bd5703b6a59a8b3b3c99a09")))
    @test !(Oid("8496071c1c46c854b31185ea97743be6a8774479") in test_repo)
    @test !(exists(test_repo, Oid("8496071c1c46c854b31185ea97743be6a8774479")))
end

sandboxed_test("testrepo.git", "test can read a raw object") do test_repo, path
    rawobj = read(test_repo, Oid("8496071c1b46c854b31185ea97743be6a8774479"))
    @test match(r"tree 181037049a54a1eb5fab404658a3a250b44335d7", data(rawobj)) != nothing
    @test sizeof(rawobj) == 172
    @test isa(rawobj, OdbObject{GitCommit})
end

sandboxed_test("testrepo.git", "test can read object headers") do test_repo, path
    h = read_header(test_repo, Oid("8496071c1b46c854b31185ea97743be6a8774479"))
    @test h[:type] == GitCommit
    @test h[:nbytes] == 172
end

sandboxed_test("testrepo.git", "test check reads rail on missing objs") do test_repo, path
    @test_throws LibGitError{:Odb,:NotFound} read(test_repo, Oid("a496071c1b46c854b31185ea97743be6a8774471"))
end

sandboxed_test("testrepo.git", "test check read headers fail on missing objects") do test_repo, path
    @test_throws LibGitError{:Odb,:NotFound} read_header(test_repo, Oid("a496071c1b46c854b31185ea97743be6a8774471"))
end

sandboxed_test("testrepo.git", "test walking with block") do test_repo, path
    oid = Oid("a4a7dce85cf63874e984719f4fdd239f5145052f")
    list = {}
    walk(test_repo, oid) do c
       push!(list, c)
    end
    @test join(map(c -> hex(c)[1:5], list), ".") == "a4a7d.c4780.9fd73.4a202.5b5b0.84960"
end

sandboxed_test("testrepo.git", "test walking without block") do test_repo, path
    oid = Oid("a4a7dce85cf63874e984719f4fdd239f5145052f")
    commits = walk(test_repo, oid)
    @test isa(commits, Task)
    @test istaskdone(commits) == false
end

sandboxed_test("testrepo.git", "test lookup object") do test_repo, path
    obj = test_repo[Oid("8496071c1b46c854b31185ea97743be6a8774479")]
    @test isa(obj, GitCommit)
end

sandboxed_test("testrepo.git", "test find reference") do test_repo, path
    ref = lookup_ref(test_repo, "refs/heads/master")
    @test isa(ref, GitReference)
    @test name(ref) == "refs/heads/master"
end

sandboxed_test("testrepo.git", "test match all refs") do test_repo, path
    refs = collect(foreach(GitReference, test_repo, "refs/heads/*"))
    @test length(refs) == 12
end

sandboxed_test("testrepo.git", "test return all ref names") do test_repo, path
    rnames = ref_names(test_repo)
    @test length(rnames) == 21
end

sandboxed_test("testrepo.git", "test return all tags") do test_repo, path
    ts = tags(test_repo)
    @test length(ts) == 7
end

sandboxed_test("testrepo.git", "test return all matching tags") do test_repo, path
    @test length(tags(test_repo, "e90810b")) == 1
    @test length(tags(test_repo, "*tag*")) == 4
end

sandboxed_test("testrepo.git", "test return a list of all remotes") do test_repo, path
    rs = remotes(test_repo)
    @test length(rs) == 5
end

sandboxed_test("testrepo.git", "test lookup head") do test_repo, path
    h = head(test_repo)
    @test isa(h, GitReference)
    @test name(h) == "refs/heads/master"
    @test target(h) == Oid("a65fedf39aefe402d3bb6e24df4d4f5fe4547750")
end

sandboxed_test("testrepo.git", "test set head ref") do test_repo, path
    set_head!(test_repo, "refs/heads/packed")
    @test name(head(test_repo)) == "refs/heads/packed"
end

sandboxed_test("testrepo.git", "test set head invalid") do test_repo, path
    @test_throws LibGitError{:Ref,:InvalidSpec} set_head!(test_repo, "a65fedf39aefe402d3bb6e24df4d4f5fe4547750")
end
#=
sandboxed_test("testrepo.git", "test access a file") do test_repo, path
    id = Oid("a65fedf39aefe402d3bb6e24df4d4f5fe4547750")
    blob = blob_at(test_repo, id, "new.txt")
    @test "my new file\n" == bytestring(blob)
end

sandboxed_test("testrepo.git", "test access a missing file") do test_repo, path
    id = Oid("a65fedf39aefe402d3bb6e24df4d4f5fe4547750")
    blob = blob_at(test_repo, id, "file-not-found.txt")
    @test blob == nothing
end
=#
sandboxed_test("testrepo.git", "test enumerate all objects") do test_repo, path
    @test count(x -> true, test_repo) == 1687
end

sandboxed_test("testrepo.git", "test load alternates") do test_repo, _
    alt_path = joinpath(pwd(), "fixtures", "alternate", "objects")
    repo = GitRepo(path(test_repo), alternates=[alt_path])
    try
      @test count(x->true, repo) == 1690
      @test read(repo, Oid("146ae76773c91e3b1d00cf7a338ec55ae58297e2")) != nothing
    finally
      close(repo)
    end
end

sandboxed_test("testrepo.git", "test alternates with invalid path type") do test_repo, _
    @test_throws ArgumentError GitRepo(path(test_repo), alternates=["error"])
end

sandboxed_test("testrepo.git", "test find merge base between ids") do test_repo, path
    commit1 = Oid("a4a7dce85cf63874e984719f4fdd239f5145052f")
    commit2 = Oid("a65fedf39aefe402d3bb6e24df4d4f5fe4547750")
    base    = Oid("c47800c7266a2be04c571c04d5a6614691ea99bd")
    @test base == merge_base(test_repo, commit1, commit2)
end

sandboxed_test("testrepo.git", "test find merge base between commits") do test_repo, path
    commit1 = test_repo[Oid("a4a7dce85cf63874e984719f4fdd239f5145052f")]
    commit2 = test_repo[Oid("a65fedf39aefe402d3bb6e24df4d4f5fe4547750")]
    base    = Oid("c47800c7266a2be04c571c04d5a6614691ea99bd")
    @test base == merge_base(test_repo, commit1, commit2)
end

sandboxed_test("testrepo.git", "test find mere base between ref and id") do test_repo, path
    commit1 = Oid("a4a7dce85cf63874e984719f4fdd239f5145052f")
    commit2 = "refs/heads/master"
    base    = Oid("c47800c7266a2be04c571c04d5a6614691ea99bd")
    @test base == merge_base(test_repo, commit1, commit2)
end

sandboxed_test("testrepo.git", "test find merge base between many") do test_repo, path
    commit1 = Oid("a4a7dce85cf63874e984719f4fdd239f5145052f")
    commit2 = "refs/heads/packed"
    commit3 = test_repo[Oid("a65fedf39aefe402d3bb6e24df4d4f5fe4547750")]

    base    = Oid("c47800c7266a2be04c571c04d5a6614691ea99bd")
    @test base == merge_base(test_repo, commit1, commit2, commit3)
end

sandboxed_test("testrepo.git", "test ahead behind with ids") do test_repo, path
    ahead, behind = ahead_behind(test_repo,
      Oid("a4a7dce85cf63874e984719f4fdd239f5145052f"),
      Oid("a65fedf39aefe402d3bb6e24df4d4f5fe4547750")
    )
    @test ahead == 2
    @test behind == 1
end

sandboxed_test("testrepo.git", "test ahead behind with commits") do test_repo, path
    ahead, behind = ahead_behind(test_repo,
      test_repo[Oid("a4a7dce85cf63874e984719f4fdd239f5145052f")],
      test_repo[Oid("a65fedf39aefe402d3bb6e24df4d4f5fe4547750")]
    )
    @test ahead == 2
    @test behind == 1
end

#---------------------------
# Merge Commits Repo Test
#---------------------------
sandboxed_test("merge-resolve", "test merge commits") do test_repo, path
    our_commit   = lookup_branch(test_repo, "master") |> tip
    their_commit = lookup_branch(test_repo, "branch") |> tip

    index = merge_commits(test_repo, our_commit, their_commit)
    #TODO: BUG very rarely the length of the index is reported as 6??
    @test length(index) == 8

    @test (Oid("233c0919c998ed110a4b6ff36f353aec8b713487") ==
            Oid(index["added-in-master.txt", 0]))
    @test (Oid("f2e1550a0c9e53d5811175864a29536642ae3821") ==
            Oid(index["automergeable.txt", 0]))
    @test (Oid("4eb04c9e79e88f6640d01ff5b25ca2a60764f216") ==
            Oid(index["changed-in-branch.txt", 0]))
    @test (Oid("11deab00b2d3a6f5a3073988ac050c2d7b6655e2") ==
            Oid(index["changed-in-master.txt", 0]))

    @test (Oid("d427e0b2e138501a3d15cc376077a3631e15bd46") ==
            Oid(index["conflicting.txt", 1]))
    @test (Oid("4e886e602529caa9ab11d71f86634bd1b6e0de10") ==
            Oid(index["conflicting.txt", 2]))
    @test (Oid("2bd0a343aeef7a2cf0d158478966a6e587ff3863") ==
            Oid(index["conflicting.txt", 3]))

    @test (Oid("c8f06f2e3bb2964174677e91f0abead0e43c9e5d") ==
            Oid(index["unchanged.txt", 0]))
    @test has_conflicts(index)
end

#---------------------------
# Shallow Repo Test
#---------------------------
sandboxed_test("testrepo.git", "test shallow repo") do test_repo, path
    sandboxed_test("shallow.git") do shallow_repo, shallow_path
        @test !isshallow(test_repo)
        @test isshallow(shallow_repo)
    end
end

with_tmp_repo_access("test shallow repo write") do test_repo, path
    TEST_CONTENT = "my test data\n"

    context("test can hash data") do
        id = hash_data(GitBlob, TEST_CONTENT)
        @test id == Oid("76b1b55ab653581d6f2c7230d34098e837197674")
    end

    context("test write to odb") do
        id = write!(GitBlob, test_repo, TEST_CONTENT)
        @test id == Oid("76b1b55ab653581d6f2c7230d34098e837197674")
        @test Oid("76b1b55ab653581d6f2c7230d34098e837197674") in test_repo
    end

    context("test no merge base between unrelated branches") do
        info = revparse(test_repo, "HEAD")
        @test isa(info, GitCommit)
        sig = Signature("test", "test@test.com")
        #baseless = commit(test_repo, "null", sig, sig, "",
        #@test merge_base(test_repo, "HEAD", baseless) == nothing
    end

    context("test default signature") do
        testname = "Test User"
        testemail = "test@example.com"
        GitConfig(test_repo)["user.name"] = testname
        GitConfig(test_repo)["user.email"] = testemail
        @assert isa(default_signature(test_repo), Signature)
        @test testname == (default_signature(test_repo) |> name)
        @test testemail == (default_signature(test_repo) |> email)
    end
end

#---------------------------
# Repo Discover Test
#---------------------------
teardown_dir(p::String) = rm(p, recursive=true)

function discover_test(f::Function)
    tmpdir = mktempdir()
    mkdir(joinpath(tmpdir, "foo"))
    try
        f(tmpdir)
    finally
        teardown_dir(tmpdir)
    end
end
discover_test(f::Function, s::String) = (println(s); discover_test(f))

discover_test("test discover false") do tmpdir
    @test_throws LibGitError{:Repo,:NotFound} repo_discover(tmpdir)
end

discover_test("test discover nested false") do tmpdir
    @test_throws LibGitError{:Repo,:NotFound} repo_discover(joinpath(tmpdir, "foo"))
end

discover_test("test discover true") do tmpdir
    repo = init_repo(tmpdir, bare=true)
    root = repo_discover(tmpdir)
    try
        @test isbare(root) == true
        @test path(root) == path(repo)
    finally
        close(root)
        close(repo)
    end
end

discover_test("test discover nested true") do tmpdir
    repo = init_repo(tmpdir, bare=true)
    root = repo_discover(joinpath(tmpdir, "foo"))
    try
        @test isbare(root) == true
        @test path(root) == path(repo)
    finally
        close(root)
        close(repo)
    end
end

#---------------------------
# Repo Init Test
#---------------------------
function repo_init_test(f::Function)
    tmpdir = mktempdir()
    try
        f(tmpdir)
    finally
        teardown_dir(tmpdir)
    end
end
repo_init_test(f::Function, s::String) = (println(s); repo_init_test(f))

repo_init_test("test init bare false") do tmpdir
    repo = init_repo(tmpdir, bare=false)
    try
        @test isbare(repo) == false
    finally
        close(repo)
    end
end

repo_init_test("test init bare true") do tmpdir
    repo = init_repo(tmpdir, bare=true)
    try
        @test isbare(repo) == true
    finally
        close
    end
end

repo_init_test("test init non bare default") do tmpdir
    repo = init_repo(tmpdir)
    try
        @test isbare(repo) == false
    finally
        close(repo)
    end
end

#---------------------------
# Repo Clone Test
#---------------------------
#=
repo_clone_test("basic repo clone") do source, dest
    @show source, dest
    repo = repo_clone(source, dest)
    try
      @test isa(repo, GitRepo)
      #@test open(readline, joinpath(tmppath, "README")) |> chomp == "hey"
      @test (target(head(repo))
                == Oid("36060c58702ed4c2a40832c51758d5344201d89a"))
      @test (target(lookup_ref(repo, "refs/heads/master"))
                == Oid("36060c58702ed4c2a40832c51758d5344201d89a"))
      @test (target(lookup_ref(repo, "refs/remotes/origin/master"))
                == Oid("36060c58702ed4c2a40832c51758d5344201d89a"))
      @test (target(lookup_ref(repo, "refs/remotes/origin/packed"))
                == Oid("41bc8c69075bbdb46c5c6f0566cc8cc5b46e8bd9"))
    finally
      close(repo)
    end
end

#:test_clone_bare
@repo_clone_test begin
    repo = repo_clone(source_path, tmppath, {:bare => true})
    try
        @test isbare(repo)
    catch
        close(repo)
    end
end

#:test_clone_with_progress
@repo_clone_test begin
    total_objects = indexed_objects = received_objects = received_bytes = 0
    callsback = 0
    repo = repo_clone(source_path, tmppath, {
      :callbacks => {
        :transfer_progress => (args...) -> begin
          total_objects, indexed_objects, received_objects, received_bytes = args
          callsback += 1
          end
        }
      }
    )
    close(repo)
    @test 22 == callsback
    @test 19 == total_objects
    @test 19 == indexed_objects
    @test 19 == received_objects
    @test 1563 == received_bytes
end

#:test_clone_quits_on_error
@repo_clone_test begin
    try
      repo_clone(source_path, tmppath, {:callbacks => {
        :transfer_progress => (args...) -> error("boom")
      }})
    catch err
      @test err.msg == "boom"
    end
    @test isdir(joinpath(tmppath, ".git")) == false
end

# test_clone_with_bad_progress_callback
@repo_clone_test begin
    @test_throws repo_clone(source_path, tmppath, {:callbacks => {
        :transfer_progress => ()
      }})
    @test isdir(joinpath(tmppath, ".git")) == false
end
=#
#---------------------------
# Repo Namespace Test
#---------------------------
sandboxed_test("testrepo.git", "test repo namespace") do test_repo, path
    @test namespace(test_repo) == nothing
end

sandboxed_test("testrepo.git", "test set repo namespace") do test_repo, path
    set_namespace!(test_repo, "foo")
    @test namespace(test_repo) == "foo"

    set_namespace!(test_repo, "bar")
    @test namespace(test_repo) == "bar"

    set_namespace!(test_repo, "foo/bar")
    @test namespace(test_repo) == "foo/bar"

    set_namespace!(test_repo, nothing)
    @test namespace(test_repo) == nothing
end

sandboxed_test("testrepo.git", "test empty namespace") do test_repo, path
    set_namespace!(test_repo, "foo")
    @test isempty(ref_names(test_repo))
end

#---------------------------
# Repo Push Test
#---------------------------
sandboxed_clone_test("testrepo.git", "test repo push") do test_repo, remote_repo, path
    create_ref(test_repo,
        "refs/heads/unit_test",
        Oid("8496071c1b46c854b31185ea97743be6a8774479"))
    result = push!(test_repo, "origin",
                ["refs/heads/master",
                "refs/heads/master:refs/heads/foobar",
                "refs/heads/unit_test"])
    @test isempty(result)
    @test (target(lookup_ref(remote_repo, "refs/heads/foobar"))
                == Oid("a65fedf39aefe402d3bb6e24df4d4f5fe4547750"))
    @test (target(lookup_ref(remote_repo, "refs/heads/unit_test"))
                == Oid("8496071c1b46c854b31185ea97743be6a8774479"))
end

sandboxed_clone_test("testrepo.git", "test push to non bare raise err") do test_repo, remote_repo, path
    create_ref(test_repo,
        "refs/heads/unit_test",
        Oid("8496071c1b46c854b31185ea97743be6a8774479"))
    try
        push!(test_repo, "origin", ["refs/heads/master"])
        @test false
    except
        @test true
    end
end

sandboxed_clone_test("testrepo.git", "test push to remote instance") do test_repo, remote_repo, path
    origin = lookup_remote(test_repo, "origin")
    result = push!(test_repo, origin, ["refs/heads/master"])
    @test isempty(result)
end

sandboxed_clone_test("testrepo.git", "test push non ff raise error") do test_repo, remote_repo, path
    create_ref(test_repo,
        "refs/heads/unit_test",
        Oid("8496071c1b46c854b31185ea97743be6a8774479"))
    @test_throws ErrorException push!(test_repo, "origin",
                       ["refs/heads/unit_test:refs/heads/master"])
    @test (target(lookup_ref(remote_repo, "refs/heads/master"))
                == Oid("a65fedf39aefe402d3bb6e24df4d4f5fe4547750"))
end

sandboxed_clone_test("testrepo.git", "test push non ff raise error") do test_repo, remote_repo, path
    create_ref(test_repo,
        "refs/heads/unit_test",
        Oid("8496071c1b46c854b31185ea97743be6a8774479"))
    result = push!(test_repo, "origin",
                   ["+refs/heads/unit_test:refs/heads/master"])
    @test isempty(result)
    @test (target(lookup_ref(remote_repo, "refs/heads/master"))
               == Oid("8496071c1b46c854b31185ea97743be6a8774479"))
end

#---------------------------
# Repo Checkout Test
#---------------------------
sandboxed_checkout_test("test checkout tree with revspec string") do test_repo, test_clone, test_bare
    checkout_tree!(test_repo, "refs/heads/dir", {:strategy => :force})
    set_head!(test_repo, "refs/heads/dir")

    @test isfile(joinpath(workdir(test_repo), "README"))
    @test isfile(joinpath(workdir(test_repo), "branch_file.txt"))
    @test isfile(joinpath(workdir(test_repo), "new.txt"))
    @test isfile(joinpath(workdir(test_repo), "a", "b.txt"))

    @test isfile(joinpath(workdir(test_repo), "ab")) == false

    checkout_tree!(test_repo, "refs/heads/subtrees", {:strategy => :safe})
    set_head!(test_repo, "refs/heads/subtrees")

    @test isfile(joinpath(workdir(test_repo), "README"))
    @test isfile(joinpath(workdir(test_repo), "branch_file.txt"))
    @test isfile(joinpath(workdir(test_repo), "new.txt"))
    @test isfile(joinpath(workdir(test_repo), "ab","4.txt"))
    @test isfile(joinpath(workdir(test_repo), "ab","c", "3.txt"))
    @test isfile(joinpath(workdir(test_repo), "ab","de","2.txt"))
    @test isfile(joinpath(workdir(test_repo), "ab","de","fgh","1.txt"))

    @test isdir(joinpath(workdir(test_repo), "a")) == false
end

sandboxed_checkout_test("test checkout raises errors in callback") do test_repo, test_clone, test_bare
    try
        checkout_tree!(test_repo, "refs/heads/dir",
                {:strategy => :force,
                 :progress => (x...) -> error("fail")})
    catch err
        @test err.msg == "fail"
    end
end

sandboxed_checkout_test("test checkout tree subdir") do test_repo, test_clone, test_bare
    @test isfile(joinpath(workdir(test_repo), "ab")) == false
    checkout_tree!(test_repo, "refs/heads/subtrees",
                   {:strategy => :safe, :paths => "ab/de/"})

    @test isdir(joinpath(workdir(test_repo)), "ab")
    @test isfile(joinpath(workdir(test_repo)), "ab","de","2.txt")
    @test isfile(joinpath(workdir(test_repo)), "ab","de","fgh","1.txt")
end

sandboxed_checkout_test("test checkout tree subtree dir") do test_repo, test_clone, test_bare
    @test isfile(joinpath(workdir(test_repo), "de")) == false
    checkout_tree!(test_repo, "refs/heads/subtrees:ab",
                   {:strategy => :safe, :paths => "de/"})

    @test isdir(joinpath(workdir(test_repo)),  "de")
    @test isfile(joinpath(workdir(test_repo)), "de", "2.txt")
    @test isfile(joinpath(workdir(test_repo)), "de", "fgh", "1.txt")
end

sandboxed_checkout_test("test checkout tree raises with bare repo") do test_repo, test_clone, test_bare
    @test_throws LibGitError{:Repo,:BareRepo} checkout_tree!(test_bare, "HEAD", {:strategy => :safe_create})
end

sandboxed_checkout_test("test checkout tree works with bare repo and target dir") do test_repo, test_clone, test_bare
    d = mktempdir()
    try
        checkout_tree!(test_bare, "HEAD", {:strategy => :safe_create, :target_directory => d})
        @test isfile(joinpath(d, "README"))
        @test isfile(joinpath(d, "new.txt"))
    finally
        rm(d, recursive=true)
    end
end

sandboxed_checkout_test() do test_repo, test_clone, test_bare
    checkout!(test_repo, "dir", {:strategy => :force})
    @test head(test_repo) |> name == "refs/heads/dir"
end

sandboxed_checkout_test("test checkout with head") do test_repo, test_clone, test_bare
    checkout!(test_repo, "dir", {:strategy => :force})
    rm(joinpath(workdir(test_repo), "README"))

    checkout!(test_repo, "HEAD", {:strategy => :force})
    @test isfile(joinpath(workdir(test_repo), "README"))
    @test name(head(test_repo)) == "refs/heads/dir"
end

sandboxed_checkout_test("test checkout with commit detaches HEAD") do test_repo, test_clone, test_bare
    checkout!(test_repo, revparse_oid(test_repo, "refs/heads/dir"), {:strategy => :force})
    @test is_head_detached(test_repo)
    @test revparse_oid(test_repo, "refs/heads/dir") == target(head(test_repo))
end

sandboxed_checkout_test("test checkout with remote branch detaches HEAD") do test_repo, test_clone, test_bare
    checkout!(test_clone, "origin/dir", {:strategy => :force})
    @test is_head_detached(test_clone)
    @test revparse_oid(test_clone, "refs/remotes/origin/dir") == target(head(test_clone))
end


text:
function smoother_kernel(Gs::T, Gf, Gpred, Φ) where {T}
    xs, Ps = meancov(Gs)
    xf, Pf = meancov(Gf)
    xpred, Ppred = meancov(Gpred)

    J = Pf*Φ'/Ppred # C/(C+w)
    xs = xf + J*(xs - xpred)
    Ps = Pf + J*(Ps - Ppred)*J'

    T(xs, Ps)
end

struct RauchTrungStriebel{T,S}
    iter::T # iterate backwards through
    M::S
end

function dyniterate(RTS::RauchTrungStriebel, ::Nothing)
    ϕ = dyniterate(RTS.iter, nothing)
    ϕ === nothing && return nothing
    (n, U), state = ϕ
    Gs, Gpred = U[1], U[2]
    n => Gs, (Gs, Gpred, state)
end

function dyniterate(RTS::RauchTrungStriebel, (Gs, Gpred, state))
    ϕ = dyniterate(RTS.iter, state)
    ϕ === nothing && return nothing
    (n, U), state = ϕ
    Gf, Gpredᵒ = U[1], U[2]
    Gs = smoother_kernel(Gs, Gf, Gpred, Phi(RTS.M.sys, n=>U))
    n => Gs, (Gs, Gpredᵒ, state)
end

function rts_smoother(M, prior, Y)
    P = filter(Y, M)

    ϕ = dyniterate(P, Start(prior))
    ϕ === nothing && error("no observations")
    (t, u), state = ϕ

    Xf = trajectory((t => (u[1], u[2]),))
    while true
        ϕ = dyniterate(P, state)
        ϕ === nothing && break
        (t, u), state = ϕ
        push!(Xf, t => (u[1], u[2])) # safe only what is needed
    end
    ll = u[3]

    n = length(Xf)
    U = Xf.x[end]
    Gf, Gpred = U[1], U[2]
    Gs = Vector{typeof(Gf)}(undef, n)
    Gs[end] = Gf
    for i in n-1:-1:1
        U = Xf.x[i]
        Gf, Gpredᵒ = U[1], U[2]
        Gs[i] = smoother_kernel(Gs[i+1], Gf, Gpred, Phi(M.sys, i => U))
        Gpred = Gpredᵒ
    end
    Trajectory(Xf.t, Gs), ll
end


text:
using Raylib
using Raylib: RayCamera3D, rayvector,
    RAYWHITE, MAROON, RED, DARKGRAY

function main()
    screenWidth = 800
    screenHeight = 450

    Raylib.InitWindow(screenWidth, screenHeight,
                      "raylib [core] example - 3d camera mode")

    camera = RayCamera3D(
        rayvector(0, 10, 10),
        rayvector(0, 0, 0),
        rayvector(0, 1, 0),
        45,
        0,
    )

    cubePosition = rayvector(0, 0, 0)

    Raylib.SetTargetFPS(60)

    while !Raylib.WindowShouldClose()
        Raylib.BeginDrawing()
        Raylib.ClearBackground(RAYWHITE)

        Raylib.BeginMode3D(camera)
        Raylib.DrawCube(cubePosition, 2f0, 2f0, 2f0, RED)

        Raylib.DrawCubeWires(cubePosition, 2f0, 2f0, 2f0, MAROON)

        Raylib.DrawGrid(10, 1f0)

        Raylib.EndMode3D()
        Raylib.DrawText("Welcome to the third dimension!",
                        10, 40, 20, DARKGRAY)
        Raylib.EndDrawing()
    end

    Raylib.CloseWindow()

end