#pragma once

#if defined( __EMSCRIPTEN__ ) || defined( __ANDROID__ )
#include <GLES3/gl3.h>
#else
#include <glad/glad.h>
#endif

#include <enkiTS/TaskScheduler.h>
#include <SDL3/SDL_assert.h>
#include <SDL3/SDL_log.h>

#include "crArray.h"
#include "crBatchedRibbonTrails.h"
#include "crMath.h"
#include "crStruct.h"

class crApp;

// enkiTS task adapter: builds a disjoint range of ribbon trails into pre-sized, prefix-summed output
// regions (no contention). render-only, no determinism concern
class crRibbonBuildTask : public enki::ITaskSet
{
public:
    crBatchedRibbonTrails*                   batch     = nullptr;
    const crBatchedRibbonTrails::TrailInput* trails    = nullptr;
    const int32_t*                           vertBase  = nullptr;
    const int32_t*                           indexBase = nullptr;
    float3 eye      = float3( 0.0f, 0.0f, 0.0f );
    float  fogStart = 0.0f;
    float  fogEnd   = 0.0f;

    void ExecuteRange( enki::TaskSetPartition range, uint32_t threadIndex ) override;
};

// per-trail authoring parameters — copied whole into the pool slot at allocation, because the slot
// must outlive the component (a detached trail keeps fading after its emitter is gone)
struct crRibbonTrailParams   // field semantics documented once, on CRibbonTrail (crEcsComponents.h)
{
    color4   color;
    color4   tailColor;
    float    headHalfWidth;
    float    tailHalfWidth;
    float    intensity;
    float    emitDistance;
    float    fadeDuration;
    float    tileLength;
    uint32_t textureId;
};

// one trail size-class: CAP points per slot, backed by a growable array. the ribbon system holds one
// instance per class and dispatches to them by CRibbonTrail.capacity. logic lives once here and is
// instantiated per CAP (crTrailPool<60>, <300>, ...)
template<int32_t CAP>
class crTrailPool
{
public:
    enum class EState : uint8_t { FREE = 0, ACTIVE, FADING };

    struct Slot
    {
        float3              points[ CAP ];
        float               times[ CAP ];  // sim-clock stamp per point — ExpireOld drops the tail past pointLife
        crRibbonTrailParams params;
        int32_t             start;
        int32_t             count;
        int32_t             maxPoints;    // working ring length (<= CAP), from the emitter's duration
        float               pointLife;    // seconds a recorded point survives — a stalled emitter's trail dissolves
        float               widthScale;   // 1 while attached; shrinks to 0 over fadeDuration once detached (the trail thins)
        float               fadeAlpha;    // 1 while attached; shrinks to 0 over fadeDuration once detached (the trail dims)
        EState              state;
        bool                seen;         // sweep mark: touched by a living emitter this step
    };

    void Init( int32_t reserve )
    {
        _reserve = reserve;
        _slots.Reserve( reserve );
    }

    void BeginSweep()
    {
        const int32_t n = _slots.Size();
        for( int32_t i = 0; i < n; ++i )
            _slots.At( i ).seen = false;
    }

    int32_t Alloc()   // reserve a slot (returns its index); grows with a warning when none are free
    {
        const int32_t n = _slots.Size();
        for( int32_t i = 0; i < n; ++i )
        {
            if( _slots.At( i ).state == EState::FREE )
            {
                _slots.At( i ).state = EState::ACTIVE;
                return i;
            }
        }

        Slot fresh  = {};
        fresh.state = EState::ACTIVE;
        _slots.Add( fresh );

        // Init's Reserve() sets CAPACITY and leaves the array empty, so every never-before-seen slot
        // arrives here — the count, not this path, is what says the reserve was actually passed
        if( _slots.Size() > _reserve )
            SDL_LogWarn( SDL_LOG_CATEGORY_APPLICATION, "crTrailPool<%d>: grew past its reserve( reserve:%d, slots:%d )", CAP, _reserve, _slots.Size() );

        return ( _slots.Size() - 1 );
    }

    // set a freshly-allocated slot's parameters. desiredPoints = the emitter's duration in ticks
    // (<= 0 means full capacity); it must fit CAP (asserted), and is clamped in release for safety
    void InitSlot( int32_t idx, const crRibbonTrailParams& params, int32_t desiredPoints, float pointLife )
    {
        Slot& slot = _slots.At( idx );
        slot.params     = params;
        slot.start      = 0;
        slot.count      = 0;
        slot.pointLife  = pointLife;
        slot.widthScale = 1.0f;
        slot.fadeAlpha  = 1.0f;
        slot.state      = EState::ACTIVE;

        if( desiredPoints <= 0 )
        {
            slot.maxPoints = CAP;
        }
        else
        {
            SDL_assert( desiredPoints <= CAP );   // caller assigned a size-class too small for the requested duration
            slot.maxPoints = desiredPoints;
            if( slot.maxPoints < 2 )
                slot.maxPoints = 2;
            if( slot.maxPoints > CAP )
                slot.maxPoints = CAP;
        }
    }

    // mark the slot alive this step + record the position (distance-gated, capped at maxPoints)
    void Touch( int32_t idx, float3 pos, float now )
    {
        Slot& slot = _slots.At( idx );
        slot.seen  = true;
        slot.state = EState::ACTIVE;

        if( slot.count > 0 )
        {
            const float3 head = slot.points[ ( ( slot.start + ( slot.count - 1 ) ) % slot.maxPoints ) ];
            const float3 d    = float3( pos.x - head.x, pos.y - head.y, pos.z - head.z );
            if( crMath::Dot3( d, d ) <= ( slot.params.emitDistance * slot.params.emitDistance ) )
                return;   // hasn't moved far enough — gate
        }

        const int32_t at = ( ( slot.start + slot.count ) % slot.maxPoints );
        slot.points[ at ] = pos;
        slot.times[ at ]  = now;
        if( slot.count < slot.maxPoints )
            ++slot.count;
        else
            slot.start = ( ( slot.start + 1 ) % slot.maxPoints );
    }

    // drop tail points older than pointLife — a stalled (or dead) emitter's trail dissolves instead
    // of freezing in place
    void ExpireOld( float now )
    {
        const int32_t n = _slots.Size();
        for( int32_t i = 0; i < n; ++i )
        {
            Slot& slot = _slots.At( i );
            if( slot.state == EState::FREE )
                continue;

            while( ( slot.count > 0 ) && ( ( now - slot.times[ slot.start ] ) > slot.pointLife ) )
            {
                slot.start = ( ( slot.start + 1 ) % slot.maxPoints );
                --slot.count;
            }
        }
    }

    void Fade( float dt )
    {
        const int32_t n = _slots.Size();
        for( int32_t i = 0; i < n; ++i )
        {
            Slot& slot = _slots.At( i );

            if( ( slot.state == EState::ACTIVE ) && ( slot.seen == false ) )
            {
                // detached (emitter destroyed / component removed)
                slot.state = EState::FADING;
            }

            if( slot.state == EState::FADING )
            {
                // a detached trail thins (width -> 0) AND dims (alpha -> 0) over fadeDuration, then frees
                const float decay = ( slot.params.fadeDuration > 0.0f ) ? ( dt / slot.params.fadeDuration ) : 1.0f;
                slot.widthScale -= decay;
                slot.fadeAlpha  -= decay;
                if( slot.widthScale <= 0.0f )
                {
                    slot.state = EState::FREE;
                    slot.count = 0;
                }
            }
        }
    }

    // append this pool's live slots to the shared build inputs. offsets are computed by the system
    // afterwards — the merged list is first sorted by texture to form contiguous draw runs
    void Gather( crArray<crBatchedRibbonTrails::TrailInput>& inputs )
    {
        const int32_t n = _slots.Size();
        for( int32_t i = 0; i < n; ++i )
        {
            const Slot& slot = _slots.At( i );
            if( slot.state == EState::FREE )
                continue;

            const crRibbonTrailParams& p     = slot.params;
            const float                atten = ( p.intensity * slot.fadeAlpha );   // HDR brightness x detach dim; widthScale thins in parallel

            crBatchedRibbonTrails::TrailInput in;
            in.points        = slot.points;
            in.start         = slot.start;
            in.ringLength    = slot.maxPoints;
            in.count         = slot.count;
            in.headColor     = color4( ( p.color.r * atten ), ( p.color.g * atten ), ( p.color.b * atten ), p.color.a );
            in.tailColor     = color4( ( p.tailColor.r * atten ), ( p.tailColor.g * atten ), ( p.tailColor.b * atten ), p.tailColor.a );
            in.headHalfWidth = ( p.headHalfWidth * slot.widthScale );
            in.tailHalfWidth = ( p.tailHalfWidth * slot.widthScale );
            in.tileLength    = p.tileLength;
            in.textureId     = p.textureId;

            inputs.Add( in );
        }
    }

    void Reclaim()   // scene transition: free all trails, return grown capacity to the reserve
    {
        _slots.Clear();
        if( _slots.Capacity() > _reserve )
        {
            _slots.Shrink();   // size is 0 — frees outright, then one alloc back to the reserve
            _slots.Reserve( _reserve );
        }
    }

private:
    crArray<Slot> _slots;
    int32_t       _reserve = 0;
};

// owns the ribbon trail pools (one per size-class) + the GPU batcher. Emit() (FixedUpdate) records
// emitter positions and fades detached trails; Render() (scene pass) builds in parallel and draws.
// the pool is visual history — deterministically recorded in the sim step, but not part of the hash
class crRibbonTrailSystem
{
private:
    static constexpr uint32_t BUILD_GRAIN = 64;   // trails per enki partition

    // expected concurrent trails per class — pools grow (with a warning) past these
    static constexpr int32_t RESERVE_POOL_60  = 512;
    static constexpr int32_t RESERVE_POOL_300 = 64;
    static constexpr int32_t RESERVE_POOL_600 = 16;

    crBatchedRibbonTrails _batch;

    crTrailPool<60>  _pool60;    // class 0 — ~1 s at 60 Hz
    crTrailPool<300> _pool300;   // class 1 — ~5 s
    crTrailPool<600> _pool600;   // class 2 — ~10 s

    float _time = 0.0f;   // sim-clock accumulator for point stamps (advances with Emit, reset by Reclaim)

    // build scratch — every pool's live slots gathered, sorted by texture, prefix-summed each frame
    crArray<crBatchedRibbonTrails::TrailInput> _inputs;
    crArray<int32_t>                           _vertBase;
    crArray<int32_t>                           _indexBase;
    crArray<crBatchedRibbonTrails::Run>        _runs;

public:
    void Init();
    void Cleanup();
    void Reclaim();

    void Emit( const crApp* app );   // sim step (FixedUpdate): record emitter positions + fade detached trails
    void Render( float3 eye, float fogStart, float fogEnd, GLuint program, GLuint defaultTexture, GLuint noiseTexture );   // scene pass: parallel build + one draw per texture run
};
