#include "crBatchedTexts.h"

#include <SDL3/SDL_stdinc.h>

#include "crDevText.h"
#include "crFontAtlas.h"
#include "crGraphicsStats.h"
#include "crMath.h"

void crBatchedTexts::Init()
{
    glGenVertexArrays( 1, &_vao );
    glGenBuffers( 1, &_quadVbo );
    glGenBuffers( 1, &_instanceVbo );

    glBindVertexArray( _vao );

    // shared unit quad (TRIANGLE_STRIP): pos.xy + texcoord.uv, centered, Y-up; v=0 = image top
    const float quad[] =
    {
        -0.5f, -0.5f,   0.0f, 1.0f,
         0.5f, -0.5f,   1.0f, 1.0f,
        -0.5f,  0.5f,   0.0f, 0.0f,
         0.5f,  0.5f,   1.0f, 0.0f,
    };
    glBindBuffer( GL_ARRAY_BUFFER, _quadVbo );
    glBufferData( GL_ARRAY_BUFFER, sizeof( quad ), quad, GL_STATIC_DRAW );

    glEnableVertexAttribArray( 0 );
    glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof( float ), reinterpret_cast<void*>( 0 ) );
    glEnableVertexAttribArray( 1 );
    glVertexAttribPointer( 1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof( float ), reinterpret_cast<void*>( 2 * sizeof( float ) ) );

    // per-instance stream — locations 2..6, divisor 1
    glBindBuffer( GL_ARRAY_BUFFER, _instanceVbo );

    const GLsizei stride = static_cast<GLsizei>( sizeof( GlyphInstance ) );
    glEnableVertexAttribArray( 2 );
    glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, stride, reinterpret_cast<void*>( 0 ) );
    glEnableVertexAttribArray( 3 );
    glVertexAttribPointer( 3, 4, GL_FLOAT, GL_FALSE, stride, reinterpret_cast<void*>( 8 ) );    // size + cosSin
    glEnableVertexAttribArray( 4 );
    glVertexAttribPointer( 4, 4, GL_FLOAT, GL_FALSE, stride, reinterpret_cast<void*>( 24 ) );
    glEnableVertexAttribArray( 5 );
    glVertexAttribPointer( 5, 4, GL_FLOAT, GL_FALSE, stride, reinterpret_cast<void*>( 40 ) );   // uvMin + uvMax
    glEnableVertexAttribArray( 6 );
    glVertexAttribPointer( 6, 2, GL_FLOAT, GL_FALSE, stride, reinterpret_cast<void*>( 56 ) );

    glVertexAttribDivisor( 2, 1 );
    glVertexAttribDivisor( 3, 1 );
    glVertexAttribDivisor( 4, 1 );
    glVertexAttribDivisor( 5, 1 );
    glVertexAttribDivisor( 6, 1 );

    glBindVertexArray( 0 );
    glBindBuffer( GL_ARRAY_BUFFER, 0 );

    _instances.Reserve( INITIAL_RESERVE );
    _pages.Reserve( INITIAL_RESERVE );
}

void crBatchedTexts::Cleanup()
{
    glDeleteBuffers( 1, &_instanceVbo );
    glDeleteBuffers( 1, &_quadVbo );
    glDeleteVertexArrays( 1, &_vao );
    _instanceVbo = 0;
    _quadVbo     = 0;
    _vao         = 0;
}

void crBatchedTexts::Begin()
{
    _instances.Clear();
    _pages.Clear();
}

void crBatchedTexts::Add( const GlyphInstance& instance, uint8_t page )
{
    _instances.Add( instance );
    _pages.Add( page );
}

void crBatchedTexts::AddString( crFontAtlas* atlas, const char* utf8, float2 anchorPx, int32_t font, color4 color, float scale, float2 pivot, crDevText* devText )
{
    const crFontMetrics metrics    = atlas->Metrics( font );
    const float         lineHeight = ( static_cast<float>( metrics.lineHeight ) * scale );
    const float         ascent     = ( static_cast<float>( metrics.ascent ) * scale );

    const float anchorX = crMath::Round( anchorPx.x );
    const float anchorY = crMath::Round( anchorPx.y );

    // measure pass: block extent (scaled px) so pivot can offset the whole block off the anchor
    float   blockW    = 0.0f;
    int32_t lineCount = 1;
    {
        float       lineW = 0.0f;
        const char* c     = utf8;
        size_t      rem   = SDL_strlen( utf8 );
        while( rem > 0 )
        {
            const uint32_t cp = SDL_StepUTF8( &c, &rem );
            if( cp == 0 )
                break;

            if( cp == static_cast<uint32_t>( '\n' ) )
            {
                if( lineW > blockW )
                    blockW = lineW;
                lineW = 0.0f;
                ++lineCount;
                continue;
            }

            lineW += ( atlas->Glyph( cp, font ).advance * scale );
        }
        if( lineW > blockW )
            blockW = lineW;
    }
    const float blockH = ( static_cast<float>( lineCount ) * lineHeight );

    const float originX = crMath::Round( anchorX - ( pivot.x * blockW ) );
    const float originY = crMath::Round( anchorY + ( pivot.y * blockH ) );

    float penX = originX;
    float penY = ( originY - ascent );   // baseline of line 0; the cell top sits at originY

    if( devText != nullptr )
        devText->BeginGroup( float2( anchorX, anchorY ) );

    const char* cursor    = utf8;
    size_t      remaining = SDL_strlen( utf8 );

    while( remaining > 0 )
    {
        const uint32_t cp = SDL_StepUTF8( &cursor, &remaining );
        if( cp == 0 )
            break;

        if( cp == static_cast<uint32_t>( '\n' ) )
        {
            penX  = originX;
            penY -= lineHeight;   // Y-up: next line is down
            continue;
        }

        const crFontGlyph g = atlas->Glyph( cp, font );

        if( ( g.sizePx.x > 0.0f ) && ( g.sizePx.y > 0.0f ) )   // has a bitmap
        {
            GlyphInstance inst;
            // whole cell placed with its left edge at penX; baseline sits 'ascent' below the cell top (bearing baked in)
            inst.pos    = float2( penX + ( g.sizePx.x * 0.5f * scale ),
                                  penY + ( ascent - ( g.sizePx.y * 0.5f * scale ) ) );
            inst.size   = float2( g.sizePx.x * scale, g.sizePx.y * scale );
            inst.cosSin = float2( 1.0f, 0.0f );
            inst.color  = color;
            inst.uvMin  = g.uvMin;
            inst.uvMax  = g.uvMax;
            inst.pivot  = float2( 0.5f, 0.5f );

            Add( inst, g.page );
            if( devText != nullptr )
                devText->AddGlyphRect( inst.pos, inst.size );
        }

        penX += ( g.advance * scale );
    }

    if( devText != nullptr )
        devText->EndGroup();
}

void crBatchedTexts::Flush( GLuint program, float4x4 proj, const crFontAtlas* atlas )
{
    const int32_t count = _instances.Size();
    if( count <= 0 )
        return;

    glUseProgram( program );
    glUniformMatrix4fv( glGetUniformLocation( program, "u_projection" ), 1, GL_FALSE, proj.m );
    glUniform1i( glGetUniformLocation( program, "u_tex" ), 0 );

    glActiveTexture( GL_TEXTURE0 );

    glDisable( GL_DEPTH_TEST );
    glDisable( GL_CULL_FACE );   // declare, don't inherit — glyph quads must draw regardless of winding
    glEnable( GL_BLEND );
    glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );

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

    int32_t runStart = 0;
    while( runStart < count )   // group consecutive same-page glyphs into a single instanced draw
    {
        const uint8_t page = _pages.At( runStart );

        int32_t runEnd = ( runStart + 1 );
        while( ( runEnd < count ) && ( _pages.At( runEnd ) == page ) )
            ++runEnd;

        const int32_t runCount = ( runEnd - runStart );

        glBindTexture( GL_TEXTURE_2D, atlas->PageTexture( page ) );
        glBufferData( GL_ARRAY_BUFFER,
                      static_cast<GLsizeiptr>( runCount ) * static_cast<GLsizeiptr>( sizeof( GlyphInstance ) ),
                      &_instances.At( runStart ),
                      GL_STREAM_DRAW );
        glDrawArraysInstanced( GL_TRIANGLE_STRIP, 0, 4, static_cast<GLsizei>( runCount ) );

        ++crGraphicsStats::drawCalls;
        crGraphicsStats::vertices += ( runCount * 4 );

        runStart = runEnd;
    }

    glBindVertexArray( 0 );
    glBindTexture( GL_TEXTURE_2D, 0 );

    _instances.Clear();
    _pages.Clear();
}
