text:
__precompile__()
module Associations

using DataStructures, AutoHashEquals, Base.Dates, Unitful, UnitfulAngles

import Base: push!, empty!, delete!, isempty, ==, in, show

export VideoFile, Point, POI, Run, Repetition, Association, loadLogVideoFiles, findVideoFiles, getVideoFiles, loadPOIs, loadRuns, loadAssociation, save, push!, empty!, delete!, isempty, replace!, in, report, fragment

exiftool_base = joinpath(Pkg.dir("Associations"), "deps", "src", "exiftool", "exiftool")
const exiftool = exiftool_base*(is_windows() ? ".exe" : "")

const exts = [".webm", ".mkv", ".flv", ".flv", ".vob", ".ogv", ".ogg", ".drc", ".mng", ".avi", ".mov", ".qt", ".wmv", ".yuv", ".rm", ".rmvb", ".asf", ".amv", ".mp4", ".m4p", ".m4v", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".mpg", ".mpeg", ".m2v", ".m4v", ".svi", ".3gp", ".3g2", ".mxf", ".roq", ".nsv", ".flv", ".f4v", ".f4p", ".f4a", ".f4b", ".MTS", ".DS_Store"]

@auto_hash_equals struct VideoFile
    file::String
    datetime::DateTime
end

function getDateTime(folder::String, file::String)::DateTime
    fullfile = joinpath(folder, file)
    dateTimeOriginal, createDate, modifyDate = strip.(split(readstring(`$exiftool -T -AllDates -n $fullfile`), '\t'))
    #duration_, dateTimeOriginal, createDate, modifyDate  = ("-", "-", "-", "-")
    datetime = DateTime(now())
    for i in [dateTimeOriginal, createDate, modifyDate]
        m = match(r"^(\d\d\d\d):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d).?(\d?\d?\d?)", i)
        m == nothing && continue
        ts = zeros(Int, 7)
        for (j, t) in enumerate(m.captures)
            if !isempty(t)
                ts[j] = parse(Int, t)
            else
                break
            end
        end
        isnull(Dates.validargs(DateTime, ts...)) || continue
        datetime = min(datetime, DateTime(ts...))
    end
    return datetime
end

#=function getVideoFiles(folder::String)
    #old = uploadsavedVideoFiles(folder)
    new = Set{String}()
    for (root, dir, files) in walkdir(folder)
        for file in files
            file[1] == '.' && continue
            last(splitext(file)) in exts || continue
            fname = relpath(joinpath(root, file), folder)
            push!(new, fname)
        end
    end
    return new
end=#

@auto_hash_equals struct Point
    file::String
    time::Second
end

Point(;file = "", time = Second(0)) = Point(file, time)
Point(f::String, t::Time) = Point(f, Second(t.instant))
Point(f::String, h::Int, m::Int, s::Int) = Point(f, Time(h, m, s))

@auto_hash_equals struct POI
    name::String
    start::Point
    stop::Point
    label::String
    comment::String

    function POI(name, start, stop, label, comment)
        @assert start.file != stop.file || start.time <= stop.time
        new(name, start, stop, label, comment)
    end
end

POI(;name = "", start = Point(), stop = Point(), label = "", comment = "") = POI(name, start, stop, label, comment)

# time differences
function duration(poi::POI, folder::String)::Int
    Δ = poi.stop.time.value - poi.start.time.value
    poi.start.file == poi.stop.file && return Δ
    fullfile = joinpath(folder, poi.start.file)
    t = round(Int, parse(readstring(`$exiftool -T -Duration -n $fullfile`)))
    return t + Δ
end

# time of day
function timeofday(poi::POI, folder::String)
    filescsv = joinpath(folder, "log", "files.csv")
    a, _ = readcsv(filescsv, String, header = true, quotes = true, comments = false)
    for i = 1:size(a,1)
        strip(a[i, 1]) == poi.start.file && return Time(DateTime(strip(a[i, 2])) + poi.start.time)
    end
end

@auto_hash_equals struct Run
    metadata::Dict{Symbol, String}
    comment::String
end

Run(;metadata = Dict{Symbol, String}(), comment = "") = Run(metadata, comment)

@auto_hash_equals struct Repetition
    run::Run
    repetition::Int
end

@auto_hash_equals struct Association
    pois::OrderedSet{POI}
    runs::OrderedSet{Repetition}
    associations::Set{Tuple{POI, Repetition}}
end

Association() = Association(OrderedSet{POI}(), OrderedSet{Repetition}(), Set{Tuple{POI, Repetition}}())

# in

in(x::POI, a::Association) = x in a.pois
in(x::Repetition, a::Association) = x in a.runs
in(x::Tuple{POI, Repetition}, a::Association) = x in a.associations

# equal keys
==(a::Base.KeyIterator, b::Base.KeyIterator) = length(a)==length(b) && all(k->in(k,b), a)

# pushes

function run2repetition(xs::OrderedSet{Repetition}, r::Run)
    isempty(xs) || @assert keys(last(xs).run.metadata) == keys(r.metadata)
    Repetition(r, reduce((x, y) -> max(x, y.run.metadata == r.metadata ? y.repetition : 0), 0, xs) + 1)
end

push!(xs::OrderedSet{Repetition}, r::Run) = push!(xs, run2repetition(xs, r))

function push!(a::Association, t::POI)
    push!(a.pois, t)
    return a
end

function push!(a::Association, r::Run)
    push!(a.runs, r)
    return a
end

function push!(a::Association, x::Tuple{POI, Repetition})
    @assert first(x) in a.pois
    @assert last(x) in a.runs
    push!(a.associations, x)
    return a
end

# replace

## runs

function replace!(a::Association, o::Repetition, n::Run)
    @assert o in a
    @assert keys(last(a.runs).run.metadata) == keys(o.run.metadata) == keys(n.metadata)

    runs = OrderedSet{Repetition}()
    associations = Set{Tuple{POI, Repetition}}()
    for r1 in a.runs
        if r1 == o
            push!(runs, n)
        else
            push!(runs, r1.run)
        end
        r2 = last(runs)
        for (p, r) in a.associations
            if r1 == r
                push!(associations, (p, r2))
            end
        end
    end

    empty!(a.runs)
    push!(a.runs, runs...)
    isempty(associations) && return a
    empty!(a.associations)
    push!(a.associations, associations...)
    return a
end

#=replace_comment(xs::OrderedSet{Repetition}, o::Repetition, n::Repetition) = OrderedSet{Repetition}(x == o ? n : x for x in xs)
function replace(xs::OrderedSet{Repetition}, o::Repetition, n::Repetition)
    runs = OrderedSet{Repetition}()
    for r in xs
        if r.run.metadata == o.run.metadata
            if r.repetition < o.repetition
                push!(runs, r)
            elseif r.repetition == o.repetition
                push!(runs, n)
            else
                push!(runs, Repetition(r.run, r.repetition - 1))
            end
        else
            push!(runs, r)
        end
    end
    return runs
end
replace_comment(xs::Set{Tuple{POI, Repetition}}, o::Repetition, n::Repetition) = Set{Tuple{POI, Repetition}}(last(x) == o ? (first(x), n) : x for x in xs)
function replace(xs::Set{Tuple{POI, Repetition}}, o::Repetition, n::Repetition)
    associations = Set{Tuple{POI, Repetition}}()
    for a in xs
        if last(a).run.metadata == o.run.metadata
            if last(a).repetition < o.repetition
                push!(associations, a)
            elseif last(a).repetition == o.repetition
                push!(associations, (first(a), n))
            else
                push!(associations, (first(a), Repetition(a.run, a.repetition - 1)))
            end
        else
            push!(associations, a)
        end
    end
    return associations
end
function replace!(a::Association, o::Repetition, n::Repetition)
    o == n && return a
    @assert o in a
    @assert keys(last(a.runs).run.metadata) == keys(o.run.metadata) == keys(n.run.metadata)
    only_comment = o.repetition == n.repetition && o.run.metadata == n.run.metadata
    runs = only_comment ? replace_comment(a.runs, o, n) : replace(a.runs, o, n)
    empty!(a.runs)
    push!(a.runs, runs...)
    isempty(a.associations) && return a
    associations = only_comment ? replace_comment(a.associations, o, n) : replace(a.associations, o, n)
    empty!(a.associations)
    push!(a.associations, associations...)
    return a
end
replace!(a::Association, o::Repetition, n::Run) = replace!(a, o, run2repetition(setdiff(a.runs, OrderedSet([o])), n))=#

## pois

replace!(xs::OrderedSet{POI}, o::POI, n::POI) = OrderedSet{POI}(x == o ? n : x for x in xs)
replace!(xs::Set{Tuple{POI, Repetition}}, o::POI, n::POI) = Set{Tuple{POI, Repetition}}(first(x) == o ? (n, last(x)) : x for x in xs)
function replace!(a::Association, o::POI, n::POI)
    o == n && return a
    @assert o in a
    pois = replace!(a.pois, o, n)
    empty!(a.pois)
    push!(a.pois, pois...)
    isempty(a.associations) && return a
    associations = replace!(a.associations, o, n)
    empty!(a.associations)
    push!(a.associations, associations...)
    return a
end

# deletes

function delete!(a::Association, r::Repetition)
    r in a || return a
    delete!(a.runs, r)
    filter!(x -> last(x) != r, a.associations)
    for x in a.runs
        if x.run.metadata == r.run.metadata && x.repetition > r.repetition
            replace!(a, x, x.run)
        end
    end
    return a
end

function delete!(a::Association, p::POI)
    p in a || return a
    delete!(a.pois, p)
    filter!(x -> first(x) != p, a.associations)
    return a
end

function delete!(a::Association, x::Tuple{POI, Repetition})
    x in a || return a
    @assert first(x) in a.pois
    @assert last(x) in a.runs
    delete!(a.associations, x)
    return a
end

# saves

function prep_file(folder::String, what::String)::String
    folder = joinpath(folder, "log")
    isdir(folder) || mkdir(folder)
    return joinpath(folder, "$what.csv")
end

function save(folder::String, x::OrderedSet{VideoFile})
    file = prep_file(folder, "files")
    #isempty(x) && rm(file, force=true)
    n = length(x)
    a = Matrix{String}(n + 1,2)
    a[1,:] .= ["file", "date and time"]
    for (i, v) in enumerate(x)
        a[i + 1, :] .= [v.file, string(v.datetime)]
    end
    writecsv(file, a)
end

function save(folder::String, x::OrderedSet{POI})
    file = prep_file(folder, "pois")
    n = length(x)
    a = Matrix{String}(n + 1,7)
    a[1,:] .= ["name", "start file", "start time (seconds)", "stop file", "stop time (seconds)", "label", "comments"]
    for (i, t) in enumerate(x)
        a[i + 1, :] .= [t.name, t.start.file, string(t.start.time.value), t.stop.file, string(t.stop.time.value), t.label, t.comment]
    end
    a .= strip.(a)
    writecsv(file, a)
end

function save(folder::String, x::OrderedSet{Repetition})
    ks = keys(first(x).run.metadata)
    @assert reduce((x, y) -> x && keys(y.run.metadata) == ks, true, x)
    file = prep_file(folder, "runs")
    ks = sort(collect(ks))
    header = string.(ks)
    push!(header, "Comment", "Repetition")
    n = length(x)
    a = Matrix{String}(n + 1, length(header))
    a[1,:] .= header
    for (i, r) in enumerate(x)
        for (j, k) in enumerate(ks)
            a[i + 1, j] = r.run.metadata[k]
        end
        a[i + 1, end - 1] = r.run.comment
        a[i + 1, end] = string(r.repetition)
    end
    a .= strip.(a)
    writecsv(file, a)
end

function save(folder::String, a::Association)
    save(folder, a.pois)
    save(folder, a.runs)
    file = prep_file(folder, "associations")
    open(file, "w") do o
        println(o, "POI number, run number")
        for (t, r) in a.associations
            ti = findfirst(a.pois, t)
            ri = findfirst(a.runs, r)
            println(o, ti, ",", ri)
        end
    end
end

# loads

function loadLogVideoFiles(folder::String)::Dict{String, DateTime}
    filescsv = joinpath(folder, "log", "files.csv")
    vfs = Dict{String, DateTime}()
    if isfile(filescsv)
        a, _ = readcsv(filescsv, String, header = true, quotes = true, comments = false)
        a .= strip.(a)
        @assert allunique(a[:,1])
        nrow, ncol = size(a)
        @assert ncol == 2
        for i = 1:nrow
            vfs[a[i, 1]] = DateTime(a[i, 2])
        end
    end
    return vfs
end

function findVideoFiles!(log::Dict{String, DateTime}, folder::String)
    for (root, dir, files) in walkdir(folder)
        for file in files
            file[1] == '.' && continue
            last(splitext(file)) in exts || continue
            fname = relpath(joinpath(root, file), folder)
            if !haskey(log, fname)
                log[fname] = getDateTime(folder, fname)
            end
        end
    end
end

function getVideoFiles(folder::String)::OrderedSet{VideoFile}
    log = loadLogVideoFiles(folder)
    findVideoFiles!(log, folder)
    o = sort([(k, v) for (k, v) in log], by = last)
    return OrderedSet{VideoFile}(VideoFile(k, v) for (k,v) in o)
end

function loadPOIs(folder::String)::OrderedSet{POI}
    filescsv = joinpath(folder, "log", "pois.csv")
    tgs = OrderedSet{POI}()
    if isfile(filescsv)
        a, _ = readcsv(filescsv, String, header = true, quotes = true, comments = false)
        a .= strip.(a)
        nrow, ncol = size(a)
        @assert ncol == 7
        for i = 1:nrow
            tg = POI(a[i, 1], Point(a[i, 2], Second(parse(Int, a[i, 3]))), Point(a[i, 4], Second(parse(Int, a[i, 5]))), a[i, 6], a[i, 7])
            @assert !(tg in tgs)
            push!(tgs, tg)
        end
    end
    return tgs
end

function getmetadata(folder)
    metadata = Dict{Symbol, String}()
    file = joinpath(folder, "metadata", "run.csv")
    if isfile(file)
        b = readcsv(file, comments = false)
        for i = 1:size(b,1)
            metadata[Symbol(strip(b[i, 1]))] = length(b[i,:]) < 2 ? "" : strip(b[i, 2])
        end
    end
    return metadata
end

function loadRuns(folder::String)::OrderedSet{Repetition}
    filescsv = joinpath(folder, "log", "runs.csv")
    rs = OrderedSet{Repetition}()
    if isfile(filescsv)
        a, ks = readcsv(filescsv, String, header = true, quotes = true, comments = false)
        ks = Symbol.(strip.(vec(ks)))
        a .= strip.(a)
        metadata = getmetadata(folder)
        for (k, v) in metadata
            if !(k in ks)
                unshift!(ks, k)
                a = [repmat([v], size(a, 1)) a]
            end
        end
        nks = length(ks)
        nrow, ncol = size(a)
        @assert nks == ncol > 2
        for i = 1:nrow
            metadata = Dict{Symbol, String}()
            for j = 1:ncol - 2
                metadata[ks[j]] = a[i, j]
            end
            comment = a[i, ncol - 1]
            repetition = parse(Int, a[i, ncol])
            r = Repetition(Run(metadata, comment), repetition)
            @assert !(r in rs)
            push!(rs, r)
        end
    end
    return rs
end

function loadAssociation(folder::String)::Association
    ts = loadPOIs(folder)
    rs = loadRuns(folder)
    filescsv = joinpath(folder, "log", "associations.csv")
    as = Set{Tuple{POI, Repetition}}()
    if isfile(filescsv)
        a, ks = readcsv(filescsv, Int, header = true, comments = false)
        nrow, ncol = size(a)
        @assert ncol == 2
        for i = 1:nrow
            push!(as, (ts[a[i,1]], rs[a[i, 2]]))
        end
    end
    return Association(ts, rs, as)
end

# empty

function empty!(a::Association)
    empty!(a.pois)
    empty!(a.runs)
    empty!(a.associations)
    return a
end

isempty(a::Association) = isempty(a.pois) && isempty(a.runs) && isempty(a.associations)

function delete_rempty_metadata!(a::Association)
    for k in keys(a.runs[1].run.metadata)
        if all(isempty(r.run.metadata[k]) for r in a.runs)
            for r in a.runs
                delete!(r.run.metadata, k)
            end
        end
    end
end

# report

function table(cur_matrix::Matrix; header_row=[], title=nothing)
    cur_table = ""
    if title != nothing
        cur_table *= "
$title
"
    end
    cur_table *= "
"
    if !isempty(header_row)
        cur_table *= "

"
        for cur_header in header_row
            cur_table *= "$cur_header	"
        end
        cur_table *= "

"
    end
    cur_table *= "
"
    for ii in 1:size(cur_matrix, 1)
        cur_table *= "
"
        for jj in 1:size(cur_matrix, 2)
            cur_table *= ""
            cur_table *= string(cur_matrix[ii, jj])
            cur_table *= "	"
        end
        cur_table *= "
"
    end
    cur_table *= "
"
    cur_table *= "
"
    return cur_table
end
function show(x::Second)::String
    ps = canonicalize(Dates.CompoundPeriod(Second(x)))
    a = Dict{DataType, Int}(k => 0 for k in [Hour, Minute, Second])
    ts = [Day, Week, Month, Year]
    for p in ps.periods
        if typeof(p) in ts
            a[Hour] += Hour(p).value
        else
            a[typeof(p)] += p.value
        end
    end
    return @sprintf "%i:%02i:%02i" a[Hour] a[Minute] a[Second]
end

function show(x::Time)::String
    i = Hour(x).value
    i = i > 12 ? i - 12 : i
    s = Minute(x).value >= 30 ? 0x1F550+(i-1)+12 : 0x1F550+(i-1)
    t = uppercase(num2hex(s)[end-4:end])
    return string("&#x$t;")
end

#=function time2radian(t::Time)
    x = t - Time(0,0,0)
    π*(x/convert(typeof(x), Hour(24)))
end

radian2time(α::Float64) = Time(0,0,0) + Nanosecond(round(Int, α/π*86400000000000))=#

function report(folder::String)
    a = loadAssociation(folder)
    delete_rempty_metadata!(a)

    rep = counter(Vector{String})
    for r in a.runs
        k = collect(values(r.run.metadata))
        push!(rep, k)
    end

    durs = Dict(poi.name => counter(Vector{String}) for poi in a.pois)
    coun = Dict(poi.name => counter(Vector{String}) for poi in a.pois)
    cosa = Dict(poi.name => Accumulator(Vector{String}, Float64) for poi in a.pois)
    sina = Dict(poi.name => Accumulator(Vector{String}, Float64) for poi in a.pois)
    for (p, r) in a.associations
        k = collect(values(r.run.metadata))
        push!(durs[p.name], k, duration(p, folder))
        push!(coun[p.name], k)
        t = timeofday(p, folder)
        # println(t)
        α = convert(u"rad", t)
        push!(cosa[p.name], k, cos(α))
        push!(sina[p.name], k, sin(α))
    end
    dur = Dict(k1 => Dict(k2 => Second(round(Int, v2/coun[k1][k2])) for (k2, v2) in v1) for (k1, v1) in durs)
    tim = Dict(k1 => Dict(k2 => convert(Time, atan2(u"rad", sina[k1][k2], v2)) for (k2, v2) in v1) for (k1, v1) in cosa)

    m = Matrix{String}[]
    for (k, v) in rep
        run = [k; string(v)]
        poi = [haskey(v1, k) ? show(v1[k])*" "*show(tim[k1][k]) : "" for (k1, v1) in dur]
        push!(m, reshape([run; poi], (1,:)))
    end
    m = vcat(m...)

    txt = table(m, header_row = [String.(collect(keys(a.runs[1].run.metadata))); "Repetition"; collect(keys(dur))], title = "Summary for $folder")
    index = """<!DOCTYPE html>      $txt  """
end

#=function minimize_unique(x::Dict{POI, Vector{String}})
    kill = Int[]
    for i = 1:length(first(values(x)))
        all(isempty(v[i]) for v in values(x)) && push!(kill, i)
    end
    for (k, v) in x
        deleteat!(v, kill)
    end
    n = length(x)
    M = length(first(values(x))) - 1
    m = 1
    for i = 1:M
        tmp = Set(p[m+1:end] for p in values(x))
        if length(tmp) < n
            break
        else
            m += 1
        end
    end
    return Dict(k => join(v[m:end], "_") for (k, v) in x)
end=#

#=function shortest_file_names(a::Association)
    x = Dict(p => [p.stop.file, p.start.file, string(Dates.value(p.stop.time)), string(Dates.value(p.start.time)), p.label, p.name] for p in a.pois)
    poinames = minimize_unique(x)
    x = Dict{POI, Vector{String}}()
    for p in a.pois
        m = Vector{String}[]
        for (pp,r) in a.associations
            if pp == p
                push!(m, [values(r.run.metadata)..., string(r.repetition)])
            end
        end
        n = length(m[1])
        y = ones(String, n)
        for i = 1:n
            y[i] = join(mi[i] for mi in m)
        end
        x[p] = y
    end
    runnames = minimize_unique(x)
    return Dict(k => string(r, "_", poinames[k]) for (k, r) in runnames)
end=#

function fragment(folder::String)
    a = loadAssociation(folder)
    # names = shortest_file_names(a)
    i = 0
    allvideofolder = joinpath(folder, "allvideofolder$i")
    while isdir(allvideofolder)
        i += 1
        allvideofolder = replace(allvideofolder, r"(\d*)$", i)
    end
    mkdir(allvideofolder)
    for (i, poi) in enumerate(a.pois)
        name = joinpath(allvideofolder, "$i.mp4")
        if poi.start.file ≠ poi.stop.file
            file = tempname()
            open(file, "w") do o
                for f in (poi.start.file, poi.stop.file)
                    fullname = joinpath(folder, f)
                    println(o, "file $fullname")
                end
            end
            Δ = duration(poi, folder)
            run(`ffmpeg -f concat -safe 0 -i $file -c copy -ss $(Dates.value(poi.start.time)) -to $Δ $name`)
        else
            fullname = joinpath(folder, poi.start.file)
            if poi.start.time == poi.stop.time
                run(`ffmpeg -i $fullname -c copy -ss $(Dates.value(poi.start.time)) -to $(Dates.value(poi.stop.time) + 1) $name`)
            else
                run(`ffmpeg -i $fullname -c copy -ss $(Dates.value(poi.start.time)) -to $(Dates.value(poi.stop.time)) $name`)
            end
        end
    end
end

end # module


text:
function test()
  x = 42
  typeof(x)
end

test()

# Output:
# Int64

ext: jl


text:
module bottle

using Base: String
using TOML

import Base64: base64encode
import Sockets: connect, TCPSocket

import BufferedStreams: BufferedInputStream, BufferedOutputStream, flushbuffer!, BufferedStreams
import Dates
import MbedTLS

function sslify!(socket::TCPSocket)
    entropy = MbedTLS.Entropy()
    rng = MbedTLS.CtrDrbg()
    MbedTLS.seed!(rng, entropy)

    conf = MbedTLS.SSLConfig()

    MbedTLS.config_defaults!(conf)
    MbedTLS.authmode!(conf, MbedTLS.MBEDTLS_SSL_VERIFY_REQUIRED)
    MbedTLS.rng!(conf, rng)
    MbedTLS.ca_chain!(conf)
    MbedTLS.setup!(ctx, conf)
    MbedTLS.set_bio!(ctx, socket)
    MbedTLS.handshake(ctx)
end

function send(msg::String; secret::Bool = false)
    if secret
        @info "> [SECRET MESSAGE REDACTED]"
    else
        @info "> $msg"
    end

    write(output, "$msg\r\n")
    flushbuffer!(output)
end

function find_command(msg::String)::String
    start = findfirst(isequal(' '), msg) + 1
    if msg[1] != ':'
        return SubString(msg, 1, start - 2)
    end

    finish = if length(msg) > start
                idx = findnext(isequal(' '), msg, start)

                if isnothing(idx)
                    length(msg)
                else
                    idx - 1
                end
             else
                length(msg)
             end

    return SubString(msg, start, finish)
end

function handle_cap(msg::String)
    parts = split(msg, " ")
    if length(parts) < 5
        return
    end

    if parts[4] == "LS"
        send("CAP REQ :sasl")
    elseif parts[4] == "ACK"
        # todo check for sasl?
        authenticate()
    end
end

function authenticate()
    send("AUTHENTICATE PLAIN")
end

function handle_auth(msg::String)
    # todo split into 400 byte chunks
    sasl_str = base64encode("$nick\0$nick\0$password")
    send("AUTHENTICATE $sasl_str", secret = true)
end

"""
903 auth successful
"""
function handle_903(msg::String)
    # todo this should be somewhere else probably?
    # todo error cases?
    send("CAP END")
end

function handle_ping(msg::String)
    send(replace(msg, "PING" => "PONG", count = 1))
end

"""
376 end of motd
"""
function handle_376(msg::String)
    send("JOIN $channels")

    # todo this upsets julia :(
    #atexit() do
    #    send("QUIT :Bottle popped")
    #    sleep(10000)
    #end
end

function BufferedStreams.readbytes!(source::MbedTLS.SSLContext, buffer::Vector{UInt8}, from::Int, to::Int)
    bytes = read(source, 1)
    buffer[from:length(bytes)] = bytes

    return length(bytes)
end

function handle_privmsg(msg::String)
    parts = split(msg, " ")
    if length(parts) < 4
        @info "Malformed PRIVMSG"
        return
    end

    user = parse_user(parts[1])
    if startswith(parts[4], ":\001")
        handle_ctcp(user, join(parts[4:end], " "))
    end
end

function handle_join(msg::String)
    parts = split(msg, " ")
    if length(parts) < 3
        @info "Malformed JOIN"
        return
    end

    channel = parts[3]
    send("PRIVMSG $channel :hello $channel :)")
end

struct User
    nick::String
    ident::String
    host::String
end

function parse_user(user::AbstractString)::User
    regex = r"^:(([^!]+)!)?(([^@]+)@)?(.+)$"
    matches = match(regex, user)

    return User(matches[2], matches[4], matches[5])
end

function handle_ctcp(user::User, msg::AbstractString)
    if startswith(msg,  ":\001CLIENTINFO")
        send("NOTICE $(user.nick) :\001CLIENTINFO PING TIME VERSION\001")
    elseif startswith(msg, ":\001PING")
        send("NOTICE $(user.nick) $msg")
    elseif startswith(msg, ":\001VERSION")
        send("NOTICE $(user.nick) :\001VERSION Bottle bot\001")
    elseif startswith(msg, ":\001TIME")
        time = Dates.format(Dates.now(Dates.UTC), "e, dd u yyyy HH:MM:SS \\G\\M\\T")
        send("NOTICE $(user.nick) :\001TIME $time\001")
    end
end

handlers = Dict{String, Function}()
handlers["CAP"] = handle_cap
handlers["AUTHENTICATE"] = handle_auth
handlers["903"] = handle_903
handlers["PING"] = handle_ping
handlers["376"] = handle_376
handlers["PRIVMSG"] = handle_privmsg
handlers["JOIN"] = handle_join

config = TOML.parsefile("Bottle.toml")

server = config["server"]
port = config["port"]
nick = config["nick"]
ident = config["ident"]
gecos = config["gecos"]
password = config["password"]
channels = config["channels"]

conn = connect(server, port)
ctx = MbedTLS.SSLContext()
input = BufferedInputStream(ctx)
output = BufferedOutputStream(ctx)
sslify!(conn)

send("CAP LS 302")
send("NICK $nick")
send("USER $ident 0 * :$gecos")

for line in eachline(input)
    @info "< $line"
    command = find_command(line)

    if haskey(handlers, command)
        @debug "handling $command"
        handlers[command](line)
    else
        @debug "$command is no good"
    end
end

end # module

max_stars_repo_name: mrmonday/bottle

max_issues_repo_name: mrmonday/bottle

max_forks_repo_name: mrmonday/bottle


text:
using Firebase

@testset "Realtime" begin

    URL = "https://fir-jl-457eb-default-rtdb.asia-southeast1.firebasedatabase.app"
    BASE_URL =  "https://fir-jl-457eb-default-rtdb.asia-southeast1.firebasedatabase.app"
    realdb_init(URL);
    # init method test
    res = realdb_init(URL);
    @test BASE_URL == URL

    # GET method test
    realdb_init(URL);
    res = realdb_get("/firebase_test/firebase_get");
    expected =  Dict{String,Any}("first" => "ashwani", "last" => "rathee")
    @test res == expected

    # POST method test
    realdb_init(URL)
    body = Dict("first" => "ashwani", "last" => "rathee")
    res = realdb_post("/firebase_test/firebase_post/post_test", body)
    test_id = res["name"]
    res = realdb_get("/firebase_test/firebase_post/post_test/$test_id");
    expected =  Dict{String,Any}("first" => "ashwani", "last" => "rathee")
    @test res == expected
    realdb_delete("/firebase_test/firebase_post/post_test/$test_id")

    # PUT method test
    realdb_init(URL)
    body = Dict("first" => "ashwani")
    res = realdb_put("/firebase_test/firebase_put", body)
    res = realdb_get("/firebase_test/firebase_put");
    @test res == body
    body = Dict("first" => "ashwani", "last" => "rathee")
    realdb_put("/firebase_test/firebase_put", body)

    # PATCH method test
    realdb_init(URL)
    body = Dict("last" => "rathi")
    realdb_patch("/firebase_test/firebase_patch", body)
    res = realdb_get("/firebase_test/firebase_patch")
    expected = Dict("first" => "ashwani", "last" => "rathi")
    @test res == expected
    body = Dict("first" => "ashwani", "last" => "rathee")
    realdb_put("/firebase_test/firebase_patch", body)

    # DELETE method test
    realdb_init(URL)
    realdb_delete("/firebase_test/firebase_delete")
    res = realdb_get("/firebase_test/firebase_delete")
    @test res === nothing
    body = Dict("first" => "ashwani", "last" => "rathee")
    realdb_put("/firebase_test/firebase_delete", body)
end


text:
# Compare line drawing performance of Matplotlib vs. GR
#
# These are the results on a MacBook Pro 2,6 GHz Intel Core i5::
#
#     fps (mpl):   27
#     fps  (GR):  775
#       speedup:   28.7

import PyPlot

x = collect(0:0.01:2*pi)

tic()
line, = PyPlot.plot(x, sin(x))
for i = 1:200
    line[:set_ydata](sin(x + i / 10.0))
    PyPlot.draw()
    PyPlot.pause(0.0001)
end

fps_mpl = round(200 / toq())
println("fps (mpl): ", fps_mpl)

import GR

tic()
for i = 1:200
    GR.plot(x, sin(x + i / 10.0))
    sleep(0.0001) # unnecessary
end

fps_gr = round(200 / toq())
println("fps  (GR): ", fps_gr)

println(@sprintf("  speedup: %6.1f",  float(fps_gr) / fps_mpl))


text:
# This file is part of the TaylorIntegration.jl package; MIT licensed

using TaylorIntegration
using LinearAlgebra: norm, tr, dot, istriu, diag, I
using Test

@testset "Testing `lyapunov.jl`" begin

    local _order = 28
    local _abstol = 1.0E-20

    #Lorenz system parameters
    local σ = 16.0
    local β = 4.0
    local ρ = 45.92

    #Lorenz system ODE:
    function lorenz!(dx, x, p, t)
        dx[1] = σ*(x[2]-x[1])
        dx[2] = x[1]*(ρ-x[3])-x[2]
        dx[3] = x[1]*x[2]-β*x[3]
        nothing
    end

    #Lorenz system Jacobian (in-place):
    function lorenz_jac!(jac, x, p, t)
        jac[1,1] = -σ+zero(x[1]); jac[1,2] = σ+zero(x[1]); jac[1,3] = zero(x[1])
        jac[2,1] = ρ-x[3]; jac[2,2] = -1.0+zero(x[1]); jac[2,3] = -x[1]
        jac[3,1] = x[2]; jac[3,2] = x[1]; jac[3,3] = -β+zero(x[1])
        nothing
    end

    @testset "Test `stabilitymatrix!`" begin
        t0 = rand() #the initial time
        xi = set_variables("δ", order=1, numvars=3)
        t_ = Taylor1([t0,1],_order)
        δx = Array{TaylorN{Taylor1{Float64}}}(undef, 3)
        dδx = similar(δx)
        jac_auto = Array{Taylor1{Float64}}(undef, 3, 3)
        jac_user = Array{Taylor1{Float64}}(undef, 3, 3)
        _δv = Array{TaylorN{Taylor1{Float64}}}(undef, 3)
        for ind in 1:3
            _δv[ind] = one(t_)*TaylorN(Taylor1{Float64}, ind, order=1)
        end
        # test computation of jac via: autodiff and user-provided Jacobian function
        for i in 1:10
            x0 = 10rand(3) #the initial condition
            x0T = Taylor1.(x0,_order)
            TaylorIntegration.stabilitymatrix!(lorenz!, t_, x0T, δx, dδx, jac_auto, _δv, nothing)
            @test tr(constant_term.(jac_auto)) == -(1+σ+β)
            TaylorIntegration.stabilitymatrix!(lorenz!, t_, x0T, δx, dδx, jac_user, _δv, nothing, lorenz_jac!)
            @test tr(constant_term.(jac_user)) == -(1+σ+β)
            @test jac_user == jac_auto
        end
    end

    @testset "Test `classicalGS!`" begin
        dof = 3
        jt = rand(dof, dof)
        QH = Array{eltype(jt)}(undef, dof, dof)
        RH = Array{eltype(jt)}(undef, dof, dof)
        aⱼ = Array{eltype(jt)}(undef, dof)
        qᵢ = similar(aⱼ)
        vⱼ = similar(aⱼ)
        TaylorIntegration.classicalGS!( jt, QH, RH, aⱼ, qᵢ, vⱼ )
        @test norm(jt-QH*RH, Inf) < 1e-14
        for i in 1:dof
            for j in i:dof
                if j == i
                    @test isapprox( dot(QH[:,i],QH[:,j]), one(eltype(jt)) )
                else
                    @test abs( dot(QH[:,i],QH[:,j]) ) < 1E-10
                end
            end
        end
        @test istriu(RH) #is RH an upper triangular matrix?
        @test prod(diag(RH) .> 0.0) #are the diagonal elements of RH positive?
    end

    @testset "Test Lyapunov spectrum integrator (t0, tmax): Lorenz system" begin
        q0 = [19.0, 20.0, 50.0] #the initial condition
        t0 = 0.0 #the initial time
        tmax = t0+20.0 #final time of integration
        dof = length(q0) # degrees of freedom
        Q0 = Matrix{Float64}(I, dof, dof) # dof x dof identity matrix
        x0 = vcat(q0, reshape(Q0, dof*dof)) # initial conditions: eqs of motion and variationals

        #Taylor1 variables for evaluation of eqs of motion
        t = Taylor1(_order)
        x = Taylor1.(x0, _order)
        dx = similar(x)

        #Calculate trace of Lorenz system Jacobian via TaylorSeries.jacobian:
        xi = set_variables("δ", order=1, numvars=length(q0))
        q0TN = [ q0[1]+xi[1], q0[2]+xi[2], q0[3]+xi[3] ]
        dq0TN = similar(q0TN)
        lorenz!(dq0TN, q0TN, nothing, t0)
        jac_autodiff = TaylorSeries.jacobian(dq0TN) # Jacobian, computed via automatic differentiation
        lorenztr = tr(jac_autodiff) #trace of Lorenz system Jacobian matrix

        @test lorenztr == -(1+σ+β)

        # Compute Jacobian using previously defined `lorenz_jac!` function
        jac = Matrix{Taylor1{eltype(q0)}}(undef, dof, dof)
        lorenz_jac!(jac, x, nothing, t)
        @test tr(constant_term.(jac)) == -(1+σ+β)

        # Jacobian values should be equal
        @test constant_term.(jac) == jac_autodiff

        # Number of TaylorN variables should be equal to length of vector of initial conditions
        xi = set_variables("δ", order=1, numvars=length(q0)-1)
        @test_throws AssertionError lyap_taylorinteg(lorenz!, q0, t0, tmax, _order, _abstol; maxsteps=2)
        xi = set_variables("δ", order=1, numvars=length(q0))

        #Test lyap_taylorinteg with autodiff-computed Jacobian, maxsteps=5
        tv, xv, λv = lyap_taylorinteg(lorenz!, q0, t0, tmax, _order, _abstol, nothing; maxsteps=5)
        @test size(tv) == (6,)
        @test size(xv) == (6,3)
        @test size(λv) == (6,3)

        #Test lyap_taylorinteg with user-defined Jacobian, maxsteps=5
        tv2, xv2, λv2 = lyap_taylorinteg(lorenz!, q0, t0, tmax, _order, _abstol, nothing, lorenz_jac!; maxsteps=5)
        @test size(tv2) == (6,)
        @test size(xv2) == (6,3)
        @test size(λv2) == (6,3)
        @test tv == tv2
        @test xv == xv2
        @test λv == λv2

        #Test lyap_taylorinteg with autodiff-computed Jacobian, maxsteps=2000
        tv, xv, λv = lyap_taylorinteg(lorenz!, q0, t0, tmax, _order, _abstol; maxsteps=2000)
        @test xv[1,:] == q0
        @test tv[1] == t0
        @test size(xv) == size(λv)
        @test isapprox(sum(λv[1,:]), lorenztr) == false
        @test isapprox(sum(λv[end,:]), lorenztr)
        mytol = 1e-4
        @test isapprox(λv[end,1], 1.47167, rtol=mytol, atol=mytol)
        @test isapprox(λv[end,2], -0.00830, rtol=mytol, atol=mytol)
        @test isapprox(λv[end,3], -22.46336, rtol=mytol, atol=mytol)
        # Check integration consistency (orbit should not depend on variational eqs)
        t_, x_ = taylorinteg(lorenz!, q0, t0, tmax, _order, _abstol; maxsteps=2000)
        @test t_ == tv
        @test x_ == xv
        # test backward integration
        tvb, xvb, λvb = lyap_taylorinteg(lorenz!, q0, t0, -tmax, _order, _abstol; maxsteps=2000)
        @test isapprox(sum(λvb[1,:]), lorenztr) == false
        @test isapprox(sum(λvb[end,:]), lorenztr)

        #Test lyap_taylorinteg with user-defined Jacobian, maxsteps=2000
        tv2, xv2, λv2 = lyap_taylorinteg(lorenz!, q0, t0, tmax, _order, _abstol, nothing, lorenz_jac!; maxsteps=2000)
        @test xv2[1,:] == q0
        @test tv2[1] == t0
        @test size(xv2) == size(λv2)
        @test tv == tv2
        @test xv == xv2
        @test λv == λv2
        @test isapprox(sum(λv2[1,:]), lorenztr) == false
        @test isapprox(sum(λv2[end,:]), lorenztr)
        mytol = 1e-4
        @test isapprox(λv2[end,1], 1.47167, rtol=mytol, atol=mytol)
        @test isapprox(λv2[end,2], -0.00830, rtol=mytol, atol=mytol)
        @test isapprox(λv2[end,3], -22.46336, rtol=mytol, atol=mytol)
        # Check integration consistency (orbit should not depend on variational eqs)
        @test t_ == tv2
        @test x_ == xv2
        # test backward integration
        tv2b, xv2b, λv2b = lyap_taylorinteg(lorenz!, q0, t0, -tmax, _order, _abstol, nothing, lorenz_jac!; maxsteps=2000)
        @test isapprox(sum(λv2b[1,:]), lorenztr) == false
        @test isapprox(sum(λv2b[end,:]), lorenztr)
    end

    @testset "Test Lyapunov spectrum integrator (trange): Lorenz system" begin
        q0 = [19.0, 20.0, 50.0] #the initial condition
        t0 = 0.0 #the initial time
        tmax = t0+20.0 #final time of integration

        #Calculate trace of Lorenz system Jacobian via TaylorSeries.jacobian:
        xi = set_variables("δ", order=1, numvars=length(q0))
        q0TN = q0+xi
        dq0TN = similar(q0TN)
        lorenz!(dq0TN, q0TN, nothing, t0)
        lorenztr = tr(TaylorSeries.jacobian(dq0TN)) #trace of Lorenz system Jacobian matrix
        @test lorenztr == -(1+σ+β)

        trange = t0:1.0:tmax
        xw, λw = lyap_taylorinteg(lorenz!, q0, trange, _order, _abstol; maxsteps=5)
        @test xw[1,:] == q0
        @test size(xw) == (length(trange), 3)
        @test λw[1,:] == zeros(3)
        @test size(λw) == size(xw)
        @test all(isnan.(xw[2:end,:]))
        @test all(isnan.(λw[2:end,:]))
        xw2, λw2 = lyap_taylorinteg(lorenz!, q0, vec(trange), _order, _abstol; maxsteps=5)
        @test xw[1, :] == xw2[1, :]
        @test all(isnan.(xw2[2:end,:]))
        @test λw2[1, :] == zeros(3)
        @test all(isnan.(λw2[2:end,:]))
        @test size(xw2) == (length(trange), 3)
        @test size(λw2) == size(xw2)
        xw_, λw_ = lyap_taylorinteg(lorenz!, q0, trange, _order, _abstol, nothing, lorenz_jac!; maxsteps=5)
        @test xw_[1,:] == q0
        @test all(isnan.(xw_[2:end,:]))
        @test size(xw_) == (length(trange), 3)
        @test size(λw_) == size(xw_)
        @test λw_[1, :] == zeros(3)
        @test all(isnan.(λw_[2:end,:]))
        xw2_, λw2_ = lyap_taylorinteg(lorenz!, q0, vec(trange), _order, _abstol, nothing, lorenz_jac!; maxsteps=5)
        @test xw_[1, :] == xw2_[1, :]
        @test all(isnan.(xw2_[2:end,:]))
        @test λw2_[1, :] == zeros(3)
        @test all(isnan.(λw2_[2:end,:]))
        @test size(xw2_) == (length(trange), 3)
        @test size(λw2_) == size(xw2_)

        xw, λw = lyap_taylorinteg(lorenz!, q0, trange, _order, _abstol; maxsteps=2000)
        @test xw[1,:] == q0
        @test size(xw) == (length(trange), length(q0))
        @test size(λw) == (length(trange), length(q0))
        @test isapprox(sum(λw[1,:]), lorenztr) == false
        @test isapprox(sum(λw[end,:]), lorenztr)
        tz, xz, λz = lyap_taylorinteg(lorenz!, q0, trange[1], trange[end], _order, _abstol; maxsteps=2000)
        @test λw[end,:] == λz[end,:]
        @test xw[end,:] == xz[end,:]
        @test tz[end] == trange[end]
        mytol = 1e-4
        @test isapprox(λw[end,1], 1.47167, rtol=mytol, atol=mytol)
        @test isapprox(λw[end,2], -0.00831, rtol=mytol, atol=mytol)
        @test isapprox(λw[end,3], -22.46336, rtol=mytol, atol=mytol)
        xw_, λw_ = lyap_taylorinteg(lorenz!, q0, trange, _order, _abstol, nothing, lorenz_jac!; maxsteps=2000)
        @test xw == xw_
        @test λw == λw_
        tz_, xz_, λz_ = lyap_taylorinteg(lorenz!, q0, trange[1], trange[end], _order, _abstol, nothing, lorenz_jac!; maxsteps=2000)
        @test λw_[end,:] == λz_[end,:]
        @test xw_[end,:] == xz_[end,:]
        @test tz_[end] == trange[end]

        xw2, λw2 = lyap_taylorinteg(lorenz!, q0, vec(trange), _order, _abstol; maxsteps=2000)
        @test xw2 == xw
        @test λw2 == λw
        @test xw2[1,:] == q0
        @test size(xw) == (length(trange), length(q0))
        @test isapprox(sum(λw2[1,:]), lorenztr) == false
        @test isapprox(sum(λw2[end,:]), lorenztr)
        tz2, xz2, λz2 = lyap_taylorinteg(lorenz!, q0, trange[1], trange[end], _order, _abstol; maxsteps=2000)
        @test λw2[end,:] == λz2[end,:]
        @test xw2[end,:] == xz2[end,:]
        @test tz2[end] == trange[end]
        mytol = 1e-4
        @test isapprox(λw2[end,1], 1.47167, rtol=mytol, atol=mytol)
        @test isapprox(λw2[end,2], -0.00831, rtol=mytol, atol=mytol)
        @test isapprox(λw2[end,3], -22.46336, rtol=mytol, atol=mytol)
        xw2_, λw2_ = lyap_taylorinteg(lorenz!, q0, vec(trange), _order, _abstol, nothing, lorenz_jac!; maxsteps=2000)
        @test xw2 == xw2_
        @test λw2 == λw2_
        tz2_, xz2_, λz2_ = lyap_taylorinteg(lorenz!, q0, trange[1], trange[end], _order, _abstol, nothing, lorenz_jac!; maxsteps=2000)
        @test λw2_[end,:] == λz2_[end,:]
        @test xw2_[end,:] == xz2_[end,:]
        @test tz2_[end] == trange[end]

        # Check integration consistency (orbit should not depend on variational eqs)
        x_ = taylorinteg(lorenz!, q0, vec(trange), _order, _abstol; maxsteps=2000)
        @test x_ == xw2

    end

end


text:
# for Facto.jl

##a grammer for english
const is=:is
const there=:there
const are=:are
const have=:have
const give=:give
const put=:put

const love=:love
const like=:like
const want=:want
const realize=:realize
const make=:make
const become=:become
const talk=:talk
const live=:live
const eat=:eat

const who=:who
const what=:what
const where=:where
const when=:when

max_stars_repo_name: sazare/cheapViews

max_issues_repo_name: sazare/cheapViews

max_forks_repo_name: sazare/cheapViews


text:
module Datasets

using UCI
using MLDatasets
using DataDeps
using StatsBase
using DelimitedFiles
using Random
using CSV
using DataFrames
using Flux # for one-hot encoding

export load_data

include("tabular.jl")
include("img.jl")
include("basics.jl")

end


text:
export save_fft, load_fft, load_corr, save_corr

"""
    save_fft(F::FFTData, OUT::String)

Save FFTData `F` to JLD2.
"""
function save_fft(F::FFTData, FFTOUT::String)
    # check if FFTDIR exists
    FFTOUT = expanduser(FFTOUT)
    if isdir(FFTOUT) == false
        mkpath(FFTOUT)
    end

    # check if F is on the GPU
    if isa(F.fft,AbstractGPUArray)
        F = F |> cpu
    end

    # create JLD2 file and save FFT
    net,sta,loc,chan = split(F.name,'.')
    filename = joinpath(FFTOUT,"$(F.name).jld2")
    file = jldopen(filename, "a+")
    if !(chan in keys(file))
        group = JLD2.Group(file, chan)
        group[F.id] = F
    else
        file[chan][F.id] = F
    end
    close(file)
end

"""

  load_fft(filename,chan,day=day)

Loads FFTData for channel `chan` from JLD2 file `filename`.
If day is specified, loads data from day `day`, else
loads data from all days of `chan`.
"""
function load_fft(filename::String,chan::String;day::Union{String,Missing}=missing)
    file = jldopen(filename,"r")
    if ismissing(day)
        days = keys(file[chan])
        num_days  = length(days)

        # get sizes of all corrs
        sizes = Array{Int}(undef,num_days,2)

        for ii = 1:num_days
            sizes[ii,:]=collect(size(file[chan][days[ii]].fft))
        end

        # create empty arrays and get indicies
        M = sizes[1]
        N = sum(sizes[:,2])
        ffts = Array{eltype(file[chan][days[1]].fft)}(undef,M,N)
        t = Array{eltype(file[chan][days[1]].t)}(undef,N)
        starts = cumsum(vcat(1 ,sizes[:,2]))[1:end-1]
        ends = cumsum(sizes[:,2])
        for ii = 1:num_days
            ffts[:,starts[ii]:ends[ii]] = file[chan][days[ii]].fft
            t[starts[ii]:ends[ii]] = file[chan][days[ii]].t
        end

        # add data to FFTData
        F = file[chan][days[1]]
        F.t = t
        F.fft = ffts
    else
        F = file[chan][day]
    end
    close(file)
    return F
end

"""
    save_corr(C::CorrData, OUT::String)

Save CorrData `C` to JLD2.
"""
function save_corr(C::CorrData, CORROUT::String)
    # check if CORRDIR exists
    CORROUT = expanduser(CORROUT)
    if isdir(CORROUT) == false
        mkpath(CORROUT)
    end

    # check if C is on the GPU
    if isa(C.corr,AbstractGPUArray)
        C = C |> cpu
    end

    # create JLD2 file and save correlation
    filename = joinpath(CORROUT,"$(C.name).jld2")
    file = jldopen(filename, "a+")
    if !(C.comp in keys(file))
        group = JLD2.Group(file, C.comp)
        group[C.id] = C
    else
        file[C.comp][C.id] = C
    end
    close(file)
end

"""

  load_corr(filename,chan,day=day)

Loads CorrData for channel `chan` on day `day` from JLD2 file `filename`.
"""
function load_corr(filename::String,chan::String;
                   day::Union{String,Missing}=missing)
    file = jldopen(filename,"r")
    if ismissing(day) # load all files
        days = keys(file[chan])
        num_days  = length(days)

        # get sizes of all corrs
        sizes = Array{Int}(undef,num_days,2)

        for ii = 1:num_days
            sizes[ii,:]=collect(size(file[chan][days[ii]].corr))
        end

        # create empty arrays and get indicies
        M = sizes[1]
        N = sum(sizes[:,2])
        corrs = Array{eltype(file[chan][days[1]].corr)}(undef,M,N)
        t = Array{eltype(file[chan][days[1]].t)}(undef,N)
        starts = cumsum(vcat(1 ,sizes[:,2]))[1:end-1]
        ends = cumsum(sizes[:,2])
        for ii = 1:num_days
            corrs[:,starts[ii]:ends[ii]] = file[chan][days[ii]].corr
            t[starts[ii]:ends[ii]] = file[chan][days[ii]].t
        end

        # add data to CorrData
        C = file[chan][days[1]]
        C.t = t
        C.corr = corrs
    else # return single day
        C = file[chan][day]
    end
    close(file)
    return C
end


text:
#
# This file is the aerodynamic drag calculations using "Zero Lift Drag"
# Individual components are calculated and the drags combined for an approximation
# This calculation is currently using the wing as well as the empennage.
# Wing lift induced drag will also be included so adds a velocity variable to the solution.
# Interference & excrescence drag are approximated using a factor based on the aircraft type.
# In this case = 1.15, which is slightly better than the general aviation factor of 1.2 - 1.3.
#
# This code is licensed under MIT license (see LICENSE.txt for details)
#
# Dec 12, 2021 		Initial code
#

using Plots

#						Mach	A		B	  R^2
Skin_friction_coeff =  [0.0	0.0391	-0.157	0.9967
						0.3	0.0399	-0.159	0.9976
						0.7	0.0392	-0.16	0.9971
						0.9	0.0376	-0.159	0.9965
						1.0	0.0381	-0.161	0.9970
						1.5	0.0371	-0.164	0.9966
						2.0	0.0329	-0.162	0.9941
						2.5	0.0286	-0.161	0.9944
						3.0	0.0261	-0.161	0.9936]

						Mach_indexes = [0.0, 0.3, 0.7, 0.9, 1.0, 1.5, 2.0, 2.5, 3.0]

# dataframe for keeping track of (book-keeping)drag variables
drag_vars = DataFrame(
		Velocity			=	Float32[],
		Cd_fuselage 		=	Float32[],
		Cd_wing 			=	Float32[],
		Cd_horizontal_tail	=	Float32[],
		Cd_vertical_tail	=	Float32[],
		Cd0_total			=	Float32[]
	)

#  interference + excrescence drag coefficient
interference_drag = 1.15

	#------------------------------ Find the Nearest Value in an Array) ------------------------------#
function findnearest(a,x)
   idx = searchsortedfirst(a,x)
   if (idx==1); return idx; end
   if (idx>length(a)); return length(a); end
   if (a[idx]==x); return idx; end
   if (abs(a[idx]-x) < abs(a[idx-1]-x))
      return idx
   else
      return idx-1
   end
end

	#------------------------------ Drag Simulation Wrapper (drag_sim) ------------------------------#
	#  These calculations assume "zero lift drag method"
	#  Itetrates through various temperatures and fills out a matrix of constituient drags
	#  of various components

function drag_sim(df)

	# define structures to hold definitions for the call: (name, value, units, stored)
	SVstart  	= req_vars("SVstart",  	0, "m/s", 	0)	# Lowest simulation velocity
	SVend	   	= req_vars("SVend",		0, "m/s",	0)	# Highest simulation velocity
	SVincr	   	= req_vars("SVincr",	0, "m/s",	0)	# Loop increment

	var_array = [SVstart, SVend, SVincr]				# Array of structures to enable easy iteration

	Extract_data(df, var_array)							# Loads the array of structures with data, checks units and presence

	# loop through the various velocities
	for V in SVstart.value:SVincr.value:SVend.value
            f_drag = fuselage_drag(df, V)
			w_drag = wing_drag(df, V)
			h_tail = 3.0
			v_tail = 4.0
			Cd0_total = (f_drag + w_drag + h_tail + v_tail) * interference_drag

		# write the results to the storage matrix
		push!(drag_vars,[V*2.237, f_drag, w_drag, h_tail, v_tail, Cd0_total])  ### temporarily changing velocity to MPH
       end

###----------------- some temporary display values to check results
# print("\n\n", drag_vars)
display(plot(drag_vars[:,1], drag_vars[:,2])); sleep(30)

end

	#---------------------------- Fuselage drag component (fuselage_drag) --------------------------#
	#	lf = fuselage length
	#	dfuse = maximum fuselage diameter
	#	Swetf = total fuselage surface area exposed to air
	#	Sw = total wing area

function fuselage_drag(df, V)

	# define structures to hold definitions for the call: (name, value, units, stored)
	Sw  	= req_vars("Sw",  		0, 	"m^2", 	0)			# wing area
	Swetf 	= req_vars("Swetf",  	0, 	"m^2", 	0)			# fuselage wet area
	dfuse	= req_vars("dfuse",		0, 	"m", 	0)			# fuselage maximum diameter
	lf		= req_vars("lf",  		0, 	"m", 	0)			# fuselage maximum length

	var_array = [Sw, Swetf, dfuse, lf]						# Array of structures to enable easy iteration
	Extract_data(df, var_array)								# Loads the array of structures with data, checks units and presence

	Cf_fuse = Cf(df, V)										# calculate the skin friction coefficient
	df_equiv = sqrt((4/π)*dfuse.value)						# factor assumes a non-circular fuselage cross section

	Cd0_fueslage = Cf_fuse * ((1 + 60/(lf.value/df_equiv)^3) + (0.0025 * (lf.value/df_equiv))) * (Swetf.value/Sw.value)

	return (Cd0_fueslage)
end

	#--------------------------------  Wing drag component (wing_drag) -----------------------------#

function wing_drag(df, V)
			return (2)
end

	#------------------------------------- Speed of Sound (c) --------------------------------------#

function c(df)

	# define structures to hold definitions for the call: (name, value, units, stored)
	T  	   = req_vars("T",  0, 	"C", 	0)			# T = Temperature

	var_array = [T]									# Array of structures to enable easy iteration

	Extract_data(df, var_array)						# Loads the array of structures with data, checks units and presence

	return ((331.3 * sqrt(1 + T.value/273.15)))

end

	#--------------------------------- Skin Friction Coefficient (Cf) --------------------------------#
	#									Cf = A * Re^B

function Cf(df, V)

	lf		= req_vars("lf",  		0, 	"m", 	0)			# fuselage maximum length
	var_array = [lf]										# Array of structures to enable easy iteration
	Extract_data(df, var_array)								# Loads the array of structures with data, checks units and presence

	Re_number = Re(V, lf.value)								# calculatealculate the fuselage reynolds number for velocity
	speed_of_sound = c(df)									# calculate the speed of sound for the temperature
	Mach_no = V / speed_of_sound							# calculate the mach number
	Mach_nearest = findnearest(Mach_indexes, Mach_no)		# find the index of the closest fit in the mach table

	Cf = Skin_friction_coeff[Mach_nearest,2] * Re_number^Skin_friction_coeff[Mach_nearest,3]
	return (Cf)

end


text:
#!/usr/bin/env julia

using neten
using Test
using BenchmarkTools

@testset "neten.jl" begin
    # Write your tests here.
	#hello()
	adjm = data("butterfly")
	netw = enhance(adjm)
end

# timed test
adjm = data("butterfly");
@benchmark enhance(adjm)


text:
# We thank QuTiP developers for sharing the notebook on Jaynes Cummings model,
# https://github.com/qutip/qutip-notebooks/blob/master/examples/example-JC-model-wigner-function.ipynb
# this is a direct translation of QuTiP version to solving in Julia using
# QuDynamics.

using QuBase
using QuDynamics

function jc_setup(N, wc, wa, g, kappa, gamma, use_rwa)
    # Hamiltonian
    idc = QuArray(eye(N))
    ida = QuArray(eye(2))
    a  = tensor(lowerop(N), ida)
    sm = tensor(idc, lowerop(2))
    if use_rwa
        # use the rotating wave approxiation
        H = wc * a' * a + wa * sm' * sm + g * (a' * sm + a * sm')
    else
        H = wc * a' * a + wa * sm' * sm + g * (a' + a) * (sm + sm')
    end
    # collapse operators
    c_op_list = Array(QuBase.AbstractQuMatrix, 0)
    n_th_a = 0.0
    rate = kappa * (1 + n_th_a)
    if rate > 0.0
       push!(c_op_list, full(sqrt(rate) * a))
    end
    rate = kappa * n_th_a
    if rate > 0.0
        push!(c_op_list, full(sqrt(rate) * a'))
    end
    rate = gamma
    if rate > 0.0
        push!(c_op_list, full(sqrt(rate) * sm))
    end
    return full(H), c_op_list
end

# parameters
wc = 1.0 * 2 * pi   # cavity frequency
wa = 1.0 * 2 * pi   # atom frequency
g  = 0.05 * 2 * pi  # coupling strength
kappa = 0.05        # cavity dissipation rate
gamma = 0.15        # atom dissipation rate
N = 2           # number of cavity fock states
use_rwa = true

# initial state
psi0 = complex(tensor(statevec(1, FiniteBasis(N)), statevec(2, FiniteBasis(2))))

tlist = 0.:0.1:2*pi

H, c_ops = jc_setup(N, wc, wa, g, kappa, gamma, use_rwa)

qprop = QuPropagator(H, c_ops, psi0*psi0', tlist, QuODE45())

# Calculating the expectation values for observable a'*a
a = tensor(lowerop(2), QuArray(eye(2)))
op = a'*a
for (t, rho) in qprop
    println("t = $t : <a'*a> = $(trace(rho*op))")
end


text:
"""
    Poll(p::DSProblem{T})::IterationOutcome where T

Generate points and call evaluate on them.
"""
function Poll(p::DSProblem{T})::IterationOutcome where T
    p.status.poll_time_total += @elapsed points = GeneratePollPoints(p, p.config.mesh)
    return EvaluatePoint!(p, points)
end

"""
    GeneratePollPoints(p::DSProblem{T}, ::AbstractMesh)::Vector{Vector{T}} where T

Generate a set of directions with the configured polling algorithm, then return
the set of points these directions give from the incumbent points.
"""
function GeneratePollPoints(p::DSProblem{T}, ::AbstractMesh)::Vector{Vector{T}} where T
    points = Vector{T}[]
    dirs = GenerateDirections(p)

    directions = []

    if !isnothing(p.x)
        for i=1:size(dirs,2)
            d = dirs[:,i]
            push!(points, SafePointGeneration(p.x, d, p.config.mesh))
            push!(directions, d)
        end
    end
    if !isnothing(p.i)
        for i=1:size(dirs,2)
            d = dirs[:,i]
            push!(points, SafePointGeneration(p.i, d, p.config.mesh))
            push!(directions, d)
        end
    end

    p.status.directions = directions

    p.full_output && OutputPollStep(points, dirs)

    return points
end

"""
    SafePointGeneration(x::Vector{T}, d::Vector{T}, m::Mesh{T})::Vector{T} where T

Generate a trial point around incumbent point `x`, using direction `d`, and removing
the potential low-level computational error for granular variables.
"""
(SafePointGeneration(x::Vector{T}, d::Vector{T}, m::Mesh{T})::Vector{T}) where T = SafePointGeneration(x, d, m.δ, m.digits)
function SafePointGeneration(x::Vector{T}, d::Vector{T}, δ::Vector{T}, digits::Vector{Union{Int, Nothing}})::Vector{T} where T
    result = []

    for i=1:length(x)
        single_result = x[i] + (d[i] * δ[i])
        push!(result, isnothing(digits[i]) ? single_result : round(single_result, digits=digits[i]))
    end
    return result
end

"""
    ScaleDirection(p::DSProblem, dir::Vector{T}) where T

Scale the direction using the mesh ratio vector ρ
"""
function ScaleDirection(p::DSProblem, dir::Vector{T}) where T
    infNorm = maximum(abs.(dir))

    if infNorm == 0
        error("Unexpected error. Poll algorithm generated a direction equal to zero.")
    end

    d_scaled = dir ./ infNorm

    return round.(d_scaled .* p.config.mesh.ρ)
end

"""
    GenerateDirections(p::DSProblem)

Generate a set of directions with the configured polling algorithm.
"""
function GenerateDirections(p::DSProblem)
    directions = GenerateDirections(p, p.config.poll)
    return mapslices(dir -> ScaleDirection(p, dir), directions, dims = 1)
end

Name(::AbstractPoll) = "Unknown poll type"


text:
using NetworkProtocols
using Sockets
using Test

include("test_data.jl")

@testset "NetworkProtocols.jl" begin
    include("mac_address_tests.jl")
    include("ethernet_tests.jl")
    include("ip_tests.jl")
    include("udp_tests.jl")
    include("dispatch_tests.jl")
end

max_stars_repo_name: ancapdev/NetworkProtocols

max_issues_repo_name: ancapdev/NetworkProtocols


text:
function tinterval(x, δ, op)
    μ = mean(x)
    σ = std(x, mean=μ)
    t = 0.0
    n = length(x)
    Zygote.ignore() do
        t = quantile(TDist(n-1), δ)
    end
    s = t * σ / √n
    return op(μ,s)
end

function tdist_ci(;δ=0.05, tail=:both)
    if tail == :right
        return x->tinterval(x,δ,(μ,s)->μ-s) # quantile of 0.05 is negative so need to subtract to get upper
    elseif tail == :left
        return x->tinterval(x,δ,(μ,s)->μ+s) # need to add to get lower bound
    else
        return x->tinterval(x,δ/2,(μ,s)->(μ+s,μ-s))
    end
end

function andersons_ucb(x, b, δ)
    n = length(x)
    ϵ = √(log(1/δ)/(2n))
    r = floor(Int, n*ϵ)
    upper = (1/n) * ((r+1)*x[r+1] + sum(view(x, r+1:n))) + ϵ * (b - x[r+1])
    return upper
end

function andersons_lcb(x, a, δ)
    n = length(x)
    ϵ = √(log(1/δ)/(2n))
    s = floor(Int, n*ϵ)
    lower = (1/n) * (sum(view(x, 1:n-s-1)) + (s+1)*x[n-s]) - ϵ * (x[n-s] - a)
    return lower
end

function andersons_ci(a, b; δ=0.05, tail=:both)
    if tail == :both
        function f(x)
            z = sort(x)
            upper = andersons_ucb(z, b, δ/2)
            lower = andersons_lcb(z, a, δ/2)
            return lower, upper
        end
        return f
    elseif tail == :left
        return x->andersons_lcb(sort(x), a, δ)
    elseif tail == :right
        return x->andersons_ucb(sort(x), b, δ)
    else
        throw(ArgumentError("tail=$(tail) is invalid"))
    end
end


text:
## SparseTensorCOO: coordinate format

@testset "constructor" begin
    @testset "N=$N, Ti=$Ti, Tv=$Tv" for N in 1:3, Ti in [Int, UInt8], Tv in [Float64, BigFloat, Int8]
        dims = (5, 3, 2)[1:N]
        inds = (Ti[2, 1, 4], Ti[1, 3, 2], Ti[1, 2, 1])[1:N]
        inds = tuple.(inds...)
        vals = Tv[1, 0, 10]
        perm = sortperm(inds; by = CartesianIndex)
        sinds, svals = inds[perm], vals[perm]

        # SparseTensorCOO(dims, inds, vals) - sorted
        A = SparseTensorCOO(dims, sinds, svals)
        @test typeof(A) === SparseTensorCOO{Tv,Ti,N}
        @test A.dims === dims
        @test A.inds === sinds && A.vals === svals

        # SparseTensorCOO(dims, inds, vals) - unsorted
        A = SparseTensorCOO(dims, inds, vals)
        @test typeof(A) === SparseTensorCOO{Tv,Ti,N}
        @test A.dims === dims
        @test A.inds == sinds && A.vals == svals

        # check_Ti(dims, Ti)
        @test_throws ArgumentError SparseTensorCOO{Tv,Ti,N}((-1, 3, 2)[1:N], sinds, svals)
        if Ti !== Int
            @test_throws ArgumentError SparseTensorCOO{Tv,Ti,N}((Int(typemax(Ti)) + 1, 3, 2)[1:N], sinds, svals)
        end
        if N >= 2
            @test_throws ArgumentError SparseTensorCOO{Tv,Ti,N}((typemax(Int) ÷ 2, 3, 2)[1:N], sinds, svals)
        end

        # check_coo_buffers(inds, vals)
        @test_throws ArgumentError SparseTensorCOO{Tv,Ti,N}(dims, sinds, svals[1:end-1])

        # check_coo_inds(dims, inds) - index in bounds
        if Ti <: Signed
            badinds = (Ti[2, -1, 4], Ti[1, 3, 2], Ti[1, 2, 1])[1:N]
            badinds = sort(tuple.(badinds...); by = CartesianIndex)
            @test_throws ArgumentError SparseTensorCOO{Tv,Ti,N}(dims, badinds, vals)
        end
        badinds = (Ti[2, 1, 6], Ti[1, 3, 2], Ti[1, 2, 1])[1:N]
        badinds = sort(tuple.(badinds...); by = CartesianIndex)
        @test_throws ArgumentError SparseTensorCOO{Tv,Ti,N}(dims, badinds, vals)

        # check_coo_inds(dims, inds) - indices sorted
        @test_throws ArgumentError SparseTensorCOO{Tv,Ti,N}(dims, inds, vals)

        # check_coo_inds(dims, inds) - indices unique
        @test_throws ArgumentError SparseTensorCOO{Tv,Ti,N}(dims, [sinds[1:1]; sinds], [svals[1:1]; svals])
    end
end

@testset "undef constructors" begin
    @testset "N=$N, Ti=$Ti, Tv=$Tv" for N in 1:3, Ti in [Int, UInt8], Tv in [Float64, BigFloat, Int8]
        dims = (5, 3, 2)[1:N]

        # SparseTensorCOO{Tv,Ti,N}(undef, dims)
        A = SparseTensorCOO{Tv,Ti,N}(undef, dims)
        @test typeof(A) === SparseTensorCOO{Tv,Ti,N}
        @test A.dims === dims
        @test A.inds == Vector{NTuple{N,Ti}}()
        @test A.vals == Vector{Tv}()

        # SparseTensorCOO{Tv,Ti}(undef, dims)
        A = SparseTensorCOO{Tv,Ti}(undef, dims)
        @test typeof(A) === SparseTensorCOO{Tv,Ti,N}
        @test A.dims === dims
        @test A.inds == Vector{NTuple{N,Ti}}()
        @test A.vals == Vector{Tv}()
    end
end

@testset "AbstractArray constructor" begin
    @testset "N=$N, Ti=$Ti, Tv=$Tv" for N in 1:3, Ti in [Int, UInt8], Tv in [Float64, BigFloat, Int8]
        dims = (5, 3, 2)[1:N]
        inds = (Ti[2, 1, 4], Ti[1, 3, 2], Ti[1, 2, 1])[1:N]
        inds = sort(tuple.(inds...); by=CartesianIndex)
        vals = Tv[1, 100, 10]

        # SparseTensorCOO(Ti, A::Array)
        A = zeros(Tv, dims)
        A[CartesianIndex.(inds)] = vals
        C = SparseTensorCOO(Ti, A)
        @test typeof(C) === SparseTensorCOO{Tv,Ti,N}
        @test C.dims === dims
        @test C.inds == inds && C.vals == vals

        # SparseTensorCOO(Ti, A::SparseTensorDOK)
        D = SparseTensorDOK(dims, Dict(inds .=> vals))
        C = SparseTensorCOO(Ti, D)
        @test typeof(C) === SparseTensorCOO{Tv,Ti,N}
        @test C.dims === dims
        @test C.inds == inds && C.vals == vals
    end
end

## Minimal AbstractArray interface

@testset "size" begin
    @testset "N=$N" for N in 1:3
        dims = (5, 3, 2)[1:N]
        inds = ([2, 1, 4], [1, 3, 2], [1, 2, 1])[1:N]
        vals = [1, 0, 10]
        A = SparseTensorCOO(dims, tuple.(inds...), vals)

        @test size(A) == dims
        for k in 1:N
            @test size(A, k) == dims[k]
        end
        @test size(A, N + 1) == 1
    end
end

@testset "getindex" begin
    @testset "N=$N, Ti=$Ti, Tv=$Tv" for N in 1:3, Ti in [Int, UInt8], Tv in [Float64, BigFloat, Int8]
        dims = (6, 3, 2)[1:N]
        inds = (Ti[5, 1, 4], Ti[1, 3, 2], Ti[1, 2, 1])[1:N]
        vals = Tv[1, 0, 10]
        A = SparseTensorCOO(dims, tuple.(inds...), vals)

        # in bounds
        ind_stored    = (4, 2, 1)[1:N]
        ind_notstored = (3, 2, 2)[1:N]
        @test typeof(A[ind_stored...]) === Tv && A[ind_stored...] == Tv(10)
        @test typeof(A[ind_notstored...]) === Tv && A[ind_notstored...] == zero(Tv)

        # out of bounds
        ind_out1 = (0, 1, 1)[1:N]
        ind_out2 = (7, 3, 2)[1:N]
        @test_throws BoundsError A[ind_out1...]
        @test_throws BoundsError A[ind_out2...]
    end
end

@testset "setindex!" begin
    @testset "N=$N, Ti=$Ti, Tv=$Tv" for N in 1:3, Ti in [Int, UInt8], Tv in [Float64, BigFloat, Int8]
        dims = (6, 3, 2)[1:N]
        inds = (Ti[5, 1, 4], Ti[1, 3, 2], Ti[1, 2, 1])[1:N]
        inds = tuple.(inds...)
        vals = Tv[1, 0, 10]
        perm = sortperm(inds; by = CartesianIndex)
        sinds, svals = inds[perm], vals[perm]

        for val in [-4, 0]
            # store new value in middle
            ind = (3, 2, 2)[1:N]
            A = SparseTensorCOO(dims, copy(inds), copy(vals))
            A[ind...] = val
            @test typeof(A) === SparseTensorCOO{Tv,Ti,N}
            @test A.dims === dims
            if iszero(val)
                @test A.inds == sinds && A.vals == svals
            else
                newperm = sortperm([inds; [ind]]; by = CartesianIndex)
                @test A.inds == [inds; [ind]][newperm] && A.vals == [vals; [val]][newperm]
            end

            # store new value at end
            ind = dims
            A = SparseTensorCOO(dims, copy(inds), copy(vals))
            A[ind...] = val
            @test typeof(A) === SparseTensorCOO{Tv,Ti,N}
            @test A.dims === dims
            if iszero(val)
                @test A.inds == sinds && A.vals == svals
            else
                @test A.inds == [sinds; [ind]] && A.vals == [svals; [val]]
            end

            # overwrite existing value
            ind = (4, 2, 1)[1:N]
            A = SparseTensorCOO(dims, copy(inds), copy(vals))
            A[ind...] = val
            @test typeof(A) === SparseTensorCOO{Tv,Ti,N}
            @test A.dims === dims
            @test A.inds == sinds && A.vals == [svals[1:1]; [val]; svals[3:3]]
        end

        # out of bounds
        ind_out1 = (0, 1, 1)[1:N]
        ind_out2 = (7, 3, 2)[1:N]
        A = SparseTensorCOO(dims, inds, vals)
        @test_throws BoundsError A[ind_out1...] = 0
        @test_throws BoundsError A[ind_out2...] = 0
    end

    # properly handle error during value conversion
    dims = (5, 3, 2)
    inds = sort(tuple.([2, 1, 4], [1, 3, 2], [1, 2, 1]); by = CartesianIndex)
    vals = [1, 0, 10]
    A = SparseTensorCOO(dims, copy(inds), copy(vals))
    @test_throws InexactError A[1, 1, 1] = 1.2
    @test A.inds !== inds && A.vals !== vals
    @test A.inds == inds
    @test A.vals == vals
end

@testset "IndexStyle" begin
    @testset "N=$N, Ti=$Ti, Tv=$Tv" for N in 1:3, Ti in [Int, UInt8], Tv in [Float64, BigFloat, Int8]
        @test IndexStyle(SparseTensorCOO{Tv,Ti,N}) === IndexCartesian()
    end
end

## Overloads for specializing outputs

@testset "similar" begin
    @testset "N=$N, Ti=$Ti, Tv=$Tv" for N in 1:3, Ti in [Int, UInt8], Tv in [Float64, BigFloat, Int8]
        dims = (5, 3, 2)[1:N]
        inds = (Ti[2, 1, 4], Ti[1, 3, 2], Ti[1, 2, 1])[1:N]
        vals = Tv[1, 0, 10]
        A = SparseTensorCOO(dims, tuple.(inds...), vals)

        # similar(A)
        S = similar(A)
        @test typeof(S) === SparseTensorCOO{Tv,Ti,N}
        @test S.dims === dims
        @test isempty(S.inds) && isempty(S.vals)

        # similar(A, ::Type{S})
        for TvNew in [UInt8]
            S = similar(A, TvNew)
            @test typeof(S) === SparseTensorCOO{TvNew,Ti,N}
            @test S.dims === dims
            @test isempty(S.inds) && isempty(S.vals)
        end

        # similar(A, dims::Dims)
        for dimsNew in [(2,), (2, 4), (2, 4, 3)]
            S = similar(A, dimsNew)
            @test typeof(S) === SparseTensorCOO{Tv,Ti,length(dimsNew)}
            @test S.dims === dimsNew
            @test isempty(S.inds) && isempty(S.vals)
        end

        # similar(A, ::Type{S}, dims::Dims)
        for TvNew in [UInt8], dimsNew in [(2,), (2, 4), (2, 4, 3)]
            S = similar(A, TvNew, dimsNew)
            @test typeof(S) === SparseTensorCOO{TvNew,Ti,length(dimsNew)}
            @test S.dims === dimsNew
            @test isempty(S.inds) && isempty(S.vals)
        end
    end
end

## Overloads for improving efficiency

@testset "iterate" begin
    @testset "N=$N, Ti=$Ti, Tv=$Tv" for N in 1:3, Ti in [Int, UInt8], Tv in [Float64, BigFloat, Int8]
        dims = (5, 3, 2)[1:N]
        inds = (Ti[2, 4], Ti[1, 2], Ti[1, 2])[1:N]
        inds = tuple.(inds...)
        vals = Tv[1, 0]
        A = SparseTensorCOO(dims, inds, vals)

        # iterate(A)
        item, state = iterate(A)
        idxstate, nextptr = state
        @test typeof(item) === Tv && item == zero(Tv)
        @test idxstate == (eachindex(A), eachindex(A)[1])
        @test nextptr == 1

        # iterate(A, state) - stored nonzero
        item, state = iterate(A, state)
        idxstate, nextptr = state
        @test typeof(item) === Tv && item == vals[1]
        @test idxstate == (eachindex(A), eachindex(A)[2])
        @test nextptr == 2

        # iterate(A, state) - unstored entries between stored entries
        for itr in LinearIndices(A)[inds[1]...]+1:LinearIndices(A)[inds[2]...]-1
            item, state = iterate(A, state)
            idxstate, nextptr = state
            @test typeof(item) === Tv && item == zero(Tv)
            @test idxstate == (eachindex(A), eachindex(A)[itr])
            @test nextptr == 2
        end

        # iterate(A, state) - stored zero
        item, state = iterate(A, state)
        idxstate, nextptr = state
        @test typeof(item) === Tv && item == vals[2]
        @test idxstate == (eachindex(A), eachindex(A)[LinearIndices(A)[inds[2]...]])
        @test nextptr == 3

        # iterate(A, state) - unstored entries after stored entries
        for itr in LinearIndices(A)[inds[2]...]+1:length(A)
            item, state = iterate(A, state)
            idxstate, nextptr = state
            @test typeof(item) === Tv && item == zero(Tv)
            @test idxstate == (eachindex(A), eachindex(A)[itr])
            @test nextptr == 3
        end

        # iterate(A, state) - finished
        state = iterate(A, state)
        @test state === nothing
    end
end

## AbstractSparseTensor interface

@testset "dropstored!" begin
    @testset "N=$N, Ti=$Ti, Tv=$Tv" for N in 1:3, Ti in [Int, UInt8], Tv in [Float64, BigFloat, Int8]
        dims = (5, 3, 2)[1:N]
        inds = (Ti[2, 3, 4], Ti[1, 3, 2], Ti[1, 2, 1])[1:N]
        inds = tuple.(inds...)
        vals = Tv[1, 0, 10]

        # iszero
        A = SparseTensorCOO(dims, copy(inds), copy(vals))
        dropstored!(iszero, A)
        @test A.inds == inds[[1, 3]] && A.vals == vals[[1, 3]]

        # beyond tolerance
        A = SparseTensorCOO(dims, copy(inds), copy(vals))
        dropstored!(x -> abs(x) > 5, A)
        @test A.inds == inds[1:2] && A.vals == vals[1:2]
    end
end

@testset "numstored / stored[indices|values|pairs]" begin
    @testset "N=$N, Ti=$Ti, Tv=$Tv" for N in 1:3, Ti in [Int, UInt8], Tv in [Float64, BigFloat, Int8]
        dims = (5, 3, 2)[1:N]
        inds = (Ti[2, 1, 4], Ti[1, 3, 2], Ti[1, 2, 1])[1:N]
        inds = tuple.(inds...)
        vals = Tv[1, 0, 10]
        perm = sortperm(inds; by = CartesianIndex)
        sinds, svals = inds[perm], vals[perm]
        A = SparseTensorCOO(dims, inds, vals)

        @test numstored(A) == length(vals)
        @test typeof(storedindices(A)) === Vector{NTuple{N,Ti}} && storedindices(A) == sinds
        @test typeof(storedvalues(A)) === Vector{Tv} && storedvalues(A) == svals
        @test collect(storedpairs(A)) == (sinds .=> svals)
    end
end


text:
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Description
# ==============================================================================
#
#   Tests related with string decorations.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

@testset "Convert decoration to string" begin
    d = Decoration(
        foreground = "35",
        background = "48;5;243",
        underline = StringManipulation.active
    )

    expected = "\e[35m\e[48;5;243m\e[4m"
    result = convert(String, d)

    d = Decoration(
        foreground = "35",
        background = "48;5;243",
        underline = StringManipulation.inactive
    )

    expected = "\e[35m\e[48;5;243m\e[24m"
    result = convert(String, d)

    d = Decoration(
        foreground = "35",
        background = "",
        bold = StringManipulation.active
    )

    expected = "\e[35m\e[1m"
    result = convert(String, d)

    d = Decoration(
        foreground = "35",
        background = "",
        bold = StringManipulation.inactive
    )

    expected = "\e[35m\e[22m"
    result = convert(String, d)

    d = Decoration(
        foreground = "",
        background = "45",
        reversed = StringManipulation.active
    )

    expected = "\e[45m\e[7m"
    result = convert(String, d)

    d = Decoration(
        foreground = "",
        background = "45",
        reversed = StringManipulation.inactive
    )

    expected = "\e[45m\e[27m"
    result = convert(String, d)

    @test result == expected
end

@testset "Drop inactive decorations" begin
    decoration = Decoration(
        foreground = "39",
        background = "49",
        bold       = StringManipulation.inactive,
        underline  = StringManipulation.inactive,
        reversed   = StringManipulation.inactive
    )

    decoration = drop_inactive_properties(decoration)

    @test decoration === Decoration()

    decoration = Decoration(
        foreground = "39",
        background = "245",
        bold       = StringManipulation.inactive,
        underline  = StringManipulation.inactive,
        reversed   = StringManipulation.inactive
    )

    decoration = drop_inactive_properties(decoration)

    @test decoration === Decoration(background = "245")
end

@testset "Get decorations" begin
    str = "Test 😅 \e[38;5;231;48;5;243mTest 😅 \e[38;5;201;48;5;243mTest\e[0m"
    expected = "\e[38;5;231;48;5;243m\e[38;5;201;48;5;243m\e[0m"
    result = get_decorations(str)
    @test expected == result
end

@testset "Get and remove decorations" begin
    str = "Test 😅 \e[38;5;231;48;5;243mTest 😅 \e[38;5;201;48;5;243mTest\e[0m"
    expected_decorations = "\e[38;5;231;48;5;243m\e[38;5;201;48;5;243m\e[0m"
    expected_text = "Test 😅 Test 😅 Test"
    decorations, text = get_and_remove_decorations(str)
    @test expected_decorations == decorations
    @test expected_text === text
end

# Those tests are also used to verify the function `_parse_ansi_code`.
@testset "Parse decorations" begin
    decoration = parse_decoration("\e[35m\e[48;5;243m\e[4;27m")

    @test decoration.foreground == "35"
    @test decoration.background == "48;5;243"
    @test decoration.underline  == StringManipulation.active
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.inactive

    decoration = parse_decoration("\e[45;1m")

    @test decoration.foreground == ""
    @test decoration.background == "45"
    @test decoration.underline  == StringManipulation.unchanged
    @test decoration.bold       == StringManipulation.active
    @test decoration.reversed   == StringManipulation.unchanged

    decoration = parse_decoration("\e[48;5;243;7;22m")

    @test decoration.foreground == ""
    @test decoration.background == "48;5;243"
    @test decoration.underline  == StringManipulation.unchanged
    @test decoration.bold       == StringManipulation.inactive
    @test decoration.reversed   == StringManipulation.active

    decoration = parse_decoration("\e[39;49;1;7m")

    @test decoration.foreground == "39"
    @test decoration.background == "49"
    @test decoration.underline  == StringManipulation.unchanged
    @test decoration.bold       == StringManipulation.active
    @test decoration.reversed   == StringManipulation.active

    decoration = parse_decoration("\e[92;103;24m")

    @test decoration.foreground == "92"
    @test decoration.background == "103"
    @test decoration.underline  == StringManipulation.inactive
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.unchanged

    # Parse decorations with text in the middle.
    decoration = parse_decoration("\e[35m\e[48;5;243mThis text should be discarded\e[4;27m")

    @test decoration.foreground == "35"
    @test decoration.background == "48;5;243"
    @test decoration.underline  == StringManipulation.active
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.inactive

    # Unsupported escape sequences.
    decoration = parse_decoration("\e]8")
    @test decoration === Decoration()

    # True-color mode.
    decoration = parse_decoration("\e[38;2;28;101;140;48;2;216;210;203mFirst decoration")

    @test decoration.foreground == "38;2;28;101;140"
    @test decoration.background == "48;2;216;210;203"
    @test decoration.underline  == StringManipulation.unchanged
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.unchanged
end

@testset "Remove decorations" begin
    str = "Test 😅 \e[38;5;231;48;5;243mTest 😅 \e[38;5;201;48;5;243mTest\e[0m"
    expected = "Test 😅 Test 😅 Test"
    result = remove_decorations(str)
    @test expected == result

    str = "This string does not have decorations"
    result = remove_decorations(str)
    @test result == str
end

@testset "Update decorations" begin
    decoration = Decoration()

    decoration = update_decoration(decoration, "\e[38;5;231m")
    @test decoration.foreground == "38;5;231"
    @test decoration.background == ""
    @test decoration.underline  == StringManipulation.unchanged
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.unchanged
    @test decoration.reset      == false

    decoration = update_decoration(decoration, "\e[48;5;243m")
    @test decoration.foreground == "38;5;231"
    @test decoration.background == "48;5;243"
    @test decoration.underline  == StringManipulation.unchanged
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.unchanged
    @test decoration.reset      == false

    decoration = update_decoration(decoration, "\e[4;27m")
    @test decoration.foreground == "38;5;231"
    @test decoration.background == "48;5;243"
    @test decoration.underline  == StringManipulation.active
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.inactive
    @test decoration.reset      == false

    decoration = update_decoration(decoration, "\e[0m")
    @test decoration.foreground == ""
    @test decoration.background == ""
    @test decoration.underline  == StringManipulation.unchanged
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.unchanged
    @test decoration.reset      == true

    decoration = update_decoration(decoration, "\e[33m")
    @test decoration.foreground == "33"
    @test decoration.background == ""
    @test decoration.underline  == StringManipulation.unchanged
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.unchanged
    @test decoration.reset      == false

    # Update decorations with text in the middle.
    decoration = Decoration()

    decoration = update_decoration(
        decoration,
        "\e[38;5;231mThis text should be discarded\e[48;5;243m"
    )
    @test decoration.foreground == "38;5;231"
    @test decoration.background == "48;5;243"
    @test decoration.underline  == StringManipulation.unchanged
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.unchanged
    @test decoration.reset      == false

    decoration = update_decoration(
        decoration,
        "This also should be discarded\e[4;27m"
    )
    @test decoration.foreground == "38;5;231"
    @test decoration.background == "48;5;243"
    @test decoration.underline  == StringManipulation.active
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.inactive
    @test decoration.reset      == false

    # True-color mode.
    decoration = Decoration()
    decoration = update_decoration(
        decoration,
        "\e[38;2;28;101;140;48;2;216;210;203mFirst decoration"
    )

    @test decoration.foreground == "38;2;28;101;140"
    @test decoration.background == "48;2;216;210;203"
    @test decoration.underline  == StringManipulation.unchanged
    @test decoration.bold       == StringManipulation.unchanged
    @test decoration.reversed   == StringManipulation.unchanged

    decoration = update_decoration(
        decoration,
        "\e[38;2;57;138;185;1mSecond decoration"
    )

    @test decoration.foreground == "38;2;57;138;185"
    @test decoration.background == "48;2;216;210;203"
    @test decoration.underline  == StringManipulation.unchanged
    @test decoration.bold       == StringManipulation.active
    @test decoration.reversed   == StringManipulation.unchanged
end


text:
# printf

# Formatted Output (B.17)

@generated function promote_c_argument(arg)
    # > When a function with a variable-length argument list is called, the variable
    # > arguments are passed using C's old ``default argument promotions.'' These say that
    # > types char and short int are automatically promoted to int, and type float is
    # > automatically promoted to double. Therefore, varargs functions will never receive
    # > arguments of type char, short int, or float.

    if arg == Cchar || arg == Cshort || arg == Cuchar || arg == Cushort
        return :(Cint(arg))
    elseif arg == Cfloat
        return :(Cdouble(arg))
    else
        return :(arg)
    end
end

macro printf(fmt::String, args...)
    fmt_val = Val(Symbol(fmt))

    return :(emit_printf($fmt_val, $(map(arg -> :(promote_c_argument($arg)), esc.(args))...)))
end

@generated function emit_printf(::Val{fmt}, argspec...) where {fmt}
    arg_exprs = [:( argspec[$i] ) for i in 1:length(argspec)]
    arg_types = [argspec...]

    Context() do ctx
        T_void = LLVM.VoidType(ctx)
        T_int32 = LLVM.Int32Type(ctx)
        T_pint8 = LLVM.PointerType(LLVM.Int8Type(ctx))

        # create functions
        param_types = LLVMType[convert(LLVMType, typ; ctx) for typ in arg_types]
        llvm_f, _ = create_function(T_int32, param_types)
        mod = LLVM.parent(llvm_f)

        # generate IR
        Builder(ctx) do builder
            entry = BasicBlock(llvm_f, "entry"; ctx)
            position!(builder, entry)

            str = globalstring_ptr!(builder, String(fmt))

            # invoke printf and return
            printf_typ = LLVM.FunctionType(T_int32, [T_pint8]; vararg=true)
            printf = LLVM.Function(mod, "printf", printf_typ)
            push!(function_attributes(printf), EnumAttribute("nobuiltin"; ctx))
            chars = call!(builder, printf, [str, parameters(llvm_f)...])

            ret!(builder, chars)
        end

        call_function(llvm_f, Int32, Tuple{arg_types...}, arg_exprs...)
    end
end

## print-like functionality

# simple conversions, defining an expression and the resulting argument type. nothing fancy,
# `@print` pretty directly maps to `@printf`; we should just support `write(::IO)`.
const print_conversions = Dict(
    Float32     => (x->:(Float64($x)),             Float64),
    Ptr{<:Any}  => (x->:(convert(Ptr{Cvoid}, $x)), Ptr{Cvoid}),
    Bool        => (x->:(Int32($x)),               Int32),
)

# format specifiers
const print_specifiers = Dict(
    # integers
    Int16       => "%hd",
    Int32       => "%d",
    Int64       => Sys.iswindows() ? "%lld" : "%ld",
    UInt16      => "%hu",
    UInt32      => "%u",
    UInt64      => Sys.iswindows() ? "%llu" : "%lu",

    # floating-point
    Float64     => "%f",

    # other
    Cchar       => "%c",
    Ptr{Cvoid}  => "%p",
)

@generated function _print(parts...)
    fmt = ""
    args = Expr[]

    for i in 1:length(parts)
        part = :(parts[$i])
        T = parts[i]

        # put literals directly in the format string
        if T <: Val
            fmt *= string(T.parameters[1])
            continue
        end

        # try to convert arguments if they are not supported directly
        if !haskey(print_specifiers, T)
            for Tmatch in keys(print_conversions)
                if T <: Tmatch
                    conv, T = print_conversions[Tmatch]
                    part = conv(part)
                    break
                end
            end
        end

        # render the argument
        if haskey(print_specifiers, T)
            fmt *= print_specifiers[T]
            push!(args, part)
        elseif T <: String
            @error("@print does not support non-literal strings")
        else
            @error("@print does not support values of type $T")
        end
    end

    quote
        Base.@_inline_meta
        @printf($fmt, $(args...))
    end
end

"""
    @print(xs...)
    @println(xs...)

Print a textual representation of values `xs` to standard output from the GPU. The
functionality builds on `@printf`, and is intended as a more use friendly alternative of
that API. However, that also means there's only limited support for argument types, handling
16/32/64 signed and unsigned integers, 32 and 64-bit floating point numbers, `Cchar`s and
pointers. For more complex output, use `@printf` directly.

Limited string interpolation is also possible:

```julia
    @print("Hello, World ", 42, "\\n")
    @print "Hello, World \$(42)\\n"
```
"""
macro print(parts...)
    args = Union{Val,Expr,Symbol}[]

    parts = [parts...]
    while true
        isempty(parts) && break

        part = popfirst!(parts)

        # handle string interpolation
        if isa(part, Expr) && part.head == :string
            parts = vcat(part.args, parts)
            continue
        end

        # expose literals to the generator by using Val types
        if isbits(part) # literal numbers, etc
            push!(args, Val(part))
        elseif isa(part, QuoteNode) # literal symbols
            push!(args, Val(part.value))
        elseif isa(part, String) # literal strings need to be interned
            push!(args, Val(Symbol(part)))
        else # actual values that will be passed to printf
            push!(args, part)
        end
    end

    quote
        _print($(map(esc, args)...))
    end
end

@doc (@doc @print) ->
macro println(parts...)
    esc(quote
        oneAPI.@print($(parts...), "\n")
    end)
end

"""
    @show(ex)

GPU analog of `Base.@show`. It comes with the same type restrictions as [`@printf`](@ref).

```julia
@show threadIdx().x
```
"""
macro show(exs...)
    blk = Expr(:block)
    for ex in exs
        push!(blk.args, :(oneAPI.@println($(sprint(Base.show_unquoted,ex)*" = "),
                                          begin local value = $(esc(ex)) end)))
    end
    isempty(exs) || push!(blk.args, :value)
    blk
end


text:
using AbstractInterpreterPlayground, Test
import Core.Compiler:
    ⊑

@testset "AbstractInterpreterPlayground.jl" begin
    # basic end to end
    interp, frame = @enter_call rand(Bool)
    @test frame.result.result ⊑ Bool

    # works for cached frames
    interp, frame = @enter_call rand(Bool)
    @test frame.result.result ⊑ Bool

    # works when inlining
    interp, frame = @enter_call inlining = true rand(Bool)
    @test frame.result.result ⊑ Bool
end

max_stars_repo_name: aviatesk/AbstractInterpreterPlayground

max_issues_repo_name: aviatesk/AbstractInterpreterPlayground

max_forks_repo_name: aviatesk/AbstractInterpreterPlayground


text:
"""
    header_strings(; kwargs...)
        --> [ String(Tuple name [Value unit]]

Take kwarg names, add value units in brackets.
Strings don't contain colour codes around units.
"""
function header_strings(; kwargs...)
    vs = String[]
    for arg in kwargs
        symbol = arg[1]
        value = arg[2]
        str = string(symbol)
        if !isa(value,  Array{String})
            str *= " ["
            str *= if value isa AbstractArray
                string(unit(value[1]))
            else
                string(unit(value))
            end
            str *= "]"
        end
        push!(vs, str)
    end
    vs
end

"""
    rounded_stripped(quantity, digits)
        --> Value of same numeric type as quantity
"""
function rounded_stripped(quantity, digits)
    rounded = if quantity isa  Quantity
        round(unit(quantity), quantity, digits = digits)
    else
         round(quantity, digits = digits)
    end
    ustrip(rounded)
end
rounded_stripped(quantity::String, digits) = quantity

"""
    value_strings(n_rows, n_columns, digits = 2; kwargs...)
    --> Matrix{Union{Missing, String}}

Integer values won't be affected by rounding to digits.
"""
function value_strings(n_rows, n_columns, digits = 2; kwargs...)
    ms = Matrix{Union{Missing, String}}(missing, n_rows, n_columns)
    for col in 1:n_columns
        ve = kwargs[col]
        for row in 1:n_rows
            q = ve[row]
            v = rounded_stripped(q, digits)
            ms[row, col] = string(v)
        end
    end
    ms
end

"Pixel width of drawing using current settings"
pixelwidth(s::String) = textextents(s)[3]

"""
    column_widths(;kwargs...)
    --> ['pixel width']
Based on header text widths with two spaces padding on both sides
"""
column_widths(;kwargs...) = map(pixelwidth, header_strings(;kwargs...) .* "   ")

"""
    row_height()
    --> 'Pixel' height of one table row.

This depends on the current 'Toy API' text size, and can be changed with
fontsize(FS). Please also change FS in that case, take care!
"""
row_height() = textextents("|")[4]

"""
    value_rows(;kwargs...)
    --> number of value rows in table
"""
function value_rows(;kwargs...)
    vs = Int64[]
    for (_, value) in kwargs
        s = if value isa AbstractArray
            size(value)[1]
        else
            1
        end
        push!(vs, s)
    end
    maximum(vs)
end

"Number of columns"
t_cols(;kwargs...) = length(header_strings(; kwargs...))

"""
    draw_header(pos::Point, pixel_widths, hdrs)
    -> positions of potentially next column
Each header centered to corresponding pixel_widths
"""
function draw_header(pos::Point, pixel_widths, hdrs)
    p = pos
    for (i, hdr) in enumerate(hdrs)
        # Center header
        columnpixelwidth = pixel_widths[i]
        phdr = pixelwidth(hdr)
        Δpx =  (columnpixelwidth - phdr) /2
        #= debug line
        line(p, p + (0, -row_height()), :stroke)
        =#
        text(hdr, p + (Δpx, 0))
        p += (columnpixelwidth, 0.0)
    end
    p
end

"""
    draw_values(pos, rows, pixel_widths; kwargs...)
    -> Array{String}
"""
function draw_values(pos, rows, pixel_widths; kwargs...)
    strs_including_missing = value_strings(rows, length(pixel_widths); kwargs...)
    strs = map(s-> ismissing(s) ? "" : String(s), strs_including_missing)
    strs_before_dot = map(stupl-> stupl[1], splitext.(strs))
    colpos = 0.0
    for col in 1:length(pixel_widths)
        rowpos = 0.0
        columnpixelwidth = pixel_widths[col]
        maxpixelwidth = maximum(pixelwidth, strs[:, col])
        maxleadingpixelwidth = maximum(pixelwidth, strs_before_dot[:, col])
        maxtrailingpixelwidth = maxpixelwidth - maxleadingpixelwidth
        sumpixelwidth = maxleadingpixelwidth + maxtrailingpixelwidth
        # If there are no dots, reldotpos = 1.
        # If there are no digits before any of the values, reldotpos = 0,
        # but that would require no leading digits.
        reldotpos = maxleadingpixelwidth / sumpixelwidth
        Δpx_textbox = (columnpixelwidth - sumpixelwidth) / 2
        for row in 1:rows
            rowpos += row_height()
            p = pos + (colpos, rowpos)
            s = strs[row, col]
            # Find offset for centering within sumpixelwidth
            sleading = strs_before_dot[row, col]
            ps = pixelwidth(s)
            psleading = pixelwidth(sleading)
            Δpx_sumpixelwidth = sumpixelwidth * reldotpos - psleading
            # For the column contents, we don't use settext, but the 'TOY API'.
            text(s, p + (Δpx_textbox + 1.0* Δpx_sumpixelwidth, 0))
            #= debug lines
            line(p, p + (Δpx_textbox, -row_height()), :stroke)
            line(p + (columnpixelwidth, 0), p + (Δpx_textbox + sumpixelwidth, -row_height()), :stroke)
            =#
        end
        colpos += columnpixelwidth
    end
    strs
end

"""
    text_table(pos::Point; kwargs...)
    -> Array{String, co}, text output without alignmnent

Draw a table with columns aligned on decimal separator.
Each keyword argument contains one column with header.
The unit from each column is included in the header for this column.

# Examples
```julia-repl
text_table(pos, diameters = diameters,
                packed_d = 2 .* radius_filled,
                MBL = mbls,
                mbl = mbls .|> g,
                Lineweight = weights)

s2 = text_table(pos; Symb(" ") => diameters)
```
"""
function text_table(pos::Point; kwargs...)
    widths = column_widths(;kwargs...)
    text_table_fixed_columns(pos::Point, widths; kwargs...)
end

"""
    text_table_fixed_columns(pos::Point, widths; kwargs...)
    -> Array{String, co}, text output without alignmnent

Draw a table with fixed column widths.
Widths can be single number, length, or a collection.
See text_table.
"""
function text_table_fixed_columns(pos::Point, widths; kwargs...)
    ncols = t_cols(;kwargs...)
    vecwidths = if widths isa Number
        [widths for i = 1:ncols]
    else
        widths
    end
    @assert length(vecwidths) == ncols
    pixwidths = scale_to_pt.(vecwidths)
    nrows = value_rows(;kwargs...)
    hdrs = header_strings(;kwargs...)
    draw_header(pos, pixwidths, hdrs)
    pos += (0.0, row_height())
    # Draw values and return text including headers
    vcat(reshape(hdrs, 1, :), draw_values(pos, nrows, pixwidths; kwargs...))
end


text:
import Pkg;
Pkg.instantiate();

using BenchmarkTools
using MonkeyLang

const m = MonkeyLang

upper = 35

function fib(x)
    if x == 0
        0
    else
        if x == 1
            1
        else
            fib(x - 1) + fib(x - 2)
        end
    end
end

naive = """
let fibonacci = fn(x) {
    if (x == 0) {
        0
    } else {
        if (x == 1) {
            1
        } else {
            fibonacci(x - 1) + fibonacci(x - 2);
        }
    }
};

fibonacci($upper);
"""

tailrec = """
let fibonacci = fn(x, a, b) {
    if (x == 1) {
        b
    } else {
        fibonacci(x - 1, b, a + b)
    }
};

fibonacci($upper);
"""

memoized = """
let d = {};

let fibonacci = fn(x) {
    if (x == 0) {
        0
    } else {
        if (x == 1) {
            1;
        } else {
            if (type(d[x]) == "NULL") {
                let g = fibonacci(x - 1) + fibonacci(x - 2);
                d = push(d, x, g);
            }

            d[x];
        }
    }
}

fibonacci($upper);
"""

dp = """
let fibonacci = fn(x) {
    let i = 2;
    let a = 0;
    let b = 1;

    while (!(i > x)) {
        let c = a + b;
        a = b;
        b = c;
        i = i + 1;
    }

    b;
}

fibonacci($upper);
"""

println("=== Julia native ===")
@btime fib($upper)

println("=== Using evaluator (naive) ===")
@btime m.evaluate($naive)

println("=== Using compiler (naive) ===")
@btime m.run($naive)

println("=== Using Julia as the Backend (naive) ===")
@btime m.Transpilers.JuliaTranspiler.run($naive)

println("=== Using evaluator (tailrec) ===")
@btime m.evaluate($tailrec)

println("=== Using compiler (tailrec) ===")
@btime m.run($tailrec)

println("=== Using Julia as the Backend ===")
@btime m.Transpilers.JuliaTranspiler.run($tailrec)

println("=== Using evaluator (memoized) ===")
@btime m.evaluate($memoized)

println("=== Using compiler (memoized) ===")
@btime m.run($memoized)

println("=== Using Julia as the Backend ===")
@btime m.Transpilers.JuliaTranspiler.run($memoized)

println("=== Using evaluator (dp) ===")
@btime m.evaluate($dp)

println("=== Using compiler (dp) ===")
@btime m.run($dp)

println("=== Using Julia as the Backend ===")
@btime m.Transpilers.JuliaTranspiler.run($dp)


text:
"""
Bridges `CP.AllDifferentExceptConstants` to a series of `CP.Disjunction`
of `CP.DifferentFrom` and `CP.Domain`.
"""
struct AllDifferentExceptConstants2ConjunctionDisjunctionBridge{T} <: MOIBC.AbstractBridge
    # An upper-triangular matrix (i.e. nothing if i < j, constraint if i >= j).
    # Standard sparse matrices cannot store anything that is not a Number
    # (more specifically, anything that does not implement `zero(T)`).
    # https://github.com/JuliaLang/julia/issues/30573
    cons::Dict{
        Tuple{Int, Int},
        MOI.ConstraintIndex, # {MOI.VectorAffineFunction{T}, CP.Disjunction{Tuple{CP.Domain{T}, CP.Domain{T}, CP.DifferentFrom{T}}}},
    }
end

function MOIBC.bridge_constraint(
    ::Type{AllDifferentExceptConstants2ConjunctionDisjunctionBridge{T}},
    model,
    f::MOI.VectorOfVariables,
    s::CP.AllDifferentExceptConstants{T},
) where {T}
    return MOIBC.bridge_constraint(
        AllDifferentExceptConstants2ConjunctionDisjunctionBridge{T},
        model,
        MOI.VectorAffineFunction{T}(f),
        s,
    )
end

function MOIBC.bridge_constraint(
    ::Type{AllDifferentExceptConstants2ConjunctionDisjunctionBridge{T}},
    model,
    f::MOI.VectorAffineFunction{T},
    s::CP.AllDifferentExceptConstants{T},
) where {T}
    f_scalars = MOIU.scalarize(f)
    dim = MOI.output_dimension(f)

    # Upper-triangular matrix of constraints: i >= j, i.e. d(d-1)/2 elements:
    #     \sum_{i=2}^{d} (n - i + 1) = d (d - 1) / 2
    cons = Dict{Tuple{Int, Int}, MOI.ConstraintIndex}()
    sizehint!(cons, dim * (dim - 1) / 2)

    for i in 1:dim
        for j in (i+1):dim
            cons[i, j] = MOI.add_constraint(
                model,
                MOIU.vectorize(
                    [
                        f_scalars[i],
                        f_scalars[j],
                        f_scalars[i] - f_scalars[j],
                    ]
                ),
                CP.Disjunction(
                    (
                        CP.Domain(copy(s.k)), # x[i] to ignore
                        CP.Domain(copy(s.k)), # x[j] to ignore
                        CP.DifferentFrom(zero(T)), # x[i] != x[j]
                    )
                ),
            )
        end
    end

    return AllDifferentExceptConstants2ConjunctionDisjunctionBridge{T}(cons)
end

function MOI.supports_constraint(
    ::Type{AllDifferentExceptConstants2ConjunctionDisjunctionBridge{T}},
    ::Union{Type{MOI.VectorOfVariables}, Type{MOI.VectorAffineFunction{T}}},
    ::Type{CP.AllDifferentExceptConstants{T}},
) where {T}
    return true
end

function MOIB.added_constrained_variable_types(::Type{AllDifferentExceptConstants2ConjunctionDisjunctionBridge{T}}) where {T}
    return Tuple{Type}[]
end

function MOIB.added_constraint_types(::Type{AllDifferentExceptConstants2ConjunctionDisjunctionBridge{T}}) where {T}
    return [
        (MOI.VectorAffineFunction{T}, CP.Disjunction),
    ]
end

function MOI.get(
    b::AllDifferentExceptConstants2ConjunctionDisjunctionBridge{T},
    ::MOI.NumberOfConstraints{
        MOI.VectorAffineFunction{T}, CP.Disjunction,
    },
) where {T}
    return length(b.cons)
end

function MOI.get(
    b::AllDifferentExceptConstants2ConjunctionDisjunctionBridge{T},
    ::MOI.ListOfConstraintIndices{
        MOI.VectorAffineFunction{T}, CP.Disjunction,
    },
) where {T}
    return collect(values(b.cons))
end