#
# ---------------------------------------------
# Bitonic v0.5 functionality
#

function partner(node, step)
    partner = node + ((((node+step) % 2) == 0) ? 1 : -1)
end

function active(id, p)
    ret =  (id >= 0) && (id < p)
end

function exchange(localid,  remoteid)
    if verbose
        println("Exchange local data from $localid with partner $remoteid")
    end
    nothing # We have all data here ;)
end

function minmax(data, localid, remoteid, keepsmall)
    # Keep min-max on local data
    temp = copy(data[localid+1, :])
    if (keepsmall)
        view(data, localid+1, :)  .= min.(temp, data[remoteid+1, :])
        view(data, remoteid+1, :) .= max.(temp, data[remoteid+1, :])
    else
        view(data, localid+1, :)  .= max.(temp, data[remoteid+1, :])
        view(data, remoteid+1, :) .= min.(temp, data[remoteid+1, :])
    end
end

"""
    distbubletonic!(p, data)

distributed bitonic v0.5 sort using a "bubble sort"-like functionality to propagate large and small
items between nodes.

p:    The number of processes
data: (p, N/p) array
"""
function distbubletonic!(p, data) 
  
    pid = 0:p-1
    ascending = mod.(pid,2) .== 0
    if verbose
        println("ascending: $ascending")
    end
    # local full sort here (run all MPI nodes)
    for i in 1:p
        sort!(view(data, i, :), rev = !ascending[i])
    end
    for step in 0:p-1
        direction = [true for x = 1:p]
        partnerid = partner.(pid, step)
        activeids = active.(partnerid, p)
        keepsmall = pid .< partnerid
        if verbose
            println("step: $step | active ids: $activeids | partner: $partnerid | keepsmall: $keepsmall")
        end
        # exchange with partner and keep small or large (run all MPI nodes)
        for i in 0:p-1
            l_idx = i+1
            r_idx = partnerid[i+1]+1
            if activeids[l_idx] && i < partnerid[l_idx]
                exchange(i, partnerid[l_idx])
                minmax(data, i, partnerid[l_idx], keepsmall[l_idx])
                sort!(view(data, l_idx, :), rev = !ascending[l_idx])    # elbow sort here
                sort!(view(data, r_idx, :), rev = !ascending[r_idx])    # elbow sort here
            end
        end
    end
    # [optional] reverse the odd positions (run all MPI nodes)
    for i in 1:p
        if !ascending[i]
            sort!(view(data, i, :))
        end
    end
    nothing
end



#
# Homework setup
# ---------------------------------------------
#
p::Int8 = 3  # The order of number of "processors"
q::Int8 = 8  # The data size order (power of 2) of each "processor"
verbose = false;


# Run Script
# ---------------------------------------------
P::Int = 2^p
Q::Int = 2^q
N::Int = 2^(q+p)

println("Distributed Bubbletonic (v0.5) test")
println("p: $p -> Number of processors: $P")
println("q: $q -> Data length for each node: $Q, Total: $(P*Q)")

println("Create an $P x $Q array")
Data = rand(Int8, P, Q)

println("Sort array with $P (MPI) nodes")
@time distbubletonic!(P, Data)

# Test
if issorted(vec(permutedims(Data)))
    println("Test: Passed")
else
    println("Test: Failed")
end
