#include "crRenderSystem.h"

#include <SDL3/SDL_timer.h>

#include "crApp.h"

#include "crBatchedPrimitives.h"
#include "crBatchedTexts.h"
#include "crCamera.h"
#include "crEcs.h"
#include "crGraphics.h"
#include "crGraphicsUniforms.h"
#include "crMath.h"
#include "crParticleSystem.h"
#include "crPostProcess.h"
#include "crRibbonTrailSystem.h"
#include "crShadowMap.h"

void crRenderSystem::Init()
{
    _primitives = new crBatchedPrimitives;
    _primitives->Init();
}
void crRenderSystem::Cleanup()
{
    if( _primitives != nullptr )
    {
        _primitives->Cleanup();
        delete _primitives;
        _primitives = nullptr;
    }
}

void crRenderSystem::BeginScenePass( const crApp* app, float alpha )
{
    crGraphics* graphics = app->graphics;

    const float4x4 viewProjection = app->camera->ViewProjection();

    float4x4 lightVPs[ crShadowMap::CASCADE_COUNT ];
    for( int32_t i = 0; i < crShadowMap::CASCADE_COUNT; ++i )
        lightVPs[ i ] = graphics->ShadowMap()->LightVP( graphics->Uniforms()->lightDir, app->camera, i );

    // shader time — integer modulo on the absolute sources (step index / SDL ticks), so wrapping is
    // exact and drift-free. simTime is render-interpolated (alpha) to stay smooth above 60 Hz
    const uint64_t wrapTicks = static_cast<uint64_t>( crMath::Round( static_cast<float>( crGraphicsUniforms::WRAP_SECONDS ) / crApp::FIXED_TIMESTEP ) );
    const float    simTime   = ( ( static_cast<float>( app->SimulationStepIndex() % wrapTicks ) + alpha ) * crApp::FIXED_TIMESTEP );
    const float    realTime  = ( static_cast<float>( SDL_GetTicks() % ( crGraphicsUniforms::WRAP_SECONDS * 1000ULL ) ) * 0.001f );

    const crPostProcess::FogSettings& fog = graphics->PostProcess()->fog;
    graphics->Uniforms()->Update( viewProjection, app->camera->EyePosition(), lightVPs, fog.color, fog.start, fog.end, fog.enabled, simTime, realTime );

    // cull volumes: camera geometry against the view frustum, shadow casters against each cascade's light frustum
    const crFrustum cameraFrustum = crMath::FrustumFromMatrix( viewProjection );
    crFrustum       cascadeFrustums[ crShadowMap::CASCADE_COUNT ];
    for( int32_t i = 0; i < crShadowMap::CASCADE_COUNT; ++i )
        cascadeFrustums[ i ] = crMath::FrustumFromMatrix( lightVPs[ i ] );

    FillPrimitives( app, alpha, cameraFrustum, cascadeFrustums );

    {// shadow passes — one light-space depth render per cascade, each drawing only its own culled casters
        const GLuint program    = graphics->Program( EShader::SHADOW );
        const GLint  cascadeLoc = glGetUniformLocation( program, "u_cascade" );
        glUseProgram( program );

        for( int32_t i = 0; i < crShadowMap::CASCADE_COUNT; ++i )
        {
            graphics->ShadowMap()->BeginPass( i );
            glUniform1i( cascadeLoc, i );
            _primitives->FlushShadow( program, i );
        }
    }

    graphics->PostProcess()->BeginScene( graphics->clearColor );   // rebinds the scene target + viewport

    graphics->BeginBatches();

    {// camera pass — samples the shadow cascades on unit 0; opaque first, then the transparent groups
        const GLuint program = graphics->Program( EShader::PRIMITIVE );
        glUseProgram( program );
        glUniform1i( glGetUniformLocation( program, "u_shadowMap" ), 0 );
        glActiveTexture( GL_TEXTURE0 );
        glBindTexture( GL_TEXTURE_2D_ARRAY, graphics->ShadowMap()->DepthTexture() );

        const float3 eye = app->camera->EyePosition();

        _primitives->Flush( program );

        _primitives->FlushTransparent( program, eye, app->camera->Forward() );

        // ribbon trails — additive, RIBBON_TRAIL (VP from the UBO). the system samples emitter transforms,
        // fades detached trails, builds in parallel, and draws in one primitive-restart pass.
        // additive fog fades to 0; disabled fog = start beyond the far plane so nothing attenuates
        const float ribbonFogStart = fog.enabled ? fog.start : 1.0e9f;
        const float ribbonFogEnd   = fog.enabled ? fog.end   : ( 1.0e9f + 1.0f );
        app->ribbonTrails->Render( eye, ribbonFogStart, ribbonFogEnd, graphics->Program( EShader::RIBBON_TRAIL ),
                                   graphics->Texture( "trail_1x1white" ),       // default base — trails with textureId 0 fall back to this
                                   graphics->Texture( "trail_1x1gray" ) );      // flowing-energy noise (scrolled by u_timeParams.x)

        // particles — additive billboards sampling atlas 0; the instance array is upload-ready (fused sim sweep)
        app->particles->Render( graphics->Program( EShader::BILLBOARD ), graphics );

        glBindTexture( GL_TEXTURE_2D_ARRAY, 0 );
    }
}
void crRenderSystem::EndScenePass( const crApp* app )
{
    app->graphics->PostProcess()->Execute();   // scene -> backbuffer; everything after draws on top, unprocessed

    app->graphics->FlushTexts( app->camera->PixelProjection() );
}

void crRenderSystem::RenderTextWorld( const crApp* app, const char* utf8, float3 worldPos, int32_t font, color4 color, float scale /*= 1.0f*/, float2 pivot /*= { 0.0f, 0.0f }*/ )
{
    // 3D anchor -> screen px (top-left, Y-down) -> pixel-ortho space (bottom-left, Y-up)
    const crScreenPoint screen = app->camera->WorldToScreen( worldPos );
    if( screen.visible == false )
        return;   // behind the camera

    const float  viewportH = static_cast<float>( app->camera->Viewport().y );
    const float2 anchorPx  = float2( screen.pos.x, viewportH - screen.pos.y );

    app->graphics->Texts()->AddString( app->graphics->FontAtlas(), utf8, anchorPx, font, color, scale, pivot, app->devText );
}
void crRenderSystem::RenderTextScreen( const crApp* app, const char* utf8, float2 screenPosPx, int32_t font, color4 color, float scale /*= 1.0f*/, float2 pivot /*= { 0.0f, 0.0f }*/ )
{
    app->graphics->Texts()->AddString( app->graphics->FontAtlas(), utf8, screenPosPx, font, color, scale, pivot, nullptr );
}

void crRenderSystem::ClearUnusedMemory()
{
    _primitives->ClearUnusedMemory();
}

uint8_t crRenderSystem::RegisterPrimitiveMaterial( GLuint program )
{
    return _primitives->RegisterMaterial( program );
}

void crRenderSystem::FillPrimitives( const crApp* app, float alpha, const crFrustum& cameraFrustum, const crFrustum* cascadeFrustums )
{
    _primitives->Begin( cameraFrustum, cascadeFrustums );

    auto view = app->ecs->registry.view<const CTransform, const CPrimitive>();
    for( entt::entity e : view )
    {
        const CTransform& t = view.get<const CTransform>( e );
        const CPrimitive&      m = view.get<const CPrimitive>( e );

        const float3 pos = crMath::Lerp3( float3( t.previous.p ), float3( t.current.p ), alpha );
        const quat4  rot = crMath::NLerp( t.previous.q, t.current.q, alpha );

        const uint8_t flags   = static_cast<uint8_t>( m.flags );
        const bool    casts   = ( flags & static_cast<uint8_t>( EPrimitiveFlags::CAST_SHADOW ) ) != 0;
        const float   receive = ( ( flags & static_cast<uint8_t>( EPrimitiveFlags::RECEIVE_SHADOW ) ) != 0 ) ? 1.0f : 0.0f;

        _primitives->Add( m.shape, m.blend, rot, pos, m.scale, m.color, m.emissive, receive, casts, m.fresnel, m.custom0, m.custom1, m.custom2, m.custom3, m.custom4, m.custom5, m.material );
    }
}
