for p in ("Knet","AutoGrad","ArgParse","Compat")
    Pkg.installed(p) == nothing && Pkg.add(p)
end

"""
charlm.jl: Knet8 version (c) Emre Yolcu, Deniz Yuret, 2016

This example implements an LSTM network for training and testing
character-level language models inspired by ["The Unreasonable
Effectiveness of Recurrent Neural
Networks"](http://karpathy.github.io/2015/05/21/rnn-effectiveness) from
the Andrej Karpathy blog.  The model can be trained with different
genres of text, and can be used to generate original text in the same
style.

Example usage:

* `julia charlm.jl`: trains a model using its own code.

* `julia charlm.jl --data foo.txt`: uses foo.txt to train instead.

* `julia charlm.jl --data foo.txt bar.txt`: uses foo.txt for training
  and bar.txt for validation.  Any number of files can be specified,
  the first two will be used for training and validation, the rest for
  testing.

* `julia charlm.jl --best foo.jld --save bar.jld`: saves the best
  model (according to validation set) to foo.jld, last model to
  bar.jld.

* `julia charlm.jl --load foo.jld --generate 1000`: generates 1000
  characters from the model in foo.jld.

* `julia charlm.jl --help`: describes all available options.

"""
module CharLM

using Knet,AutoGrad,ArgParse,Compat

function main(args=ARGS)
    s = ArgParseSettings()
    s.description="charlm.jl (c) Emre Yolcu, Deniz Yuret, 2016. Character level language model based on http://karpathy.github.io/2015/05/21/rnn-effectiveness."
    s.exc_handler=ArgParse.debug_handler
    @add_arg_table s begin
        ("--datafiles"; nargs='+'; help="If provided, use first file for training, second for dev, others for test.")
        ("--loadfile"; help="Initialize model from file")
        ("--savefile"; help="Save final model to file")
        ("--bestfile"; help="Save best model to file")
        ("--generate"; arg_type=Int; default=0; help="If non-zero generate given number of characters.")
        ("--hidden"; nargs='+'; arg_type=Int; default=[256]; help="Sizes of one or more LSTM layers.")
        ("--embed"; arg_type=Int; default=256; help="Size of the embedding vector.")
        ("--epochs"; arg_type=Int; default=3; help="Number of epochs for training.")
        ("--batchsize"; arg_type=Int; default=128; help="Number of sequences to train on in parallel.")
        ("--seqlength"; arg_type=Int; default=100; help="Number of steps to unroll the network for.")
        ("--decay"; arg_type=Float64; default=0.9; help="Learning rate decay.")
        ("--lr"; arg_type=Float64; default=4.0; help="Initial learning rate.")
        ("--gclip"; arg_type=Float64; default=3.0; help="Value to clip the gradient norm at.")
        ("--winit"; arg_type=Float64; default=0.3; help="Initial weights set to winit*randn().")
        ("--gcheck"; arg_type=Int; default=0; help="Check N random gradients.")
        ("--seed"; arg_type=Int; default=-1; help="Random number seed.")
        ("--atype"; default=(gpu()>=0 ? "KnetArray{Float32}" : "Array{Float32}"); help="array type: Array for cpu, KnetArray for gpu")
        ("--fast"; action=:store_true; help="skip loss printing for faster run")
        #TODO ("--dropout"; arg_type=Float64; default=0.0; help="Dropout probability.")
    end
    println(s.description)
    isa(args, AbstractString) && (args=split(args))
    o = parse_args(args, s; as_symbols=true)
    println("opts=",[(k,v) for (k,v) in o]...)
    o[:seed] > 0 && srand(o[:seed])
    o[:atype] = eval(parse(o[:atype]))
    if any(f->(o[f]!=nothing), (:loadfile, :savefile, :bestfile))
        Pkg.installed("JLD")==nothing && Pkg.add("JLD") # error("Please Pkg.add(\"JLD\") to load or save files.")
        eval(Expr(:using,:JLD))
    end

    # we initialize a model from loadfile, train using datafiles (both optional).
    # if the user specifies neither, train a model using the charlm.jl source code.
    isempty(o[:datafiles]) && o[:loadfile]==nothing && push!(o[:datafiles],@__FILE__) # shakespeare()

    # read text and report lengths
    text = map((@compat readstring), o[:datafiles])
    !isempty(text) && info("Chars read: $(map((f,c)->(basename(f),length(c)),o[:datafiles],text))")

    # vocab (char_to_index) comes from the initial model if there is one, otherwise from the datafiles.
    # if there is an initial model make sure the data has no new vocab
    if o[:loadfile]==nothing
        vocab = Dict{Char,Int}()
        for t in text, c in t; get!(vocab, c, 1+length(vocab)); end
        model = initweights(o[:atype], o[:hidden], length(vocab), o[:embed], o[:winit])
    else
        info("Loading model from $(o[:loadfile])")
        vocab = load(o[:loadfile], "vocab")
        for t in text, c in t; haskey(vocab, c) || error("Unknown char $c"); end
        model = map(p->convert(o[:atype],p), load(o[:loadfile], "model"))
    end
    info("$(length(vocab)) unique chars.")
    if !isempty(text)
        train!(model, text, vocab, o)
    end
    if o[:savefile] != nothing
        info("Saving last model to $(o[:savefile])")
        save(o[:savefile], "model", model, "vocab", vocab)
    end
    if o[:generate] > 0
        state = initstate(o[:atype],o[:hidden],1)
        generate(model, state, vocab, o[:generate])
    end
end

function train!(model, text, vocab, o)
    s0 = initstate(o[:atype], o[:hidden], o[:batchsize])
    data = map(t->minibatch(t, vocab, o[:batchsize]), text)
    lr = o[:lr]
    if o[:fast]
        @time (for epoch=1:o[:epochs]
               train1(model, copy(s0), data[1]; slen=o[:seqlength], lr=lr, gclip=o[:gclip])
               end; gpu()>=0 && Knet.cudaDeviceSynchronize())
        return
    end
    losses = map(d->loss(model,copy(s0),d), data)
    println((:epoch,0,:loss,losses...))
    devset = ifelse(length(data) > 1, 2, 1)
    devlast = devbest = losses[devset]
    for epoch=1:o[:epochs]
        @time train1(model, copy(s0), data[1]; slen=o[:seqlength], lr=lr, gclip=o[:gclip])
        @time losses = map(d->loss(model,copy(s0),d), data)
        println((:epoch,epoch,:loss,losses...))
        if o[:gcheck] > 0
            gradcheck(loss, model, copy(s0), data[1], 1:o[:seqlength]; gcheck=o[:gcheck])
        end
        devloss = losses[devset]
        if devloss < devbest
            devbest = devloss
            if o[:bestfile] != nothing
                info("Saving best model to $(o[:bestfile])")
                save(o[:bestfile], "model", model, "vocab", vocab)
            end
        end
        if devloss > devlast
            lr *= o[:decay]
            info("New learning rate: $lr")
        end
        devlast = devloss
    end
end

# sequence[t]: input token at time t
# state is modified in place
function train1(param, state, sequence; slen=100, lr=1.0, gclip=0.0)
    for t = 1:slen:length(sequence)-slen
        range = t:t+slen-1
        gloss = lossgradient(param, state, sequence, range)
        gscale = lr
        if gclip > 0
            gnorm = sqrt(mapreduce(sumabs2, +, 0, gloss))
            if gnorm > gclip
                gscale *= gclip / gnorm
            end
        end
        for k in 1:length(param)
            # param[k] -= gscale * gloss[k]
            axpy!(-gscale, gloss[k], param[k])
        end
        isa(state,Vector{Any}) || error("State should not be Boxed.")
        # The following is needed in case AutoGrad boxes state values during gradient calculation
        for i = 1:length(state)
            state[i] = AutoGrad.getval(state[i])
        end
    end
end

# param[2k-1,2k]: weight and bias for the k'th lstm layer
# param[end-2]: embedding matrix
# param[end-1,end]: weight and bias for final prediction
function initweights(atype, hidden, vocab, embed, winit)
    param = Array(Any, 2*length(hidden)+3)
    input = embed
    for k = 1:length(hidden)
        param[2k-1] = winit*randn(input+hidden[k], 4*hidden[k])
        param[2k]   = zeros(1, 4*hidden[k])
        param[2k][1:hidden[k]] = 1 # forget gate bias
        input = hidden[k]
    end
    param[end-2] = winit*randn(vocab,embed)
    param[end-1] = winit*randn(hidden[end],vocab)
    param[end] = zeros(1,vocab)
    return map(p->convert(atype,p), param)
end

# state[2k-1,2k]: hidden and cell for the k'th lstm layer
function initstate(atype, hidden, batchsize)
    state = Array(Any, 2*length(hidden))
    for k = 1:length(hidden)
        state[2k-1] = zeros(batchsize,hidden[k])
        state[2k] = zeros(batchsize,hidden[k])
    end
    return map(s->convert(atype,s), state)
end

function lstm(weight,bias,hidden,cell,input)
    gates   = hcat(input,hidden) * weight .+ bias
    hsize   = size(hidden,2)
    forget  = sigm(gates[:,1:hsize])
    ingate  = sigm(gates[:,1+hsize:2hsize])
    outgate = sigm(gates[:,1+2hsize:3hsize])
    change  = tanh(gates[:,1+3hsize:end])
    cell    = cell .* forget + ingate .* change
    hidden  = outgate .* tanh(cell)
    return (hidden,cell)
end

# s[2k-1,2k]: hidden and cell for the k'th lstm layer
# w[2k-1,2k]: weight and bias for k'th lstm layer
# w[end-2]: embedding matrix
# w[end-1,end]: weight and bias for final prediction
# state is modified in place
function predict(w, s, x)
    x = x * w[end-2]
    for i = 1:2:length(s)
        (s[i],s[i+1]) = lstm(w[i],w[i+1],s[i],s[i+1],x)
        x = s[i]
    end
    return x * w[end-1] .+ w[end]
end

# sequence[t]: input token at time t
# state is modified in place
function loss(param,state,sequence,range=1:length(sequence)-1)
    total = 0.0; count = 0
    atype = typeof(AutoGrad.getval(param[1]))
    input = convert(atype,sequence[first(range)])
    for t in range
        ypred = predict(param,state,input)
        ynorm = logp(ypred,2) # ypred .- log(sum(exp(ypred),2))
        ygold = convert(atype,sequence[t+1])
        total += sum(ygold .* ynorm)
        count += size(ygold,1)
        input = ygold
    end
    return -total / count
end

lossgradient = grad(loss)

function generate(param, state, vocab, nchar)
    index_to_char = Array(Char, length(vocab))
    for (k,v) in vocab; index_to_char[v] = k; end
    input = oftype(param[1], zeros(1,length(vocab)))
    index = 1
    for t in 1:nchar
        ypred = predict(param,state,input)
        input[index] = 0
        index = sample(exp(logp(ypred)))
        print(index_to_char[index])
        input[index] = 1
    end
    println()
end

function sample(p)
    p = convert(Array,p)
    r = rand()
    for c = 1:length(p)
        r -= p[c]
        r < 0 && return c
    end
end

function shakespeare()
    file = Knet.dir("data","100.txt")
    if !isfile(file)
        info("Downloading 'The Complete Works of William Shakespeare'")
        url = "http://www.gutenberg.org/files/100/100.txt"
        download(url,file)
    end
    return file
end

function minibatch(chars, char_to_index, batch_size)
    nbatch = div(length(chars), batch_size)
    vocab_size = length(char_to_index)
    data = [ falses(batch_size, vocab_size) for i=1:nbatch ] # using BitArrays
    cidx = 0
    for c in chars            # safest way to iterate over utf-8 text
        idata = 1 + cidx % nbatch
        row = 1 + div(cidx, nbatch)
        row > batch_size && break
        col = char_to_index[c]
        data[idata][row,col] = 1
        cidx += 1
    end
    return data
end

# To be able to load/save KnetArrays:
if Pkg.installed("JLD") != nothing
    import JLD: writeas, readas
    type KnetJLD; a::Array; end
    writeas(c::KnetArray) = KnetJLD(Array(c))
    readas(d::KnetJLD) = KnetArray(d.a)
end

# This allows both non-interactive (shell command) and interactive calls like:
# $ julia charlm.jl --epochs 10
# julia> CharLM.main("--epochs 10")
!isinteractive() && !isdefined(Core.Main,:load_only) && main(ARGS)

end  # module

# Note: 10.txt used in the sample runs below was generated using
#   head -10000 100.txt > 10.txt
# where 100.txt is the file downloaded by shakespeare().

### SAMPLE RUN 74a2e6c+ Mon Sep 19 14:03:10 EEST 2016
### Implemented multi-layer.  Removed the keepstate option fixing it to true.
### Note that winit default changed so I specify it below for comparison.
### The slight difference is due to keepstate.

# julia> CharLM.main("--data 10.txt --winit 0.3 --fast")
# charlm.jl (c) Emre Yolcu, Deniz Yuret, 2016. Character level language model based on http://karpathy.github.io/2015/05/21/rnn-effectiveness.
# opts=(:lr,4.0)(:atype,"KnetArray{Float32}")(:winit,0.3)(:savefile,nothing)(:loadfile,nothing)(:generate,0)(:bestfile,nothing)(:gclip,3.0)(:hidden,[256])(:epochs,3)(:decay,0.9)(:gcheck,0)(:seqlength,100)(:seed,42)(:embed,256)(:batchsize,128)(:datafiles,Any["10.txt"])(:fast,true)
# INFO: Chars read: [("10.txt",425808)]
# INFO: 87 unique chars.
#   1.406687 seconds (1.74 M allocations: 196.799 MB, 2.49% gc time)
# (:epoch,0,:loss,6.1075509900258)
#   4.002638 seconds (6.12 M allocations: 374.188 MB, 2.41% gc time)
#   3.990772 seconds (6.11 M allocations: 374.129 MB, 2.44% gc time)
#   4.006249 seconds (6.12 M allocations: 374.240 MB, 2.53% gc time)
#   1.405878 seconds (1.75 M allocations: 197.059 MB, 2.56% gc time)
# (:epoch,3,:loss,1.8713183968407767)

### SAMPLE RUN 4ce58d1+ Fri Sep 16 12:24:00 EEST 2016
### Transposed everything so getindex does not need to copy

# charlm.jl (c) Emre Yolcu, Deniz Yuret, 2016. Character level language model based on http://karpathy.github.io/2015/05/21/rnn-effectiveness.
# opts=(:keepstate,false)(:lr,4.0)(:atype,"KnetArray{Float32}")(:winit,0.3)(:savefile,nothing)(:loadfile,nothing)(:generate,0)(:bestfile,nothing)(:gclip,3.0)(:embedding,256)(:hidden,256)(:epochs,3)(:decay,0.9)(:gcheck,0)(:seqlength,100)(:seed,42)(:batchsize,128)(:datafiles,Any["data/10.txt"])(:fast,true)
# INFO: Chars read: [("10.txt",425808)]
# INFO: 87 unique chars.
#   1.388652 seconds (1.73 M allocations: 196.686 MB, 2.04% gc time)
# (:epoch,0,:loss,6.1075509900258)
#   3.940298 seconds (6.06 M allocations: 373.166 MB, 2.06% gc time)
#   3.935995 seconds (6.06 M allocations: 373.244 MB, 2.07% gc time)
#   3.934983 seconds (6.06 M allocations: 373.245 MB, 2.08% gc time)
#   1.390374 seconds (1.73 M allocations: 196.820 MB, 2.11% gc time)
# (:epoch,3,:loss,1.860654126576015)

### SAMPLE RUN 31136d5+ Wed Sep 14 17:51:44 EEST 2016: using vcat(x,h) and vcat(w...)
### optimized learning parameters: winit=0.3, lr=4.0, gclip=3.0

# charlm.jl (c) Emre Yolcu, Deniz Yuret, 2016. Character level language model based on http://karpathy.github.io/2015/05/21/rnn-effectiveness.
# opts=(:keepstate,false)(:lr,4.0)(:atype,"KnetArray{Float32}")(:winit,0.3)(:savefile,nothing)(:loadfile,nothing)(:generate,0)(:bestfile,nothing)(:gclip,3.0)(:embedding,256)(:hidden,256)(:epochs,3)(:decay,0.9)(:gcheck,0)(:seqlength,100)(:seed,42)(:batchsize,128)(:datafiles,Any["10.txt"])(:fast,true)
# INFO: Chars read: [("10.txt",425808)]
# INFO: 87 unique chars.
#   1.596959 seconds (1.79 M allocations: 197.432 MB, 2.56% gc time)
# (:epoch,0,:loss,5.541976199042528)
#   4.421566 seconds (6.23 M allocations: 375.843 MB, 2.54% gc time)
#   4.418540 seconds (6.25 M allocations: 376.058 MB, 2.51% gc time)
#   4.402317 seconds (6.26 M allocations: 376.297 MB, 2.66% gc time)
#   1.594677 seconds (1.81 M allocations: 197.737 MB, 2.64% gc time)
# (:epoch,3,:loss,1.8484550957572192)

### SAMPLE RUN 80503e7+ Wed Sep 14 17:35:36 EEST 2016: using vcat(x,h)
#
# charlm.jl (c) Emre Yolcu, Deniz Yuret, 2016. Character level language model based on http://karpathy.github.io/2015/05/21/rnn-effectiveness.
# opts=(:keepstate,false)(:lr,1.0)(:atype,"KnetArray{Float32}")(:savefile,nothing)(:loadfile,nothing)(:generate,0)(:bestfile,nothing)(:embedding,256)(:gclip,5.0)(:hidden,256)(:epochs,3)(:decay,0.9)(:gcheck,0)(:seqlength,100)(:seed,42)(:batchsize,128)(:datafiles,Any["10.txt"])(:fast,true)
# INFO: Chars read: [("10.txt",425808)]
# INFO: 87 unique chars.
#   1.930180 seconds (1.95 M allocations: 213.741 MB, 1.82% gc time)
# (:epoch,0,:loss,4.462641664662756)
#   4.968101 seconds (7.47 M allocations: 454.259 MB, 2.24% gc time)
#   4.963733 seconds (7.47 M allocations: 454.363 MB, 2.26% gc time)
#   4.967413 seconds (7.45 M allocations: 454.024 MB, 2.14% gc time)
#   1.945658 seconds (1.98 M allocations: 214.183 MB, 2.02% gc time)
# (:epoch,3,:loss,3.2389672966290237)

### SAMPLE RUN 65f57ff+ Wed Sep 14 10:02:30 EEST 2016: separate x, h, w, b
#
# charlm.jl (c) Emre Yolcu, Deniz Yuret, 2016. Character level language model based on http://karpathy.github.io/2015/05/21/rnn-effectiveness.
# opts=(:keepstate,false)(:lr,1.0)(:atype,"KnetArray{Float32}")(:savefile,nothing)(:loadfile,nothing)(:generate,0)(:bestfile,nothing)(:embedding,256)(:gclip,5.0)(:hidden,256)(:epochs,3)(:decay,0.9)(:gcheck,0)(:seqlength,100)(:seed,42)(:batchsize,128)(:datafiles,Any["10.txt"])(:fast,true)
# INFO: Chars read: [("10.txt",425808)]
# INFO: 87 unique chars.
#   2.156358 seconds (2.31 M allocations: 237.913 MB, 2.30% gc time)
# (:epoch,0,:loss,4.465127425659868)
#   6.287736 seconds (9.54 M allocations: 574.703 MB, 2.84% gc time)
#   6.272144 seconds (9.54 M allocations: 574.633 MB, 2.80% gc time)
#   6.277462 seconds (9.54 M allocations: 574.637 MB, 2.86% gc time)
#   2.165516 seconds (2.34 M allocations: 238.323 MB, 2.56% gc time)
# (:epoch,3,:loss,3.226540256084356)

### SAMPLE OUTPUT (with head -10000 100.txt): first version
# julia> CharLM.main("--gpu --data 10.txt")
# opts=(:lr,1.0)(:savefile,nothing)(:loadfile,nothing)(:dropout,0.0)(:generate,0)(:bestfile,nothing)(:embedding,256)(:gclip,5.0)(:hidden,256)(:epochs,10)(:nlayer,1)(:decay,0.9)(:gpu,true)(:seqlength,100)(:seed,42)(:batchsize,128)(:datafiles,Any["10.txt"])
# INFO: Chars read: [("10.txt",425808)]
# INFO: 87 unique chars.
# (0,4.465127425659868)
#   2.182693 seconds (2.36 M allocations: 240.394 MB, 1.74% gc time)
#   7.861079 seconds (10.12 M allocations: 601.311 MB, 1.84% gc time)
# (1,3.3244698245543285)
#   2.159062 seconds (2.35 M allocations: 239.217 MB, 1.80% gc time)
#   6.200085 seconds (9.55 M allocations: 575.573 MB, 2.28% gc time)
# (2,3.24593908969621)
#   2.352389 seconds (2.34 M allocations: 239.381 MB, 1.51% gc time)
#   6.211946 seconds (9.55 M allocations: 575.568 MB, 2.21% gc time)
# (3,3.226540256084356)


module JuliennedArrays

import Base: axes, getindex, setindex!, size
using Base: promote_op, @pure, @propagate_inbounds, tail

map_unrolled(call, variables::Tuple{}) = ()
map_unrolled(call, variables) =
    call(first(variables)), map_unrolled(call, tail(variables))...

map_unrolled(call, variables1::Tuple{}, variables2::Tuple{}) = ()
map_unrolled(call, variables1, variables2) =
    call(first(variables1), first(variables2)),
    map_unrolled(call, tail(variables1), tail(variables2))...

is_in(needle::Needle, straw1::Needle, straws...) where {Needle} = True()
is_in(needle, straw1, straws...) = is_in(needle, straws...)
is_in(needle) = False()

in_unrolled(straws, needle1, needles...) =
    is_in(needle1, straws...),
    in_unrolled(straws, needles...)...
in_unrolled(straws) = ()

@pure as_vals(them::Int...) = map(Val, them)

abstract type TypedBool end
"""
    struct True

Typed `true`
"""
struct True <: TypedBool end
"""
    struct False

Typed `false`
"""
struct False <: TypedBool end

@inline untyped(::True) = true
@inline untyped(::False) = false

not(::False) = True()
not(::True) = False()

export True
export False

getindex_unrolled(into::Tuple{}, switches::Tuple{}) = ()
function getindex_unrolled(into, switches)
    next = getindex_unrolled(tail(into), tail(switches))
    if untyped(first(switches))
        (first(into), next...)
    else
        next
    end
end

setindex_unrolled(old::Tuple{}, something, ::Tuple{}) = ()
setindex_unrolled(old, new, switches) =
    if untyped(first(switches))
        first(new),
        setindex_unrolled(tail(old), tail(new), tail(switches))...
    else
        first(old),
        setindex_unrolled(tail(old), new, tail(switches))...
    end

struct Slices{Item, Dimensions, Whole, Alongs} <: AbstractArray{Item, Dimensions}
    whole::Whole
    alongs::Alongs
end
Slices{Item, Dimensions}(whole::Whole, alongs::Alongs) where {Item, Dimensions, Whole, Alongs} =
    Slices{Item, Dimensions, Whole, Alongs}(whole, alongs)

axes(slices::Slices) =
    getindex_unrolled(axes(slices.whole), map_unrolled(not, slices.alongs))
size(slices::Slices) = map_unrolled(length, axes(slices))

slice_index(slices, indices) = setindex_unrolled(
    axes(slices.whole),
    indices,
    map_unrolled(not, slices.alongs)
)
@propagate_inbounds getindex(slices::Slices, indices::Int...) =
    view(slices.whole, slice_index(slices, indices)...)
@propagate_inbounds setindex!(slices::Slices, value, indices::Int...) =
    slices.whole[slice_index(slices, indices)...] = value

axis_or_1(switch, axis) =
    if untyped(switch)
        axis
    else
        1
    end
"""
    Slices(whole, alongs::TypedBool...)

Slice `whole` into `view`s.

`alongs`, made of [`True`](@ref) and [`False`](@ref) objects, shows which dimensions will be replaced with `:` when slicing.

```jldoctest
julia> using JuliennedArrays

julia> whole = [1 2; 3 4];

julia> slices = Slices(whole, False(), True())
2-element Slices{SubArray{Int64,1,Array{Int64,2},Tuple{Int64,Base.OneTo{Int64}},true},1,Array{Int64,2},Tuple{False,True}}:
 [1, 2]
 [3, 4]

julia> slices[1] == whole[1, :]
true

julia> slices[1] = [2, 1];

julia> whole
2×2 Array{Int64,2}:
 2  1
 3  4

julia> larger = rand(5, 5, 5);

julia> larger_slices = Slices(larger, True(), False(), False());

julia> size(first(larger_slices))
(5,)
```
"""
Slices(whole::AbstractArray, alongs::TypedBool...) =
    Slices{
        typeof(@inbounds view(
            whole,
            map_unrolled(axis_or_1, alongs, axes(whole))...
        )),
        length(getindex_unrolled(alongs, map_unrolled(not, alongs)))
    }(whole, alongs)

"""
    Slices(whole, alongs::Int...)

Alternative syntax: `alongs` is which dimensions will be replaced with `:` when slicing.

```jldoctest
julia> using JuliennedArrays

julia> input = reshape(1:8, 2, 2, 2)
2×2×2 reshape(::UnitRange{Int64}, 2, 2, 2) with eltype Int64:
[:, :, 1] =
 1  3
 2  4

[:, :, 2] =
 5  7
 6  8

julia> Slices(input, 1, 3)
2-element Slices{SubArray{Int64,2,Base.ReshapedArray{Int64,3,UnitRange{Int64},Tuple{}},Tuple{Base.OneTo{Int64},Int64,Base.OneTo{Int64}},false},1,Base.ReshapedArray{Int64,3,UnitRange{Int64},Tuple{}},Tuple{True,False,True}}:
 [1 5; 2 6]
 [3 7; 4 8]
```
"""
Slices(whole::AbstractArray{Item, NumberOfDimensions}, alongs::Int...) where {Item, NumberOfDimensions} =
    Slices(whole, in_unrolled(
        as_vals(alongs...),
        ntuple(Val, NumberOfDimensions)...
    )...)
export Slices

struct Align{Item, Dimensions, Sliced, Alongs} <: AbstractArray{Item, Dimensions}
    slices::Sliced
    alongs::Alongs
end
Align{Item, Dimensions}(slices::Sliced, alongs::Alongs) where {Item, Dimensions, Sliced, Alongs} =
    Align{Item, Dimensions, Sliced, Alongs}(slices, alongs)

axes(aligned::Align) = setindex_unrolled(
    setindex_unrolled(
        aligned.alongs,
        axes(aligned.slices),
        map_unrolled(not, aligned.alongs)
    ),
    axes(first(aligned.slices)),
    aligned.alongs
)
size(aligned::Align) = map_unrolled(length, axes(aligned))

split_indices(aligned, indices) =
    getindex_unrolled(indices, map_unrolled(not, aligned.alongs)),
    getindex_unrolled(indices, aligned.alongs)
@propagate_inbounds function getindex(aligned::Align, indices::Int...)
    outer, inner = split_indices(aligned, indices)
    aligned.slices[outer...][inner...]
end
@propagate_inbounds function setindex!(aligned::Align, value, indices::Int...)
    outer, inner = split_indices(aligned, indices)
    aligned.slices[outer...][inner...] = value
end

"""
    Align(slices, alongs::TypedBool...)

`Align` an array of arrays, all with the same size.

`alongs`, made of [`True`](@ref) and [`False`](@ref) objects, shows which dimensions will be taken up by the inner arrays. Inverse of [`Slices`](@ref).

```jldoctest
julia> using JuliennedArrays

julia> slices = [[1, 2], [3, 4]];

julia> aligned = Align(slices, False(), True())
2×2 Align{Int64,2,Array{Array{Int64,1},1},Tuple{False,True}}:
 1  2
 3  4

julia> aligned[1, :] == slices[1]
true

julia> aligned[1, 1] = 0;

julia> slices
2-element Array{Array{Int64,1},1}:
 [0, 2]
 [3, 4]
```
"""
Align(slices::AbstractArray{<:AbstractArray{Item, InnerDimensions}, OuterDimensions}, alongs::TypedBool...) where {Item, InnerDimensions, OuterDimensions} =
    Align{Item, OuterDimensions + InnerDimensions}(slices, alongs)
export Align

"""
    Along(slices, alongs::Int...)

Alternative syntax: `alongs` is which dimensions will be taken up by the inner arrays.

```jldoctest
julia> using JuliennedArrays

julia> input = reshape(1:8, 2, 2, 2)
2×2×2 reshape(::UnitRange{Int64}, 2, 2, 2) with eltype Int64:
[:, :, 1] =
 1  3
 2  4

[:, :, 2] =
 5  7
 6  8

julia> slices = collect(Slices(input, 1, 3))
2-element Array{SubArray{Int64,2,Base.ReshapedArray{Int64,3,UnitRange{Int64},Tuple{}},Tuple{Base.OneTo{Int64},Int64,Base.OneTo{Int64}},false},1}:
 [1 5; 2 6]
 [3 7; 4 8]

julia> Align(slices, 1, 3)
2×2×2 Align{Int64,3,Array{SubArray{Int64,2,Base.ReshapedArray{Int64,3,UnitRange{Int64},Tuple{}},Tuple{Base.OneTo{Int64},Int64,Base.OneTo{Int64}},false},1},Tuple{True,False,True}}:
[:, :, 1] =
 1  3
 2  4

[:, :, 2] =
 5  7
 6  8
```
"""
Align(slices::AbstractArray{<:AbstractArray{Item, InnerDimensions}, OuterDimensions}, alongs::Int...) where {Item, InnerDimensions, OuterDimensions} =
    Align(slices, in_unrolled(
        as_vals(alongs...),
        ntuple(Val, InnerDimensions + OuterDimensions)...
    )...)

end


## Exercise 7-1
## Rewrite the function printn from Recursion using iteration instead of recursion.
println("Ans: ")

function printn(s, n::Int)
    while n > 0
        println(s)
        n -= 1
    end
end

printn("Hello iteration!", 4)

println("End.")


### A Pluto.jl notebook ###
# v0.19.8

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el)
        el
    end
end

# ╔═╡ 4712d6fd-efb3-4f46-9160-c82797a3b5e4
# ╠═╡ show_logs = false
begin
	using Logging
	global_logger(NullLogger())
	display("")
end

# ╔═╡ f0545c67-5cfd-438f-a9ef-92c35ebaefa4
# ╠═╡ show_logs = false
#Set-up packages
begin

	using DataFrames, HTTP, CSV, Dates, Plots, PlutoUI, Printf, LaTeXStrings, HypertextLiteral, ShortCodes

	gr();
	Plots.GRBackend()

	#Define html elements
	nbsp = html"&nbsp" #non-breaking space
	vspace = html"""

"""
	br = html"
"

	#Sets the width of cells, caps the cell width by 90% of screen width
	#(setting overwritten by cell below)
	# @bind screenWidth @htl("""
	#

	#
	#

	# """)

	# cellWidth= min(1000, screenWidth*0.9)
	# @htl("""
	#
	# """)

	#Sets the width of the cells
	#begin
	#	html"""


ex = :(a+b*c+1)

typeof(ex)

:(a + b*c + 1) ==
    Meta.parse("a + b*c + 1") ==
    Expr(:call, :+, :a, Expr(:call, :*, :b, :c), 1)

ex = quote
    x = 1
    y = 2
    x + y
end

typeof(ex)

:(1 + 2)

eval(ans)

ex = :(a + b)

eval(ex)

a = 1; b = 2;

eval(ex)

ex = :(x = 1)

x

eval(ex)

x

a = 1;

ex = Expr(:call, :+, a, :b)

a = 0; b = 2;

eval(ex)


"""
This module provides an implementation of NetMSA algorithm in Julia, which
can be used for multiple sequence alignment.
"""
module NetMSA

using StatsBase

export createPeerMatrix, matrixalignment

@doc raw"""
Store the position of a given particle. Position ``x_{s_{i}}(r)`` of the particle
``p_{s_{i}}`` is defined by using the row ``r`` that contains the symbol ``s_i`` as
well as locations of the symbol ``s_i`` in the different columns (indexes of the
columns that contain ``s_i`` ) in the row ``r``.
"""
mutable struct Position
    row::Int64
    indexes::Vector{Int64}
end

"""
A particle that is used for creating swarms.

# Fields
- value::Char : Value of the particle, e.g. 'b' or 'c'
- updated::Int64 : Number of turns till last updated
- pos::Position : The original position of the particle
- best::Position : The best local position of the particle
- bestvalue::Float64 : Best local score
"""
mutable struct Particle
    value::Char
    updated::Int64
    pos::Position
    best::Position
    bestvalue::Float64

    """
        Particle(value::Char, pos::Position)

    Initializes the Particle, where the best position is intialized
    to be the current position.
    """
    function Particle(value::Char, pos::Position)
        return new(value, 0, pos, pos, 0.0)
    end
end

"""
    createPeerMatrix(inputStrings::Vector{String})::Matrix{Union{Missing,Char}}

Create and return a Peer matrix, containing charachters as elements, where each input sequence,
provided in the inputStrings, is represented as a column. Missing values are represented in the
matrix by the `missing` keyword.

# Examples
```jldoctest
julia> NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'
```
"""
function createPeerMatrix(inputStrings::Vector{String})::Matrix{Union{Missing,Char}}
		col_size = size(inputStrings, 1);
		row_size = max([length(s) for s in inputStrings]...);
		peer_matrix = Matrix{Union{Missing,Char}}(missing, row_size, col_size);
		for (i, s) in enumerate(inputStrings)
			peer_matrix[1:length(s), i] = collect(s);
		end
		return peer_matrix;
end

"""
    getposition(value, rowindex, matrix)

Return the Position (rowindex, [colindex1, colindex2, ...]) of the Particle
represented by `value`, at the `rowindex` in the `matrix`.

# Examples
```jldoctest
julia> M = NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

 juila> NetMSA.getposition('b', 2, M)
 NetMSA.Position(2, [1, 3, 4])
```
"""
function getposition(value, rowindex, matrix)
  indexes = findall(i -> i == value, skipmissing(matrix[rowindex, :]))
  return Position(rowindex, indexes);
end

"""
    mostfrequent(row)

Return a tuple containing the most frequent element occuring in the `row`, along
with its frequency.

# Examples
```jldoctest
julia> M = NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

 juila> NetMSA.mostfrequent(M[2, :])
 (3, 'b')
```
"""
function mostfrequent(row)
  counts = countmap(row);
  delete!(counts, '-');
  max = isempty(counts) ? 0 : findmax(counts);
  return max;
end

"""
    aligned(row)::Bool

Return whether a row is aligned or not.

A row is *aligned* if it only contains different occurrences of the same symbol.

# Examples
```jldoctest
julia> M = NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

 juila> NetMSA.aligned(M[1, :])
 true

 juila> NetMSA.aligned(M[2, :])
 false
```
"""
function aligned(row)::Bool
  row = Set(row)
  return (length(row) == 1 && !(missing in row)) || (length(row) == 2 && ('-' in row || missing in row))
end

"""
    aligned(row)::Bool

Return whether a row is full or not.

An aligned row r is called full if no gaps (—) are added in the row r .
That is, the number of occurrences of the symbol in the row is equal to
the number of columns in the matrix.

# Examples
```jldoctest
julia> M = NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

 juila> NetMSA.full(M[1, :])
 true

 juila> NetMSA.full(M[2, :])
 false
```
"""
function full(row)::Bool
  return length(Set(row)) == 1 && !(missing in Set(row))
end

@doc raw"""
    weight(row; w1=0.25, w2=0.5, w3=1.0)

Return the weight of the row, calculated as:

```math
w(r) = \begin{cases}
  w_1  \times  \frac{x}{c}; & \text{ if r is not aligned}   \\
  w_2  \times  \frac{n_s}{c}; & \text{ if r is aligned}   \\
  w_3; & \text{ if r is full}   \\
\end{cases}
```
where ``n_s`` is the number of occurrences of the symbol ``s`` in the aligned row ``r``,
and ``c`` is the total number of columns in the row. The value of ``x`` is equal to zero
if every symbol in the row ``r`` occurred at most once, otherwise ``x`` is equal to the
max number of occurrences (matches) of some symbol in ``r``.

# Examples
```jldoctest
julia> M = NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

 juila> NetMSA.weight(M[1, :])
 1.0

 juila> NetMSA.weight(M[2, :])
 0.1875
```
"""
function weight(row; w1=0.25, w2=0.5, w3=1.0)
  if full(row)
    return w3;
    end

  c = length(row);
  max = mostfrequent(row)[1];

  if aligned(row)
    return w2 * max / c;
  else
    x = max <= 1 ? 0 : max;
    return w1 * x / c;
  end
end

@doc raw"""
    objective(M, rowindex; endindex=0)

Return the objective score of the row, calculated as follows:

```math
f(x_s(r)) = \frac{A(r) \times C(r)}{1 + Gaps(r)} \times  \sum_{j=r}^{k} w(j)
```
where ``A(r)`` is the number of aligned rows in M from ``r`` to the last row,
``C(r)`` is the maximum number of matched charachters in the current row,
``Gaps(r)`` is the number of gaps added to the matrix M from row ``r`` to the
last row, and ``w(r)`` is the weight of the row ``r``.

`endindex` is used to reduce the search area for Gaps, and if it is not provided,
it would default to `size(M)[1]`.

# Examples
```jldoctest
julia> M = NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

 juila> NetMSA.objective(M, 2)
 2.625
```
"""
function objective(M, rowindex; endindex=0)
  weights = sum(weight.(eachrow(M[rowindex:end, :])))
  C = mostfrequent(M[rowindex, :])[1];
  A = sum(aligned.(eachrow(M))[rowindex:end])

  endindex = endindex == 0 ? size(M)[1] : endindex;
  if endindex > size(M)[1]
    throw(ArgumentError("endindex exceeds the matrix size"));
    end

  counts = countmap(M[rowindex:endindex, :]);
  Gaps = get(counts, '-', 0);

  return weights * (A * C) / (1 + Gaps)
end

"""
    createswarm(rowindex::Int64, M)

Create a swarm containing unique Particles in the current row.

# Examples
```jldoctest
julia> M = NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

juila> NetMSA.createswarm(2, M)
2-element Array{NetMSA.Particle,1}:
 NetMSA.Particle('c', 0, NetMSA.Position(2, [2]), NetMSA.Position(2, [2]), 0.0)
 NetMSA.Particle('b', 0, NetMSA.Position(2, [1, 3, 4]), NetMSA.Position(2, [1, 3, 4]), 0.0)
```
"""
function createswarm(rowindex::Int64, M)
  unique = Set(skipmissing(M[rowindex, :]))
  swarm = Vector{Particle}(undef, length(unique))
  for (i, c) in enumerate(unique)
    swarm[i] = Particle(c, getposition(c, rowindex, M))
  end
  return swarm
end

function criteria3(p::Particle, newindex::Int, M)
  return length(p.pos.indexes) != length(getposition(p.value, newindex, M).indexes)
end

function criteria2(p::Particle; threshold::Int=5)
return p.updated > threshold;
end

"""
    stopcriteria(p::Particle, newindex, M; threshold::Int=5, debug=false)

Check whether a stopping criteria has been met. Two stopping criteria
are checked in this function:
  - Criteria 2: If a particle hasn't updated its best score in the last `threshold` turns.
  - Criteria 3: If a particle moves to a new row which already contains the same symbol as that of the particle.

# Examples
```jldoctest
julia> M = NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

juila> p = NetMSA.Particle('b', NetMSA.getposition('b', 2, M));
NetMSA.Particle('b', 0, Main.NetMSA.Position(2, [1, 3, 4]), Main.NetMSA.Position(2, [1, 3, 4]), 0.0)

julia> NetMSA.stopcriteria(p, 3, M; debug=true)
"Terminating because of criteria 3"
true
```
"""
function stopcriteria(p::Particle, newindex, M; threshold::Int=5, debug=false)
  c3 = criteria3(p, newindex, M);
  c2 = criteria2(p; threshold=threshold);
  if debug && c3
    display("Terminating because of criteria 3")
  elseif debug && c2
    display("Terminating because of criteria 2")
  end
  return c3 || c2;
end

function remove_missing_rows(M)
  return M[[length(Set(skipmissing(r))) != 0 for r in eachrow(M)], :]
end

"""
    flydown(p, M; stride=1)

Fly down the given particle by `stride`.

# Examples
```jldoctest
julia> M = NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

juila> p = NetMSA.Particle('b', NetMSA.getposition('b', 2, M));
NetMSA.Particle('b', 0, Main.NetMSA.Position(2, [1, 3, 4]), Main.NetMSA.Position(2, [1, 3, 4]), 0.0)

julia> NetMSA.flydown(p, M)
9×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 '-'  'c'      '-'      '-'
 'b'  'b'      'b'      'b'
 'c'  'c'      'c'      'c'
 'b'  'f'      'h'      'b'
 'c'  'g'      'i'      'c'
 'd'  missing  'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

julia> NetMSA.flydown(p, M; stride=3)
11×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 '-'  'c'      '-'      '-'
 '-'  'b'      '-'      '-'
 '-'  'c'      '-'      '-'
 'b'  'f'      'b'      'b'
 'c'  'g'      'c'      'c'
 'b'  missing  'h'      'b'
 'c'  missing  'i'      'c'
 'd'  missing  'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'
```
"""
function flydown(p, M; stride=1)
  notpcols = setdiff(collect(1:size(M, 2)), p.pos.indexes)
  colsize = size(M, 2)
  pos = p.pos
  newrows = fill('-', (stride, colsize))
  M = vcat(M[1:pos.row - 1, :], reshape(newrows, stride, colsize), M[pos.row:end, :])
  for i in collect(pos.row + stride:size(M, 1))
    M[i - stride,notpcols] = M[i, notpcols]
    M[i, notpcols] .= missing
  end
  M = remove_missing_rows(M)
end

"""
    rowalignment(rowindex, M)

Return Particle with best position that aligns (maximizes the objective score)
the given row in the matrix.

# Examples
```jldoctest
julia> M = NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

juila> p = NetMSA.Particle('b', NetMSA.getposition('b', 2, M));
NetMSA.Particle('b', 0, Main.NetMSA.Position(2, [1, 3, 4]), Main.NetMSA.Position(2, [1, 3, 4]), 0.0)

julia> NetMSA.rowalignment(2, M)
NetMSA.Particle('c', 0, Main.NetMSA.Position(2, [2]), Main.NetMSA.Position(3, [1]), 9.0)
```
"""
function rowalignment(rowindex, M)
  row = M[rowindex, :];
  if aligned(row)
    return nothing;
  end

  swarm = createswarm(rowindex, M);

  gₒ = g = swarm[1];
  gₒvalue = gvalue = objective(M, rowindex, endindex=rowindex);
  cols = size(row, 1)

  for p in swarm

    t = rowindex;
    N = copy(M);

    p.bestvalue = objective(M, rowindex, endindex=t)

    missingp = setdiff(collect(1:size(N, 2)), p.pos.indexes)
    maxlen = maximum([length(collect(skipmissing(col))) for col in eachcol(N[:, missingp])])
    criteria1 = maxlen;

    while stopcriteria(p, t, N) != true && t < criteria1
      t += 1;
      p.updated += 1;

      N = flydown(p, N);
      score = objective(N, rowindex);
      if score > p.bestvalue
        p.bestvalue = score;
        p.updated = 0;
      end

      if score > gvalue
        gvalue = score;
        g = deepcopy(p);
        g.best = getposition(p.value, t, N);
        g.bestvalue = score;
    end

    end
  end

  if gvalue == gₒvalue
    return nothing;
  end

  return g;
end

"""
    matrixalignment(M)

Align the matrix using NetMSA algorithm.

# Examples
```jldoctest
julia> M = NetMSA.createPeerMatrix(["abcbcdem", "acbcfg", "abchimn", "abcbcjkm"])
8×4 Array{Union{Missing, Char},2}:
 'a'  'a'      'a'      'a'
 'b'  'c'      'b'      'b'
 'c'  'b'      'c'      'c'
 'b'  'c'      'h'      'b'
 'c'  'f'      'i'      'c'
 'd'  'g'      'm'      'j'
 'e'  missing  'n'      'k'
 'm'  missing  missing  'm'

juila> NetMSA.matrixalignment(M)
9×4 Array{Union{Missing, Char},2}:
 'a'  'a'  'a'  'a'
 'b'  '-'  'b'  'b'
 'c'  'c'  'c'  'c'
 'b'  'b'  '-'  'b'
 'c'  'c'  '-'  'c'
 'd'  'f'  'h'  'j'
 'e'  'g'  'i'  'k'
 'm'  '-'  'm'  'm'
 '-'  '-'  'n'  '-'
```
"""
function matrixalignment(M)
  for (index, row) in enumerate(eachrow(M))
    g = rowalignment(index, M)
    if !isnothing(g)
      M = flydown(g, M, stride=g.best.row - g.pos.row)
    end
  end
  replace!(M, missing => '-')
end

end


"""
Functions to run quantum error correction simulations and merge/read/write output data.
"""
module App

# imports
using ..Model
using ..PauliTools:bsp, pack, weight
using JSON
using Random:AbstractRNG, GLOBAL_RNG, MersenneTwister
using Statistics:var

# exports
export RunResult, qec_merge, qec_read, qec_run, qec_run_once, qec_write

@doc raw"""
    qec_run_once(
        code, error_model, decoder, p::Real, rng::AbstractRNG=GLOBAL_RNG
    ) -> RunResult

Execute a stabilizer code error-decode-recovery (ideal) simulation and return run result.

The parameters `code`, `error_model` and `decoder` should be concrete subtypes or duck-typed
implementations of [`StabilizerCode`](@ref), [`ErrorModel`](@ref) and [`Decoder`](@ref),
respectively.

The simulation algorithm is as follows:

1. ``S ←`` `stabilizers(code)`
2. ``L ←`` `logicals(code)`
3. ``e ←`` `generate(error_model, code, p, rng)`
4. ``y ← S ⊙ e``
5. `decode_result` ``←`` `decode(decoder, code,` ``y```; kwargs...)`
6. ``r ←`` `decode_result.recovery`
7. sanity check: ``S ⊙ (r ⊕ e) = 0``
8. `logical_commutations` ``← L ⊙ (r ⊕ e)``
8. `success` ``← L ⊙ (r ⊕ e) = 0``

where ``⊕`` denotes element-wise exclusive-or, and ``⊙`` is defined in
[`PauliTools.bsp`](@ref bsp).

The `kwargs` passed to [`decode`](@ref) include `error_model`, `p` and `error`; most
decoders will ignore these parameters. The [`decode`](@ref) method returns a
[`DecodeResult`](@ref). If `decode_result.success` and/or
`decode_result.logical_commutations` are specified, they override the values of `success`
and `logical_commutations`, irrespective of whether `decode_result.recovery` is specified or
not. The value `decode_result.custom_values` is passed through in the run result.

See also [`RunResult`](@ref).

# Examples
```jldoctest
julia> using Qecsim.BasicModels, Qecsim.GenericModels, Random

julia> rng = MersenneTwister(6);  # use random seed for reproducible result

julia> qec_run_once(FiveQubitCode(), DepolarizingErrorModel(), NaiveDecoder(), 0.2, rng)
RunResult{Nothing}(false, 2, Bool[1, 0], nothing)
```
"""
function qec_run_once(code, error_model, decoder, p::Real, rng::AbstractRNG=GLOBAL_RNG)
    error = generate(error_model, code, p, rng)
    @debug "qec_run_once: error=$(error)" error
    syndrome = bsp(stabilizers(code), error)
    @debug "qec_run_once: syndrome=$(syndrome)" syndrome
    ctx = Dict(:error_model => error_model, :p => p, :error => error)
    result = decode(decoder, code, syndrome; ctx...)
    @debug "qec_run_once: result=$(result)" result
    success, l_commutations = _resolve_decoding(result.recovery, result.success,
        result.logical_commutations, code, error)
    @debug "qec_run_once: success=$(success)"
    @debug "qec_run_once: logical_commutations=$(l_commutations)"
    return RunResult(success, weight(error), l_commutations, result.custom_values)
end

# resolve tuple (success, logical_commutations)
function _resolve_decoding(::Nothing, success, logical_commutations, code, error)
    # recovery is null, so just return overrides
    return (success, logical_commutations)
end
function _resolve_decoding(recovery, success, logical_commutations, code, error)
    # recovery is not null, so evaluate success and logical_commutations
    recovered = xor.(error, recovery)
    s_comms = bsp(stabilizers(code), recovered)
    l_comms = bsp(logicals(code), recovered)
    s_commutes = !any(s_comms)
    l_commutes = !any(l_comms)
    if !s_commutes
        @warn "RECOVERY DOES NOT RETURN TO CODESPACE" pack(error) pack(recovery)
    end
    # derived overrides
    dsuccess = isnothing(success) ? s_commutes && l_commutes : success
    dl_comms = isnothing(logical_commutations) ? l_comms : logical_commutations
    return (dsuccess, dl_comms)
end

"""
    RunResult(
        success::Bool,
        error_weight::Int,
        logical_commutations::Union{Nothing,BitVector}
        custom_values::Union{Nothing,Vector}
    )

Construct a run result as returned by [`qec_run_once`](@ref).

# Examples
```jldoctest
julia> r = RunResult(false, 2, BitVector([0, 1]), [1.2, 3.1])
RunResult{Vector{Float64}}(false, 2, Bool[0, 1], [1.2, 3.1])

julia> r.success, r.error_weight, r.logical_commutations, r.custom_values
(false, 2, Bool[0, 1], [1.2, 3.1])
```
"""
struct RunResult{T <: Union{Nothing,Vector}}
    success::Bool
    error_weight::Int
    logical_commutations::Union{Nothing,BitVector}
    custom_values::T
end
# equality methods TODO: write equality macro
function Base.hash(a::RunResult, h::UInt)
    h = hash(typeof(a), h)
    h = hash(a.success, h)
    h = hash(a.error_weight, h)
    h = hash(a.logical_commutations, h)
    h = hash(a.custom_values, h)
    return h
end
function Base.:(==)(a::RunResult, b::RunResult)
    return (a.success == b.success
        && a.error_weight == b.error_weight
        && a.logical_commutations == b.logical_commutations
        && a.custom_values == b.custom_values)
end
function Base.isequal(a::RunResult, b::RunResult)
    return (isequal(a.success, b.success)
        && isequal(a.error_weight, b.error_weight)
        && isequal(a.logical_commutations, b.logical_commutations)
        && isequal(a.custom_values, b.custom_values))
end

@doc raw"""
    qec_run(
        code, error_model, decoder, p::Real, random_seed=nothing;
        max_runs::Union{Integer,Nothing}=nothing,
        max_failures::Union{Integer,Nothing}=nothing
    ) -> Dict

Execute stabilizer code error-decode-recovery (ideal) simulations many times and return
aggregated run data, see [`qec_run_once`](@ref) for details of a single run.

The parameters `code`, `error_model` and `decoder` should be concrete subtypes or duck-typed
implementations of [`StabilizerCode`](@ref), [`ErrorModel`](@ref) and [`Decoder`](@ref),
respectively.

Simulations are run one or more times as determined by `max_runs` and `max_failures`. If
`max_runs` and/or `max_failures` are specified, stop after `max_runs` runs or `max_failures`
failures, whichever happens first. If neither is specified, stop after one run.

The returned aggregated data has the following format:

    Dict(
        :code => "5-qubit"              # label(code)
        :n_k_d => (5, 1, 3)             # nkd(code)
        :time_steps => 1                # always 1 for ideal simulations
        :error_model => "Depolarizing"  # label(error_model)
        :decoder => "Naive"             # label(decoder)
        :error_probability => 0.1       # p
        :measurement_error_probability => 0.0   # always 0.0 for ideal simulations
        :n_run => 100                   # count of runs
        :n_success => 92                # count of successful recoveries
        :n_fail => 8                    # count of failed recoveries
        :n_logical_commutations => [5, 6]   # count of logical_commutations
        :custom_totals => nothing       # sum of custom_values
        :error_weight_total => 55       # sum of error_weight over n_run runs
        :error_weight_pvar => 0.4075    # pvariance of error_weight over n_run runs
        :logical_failure_rate => 0.08   # n_fail / n_run
        :physical_error_rate => 0.11    # error_weight_total / n_k_d[1] / time_steps / n_run
        :wall_time => 0.00253906        # wall-time for run in fractional seconds
    )

# Examples
```jldoctest; filter = r":wall_time +=> \d*\.?\d*"
julia> using Qecsim.BasicModels, Qecsim.GenericModels

julia> seed = 7;

julia> data = qec_run(FiveQubitCode(), DepolarizingErrorModel(), NaiveDecoder(), 0.1, seed;
           max_runs=100);
┌ Info: qec_run: starting
│   code = Qecsim.BasicModels.BasicCode(["XZZXI", "IXZZX", "XIXZZ", "ZXIXZ"], ["XXXXX"], ["ZZZZZ"], (5, 1, 3), "5-qubit")
│   error_model = Qecsim.GenericModels.DepolarizingErrorModel()
│   decoder = Qecsim.GenericModels.NaiveDecoder(10)
│   p = 0.1
│   random_seed = 7
│   max_runs = 100
└   max_failures = nothing
[ Info: qec_run: rng=MersenneTwister(7)
[ Info: qec_run: complete: data=Dict{Symbol, Any}(:error_weight_pvar => 0.4075000000000001, :time_steps => 1, :n_logical_commutations => [5, 6], :error_weight_total => 55, :wall_time => 0.002539058, :n_k_d => (5, 1, 3), :error_model => "Depolarizing", :physical_error_rate => 0.11, :measurement_error_probability => 0.0, :error_probability => 0.1, :n_success => 92, :logical_failure_rate => 0.08, :custom_totals => nothing, :code => "5-qubit", :decoder => "Naive", :n_fail => 8, :n_run => 100)

julia> data
Dict{Symbol, Any} with 17 entries:
  :error_weight_pvar             => 0.4075
  :time_steps                    => 1
  :n_logical_commutations        => [5, 6]
  :error_weight_total            => 55
  :wall_time                     => 0.00253906
  :n_k_d                         => (5, 1, 3)
  :error_model                   => "Depolarizing"
  :physical_error_rate           => 0.11
  :measurement_error_probability => 0.0
  :error_probability             => 0.1
  :n_success                     => 92
  :logical_failure_rate          => 0.08
  :custom_totals                 => nothing
  :code                          => "5-qubit"
  :decoder                       => "Naive"
  :n_fail                        => 8
  :n_run                         => 100
```
"""
function qec_run(
    code, error_model, decoder, p::Real, random_seed=nothing;
    max_runs::Union{Integer,Nothing}=nothing, max_failures::Union{Integer,Nothing}=nothing,
)
    # derived defaults
    dmax_runs = isnothing(max_runs) && isnothing(max_failures) ? 1 : max_runs

    @info "qec_run: starting" code = code error_model = error_model decoder = decoder #=
        =# p = p random_seed = random_seed max_runs = dmax_runs max_failures = max_failures
    wall_time_start = time_ns()

    rng = MersenneTwister(random_seed)
    @info "qec_run: rng=$(rng)"

    # run counters
    n_run = n_success = 0
    error_weights = Int[]
    if !isnothing(dmax_runs) sizehint!(error_weights, dmax_runs) end
    n_logical_commutations::Union{Nothing,Vector{Int}} = nothing  # promote to Int[]
    custom_totals = nothing

    # do runs
    while ((isnothing(dmax_runs) || n_run < dmax_runs)
        && (isnothing(max_failures) || (n_run - n_success) < max_failures)
    )
        data = qec_run_once(code, error_model, decoder, p, rng)
        n_run += 1
        n_success += data.success ? 1 : 0
        push!(error_weights, data.error_weight)
        if n_run == 1  # initialize vector sums
            n_logical_commutations = _null_vec_copy(data.logical_commutations)
            custom_totals = _null_vec_copy(data.custom_values)
        else  # update vector sums
            _null_vec_add!(n_logical_commutations, data.logical_commutations)
            _null_vec_add!(custom_totals, data.custom_values)
        end
    end

    # prepare data
    runs_data = Dict(
        :code => label(code),
        :n_k_d => nkd(code),
        :time_steps => 1,
        :error_model => label(error_model),
        :decoder => label(decoder),
        :error_probability => p,
        :measurement_error_probability => 0.0,
        :n_run => n_run,
        :n_success => n_success,
        :n_fail => n_run - n_success,
        :n_logical_commutations => n_logical_commutations,
        :custom_totals => custom_totals,
        :error_weight_total => sum(error_weights),
        :error_weight_pvar => var(error_weights; corrected=false),
        :logical_failure_rate => 0.0,  # set below
        :physical_error_rate => 0.0,  # set below
        :wall_time => 0.0,  # set below
    )
    # set rate statistics
    _rate_statistics!(runs_data)

    runs_data[:wall_time] = (time_ns() - wall_time_start) / 10^9
    @info "qec_run: complete: data=$(runs_data)"

    return runs_data
end

# return deepcopy of val (or nothing if null)
_null_vec_copy(::Nothing) = nothing
_null_vec_copy(vec::AbstractVector) = deepcopy(vec)
# sum scalars or vcat arrays
_sum_vec_el(el1, el2) = el1 + el2
_sum_vec_el(el1::AbstractArray, el2::AbstractArray) = vcat(el1, el2)
# update vec1 with vec2 (sum scalars / vcat arrays) (or nothing if both null)
_null_vec_add!(::Nothing, ::Nothing) = nothing
_null_vec_add!(vec1::AbstractVector, vec2::AbstractVector) = vec1 .= _sum_vec_el.(vec1,vec2)

# set :logical_failure_rate and :physical_error_rate as defined in qec_run
function _rate_statistics!(runs_data)
    # extract data
    time_steps = runs_data[:time_steps]
    n_run = runs_data[:n_run]
    n_fail = runs_data[:n_fail]
    error_weight_total = runs_data[:error_weight_total]
    nkd = runs_data[:n_k_d][1]
    # add rate statistics
    runs_data[:logical_failure_rate] = n_fail / n_run
    runs_data[:physical_error_rate] = error_weight_total / nkd[1] / time_steps / n_run
    return nothing
end

@doc raw"""
    qec_merge(data...) -> Vector{Dict}

Merge simulation run data.

Run data is expected in the format specified by [`qec_run`](@ref). Merged data is grouped
by: `(:code, :n_k_d, :error_model, :decoder, :error_probability, :time_steps,
:measurement_error_probability)`. The scalar values: `:n_run`, `:n_success`,
`:n_fail`, `:error_weight_total` and `:wall_time` are summed. The vector value:
`:n_logical_commutations` is summed element-wise. The vector value: `:custom_totals` is
summed element-wise for scalar elements and concatenated along dimension 1 (`vcat`) for
array elements. The values: `:logical_failure_rate` and `:physical_error_rate` are
recalculated. The value `:error_weight_pvar` is *not* currently recalculated and is
therefore omitted.

# Examples
```jldoctest; filter = r":wall_time +=> \d*\.?\d*"
julia> using Qecsim.BasicModels, Qecsim.GenericModels, Logging

julia> code, error_model, decoder = FiveQubitCode(), BitFlipErrorModel(), NaiveDecoder();

julia> data = Dict[];

julia> with_logger(NullLogger()) do # disable logging for brevity in doctest
           push!(data, qec_run(code, error_model, decoder, 0.08, 19; max_runs=100))
           push!(data, qec_run(code, error_model, decoder, 0.08, 23; max_runs=100))
       end;

julia> qec_merge(data...)
1-element Vector{Dict{Symbol, Any}}:
 Dict(:measurement_error_probability => 0.0, :error_probability => 0.08, :time_steps => 1, :error_weight_total => 83, :n_logical_commutations => [11, 3], :wall_time => 0.0028351720000000004, :n_k_d => (5, 1, 3), :error_model => "Bit-flip", :n_success => 189, :logical_failure_rate => 0.055…)
```
"""
function qec_merge(data...)
    # define group keys, value keys and zero values
    grp_keys = (:code, :n_k_d, :error_model, :decoder, :error_probability, :time_steps,
                :measurement_error_probability)
    scalar_val_keys = (:n_run, :n_fail, :n_success, :error_weight_total, :wall_time)
    scalar_zero_vals = (0, 0, 0, 0, 0.0)
    vector_val_keys = (:n_logical_commutations, :custom_totals,)
    # map of groups to sums
    grps_to_scalar_sums = Dict()
    grps_to_vector_sums = Dict()
    # iterate through single list from given data lists
    for runs_data in data
        # define defaults, create new data with defaults overwritten by data
        # support for 0.10 and 0.15 files:
        defaults_0_16 = Dict(:time_steps => 1, :measurement_error_probability => 0.0)
        # support for pre-1.0b6 files:
        defaults_1_0b6 = Dict(:n_logical_commutations => nothing, :custom_totals => nothing)
        # runs_data = Dict(defaults_0_16..., defaults_1_0b6..., runs_data...)
        runs_data = merge(defaults_0_16, defaults_1_0b6, runs_data)
        # extract group from data
        grp_id = Tuple(runs_data[k] for k in grp_keys)
        # scalars: e.g. (10, 6, 4, 256, 10.34) extracted from data
        scalar_vals = Tuple(runs_data[k] for k in scalar_val_keys)
        scalar_sums = get(grps_to_scalar_sums, grp_id, scalar_zero_vals)  # get sums
        scalar_sums = Tuple(sum(x) for x in zip(scalar_sums, scalar_vals))  # update sums
        grps_to_scalar_sums[grp_id] = scalar_sums  # put sums
        # vectors: e.g. ([2, 5], [3, 8, 2], nothing) extracted from data
        vector_vals = Tuple(_null_vec_copy(runs_data[k]) for k in vector_val_keys)
        if haskey(grps_to_vector_sums, grp_id)  # sums already in map
            vector_sums = grps_to_vector_sums[grp_id]  # get sums
            vector_sums = Tuple(_null_vec_add!(s, v)  # update sums
                                for (s, v) in zip(vector_sums, vector_vals))
        else  # sums not in map yet
            vector_sums = vector_vals  # update sums
        end
        grps_to_vector_sums[grp_id] = vector_sums  # put sums
    end
    # flatten grps_to_scalar_sums and grps_to_vector_sums
    merged_data_list = [Dict(
        zip((grp_keys..., scalar_val_keys..., vector_val_keys...),
            (grp_id..., scalar_sums..., grps_to_vector_sums[grp_id]...))
        ) for (grp_id, scalar_sums) in grps_to_scalar_sums]
    # update rate statistics
    for runs_data in merged_data_list
        _rate_statistics!(runs_data)
    end
    return merged_data_list
end

@doc raw"""
    qec_write(io::IO, data...)
    qec_write(filename::AbstractString, data...)

Write simulation run data to the given I/O stream or file.

Run data is expected in the format specified by [`qec_run`](@ref) and written as a JSON
array of objects using default JSON encoding. No checking of the format of the given data is
performed. The file version of this method will refuse to overwrite an existing file,
instead attempting to log the unwritten data and throwing an exception.

The JSON format is compatible with the format used by the Python package
[qecsim](https://github.com/qecsim/qecsim).

See also [`qec_read`](@ref).
"""
function qec_write(io::IO, data...)
    JSON.print(io, data)
end
function qec_write(filename::AbstractString, data...)
    try
        # throw error if file exists
        !ispath(filename) || error("file \"$filename\" exists; refusing to overwrite")
        open(io -> qec_write(io, data...), filename, "w")
    catch
        @error "Recovered data: $(JSON.json(data))"
        rethrow()
    end
end

@doc raw"""
    qec_read(io::IO) -> Vector{Dict{Symbol,Any}}
    qec_read(filename::AbstractString) -> Vector{Dict{Symbol,Any}}

Read simulation run data from the given I/O stream or file.

Run data is expected in the format written by [`qec_write`](@ref) (i.e. a JSON
array of objects using default JSON encoding of the format specified by [`qec_run`](@ref)).
The read data is converted to the specified return type as follows: dictionary keys are
converted to symbols, `:n_k_d` entries are converted to tuples, and vector types are
inferred from their elements. If this conversion fails, an exception is thrown.

The JSON format is compatible with the format used by the Python package
[qecsim](https://github.com/qecsim/qecsim).

See also [`qec_write`](@ref).
"""
function qec_read(io::IO)::Vector{Dict{Symbol,Any}}
    # load into expected raw format
    raw_data::Vector{Dict{String,Any}} = JSON.parse(io)
    # convert type of keys to Symbol
    data = [Dict(Symbol(k) => v for (k, v) in d) for d in raw_data]
    # convert type of values
    for d in data
        if haskey(d, :n_k_d)  # convert type of :n_k_d to Tuple
            d[:n_k_d] = Tuple(d[:n_k_d])
        end
        for (k, v) in d  # narrow type of vector values from Any
            if isa(v, AbstractVector)
                d[k] = [v...]
            end
        end
    end
    return data
end
function qec_read(filename::AbstractString)::Vector{Dict{Symbol,Any}}
    return open(io -> qec_read(io), filename, "r")
end

end


"""
    type_complexity(T::Type) -> c

Return an integer `c` representing a measure of the "complexity" of
type `T`. For unnested types, `c = n+1` where `n` is the number of
parameters in type `T`. However, `type_complexity` calls itself
recursively on the parameters, so if the parameters have their own
parameters then the complexity of the type will be (potentially much)
higher than this.

# Examples

    # Array{Float64,2}
    julia> a = rand(3,5);

    julia> type_complexity(typeof(a))
    3

    julia> length(typeof(a).parameters)+1
    3

    # Create an object that has type:
    #    SubArray{Int64,2,Base.ReshapedArray{Int64,2,UnitRange{Int64},Tuple{}},Tuple{StepRange{Int64,Int64},Colon},false}
    julia> r = reshape(1:9, 3, 3);

    julia> v = view(r, 1:2:3, :);

    julia> type_complexity(typeof(v))
    15

    julia> length(typeof(v).parameters)+1
    6

The second example indicates that the total complexity of `v`'s type
is considerably higher than the complexity of just its "outer"
SubArray type.
"""
function type_complexity(::Type{T}) where T
    if isa(T, Union)
        1 + type_complexity(T.b)
    else
        isempty(T.parameters) ? 1 : sum(type_complexity, T.parameters)+1
    end
end
type_complexity(::Type{Union{}}) = 1
type_complexity(x)               = 1

# Fallback definitions
"""
    showarg(stream::IO, x)

Show `x` as if it were an argument to a function. This function is
used in the printing of "type summaries" in terms of sequences of
function calls on objects.

The fallback definition is to print `x` as `::\$(typeof(x))`,
representing argument `x` in terms of its type. However, you can
specialize this function for specific types to customize printing.

# Example

A SubArray created as `view(a, :, 3, 2:5)`, where `a` is a
3-dimensional Float64 array, has type

    SubArray{Float64,2,Array{Float64,3},Tuple{Colon,Int64,UnitRange{Int64}},false}

and this type will be printed in the summary. To change the printing of this object to

    view(::Array{Float64,3}, Colon(), 3, 2:5)

you could define

    function ShowItLikeYouBuildIt.showarg(io::IO, v::SubArray)
        print(io, "view(")
        showarg(io, parent(v))
        print(io, ", ", join(v.indexes, ", "))
        print(io, ')')
    end

Note that we're calling `showarg` recursively for the parent array
type.  Printing the parent as `::Array{Float64,3}` is the fallback
behavior, assuming no specialized method for `Array` has been defined.
More generally, this would display as

    view(<a>, Colon(), 3, 2:5)

where `<a>` is the output of `showarg` for `a`.

This printing might be activated any time `v` is a field in some other
container, or if you specialize `Base.summary` for `SubArray` to call
`summary_build`.

See also: summary_build.
"""
showarg(io::IO, ::Type{T}) where {T} = print(io, "::Type{", T, "}")
showarg(io::IO, x) = print(io, "::", typeof(x))

function summary_build(io::IO, A::AbstractArray, cthresh=default_cthresh(A))
    type_complexity(typeof(A)) <= cthresh && return show_summary_type(io, A)
    print(io, dimstring(indices(A)), ' ')
    showarg(io, A)
    print(io, " with element type ", eltype(A))
end

"""
    summary_build(A::AbstractArray, [cthresh])

Return a string representing `A` in terms of the sequence of function
calls that might be used to create `A`, along with information about
`A`'s size or indices and element type. This function should never be
called directly, but instead used to specialize `Base.summary` for
specific `AbstractArray` subtypes. For example, if you want to change
the summary of SubArray, you might define

    Base.summary(v::SubArray) = summary_build(v)

This function goes hand-in-hand with `showarg`. If you have defined a
`showarg` method for `SubArray` as in the documentation for `showarg`,
then the summary of a SubArray might look like this:

    3×4 view(::Array{Float64,3}, :, 3, 2:5) with element type Float64

instead of this:

    3×4 SubArray{Float64,2,Array{Float64,3},Tuple{Colon,Int64,UnitRange{Int64}},false}

The optional argument `cthresh` is a "complexity threshold"; objects
with type descriptions that are less complex than the specified
threshold will be printed using the traditional type-based
summary. The default value is `n+1`, where `n` is the number of
parameters in `typeof(A)`.  The complexity is calculated with
`type_complexity`. You can choose a `cthresh` of 0 if you want to
ensure that your `showarg` version is always used.

See also: showarg, type_complexity.
"""
function summary_build(A::AbstractArray, cthresh=default_cthresh(A))
    io = IOBuffer()
    summary_build(io, A, cthresh)
    String(io)
end

# This is effectively the "original summary" as defined in Base. We
# can't just use that function directly because `summary_build`
# should only be called by `summary` method that has been specialized
# for the type, so `summary` isn't available as a fallback.
show_summary_type(io::IO, A::AbstractArray) = print(io, dimstring(indices(A)), ' ', typeof(A))

default_cthresh(x) = default_cthresh(typeof(x))
default_cthresh(::Type{T}) where {T} = length(T.parameters)+1


# ---
# title: 5. Longest Palindromic Substring
# id: problem5
# author: Tian Jun
# date: 2020-10-31
# difficulty: Medium
# categories: String, Dynamic Programming
# link: <https://leetcode.com/problems/longest-palindromic-substring/description/>
# hidden: true
# ---
#
# Given a string `s`, return  _the longest palindromic substring_ in `s`.
#
#
#
# **Example 1:**
#
#
#
#     Input: s = "babad"
#     Output: "bab"
#     **Note:** "aba" is also a valid answer.
#
#
# **Example 2:**
#
#
#
#     Input: s = "cbbd"
#     Output: "bb"
#
#
# **Example 3:**
#
#
#
#     Input: s = "a"
#     Output: "a"
#
#
# **Example 4:**
#
#
#
#     Input: s = "ac"
#     Output: "a"
#
#
#
#
# **Constraints:**
#
#   * `1 <= s.length <= 1000`
#   * `s` consist of only digits and English letters (lower-case and/or upper-case),
#
#
## @lc code=start
using LeetCode

function longest_palindrome(s::String)::AbstractString
    res = ""
    for i in 1:length(s)
        s_odd = _longest_palindrome(s, i, i)
        if length(s_odd) > length(res)
            res = s_odd
        end
        s_even = _longest_palindrome(s, i, i + 1)
        if length(s_even) > length(res)
            res = s_even
        end
    end
    return res
end

function _longest_palindrome(s, l, r)
    while l >= 1 && r <= length(s) && s[l] == s[r]
        l -= 1
        r += 1
    end
    return SubString(s, l + 1, r - 1)
end
## @lc code=end


"""
    NestedSamplers.Proposals

This module contains the different algorithms for proposing new points within a bounding volume in unit space.

The available implementations are
* [`Proposals.Rejection`](@ref) - samples uniformly within the bounding volume
* [`Proposals.RWalk`](@ref) - random walks to a new point given an existing one
* [`Proposals.RStagger`](@ref) - random staggering away to a new point given an existing one
* [`Proposals.Slice`](@ref) - slicing away to a new point given an existing one
* [`Proposals.RSlice`](@ref) - random slicing away to a new point given an existing one
"""
module Proposals

using ..NestedSamplers: prior_transform_and_loglikelihood
using ..Bounds

using Random
using LinearAlgebra
using Parameters

export AbstractProposal

"""
    NestedSamplers.AbstractProposal

The abstract type for live point proposal algorithms.

# Interface

Each `AbstractProposal` must have this function,
```julia
(::AbstractProposal)(::AbstractRNG, point, loglstar, bounds, loglikelihood, prior_transform)
```
which, given the input `point` with loglikelihood `loglstar` inside a `bounds`, returns a new point in unit space, prior space, the loglikelihood, and the number of function calls.
"""
abstract type AbstractProposal end

# ----------------------------------------

# Helper for checking unit-space bounds
unitcheck(us) = all(u -> 0 < u < 1, us)

"""
    Proposals.Rejection(;maxiter=100_000)

Propose a new live point by uniformly sampling within the bounding volume and rejecting samples that do not meet the likelihood constraints. This follows the original nested sampling algorithm proposed in Skilling (2004)[^1]

[^1]: John Skilling, 2004, AIP 735, 395  ["Nested Sampling"](https://aip.scitation.org/doi/abs/10.1063/1.1835238)

## Parameters
- `maxiter` is the maximum number of samples that can be rejected before giving up and throwing an error.
"""
Base.@kwdef struct Rejection <: AbstractProposal
    maxiter::Int = 100_000
end

@deprecate Uniform() Rejection()

function (prop::Rejection)(
    rng::AbstractRNG,
    point::AbstractVector,
    logl_star,
    bounds::AbstractBoundingSpace,
    model
)

    ncall = 0
    for _ in 1:prop.maxiter
        u = rand(rng, bounds)
        unitcheck(u) || continue
        v, logl = prior_transform_and_loglikelihood(model, u)
        ncall += 1
        logl ≥ logl_star && return u, v, logl, ncall
    end
    throw(ErrorException("Couldn't generate a proper point after $(prop.maxiter) attempts including $ncall likelihood calls. Bounds=$bounds, logl_star=$logl_star."))
end

Base.show(io::IO, p::Rejection) = print(io, "NestedSamplers.Proposals.Rejection")

"""
    Proposals.RWalk(;ratio=0.5, walks=25, scale=1)

Propose a new live point by random walking away from an existing live point. This follows the algorithm outlined in Skilling (2006).[^5]

[^5]: Skilling, 2006, Bayesian Anal. 1(4), ["Nested sampling for general Bayesian computation"](https://projecteuclid.org/journals/bayesian-analysis/volume-1/issue-4/Nested-sampling-for-general-Bayesian-computation/10.1214/06-BA127.full)

## Parameters
- `ratio` is the target acceptance ratio
- `walks` is the minimum number of steps to take
- `scale` is the proposal distribution scale, which will update _between_ proposals.
"""
@with_kw mutable struct RWalk <: AbstractProposal
    ratio = 0.5
    walks = 25
    scale = 1.0

    @assert 1 / walks ≤ ratio ≤ 1 "Target acceptance ratio must be between 1/`walks` and 1"
    @assert walks > 1 "Number of steps must be greater than 1"
    @assert scale ≥ 0 "Proposal scale must be non-negative"
end

function (prop::RWalk)(
    rng::AbstractRNG,
    point::AbstractVector,
    logl_star,
    bounds::AbstractBoundingSpace,
    model;
    kwargs...
)
    # setup
    n = length(point)
    scale_init = prop.scale
    accept = reject = fail = nfail = nc = ncall = 0
    local du, u_prop, logl_prop, u, v, logl

    while nc < prop.walks || iszero(accept)
        # get proposed point
        while true
            # check scale factor to avoid over-shrinking
            prop.scale < 1e-5 * scale_init && error("Random walk sampling appears to be stuck.")
            # transform to proposal distribution
            du = randoffset(rng, bounds)
            u_prop = @. point + prop.scale * du
            # inside unit-cube
            unitcheck(u_prop) && break

            fail += 1
            nfail += 1
            # check if stuck generating bad numbers
            if fail > 100 * prop.walks
                @warn "Random number generation appears extremely inefficient. Adjusting the scale-factor accordingly"
                fail = 0
                prop.scale *= exp(-1/n)
            end
        end
        # check proposed point
        v_prop, logl_prop = prior_transform_and_loglikelihood(model, u_prop)
        if logl_prop ≥ logl_star
            u = u_prop
            v = v_prop
            logl = logl_prop
            accept += 1
        else
            reject += 1
        end
        nc += 1
        ncall += 1

        # check if stuck generating bad points
        if nc > 50 * prop.walks
            @warn "Random walk proposals appear to be extremely inefficient. Adjusting the scale-factor accordingly"
            prop.scale *= exp(-1/n)
            nc = accept = reject = 0
        end
    end

    # update proposal scale using acceptance ratio
    update_scale!(prop, accept, reject, n)

    return u, v, logl, ncall
end

# update proposal scale using target acceptance ratio
function update_scale!(prop, accept, reject, n)
    ratio = accept / (accept + reject)
    norm = max(prop.ratio, 1 - prop.ratio) * n
    scale = prop.scale * exp((ratio - prop.ratio) / norm)
    prop.scale = min(scale, sqrt(n))
    return prop
end

"""
    Proposals.RStagger(;ratio=0.5, walks=25, scale=1)

Propose a new live point by random staggering away from an existing live point.
This differs from the random walk proposal in that the step size here is exponentially adjusted
to reach a target acceptance rate _during_ each proposal, in addition to _between_
proposals. This follows the algorithm outlined in Skilling (2006).[^5]

[^5]: Skilling, 2006, Bayesian Anal. 1(4), ["Nested sampling for general Bayesian computation"](https://projecteuclid.org/journals/bayesian-analysis/volume-1/issue-4/Nested-sampling-for-general-Bayesian-computation/10.1214/06-BA127.full)

## Parameters
- `ratio` is the target acceptance ratio
- `walks` is the minimum number of steps to take
- `scale` is the proposal distribution scale, which will update _between_ proposals.
"""
@with_kw mutable struct RStagger <: AbstractProposal
    ratio = 0.5
    walks = 25
    scale = 1.0

    @assert 1 / walks ≤ ratio ≤ 1 "Target acceptance ratio must be between 1/`walks` and 1"
    @assert walks > 1 "Number of steps must be greater than 1"
    @assert scale ≥ 0 "Proposal scale must be non-negative"
end

function (prop::RStagger)(
    rng::AbstractRNG,
    point::AbstractVector,
    logl_star,
    bounds::AbstractBoundingSpace,
    model;
    kwargs...
)
    #setup
    n = length(point)
    scale_init = prop.scale
    accept = reject = fail = nfail = nc = ncall = 0
    stagger = 1
    local du, u_prop, logl_prop, u, v, logl

    while nc < prop.walks || iszero(accept)
        # get proposed point
        while true
            # check scale factor to avoid over-shrinking
            prop.scale < 1e-5 * scale_init && error("Random walk sampling appears to be stuck.")
            # transform to proposal distribution
            du = randoffset(rng, bounds)
            u_prop = @. point + prop.scale * stagger * du
            # inside unit-cube
            unitcheck(u_prop) && break

            fail += 1
            nfail += 1
            # check if stuck generating bad numbers
            if fail > 100 * prop.walks
                @warn "Random number generation appears extremely inefficient. Adjusting the scale-factor accordingly"
                fail = 0
                prop.scale *= exp(-1/n)
            end
        end
        # check proposed point
        v_prop, logl_prop = prior_transform_and_loglikelihood(model, u_prop)
        if logl_prop ≥ logl_star
            u = u_prop
            v = v_prop
            logl = logl_prop
            accept += 1
        else
            reject += 1
        end
        nc += 1
        ncall += 1

        # adjust _stagger_ to target an acceptance ratio of `prop.ratio`
        ratio = accept / (accept + reject)
        if ratio > prop.ratio
            stagger *= exp(1 / accept)
        elseif ratio < prop.ratio
            stagger /= exp(1 / reject)
        end

        # check if stuck generating bad points
        if nc > 50 * prop.walks
            @warn "Random walk proposals appear to be extremely inefficient. Adjusting the scale-factor accordingly"
            prop.scale *= exp(-1 / n)
            nc = accept = reject = 0
        end
    end

    # update proposal scale using acceptance ratio
    update_scale!(prop, accept, reject, n)

    return u, v, logl, ncall
end

"""
    Proposals.Slice(;slices=5, scale=1)

Propose a new live point by a series of random slices away from an existing live point.
This is a standard _Gibbs-like_ implementation where a single multivariate slice is a combination of `slices` univariate slices through each axis. This follows the algorithm outlined in Neal (2003).[^6]

[^6]: Neal, 2003, Ann. Statist. 31(3), ["Slice Sampling"](https://projecteuclid.org/journals/annals-of-statistics/volume-31/issue-3/Slice-sampling/10.1214/aos/1056562461.full)

## Parameters
- `slices` is the minimum number of slices
- `scale` is the proposal distribution scale, which will update _between_ proposals.
"""
@with_kw mutable struct Slice <: AbstractProposal
    slices = 5
    scale = 1.0

    @assert slices ≥ 1 "Number of slices must be greater than or equal to 1"
    @assert scale ≥ 0 "Proposal scale must be non-negative"
end

function (prop::Slice)(
    rng::AbstractRNG,
    point::AbstractVector,
    logl_star,
    bounds::AbstractBoundingSpace,
    model;
    kwargs...
)
    # setup
    n = length(point)
    nc = nexpand = ncontract = 0
    local u, v, logl

    # modifying axes and computing lengths
    axes = Bounds.axes(bounds)
    axes = prop.scale .* axes'
    # slice sampling loop
    for it in 1:prop.slices

        # shuffle axis update order
        idxs = shuffle!(rng, collect(Base.axes(axes, 1)))

        # slice sample along a random direction
        for idx in idxs

            # select axis
            axis = axes[idx, :]

            u, v, logl, nc, nexpand, ncontract = sample_slice(
                rng, axis, point, logl_star,
                model,
                nc, nexpand, ncontract
            )
        end # end of slice sample along a random direction
    end # end of slice sampling loop

    # update slice proposal scale based on the relative size of the slices compared to the initial guess
    prop.scale = prop.scale * nexpand / (2.0 * ncontract)

    return u, v, logl, nc
end   # end of function Slice

"""
    Proposals.RSlice(;slices=5, scale=1)

Propose a new live point by a series of random slices away from an existing live point. This is a standard _random_ implementation where each slice is along a random direction based on the provided axes. This more closely matches the PolyChord implementation outlined in Handley et al. (2015a,b).[^7][^8]

[^7]: Handley, et al., 2015a, MNRAS 450(1), ["polychord: nested sampling for cosmology"](https://academic.oup.com/mnrasl/article/450/1/L61/986122)
[^8]: Handley, et al., 2015b, MNRAS 453(4), ["polychord: next-generation nested sampling"](https://academic.oup.com/mnras/article/453/4/4384/2593718)

## Parameters
- `slices` is the minimum number of slices
- `scale` is the proposal distribution scale, which will update _between_ proposals.
"""
@with_kw mutable struct RSlice <: AbstractProposal
    slices = 5
    scale = 1.0

    @assert slices ≥ 1 "Number of slices must be greater than or equal to 1"
    @assert scale ≥ 0 "Proposal scale must be non-negative"
end

function (prop::RSlice)(
    rng::AbstractRNG,
    point::AbstractVector,
    logl_star,
    bounds::AbstractBoundingSpace,
    model;
    kwargs...
)
    # setup
    n = length(point)
    nc = nexpand = ncontract = 0
    local u, v, logl
    # random slice sampling loop
    for it in 1:prop.slices

        # propose a direction on the unit n-sphere
        drhat = randn(rng, n)
        drhat /= norm(drhat)

        # transform and scale into parameter space
        axis = prop.scale .* (Bounds.axes(bounds) * drhat)
        u, v, logl, nc, nexpand, ncontract = sample_slice(
            rng, axis, point, logl_star,
            model,
            nc, nexpand, ncontract
        )
    end # end of random slice sampling loop

    # update random slice proposal scale based on the relative size of the slices compared to the initial guess
    prop.scale = prop.scale * nexpand / (2.0 * ncontract)

    return u, v, logl, nc
end # end of function RSlice

# Method for slice sampling
function sample_slice(rng, axis, u, logl_star, model, nc, nexpand, ncontract)
    # define starting window
    r = rand(rng)  # initial scale/offset
    u_l = @. u - r * axis  # left bound
    if unitcheck(u_l)
        v_l, logl_l = prior_transform_and_loglikelihood(model, u_l)
    else
        logl_l = -Inf
    end
    nc += 1
    nexpand += 1

    u_r = u_l .+ axis # right bound
    if unitcheck(u_r)
        v_r, logl_r = prior_transform_and_loglikelihood(model, u_r)
    else
        logl_r = -Inf
    end
    nc += 1
    nexpand += 1

    # stepping out left and right bounds
    while logl_l ≥ logl_star
        u_l .-= axis
        if unitcheck(u_l)
            v_l, logl_l = prior_transform_and_loglikelihood(model, u_l)
        else
            logl_l = -Inf
        end
        nc += 1
        nexpand += 1
    end

    while logl_r ≥ logl_star
        u_r .+= axis
        if unitcheck(u_r)
            v_r, logl_r = prior_transform_and_loglikelihood(model, u_r)
        else
            logl_r = -Inf
        end
        nc += 1
        nexpand += 1
    end

    # sample within limits. If the sample is not valid, shrink the limits until the `logl_star` bound is hit
    window_init = norm(u_r - u_l)  # initial window size
    while true
        # define slice and window
        u_hat = u_r - u_l
        window = norm(u_hat)

        # check if the slice has shrunk to be ridiculously small
        window < 1e-5 * window_init && error("Slice sampling appears to be stuck.")

        # propose a new position
        r = rand(rng)
        u_prop = @. u_l + r * u_hat
        if unitcheck(u_prop)
            v_prop, logl_prop = prior_transform_and_loglikelihood(model, u_prop)
        else
            logl_prop = -Inf
        end
        nc += 1
        ncontract += 1

        # if success, then move to the new position
        if logl_prop ≥ logl_star
            return u_prop, v_prop, logl_prop, nc, nexpand, ncontract
        # if fail, then check if the new point is to the left/right of the original point along the proposal axis and update the bounds accordingly
        else
            s = dot(u_prop - u, u_hat) # check sign (+/-)
            if s < 0   # left
                u_l = u_prop
            elseif s > 0  # right
                u_r = u_prop
            else # if `s = 0` something has gone wrong
                error("Slice sampler has failed to find a valid point.")
            end
        end
    end # end of sample within limits while
end

end # module Proposals