#include "crParticleSystem.h"

#include <SDL3/SDL_log.h>

#include "crApp.h"
#include "crSpriteAtlas.h"
#include "crEcs.h"
#include "crEcsComponents.h"
#include "crGraphics.h"
#include "crMath.h"
#include "crTasks.h"

void crParticleUpdateTask::ExecuteRange( enki::TaskSetPartition range, uint32_t threadIndex )
{
    ( void )threadIndex;

    for( uint32_t i = range.start; i < range.end; ++i )
    {
        const float3 g    = gravity[ i ];
        const float  damp = ( 1.0f - ( drag[ i ] * dt ) );

        float3& v = vel[ i ];
        v.x = ( ( v.x + ( g.x * dt ) ) * damp );
        v.y = ( ( v.y + ( g.y * dt ) ) * damp );
        v.z = ( ( v.z + ( g.z * dt ) ) * damp );

        float3& p = instances[ i ].pos;
        p.x += ( v.x * dt );
        p.y += ( v.y * dt );
        p.z += ( v.z * dt );

        age[ i ] += dt;

        // life ramps — size and color lerp start -> end over the particle's lifetime
        const float t = crMath::Clamp01( age[ i ] * invLifetime[ i ] );
        instances[ i ].size  = crMath::Lerp( sizeStart[ i ], sizeEnd[ i ], t );
        instances[ i ].color = crMath::LerpColor( colorStart[ i ], colorEnd[ i ], t );

        // spin — advance the in-plane angle by one step's delta (complex multiply, no trig).
        // float drift over a particle lifetime is ~1e-5 off unit length — no renormalization needed
        const float2 cs = instances[ i ].cosSin;
        const float2 d  = rotStep[ i ];
        instances[ i ].cosSin = float2( ( cs.x * d.x ) - ( cs.y * d.y ), ( cs.x * d.y ) + ( cs.y * d.x ) );
    }
}

void crParticleSystem::Init()
{
    _batch.Init();

    _burstRng.Seed( BURST_SEED );

    _instances.Resize( CAPACITY );
    _vel.Resize( CAPACITY );
    _age.Resize( CAPACITY );
    _invLifetime.Resize( CAPACITY );
    _sizeStart.Resize( CAPACITY );
    _sizeEnd.Resize( CAPACITY );
    _colorStart.Resize( CAPACITY );
    _colorEnd.Resize( CAPACITY );
    _rotStep.Resize( CAPACITY );
    _gravity.Resize( CAPACITY );
    _drag.Resize( CAPACITY );
    _atlas.Resize( CAPACITY );

    _emitters.Reserve( 64 );
}
void crParticleSystem::Cleanup()
{
    _batch.Cleanup();
}
void crParticleSystem::Clear()
{
    _count = 0;
    _emitters.Clear();
    _seedCounter = 0;
    _warned      = false;
    _burstRng.Seed( BURST_SEED );
}

void crParticleSystem::FixedUpdate( const crApp* app, float fdt )
{
    // parallel fused sweep — each particle touches only its own slot
    if( _count > 0 )
    {
        crParticleUpdateTask task;
        task.instances   = _instances.Data();
        task.vel         = _vel.Data();
        task.age         = _age.Data();
        task.invLifetime = _invLifetime.Data();
        task.sizeStart   = _sizeStart.Data();
        task.sizeEnd     = _sizeEnd.Data();
        task.colorStart  = _colorStart.Data();
        task.colorEnd    = _colorEnd.Data();
        task.rotStep     = _rotStep.Data();
        task.gravity     = _gravity.Data();
        task.drag        = _drag.Data();
        task.dt          = fdt;
        task.m_SetSize   = static_cast<uint32_t>( _count );
        task.m_MinRange  = UPDATE_GRAIN;
        crTasks::scheduler.AddTaskSetToPipe( &task );
        crTasks::scheduler.WaitforTask( &task );
    }

    // serial dead compaction (swap-remove keeps [0, _count) dense; order changes are fine — additive
    // draw is order-independent, and the swapped-in tail particle was already integrated this step)
    int32_t i = 0;
    while( i < _count )
    {
        if( ( _age.At( i ) * _invLifetime.At( i ) ) >= 1.0f )
        {
            const int32_t last   = ( _count - 1 );
            _instances.At( i )   = _instances.At( last );
            _vel.At( i )         = _vel.At( last );
            _age.At( i )         = _age.At( last );
            _invLifetime.At( i ) = _invLifetime.At( last );
            _sizeStart.At( i )   = _sizeStart.At( last );
            _sizeEnd.At( i )     = _sizeEnd.At( last );
            _colorStart.At( i )  = _colorStart.At( last );
            _colorEnd.At( i )    = _colorEnd.At( last );
            _rotStep.At( i )     = _rotStep.At( last );
            _gravity.At( i )     = _gravity.At( last );
            _drag.At( i )        = _drag.At( last );
            _atlas.At( i )       = _atlas.At( last );
            --_count;
        }
        else
            ++i;
    }

    Emit( app, fdt );
}

void crParticleSystem::EmitBurst( const crApp* app, const CParticleEmitter& params, float3 pos, int32_t count )
{
    const crAtlasSprite sprite = app->graphics->Sprite( params.spriteIndex );
    const int32_t       atlas  = crGraphics::SpriteHandleAtlas( params.spriteIndex );
    for( int32_t i = 0; i < count; ++i )
        SpawnFromParams( params, pos, sprite, atlas, _burstRng, crApp::FIXED_TIMESTEP );
}

void crParticleSystem::Render( GLuint program, const crGraphics* graphics )
{
    if( _count <= 0 )
    {
        _drawsPrev = 0;
        return;
    }

    int32_t counts[ 256 ] = {};
    for( int32_t i = 0; i < _count; ++i )
        ++counts[ _atlas.At( i ) ];

    int32_t buckets   = 0;
    int32_t onlyAtlas = 0;
    for( int32_t a = 0; a < 256; ++a )
    {
        if( counts[ a ] > 0 )
        {
            ++buckets;
            onlyAtlas = a;
        }
    }

    if( buckets == 1 )
    {
        _batch.Flush( program, graphics->SpriteAtlas( onlyAtlas )->Texture(), _instances.Data(), _count );
    }
    else
    {
        // scatter into an atlas-grouped copy; the pool itself stays in sim order
        int32_t offsets[ 256 ];
        int32_t running = 0;
        for( int32_t a = 0; a < 256; ++a )
        {
            offsets[ a ] = running;
            running += counts[ a ];
        }

        _renderScratch.Resize( _count );
        for( int32_t i = 0; i < _count; ++i )
        {
            int32_t& at = offsets[ _atlas.At( i ) ];
            _renderScratch.At( at ) = _instances.At( i );
            ++at;
        }

        int32_t start = 0;
        for( int32_t a = 0; a < 256; ++a )
        {
            if( counts[ a ] > 0 )
                _batch.Flush( program, graphics->SpriteAtlas( a )->Texture(), &_renderScratch.At( start ), counts[ a ] );
            start += counts[ a ];
        }
    }

    if( ( buckets > _drawsPrev ) && ( buckets > 1 ) )
        SDL_LogWarn( SDL_LOG_CATEGORY_RENDER, "crParticleSystem: atlas buckets increased( %d -> %d, particles:%d )", _drawsPrev, buckets, _count );
    _drawsPrev = buckets;
}

int32_t crParticleSystem::AllocEmitterSlot()
{
    const int32_t n = _emitters.Size();
    for( int32_t i = 0; i < n; ++i )
    {
        if( _emitters.At( i ).used == false )
            return i;
    }

    if( _emitterWarned == false )
    {
        SDL_LogWarn( SDL_LOG_CATEGORY_APPLICATION, "crParticleSystem: emitter slots grew past the reserve - raise the reserve" );
        _emitterWarned = true;
    }
    _emitters.Add( EmitterSlot{} );
    return ( _emitters.Size() - 1 );
}
void crParticleSystem::Emit( const crApp* app, float fdt )
{
    const uint64_t step = app->SimulationStepIndex();

    const int32_t n = _emitters.Size();
    for( int32_t i = 0; i < n; ++i )
        _emitters.At( i ).seen = false;

    // expired fire-and-forget emitters — collected here (destroying mid-view is unsafe), reaped
    // after the sweep; their unseen slots free below. overflow just waits one more step
    entt::entity dead[ 64 ];
    int32_t      deadCount = 0;

    // spawn from each living emitter at its sim position (deterministic: fixed iteration order,
    // per-emitter RNG streams). serial for now — flip to prefix-sum + parallel fill if per-step
    // spawn totals reach the thousands
    auto view = app->ecs->registry.view<CParticleEmitter, const CTransform>();
    for( entt::entity entity : view )
    {
        CParticleEmitter& e = view.get<CParticleEmitter>( entity );
        const CTransform& t = view.get<const CTransform>( entity );

        if( ( e.killStep != 0 ) && ( step >= e.killStep ) )
        {
            if( deadCount < 64 )
                dead[ deadCount++ ] = entity;
            continue;   // no longer emits — slot goes unseen and frees below
        }

        if( e.poolSlot < 0 )
        {
            e.poolSlot = AllocEmitterSlot();

            EmitterSlot& fresh = _emitters.At( e.poolSlot );
            fresh.rng.Seed( EMITTER_SEED_BASE + _seedCounter );   // allocation-ordered — deterministic
            fresh.accum = 0.0f;
            fresh.used  = true;
            ++_seedCounter;
        }

        EmitterSlot& slot = _emitters.At( e.poolSlot );
        slot.seen   = true;
        slot.accum += ( e.rate * fdt );

        int32_t spawns = static_cast<int32_t>( slot.accum );   // floor — the fraction carries over
        slot.accum -= static_cast<float>( spawns );

        const float3        pos    = float3( t.current.p );
        const crAtlasSprite sprite = app->graphics->Sprite( e.spriteIndex );
        const int32_t       atlas  = crGraphics::SpriteHandleAtlas( e.spriteIndex );
        for( int32_t k = 0; k < spawns; ++k )
            SpawnFromParams( e, pos, sprite, atlas, slot.rng, fdt );
    }

    // free slots whose emitter is gone (component removed / entity destroyed) — live particles are
    // fire-and-forget and simply finish their lifetimes
    for( int32_t i = 0; i < n; ++i )
    {
        EmitterSlot& slot = _emitters.At( i );
        if( slot.used && ( slot.seen == false ) )
            slot.used = false;
    }

    for( int32_t i = 0; i < deadCount; ++i )
        app->ecs->registry.destroy( dead[ i ] );
}
void crParticleSystem::SpawnFromParams( const CParticleEmitter& e, float3 pos, const crAtlasSprite& sprite, int32_t atlas, crRandom& rng, float fdt )
{
    // uniform random direction (z + azimuth method), scaled by 0..spread, on top of the base velocity
    const float  z   = rng.NextFloat32( -1.0f, 1.0f );
    const float  r   = crMath::Sqrt( 1.0f - ( z * z ) );
    const float2 cs  = crMath::CosSin( rng.NextFloat32( 0.0f, 2.0f * crMath::PI ) );
    const float  mag = rng.NextFloat32( 0.0f, e.spread );

    const float3 vel = float3( e.velocity.x + ( r * cs.x * mag ),
                               e.velocity.y + ( z * mag ),
                               e.velocity.z + ( r * cs.y * mag ) );

    const float  lifetime   = rng.NextFloat32( e.lifetimeMin, e.lifetimeMax );
    const float  sizeStart  = rng.NextFloat32( e.sizeMin, e.sizeMax );
    const float  intensity  = rng.NextFloat32( e.intensityMin, e.intensityMax );
    const color4 colorStart = color4( ( e.color.r * intensity ),    ( e.color.g * intensity ),    ( e.color.b * intensity ),    e.color.a );
    const color4 colorEnd   = color4( ( e.colorEnd.r * intensity ), ( e.colorEnd.g * intensity ), ( e.colorEnd.b * intensity ), e.colorEnd.a );

    // spin — trig happens once here: a random start angle (orientation variety), and the
    // per-step delta the sweep advances by complex multiply
    float2 rotCosSin = crMath::CosSin( rng.NextFloat32( 0.0f, 2.0f * crMath::PI ) );
    if( e.alignToVelocity )
    {
        const float len = crMath::Sqrt( ( vel.x * vel.x ) + ( vel.y * vel.y ) );
        if( len > 0.0001f )
            rotCosSin = float2( vel.x / len, vel.y / len );
    }
    const float  spin    = rng.NextFloat32( e.spinMin, e.spinMax );
    const float2 rotStep = crMath::CosSin( spin * fdt );

    Spawn( pos, vel, lifetime, sizeStart, ( sizeStart * e.sizeEndScale ), colorStart, colorEnd, sprite.uvMin, sprite.uvMax, atlas, rotCosSin, rotStep, e.gravity, e.drag );
}
void crParticleSystem::Spawn( float3 pos, float3 vel, float lifetime, float sizeStart, float sizeEnd, color4 colorStart, color4 colorEnd, float2 uvMin, float2 uvMax, int32_t atlas, float2 rotCosSin, float2 rotStep, float3 gravity, float drag )
{
    if( _count >= CAPACITY )
    {
        if( _warned == false )
        {
            SDL_LogWarn( SDL_LOG_CATEGORY_APPLICATION, "crParticleSystem: pool full (%d) — dropping spawns", CAPACITY );
            _warned = true;
        }
        return;
    }

    // newborns render with start values until the next step's sweep ramps them
    crBatchedBillboards::BillboardInstance& in = _instances.At( _count );
    in.pos    = pos;
    in.size   = sizeStart;
    in.color  = colorStart;
    in.uvMin  = uvMin;
    in.uvMax  = uvMax;
    in.cosSin = rotCosSin;

    _vel.At( _count )         = vel;
    _age.At( _count )         = 0.0f;
    _invLifetime.At( _count ) = ( lifetime > 0.0001f ) ? ( 1.0f / lifetime ) : 10000.0f;   // <= 0 would divide by zero; a huge inverse just dies next step
    _sizeStart.At( _count )   = sizeStart;
    _sizeEnd.At( _count )     = sizeEnd;
    _colorStart.At( _count )  = colorStart;
    _colorEnd.At( _count )    = colorEnd;
    _rotStep.At( _count )     = rotStep;
    _gravity.At( _count )     = gravity;
    _drag.At( _count )        = drag;
    _atlas.At( _count )       = static_cast<uint8_t>( atlas );
    ++_count;
}
