######################################################################
# Application Interface
# This is one of many possible ways that devs can use to integrate
# with the Pia framework.
# -----
# Licensed under Apache License 2.0
module Applications
using ExtraFun: Ident
using ..Pia
using ..Packets
import Base.Threads: @spawn

export Application
struct Application <: AbstractApplication
    appname::String
    version::VersionNumber
    protocols::ProtocolStack
end

export appname, appversion
appname(app::AbstractApplication) = app.appname
appversion(app::AbstractApplication) = app.version


export application
function application(name::AbstractString, version::VersionNumber;
        transport::TransportProtocol = PiaLink(),
        cryptography::Optional{CryptographyProtocol} = nothing,
        discovery::DiscoveryProtocol = PiaPortal(32),
        data::Optional{DataProtocol} = nothing,
        return_task::Bool = false,
    )
    app = Application(name, version, ProtocolStack(transport, cryptography, discovery, data))
    for protocol ∈ protocolstack(app)
        Pia.init(protocol, app)
        Pia.restore(protocol)
    end
    
    task = @spawn Pia.loop(app)
    if return_task
        return app, task
    else
        return app
    end
end
function application(cb, name::AbstractString, version::VersionNumber; kwargs...)
    app, task = application(name, version; kwargs..., return_task=true)
    try
        wait(@async(cb(app)))
        wait(task)
    finally
        for protocol ∈ protocolstack(app)
            Pia.store(protocol)
        end
        close(app)
    end
end


# ===== process_packet =====
# This special method leverages meta programming to achieve dynamic dispatch partially on value rather than type.
# Identified packets are handled. The default fallback forwards packets to waiting futures only if previously registered.
# This solution is the backbone of the secure multiplexed connection.

process_packet(ident::Ident, app::AbstractApplication, ch::PeerChannel, bytes) = process_packet(ident, app, ch.transport, ch.endpoint, bytes)
process_packet(ident::Ident, app::AbstractApplication, tp::TransportProtocol, phys, bytes) = throw(MethodError(process_packet, (ident, app, tp, phys, bytes)))

# --- Handshake Start ---
process_packet(::Ident{Packets.HandshakeStart}, app::AbstractApplication, ch::PeerChannel, bytes) = process_packet(Ident{Packets.HandshakeStart}(), app, ch.transport, ch.endpoint, bytes)

# --- Ping ---
function process_packet(::Ident{Packets.Ping}, ::AbstractApplication, ch::PeerChannel, _)
    if haskey(peers(ch.transport), ch.endpoint)
        @sync begin
            @async write(ch, packetbytes(Packets.Pong))
            @async refreshpeer(ch.transport, ch.endpoint)
        end
    end
end

# --- Goodbye ---
process_packet(::Ident{Packets.Goodbye}, app::AbstractApplication, ch::PeerChannel, bytes) = process_packet(Ident{Packets.Goodbye}(), app, ch.transport, ch.endpoint, bytes)


function Base.close(app::AbstractApplication)
    close(protocolstack(app).transport)
end

end # module Applications
