/*==============================================================================
 크로스플랫폼 결정론 (lockstep / replay) — 빌드 체크리스트

 목표: WASM, Android, x64 desktop에서 비트 단위로 동일한 시뮬레이션.
 부동소수점 결정론은 깨지기 쉬우므로, 모든 타깃에서 아래를 일치시킬 것:

   - FMA contraction 금지:
       Clang / Emscripten / Android NDK  ->  -ffp-contract=off
       MSVC                              ->  /fp:precise (기본값), /fp:fast 절대 금지
   - fast-math (-ffast-math) 금지.
   - FTZ/DAZ (denormal 처리) 플랫폼 간 일관성 유지.

 Box3D는 prebuilt 라이브러리로 링크되므로, 앱 컴파일 플래그는 우리 소스 +
 Box3D 인라인 헤더 수학만 덮고 prebuilt 솔버 내부는 덮지 못함:
   - WASM     : FMA 명령이 없음 -> OK. 단 .a 는 relaxed-simd 0 필수
                (relaxed_madd 는 융합 여부가 구현 정의).
   - MSVC x64 : 기본 /fp:precise -> OK
   - Android  : NDK clang(ARM)은 FMA 있음 -> Box3D를 소스에서 -ffp-contract=off로
                반드시 재빌드, 아니면 물리 발산.

 또한: 물리는 고정 timeStep + subStepCount로 스텝할 것.
 검증: 플랫폼 간 물리 상태 해시 비교 (공식 보장 없음).
==============================================================================*/

#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

#include <imgui.h>
#include <imgui_impl_sdl3.h>
#include <imgui_impl_opengl3.h>

#include "crApp.h"
#include "crScreenRef.h"
#include "crGraphics.h"
#include "crWasmBridge.h"

SDL_AppResult SDL_AppInit( void** appstate, int argc, char* argv[] )
{
    ( void )argc;
    ( void )argv;

#ifdef __EMSCRIPTEN__
    crWasmBridge::InstallLogRedirect();
#endif

    SDL_SetAppMetadata( "cromakga3d", "0.0.1", "com.visai.cromakga3d" );

    SDL_SetHint( SDL_HINT_VIDEO_FORCE_EGL, "1" );
    SDL_SetHint( SDL_HINT_OPENGL_ES_DRIVER, "1" );

    // without this SDL infers the orientation from the window it created, and a fullscreen window
    // takes the device's own size — which locks a landscape build to portrait on a tall phone
    SDL_SetHint( SDL_HINT_ORIENTATIONS, crScreenRef::ORIENTATIONS );

#ifdef __EMSCRIPTEN__
    // both default to the id "canvas"; the shell names its element cr-canvas
    SDL_SetHint( SDL_HINT_EMSCRIPTEN_CANVAS_SELECTOR, "#cr-canvas" );

    // default is the whole window, which swallows keystrokes meant for the shell's own inputs and
    // feeds them to the game as well. same selector form as above — the value reaches
    // querySelector, so a bare id matches a tag name and "#canvas" matches an element we renamed
    SDL_SetHint( SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT, "#cr-canvas" );
#endif

//#ifndef NDEBUG
    SDL_SetLogPriority( SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_TRACE );
    SDL_SetLogPriority( SDL_LOG_CATEGORY_AUDIO, SDL_LOG_PRIORITY_TRACE );
    SDL_SetLogPriority( SDL_LOG_CATEGORY_RENDER, SDL_LOG_PRIORITY_TRACE );
    SDL_SetLogPriority( SDL_LOG_CATEGORY_GPU, SDL_LOG_PRIORITY_TRACE );
    SDL_SetLogPriority( CR_LOG_CATEGORY_NET, SDL_LOG_PRIORITY_TRACE );
    SDL_SetLogPriority( CR_LOG_CATEGORY_PHYSICS, SDL_LOG_PRIORITY_TRACE );
    SDL_SetLogPriority( CR_LOG_CATEGORY_ECS, SDL_LOG_PRIORITY_TRACE );
    SDL_SetLogPriority( CR_LOG_CATEGORY_SAVELOAD, SDL_LOG_PRIORITY_TRACE );
    SDL_SetLogPriority( CR_LOG_CATEGORY_POSTPROCESS, SDL_LOG_PRIORITY_TRACE );
    SDL_SetLogPriority( CR_LOG_CATEGORY_ASSET_TEXTURE, SDL_LOG_PRIORITY_TRACE );
    SDL_SetLogPriority( CR_LOG_CATEGORY_ASSET_FONT, SDL_LOG_PRIORITY_TRACE );
    SDL_SetLogPriority( CR_LOG_CATEGORY_ASSET_AUDIO, SDL_LOG_PRIORITY_TRACE );
//#endif

    {
        SDL_SetLogPriority( CR_LOG_CATEGORY_TODO, SDL_LOG_PRIORITY_TRACE );
    }

    if( SDL_Init( SDL_INIT_VIDEO | SDL_INIT_GAMEPAD ) == false )
    {
        SDL_LogError( SDL_LOG_CATEGORY_APPLICATION, "SDL_AppInit() - Couldn't initialize SDL: %s", SDL_GetError() );
        return SDL_APP_FAILURE;
    }

    SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES );
    SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 3 );
    SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 0 );
    SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 24 );   // the 3D pass renders straight to the backbuffer when post-processing is off
    SDL_GL_SetAttribute( SDL_GL_ALPHA_SIZE, 0 );    // opaque backbuffer — on the web this is the WebGL `alpha` attribute, and leaving it on
                                                    // both costs a per-pixel composite against the page and lets a stray output alpha show it through

#if defined(__ANDROID__) || defined(__IPHONEOS__)
    uint64_t windowFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN;
#else
    uint64_t windowFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE;
#endif

#ifdef __EMSCRIPTEN__
    windowFlags |= SDL_WINDOW_HIGH_PIXEL_DENSITY;
#endif

    SDL_Window* sdlWindow = SDL_CreateWindow( "cromakga3d", crScreenRef::WIDTH, crScreenRef::HEIGHT, windowFlags );
    if( sdlWindow == nullptr )
    {
        SDL_LogError( SDL_LOG_CATEGORY_APPLICATION, "SDL_AppInit() - Couldn't create window: %s", SDL_GetError() );
        return SDL_APP_FAILURE;
    }

#ifdef __EMSCRIPTEN__
    crWasmBridge::AttachWindow( sdlWindow );
#endif

    SDL_GLContext glContext = SDL_GL_CreateContext( sdlWindow );
    if( glContext == nullptr )
    {
        SDL_LogError( SDL_LOG_CATEGORY_APPLICATION, "SDL_AppInit() - Couldn't create GL context: %s", SDL_GetError() );
        return SDL_APP_FAILURE;
    }

    //SDL_GL_SetSwapInterval( 0 );
    SDL_GL_SetSwapInterval( 1 );    // * for vsync

#if !defined( __EMSCRIPTEN__ ) && !defined( __ANDROID__ )
    if( gladLoadGLES2Loader( ( GLADloadproc )SDL_GL_GetProcAddress ) == 0 )
    {
        SDL_LogError( SDL_LOG_CATEGORY_APPLICATION, "SDL_AppInit() - Failed to load GLES functions via GLAD" );
        return SDL_APP_FAILURE;
    }
#endif

    SDL_LogTrace( SDL_LOG_CATEGORY_RENDER, "GL_VERSION:  %s", glGetString( GL_VERSION ) );
    SDL_LogTrace( SDL_LOG_CATEGORY_RENDER, "GL_RENDERER: %s", glGetString( GL_RENDERER ) );

    {
        IMGUI_CHECKVERSION();
        ImGui::CreateContext();
        ImGuiIO& io = ImGui::GetIO();
        io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
        io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;

        io.ConfigNavCaptureKeyboard = false;
        ImGui::StyleColorsDark();

        ImGui_ImplSDL3_InitForOpenGL( sdlWindow, glContext );
        ImGui_ImplOpenGL3_Init( "#version 300 es" );

        // ImFontAtlas takes ownership and releases with IM_FREE, so the bytes must come from
        // IM_ALLOC rather than straight from SDL_LoadFile
        size_t fontSize = 0;
        void*  fontFile = SDL_LoadFile( "crassets/fonts/DejaVuSansMono.ttf", &fontSize );
        if( fontFile != nullptr )
        {
            void* fontData = IM_ALLOC( fontSize );
            SDL_memcpy( fontData, fontFile, fontSize );
            SDL_free( fontFile );
            io.Fonts->AddFontFromMemoryTTF( fontData, static_cast<int>( fontSize ), 15.0f );
        }
    }

    crApp* app = new crApp();
    *appstate = app;

#ifdef __EMSCRIPTEN__
    crWasmBridge::AttachApp( app );
#endif

    return app->Init( sdlWindow, glContext );
}

SDL_AppResult SDL_AppEvent( void* appstate, SDL_Event* event )
{
    crApp* app = static_cast<crApp*>( appstate );
    return app->HandleEvent( event );
}

SDL_AppResult SDL_AppIterate( void* appstate )
{
    crApp* app = static_cast<crApp*>( appstate );

    if( app->HasTargetFrameTimeElapsed() == false )
        return SDL_APP_CONTINUE;

    crFrameDelta frameDelta = app->FrameBegin();
    app->EarlyUpdate();
    while( app->FixedUpdate() )
    {
        ;
    }
    app->Update( frameDelta );

    app->Render( frameDelta );

#ifdef __EMSCRIPTEN__
    crWasmBridge::SyncDevUi();
    crWasmBridge::PumpShellConsole();
#endif

    return SDL_APP_CONTINUE;
}

void SDL_AppQuit( void* appstate, SDL_AppResult result )
{
    ( void )result;

    SDL_GLContext glContext = nullptr;

    crApp* app = static_cast<crApp*>( appstate );
    if( app != nullptr )
    {
        glContext = app->glContext;
        app->Cleanup();
        delete app;
    }

    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplSDL3_Shutdown();
    ImGui::DestroyContext();

    if( glContext != nullptr )
    {
        SDL_GL_DestroyContext( glContext );
    }
}
