#include "crApp.h"

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

#include "crDevApp.h"
#include "crDevAudio.h"
#include "crDevCamera.h"
#include "crDevConsole.h"
#include "crDevGizmos.h"
#include "crDevGraphics.h"
#include "crDevNet.h"
#include "crDevPhysics.h"
#include "crDevPostProcess.h"
#include "crDevProfiler.h"
#include "crDevText.h"

#include "game/Game.h"

#include "crAudio.h"
#include "crBuildInfo.h"
#include "crCamera.h"
#include "crScreenRef.h"
#include "crEcs.h"
#include "crFontAtlas.h"
#include "crGraphics.h"
#include "crGraphicsStats.h"
#include "crInputSystem.h"
#include "crNet.h"
#include "crParticleSystem.h"
#include "crPhysics.h"
#include "crPostProcess.h"
#include "crRenderSystem.h"
#include "crRibbonTrailSystem.h"
#include "crTasks.h"
#include "crUi.h"
#include "crUserData.h"
#include "crWasmBridge.h"

SDL_AppResult crApp::Init( SDL_Window* window, SDL_GLContext glContext )
{
    SDL_LogInfo( SDL_LOG_CATEGORY_APPLICATION, "======== buildstamp %s ========", crBuildInfo::STAMP );

    this->window = window;
    this->glContext = glContext;

    _lastTicks = SDL_GetPerformanceCounter();
    _simAccumulator = 0.0f;

    crTasks::scheduler.Initialize( crTasks::WORKER_COUNT );

    devApp = new crDevApp;
    devApp->Init();

    devAudio = new crDevAudio;
    devAudio->Init( this );

    devCamera = new crDevCamera;
    devCamera->Init( this );

    devConsole = new crDevConsole;
    devConsole->Init( this );

    devGizmos = new crDevGizmos;
    devGizmos->Init( this );

    devGraphics = new crDevGraphics;
    devGraphics->Init( this );

    devNet = new crDevNet;
    devNet->Init( this );   // only registers console commands — safe before net exists; Drain runs later once net is up

    devPhysics = new crDevPhysics;
    devPhysics->Init( this );

    devPostProcess = new crDevPostProcess;
    devPostProcess->Init( this );

    devProfiler = new crDevProfiler;
    devProfiler->Init();

    devText = new crDevText;
    devText->Init( this );

    audio = new crAudio;
    audio->Init();   // failure is non-fatal — audio calls just stay silent

    camera = new crCamera;
    camera->Init();

    ecs = new crEcs;
    ecs->Init();

    int2 windowSizeInPixel = {};
    SDL_GetWindowSizeInPixels( window, &windowSizeInPixel.x, &windowSizeInPixel.y );

    graphics = new crGraphics;
    bool rv = graphics->Init( windowSizeInPixel );
    if( rv == false )
        return SDL_APP_FAILURE;

    inputSys = new crInputSystem;
    inputSys->Init();

    physics = new crPhysics;
    physics->Init( this );

    renderSys = new crRenderSystem;
    renderSys->Init();

    ribbonTrails = new crRibbonTrailSystem;
    ribbonTrails->Init();

    particles = new crParticleSystem;
    particles->Init();

    ui = new crUi;
    ui->Init();

    userData = new crUserData;
    userData->Init( this );

    net = new crNet;
    net->Init();   // failure is non-fatal — net calls return INVALID_ID, game runs offline

    game = new Game;
    game->Init( this );

    AppendState( EAppStateFlags::INITTED );

    return SDL_APP_CONTINUE;
}

SDL_AppResult crApp::HandleEvent( SDL_Event* event )
{
    switch( event->type )   // ahead of the FILE_IO gate — it swallows every event while a save/load is in flight
    {
    case SDL_EVENT_WINDOW_MINIMIZED:
    case SDL_EVENT_WINDOW_HIDDEN:
    case SDL_EVENT_WINDOW_FOCUS_LOST:
    case SDL_EVENT_DID_ENTER_BACKGROUND:
        if( HasState( EAppStateFlags::BACKGROUNDED ) == false )
        {
            AppendState( EAppStateFlags::BACKGROUNDED );
            if( IsRunInBackground() == false )
                audio->SetPaused( true );
        }
        break;

    case SDL_EVENT_WINDOW_RESTORED:
    case SDL_EVENT_WINDOW_SHOWN:
    case SDL_EVENT_WINDOW_FOCUS_GAINED:
    case SDL_EVENT_DID_ENTER_FOREGROUND:
        if( HasState( EAppStateFlags::BACKGROUNDED ) )
        {
            RemoveState( EAppStateFlags::BACKGROUNDED );
            audio->SetPaused( false );
        }
        break;

    default:
        break;
    }

    const bool isMouseEvent = ( event->type == SDL_EVENT_MOUSE_MOTION ) ||
                              ( event->type == SDL_EVENT_MOUSE_BUTTON_DOWN ) ||
                              ( event->type == SDL_EVENT_MOUSE_BUTTON_UP ) ||
                              ( event->type == SDL_EVENT_MOUSE_WHEEL );
    const bool isKeyEvent = ( event->type == SDL_EVENT_KEY_DOWN ) ||
                            ( event->type == SDL_EVENT_KEY_UP ) ||
                            ( event->type == SDL_EVENT_TEXT_INPUT );

    if( event->type == SDL_EVENT_QUIT )
    {
        return SDL_APP_SUCCESS;
    }

    if( HasState( EAppStateFlags::FILE_IO_PENDING ) )
    {
        return SDL_APP_CONTINUE;
    }

    ImGui_ImplSDL3_ProcessEvent( event );   // ImGui sees every event; the gates below only decide who else does

    const ImGuiIO& io = ImGui::GetIO();

    if( isMouseEvent && io.WantCaptureMouse )
    {
        return SDL_APP_CONTINUE;   // an ImGui widget is under the pointer
    }

    if( isKeyEvent && io.WantCaptureKeyboard )
    {
        // one exception: a key going up states a fact about the key, not an intent to act, and
        // dropping it would leave a button latched from before the capture began. crInputSystem
        // discards an up that no press preceded; game->HandleEvent gets it raw and must not
        // assume pairing — a key can also come up while the window has no focus at all
        if( event->type != SDL_EVENT_KEY_UP )
        {
            return SDL_APP_CONTINUE;
        }
    }

    inputSys->UpdateInput( this, event );   // unified pointer (mouse + first finger) for the UI, plus the finger slots

    const bool isLeftDown = ( event->type == SDL_EVENT_MOUSE_BUTTON_DOWN ) && ( event->button.button == SDL_BUTTON_LEFT );
    const bool isLeftUp   = ( event->type == SDL_EVENT_MOUSE_BUTTON_UP )   && ( event->button.button == SDL_BUTTON_LEFT );

    const bool uiOwnsEvent = ( isLeftDown && ui->PointerCaptured() ) ||
                             ( isLeftUp   && ui->PointerHeld() ) ||
                             ( ( event->type == SDL_EVENT_MOUSE_MOTION ) && ui->PointerHeld() );

    if( uiOwnsEvent )
        return SDL_APP_CONTINUE;

    game->HandleEvent( this, event );

    return inputSys->TEST_HandleUserInput( this, event );
}

void crApp::Cleanup()
{
    if( game != nullptr )
    {
        game->Cleanup();
        delete game;
        game = nullptr;
    }

    if( net != nullptr )   // after game — game code may hold in-flight requests
    {
        net->Cleanup();
        delete net;
        net = nullptr;
    }

    if( devNet != nullptr )   // before devConsole — Cleanup unregisters its commands from the console
    {
        devNet->Cleanup();
        delete devNet;
        devNet = nullptr;
    }

    if( devApp != nullptr )
    {
        devApp->Cleanup();
        delete devApp;
        devApp = nullptr;
    }
    if( devAudio != nullptr )
    {
        devAudio->Cleanup();
        delete devAudio;
        devAudio = nullptr;
    }
    if( devCamera != nullptr )
    {
        devCamera->Cleanup();
        delete devCamera;
        devCamera = nullptr;
    }
    if( devConsole != nullptr )
    {
        devConsole->Cleanup();
        delete devConsole;
        devConsole = nullptr;
    }
    if( devGizmos != nullptr )
    {
        devGizmos->Cleanup();
        delete devGizmos;
        devGizmos = nullptr;
    }
    if( devGraphics != nullptr )
    {
        devGraphics->Cleanup();
        delete devGraphics;
        devGraphics = nullptr;
    }
    if( devPhysics != nullptr )
    {
        devPhysics->Cleanup();
        delete devPhysics;
        devPhysics = nullptr;
    }
    if( devPostProcess != nullptr )
    {
        devPostProcess->Cleanup();
        delete devPostProcess;
        devPostProcess = nullptr;
    }
    if( devProfiler != nullptr )
    {
        devProfiler->Cleanup();
        delete devProfiler;
        devProfiler = nullptr;
    }
    if( devText != nullptr )
    {
        devText->Cleanup();
        delete devText;
        devText = nullptr;
    }

    if( audio != nullptr )
    {
        audio->Cleanup();
        delete audio;
        audio = nullptr;
    }
    if( camera != nullptr )
    {
        camera->Cleanup();
        delete camera;
        camera = nullptr;
    }
    if( ecs != nullptr )
    {
        ecs->Cleanup();
        delete ecs;
        ecs = nullptr;
    }
    if( graphics != nullptr )
    {
        graphics->Cleanup();
        delete graphics;
        graphics = nullptr;
    }
    if( inputSys != nullptr )
    {
        inputSys->Cleanup();
        delete inputSys;
        inputSys = nullptr;
    }
    if( physics != nullptr )
    {
        physics->Cleanup();
        delete physics;
        physics = nullptr;
    }
    if( renderSys != nullptr )
    {
        renderSys->Cleanup();
        delete renderSys;
        renderSys = nullptr;
    }
    if( particles != nullptr )
    {
        particles->Cleanup();
        delete particles;
        particles = nullptr;
    }
    if( ui != nullptr )
    {
        ui->Cleanup();
        delete ui;
        ui = nullptr;
    }
    if( ribbonTrails != nullptr )
    {
        ribbonTrails->Cleanup();
        delete ribbonTrails;
        ribbonTrails = nullptr;
    }
    if( userData != nullptr )
    {
        userData->Cleanup();
        delete userData;
        userData = nullptr;
    }
}

bool crApp::IsVSyncSupported() const
{
#ifdef __EMSCRIPTEN__
    // the browser drives the loop through requestAnimationFrame and presents at the end of the
    // callback — SDL accepts the swap interval and it changes nothing. targetFps is the lever here
    return false;
#else
    return true;
#endif
}
bool crApp::IsVSyncEnabled() const
{
    int interval = 0;
    return SDL_GL_GetSwapInterval( &interval ) && ( interval != 0 );
}
void crApp::SetVSync( bool enable )
{
    if( SDL_GL_SetSwapInterval( enable ? 1 : 0 ) == false )
        SDL_LogWarn( SDL_LOG_CATEGORY_APPLICATION, "crApp: swap interval change failed: %s", SDL_GetError() );
}

void crApp::ResetSession()
{
    SetSimulationActive( false );
    SetSimulationSpeed( 1.0f );

    ecs->Reset();
    physics->ResetWorld( this );
    ribbonTrails->Reclaim();
    particles->Clear();
    renderSys->ClearUnusedMemory();
    graphics->PostProcess()->ClearQueuedRipples();
}

bool crApp::HasTargetFrameTimeElapsed()
{
    if( targetFps <= 0.0f )
        return true;

    const uint64_t now  = SDL_GetPerformanceCounter();
    const uint64_t freq = SDL_GetPerformanceFrequency();

    const float elapsed  = ( float )( now - _lastTicks ) / ( float )freq;
    const float deadline = ( 1.0f / targetFps ) - TARGET_FRAME_TIME_SLACK;

    if( elapsed >= deadline )
        return true;

#ifndef __EMSCRIPTEN__
    // skipping the swap leaves nothing to block on; WASM must not sleep — the browser drives the loop from rAF
    const float remain = deadline - elapsed;
    if( remain > 0.002f )
        SDL_Delay( ( uint32_t )( ( remain - 0.001f ) * 1000.0f ) );
#endif

    return false;
}
crFrameDelta crApp::FrameBegin()
{
    const uint64_t now  = SDL_GetPerformanceCounter();
    const uint64_t freq = SDL_GetPerformanceFrequency();

    float simSpeed = _simSpeed;
    if( HasState( EAppStateFlags::SIM_PAUSED ) )
        simSpeed = 0.0f;
    if( HasState( EAppStateFlags::FILE_IO_PENDING ) )
        simSpeed = 0.0f;
    if( HasState( EAppStateFlags::BACKGROUNDED ) && ( IsRunInBackground() == false ) )
        simSpeed = 0.0f;

    float unscaledDelta = ( float )( now - _lastTicks ) / ( float )freq;
    if( unscaledDelta > MAX_FRAME_DELTA )
        unscaledDelta = MAX_FRAME_DELTA;   // a stall (window drag, breakpoint, hidden tab) drops time instead of spiraling the fixed loop

    float scaledDelta = simSpeed * unscaledDelta;

    _lastTicks = now;

    _simAccumulator += scaledDelta;

    _frameDelta = { unscaledDelta, scaledDelta };
    return _frameDelta;
}
void crApp::EarlyUpdate()
{
    if( HasState( EAppStateFlags::FILE_IO_PENDING ) )
    {
        userData->UpdateEarly();
    }

    inputSys->PollInput( this );   // ahead of the fixed-step loop: the sim must see this frame's input, not the last one
}

void crApp::FixedUpdateOnce()
{
    inputSys->BeginSimStep();                       // this step's button edges; the latch keeps them until a step arrives

    ecs->SnapshotPrevTransforms();
    game->FixedUpdatePre( this );
    physics->FixedUpdate();
    ecs->SyncDynamicTransforms( FIXED_TIMESTEP );
    game->FixedUpdatePost( this );
    ribbonTrails->Emit( this );                     // record emitter positions at the fixed rate (after transforms are synced)
    particles->FixedUpdate( this, FIXED_TIMESTEP ); // parallel integrate -> compact -> emit (deterministic)

    ++_simStepIndex;
}
bool crApp::FixedUpdate()
{
    if( _simAccumulator < FIXED_TIMESTEP )
    {
        return false;
    }

    _simAccumulator -= FIXED_TIMESTEP;
    FixedUpdateOnce();

    return true;
}
void crApp::Update( crFrameDelta frameDelta )
{
    net->Update();      // transports push this frame's results into the queue
    devNet->Drain();    // test scaffolding: prints net events to the dev console (remove when a real game consumes them)

    game->Update( this );

    net->ClearAllEvents();   // frame-boundary consume — after every reader (test drain + game)

    audio->Update();

    inputSys->TEST_PollUserInput( this, frameDelta.unscaled );   // fly mode key polling
}
bool crApp::Render( crFrameDelta frameDelta )
{
    const float alpha = _simAccumulator / FIXED_TIMESTEP;

    int2 screenSizeInPixel = {};
    SDL_GetWindowSizeInPixels( window, &screenSizeInPixel.x, &screenSizeInPixel.y );

    if( ( screenSizeInPixel.x <= 0 ) || ( screenSizeInPixel.y <= 0 ) )
        return true;   // minimized / occluded — skip this frame (avoids glViewport(0,0) + aspect div-by-0)

    if( camera->SetViewport( screenSizeInPixel ) )
    {
        graphics->FontAtlas()->Rebuild( crUi::RasterScale( screenSizeInPixel ) );
        graphics->PostProcess()->Resize( screenSizeInPixel );

        // content scale, not SDL_GetWindowDisplayScale: that one multiplies in the pixel density,
        // which on Emscripten is the DPR the canvas has already applied to the drawable
        const float contentScale = SDL_GetDisplayContentScale( SDL_GetDisplayForWindow( window ) );
        crDevApp::ScaleUi( ( contentScale > 0.0f ) ? contentScale : 1.0f );
    }

    glViewport( 0, 0, screenSizeInPixel.x, screenSizeInPixel.y );

    crGraphicsStats::ResetGraphicsStats();

    renderSys->BeginScenePass( this, alpha );

    game->Render( this );

    renderSys->EndScenePass( this );

    game->RenderUi( this );

    {// DEV overlays — PP-clean; the wireframe still occludes via the scene depth texture
        devPhysics->TryRenderDebugDraw();
        devGizmos->TryRenderGizmos();
        devText->TryRenderDebugDraw();
    }

    {
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplSDL3_NewFrame();
        ImGui::NewFrame();

        if( showDevUi )
        {// DEV
            devApp->TryRenderPanel( this );
            devAudio->TryRenderPanel();
            devCamera->TryRenderPanel();
            devConsole->TryRenderPanel();
            devGizmos->TryRenderPanel();
            devGraphics->TryRenderPanel();
            devPostProcess->TryRenderPanel();
            devProfiler->TryRenderPanel( this, frameDelta.unscaled );

            game->RenderDevUi( this );   // game hooks run outside the ImGui frame — this one is the exception, for dev tuning panels
        }

        ImGui::Render();

        ImGui_ImplOpenGL3_RenderDrawData( ImGui::GetDrawData() );
    }

    inputSys->EndFrame();   // the frame consumed the pointer edges — clear before the next event batch

    return SDL_GL_SwapWindow( window );
}

bool crApp::IsFullscreen() const
{
    #ifdef __EMSCRIPTEN__
    return crWasmBridge::IsFullscreen();
    #else
    return ( ( SDL_GetWindowFlags( window ) & SDL_WINDOW_FULLSCREEN ) != 0 );
    #endif
}
void crApp::SetFullscreen( bool enable )
{
    #ifdef __EMSCRIPTEN__
    crWasmBridge::RequestFullscreen( enable );   // SDL's path skips the shell's orientation + wake lock
    #else
    if( SDL_SetWindowFullscreen( window, enable ) == false )
        SDL_LogWarn( SDL_LOG_CATEGORY_APPLICATION, "crApp: fullscreen request failed: %s", SDL_GetError() );
    #endif
}
