text:
table1 = readtable("csv/fig_4.1.csv")
p_fig4_l = plot(table1,
    layer(x=:λ_E, y=:tr_chains_d1, Geom.line, Theme(default_color=colorant"purple")),
    layer(x=:λ_E, y=:tr_chains_d10, Geom.line, Theme(default_color=colorant"green")),
    layer(x=:λ_E, y=:tr_chains_d50 , Geom.line, Theme(default_color=colorant"blue")),
    layer(x=:λ_E, y=:tr_chains_d100, Geom.line, Theme(default_color=colorant"red")),
    Guide.manual_color_key("", ["C(1)", "C(10)", "C(50)", "C(100)"],
        [colorant"purple",colorant"green", colorant"blue", colorant"red"]),
    Guide.YLabel("wH", orientation=:horizontal),
    Guide.XLabel("λE"),
    #Guide.Title("C(d) vs B_H, vs B_E, p_E = $p_E, d = $d, p_H = $p_H, λ_H = $λ_H")
    my_theme
)
draw(PDF("figs/fig_4.1.pdf", 6inch, 4.5inch), p_fig4_l)

table2 = readtable("csv/fig_4.2.csv")
p_fig4_bot = plot(table2,
    layer(x=:λ_H, y=:tr_chains_d1, Geom.line, Theme(default_color=colorant"purple")),
    layer(x=:λ_H, y=:tr_chains_d10, Geom.line, Theme(default_color=colorant"green")),
    layer(x=:λ_H, y=:tr_chains_d50 , Geom.line, Theme(default_color=colorant"blue")),
    layer(x=:λ_H, y=:tr_chains_d100, Geom.line, Theme(default_color=colorant"red")),
    Guide.manual_color_key("", ["C(1)", "C(10)", "C(50)", "C(100)"],
        [colorant"purple",colorant"green", colorant"blue", colorant"red"]),
    Guide.YLabel("wH", orientation=:horizontal),
    Guide.XLabel("λH"),
    #Guide.Title("C(d) vs B_H, vs B_E, p_E = $p_E, d = $d, p_H = $p_H, λ_H = $λ_H")
    my_theme
)
draw(PDF("figs/fig_4.2.pdf", 6inch, 4.5inch), p_fig4_bot)

table3 = readtable("csv/fig_4.3.csv")
p_fig4_r = plot(table3,
    layer(x=:log_p_H, y=:norm_d1, Geom.line, Theme(default_color=colorant"purple")),
    layer(x=:log_p_H, y=:norm_d10, Geom.line, Theme(default_color=colorant"green")),
    layer(x=:log_p_H, y=:norm_d50 , Geom.line, Theme(default_color=colorant"blue")),
    layer(x=:log_p_H, y=:norm_d100, Geom.line, Theme(default_color=colorant"red")),
    Guide.manual_color_key("", ["C(1)", "C(10)", "C(50)", "C(100)"],
        [colorant"purple",colorant"green", colorant"blue", colorant"red"]),
    Guide.YLabel("wH * pH", orientation=:horizontal),
    Guide.XLabel("- log(pH)"),
    #Guide.Title("C(d) vs B_H, vs B_E, p_E = $p_E, d = $d, p_H = $p_H, λ_H = $λ_H")
    my_theme
)
draw(PDF("figs/fig_4.3.pdf", 6inch, 4.5inch), p_fig4_r)

table4 = readtable("csv/fig_4.4.csv")
p_fig4_bot = plot(table4,
    layer(x=:p_E, y=:tr_chains_d1, Geom.line, Theme(default_color=colorant"purple")),
    layer(x=:p_E, y=:tr_chains_d10, Geom.line, Theme(default_color=colorant"green")),
    layer(x=:p_E, y=:tr_chains_d50 , Geom.line, Theme(default_color=colorant"blue")),
    layer(x=:p_E, y=:tr_chains_d100, Geom.line, Theme(default_color=colorant"red")),
    Guide.manual_color_key("", ["C(1)", "C(10)", "C(50)", "C(100)"],
        [colorant"purple",colorant"green", colorant"blue", colorant"red"]),
    Guide.YLabel("wH", orientation=:horizontal),
    Guide.XLabel("pE"),
    my_theme
)
draw(PDF("figs/fig_4.4.pdf", 6inch, 4.5inch), p_fig4_bot)


text:
"
    expands the given mps using the algorithm given in the vumps paper
"
@with_kw struct OptimalExpand<:Algorithm
    trscheme::TruncationScheme = truncdim(1)
end

function changebonds(state::InfiniteMPS, H::Hamiltonian,alg::OptimalExpand,pars=params(state,H))
    #determine optimal expansion spaces around bond i
    exps = map(1:length(state)) do i
        ACAR = MPSKit._permute_front(state.AC[i])*MPSKit._permute_tail(state.AR[i+1])
        AC2 = ac2_prime(ACAR,i,state,pars)

        #Calculate nullspaces for AL and AR
        NL = leftnull(state.AL[i])
        NR = rightnull(MPSKit._permute_tail(state.AR[i+1]))

        #Use this nullspaces and SVD decomposition to determine the optimal expansion space
        intermediate = adjoint(NL)*AC2*adjoint(NR)
        (U,S,V) = tsvd(intermediate,trunc=alg.trscheme,alg=TensorKit.SVD())

        (NL*U,V*NR)
    end

    pexp = MPSKit.PeriodicArray(collect(exps));

    #do the actual expansion
    for i in 1:length(state)
        al = MPSKit._permute_tail(TensorKit.catdomain(state.AL[i],pexp[i][1]))
        lz = TensorMap(zeros,_lastspace(pexp[i-1][1])',domain(al))
        state.AL[i] = MPSKit._permute_front(TensorKit.catcodomain(al,lz))

        ar = MPSKit._permute_front(TensorKit.catcodomain(MPSKit._permute_tail(state.AR[i+1]),pexp[i][2]))
        rz = TensorMap(zeros,codomain(ar),space(pexp[i+1][2],1))
        state.AR[i+1] = TensorKit.catdomain(ar,rz)

        l = TensorMap(zeros,codomain(state.CR[i]),space(pexp[i][2],1))
        state.CR[i] = TensorKit.catdomain(state.CR[i],l)
        r = TensorMap(zeros,_lastspace(pexp[i][1])',domain(state.CR[i]))
        state.CR[i] = TensorKit.catcodomain(state.CR[i],r)

        state.AC[i] = state.AL[i]*state.CR[i]
    end

    poison!(pars)

    return state,pars
end

function MPSKit.changebonds(state::MPSMultiline, H,alg::OptimalExpand,pars=params(state,H))
    #=
        todo : merge this with the MPSCentergauged implementation
    =#
    #determine optimal expansion spaces around bond i
    exps = map(Iterators.product(1:size(state,1),1:size(state,2))) do (i,j)
        ACAR = _permute_front(state.AC[i,j])*_permute_tail(state.AR[i,j+1])
        AC2 = ac2_prime(ACAR,i,j,state,pars)

        #Calculate nullspaces for AL and AR
        NL = leftnull(state.AL[i,j])
        NR = rightnull(_permute_tail(state.AR[i,j+1]))

        #Use this nullspaces and SVD decomposition to determine the optimal expansion space
        intermediate = adjoint(NL)*AC2*adjoint(NR)
        (U,S,V) = tsvd(intermediate,trunc=alg.trscheme,alg=TensorKit.SVD())

        (NL*U,V*NR)
    end

    pexp = PeriodicArray(collect(exps));

    #do the actual expansion
    for (i,j) in Iterators.product(1:size(state,1),1:size(state,2))
        al = _permute_tail(TensorKit.catdomain(state.AL[i,j],pexp[i,j][1]))
        lz = TensorMap(zeros,_lastspace(pexp[i,j-1][1])',domain(al))

        state.AL[i,j] = _permute_front(TensorKit.catcodomain(al,lz))

        ar = _permute_front(TensorKit.catcodomain(_permute_tail(state.AR[i,j+1]),pexp[i,j][2]))
        rz = TensorMap(zeros,codomain(ar),space(pexp[i,j+1][2],1))
        state.AR[i,j+1] = TensorKit.catdomain(ar,rz)

        l = TensorMap(zeros,codomain(state.CR[i,j]),space(pexp[i,j][2],1))
        state.CR[i,j] = TensorKit.catdomain(state.CR[i,j],l)
        r = TensorMap(zeros,_lastspace(pexp[i,j][1])',domain(state.CR[i,j]))
        state.CR[i,j] = TensorKit.catcodomain(state.CR[i,j],r)

        state.AC[i,j] = state.AL[i,j]*state.CR[i,j]
    end

    poison!(pars)

    return state,pars
end
function changebonds(state::Union{FiniteMPS,MPSComoving}, H::Hamiltonian,alg::OptimalExpand,pars=params(state,H))
    #inspired by the infinite mps algorithm, alternative is to use https://arxiv.org/pdf/1501.05504.pdf
    #didn't use the paper because generically it'd be annoying to implement (again having to fuse and stuff)

    #the idea is that we always want to expand the state in such a way that there are zeros at site i
    #but "optimal vectors" at site i+1
    #so during optimization of site i, you have access to these optimal vectors :)

    for i in 1:(length(state)-1)
        ACAR = _permute_front(state.AC[i])*_permute_tail(state.AR[i+1])
        AC2 = ac2_prime(ACAR,i,state,pars)

        #Calculate nullspaces for left and right
        NL = leftnull(state.AC[i])
        NR = rightnull(_permute_tail(state.AR[i+1]))

        #Use this nullspaces and SVD decomposition to determine the optimal expansion space
        intermediate = adjoint(NL) * AC2 * adjoint(NR);
        (U,S,V) = tsvd(intermediate,trunc=alg.trscheme,alg=TensorKit.SVD())

        ar_re = V*NR;
        ar_le = TensorMap(zeros,codomain(state.AC[i]),space(V,1))

        (nal,nc) = leftorth(TensorKit.catdomain(state.AC[i],ar_le),alg=QRpos())
        nar = _permute_front(TensorKit.catcodomain(_permute_tail(state.AR[i+1]),ar_re));

        state.AC[i] = (nal,nc)
        state.AC[i+1] = (nc,nar)
    end

    return state,pars
end

ext: jl


text:
export
    NodeManager,
    pre_visit, post_visit, visit, update,
    pop_node, push_node,
    register_target, unregister_target,
    register_event_target, unregister_event_target

using ..Rendering
using ...Ranger
using .Nodes
using .Scenes
using ..Events

import .Nodes.visit

mutable struct NodeManager
    clear_background::Bool

    context::RenderContext

    # A stack of nodes
    stack::NodeStack

    timing_targets::Array{Ranger.AbstractNode,1}
    event_targets::Array{Ranger.AbstractNode,1}

    function NodeManager(world::Ranger.World, raster_font::Rendering.RasterFont, vector_font::Rendering.VectorFont)
        o = new()

        o.clear_background = true

        o.context = Rendering.RenderContext(world, raster_font, vector_font)
        Rendering.initialize(o.context, world)

        o.stack = Nodes.NodeStack()

        o.timing_targets = Array{Ranger.AbstractNode,1}[]
        o.event_targets = Array{Ranger.AbstractNode,1}[]

        o
    end
end

function pre_visit(man::NodeManager)
    # Typically Scenes/Layers will clear the background themselves so the default
    # is to NOT perform a clear here.
    if man.clear_background
        # If vsync is enabled then this takes nearly 1/fps milliseconds.
        # For example, 60fps -> 1/60 = ~16.666ms
        Rendering.pre(man.context)
    end
end

function visit(man::NodeManager, interpolation::Float64)
    if Nodes.is_empty(man.stack)
        println("NodeManager: no more nodes to visit.")
        return false
    end

    if Nodes.has_next_node(man.stack)
        Nodes.set_next_node(man)
    end

    # This will save view-space matrix
    Rendering.save!(man.context)

    # If mouse coords changed then update view coords.
    # self.global_data.update_view_coords(&mut self.context);
    action = Nodes.transition(man.stack.running_node)

    if action == Scenes.REPLACE_TAKE
        repl = Nodes.get_replacement(man.stack.running_node)
        Nodes.replace(man.stack, repl)

        # Immediately switch to the new runner
        if Nodes.has_next_node(man.stack)
            Nodes.set_next_node(man)
        end
    end

    # Visit the running node
    Nodes.visit(man.stack.running_node, man.context, interpolation)

    Rendering.restore!(man.context)

    true # continue to draw.
end

function post_visit(man::NodeManager)
    Rendering.post(man.context);
end

function set_next_node(man::NodeManager)
    if Nodes.has_running_node(man.stack)
        exit_nodes(man.stack.running_node, man)
    end

    Nodes.set_running_node(man.stack)

    Nodes.set_next_node_nil(man.stack)

    println("Running node ", man.stack.running_node)

    enter_nodes(man.stack.running_node, man);
end

function enter_nodes(node::Ranger.AbstractNode, man::NodeManager)
    Nodes.enter_node(node, man)

    children = get_children(node)
    if children ≠ nothing
        for child in children
            enter_nodes(child, man)
        end
    end
end

function exit_nodes(node::Ranger.AbstractNode, man::NodeManager)
    exit_node(node, man)

    children = get_children(node)
    if children ≠ nothing
        for child in children
            exit_nodes(child, man)
        end
    end
end

function pop_node(man::NodeManager)
    Nodes.pop(man.stack);
end

function push_node(man::NodeManager, node::Ranger.AbstractNode)
    man.stack.next_node = node
    println("Pushing : ", node)
    Nodes.push(man.stack, node);
end

# --------------------------------------------------------------------------
# Timing
# --------------------------------------------------------------------------
function update(man::NodeManager, dt::Float64)
    # println("NodeManager::update")
    for target in man.timing_targets
        Nodes.update(target, dt)
    end
end

function register_target(man::NodeManager, target::Ranger.AbstractNode)
    println("Register ", target, " target")
    push!(man.timing_targets, target);
end

function unregister_target(man::NodeManager, target::Ranger.AbstractNode)
    idx = findfirst(isequal(target), man.timing_targets)
    if idx ≠ nothing
        println("UnRegistering idx:(", idx, ") ", man.timing_targets[idx], " target");
        node = deleteat!(man.timing_targets, idx)
    else
        println("Unable to UnRegister ", target, " target");
    end
end

# --------------------------------------------------------------------------
# IO events
# --------------------------------------------------------------------------
function register_event_target(man::NodeManager, target::Ranger.AbstractNode)
    println("Register ", target, " event target")
    push!(man.event_targets, target);
end

function unregister_event_target(man::NodeManager, target::Ranger.AbstractNode)
    idx = findfirst(isequal(target), man.event_targets)
    if idx ≠ nothing
        println("UnRegistering idx:(", idx, ") ", man.event_targets[idx], " event target");
        node = deleteat!(man.event_targets, idx)
    else
        println("Unable to UnRegister ", target, " event target");
    end
end

function route_events(man::NodeManager, keyboard::Events.KeyboardEvent)
    # println(keyboard)
    for target in man.event_targets
        Nodes.io_event(target, keyboard)
    end
end

function route_events(man::NodeManager, mouse::Events.MouseMotionEvent)
    for target in man.event_targets
        Nodes.io_event(target, mouse)
    end
end

function route_events(man::NodeManager, mouse::Events.MouseWheelEvent)
    for target in man.event_targets
        Nodes.io_event(target, mouse)
    end
end

function route_events(man::NodeManager, mouse::Events.MouseButtonEvent)
    for target in man.event_targets
        Nodes.io_event(target, mouse)
    end
end

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:
using HTTP
using JSON
function toJson(longitude::Float64, latitude::Float64, longitude2::Float64, latitude2::Float64)
  r = HTTP.request("GET", "https://api.mapbox.com/directions/v5/mapbox/driving/$longitude%2C$latitude%3B$longitude2%2C$latitude2?alternatives=false&geometries=geojson&steps=false&access_token=pk.eyJ1IjoiZGlhZ2EiLCJhIjoiY2them1jMGY1MGI1YjJ6bG8yZHlnd2trNSJ9.FSly4QvdXoFjVlqXldieew")
  j = JSON.parse(String(r.body))
  print(j["routes"][1]["legs"][1]["duration"])
  open("./data/foo.json","a") do f
    JSON.print(f, j)
  end
end

max_issues_repo_name: MLH-Fellowship/CodeVidLive


text:
struct Edit
    offset::Int
    span::Int
    content::String
end

struct SourceCode
    path::String
    content::String
    cst::CSTParser.EXPR
    edits::Vector{Edit}
end

SourceCode(path, content, cst) = SourceCode(path, content, cst, [])
function SourceCode(path, content)
    cst, ps = CSTParser.parse(CSTParser.ParseState(content), true)
    return SourceCode(path, content, cst)
end
SourceCode(path) = SourceCode(path, read(path, String))

newusing(pkgname, allvars) = format_text("using $pkgname: $(join(allvars, ", "))")
newimport(pkgname, allvars) = format_text("import $pkgname: $(join(allvars, ", "))")

isimportable(cst) = cst.typ !== CSTParser.PUNCTUATION

ascode(cst::CSTParser.EXPR) =
    if cst.typ === CSTParser.MacroName
        @assert cst.args[1].kind === Tokens.AT_SIGN
        x = cst.args[2]
        if x.typ == CSTParser.IDENTIFIER
            "@" * x.val
        elseif x.typ == CSTParser.OPERATOR
            string('@', Tokens.UNICODE_OPS_REVERSE[x.kind])
        end
    elseif cst.typ === CSTParser.IDENTIFIER
        cst.val
    elseif cst.typ === CSTParser.OPERATOR
        String(Tokens.UNICODE_OPS_REVERSE[x.kind])
    end

function comma_separated_names(args::Vector{CSTParser.EXPR})
    strs = map(ascode, filter(isimportable, args))
    all(x -> x isa AbstractString, strs) || return nothing
    return strs
end

function importednames(cst::CSTParser.EXPR)::Union{Nothing,Vector{String}}
    @assert cst.typ in (CSTParser.Using, CSTParser.Import)
    i = findfirst(x -> x.kind == Tokens.COLON, cst.args)
    if i === nothing
        if findfirst(x -> x.kind == Tokens.DOT, cst.args) === nothing
            return [cst.args[2].val]  # Handle: using A
        end
        x = cst.args[end]
        if x.typ == CSTParser.IDENTIFIER
            return [x.val]  # Handle: using A.B
        end
    else
        return comma_separated_names(cst.args[i+1:end])
    end
    return nothing
end

_add_using_vars!(f::SourceCode, tobeimported) = _add_using_vars!(f, f.cst, 1, tobeimported)
function _add_using_vars!(f::SourceCode, ex, offset, tobeimported)
    modified = false
    if ex.typ == CSTParser.Using
        for (pkg, vars) in collect(tobeimported)
            if ex.args[2].val == pkg.name
                allvars = sort!(append!(String.(vars), importednames(ex)))
                e = Edit(offset, ex.span, newusing(pkg.name, allvars))
                push!(f.edits, e)
                pop!(tobeimported, pkg)
                modified = true
                break
            end
        end
        # if !modified
        #     vars = importednames(ex)
        #     if vars !== nothing && !issorted(vars)
        #         e = Edit(offset, ex.span, newusing(ex.args[2].val, sort(vars)))
        #         push!(f.edits, e)
        #         modified = true
        #     end
        # end
    else
        for sub in something(ex.args, ())
            modified |= _add_using_vars!(f, sub, offset, tobeimported)
            offset += sub.fullspan
            # # Commented out to sort existing imports:
            # # isempty(tobeimported) && break
        end
    end
    return modified
end

function new_using_vars!(f::SourceCode, tobeimported)
    ex = f.cst::CSTParser.EXPR
    @assert ex.typ == CSTParser.FileH
    i0 = findfirst(ex.args) do x
        x.typ == CSTParser.Using
    end
    if i0 === nothing
        inew = 1
    else
        i1 = findfirst(ex.args[i0+1:end]) do x
            x.typ != CSTParser.Using
        end
        inew = i0 + something(i1, 1)
    end

    offset = 1 + sum(x.fullspan for x in ex.args[1:inew-1])
    lines = map((pkg, vars) -> newusing(pkg.name, sort(vars)), tobeimported)
    push!(f.edits, Edit(offset, 0, join(lines, "\n")))

    empty!(tobeimported)
    return
end

function add_using!(paths::AbstractVector{<:AbstractString}, tobeimported)
    files = SourceCode[]
    local f1
    for (i, p) in enumerate(paths)
        f = SourceCode(p)
        if _add_using_vars!(f, tobeimported)
            push!(files, f)
        end
        if i == 1
            f1 = f
        end
        isempty(tobeimported) && break
    end

    # Add new `using` statements
    if !isempty(tobeimported)
        new_using_vars!(f1, tobeimported)
        if files[1] !== f1
            pushfirst!(files, f1)
        end
    end

    return files
end

format_code(f::SourceCode) = sprint(format_code, f)
function format_code(io, f::SourceCode)
    @assert all(diff([e.offset for e in f.edits]) .> 1)
    cursor = 1
    for e in f.edits
        # @show e.offset e.content
        # print(f.content[cursor:e.offset-1])
        # println()
        # println("---")
        # print(f.content[e.offset:e.offset+e.span-1])
        # println()
        # println("===")
        # print(e.content)
        # println()
        # println("^^^")
        print(io, f.content[cursor:e.offset-1])
        print(io, e.content)
        cursor = e.offset + e.span
    end
    print(io, f.content[cursor:end])
end

function format_files_to_dict(files)
    formatted = Dict{String,String}()
    for f in files
        formatted[f.path] = format_code(f)
    end
    return formatted
end

function Base.print(io::IO, f::SourceCode)
    printstyled(io, "---a/", f.path, '\n'; color=:red)
    printstyled(io, "+++b/", f.path, '\n'; color=:green)
    offset = 1
    for e in f.edits
        eend = e.offset+e.span-1

        printstyled(io, "@@\n"; color=:cyan)
        aend = something(findnext('\n', f.content, eend), lastindex(f.content))
        atxt = f.content[e.offset:aend]
        for line in split(atxt, '\n')
            printstyled(io, '+', line, '\n'; color=:red)
        end

        btxt = string(e.content, f.content[eend+1:aend])
        for line in split(btxt, '\n')
            printstyled(io, '-', line, '\n'; color=:green)
        end

        offset += e.span
    end
end


text:
export LSFManager, addprocs_lsf

struct LSFManager <: ClusterManager
    np::Integer
    bsub_flags::Cmd
    bpeek_flags::Cmd
    ssh_cmd::Cmd
    bsub_cmd::Cmd
    bpeek_cmd::Cmd
    retry_delays
    throttle::Integer
end

struct LSFException <: Exception
    msg
end

function lsf_bpeek(manager::LSFManager, jobid, iarray)
    port_host_regex = r"julia_worker:([0-9]+)#([0-9.]+)"
    stream = Base.BufferStream()
    mark(stream)    # so that we can reset to beginning after ensuring process started

    streamer_cmd = pipeline(`$(manager.ssh_cmd) $(manager.bpeek_cmd) $(manager.bpeek_flags) $(jobid)\[$iarray\]`; stdout=stream, stderr=stream)
    backoff = manager.retry_delays
    delay, backoff_state = iterate(backoff)
    streamer_proc = run(streamer_cmd; wait=false)
    worker_started = false
    host = nothing
    port = nothing

    while !worker_started
        bytestr = readline(stream)
        conn_info_match = match(port_host_regex, bytestr)
        if !isnothing(conn_info_match)
            host = conn_info_match.captures[2]
            port = parse(Int, conn_info_match.captures[1])
            @debug("lsf worker listening", connect_info=bytestr, host, port)
            # process started, reset to marked position and hand over to Distributed module
            reset(stream)
            worker_started = true
        else
            if occursin("Not yet started", bytestr)
                # reset to marked position, bpeek process would have stopped
                wait(streamer_proc)
                mark(stream)

                # retry with backoff if within retry limit
                if backoff_state[1] == 0
                    close(stream)
                    throw(LSFException(bytestr))
                end
                sleep(delay)
                delay, backoff_state = iterate(backoff, backoff_state)
                streamer_proc = run(streamer_cmd; wait=false)
            elseif occursin("<< output from stdout >>", bytestr) || occursin("<< output from stderr >>", bytestr)
                # ignore this bpeek output decoration and continue to read the next line
                mark(stream)
            else
                # unknown response from worker process
                close(stream)
                throw(LSFException(bytestr))
            end
        end
    end

    return stream, host, port
end

function lsf_launch_and_monitor(manager::LSFManager, launched, c, jobid, iarray)
    config = WorkerConfig()
    io, host, port = lsf_bpeek(manager, jobid, iarray)
    config.io = io
    config.host = host
    config.port = port
    config.userdata = `$jobid\[$iarray\]`

    push!(launched, config)
    notify(c)
end

function launch(manager::LSFManager, params::Dict, launched::Array, c::Condition)
    try
        dir = params[:dir]
        exename = params[:exename]
        exeflags = params[:exeflags]

        np = manager.np

        jobname = `julia-$(getpid())`

        cmd = `$exename $exeflags $(worker_arg())`
        bsub_cmd = `$(manager.ssh_cmd) $(manager.bsub_cmd) $(manager.bsub_flags) -cwd $dir -J $(jobname)\[1-$np\] "$cmd"`

        line = open(readline, bsub_cmd)
        m = match(r"Job <([0-9]+)> is submitted", line)
        jobid = m.captures[1]

        asyncmap((i)->lsf_launch_and_monitor(manager, launched, c, jobid, i),
                 1:np;
                 ntasks=manager.throttle)

    catch e
        println("Error launching workers")
        println(e)
    end
end

manage(manager::LSFManager, id::Int64, config::WorkerConfig, op::Symbol) = nothing

kill(manager::LSFManager, id::Int64, config::WorkerConfig) = remote_do(exit, id)

"""
    addprocs_lsf(np::Integer;
                 bsub_flags::Cmd=``,
                 bpeek_flags::Cmd=`-f`,
                 ssh_cmd::Cmd=``,
                 bsub_cmd::Cmd=`bsub`,
                 bpeek_cmd::Cmd=`bpeek`,
                 retry_delays=ExponentialBackOff(n=10,
                                                 first_delay=1, max_delay=512,
                                                 factor=2),
                 throttle::Integer=np,
                 params...) =

Launch `np` workers on a cluster managed by IBM's Platform Load Sharing
Facility.  `bsub_flags` can be used to pass flags to `bsub` that are specific
to your cluster or workflow needs.  `ssh_cmd` can be used to launch workers
from other than the cluster head node (e.g. your personal workstation).
`retry_delays` is a vector of numbers specifying in seconds how long to
repeatedly wait for a worker to start.  `throttle` specifies how many workers
to launch at once. Having `-f` in bpeek flags (which is the default) will let
stdout of workers to be displayed on master too.

# Examples

```
addprocs_lsf(1000; ssh_cmd=`ssh login`, throttle=10)
```
"""
addprocs_lsf(np::Integer;
             bsub_flags::Cmd=``,
             bpeek_flags::Cmd=`-f`,
             ssh_cmd::Cmd=``,
             bsub_cmd::Cmd=`bsub`,
             bpeek_cmd::Cmd=`bpeek`,
             retry_delays=ExponentialBackOff(n=10,
                                             first_delay=1, max_delay=512,
                                             factor=2),
             throttle::Integer=np,
             params...) =
        addprocs(LSFManager(np, bsub_flags, bpeek_flags, ssh_cmd, bsub_cmd, bpeek_cmd, retry_delays, throttle); params...)


text:
"""
get_best_ROI_index(r::Matrix{Float64})
Description of ```get_best_ROI_index```
------------------------------
Returns tuple of indeces of the best ROI as: ``` (id_row, id_col) ```

Arguments:
+ r   : array of rations S/B

see also get_best_ROI(r, Emins, Emaxs)
"""
function get_best_ROI_index(r::Matrix{<:Real})
    return (argmax(r)[2], argmax(r)[1])
end

"""
get_best_ROI_index(r::Vector{Real})
Description of ```get_best_ROI_index```
------------------------------
Returns tuple of indeces of the best ROI as: ``` (id_row, id_col) ```

Arguments:
+ r   : array of rations S/B

see also get_best_ROI(r, Emins, Emaxs)
"""
function get_best_ROI_index(r::Vector{<:Real})
    return (argmax(r))
end

"""
get_best_ROI(r::Vector{Real}, Emins::Vector{Real}, Emaxs::Vector{Real})
Description of ```get_best_ROI_index```
------------------------------
Returns tuple of energies of the best ROI as: ``` (E_min, E_max) ```

Arguments:
+ r    : array of rations S/B
+ Emins: array of Energies E_min
+ Emaxs: array of Energies E_max

"""
function get_best_ROI(r::Vector{<:Real}, Emins::Vector{<:Real}, Emaxs::Vector{<:Real})
    return (Emins[argmax(r)], Emaxs[argmax(r)])
end

"""
get_best_ROI(r::Matrix{Float64}, Emins::Vector{Real}, Emaxs::Vector{Real})
Description of ```get_best_ROI_index```
------------------------------
Returns tuple of energies of the best ROI as: ``` (E_min, E_max) ```

Arguments:
+ r    : Tuple of rations S/B
+ Emins: array of Energies E_min
+ Emaxs: array of Energies E_max

"""
function get_best_ROI(r::Matrix{<:Float64}, Emins::Vector{<:Real}, Emaxs::Vector{<:Real})
    if (Emins[argmax(r)[1]] < Emaxs[argmax(r)[2]])
        return (Emins[argmax(r)[1]], Emaxs[argmax(r)[2]])
    else
        return (Emins[argmax(r)[2]], Emaxs[argmax(r)[1]])
    end
end


text:
list_test_str1 = split(
"on x=10..12,y=10..12,z=10..12
on x=11..13,y=11..13,z=11..13
off x=9..11,y=9..11,z=9..11
on x=10..10,y=10..10,z=10..10",
'\n'
)

list_test_str2 = split(
"on x=-20..26,y=-36..17,z=-47..7
on x=-20..33,y=-21..23,z=-26..28
on x=-22..28,y=-29..23,z=-38..16
on x=-46..7,y=-6..46,z=-50..-1
on x=-49..1,y=-3..46,z=-24..28
on x=2..47,y=-22..22,z=-23..27
on x=-27..23,y=-28..26,z=-21..29
on x=-39..5,y=-6..47,z=-3..44
on x=-30..21,y=-8..43,z=-13..34
on x=-22..26,y=-27..20,z=-29..19
off x=-48..-32,y=26..41,z=-47..-37
on x=-12..35,y=6..50,z=-50..-2
off x=-48..-32,y=-32..-16,z=-15..-5
on x=-18..26,y=-33..15,z=-7..46
off x=-40..-22,y=-38..-28,z=23..41
on x=-16..35,y=-41..10,z=-47..6
off x=-32..-23,y=11..30,z=-14..3
on x=-49..-5,y=-3..45,z=-29..18
off x=18..30,y=-20..-8,z=-3..13
on x=-41..9,y=-7..43,z=-33..15
on x=-54112..-39298,y=-85059..-49293,z=-27449..7877
on x=967..23432,y=45373..81175,z=27513..53682",
'\n'
)

list_test_str3 = split(
"on x=-5..47,y=-31..22,z=-19..33
on x=-44..5,y=-27..21,z=-14..35
on x=-49..-1,y=-11..42,z=-10..38
on x=-20..34,y=-40..6,z=-44..1
off x=26..39,y=40..50,z=-2..11
on x=-41..5,y=-41..6,z=-36..8
off x=-43..-33,y=-45..-28,z=7..25
on x=-33..15,y=-32..19,z=-34..11
off x=35..47,y=-46..-34,z=-11..5
on x=-14..36,y=-6..44,z=-16..29
on x=-57795..-6158,y=29564..72030,z=20435..90618
on x=36731..105352,y=-21140..28532,z=16094..90401
on x=30999..107136,y=-53464..15513,z=8553..71215
on x=13528..83982,y=-99403..-27377,z=-24141..23996
on x=-72682..-12347,y=18159..111354,z=7391..80950
on x=-1060..80757,y=-65301..-20884,z=-103788..-16709
on x=-83015..-9461,y=-72160..-8347,z=-81239..-26856
on x=-52752..22273,y=-49450..9096,z=54442..119054
on x=-29982..40483,y=-108474..-28371,z=-24328..38471
on x=-4958..62750,y=40422..118853,z=-7672..65583
on x=55694..108686,y=-43367..46958,z=-26781..48729
on x=-98497..-18186,y=-63569..3412,z=1232..88485
on x=-726..56291,y=-62629..13224,z=18033..85226
on x=-110886..-34664,y=-81338..-8658,z=8914..63723
on x=-55829..24974,y=-16897..54165,z=-121762..-28058
on x=-65152..-11147,y=22489..91432,z=-58782..1780
on x=-120100..-32970,y=-46592..27473,z=-11695..61039
on x=-18631..37533,y=-124565..-50804,z=-35667..28308
on x=-57817..18248,y=49321..117703,z=5745..55881
on x=14781..98692,y=-1341..70827,z=15753..70151
on x=-34419..55919,y=-19626..40991,z=39015..114138
on x=-60785..11593,y=-56135..2999,z=-95368..-26915
on x=-32178..58085,y=17647..101866,z=-91405..-8878
on x=-53655..12091,y=50097..105568,z=-75335..-4862
on x=-111166..-40997,y=-71714..2688,z=5609..50954
on x=-16602..70118,y=-98693..-44401,z=5197..76897
on x=16383..101554,y=4615..83635,z=-44907..18747
off x=-95822..-15171,y=-19987..48940,z=10804..104439
on x=-89813..-14614,y=16069..88491,z=-3297..45228
on x=41075..99376,y=-20427..49978,z=-52012..13762
on x=-21330..50085,y=-17944..62733,z=-112280..-30197
on x=-16478..35915,y=36008..118594,z=-7885..47086
off x=-98156..-27851,y=-49952..43171,z=-99005..-8456
off x=2032..69770,y=-71013..4824,z=7471..94418
on x=43670..120875,y=-42068..12382,z=-24787..38892
off x=37514..111226,y=-45862..25743,z=-16714..54663
off x=25699..97951,y=-30668..59918,z=-15349..69697
off x=-44271..17935,y=-9516..60759,z=49131..112598
on x=-61695..-5813,y=40978..94975,z=8655..80240
off x=-101086..-9439,y=-7088..67543,z=33935..83858
off x=18020..114017,y=-48931..32606,z=21474..89843
off x=-77139..10506,y=-89994..-18797,z=-80..59318
off x=8476..79288,y=-75520..11602,z=-96624..-24783
on x=-47488..-1262,y=24338..100707,z=16292..72967
off x=-84341..13987,y=2429..92914,z=-90671..-1318
off x=-37810..49457,y=-71013..-7894,z=-105357..-13188
off x=-27365..46395,y=31009..98017,z=15428..76570
off x=-70369..-16548,y=22648..78696,z=-1892..86821
on x=-53470..21291,y=-120233..-33476,z=-44150..38147
off x=-93533..-4276,y=-16170..68771,z=-104985..-24507",
'\n'
)

list_str = let input = @__DIR__() * "/day22_input.txt"
    isfile(input) ? readlines(input) : list_test_str1
end

# Test function

function testit(name, result, check::Function)
    if check(result)
        @info "$name successful, with result $result"
    else
        @warn "$name failed, with result $result"
    end
end

# Part 1

function part1(list)
    reactor = Set{NTuple{3, Int}}()
    for l in list
        on = l[2] == 'n'
        m = parse.(
            Int,
            match(
                r"^[^x]*x=(-?\d*)\.\.(-?\d*),y=(-?\d*)\.\.(-?\d*),z=(-?\d*)\.\.(-?\d*)$",
                l).captures
        )
        I = intersect(m[1]:m[2], -50:50)
        J = intersect(m[3]:m[4], -50:50)
        K = intersect(m[5]:m[6], -50:50)
        for i in I, j in J, k in K
            if on && (i, j, k) ∉ reactor
                push!(reactor, (i, j, k))
            elseif !on && (i, j, k) ∈ reactor
                pop!(reactor, (i, j, k))
            end
        end
    end
    return length(reactor)
end

testit("Test 1 Part 1 ", part1(list_test_str1), ==(39))
testit("Test 2 Part 1 ", part1(list_test_str2), ==(590784))
testit("Challenge Part 1", part1(list_str), ==(620241))

# Part 2

struct Rectangle
    I::UnitRange{Int64}
    J::UnitRange{Int64}
end

Base.size(r::Rectangle) = (length(r.I), length(r.J))
Base.length(r::Rectangle) = prod(size(r))
Base.issubset(r1::Rectangle, r2::Rectangle) = r1.I ⊆ r2.I && r1.J ⊆ r2.J
Base.isdisjoint(r1::Rectangle, r2::Rectangle) = isdisjoint(r1.I, r2.I) || isdisjoint(r1.J, r2.J)

Base.intersect(r1::Rectangle, r2::Rectangle) = Rectangle(
    intersect(r1.I, r2.I),
    intersect(r1.J, r2.J)
)

function Base.setdiff(r1::Rectangle, r2::Rectangle)
    d = Set{Rectangle}()
    if isdisjoint(r1, r2)
        push!(d, r1)
    elseif !issubset(r1, r2)
        first(r1.I) < first(r2.I) && push!(d, Rectangle(first(r1.I):first(r2.I)-1, r1.J))
        last(r1.I) > last(r2.I) && push!(d, Rectangle(last(r2.I)+1:last(r1.I), r1.J))
        first(r1.J) < first(r2.J) &&
            push!(d, Rectangle(intersect(r1.I, r2.I), first(r1.J):first(r2.J)-1))
        last(r1.J) > last(r2.J) &&
            push!(d, Rectangle(intersect(r1.I, r2.I), last(r2.J)+1:last(r1.J)))
    end
    return d
end

struct Cuboid
    I::UnitRange{Int64}
    J::UnitRange{Int64}
    K::UnitRange{Int64}
end

Base.size(c::Cuboid) = (length(c.I), length(c.J), length(c.K))
Base.length(c::Cuboid) = prod(size(c))
Base.issubset(c1::Cuboid, c2::Cuboid) = c1.I ⊆ c2.I && c1.J ⊆ c2.J && c1.K ⊆ c2.K
Base.isdisjoint(c1::Cuboid, c2::Cuboid) =
    isdisjoint(c1.I, c2.I) || isdisjoint(c1.J, c2.J) || isdisjoint(c1.K, c2.K)

Base.intersect(c1::Cuboid, c2::Cuboid) = Cuboid(
    intersect(c1.I, c2.I),
    intersect(c1.J, c2.J),
    intersect(c1.K, c2.K)
)

function Base.setdiff(c1::Cuboid, c2::Cuboid)
    d = Set{Cuboid}()
    if isdisjoint(c1, c2)
        push!(d, c1)
    elseif !issubset(c1, c2)
        first(c1.I) < first(c2.I) &&
            push!(d, Cuboid(first(c1.I):first(c2.I)-1, c1.J, c1.K))
        last(c1.I) > last(c2.I) &&
            push!(d, Cuboid(last(c2.I)+1:last(c1.I), c1.J, c1.K))
        first(c1.J) < first(c2.J) &&
            push!(d, Cuboid(intersect(c1.I, c2.I), first(c1.J):first(c2.J)-1, c1.K))
        last(c1.J) > last(c2.J) &&
            push!(d, Cuboid(intersect(c1.I, c2.I), last(c2.J)+1:last(c1.J), c1.K))
        first(c1.K) < first(c2.K) &&
            push!(d, Cuboid(intersect(c1.I, c2.I), intersect(c1.J, c2.J), first(c1.K):first(c2.K)-1))
        last(c1.K) > last(c2.K) &&
            push!(d, Cuboid(intersect(c1.I, c2.I), intersect(c1.J, c2.J), last(c2.K)+1:last(c1.K)))
    end
    return d
end

function part2(list)
    reactor = Set{Cuboid}()
    for l in list
        on = l[2] == 'n'
        m = parse.(
            Int,
            match(
                r"^[^x]*x=(-?\d*)\.\.(-?\d*),y=(-?\d*)\.\.(-?\d*),z=(-?\d*)\.\.(-?\d*)$",
                l).captures
        )
        cm = Cuboid(m[1]:m[2], m[3]:m[4], m[5]:m[6])
        if on
            cm_parts = Set([cm])
            for c in reactor
                done = false
                while !done
                    done = true
                    for cm_part in cm_parts
                        if !isdisjoint(cm_part, c)
                            pop!(cm_parts, cm_part)
                            union!(cm_parts, setdiff(cm_part, c))
                            done = false
                        end
                    end
                end
            end
            union!(reactor, cm_parts)
        else
            for c in reactor
                if !isdisjoint(c, cm)
                    pop!(reactor, c)
                    union!(reactor, setdiff(c, cm))
                end
            end
        end
    end
    return sum(length.(reactor))
end

testit("Test Part 2", part2(list_test_str3), ==(2758514936282235))
testit("Challenge Part 2", part2(list_str), ==(1284561759639324))


text:
"""
    ld(bm, p, q, e, em, dlim)

Apply light deflection by a solar-system body, as part of
transforming coordinate direction into natural direction.

### Given ###

- `bm`: Mass of the gravitating body (solar masses)
- `p`: Direction from observer to source (unit vector)
- `q`: Direction from body to source (unit vector)
- `e`: Direction from body to observer (unit vector)
- `em`: Distance from body to observer (au)
- `dlim`: Deflection limiter (Note 4)

### Returned ###

- `p1`: Observer to deflected source (unit vector)

### Notes ###

1. The algorithm is based on Expr. (70) in Klioner (2003) and
   Expr. (7.63) in the Explanatory Supplement (Urban & Seidelmann
   2013), with some rearrangement to minimize the effects of machine
   precision.

2. The mass parameter bm can, as required, be adjusted in order to
   allow for such effects as quadrupole field.

3. The barycentric position of the deflecting body should ideally
   correspond to the time of closest approach of the light ray to
   the body.

4. The deflection limiter parameter dlim is phi^2/2, where phi is
   the angular separation (in radians) between source and body at
   which limiting is applied.  As phi shrinks below the chosen
   threshold, the deflection is artificially reduced, reaching zero
   for phi = 0.

5. The returned vector p1 is not normalized, but the consequential
   departure from unit magnitude is always negligible.

6. The arguments p and p1 can be the same array.

7. To accumulate total light deflection taking into account the
   contributions from several bodies, call the present function for
   each body in succession, in decreasing order of distance from the
   observer.

8. For efficiency, validation is omitted.  The supplied vectors must
   be of unit magnitude, and the deflection limiter non-zero and
   positive.

### References ###

- Urban, S. & Seidelmann, P. K. (eds), Explanatory Supplement to
    the Astronomical Almanac, 3rd ed., University Science Books
    (2013).

- Klioner, Sergei A., "A practical relativistic model for micro-
    arcsecond astrometry in space", Astr. J. 125, 1580-1597 (2003).

### Called ###

- [`pdp`](@ref): scalar product of two p-vectors
- [`pxp`](@ref): vector product of two p-vectors

"""
function ld(bm, p, q, e, em, dlim)
    @checkdims 3 p q e
    p1 = zeros(Cdouble, 3)
    ccall((:eraLd, liberfa), Cvoid,
          (Cdouble, Ptr{Cdouble}, Ptr{Cdouble}, Ptr{Cdouble}, Cdouble, Cdouble, Ptr{Cdouble}),
          bm, p, q, e, em, dlim, p1)
    return p1
end

"""
    ldn(l::Vector{LDBODY}, ob, sc)

For a star, apply light deflection by multiple solar-system bodies,
as part of transforming coordinate direction into natural direction.

### Given ###

- `n`: Number of bodies (note 1)
- `b`: Data for each of the n bodies (Notes 1,2):
    - `bm`: Mass of the body (solar masses, Note 3)
    - `dl`: Deflection limiter (Note 4)
    - `pv`: Barycentric PV of the body (au, au/day)
- `ob`: Barycentric position of the observer (au)
- `sc`: Observer to star coord direction (unit vector)

### Returned ###

- `sn`: Observer to deflected star (unit vector)

1. The array b contains n entries, one for each body to be
   considered.  If n = 0, no gravitational light deflection will be
   applied, not even for the Sun.

2. The array b should include an entry for the Sun as well as for
   any planet or other body to be taken into account.  The entries
   should be in the order in which the light passes the body.

3. In the entry in the b array for body i, the mass parameter
   b[i].bm can, as required, be adjusted in order to allow for such
   effects as quadrupole field.

4. The deflection limiter parameter b[i].dl is phi^2/2, where phi is
   the angular separation (in radians) between star and body at
   which limiting is applied.  As phi shrinks below the chosen
   threshold, the deflection is artificially reduced, reaching zero
   for phi = 0.   Example values suitable for a terrestrial
   observer, together with masses, are as follows:

   | `body i` | `b[i].bm`  | `b[i].dl` |
   |:---------|:-----------|:----------|
   |  Sun     | 1.0        |  6e-6     |
   |  Jupiter | 0.00095435 |  3e-9     |
   |  Saturn  | 0.00028574 |  3e-10    |

5. For cases where the starlight passes the body before reaching the
   observer, the body is placed back along its barycentric track by
   the light time from that point to the observer.  For cases where
   the body is "behind" the observer no such shift is applied.  If
   a different treatment is preferred, the user has the option of
   instead using the [`ld`](@ref) function.  Similarly, [`ld`](@ref) can be used
   for cases where the source is nearby, not a star.

6. The returned vector sn is not normalized, but the consequential
   departure from unit magnitude is always negligible.

7. The arguments sc and sn can be the same array.

8. For efficiency, validation is omitted.  The supplied masses must
   be greater than zero, the position and velocity vectors must be
   right, and the deflection limiter greater than zero.

### Reference ###

- Urban, S. & Seidelmann, P. K. (eds), Explanatory Supplement to
    the Astronomical Almanac, 3rd ed., University Science Books
    (2013), Section 7.2.4.

### Called ###

- [`erfa_cp`](@ref): copy p-vector
- [`pdp`](@ref): scalar product of two p-vectors
- [`pmp`](@ref): p-vector minus p-vector
- [`ppsp`](@ref): p-vector plus scaled p-vector
- [`pn`](@ref): decompose p-vector into modulus and direction
- [`ld`](@ref): light deflection by a solar-system body

"""
function ldn(l::Vector{LDBODY}, ob, sc)
    @checkdims 3 ob sc
    sn = zeros(Cdouble, 3)
    n = length(l)
    ccall((:eraLdn, liberfa), Cvoid,
          (Cint, Ptr{LDBODY}, Ptr{Cdouble}, Ptr{Cdouble}, Ptr{Cdouble}),
          n, l, ob, sc, sn)
    return sn
end

"""
    ldsun(p, e, em)

Deflection of starlight by the Sun.

### Given ###

- `p`: Direction from observer to star (unit vector)
- `e`: Direction from Sun to observer (unit vector)
- `em`: Distance from Sun to observer (au)

### Returned ###

- `p1`: Observer to deflected star (unit vector)

### Notes ###

1. The source is presumed to be sufficiently distant that its
   directions seen from the Sun and the observer are essentially
   the same.

2. The deflection is restrained when the angle between the star and
   the center of the Sun is less than a threshold value, falling to
   zero deflection for zero separation.  The chosen threshold value
   is within the solar limb for all solar-system applications, and
   is about 5 arcminutes for the case of a terrestrial observer.

3. The arguments p and p1 can be the same array.

### Called ###

- [`ld`](@ref): light deflection by a solar-system body

"""
function ldsun(p, e, em)
    @checkdims 3 p e
    p1 = zeros(Cdouble, 3)
    ccall((:eraLdsun, liberfa), Cvoid,
          (Ptr{Cdouble}, Ptr{Cdouble}, Cdouble, Ptr{Cdouble}),
          p, e, em, p1)
    return p1
end

"""
    ltecm(dr, dd)

ICRS equatorial to ecliptic rotation matrix, long-term.

### Given ###

- `epj`: Julian epoch (TT)

### Returned ###

- `rm`: ICRS to ecliptic rotation matrix

### Notes ###

1. The matrix is in the sense

   ```
   E_ep = rm x P_ICRS,
   ```

   where `P_ICRS` is a vector with respect to ICRS right ascension
   and declination axes and `E_ep` is the same vector with respect to
   the (inertial) ecliptic and equinox of epoch epj.

2. `P_ICRS` is a free vector, merely a direction, typically of unit
   magnitude, and not bound to any particular spatial origin, such
   as the Earth, Sun or SSB.  No assumptions are made about whether
   it represents starlight and embodies astrometric effects such as
   parallax or aberration.  The transformation is approximately that
   between mean J2000.0 right ascension and declination and ecliptic
   longitude and latitude, with only frame bias (always less than
   25 mas) to disturb this classical picture.

3. The Vondrak et al. (2011, 2012) 400 millennia precession model
   agrees with the IAU 2006 precession at J2000.0 and stays within
   100 microarcseconds during the 20th and 21st centuries.  It is
   accurate to a few arcseconds throughout the historical period,
   worsening to a few tenths of a degree at the end of the
   +/- 200,000 year time span.

### Called ###

- [`ltpequ`](@ref): equator pole, long term
- [`ltpecl`](@ref): ecliptic pole, long term
- [`pxp`](@ref): vector product
- [`pn`](@ref): normalize vector

### References ###

- Vondrak, J., Capitaine, N. and Wallace, P., 2011, New precession
    expressions, valid for long time intervals, Astron.Astrophys. 534,
    A22

- Vondrak, J., Capitaine, N. and Wallace, P., 2012, New precession
    expressions, valid for long time intervals (Corrigendum),
    Astron.Astrophys. 541, C1

"""
ltecm

"""
    ltp(dr, dd)

Long-term precession matrix.

### Given ###

- `epj`: Julian epoch (TT)

### Returned ###

- `rp`: Precession matrix, J2000.0 to date

### Notes ###

1. The matrix is in the sense

   ```
   P_date = rp x P_J2000,
   ```

   where `P_J2000` is a vector with respect to the J2000.0 mean
   equator and equinox and `P_date` is the same vector with respect to
   the equator and equinox of epoch epj.

2. The Vondrak et al. (2011, 2012) 400 millennia precession model
   agrees with the IAU 2006 precession at J2000.0 and stays within
   100 microarcseconds during the 20th and 21st centuries.  It is
   accurate to a few arcseconds throughout the historical period,
   worsening to a few tenths of a degree at the end of the
   +/- 200,000 year time span.

### Called ###

- [`ltpequ`](@ref): equator pole, long term
- [`ltpecl`](@ref): ecliptic pole, long term
- [`pxp`](@ref): vector product
- [`pn`](@ref): normalize vector

### References ###

- Vondrak, J., Capitaine, N. and Wallace, P., 2011, New precession
    expressions, valid for long time intervals, Astron.Astrophys. 534,
    A22

- Vondrak, J., Capitaine, N. and Wallace, P., 2012, New precession
    expressions, valid for long time intervals (Corrigendum),
    Astron.Astrophys. 541, C1

"""
ltp

"""
    ltpb(dr, dd)

Long-term precession matrix, including ICRS frame bias.

### Given ###

- `epj`: Julian epoch (TT)

### Returned ###

- `rpb`: Precession-bias matrix, J2000.0 to date

### Notes ###

1. The matrix is in the sense

   ```
   P_date = rpb x P_ICRS,
   ```

   where `P_ICRS` is a vector in the Geocentric Celestial Reference
   System, and `P_date` is the vector with respect to the Celestial
   Intermediate Reference System at that date but with nutation
   neglected.

2. A first order frame bias formulation is used, of sub-
   microarcsecond accuracy compared with a full 3D rotation.

3. The Vondrak et al. (2011, 2012) 400 millennia precession model
   agrees with the IAU 2006 precession at J2000.0 and stays within
   100 microarcseconds during the 20th and 21st centuries.  It is
   accurate to a few arcseconds throughout the historical period,
   worsening to a few tenths of a degree at the end of the
   +/- 200,000 year time span.

### References ###

- Vondrak, J., Capitaine, N. and Wallace, P., 2011, New precession
    expressions, valid for long time intervals, Astron.Astrophys. 534,
    A22

- Vondrak, J., Capitaine, N. and Wallace, P., 2012, New precession
    expressions, valid for long time intervals (Corrigendum),
    Astron.Astrophys. 541, C1

"""
ltpb

for name in ("ltecm",
             "ltp",
             "ltpb")
    f = Symbol(name)
    fc = "era" * uppercasefirst(name)
    @eval begin
        function ($f)(epj)
            rp = zeros(Cdouble, 3, 3)
            ccall(($fc, liberfa), Cvoid,
                  (Cdouble, Ptr{Cdouble}),
                  epj, rp)
            return permutedims(rp)
        end
    end
end

"""
    ltpecl(epj)

Long-term precession of the ecliptic.

### Given ###

- `epj`: Julian epoch (TT)

### Returned ###

- `vec`: Ecliptic pole unit vector

### Notes ###

1. The returned vector is with respect to the J2000.0 mean equator
   and equinox.

2. The Vondrak et al. (2011, 2012) 400 millennia precession model
   agrees with the IAU 2006 precession at J2000.0 and stays within
   100 microarcseconds during the 20th and 21st centuries.  It is
   accurate to a few arcseconds throughout the historical period,
   worsening to a few tenths of a degree at the end of the
   +/- 200,000 year time span.

### References ###

- Vondrak, J., Capitaine, N. and Wallace, P., 2011, New precession
    expressions, valid for long time intervals, Astron.Astrophys. 534,
    A22

- Vondrak, J., Capitaine, N. and Wallace, P., 2012, New precession
    expressions, valid for long time intervals (Corrigendum),
    Astron.Astrophys. 541, C1

"""
ltpecl

"""
    ltpequ(epj)

Long-term precession of the equator.

### Given ###

- `epj`: Julian epoch (TT)

### Returned ###

- `veq`: Equator pole unit vector

### Notes ###

1. The returned vector is with respect to the J2000.0 mean equator
   and equinox.

2. The Vondrak et al. (2011, 2012) 400 millennia precession model
   agrees with the IAU 2006 precession at J2000.0 and stays within
   100 microarcseconds during the 20th and 21st centuries.  It is
   accurate to a few arcseconds throughout the historical period,
   worsening to a few tenths of a degree at the end of the
   +/- 200,000 year time span.

### References ###

- Vondrak, J., Capitaine, N. and Wallace, P., 2011, New precession
    expressions, valid for long time intervals, Astron.Astrophys. 534,
    A22

- Vondrak, J., Capitaine, N. and Wallace, P., 2012, New precession
    expressions, valid for long time intervals (Corrigendum),
    Astron.Astrophys. 541, C1

"""
ltpequ

for name in ("ltpecl",
             "ltpequ")
    f = Symbol(name)
    fc = "era" * uppercasefirst(name)
    @eval begin
        function ($f)(epj)
            vec = zeros(Cdouble, 3)
            ccall(($fc, liberfa), Cvoid,
                  (Cdouble, Ptr{Cdouble}),
                  epj, vec)
            return vec
        end
    end
end

"""
    lteceq(epj, dr, dd)

Transformation from ecliptic coordinates (mean equinox and ecliptic
of date) to ICRS RA,Dec, using a long-term precession model.

### Given ###

- `epj`: Julian epoch (TT)
- `dl`, `db`: Ecliptic longitude and latitude (radians)

### Returned ###

- `dr`, `dd`: ICRS right ascension and declination (radians)

1. No assumptions are made about whether the coordinates represent
   starlight and embody astrometric effects such as parallax or
   aberration.

2. The transformation is approximately that from ecliptic longitude
   and latitude (mean equinox and ecliptic of date) to mean J2000.0
   right ascension and declination, with only frame bias (always
   less than 25 mas) to disturb this classical picture.

3. The Vondrak et al. (2011, 2012) 400 millennia precession model
   agrees with the IAU 2006 precession at J2000.0 and stays within
   100 microarcseconds during the 20th and 21st centuries.  It is
   accurate to a few arcseconds throughout the historical period,
   worsening to a few tenths of a degree at the end of the
   +/- 200,000 year time span.

### Called ###

- [`s2c`](@ref): spherical coordinates to unit vector
- [`ltecm`](@ref): J2000.0 to ecliptic rotation matrix, long term
- [`trxp`](@ref): product of transpose of r-matrix and p-vector
- [`c2s`](@ref): unit vector to spherical coordinates
- [`anp`](@ref): normalize angle into range 0 to 2pi
- [`anpm`](@ref): normalize angle into range +/- pi

### References ###

- Vondrak, J., Capitaine, N. and Wallace, P., 2011, New precession
    expressions, valid for long time intervals, Astron.Astrophys. 534,
    A22

- Vondrak, J., Capitaine, N. and Wallace, P., 2012, New precession
    expressions, valid for long time intervals (Corrigendum),
    Astron.Astrophys. 541, C1

"""
lteceq

"""
    lteqec(epj, dr, dd)

Transformation from ICRS equatorial coordinates to ecliptic
coordinates (mean equinox and ecliptic of date) using a long-term
precession model.

### Given ###

- `epj`: Julian epoch (TT)
- `dr`, `dd`: ICRS right ascension and declination (radians)

### Returned ###

- `dl`, `db`: Ecliptic longitude and latitude (radians)

1. No assumptions are made about whether the coordinates represent
   starlight and embody astrometric effects such as parallax or
   aberration.

2. The transformation is approximately that from mean J2000.0 right
   ascension and declination to ecliptic longitude and latitude
   (mean equinox and ecliptic of date), with only frame bias (always
   less than 25 mas) to disturb this classical picture.

3. The Vondrak et al. (2011, 2012) 400 millennia precession model
   agrees with the IAU 2006 precession at J2000.0 and stays within
   100 microarcseconds during the 20th and 21st centuries.  It is
   accurate to a few arcseconds throughout the historical period,
   worsening to a few tenths of a degree at the end of the
   +/- 200,000 year time span.

### Called ###

- [`s2c`](@ref): spherical coordinates to unit vector
- [`ltecm`](@ref): J2000.0 to ecliptic rotation matrix, long term
- [`rxp`](@ref): product of r-matrix and p-vector
- [`c2s`](@ref): unit vector to spherical coordinates
- [`anp`](@ref): normalize angle into range 0 to 2pi
- [`anpm`](@ref): normalize angle into range +/- pi

### References ###

- Vondrak, J., Capitaine, N. and Wallace, P., 2011, New precession
    expressions, valid for long time intervals, Astron.Astrophys. 534,
    A22

- Vondrak, J., Capitaine, N. and Wallace, P., 2012, New precession
    expressions, valid for long time intervals (Corrigendum),
    Astron.Astrophys. 541, C1

"""
lteqec

for name in ("lteceq",
             "lteqec")
    f = Symbol(name)
    fc = "era" * uppercasefirst(name)
    @eval begin
        function ($f)(epj, d1, d2)
            r1 = Ref{Cdouble}()
            r2 = Ref{Cdouble}()
            ccall(($fc, liberfa), Cvoid,
                  (Cdouble, Cdouble, Cdouble, Ref{Cdouble}, Ref{Cdouble}),
                  epj, d1, d2, r1, r2)
            return r1[], r2[]
        end
    end
end


text:
module vobs

using Base
import Dates
import DataFrames
import SQLite

include("arg_handler.jl")
include("vobs_to_sqlite.jl")

import .arg_handler
import .vobs_to_sqlite

function __init__()

end

function main(args)
    cmd_message = arg_handler.main_args(args)
    cmd_message[1]==="vobs_to_sqlite" ? vobs_to_sqlite.make_sqlite(cmd_message) : nothing
end

end


text:
"""
    LayerScale(scale)

Implements LayerScale.
([reference](https://arxiv.org/abs/2103.17239))

# Arguments
- `scale`: Scaling factor, a learnable diagonal matrix which is multiplied to the input.
"""
struct LayerScale{T<:AbstractVector{<:Real}}
    scale::T
end

"""
    LayerScale(λ, planes::Int)

Implements LayerScale.
([reference](https://arxiv.org/abs/2103.17239))

# Arguments
- `planes`: Size of channel dimension in the input.
- `λ`: initialisation value for the learnable diagonal matrix.
"""
LayerScale(planes::Int, λ) = λ > 0 ? LayerScale(fill(Float32(λ), planes)) : identity

@functor LayerScale
(m::LayerScale)(x::AbstractArray) = m.scale .* x

"""
    DropPath(p)

Implements Stochastic Depth - equivalent to `Dropout(p; dims = 4)` when `p` ≥ 0.
([reference](https://arxiv.org/abs/1603.09382))

# Arguments
- `p`: rate of Stochastic Depth.
"""
DropPath(p) = p ≥ 0 ? Dropout(p; dims = 4) : identity


text:
using Plots, Distributions, LinearAlgebra

n = 10

function obj(x)
	x' * x
end

function gradient(x, w = zero(x))
	2.0 * x + w
end

# sgd
max_iter = 1000
w_dist = Distributions.MvNormal(zeros(n),
	Diagonal(10.0 * ones(n)))
w_i = rand(w_dist, max_iter)
plt = plot();

for α in [0.1, 0.01, 0.001, 0.0001]
	x = 1.0 * ones(n)
	J_hist = [obj(x)]

	for i = 1:max_iter
		x .-= α * gradient(x, vec(w_i[:, i]))
		push!(J_hist, obj(x))
	end

	plt = plot!(J_hist,
		xlabel = "iteration", ylabel = "obj.",
		label = "α = $α (sgd)")
end

display(plt)

# adam
x = ones(n)

function adam(obj, gradient, x;
		max_iter = 1000)

	# problem size
	n = length(x)

	# history
	J_hist = [obj(x)]

	# parameters
	α = 0.01
	β1 = 0.9
	β2 = 0.999
	ϵ = 10.0^(-8.0)
	m = zero(x)
	v = zero(x)
	m̂ = zero(x)
	v̂ = zero(x)

	# iterate
	for i = 1:max_iter
		g = gradient(x, vec(w_i[:, i]))
		m .= β1 .* m + (1.0 - β1) .* g
		v .= β2 .* v + (1.0 - β2) .* (g.^2.0)
		m̂ .= m ./ (1.0 - β1^i)
		v̂ .= v ./ (1.0 - β2^i)
		x .-= α * m̂ ./ (sqrt.(v̂) .+ ϵ)
		push!(J_hist, obj(x))
	end

	return x, J_hist
end

x_sol, J_hist = adam(obj, gradient, ones(n))

plt = plot!(J_hist,
	xlabel = "iteration", ylabel = "obj.", title = "sgd",
	label = "α = $α (adam)",
	color = :red, width = 2.0)

display(plt)


text:
# MIT License
#
# Copyright (c) 2020 Joshua E Gentry

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

mutable struct PdfFontInfo
    version::Int64
    lastcheck::String
    server::String
    font::GitHubFile
    PdfFontInfo() = new(INDEX_VERSION, "", "", GitHubFile("pdf.ttf", 0.0, "", "", ""))
    PdfFontInfo(version, lastcheck, server, font) = new(version, lastcheck, server, font)
end

# =========================================================================================
"""
    is_expired(
        info::PdfFontInfo,
        frequency::Integer
    )::Bool

Check if our pdf font info data is older than `frequency` days.  Returns `true` if the
data is older.

__Arguments:__

| T | Name      | Default | Description
|:--| :-------- | :------ | :----------
| R | info      |         | The object to check.
| R | frequency |         | How old our data should be before we synchronize again.
"""
function is_expired(
            info::PdfFontInfo,
            frequency::Integer
        )::Bool
    if isempty(info.lastcheck)
        return true
    elseif info.server == ""
        return true
    elseif now() > DateTime(info.lastcheck, DATE_FORMAT) + Day(frequency)
        return true
    end

    return false
end

# =================================================================================================
"""
    update_pdf_font_info(
        info::PdfFontInfo,
        source::GitHubProject,
    )::Nothing

Grab the latest URL and SHA1 code for the PDF font file from GitHub and update the index
file with them.

__Arguments:__

| T | Name | Default | Description
|:--| :--- | :------ | :----------
| R | gh   |         | The GitHub project to sync the font file with.
| R | info |         | The object to synchronize with GitHub.

__Exceptions:__

| Name         | Reason
| :----------- | :-----
| NetworkError | There was a network error retrieving the contents from GitHub.
"""
function update_pdf_font_info(
            info::PdfFontInfo,
            source::GitHubProject
        )::Nothing
    local data = gh_contents(source)

    if isa(data, AbstractArray) == false
        @warn "Unexpected response from the server: $(JSON.json(data))"
        throw(NetworkError(gh_url(source)))
    end

    local found = false

    for file in data
        local name = get_json_safe(String, file, "name")

        if name !== nothing && name == "pdf.ttf"
            local sha  = get_json_safe(String, file, "sha")
            local url  = get_json_safe(String, file, "download_url")

            if sha !== nothing && url !== nothing

                info.font.remotesha1 = sha
                info.font.url        = url
                info.server          = gh_url(source)
                info.lastcheck       = Dates.format(now(), DATE_FORMAT)

                found = true

            elseif sha === nothing

                @warn "Directory entry has no sha!"

            else

                @warn "Directory entry has no download_url!"

            end
        end
    end

    if found == false
        @error "Could not fid pdf.ttf in the specified repository."
    end

    nothing
end

# =========================================================================================
"""
    refresh_pdf_font_info(
        info::PdfFontInfo,
        source::GitHubProject,
        frequency::Integer
    )::Bool

Refresh our data with the server if it's out of date.  Returns true if we refreshed with
the server.

__Arguments:__

| T | Name      | Default | Description
|:--| :-------- | :------ | :----------
| R | info      |         | The `PdfFontINfo` object to refresh.
| R | source    |         | The GitHub project/directory to synchronize with.
| R | frequency |         | How old our data should be before we synchronize again.

__Details:__

This method returns true under the following conditions:

  * We've never synchronized with the server.
  * The URL for the server is different from the URL we synchronized with last.
  * The last synchronization was more than `frequency` days ago.
"""
function refresh_pdf_font_info(
            info::PdfFontInfo,
            source::GitHubProject,
            frequency::Integer
        )::Bool
    local result = false

    try
        # ---------------------------------------------------------------------------------
        # Update the training information if needed.
        if is_expired(info, frequency)
            update_pdf_font_info(info, source)
            result = true
        end

    catch ex
        # ---------------------------------------------------------------------------------
        # If the exception is NOT a NetworkError rethrow it.
        if isa(ex, NetworkError) == false
            rethrow(ex)
        end
    end

    return result
end

# =========================================================================================
"""
parse_pdf_font_info(
        json::AbstractDict
    )::PdfFontInfo

Populate the `PdfFontInfo` object from a JSON object loaded from disk.

__Arguments:__

| T | Name | Default | Description
|:--| :--- | :------ | :----------
| R | json |         | The JSON object to extract the values from.

__Details:__

If JSON contains invalid information then an empty `PdfFontInfo` structure is returned.
"""
function parse_pdf_font_info(
            json::AbstractDict
        )::PdfFontInfo

    # -------------------------------------------------------------------------------------
    # Grab the values we're interested in from the JSON.
    local version    = get_json_safe(Int64, json, "version")
    local lastcheck  = get_json_safe(AbstractString, json, "lastcheck")
    local server     = get_json_safe(AbstractString, json, "server")
    local file       = parse_github_file(get_json_safe(AbstractDict, json, "font"))

    version === nothing && return PdfFontInfo()
    lastcheck === nothing && return PdfFontInfo()
    server === nothing && return PdfFontInfo()
    file === nothing && return PdfFontInfo()

    # -------------------------------------------------------------------------------------
    # Check the version.
    if version != INDEX_VERSION
        return PdfFontInfo()
    end

    # -------------------------------------------------------------------------------------
    # Create the object.
    return PdfFontInfo(version, lastcheck, server, file)
end

# =========================================================================================
"""
    load_pdf_font_info(
        path::AbstractString
    )::PdfFontInfo

Load the contents of the PdfFontInfo object from the specified directory.  If the file
doesn't exist or is corrupt a new empty object is returned.

__Arguments:__

| T | Name | Default | Description
|:--| :--- | :------ | :----------
| R | path |         | The path to load the file from.
"""
function load_pdf_font_info(
            path::AbstractString
        )::PdfFontInfo
    local result   = nothing
    local filename = joinpath(path, PDF_FONT_INFO_FILE)

    if create_data_dir(path) == true && isfile(filename) == true
        try
            # -----------------------------------------------------------------------------
            # Load and parse the file.
            local file = read(filename, String)
            local data = JSON.parse(file)

            if isa(data, AbstractDict) == true
                result = parse_pdf_font_info(data)
            else
                # -------------------------------------------------------------------------
                # Bad JSON file.
                @warn "Could not parse: $filename"
                result = PdfFontInfo()
            end
        catch ex
            # -----------------------------------------------------------------------------
            # Error reading the file.
            if isa(ex, SystemError) == true
                @warn "Could not read: $filename"
            elseif isa(ex, ErrorException) == false
                @warn "Could not parse: $filename"
            else
                rethrow(ex)
            end
            result = PdfFontInfo()
        end
    else
        # ---------------------------------------------------------------------------------
        # File does not exist.
        result = PdfFontInfo()
    end

    return result
end

# =========================================================================================
"""
save_pdf_font_info(
        info::PdfFontInfo,
        path::AbstractString
    )::Bool

Save pdf font info file to the specified directory.  Returns `false` if the the operation
fails.

__Arguments:__

| T | Name | Default | Description
|:--| :--- | :------ | :----------
| R | info |         | The `PdfFontInfo` object to save.
| R | path |         | The path to save the file to.
"""
function save_pdf_font_info(
            info::PdfFontInfo,
            path::AbstractString
        )::Bool
    local result   = false
    local filename = joinpath(path, PDF_FONT_INFO_FILE)
    local data     = Dict{String, Any}()

    data["version"]    = INDEX_VERSION
    data["lastcheck"]  = info.lastcheck
    data["server"]     = info.server
    data["font"]       = info.font

    try
        write(filename, JSON.json(data))
        result = true
    catch ex
        if isa(ex, SystemError) == false
            rethrow(ex)
        end
    end

    return result
end


text:
@testset "Transformations" begin
    function test_reverse(T, seq)
        revseq = reverse(T(seq))
        @test String(revseq) == reverse(seq)
    end

    function test_dna_complement(T, seq)
        comp = complement(T(seq))
        @test String(comp) == dna_complement(seq)
    end

    function test_rna_complement(T, seq)
        comp = complement(T(seq))
        @test String(comp) == rna_complement(seq)
    end

    function test_dna_revcomp(T, seq)
        revcomp = reverse_complement(T(seq))
        @test String(revcomp) == reverse(dna_complement(seq))
    end

    function test_rna_revcomp(T, seq)
        revcomp = reverse_complement(T(seq))
        @test String(revcomp) == reverse(rna_complement(seq))
    end

    @testset "Reverse" begin
        for len in 1:64, _ in 1:10
            if len <= 32
                test_reverse(DNAMer{len}, random_dna_kmer(len))
                test_reverse(RNAMer{len}, random_rna_kmer(len))
            end
            test_reverse(BigDNAMer{len}, random_dna_kmer(len))
            test_reverse(BigRNAMer{len}, random_rna_kmer(len))
        end

        seq = dna"AAAAAAAAAAAAAAAAAAAAAAAAAAAAGATAC"
        @test reverse(seq[(length(seq)-9):length(seq)]) == dna"CATAGAAAAA"
    end

    @testset "Complement" begin
        for len in 1:64, _ in 1:10
            if len <= 32
                test_dna_complement(DNAMer{len}, random_dna_kmer(len))
                test_rna_complement(RNAMer{len}, random_rna_kmer(len))
            end
            test_dna_complement(BigDNAMer{len}, random_dna_kmer(len))
            test_rna_complement(BigRNAMer{len}, random_rna_kmer(len))
        end
    end

    @testset "Reverse Complement" begin
        for len in 1:64, _ in 1:10
            if len <= 32
                test_dna_revcomp(DNAMer{len}, random_dna_kmer(len))
                test_rna_revcomp(RNAMer{len}, random_rna_kmer(len))
            end
            test_dna_revcomp(BigDNAMer{len}, random_dna_kmer(len))
            test_rna_revcomp(BigRNAMer{len}, random_rna_kmer(len))
        end
    end
end


text:
module MapImageDumper

using Maple, Cairo
using Gtk, Gtk.ShortNames
using ..Ahorn

surfaceSize(tlx::Number, tly::Number, brx::Number, bry::Number) = brx - tlx, bry - tly
surfaceSize(m::Maple.Map) = surfaceSize(Maple.bounds(m)...)

function drawMapSurface(m::Maple.Map)
    dummyMap = deepcopy(m)

    tlx, tly, brx, bry = Maple.bounds(dummyMap)
    width, height = surfaceSize(tlx, tly, brx, bry)

    camera = Ahorn.Camera(tlx, tly, 1)
    surface = Cairo.CairoARGBSurface(width, height)
    ctx = Ahorn.getSurfaceContext(surface)

    Ahorn.paintSurface(ctx, (1.0, 1.0, 1.0, 0.0))
    pattern_set_filter(get_source(ctx), Cairo.FILTER_NEAREST)

    for room in dummyMap.rooms
        dr = Ahorn.getDrawableRoom(dummyMap, room)
        dr.fillColor = (1.0, 1.0, 1.0, 0.0)

        Ahorn.getLayerByName(dr.layers, "triggers").visible = false
        Ahorn.getLayerByName(dr.layers, "tools").visible = false

        Ahorn.drawRoom(ctx, camera, dr, alpha=1.0)
    end

    Ahorn.deleteDrawableRoomCache(dummyMap)

    return surface
end

function dumpMapImageDialog(w=nothing)
    celesteDir = get(Ahorn.config, "celeste_dir", "")
    targetDir = ispath(celesteDir) ? celesteDir : pwd()

    map = Ahorn.loadedState.map

    if isa(map, Map)
        filename = Ahorn.saveDialog("Save as", Ahorn.window, ["*.png"], folder=targetDir)

        if filename != "" && isa(map, Map)
            surface = drawMapSurface(map)
            ctx = Ahorn.getSurfaceContext(surface)

            surfaceStatus = Cairo.status(surface)
            if surfaceStatus == Cairo.STATUS_NO_MEMORY
                warn_dialog("Not enough memory to save image.", Ahorn.window)

            else
                fn = Ahorn.hasExt(filename, ".png") ? filename : filename * ".png"
                open(io -> write_to_png(surface, io), fn, "w")
            end

            deleteSurface(surface)
        end

    else
        warn_dialog("No map is currently loaded.", Ahorn.window)
    end
end

end

max_stars_repo_name: asdfsdfadfasfasd/Ahorn

max_issues_repo_name: asdfsdfadfasfasd/Ahorn

max_forks_repo_name: asdfsdfadfasfasd/Ahorn


text:
#
# Some general tools for working with networks of ITensors.
#

"""
    itensor_network(dims::Int...; linkdims)
    itensor_network(s::Array{<:Index}; linkdims)
    itensor_network(s::Array{<:Vector{<:Index}}; linkdims)

Create a tensor network on a hypercubic lattice of
dimension `dims` with link dimension `linkdims`.

Alternatively, specify the site indices with an Array `s`,
in which case the lattice will be of dimension `size(s)`.

The network will have periodic boundary conditions.
To remove the periodic boundary condiitions, use
the function `project_boundary`.
"""
function itensor_network(dims::Int...; linkdims)
  return ITensor.(inds_network(dims...; linkdims=linkdims))
end

function itensor_network(s::Array; linkdims)
  return ITensor.(inds_network(s; linkdims=linkdims))
end

"""
    boundary_projectors(tn::Matrix{ITensor}, state=1)

For a 2D tensor network, return the right and bottom boundary projectors onto
the local state `state`.
"""
function boundary_projectors(tn::Matrix{ITensor}, state=1)
  top_row = tn[1, :]
  bottom_row = tn[end, :]
  left_column = tn[:, 1]
  right_column = tn[:, end]
  bottom_boundary_inds = commonind.(bottom_row, top_row)
  right_boundary_inds = commonind.(right_column, left_column)
  ψr = ITensors.state.(right_boundary_inds, state)
  ψb = ITensors.state.(bottom_boundary_inds, state)
  return ψr, ψb
end

"""
    project_boundary(tn::Matrix{ITensor}, state=1)

Project the boundary of a periodic 2D tensor network onto
the specified state.
"""
function project_boundary(tn::Matrix{ITensor}, state=1)
  Nx, Ny = size(tn)
  ψr, ψb = boundary_projectors(tn, state)
  for n in 1:Nx
    tn[n, 1] = tn[n, 1] * ψr[n]
    tn[n, end] = tn[n, end] * dag(ψr[n])
  end
  for n in 1:Ny
    tn[1, n] = tn[1, n] * ψb[n]
    tn[end, n] = tn[end, n] * dag(ψb[n])
  end
  return tn
end

function filter_alllinkinds(f, tn)
  linkinds = Dict{Tuple{keytype(tn),keytype(tn)},Vector{indtype(tn)}}()
  for n in keys(tn), m in keys(tn)
    if f(n, m)
      is = commoninds(tn[n], tn[m])
      if !isempty(is)
        linkinds[(n, m)] = is
      end
    end
  end
  return linkinds
end

"""
    alllinkinds(tn)

Return a dictionary of all of the link indices of the network.
The link indices are determined by searching through the network
for tensors with indices in common with other tensors, and
the keys of the dictionary store a tuple of the sites with
the common indices.

Notice that this version will return a dictionary containing
repeated link indices, since the
For example:
```julia
i, j, k, l = Index.((2, 2, 2, 2))
inds_network = [(i, dag(j)), (j, dag(k)), (k, dag(l)), (l, dag(i))]
tn_network = randomITensor.(inds_network)
links = allinkinds(tn_network)
links[(1, 2)] == (dag(j),)
links[(2, 1)] == (j,)
links[(2, 3)] == (dag(l),)
links[(1, 3)] # Error! In the future this may return an empty Tuple
```

Use `inlinkinds` and `outlinkinds` to return dictionaries without
repeats (such as only the link `(2, 1)` and not `(1, 2)` or vice versa).
"""
alllinkinds(tn) = filter_alllinkinds(≠, tn)
inlinkinds(tn) = filter_alllinkinds(>, tn)
outlinkinds(tn) = filter_alllinkinds(<, tn)

function filterneighbors(f, tn, n)
  neighbors_tn = keytype(tn)[]
  tnₙ = tn[n]
  for m in keys(tn)
    if f(n, m) && hascommoninds(tnₙ, tn[m])
      push!(neighbors_tn, m)
    end
  end
  return neighbors_tn
end

"""
    neighbors(tn, n)

From a tensor network `tn` and a site/node `n`, determine the neighbors
of the specified tensor `tn[n]` by searching for which other
tensors in the network have indices in common with `tn[n]`.

Use `inneighbors` and `outneighbors` for directed versions.
"""
neighbors(tn, n) = filterneighbors(≠, tn, n)
inneighbors(tn, n) = filterneighbors(>, tn, n)
outneighbors(tn, n) = filterneighbors(<, tn, n)

function mapinds(f, ::typeof(linkinds), tn)
  tn′ = copy(tn)
  inds_dict = Dict()
  for n in keys(tn)
    for nn in neighbors(tn, n)
      commonindsₙ = commoninds(tn[n], tn[nn])
      newinds = []
      for i in commonindsₙ
        if !haskey(inds_dict, i)
          inds_dict[i] = f(i)
        end
        newinds = vcat(newinds, [inds_dict[i]])
      end
      tn′[n] = replaceinds(tn′[n], commonindsₙ => newinds)
    end
  end
  return tn′
end

function ITensors.prime(::typeof(linkinds), tn, args...)
  return mapinds(x -> prime(x, args...), linkinds, tn)
end

function ITensors.sim(::typeof(linkinds), tn, args...)
  return mapinds(x -> sim(x, args...), linkinds, tn)
end

function ITensors.addtags(::typeof(linkinds), tn, args...)
  return mapinds(x -> addtags(x, args...), linkinds, tn)
end

function ITensors.removetags(::typeof(linkinds), tn, args...)
  return mapinds(x -> removetags(x, args...), linkinds, tn)
end

function ITensors.prime(indices::Array{<:Index,1}, network::Array{ITensor}, n::Integer=1)
  function primeinds(tensor)
    prime_inds = [ind for ind in inds(tensor) if ind in indices]
    if (length(prime_inds) == 0)
      return tensor
    end
    return replaceinds(tensor, prime_inds => prime(prime_inds, n))
  end
  return map(x -> primeinds(x), network)
end

function ITensors.sim(indices::Array{<:Index,1}, network::Array{ITensor}, sim_dict::Dict)
  function siminds(tensor)
    sim_inds = [ind for ind in inds(tensor) if ind in indices]
    if (length(sim_inds) == 0)
      return tensor
    end
    outinds = map(i -> sim_dict[i], sim_inds)
    return replaceinds(tensor, sim_inds => outinds)
  end
  return map(x -> siminds(x), network)
end

function ITensors.commoninds(n1::Array{ITensor}, n2::Array{ITensor})
  return mapreduce(a -> commoninds(a...), vcat, zip(n1, n2))
end

# Compute the sets of combiners that combine the link indices
# of the tensor network so that neighboring tensors only
# share a single larger index.
# Return a dictionary from a site to a combiner.
function combiners(::typeof(linkinds), tn)
  Cs = Dict(keys(tn) .=> (ITensor[] for _ in keys(tn)))
  for n in keys(tn)
    for nn in inneighbors(tn, n)
      commonindsₙ = commoninds(tn[n], tn[nn])
      C = combiner(commonindsₙ)
      push!(Cs[n], C)
      push!(Cs[nn], dag(C))
    end
  end
  return Cs
end

# Insert the gauge tensors `gauge` into the links of the tensor
# network `tn`.
function insert_gauge(tn, gauge)
  tn′ = copy(tn)
  for n in keys(gauge)
    for g in gauge[n]
      if hascommoninds(tn′[n], g)
        tn′[n] *= g
      end
    end
  end
  return tn′
end

# Insert the gauge tensors `gauge` into the links of the sets
# of tensor networks `tn` stored in a NamedTuple.
# TODO: is this used anywhere?
function insert_gauge(tn::NamedTuple, gauge)
  return map(x -> insert_gauge.(x, (gauge,)), tn)
end

# Split the links of an ITensor network by changing the prime levels
# or tags of pairs of links.
function split_links(H::Union{MPS,MPO}; split_tags=("" => ""), split_plevs=(0 => 1))
  left_tags, right_tags = split_tags
  left_plev, right_plev = split_plevs
  l = outlinkinds(H)
  Hsplit = copy(H)
  for bond in keys(l)
    n1, n2 = bond
    lₙ = l[bond]
    left_l_n = prime(addtags(lₙ, left_tags), left_plev)
    right_l_n = prime(addtags(lₙ, right_tags), right_plev)
    Hsplit[n1] = replaceinds(Hsplit[n1], lₙ => left_l_n)
    Hsplit[n2] = replaceinds(Hsplit[n2], lₙ => right_l_n)
  end
  return Hsplit
end

function split_links(H::Vector{ITensor}, args...; kwargs...)
  return data(split_links(MPS(H), args...; kwargs...))
end


text:
### A Pluto.jl notebook ###
# v0.14.2

using Markdown
using InteractiveUtils

# ╔═╡ 94cddadf-ac24-4523-8a2a-69c4c5eb3514
begin
	try
		using Memoize
	catch
		import Pkg
		Pkg.add("Memoize")
		using Memoize
	end
end

# ╔═╡ 66c66574-a23c-11eb-33fe-c7f4ea73d016
md"""
# Day 19: Monster Messages

You land in an airport surrounded by dense forest. As you walk to your high-speed train, the Elves at the Mythical Information Bureau contact you again. They think their satellite has collected an image of a *sea monster*! Unfortunately, the connection to the satellite is having problems, and many of the messages sent back from the satellite have been corrupted.

They sent you a list of *the rules valid messages should obey* and a list of *received messages* they've collected so far (your puzzle input).

The *rules for valid messages* (the top part of your puzzle input) are numbered and build upon each other. For example:

```
0: 1 2
1: "a"
2: 1 3 | 3 1
3: "b"
```

Some rules, like 3: "b", simply match a single character (in this case, b).

The remaining rules list the sub-rules that must be followed; for example, the rule `0: 1 2` means that to match rule 0, the text being checked must match rule `1`, and the text after the part that matched rule `1` must then match rule `2`.

Some of the rules have multiple lists of sub-rules separated by a pipe (`|`). This means that at least one list of sub-rules must match. (The ones that match might be different each time the rule is encountered.) For example, the rule `2: 1 3 | 3 1` means that to match rule `2`, the text being checked must match rule `1` followed by rule `3` or it must match rule `3` followed by rule `1`.

Fortunately, there are no loops in the rules, so the list of possible matches will be finite. Since rule 1 matches `a` and rule `3` matches `b`, rule `2` matches either `ab` or `ba`. Therefore, rule `0` matches aab or aba.

Here's a more interesting example:

```
0: 4 1 5
1: 2 3 | 3 2
2: 4 4 | 5 5
3: 4 5 | 5 4
4: "a"
5: "b"
```

Here, because rule `4` matches `a` and rule `5` matches `b`, rule `2` matches two letters that are the same (`aa` or `bb`), and rule `3` matches two letters that are different (`ab` or `ba`).

Since rule `1` matches rules `2` and `3` once each in either order, it must match two pairs of letters, one pair with matching letters and one pair with different letters. This leaves eight possibilities: `aaab`, `aaba`, `bbab`, `bbba`, `abaa`, `abbb`, `baaa`, or `babb`.

Rule `0`, therefore, matches a (rule `4`), then any of the eight options from rule `1`, then `b` (rule `5`): `aaaabb`, `aaabab`, `abbabb`, `abbbab`, `aabaab`, `aabbbb`, `abaaab`, or `ababbb`.

The received messages (the bottom part of your puzzle input) need to be checked against the rules so you can determine which are valid and which are corrupted. Including the rules and the messages together, this might look like:

```
0: 4 1 5
1: 2 3 | 3 2
2: 4 4 | 5 5
3: 4 5 | 5 4
4: "a"
5: "b"

ababbb
bababa
abbbab
aaabbb
aaaabbb
```

Your goal is to determine the number of messages that completely match rule `0`. In the above example, `ababbb` and `abbbab` match, but `bababa`, `aaabbb`, and `aaaabbb` do not, producing the answer `2`. The whole message must match all of rule `0`; there can't be extra unmatched characters in the message. (For example, `aaaabbb` might appear to match rule `0` above, but it has an extra unmatched b on the end.)

How many messages completely match rule `0`?
"""

# ╔═╡ ca24cb61-0129-44a0-829b-9a160a89d41b
abstract type Rule end

# ╔═╡ 6fae10d7-0bad-44f3-a1d2-3d2fca9562ee
struct PrimitiveRule <: Rule
	id::Int
	match::String
end

# ╔═╡ 64e754ac-b914-4bec-98e3-551fc0d9812d
struct CompositeRule <: Rule
	id::Int
	parts::Vector{Rule}
end

# ╔═╡ a804d58d-d5ff-4749-a4f7-f93306b72bb8
struct UnionRule <: Rule
	id::Int
	left::Rule
	right::Rule
end

# ╔═╡ b07d749f-e3ca-41ec-8503-d5133dab0d8c
struct Rule0 <: Rule
	id::Int
	rule31::Rule
	rule42::Rule
end

# ╔═╡ eb65a909-9f30-41b0-a1a2-aa7bee4e8d99
Base.show(io::IO, rule::PrimitiveRule) = print(io, rule.match)

# ╔═╡ 7ba12366-5d0c-4521-8b7a-cf5a22c2c20a
Base.show(io::IO, rule::CompositeRule) = print(io, join(rule.parts))

# ╔═╡ 9a963852-57ac-4e47-ab95-c371111b0b87
Base.show(io::IO, rule::Rule0) = begin
	print(io, "($(string(rule.rule42))+")
	print(io, "($(string(rule.rule42))){n}($(string(rule.rule31))){n}")
end

# ╔═╡ 702f2804-0dcf-4488-bfaf-26e1ebb9895d
Base.show(io::IO, rule::UnionRule) = begin
	print(io, "(")
	print(io, rule.left)
	print(io, "|")
	print(io, rule.right)
	print(io, ")")
end

# ╔═╡ 850282ea-1bd8-4b58-802c-7df2c5d4b786
function parse_rule(s, n, raw_rules, parsed_rules)
	m = match(r"\"([a-z]+)\"", s)
	if m !== nothing
		return PrimitiveRule(n, m.captures[1])
	elseif contains(s, "|")
		left_s, right_s = split(s, "|")
		left = parse_rule(left_s, n, raw_rules, parsed_rules)
		right = parse_rule(right_s, n, raw_rules, parsed_rules)
		return UnionRule(n, left, right)
	else
		indices = map(p -> parse(Int, p), split(s))
		parts = Rule[]
		for i ∈ indices
			if i ∉ keys(parsed_rules)
				parsed_rules[i] = parse_rule(raw_rules[i], i, raw_rules, parsed_rules)
			end
			push!(parts, parsed_rules[i])
		end
		return CompositeRule(n, parts)
	end
end

# ╔═╡ acf2ea81-d8a6-4a1c-b5bf-73b4dd56ace2
function parse_rule_0(s)
	lines = split(s, "\n")
	raw_rules = Dict{Int, String}()
	for line ∈ lines
		m = match(r"^([0-9]+): (.*)$", line)
		i = parse(Int, m.captures[1])
		raw_rule = m.captures[2]
		raw_rules[i] = raw_rule
	end
	parsed_rules = Dict{Int, Rule}()

	parse_rule(raw_rules[0], 0, raw_rules, parsed_rules)
end

# ╔═╡ aaadb3ea-f565-497d-847c-618700a20745
function parse_rules(s)
	lines = split(s, "\n")
	raw_rules = Dict{Int, String}()
	for line ∈ lines
		m = match(r"^([0-9]+): (.*)$", line)
		i = parse(Int, m.captures[1])
		raw_rule = m.captures[2]
		raw_rules[i] = raw_rule
	end
	parsed_rules = Dict{Int, Rule}()

	rule0 = parse_rule(raw_rules[0], 0, raw_rules, parsed_rules)
	parsed_rules[0] = rule0
	parsed_rules
end

# ╔═╡ 74506116-f26c-467d-8bb0-f15bdac5d2c7
function match_rule(rule::Rule, s::AbstractString)
	regex = Regex(string(rule))
	m = match(regex, s)
	return m !== nothing && m.match == s
end

# ╔═╡ 9d8aab4f-f7bf-492d-86d4-075a60cf3006
function get_input(filename)
	contents = read(filename, String)
	rules_text, messages_text = split(contents, "\n\n")
    return parse_rule_0(rules_text), split(messages_text)
end

# ╔═╡ 0a5bbd42-aeba-4d4a-9879-5c0ea8ecfebf
function get_parsed_rules(filename)
	contents = read(filename, String)
	rules_text, messages_text = split(contents, "\n\n")
	return parse_rules(rules_text)
end

# ╔═╡ b05c4124-89cd-4985-91be-6d65f32032cd
function get_messages(filename)
	contents = read(filename, String)
	rules_text, messages_text = split(contents, "\n\n")
    return split(messages_text)
end

# ╔═╡ e6f0828d-be18-4e77-a105-a51c3a538b64
rules = get_parsed_rules("day19input.txt")

# ╔═╡ fc067185-76a8-4f4d-bdbb-740cad8e9e05
function add_deps!(deps, rule)
	if typeof(rule) == PrimitiveRule
		push!(deps, rule.id)
	elseif typeof(rule) == CompositeRule
		push!(deps, rule.id)
		foreach(r->add_deps!(deps, r), rule.parts)
	elseif typeof(rule) == UnionRule
		push!(deps, rule.id)
		add_deps!(deps, rule.left)
		add_deps!(deps, rule.right)
	end
end

# ╔═╡ 281c7783-fdb7-4b49-99a0-89cb8fcf3998
function get_deps(rule)
	deps = Set{Int}()
	add_deps!(deps, rule)
	deps
end

# ╔═╡ cceba7b4-27c4-47d8-8bc7-7847cfc1b82e
isdisjoint(union(get_deps(rules[42]), get_deps(rules[31])),  Set([8, 11]))

# ╔═╡ 33ef31d2-0aa0-4856-b1cd-153924633cc4
md"""
Because the dependencies of rules 42 and 31 don't include 8 and 11, we can treat 8 and 11 specially.
"""

# ╔═╡ a47285ba-aca6-46dd-8513-6d0ae4134f5d
function find_potentially_infinite_rules(rules)
	recursive = Int[]
	for rule ∈ values(rules)
		deps = get_deps(rule)
		if 8 ∈ deps || 11 ∈ deps
			push!(recursive, rule.id)
		end
	end
	recursive
end

# ╔═╡ f0c36d34-6250-4eb8-9e85-39b7cc3e315b
find_potentially_infinite_rules(rules)

# ╔═╡ 43d7544c-fc2c-4240-90e9-f1b87ab56375
function get_regex(rule::Rule0)
	# Synthesize a regex from examining rule 8 and 11
	# Rule 0 becomes (rule_42)+(rule_42){n}(rule_31){n}
	# This synthesized regex only handles n ∈ 1..5, but that appears
	# to work
	rule_42 = string(rule.rule42)
	rule_31 = string(rule.rule31)
	regex_string = "^($(rule_42))+"
	regex_string *= "("
	for i ∈ 1:4
		regex_string *= "($(rule_42)){$(i)}($(rule_31)){$(i)}|"
	end
	regex_string *= "($(rule_42)){5}($(rule_31)){5})\$"
	Regex(regex_string)
end

# ╔═╡ 86c123a9-93ea-4d06-90a6-830a704414a6
function match_rule(rule::Rule0, s::AbstractString)
	match(get_regex(rule), s) !== nothing
end

# ╔═╡ c0f37e21-e5b2-4f39-8ba3-6c2e90cc1e5c
function part1(filename="day19input.txt")
	rule_0, messages = get_input(filename)
	count(m->match_rule(rule_0, m), messages)
end

# ╔═╡ 1b5cad05-3432-4acd-b74f-b831ac0b5a44
part1()

# ╔═╡ e8f096d8-6f50-43b1-ad1e-157e35801d45
function part2(filename = "day19input.txt")
	rules = get_parsed_rules(filename)
	rules[0] = Rule0(0, rules[31], rules[42])
	# rules[0] = Rule0(0, PrimitiveRule(31, "31"), PrimitiveRule(42, "42"))
	messages = get_messages(filename)
	get_regex(rules[0])
	count(m->match_rule(rules[0], m), messages)
end

# ╔═╡ b91e0294-4019-4e83-80de-36565cb63e06
part2()

# ╔═╡ Cell order:
# ╟─66c66574-a23c-11eb-33fe-c7f4ea73d016
# ╠═94cddadf-ac24-4523-8a2a-69c4c5eb3514
# ╠═ca24cb61-0129-44a0-829b-9a160a89d41b
# ╠═6fae10d7-0bad-44f3-a1d2-3d2fca9562ee
# ╠═64e754ac-b914-4bec-98e3-551fc0d9812d
# ╠═a804d58d-d5ff-4749-a4f7-f93306b72bb8
# ╠═b07d749f-e3ca-41ec-8503-d5133dab0d8c
# ╠═eb65a909-9f30-41b0-a1a2-aa7bee4e8d99
# ╠═7ba12366-5d0c-4521-8b7a-cf5a22c2c20a
# ╠═9a963852-57ac-4e47-ab95-c371111b0b87
# ╠═702f2804-0dcf-4488-bfaf-26e1ebb9895d
# ╠═850282ea-1bd8-4b58-802c-7df2c5d4b786
# ╠═acf2ea81-d8a6-4a1c-b5bf-73b4dd56ace2
# ╠═aaadb3ea-f565-497d-847c-618700a20745
# ╠═74506116-f26c-467d-8bb0-f15bdac5d2c7
# ╠═9d8aab4f-f7bf-492d-86d4-075a60cf3006
# ╠═0a5bbd42-aeba-4d4a-9879-5c0ea8ecfebf
# ╠═b05c4124-89cd-4985-91be-6d65f32032cd
# ╠═c0f37e21-e5b2-4f39-8ba3-6c2e90cc1e5c
# ╠═1b5cad05-3432-4acd-b74f-b831ac0b5a44
# ╠═e6f0828d-be18-4e77-a105-a51c3a538b64
# ╠═fc067185-76a8-4f4d-bdbb-740cad8e9e05
# ╠═281c7783-fdb7-4b49-99a0-89cb8fcf3998
# ╠═cceba7b4-27c4-47d8-8bc7-7847cfc1b82e
# ╟─33ef31d2-0aa0-4856-b1cd-153924633cc4
# ╠═a47285ba-aca6-46dd-8513-6d0ae4134f5d
# ╠═f0c36d34-6250-4eb8-9e85-39b7cc3e315b
# ╠═43d7544c-fc2c-4240-90e9-f1b87ab56375
# ╠═86c123a9-93ea-4d06-90a6-830a704414a6
# ╠═e8f096d8-6f50-43b1-ad1e-157e35801d45
# ╠═b91e0294-4019-4e83-80de-36565cb63e06


text:
########################
# helper functions
########################

# return a list of file names without suffix in the directory
# e.g. filenames(mm) and filenames(uf)
function filenames(directory::AbstractString)

    namevec = AbstractString[]

    matdatadir = joinpath(dirname(@__FILE__),"..", "data", "$directory")
    matvec = readdir(matdatadir)
    for file in matvec
        filename = split(file, '.')[1]
        push!(namevec, filename)
    end
    return namevec
end

# return the path to a folder inside the data directory
data_dir(name::AbstractString) = joinpath(dirname(@__FILE__), "..", "data", name)
user_file(name::AbstractString) = string(MY_DEPOT_DIR, "/$(name)")

# return a list of matrix data name in the collection
function matrix_data_name_list()
    matrices = AbstractString[]
    if isdir(data_dir("uf"))
        for col in filenames("uf")
            for mat in filenames("uf/$(col)")
                push!(matrices, string(col, '/', mat))
            end
        end
    end

    if isdir(data_dir("mm"))
        for col in filenames("mm")
            for d in filenames("mm/$(col)")
                for mat in filenames("mm/$(col)/$(d)")
                    push!(matrices, string(col, '/', d, '/', mat))
                end
            end
        end
    end
    matrices
end

# return a list of matrix name in the collection
function matrix_name_list()
    matrices = sort(collect(keys(matrixdict)))
    append!(matrices, matrix_data_name_list())
    matrices
end

# return a list of groups in the collection
_matrix_class() = collect(keys(matrixclass))
_user_matrix_class() = collect(keys(usermatrixclass))

function group_list()
    groups = _matrix_class()
    try
        append!(groups, _user_matrix_class())
    end
    push!(groups, "data")
    push!(groups, "all")
    sort(groups)
end

##########################
# display information
##########################

# print info about all matrices in the collection
"""
`matrixdepot()`

Print all the matrices and groups in the collection.
"""
function matrixdepot()
    # Print information strings
    println()
    println("Matrices:")

    matrices = matrix_name_list()

    i = 1
    for (index, mat) in enumerate(matrices)
        if i < 4 && length(mat) < 14
            i += 1
            @printf "%4d) %-14s" index mat
        else
            i = 1
            @printf "%4d) %-14s\n" index mat
        end
    end
    println()

    println("Groups:")

    groups = group_list()

    j = 1
    for name in groups
        if j < 4 && length(name) < 12
            j += 1
            @printf "  %-12s" name
        else
            j = 1
            @printf "  %-12s\n" name
        end
    end
    println()
end

# Return information strings if name is a matrix name
# and return a list of matrix names if name is a group.
"""
`matrixdepot(name)`

Return the documentation if `name` is a matrix name;
return a list of matrix names if `name` is a group name.
"""
function matrixdepot(name::AbstractString)
    # name is the matrix name or matrix properties
    if name in keys(matrixdict)
        eval(parse("Base.Docs.@repl $(matrixdict[name])", raise = false))
    elseif name in _matrix_class()
        matrices = matrixclass[name]
        return sort(matrices)
    elseif '/' in name  # print matrix data info

        namelist = split(name, '/')
        if length(namelist) == 2
            ufreader(string(data_dir("uf"), '/', namelist[1]), namelist[2])
        else
            mmreader(data_dir("mm"), name)
        end

    elseif name == "data" # deal with the group "data"
        return matrix_data_name_list()
    elseif name == "all" # all the matrix names in the collection
        return matrix_name_list()
    elseif name in _user_matrix_class()
        matrices = usermatrixclass[name]
        return sort(matrices)
    else
        throw(ArgumentError("No information is available for \"$(name)\"."))
    end
end

#############################
# matrix generators
#############################

"""
`matrixdepot(matrix name, p1, p2...)`

Return a matrix specified by the query string `matrix name`.
`p1, p2...` are input parameters depending on `matrix name`.
"""
matrixdepot(name::AbstractString, args...) = matrixdict[name](args...)

# generate the required matrix
# method = :read   (or :r) read matrix data
#          :get    (or :g) download matrix data
#          :search (or :s) search collection information
"""
`matrixdepot(data, symbol)`

Generate the data if `symbol = :r (or :read)`; download the data if `symbol = :g (or :get)`.
"""
function matrixdepot(name::AbstractString, method::Symbol; meta::Bool = false)
    if method == :r || method == :read
        namelist = split(name, '/')
        if  length(namelist) == 2
            ufreader(string(data_dir("uf"), '/', namelist[1]), namelist[2], info = false, meta = meta)
        else
            mmreader(data_dir("mm"), name, info = false)
        end

    elseif method == :g || method == :get
        MatrixDepot.get(name)
    elseif method == :s || method == :search
        MatrixDepot.search(name)
    else
        throw(ArgumentError("unknown symbol $method.
              use :read (or :r) to read matrix data;
              use :get  (or :g) to download matrix data;
              use :search (or :s) to search for collection information."))
    end
end

#########################
# access matrices
#########################

"""
`matrixdepot(number, range...)`

Access matrices by number, range or a mixture of numbers and ranges.
"""
function matrixdepot(num::Integer)
    matrixstrings = matrix_name_list()
    n = length(matrixstrings)
    if num > n
        throw(ArgumentError("There are $(n) parameterized matrices, but you asked for the $(num)-th."))
    end
    return matrixstrings[num]
end

function matrixdepot(ur::UnitRange)
    matrixnamelist = AbstractString[]
    for i in ur
        push!(matrixnamelist, matrixdepot(i))
    end
    return matrixnamelist
end

IntOrUnitRange = Union{Integer, UnitRange}

function matrixdepot(vs::IntOrUnitRange...)
    matrixnames = AbstractString[]
    for i in vs
        if typeof(i) <: Integer
            push!(matrixnames, matrixdepot(i))
        else
            append!(matrixnames, matrixdepot(i))
        end
    end
    return matrixnames
end

# Return a list of matrix names with common properties
# when multiple properties are given.
function matrixdepot(props::AbstractString...)
    common = matrixdepot(props[1])
    for prop in props[2:end]
        common = intersect(common, matrixdepot(prop))
    end
    return common
end

#######################
# matrix group
#######################

#add new group
function addgroup(ex)
    isdir(MY_DEPOT_DIR) || error("can not find directory myMatrixDepot")
    propname = string(ex.args[1])
    !(propname in group_list()) || throw(ArgumentError("$propname is an existing group."))

    for matname in eval(ex.args[2])
        matname in matrix_name_list() || throw(ArgumentError("$matname is not in the collection."))
    end

    user = joinpath(user_file("group.jl"))
    s = readall(user)
    iofile = open(user, "w")
    newprop = s[1:end-4] * "\""  * propname * "\" => ["
    for str in eval(ex.args[2])
        newprop *= "\"" * str * "\", "
    end
    newprop = newprop * "],\n" * s[end-3:end]
    try
        write(iofile, newprop);
    finally
        close(iofile)
    end
end

"add a group to Matrix Depot"
macro addgroup(ex)
    esc(addgroup(ex))
end

# remove an added group
function rmgroup(ex)
    propname = string(ex)
    !(propname in keys(matrixclass)) || throw(ArgumentError("$propname can not be removed."))
    propname in keys(usermatrixclass) || throw(ArgumentError("Can not find group $propname."))

    user = joinpath(user_file("group.jl"))
    s = readall(user)
    iofile = open(user, "w")
    rg = Regex("""\"""" * eval(propname) * ".+")
    key = search(s, rg) # locate the propname in user.jl to remove.
    start_char = key[1] # the start of the line
    end_char = key[end] # the end of the line
    s = s[1:start_char - 2] * s[end_char+1:end]
    try
        write(iofile, s);
    finally
        close(iofile)
    end
end

"remove an added group from Matrix Depot"
macro rmgroup(ex)
    esc(rmgroup(ex))
end

################################
# user defined matrix generators
################################

abstract MatrixGenerator
abstract FunctionName <: MatrixGenerator
abstract Group <: MatrixGenerator

include_generator(::Type{FunctionName}, fn::AbstractString, f::Function) = (matrixdict[fn] = f)

function include_generator(::Type{Group}, groupname::AbstractString, f::Function)
    if groupname in keys(matrixclass)
        push!(matrixclass[groupname], fname(f))
    elseif groupname in keys(usermatrixclass)
        push!(usermatrixclass[groupname], fname(f))
    else
        error("$(groupname) is not a group in MatrixDepot, use
              @addgroup to add this group")
    end
end

"return the name of the function `f` as a string."
function fname(f::Function)
    for (key, value) in matrixdict
        if value == f
            return key
        end
    end
end


text:
# Tests for network learning
function t_networklearner()

	pixel_adjacency(X) = begin
		n = size(X,2)
		A = spzeros(Int,n,n)
		for i in 1:n
			for j in i:n
				if i!=j && abs(X[1,i] - X[1,j]) < 2.0 && abs(X[2,i] - X[2,j]) < 2.0
					A[i,j] = 1
					A[j,i] = 1
				end
			end
		end
		return A
	end

	Ac = j4pr.DataGenerator.fish(20)   # classification dataset

	fl_train = j4pr.knn
	fl_exec = (m,x) ->j4pr.knn(x,m.x)
	fr_train = j4pr.knn
	fr_exec = (m,x) ->j4pr.knn(x,m.x)
	Adj = [NetworkLearning.adjacency(pixel_adjacency(+Ac))]

	w = j4pr.networklearner(Adj, fl_train, fl_exec, fr_train, fr_exec, maxiter=3)

	Test.@test try
		wt = Ac |> w
		NetworkLearning.add_adjacency!(wt.x.data, Adj)
		result = Ac |> wt
		true
	catch
		false
	end
end


text:
@testset "TicTacToeEnv" begin

    env = TicTacToeEnv()

    RLBase.test_interfaces!(env)
    RLBase.test_runnable!(env)

    @test length(state_space(env, Observation{Int}())) == 5478

end