#include "crRibbonTrailSystem.h"

#include <algorithm>

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

void crRibbonBuildTask::ExecuteRange( enki::TaskSetPartition range, uint32_t threadIndex )
{
    ( void )threadIndex;
    batch->BuildRange( trails, vertBase, indexBase, static_cast<int32_t>( range.start ), static_cast<int32_t>( range.end ), eye, fogStart, fogEnd );
}

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

    _pool60.Init( RESERVE_POOL_60 );
    _pool300.Init( RESERVE_POOL_300 );
    _pool600.Init( RESERVE_POOL_600 );
}
void crRibbonTrailSystem::Cleanup()
{
    _batch.Cleanup();
}
void crRibbonTrailSystem::Reclaim()
{
    _pool60.Reclaim();
    _pool300.Reclaim();
    _pool600.Reclaim();
    _time = 0.0f;
}

void crRibbonTrailSystem::Emit( const crApp* app )
{
    const float dt = crApp::FIXED_TIMESTEP;
    _time += dt;

    _pool60.BeginSweep();
    _pool300.BeginSweep();
    _pool600.BeginSweep();

    // record each living emitter's sim position at the fixed rate (deterministic; no interpolation)
    auto view = app->ecs->registry.view<CRibbonTrail, const CTransform>();
    for( entt::entity e : view )
    {
        CRibbonTrail&     rt = view.get<CRibbonTrail>( e );
        const CTransform& t  = view.get<const CTransform>( e );

        if( rt.poolSlot < 0 )
        {
            crRibbonTrailParams params;
            params.color         = rt.color;
            params.tailColor     = rt.tailColor;
            params.headHalfWidth = rt.headHalfWidth;
            params.tailHalfWidth = rt.tailHalfWidth;
            params.intensity     = rt.intensity;
            params.emitDistance  = rt.emitDistance;
            params.fadeDuration  = rt.fadeDuration;
            params.tileLength    = rt.tileLength;
            params.textureId     = rt.textureId;

            // duration (seconds) -> ticks; 0 = full capacity (InitSlot resolves)
            const int32_t desiredPoints = static_cast<int32_t>( crMath::Round( rt.duration / dt ) );
            const int32_t pc            = static_cast<int32_t>( rt.capacity );   // caller-chosen size-class = pool index
            int32_t       idx           = -1;
            switch( rt.capacity )
            {
            case ERibbonCapacity::SEC_1: idx = _pool60.Alloc();  _pool60.InitSlot(  idx, params, desiredPoints, ( rt.duration > 0.0f ) ? rt.duration : ( 60.0f * dt ) ); break;
            case ERibbonCapacity::SEC_5: idx = _pool300.Alloc(); _pool300.InitSlot( idx, params, desiredPoints, ( rt.duration > 0.0f ) ? rt.duration : ( 300.0f * dt ) ); break;
            default:                     idx = _pool600.Alloc(); _pool600.InitSlot( idx, params, desiredPoints, ( rt.duration > 0.0f ) ? rt.duration : ( 600.0f * dt ) ); break;
            }
            rt.poolSlot = ( pc << 24 ) | idx;
        }

        const int32_t pc  = ( rt.poolSlot >> 24 );
        const int32_t idx = ( rt.poolSlot & 0x00FFFFFF );

        float3 pos = float3( t.current.p );
        if( ( rt.emitOffset.x != 0.0f ) || ( rt.emitOffset.y != 0.0f ) || ( rt.emitOffset.z != 0.0f ) )
        {
            const b3Vec3 local = b3RotateVector( t.current.q, { rt.emitOffset.x, rt.emitOffset.y, rt.emitOffset.z } );
            pos = float3( pos.x + local.x, pos.y + local.y, pos.z + local.z );
        }

        switch( pc )
        {
        case 0:  _pool60.Touch(  idx, pos, _time ); break;
        case 1:  _pool300.Touch( idx, pos, _time ); break;
        default: _pool600.Touch( idx, pos, _time ); break;
        }
    }

    _pool60.ExpireOld( _time );
    _pool300.ExpireOld( _time );
    _pool600.ExpireOld( _time );

    _pool60.Fade( dt );
    _pool300.Fade( dt );
    _pool600.Fade( dt );
}

static bool CompareTrailTexture( const crBatchedRibbonTrails::TrailInput& a, const crBatchedRibbonTrails::TrailInput& b )
{
    return a.textureId < b.textureId;
}

void crRibbonTrailSystem::Render( float3 eye, float fogStart, float fogEnd, GLuint program, GLuint defaultTexture, GLuint noiseTexture )
{
    // gather every pool's live slots into one contiguous input list (serial, cheap)
    _inputs.Clear();
    _vertBase.Clear();
    _indexBase.Clear();
    _runs.Clear();

    _pool60.Gather(  _inputs );
    _pool300.Gather( _inputs );
    _pool600.Gather( _inputs );

    const int32_t buildCount = _inputs.Size();

    // resolve the default texture, then sort by texture so each texture's trails are contiguous in
    // the index buffer (one draw per run). sorting happens BEFORE the prefix sums, so the workers'
    // disjoint-region invariant is untouched
    for( int32_t i = 0; i < buildCount; ++i )
    {
        if( _inputs.At( i ).textureId == 0 )
            _inputs.At( i ).textureId = defaultTexture;
    }
    std::sort( _inputs.Data(), _inputs.Data() + buildCount, CompareTrailTexture );

    // prefix-sum offsets + texture runs over the sorted list
    int32_t totalVerts   = 0;
    int32_t totalIndices = 0;
    for( int32_t i = 0; i < buildCount; ++i )
    {
        const crBatchedRibbonTrails::TrailInput& in = _inputs.At( i );

        _vertBase.Add( totalVerts );
        _indexBase.Add( totalIndices );

        const int32_t vc = ( in.count >= 2 ) ? ( in.count * 2 ) : 0;
        const int32_t ic = ( vc > 0 ) ? ( vc + 1 ) : 0;   // +1 restart separator

        if( ( _runs.Size() > 0 ) && ( _runs.At( _runs.Size() - 1 ).texture == in.textureId ) )
        {
            _runs.At( _runs.Size() - 1 ).indexCount += ic;
        }
        else
        {
            crBatchedRibbonTrails::Run run;
            run.texture     = in.textureId;
            run.indexOffset = totalIndices;
            run.indexCount  = ic;
            _runs.Add( run );
        }

        totalVerts   += vc;
        totalIndices += ic;
    }

    _batch.Prepare( totalVerts, totalIndices );

    // build in parallel (one task over the merged inputs), then one primitive-restart draw per run
    if( buildCount > 0 )
    {
        crRibbonBuildTask task;
        task.batch     = &_batch;
        task.trails    = _inputs.Data();
        task.vertBase  = _vertBase.Data();
        task.indexBase = _indexBase.Data();
        task.eye       = eye;
        task.fogStart  = fogStart;
        task.fogEnd    = fogEnd;
        task.m_SetSize  = buildCount;
        task.m_MinRange = BUILD_GRAIN;
        crTasks::scheduler.AddTaskSetToPipe( &task );
        crTasks::scheduler.WaitforTask( &task );
    }

    _batch.Flush( program, noiseTexture, _runs.Data(), _runs.Size() );
}
