# OpenSoundControl.jl
# Copyright (c) 2019, Mark McCurry, All rights reserved.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.

module OpenSoundControl

using Printf
import Base: show, getindex
export OscMsg, path

macro incfp(x) quote begin
            local gensym_ = $(esc(x))
            $(esc(x)) = $(esc(x))+1
            gensym_
end end end

struct OscMsg
    data::Array{UInt8}
end

path(msg::OscMsg) = stringify(msg.data)

function stringify(data::Array{UInt8})
    zeroInd = findall(data.== 0)
    if length(zeroInd) == 0
        return string(map(Char, data)...)
    elseif zeroInd[1] == 0
        return nothing
    else
        return string(map(Char, data[1:zeroInd[1]-1])...)
    end
end

function names(msg::OscMsg) #::String
    pos = 1
    while(msg.data[pos += 1] != 0) end      #skip pattern
    while(msg.data[pos += 1] == 0) end      #skip null
    return stringify(msg.data[pos+1:end])   #skip comma
end

strip_args(args::AbstractString) = replace(replace(args,"]"=>""),"["=>"")

function narguments(msg::OscMsg)
    length(strip_args(names(msg)))
end

has_reserved(typeChar::Char) = typeChar in "isbfhtdSrmc"
nreserved(args::String) = sum(map(has_reserved, collect(args)))

function argType(msg::OscMsg, nargument::Int) #::Char
    @assert(nargument > 0 && nargument <= narguments(msg))
    return strip_args(names(msg))[nargument]
end

align(pos) = pos+(4-(pos-1)%4)
function arg_off(msg::OscMsg, idx::Int) #::Int
    if(!has_reserved(argType(msg,idx)))
        return 0
    end

    # Iterate to the right position
    args::String = names(msg)
    argc::Int = 1
    pos::Int  = 1

    # Get past the Argument String
    while(msg.data[pos] != UInt8(',')) pos += 1 end
    while(msg.data[pos] != 0)   pos += 1 end

    # Alignment
    pos = align(pos)

    # ignore any leading '[' or ']'
    while(args[argc] in "[]") argc += 1 end

    while(idx != 1)
        bundle_length::UInt32 = 0
        arg = args[argc]
        argc += 1
        if arg in "htd"
            pos +=8
        elseif(arg in "mrfci")
            pos += 4
        elseif(arg in "Ss")
            while(msg.data[pos += 1] != 0) end
            pos = align(pos)
        elseif arg == 'b'
            bundle_length |= (msg.data[@incfp(pos)] << 24)
            bundle_length |= (msg.data[@incfp(pos)] << 16)
            bundle_length |= (msg.data[@incfp(pos)] << 8)
            bundle_length |= (msg.data[@incfp(pos)])
            bundle_length += 4-bundle_length%4
            pos += bundle_length
        elseif(arg in "[]") # completely ignore array chars
            idx += 1
        else # TFI
        end
        idx -= 1
    end
    pos
end

# Calculate the size of the message without writing to a buffer
function vsosc_null(address::String,
                    arguments::String,
                    args...)
    pos::Int  = length(address) + 1
    pos       = align(pos)
    pos      += 1+length(arguments)
    pos       = align(pos)

    arg_pos = 1

    # Take care of varargs
    for arg in arguments
        if arg in "htd"
            arg_pos += 1
            pos     += 8
        elseif arg in "mrcfi"
            arg_pos += 1
            pos     += 4
        elseif arg in "sS"
            s::String = args[@incfp(arg_pos)]
            pos += length(s)
            pos =  align(pos)
        elseif arg in "b"
            i::Int32 = sizeof(args[@incfp(arg_pos)])
            pos += 4 + i
            pos  = align(pos)
        end # other args classes are ignored
    end

    return pos - 1
end

function rtosc_amessage(buffer::Array{UInt8},
                        len::Int,
                        address::String,
                        arguments::String,
                        args...)
    total_len::Int = vsosc_null(address, arguments, args...)

    for i=1:total_len
        buffer[i] = 0
    end

    # Abort if the message cannot fit
    if total_len > len
        return 0
    end

    pos::Int = 1
    #Address
    for C in address
        buffer[@incfp(pos)] = C
    end
    pos = align(pos)

    #Arguments
    buffer[@incfp(pos)] = UInt8(',')
    for A in arguments
        buffer[@incfp(pos)] = A
    end
    pos = align(pos)

    arg_pos::Int = 1
    for arg in arguments
        @assert(UInt32(arg) != 0)
        if arg in "htd"
            d::UInt64 = reinterpret(UInt64, args[@incfp(arg_pos)])
            buffer[@incfp(pos)] = ((d>>56) & 0xff)
            buffer[@incfp(pos)] = ((d>>48) & 0xff)
            buffer[@incfp(pos)] = ((d>>40) & 0xff)
            buffer[@incfp(pos)] = ((d>>32) & 0xff)
            buffer[@incfp(pos)] = ((d>>24) & 0xff)
            buffer[@incfp(pos)] = ((d>>16) & 0xff)
            buffer[@incfp(pos)] = ((d>>8) & 0xff)
            buffer[@incfp(pos)] = (d & 0xff)
        elseif arg in "rfci"
            i::Int32 = reinterpret(Int32, args[@incfp(arg_pos)])
            buffer[@incfp(pos)] = ((i>>24) & 0xff)
            buffer[@incfp(pos)] = ((i>>16) & 0xff)
            buffer[@incfp(pos)] = ((i>>8) & 0xff)
            buffer[@incfp(pos)] = (i & 0xff)
        elseif arg in "m"
            m = args[@incfp(arg_pos)]
            buffer[@incfp(pos)] = m[1]
            buffer[@incfp(pos)] = m[2]
            buffer[@incfp(pos)] = m[3]
            buffer[@incfp(pos)] = m[4]
        elseif arg in "Ss"
            s = args[@incfp(arg_pos)]
            for C in s
                buffer[@incfp(pos)] = C
            end
            pos = align(pos)
        elseif arg == 'b'
            b = args[@incfp(arg_pos)]
            i = sizeof(b)
            buffer[@incfp(pos)] = ((i>>24) & 0xff)
            buffer[@incfp(pos)] = ((i>>16) & 0xff)
            buffer[@incfp(pos)] = ((i>>8) & 0xff)
            buffer[@incfp(pos)] = (i & 0xff)
            for U in b
                buffer[@incfp(pos)] = UInt8(U)
            end
            pos = align(pos)
        end
    end

    return pos - 1
end

OscMsg(address, arguments, args...) = message(address, arguments, args...)

function message(address::String,
                 arguments::String,
                 args...)
    len::Int = vsosc_null(address, arguments, args...)
    data::Vector{UInt8} = Array{UInt8}(undef, len)
    rtosc_amessage(data,len,address,arguments,args...)
    return OscMsg(data)
end

function rtosc_argument(msg::OscMsg, idx::Int)
    typeChar::Char = argType(msg, idx)
    # trivial case
    if(!has_reserved(typeChar))
        if typeChar == 'T'
            return true
        elseif typeChar == 'F'
            return false
        end
    else
        arg_pos::Int = arg_off(msg, idx)

        if typeChar in "htd"
            t::UInt64 = 0
            t |= (UInt64(msg.data[@incfp(arg_pos)]) << 56)
            t |= (UInt64(msg.data[@incfp(arg_pos)]) << 48)
            t |= (UInt64(msg.data[@incfp(arg_pos)]) << 40)
            t |= (UInt64(msg.data[@incfp(arg_pos)]) << 32)
            t |= (UInt64(msg.data[@incfp(arg_pos)]) << 24)
            t |= (UInt64(msg.data[@incfp(arg_pos)]) << 16)
            t |= (UInt64(msg.data[@incfp(arg_pos)]) << 8)
            t |= (UInt64(msg.data[@incfp(arg_pos)]))
            if typeChar == 'h'
                return reinterpret(Int64, t)
            elseif typeChar == 'd'
                return reinterpret(Float64, t)
            else
                return t
            end
        elseif typeChar in "f"
            return read(IOBuffer(msg.data[arg_pos.+(3:-1:0)]), Float32)
        elseif typeChar in "rci"
            i::UInt32 = 0
            i |= (UInt32(msg.data[@incfp(arg_pos)]) << 24)
            i |= (UInt32(msg.data[@incfp(arg_pos)]) << 16)
            i |= (UInt32(msg.data[@incfp(arg_pos)]) << 8)
            i |= (UInt32(msg.data[@incfp(arg_pos)]))
            if typeChar == 'r'
                return UInt32(i)
            elseif typeChar == 'c'
                return Char(i/(2^24))
            else
                return reinterpret(Int32, i)
            end
        elseif typeChar in "m"
            m = Array{UInt8}(undef, 4)
            m[1] = msg.data[@incfp(arg_pos)]
            m[2] = msg.data[@incfp(arg_pos)]
            m[3] = msg.data[@incfp(arg_pos)]
            m[4] = msg.data[@incfp(arg_pos)]
            return m
        elseif typeChar in "b"
            len::Int32 = 0
            len |= (msg.data[@incfp(arg_pos)] << 24)
            len |= (msg.data[@incfp(arg_pos)] << 16)
            len |= (msg.data[@incfp(arg_pos)] << 8)
            len |= (msg.data[@incfp(arg_pos)])
            return msg.data[arg_pos.+(0:len-1)]
        elseif typeChar in "Ss"
            return stringify(msg.data[arg_pos:end])
        end
    end

    return nothing
end

getindex(msg::OscMsg, idx::Int) = rtosc_argument(msg, idx)

function show(io::IO, msg::OscMsg)
    println(io, "OSC Message to ", stringify(msg.data))
    println(io, "    Arguments:")
    for i=1:narguments(msg)
        showField(io, msg,i)
    end
end

function showField(io::IO, msg::OscMsg, arg_id)
    map = Any['i' Int32;
           'f' Float32;
           's' String;
           'b' :Blob;
           'h' Int32;
           't' UInt64;
           'd' Float64;
           'S' Symbol;
           'c' Char;
           'r' :RBG;
           'm' :Midi;
           'T' true;
           'F' false;
           'I' Inf;
           'N' Nothing]
    dict = Dict{Char, Any}(zip(Vector{Char}(map[:,1][:]),map[:,2][:]))
    typeChar::Char = argType(msg, arg_id)
    value = msg[arg_id]
    if typeof(value) <: Array
        value = value'
    end
    if(value == nothing)
        value = "nothing"
    end
    @printf(io, "    #%2d %c:", arg_id, typeChar)
    println(dict[typeChar]," - ", value)
end


end
