#include "crNet.h"

#ifdef __EMSCRIPTEN__   // native builds compile this to an empty TU; the backend lives in crNet_native.cpp

#include <emscripten/fetch.h>
#include <emscripten/websocket.h>

#include "crStruct.h"

struct crNetBackend;

struct HttpRequest
{
    int32_t             id       = crNet::INVALID_ID;
    crNetBackend*       be       = nullptr;
    emscripten_fetch_t* fetch    = nullptr;
    void*               bodyCopy = nullptr;
};

enum class EWsState : uint8_t
{
    CONNECTING = 0,
    OPEN,
    CLOSING,
};

struct WsSocket
{
    int32_t                id              = crNet::INVALID_ID;
    crNetBackend*          be              = nullptr;
    EMSCRIPTEN_WEBSOCKET_T handle          = 0;
    EWsState               state           = EWsState::CONNECTING;
    bool                   dead            = false;   // onclose fired — reaped in Update (avoids delete-in-callback reentrancy)
    uint64_t               connectDeadline = 0;
};

struct crNetBackend
{
    crNet* net = nullptr;

    crArray<HttpRequest*> requests{ 8 };
    crArray<WsSocket*>    sockets{ 2 };

    static void OnFetchDone( emscripten_fetch_t* fetch )
    {
        HttpRequest* req = static_cast<HttpRequest*>( fetch->userData );

        if( ( fetch->status >= 200 ) && ( fetch->status < 400 ) )
        {
            const void*   data = ( fetch->data != nullptr ) ? fetch->data : "";
            const int32_t size = ( fetch->data != nullptr ) ? static_cast<int32_t>( fetch->numBytes ) : 0;
            req->be->net->PushEvent( req->id, ENetEventType::HTTP_RESULT, fetch->status, data, size );
        }
        else if( fetch->status != 0 )   // 4xx/5xx — server responded; keep the body if emscripten populated it
        {
            const void*   data = ( fetch->data != nullptr ) ? fetch->data : "";
            const int32_t size = ( fetch->data != nullptr ) ? static_cast<int32_t>( fetch->numBytes ) : 0;
            req->be->net->PushEvent( req->id, ENetEventType::HTTP_FAILED, fetch->status, data, size );
        }
        else   // transport failure — no HTTP response reached
        {
            const char* err = ( fetch->statusText[ 0 ] != 0 ) ? fetch->statusText : "fetch failed";
            req->be->net->PushEvent( req->id, ENetEventType::HTTP_FAILED, 0, err, static_cast<int32_t>( SDL_strlen( err ) ) );
        }

        req->be->RemoveRequest( req );
        emscripten_fetch_close( fetch );
    }

    static bool OnWsOpen( int, const EmscriptenWebSocketOpenEvent*, void* user )
    {
        WsSocket* s = static_cast<WsSocket*>( user );
        s->state = EWsState::OPEN;
        s->be->net->PushEvent( s->id, ENetEventType::WS_OPEN, 0, nullptr, 0 );
        return true;
    }

    static bool OnWsMessage( int, const EmscriptenWebSocketMessageEvent* e, void* user )
    {
        WsSocket*           s    = static_cast<WsSocket*>( user );
        const ENetEventType type = e->isText ? ENetEventType::WS_TEXT : ENetEventType::WS_BINARY;
        s->be->net->PushEvent( s->id, type, 0, e->data, static_cast<int32_t>( e->numBytes ) );
        return true;
    }

    static bool OnWsClose( int, const EmscriptenWebSocketCloseEvent* e, void* user )
    {
        WsSocket* s = static_cast<WsSocket*>( user );
        s->be->net->PushEvent( s->id, ENetEventType::WS_CLOSED, e->code, nullptr, 0 );
        s->dead = true;
        return true;
    }

    int32_t StartRequest( crNet* owner, const char* url, bool isPost, const char* contentType,
                          const void* body, int32_t bodySize, const char* const* headers, int32_t headerCount )
    {
        HttpRequest* req = new HttpRequest();
        req->id = ++owner->_idCounter;
        req->be = this;

        emscripten_fetch_attr_t attr;
        emscripten_fetch_attr_init( &attr );
        SDL_strlcpy( attr.requestMethod, isPost ? "POST" : "GET", sizeof( attr.requestMethod ) );
        attr.attributes  = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
        attr.timeoutMSecs = crNet::HTTP_TOTAL_TIMEOUT_MS;   // browser owns connect phase; only the total is ours to set
        attr.onsuccess   = OnFetchDone;
        attr.onerror     = OnFetchDone;
        attr.userData    = req;

        // headers copied into scratch so each "Name: value" can be split in place; array + scratch freed after the (synchronous) emscripten_fetch read
        const int32_t pairs   = headerCount + ( ( contentType != nullptr ) ? 1 : 0 );
        const char**  harr    = nullptr;
        char*         scratch = nullptr;
        if( pairs > 0 )
        {
            int32_t total = 0;
            for( int32_t i = 0; i < headerCount; ++i )
                total += ( static_cast<int32_t>( SDL_strlen( headers[ i ] ) ) + 1 );

            harr    = static_cast<const char**>( SDL_malloc( static_cast<size_t>( pairs * 2 + 1 ) * sizeof( char* ) ) );
            scratch = static_cast<char*>( SDL_malloc( static_cast<size_t>( ( total > 0 ) ? total : 1 ) ) );

            int32_t off = 0;
            int32_t j   = 0;
            for( int32_t i = 0; i < headerCount; ++i )
            {
                char*         dst = scratch + off;
                const int32_t len = static_cast<int32_t>( SDL_strlen( headers[ i ] ) );
                SDL_memcpy( dst, headers[ i ], static_cast<size_t>( len + 1 ) );
                off += ( len + 1 );

                char* colon = SDL_strchr( dst, ':' );
                if( colon == nullptr )
                    continue;   // malformed header — skip
                *colon = 0;
                char* val = colon + 1;
                while( *val == ' ' )
                    ++val;
                harr[ j++ ] = dst;
                harr[ j++ ] = val;
            }
            if( contentType != nullptr )
            {
                harr[ j++ ] = "Content-Type";
                harr[ j++ ] = contentType;
            }
            harr[ j ] = nullptr;
            attr.requestHeaders = harr;
        }

        if( isPost && ( bodySize > 0 ) )
        {
            req->bodyCopy = SDL_malloc( static_cast<size_t>( bodySize ) );
            SDL_memcpy( req->bodyCopy, body, static_cast<size_t>( bodySize ) );
            attr.requestData     = static_cast<const char*>( req->bodyCopy );
            attr.requestDataSize = static_cast<size_t>( bodySize );
        }

        req->fetch = emscripten_fetch( &attr, url );
        SDL_free( harr );
        SDL_free( scratch );

        requests.Add( req );
        return req->id;
    }

    int32_t StartWs( crNet* owner, const char* url )
    {
        EmscriptenWebSocketCreateAttributes attr;
        emscripten_websocket_init_create_attributes( &attr );
        attr.url               = url;
        attr.createOnMainThread = true;

        const EMSCRIPTEN_WEBSOCKET_T handle = emscripten_websocket_new( &attr );
        if( handle <= 0 )
        {
            const int32_t id  = ++owner->_idCounter;
            const char*   err = "websocket unsupported";
            net->PushEvent( id, ENetEventType::WS_CLOSED, 0, err, static_cast<int32_t>( SDL_strlen( err ) ) );
            return id;
        }

        WsSocket* s = new WsSocket();
        s->id              = ++owner->_idCounter;
        s->be              = this;
        s->handle          = handle;
        s->connectDeadline = SDL_GetTicks() + crNet::WS_CONNECT_TIMEOUT_MS;
        sockets.Add( s );

        emscripten_websocket_set_onopen_callback( handle, s, OnWsOpen );
        emscripten_websocket_set_onmessage_callback( handle, s, OnWsMessage );
        emscripten_websocket_set_onclose_callback( handle, s, OnWsClose );
        return s->id;
    }

    WsSocket* SocketById( int32_t id )
    {
        for( int32_t i = 0; i < sockets.Size(); ++i )
        {
            if( sockets.At( i )->id == id )
                return sockets.At( i );
        }
        return nullptr;
    }

    bool Send( int32_t id, const void* data, int32_t size, bool binary )
    {
        WsSocket* s = SocketById( id );
        if( ( s == nullptr ) || ( s->state != EWsState::OPEN ) )
            return false;

        if( binary )
            emscripten_websocket_send_binary( s->handle, const_cast<void*>( data ), static_cast<uint32_t>( size ) );
        else
            emscripten_websocket_send_utf8_text( s->handle, static_cast<const char*>( data ) );   // WsSendText guarantees NUL-terminated
        return true;
    }

    void RemoveRequest( HttpRequest* req )
    {
        for( int32_t i = 0; i < requests.Size(); ++i )
        {
            if( requests.At( i ) == req )
            {
                requests.At( i ) = requests.At( requests.Size() - 1 );
                requests.Pop();
                break;
            }
        }
        SDL_free( req->bodyCopy );
        delete req;
    }
};

bool crNet::Init()
{
    crNetBackend* be = new crNetBackend();
    be->net  = this;
    _backend = be;

    SDL_LogTrace( CR_LOG_CATEGORY_NET, "crNet: initted( emscripten fetch/websocket )" );
    return true;
}

void crNet::Cleanup()
{
    if( _backend == nullptr )
        return;
    crNetBackend* be = _backend;

    for( int32_t i = 0; i < be->requests.Size(); ++i )
    {
        HttpRequest* req = be->requests.At( i );
        emscripten_fetch_close( req->fetch );
        SDL_free( req->bodyCopy );
        delete req;
    }
    be->requests.Clear();

    for( int32_t i = 0; i < be->sockets.Size(); ++i )
    {
        WsSocket* s = be->sockets.At( i );
        emscripten_websocket_delete( s->handle );
        delete s;
    }
    be->sockets.Clear();

    delete be;
    _backend = nullptr;

    ClearAllEvents();
}

void crNet::Update()
{
    if( _backend == nullptr )
        return;
    crNetBackend* be = _backend;

    // fetch + ws are browser-driven (callbacks fire between frames); Update only reaps dead sockets and enforces the connect deadline
    const uint64_t now = SDL_GetTicks();
    for( int32_t i = 0; i < be->sockets.Size(); )
    {
        WsSocket* s = be->sockets.At( i );
        if( s->dead )
        {
            emscripten_websocket_delete( s->handle );
            be->sockets.At( i ) = be->sockets.At( be->sockets.Size() - 1 );
            be->sockets.Pop();
            delete s;
            continue;
        }
        if( ( s->state == EWsState::CONNECTING ) && ( now >= s->connectDeadline ) )
        {
            s->state = EWsState::CLOSING;
            emscripten_websocket_close( s->handle, 1000, "timeout" );   // onclose follows → WS_CLOSED → dead
        }
        ++i;
    }
}

int32_t crNet::Get( const char* url, const char* const* headers, int32_t headerCount )
{
    if( _backend == nullptr )
        return INVALID_ID;
    return _backend->StartRequest( this, url, false, nullptr, nullptr, 0, headers, headerCount );
}

int32_t crNet::Post( const char* url, const char* contentType, const void* body, int32_t bodySize,
                     const char* const* headers, int32_t headerCount )
{
    if( _backend == nullptr )
        return INVALID_ID;
    return _backend->StartRequest( this, url, true, contentType, body, bodySize, headers, headerCount );
}

int32_t crNet::WsConnect( const char* url )
{
    if( _backend == nullptr )
        return INVALID_ID;
    return _backend->StartWs( this, url );
}

bool crNet::WsSendText( int32_t id, const char* text )
{
    if( _backend == nullptr )
        return false;
    return _backend->Send( id, text, static_cast<int32_t>( SDL_strlen( text ) ), false );
}

bool crNet::WsSendBinary( int32_t id, const void* data, int32_t size )
{
    if( _backend == nullptr )
        return false;
    return _backend->Send( id, data, size, true );
}

void crNet::WsClose( int32_t id )
{
    if( _backend == nullptr )
        return;

    WsSocket* s = _backend->SocketById( id );
    if( ( s == nullptr ) || ( s->state == EWsState::CLOSING ) )
        return;

    s->state = EWsState::CLOSING;
    emscripten_websocket_close( s->handle, 1000, "" );   // onclose → WS_CLOSED → reaped in Update
}

#endif   // __EMSCRIPTEN__
