#include "crBatchedRibbonTrails.h"

#include "crGraphicsStats.h"
#include "crMath.h"

void crBatchedRibbonTrails::Init()
{
    glGenVertexArrays( 1, &_vao );
    glGenBuffers( 1, &_vbo );
    glGenBuffers( 1, &_ibo );

    glBindVertexArray( _vao );
    glBindBuffer( GL_ARRAY_BUFFER, _vbo );

    const GLsizei stride = static_cast<GLsizei>( sizeof( RibbonVertex ) );
    glEnableVertexAttribArray( 0 );
    glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, stride, reinterpret_cast<void*>( 0 ) );
    glEnableVertexAttribArray( 1 );
    glVertexAttribPointer( 1, 4, GL_FLOAT, GL_FALSE, stride, reinterpret_cast<void*>( 12 ) );
    glEnableVertexAttribArray( 2 );
    glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, stride, reinterpret_cast<void*>( 28 ) );

    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, _ibo );   // the element binding is VAO state

    glBindVertexArray( 0 );
    glBindBuffer( GL_ARRAY_BUFFER, 0 );
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );   // after the VAO unbind — keeps the binding recorded in the VAO

    _verts.Reserve( INITIAL_RESERVE );
    _indices.Reserve( INITIAL_RESERVE );
}
void crBatchedRibbonTrails::Cleanup()
{
    glDeleteBuffers( 1, &_ibo );
    glDeleteBuffers( 1, &_vbo );
    glDeleteVertexArrays( 1, &_vao );
    _ibo = 0;
    _vbo = 0;
    _vao = 0;
}

void crBatchedRibbonTrails::Prepare( int32_t totalVerts, int32_t totalIndices )
{
    _verts.Resize( totalVerts );
    _indices.Resize( totalIndices );
}
void crBatchedRibbonTrails::BuildRange( const TrailInput* trails, const int32_t* vertBase, const int32_t* indexBase, int32_t first, int32_t lastExclusive, float3 eye, float fogStart, float fogEnd )
{
    RibbonVertex* verts    = _verts.Data();
    uint32_t*     indices  = _indices.Data();
    const float   fogDenom = ( ( fogEnd - fogStart ) > 0.0001f ) ? ( fogEnd - fogStart ) : 0.0001f;

    // each trail writes only its own [vertBase, +count*2) and [indexBase, +count*2+1) regions —
    // disjoint by construction (prefix sums), so ranges run in parallel without contention
    for( int32_t ti = first; ti < lastExclusive; ++ti )
    {
        const int32_t count = trails[ ti ].count;
        if( count < 2 )
            continue;

        const float3* points        = trails[ ti ].points;
        const int32_t start         = trails[ ti ].start;
        const int32_t ringLen       = trails[ ti ].ringLength;
        const color4  headColor     = trails[ ti ].headColor;
        const color4  tailColor     = trails[ ti ].tailColor;
        const float   headHalfWidth = trails[ ti ].headHalfWidth;
        const float   tailHalfWidth = trails[ ti ].tailHalfWidth;
        const float   tileLength    = trails[ ti ].tileLength;
        const int32_t vbase         = vertBase[ ti ];
        const int32_t ibase         = indexBase[ ti ];

        // tiled u is anchored at the head (u = 0) and runs negative tail-ward, so the pattern stays
        // pinned to the head and flows out the tail as old points drop. anchoring needs the total
        // length up front — one extra pass over <= CAP in-cache points
        float totalLen = 0.0f;
        if( tileLength > 0.0f )
        {
            float3 prev = points[ start ];
            for( int32_t i = 1; i < count; ++i )
            {
                const float3 pt = points[ ( ( start + i ) % ringLen ) ];
                const float3 d  = float3( pt.x - prev.x, pt.y - prev.y, pt.z - prev.z );
                totalLen += crMath::Sqrt( crMath::Dot3( d, d ) );
                prev = pt;
            }
        }

        // logical order runs tail(0) -> head(count-1). each point is pushed sideways in the view plane,
        // emitting L,R vertices consumed as GL_TRIANGLE_STRIP. width and color both lerp tail -> head
        float  cumLen = 0.0f;
        float3 ptPrev = points[ start ];
        for( int32_t i = 0; i < count; ++i )
        {
            const int32_t iNext  = ( i < ( count - 1 ) ) ? ( i + 1 ) : i;
            const float3  pt     = points[ ( ( start + i ) % ringLen ) ];
            const float3  ptNext = points[ ( ( start + iNext ) % ringLen ) ];

            const float3 segDir  = crMath::Normalize3( float3( ptNext.x - ptPrev.x,
                                                               ptNext.y - ptPrev.y,
                                                               ptNext.z - ptPrev.z ) );
            const float3 toEye   = float3( eye.x - pt.x, eye.y - pt.y, eye.z - pt.z );
            const float3 viewDir = crMath::Normalize3( toEye );
            const float3 side    = crMath::Normalize3( crMath::Cross3( segDir, viewDir ) );

            const float  t = ( static_cast<float>( i ) / static_cast<float>( count - 1 ) );   // 0 tail -> 1 head
            const float  w = crMath::Lerp( tailHalfWidth, headHalfWidth, t );

            float u = t;   // <= 0 tileLength: one texture repeat stretched over the trail
            if( tileLength > 0.0f )
            {
                if( i > 0 )
                {
                    const float3 seg = float3( pt.x - ptPrev.x, pt.y - ptPrev.y, pt.z - ptPrev.z );
                    cumLen += crMath::Sqrt( crMath::Dot3( seg, seg ) );
                }
                u = ( ( cumLen - totalLen ) / tileLength );   // head-anchored: 0 at the head, negative tail-ward (GL_REPEAT wraps negatives)
            }

            // additive fog dims the added light by view distance (a separate, scene-wide effect); RGB only — alpha isn't light
            const float  fogF  = crMath::Clamp01( ( crMath::Sqrt( crMath::Dot3( toEye, toEye ) ) - fogStart ) / fogDenom );
            const float  atten = ( 1.0f - fogF );
            const color4 lc    = crMath::LerpColor( tailColor, headColor, t );
            const color4 c     = color4( ( lc.r * atten ), ( lc.g * atten ), ( lc.b * atten ), lc.a );

            RibbonVertex vl;
            vl.pos   = float3( pt.x + ( side.x * w ), pt.y + ( side.y * w ), pt.z + ( side.z * w ) );
            vl.color = c;
            vl.uv    = float2( u, 0.0f );

            RibbonVertex vr;
            vr.pos   = float3( pt.x - ( side.x * w ), pt.y - ( side.y * w ), pt.z - ( side.z * w ) );
            vr.color = c;
            vr.uv    = float2( u, 1.0f );

            verts[ vbase + ( i * 2 ) + 0 ] = vl;
            verts[ vbase + ( i * 2 ) + 1 ] = vr;

            ptPrev = pt;
        }

        // this trail's strip indices, then a restart so the strip does not connect to the next trail
        const int32_t vc = ( count * 2 );
        for( int32_t j = 0; j < vc; ++j )
            indices[ ibase + j ] = static_cast<uint32_t>( vbase + j );
        indices[ ibase + vc ] = RESTART_INDEX;
    }
}
void crBatchedRibbonTrails::Flush( GLuint program, GLuint noiseTexture, const Run* runs, int32_t runCount )
{
    const int32_t vertCount  = _verts.Size();
    const int32_t indexCount = _indices.Size();
    if( ( indexCount <= 0 ) || ( runCount <= 0 ) )
        return;

    glUseProgram( program );
    glUniform1i( glGetUniformLocation( program, "u_tex" ), 0 );
    glUniform1i( glGetUniformLocation( program, "u_noise" ), 1 );
    glActiveTexture( GL_TEXTURE1 );
    glBindTexture( GL_TEXTURE_2D, noiseTexture );
    glActiveTexture( GL_TEXTURE0 );

    glEnable( GL_DEPTH_TEST );
    glDepthMask( GL_FALSE );        // read depth (occluded by opaque) but never write
    glEnable( GL_BLEND );
    glBlendFunc( GL_ONE, GL_ONE );  // additive — order-independent, HDR accumulates into bloom
    glDisable( GL_CULL_FACE );      // a camera-facing strip has no consistent winding
#ifndef __EMSCRIPTEN__
    glEnable( GL_PRIMITIVE_RESTART_FIXED_INDEX );   // native/desktop GL needs the toggle; WebGL2 has restart permanently on (trying to enable it is error in WebGL2)
#endif

    glBindVertexArray( _vao );

    glBindBuffer( GL_ARRAY_BUFFER, _vbo );
    glBufferData( GL_ARRAY_BUFFER, static_cast<GLsizeiptr>( vertCount ) * static_cast<GLsizeiptr>( sizeof( RibbonVertex ) ), _verts.Data(), GL_STREAM_DRAW );

    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, _ibo );
    glBufferData( GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr>( indexCount ) * static_cast<GLsizeiptr>( sizeof( uint32_t ) ), _indices.Data(), GL_STREAM_DRAW );

    // one draw per texture run — the buffers are shared, only the base texture rebinds between runs
    for( int32_t r = 0; r < runCount; ++r )
    {
        if( runs[ r ].indexCount <= 0 )
            continue;

        glBindTexture( GL_TEXTURE_2D, runs[ r ].texture );
        glDrawElements( GL_TRIANGLE_STRIP, static_cast<GLsizei>( runs[ r ].indexCount ), GL_UNSIGNED_INT,
                        reinterpret_cast<void*>( static_cast<intptr_t>( runs[ r ].indexOffset ) * static_cast<intptr_t>( sizeof( uint32_t ) ) ) );
        ++crGraphicsStats::drawCalls;
    }

    glBindVertexArray( 0 );
    glBindTexture( GL_TEXTURE_2D, 0 );
    glActiveTexture( GL_TEXTURE1 );
    glBindTexture( GL_TEXTURE_2D, 0 );
    glActiveTexture( GL_TEXTURE0 );   // leave unit 0 active — the engine-wide assumption

#ifndef __EMSCRIPTEN__
    glDisable( GL_PRIMITIVE_RESTART_FIXED_INDEX );
#endif
    glDepthMask( GL_TRUE );   // restore — next frame's depth clears need writes on

    crGraphicsStats::vertices += vertCount;
}
