#include "crUserData.h"

#include <yyjson.h>

#include "crApp.h"

#ifdef __EMSCRIPTEN__
#include <emscripten.h>

// IMPL NOTE( 2026.8.11 ): SDL_EMSCRIPTEN_PERSISTENT_PATH looks like it would replace this block, but it is a build-time
//                         SDL option rather than a runtime hint — adopting it means rebuilding SDL, and autoPersist writes
//                         some frames later, which is weaker than the explicit flush below
EM_JS( void, InitIDBFS, (), {
    try { FS.mkdir( '/userdata' ); } catch( e ) {}
    FS.mount( IDBFS, {}, '/userdata' );
    Module.__crUserDataReady = 0;
    FS.syncfs( true, function( err ) { Module.__crUserDataReady = 1; } );
} );

EM_JS( void, FlushBegin, (), {
    Module.__crFlushDone = 0;
    FS.syncfs( false, function( err ) { Module.__crFlushDone = 1; } );
} );

static int IsIDBReady()
{
    return EM_ASM_INT( { return Module.__crUserDataReady | 0; } );
}

static int IsFlushDone()
{
    return EM_ASM_INT( { return Module.__crFlushDone | 0; } );
}
#endif

struct crJsonSdlAllocator
{
    // route yyjson through SDL allocators — one allocator family everywhere (blob.Free()/SDL_free stays valid)
    static void* JsonMalloc( void* ctx, size_t size )
    {
        ( void )ctx;
        return SDL_malloc( size );
    }
    static void* JsonRealloc( void* ctx, void* ptr, size_t oldSize, size_t size )
    {
        ( void )ctx;
        ( void )oldSize;
        return SDL_realloc( ptr, size );
    }
    static void JsonFree( void* ctx, void* ptr )
    {
        ( void )ctx;
        SDL_free( ptr );
    }
};
static const yyjson_alc YYJSON_SDL_ALC = { crJsonSdlAllocator::JsonMalloc, crJsonSdlAllocator::JsonRealloc, crJsonSdlAllocator::JsonFree, nullptr };

bool crUserData_UserSettings::FromBlob( crUserDataBlob blob )
{
    SDL_free( myString );
    myString = nullptr;

    if( blob.data == nullptr )
    {
        return false;
    }

    yyjson_doc* doc = yyjson_read_opts( ( char* )blob.data, blob.size, 0, &YYJSON_SDL_ALC, nullptr );
    if( doc == nullptr )
    {
        blob.Free();
        return false;
    }
    else
    {
        yyjson_val* root = yyjson_doc_get_root( doc );

        myInt   = yyjson_get_sint( yyjson_obj_get( root, "myInt" ) );
        myFloat = ( float )yyjson_get_real( yyjson_obj_get( root, "myFloat" ) );

        const char* str = yyjson_get_str( yyjson_obj_get( root, "myString" ) );
        if( str != nullptr )
            myString = SDL_strdup( str );

        yyjson_doc_free( doc );

        blob.Free();
        return true;
    }
}
crUserDataBlob crUserData_UserSettings::ToBlob() const
{
    yyjson_mut_doc* doc  = yyjson_mut_doc_new( &YYJSON_SDL_ALC );
    yyjson_mut_val* root = yyjson_mut_obj( doc );
    yyjson_mut_doc_set_root( doc, root );

    const char* str = "";
    if( myString != nullptr )
        str = myString;

    yyjson_mut_obj_add_int ( doc, root, "myInt", myInt );
    yyjson_mut_obj_add_real( doc, root, "myFloat", myFloat );
    yyjson_mut_obj_add_str ( doc, root, "myString", str );

    size_t len  = 0;
    char*  json = yyjson_mut_write_opts( doc, YYJSON_WRITE_PRETTY, &YYJSON_SDL_ALC, &len, nullptr );
    yyjson_mut_doc_free( doc );

    crUserDataBlob rv;
    rv.size = len;
    rv.data = json;

    return rv;
}

void crUserData::Init( crApp* app )
{
    _app = app;

#ifdef __EMSCRIPTEN__
    InitIDBFS();
#endif
}
void crUserData::Cleanup()
{
    _loadedBlob.Free();
    _saveBlob.Free();

#ifdef __EMSCRIPTEN__
    FlushBegin();
#endif

    _app = nullptr;
}
void crUserData::UpdateEarly()
{
    SDL_assert( _pending == true );

    if( _op == EOp::LOAD )
    {
        _loadedBlob = LoadRaw( _filename );
        _success    = ( _loadedBlob.data != nullptr );

        FinishOp();
    }
    else if( _op == EOp::SAVE )
    {
        if( _started == false )
        {
            _success = SaveRaw( _filename, _saveBlob.data, _saveBlob.size );
            _started = true;

#ifdef __EMSCRIPTEN__
            if( _success )
            {
                FlushBegin();
                return;   // wait for flush completion in later Update()s
            }
#endif
            FinishOp();
        }
#ifdef __EMSCRIPTEN__
        else
        {
            if( IsFlushDone() != 0 )
                FinishOp();
        }
#endif
    }
}

bool crUserData::IsStorageReady()
{
#ifdef __EMSCRIPTEN__
    return IsIDBReady() != 0;
#else
    return true;
#endif
}

bool crUserData::RequestLoadUserSettings( crUserData_OnLoaded callbackOnLoaded, void* context )
{
    SDL_assert( _pending == false );

    if( _pending )
        return false;

    _loadedBlob.Free();

    _onLoaded        = callbackOnLoaded;
    _onSaved         = nullptr;
    _callbackContext = context;

    SDL_strlcpy( _filename, FILENAME_USERSETTINGS, sizeof( _filename ) );
    _op      = EOp::LOAD;
    _started = false;
    _success = false;

    _pending = true;
    _app->AppendState( crApp::EAppStateFlags::FILE_IO_PENDING );

    return true;
}
bool crUserData::RequestSaveUserSettings( crUserData_OnSaved callbackOnSaved, crUserData_UserSettings setting, void* context )
{
    SDL_assert( _pending == false );

    if( _pending )
        return false;

    // Serialize now, while the caller's setting.myString is still valid.
    _saveBlob = setting.ToBlob();
    if( _saveBlob.data == nullptr )
        return false;

    _onLoaded        = nullptr;
    _onSaved         = callbackOnSaved;
    _callbackContext = context;

    SDL_strlcpy( _filename, FILENAME_USERSETTINGS, sizeof( _filename ) );
    _op      = EOp::SAVE;
    _started = false;
    _success = false;

    _pending = true;
    _app->AppendState( crApp::EAppStateFlags::FILE_IO_PENDING );

    return true;
}

crUserDataBlob crUserData::LoadRaw( const char* filename )
{
    SDL_assert( IsStorageReady() );

    crUserDataBlob blob;

#ifdef __EMSCRIPTEN__
    char path[ 256 ];
    SDL_snprintf( path, sizeof( path ), "/userdata/%s", filename );
    blob.data = SDL_LoadFile( path, &blob.size );
#else
    SDL_Storage* storage = SDL_OpenUserStorage( "visai", "cromakga3d", 0 );
    if( ( storage == nullptr ) || ( SDL_StorageReady( storage ) == false ) )
    {
        if( storage != nullptr )
            SDL_CloseStorage( storage );
        return blob;
    }

    Uint64 length = 0;
    if( SDL_GetStorageFileSize( storage, filename, &length ) )
    {
        void* buffer = SDL_malloc( ( size_t )length );
        if( buffer != nullptr )
        {
            if( SDL_ReadStorageFile( storage, filename, buffer, length ) )
            {
                blob.data = buffer;
                blob.size = ( size_t )length;
            }
            else
            {
                SDL_free( buffer );
            }
        }
    }

    SDL_CloseStorage( storage );
#endif

    return blob;
}
bool crUserData::SaveRaw( const char* filename, const void* data, size_t size )
{
    SDL_assert( IsStorageReady() );

#ifdef __EMSCRIPTEN__
    char path[ 256 ];
    SDL_snprintf( path, sizeof( path ), "/userdata/%s", filename );
    return SDL_SaveFile( path, data, size );
#else
    SDL_Storage* storage = SDL_OpenUserStorage( "visai", "cromakga3d", 0 );
    if( ( storage == nullptr ) || ( SDL_StorageReady( storage ) == false ) )
    {
        if( storage != nullptr )
            SDL_CloseStorage( storage );
        return false;
    }

    bool ok = SDL_WriteStorageFile( storage, filename, data, ( Uint64 )size );

    SDL_CloseStorage( storage );
    return ok;
#endif
}
void crUserData::FinishOp()
{
    const EOp  op      = _op;
    const bool success = _success;

    crUserDataBlob loadedBlob = _loadedBlob;
    _loadedBlob = crUserDataBlob();   // ownership moves to the callback

    const crUserData_OnLoaded onLoaded = _onLoaded;
    const crUserData_OnSaved  onSaved  = _onSaved;
    void*                     context  = _callbackContext;

    _saveBlob.Free();

    _onLoaded        = nullptr;
    _onSaved         = nullptr;
    _callbackContext = nullptr;

    _op      = EOp::NONE;
    _started = false;

    _pending = false;
    _app->RemoveState( crApp::EAppStateFlags::FILE_IO_PENDING );

    // state fully cleared before invoking — a callback may immediately issue the next request
    if( ( op == EOp::LOAD ) && ( onLoaded != nullptr ) )
        onLoaded( success, loadedBlob, context );
    else if( ( op == EOp::SAVE ) && ( onSaved != nullptr ) )
        onSaved( success, context );
    else if( op == EOp::LOAD )
        loadedBlob.Free();   // no callback took ownership
}
