######################################################################
# Packet Construction & Destructuring methods
# -----
# Licensed under Apache License 2.0
module PacketFactories
import ExtraFun: Ident
using ..Pia
using ..Packets
import ..Packets: packetid, classid

# ===== Factory Methods =====
export packetbytes
"""`packetbytes(classid::PacketClass, packetid, args...; kwargs...)`
Construct packet of class `classid` and type `packetid`, passing individually required `args` and `kwargs` as necessary.
While this is a factory method, the underlying construction methods may require specific arguments."""
packetbytes(packetid::Enum, args...; kwargs...) = packetbytes(classid(packetid), packetid, args...; kwargs...)
function packetbytes(classid::PacketClass, packetid::Enum, args...; kwargs...)
    buffer = IOBuffer()
    write(buffer, UInt8(classid))
    write(buffer, UInt8(packetid))
    packetbytes(Ident{classid}(), Ident{packetid}(), buffer, args...; kwargs...)
    return take!(buffer)
end

export packetcontent
"""`packetcontent(bytes)`
Destructure packet contents from the supplied packet data. Throws respective errors if not possible."""
function packetcontent(bytes; without_id::Bool = false)
    ids, bytes = bytes[1:2], bytes[3:length(bytes)]
    
    cls = PacketClass(ids[1])
    pkt = packetid(Ident{cls}(), ids[2])
    ret = packetcontent(Ident{cls}(), Ident{pkt}(), bytes)
    if without_id
        return ret
    else
        return (cls, pkt), ret
    end
end

export peekpacket
"""`peekpacket(bytes)`
Peek the packet class & packet ID from the packet header."""
function peekpacket(bytes)
    cls = PacketClass(bytes[1])
    pkt = packetid(cls, bytes[2])
    return cls, pkt
end


# ===== Connectivity Class Packets =====
# --- Handshake Start ---
function packetbytes(::Ident{Packets.ConnectivityClass}, ::Ident{Packets.HandshakeStart}, buffer::IOBuffer, appname::AbstractString, version::VersionNumber, handshake_id::UInt8)
    if !isempty(version.prerelease) || !isempty(version.build)
        throw(ArgumentError("Version must only consist of major.minor.patch components"))
    end
    if length(appname) > typemax(UInt8)
        throw(ArgumentError("Application name too long ($(typemax(UInt8)) characters)"))
    end
    
    appname_str = string(appname)
    
    write(buffer, UInt8(length(appname_str)))
    write(buffer, appname_str)
    write(buffer, UInt8.([version.major, version.minor, version.patch]))
    write(buffer, handshake_id)
end

function packetcontent(::Ident{Packets.ConnectivityClass}, ::Ident{Packets.HandshakeStart}, bytes)
    buffer = IOBuffer(bytes)
    appname_len = read(buffer, UInt8)
    appname     = read(buffer, appname_len)
    major, minor, patch = read(buffer, 3)
    handshake_id = read(buffer, UInt8)
    @assert isempty(buffer) "unexpected excess packet data"
    return unsafe_string(pointer(appname)), VersionNumber(major, minor, patch), handshake_id
end

# --- Handshake Response ---
function packetbytes(::Ident{Packets.ConnectivityClass}, ::Ident{Packets.HandshakeResponse}, buffer::IOBuffer, handshake_id::UInt8)
    write(buffer, handshake_id)
end

function packetcontent(::Ident{Packets.ConnectivityClass}, ::Ident{Packets.HandshakeResponse}, bytes)
    buffer = IOBuffer(bytes)
    handshake_incr_id = read(buffer, UInt8)
    @assert isempty(buffer) "unexpected excess packet data"
    return handshake_incr_id
end

# --- Handshake Confirm ---
function packetbytes(::Ident{Packets.ConnectivityClass}, ::Ident{Packets.HandshakeConfirm}, buffer::IOBuffer)
    nothing
end

function packetcontent(::Ident{Packets.ConnectivityClass}, ::Ident{Packets.HandshakeConfirm}, bytes)
    nothing
end

# --- Goodbye ---
function packetbytes(::Ident{Packets.ConnectivityClass}, ::Ident{Packets.Goodbye}, buffer::IOBuffer)
    nothing
end

function packetcontent(::Ident{Packets.ConnectivityClass}, ::Ident{Packets.Goodbye}, bytes)
    nothing
end


# ===== Discovery Class Packets =====
# --- Query Peer ---
function packetbytes(::Ident{Packets.DiscoveryClass}, ::Ident{Packets.QueryPeer}, buffer::IOBuffer, address::Address{S}) where S
    @assert S isa Integer "invalid address length parameter"
    write(buffer, hton(UInt32(S)))
    write(buffer, address)
end

function packetcontent(::Ident{Packets.DiscoveryClass}, ::Ident{Packets.QueryPeer}, bytes)
    S = ntoh(read(buffer, UInt32))
    addr = read(buffer, Address{S})
    return addr
end


# ===== Utility Class Packets =====
# --- Wakeup Loop ---
function packetbytes(::Ident{Packets.UtilityClass}, ::Ident{Packets.Wakeup}, buffer::IOBuffer)
    nothing
end

function packetcontent(::Ident{Packets.UtilityClass}, ::Ident{Packets.Wakeup}, bytes)
    nothing
end


# ===== Data Class Packets =====
# --- AppData ---
function packetbytes(::Ident{Packets.DataClass}, ::Ident{Packets.AppData}, buffer::IOBuffer, data)
    write(buffer, data)
end

function packetcontent(::Ident{Packets.DataClass}, ::Ident{Packets.AppData}, bytes)
    return bytes
end

end # module PacketFactories
