#include "crFontAtlas.h"

#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>

#include "crMath.h"
#include "crStruct.h"

bool crFontAtlas::Init()
{
    if( TTF_Init() == false )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_FONT, "crFontAtlas: TTF_Init failed: %s", SDL_GetError() );
        return false;
    }

    _glyphs.reserve( 1024 );   // ASCII x fonts preload + headroom; entt::dense_map grows x2

    return true;
}
void crFontAtlas::Cleanup()
{
    ReleaseFontsAndPages();
    _fonts.Clear();

    TTF_Quit();
}

int32_t crFontAtlas::LoadFont( const char* baseName, int32_t px )
{
    const int32_t n = _fonts.Size();
    for( int32_t i = 0; i < n; ++i )
    {
        if( ( _fonts.At( i ).px == px ) && ( SDL_strcmp( _fonts.At( i ).name, baseName ) == 0 ) )
            return i;
    }

    FontEntry entry = {};
    if( SDL_strlcpy( entry.name, baseName, sizeof( entry.name ) ) >= sizeof( entry.name ) )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_FONT, "crFontAtlas: font name too long( %s ) — max %d chars", baseName, static_cast<int32_t>( sizeof( entry.name ) - 1 ) );
        SDL_assert( false );
    }
    entry.px = px;
    _fonts.Add( entry );

    const int32_t font = ( _fonts.Size() - 1 );
    OpenFont( font );
    PreloadGlyphs( font );

    return font;
}

void crFontAtlas::Rebuild( float resScale )
{
    if( crMath::Abs( resScale - _resScale ) < 0.001f )
        return;

    _resScale = resScale;

    ReleaseFontsAndPages();

    const int32_t n = _fonts.Size();
    for( int32_t i = 0; i < n; ++i )
        OpenFont( i );
    for( int32_t i = 0; i < n; ++i )
        PreloadGlyphs( i );

    SDL_LogTrace( CR_LOG_CATEGORY_ASSET_FONT, "crFontAtlas: rebuilt( resScale:%.2f, fonts:%d, pages:%d )", _resScale, n, _pages.Size() );
}

crFontGlyph crFontAtlas::Glyph( uint32_t codepoint, int32_t font )
{
    const uint64_t key = GlyphKey( codepoint, font );

    const auto it = _glyphs.find( key );
    if( it != _glyphs.end() )
        return it->second;

    const crFontGlyph g = RasterizeGlyph( codepoint, font );
    _glyphs[ key ] = g;
    return g;
}

void crFontAtlas::OpenFont( int32_t font )
{
    FontEntry& entry = _fonts.At( font );

    char path[ 256 ];
    SDL_snprintf( path, sizeof( path ), "crassets/fonts/%s.ttf", entry.name );

    const int32_t px = static_cast<int32_t>( crMath::Round( static_cast<float>( entry.px ) * _resScale ) );

    // closeio=true: SDL_ttf closes the stream on TTF_CloseFont and on its own failure paths, so it
    // is never ours to close
    entry.font = TTF_OpenFontIO( SDL_IOFromFile( path, "rb" ), true, static_cast<float>( px ) );
    if( entry.font == nullptr )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_FONT, "crFontAtlas: open failed( %s, %dpx ): %s", path, px, SDL_GetError() );
        return;
    }

    entry.metrics.lineHeight = TTF_GetFontHeight( entry.font );
    entry.metrics.ascent     = TTF_GetFontAscent( entry.font );
}
void crFontAtlas::ReleaseFontsAndPages()
{
    int32_t sz = _pages.Size();
    for( int32_t i = 0; i < sz; ++i )
        glDeleteTextures( 1, &_pages.At( i ).texture );
    _pages.Clear();

    _glyphs.clear();

    const int32_t n = _fonts.Size();
    for( int32_t i = 0; i < n; ++i )
    {
        if( _fonts.At( i ).font != nullptr )   // guard: an open may have failed, leaving the slot null
        {
            TTF_CloseFont( _fonts.At( i ).font );
            _fonts.At( i ).font = nullptr;
        }
    }
}

void crFontAtlas::PreloadGlyphs( int32_t font )
{
    if( _fonts.At( font ).font == nullptr )
        return;

    for( uint32_t cp = 0x20; cp <= 0x7e; ++cp )   // printable ASCII
        Glyph( cp, font );
}
crFontGlyph crFontAtlas::RasterizeGlyph( uint32_t codepoint, int32_t fontId )
{
    TTF_Font* font = _fonts.At( fontId ).font;
    if( font == nullptr )
        return crFontGlyph{};

    int advance = 0;
    TTF_GetGlyphMetrics( font, codepoint, nullptr, nullptr, nullptr, nullptr, &advance );

    crFontGlyph g;
    g.advance = static_cast<float>( advance );

    const SDL_Color white = { 255, 255, 255, 255 };
    SDL_Surface*    surf  = TTF_RenderGlyph_Blended( font, codepoint, white );
    if( surf == nullptr )
        return g;   // no bitmap (e.g. space) — advance-only

    const int32_t w = surf->w;
    const int32_t h = surf->h;

    const PackResult pack = PackGlyph( w, h );
    if( pack.ok == false )
    {
        SDL_DestroySurface( surf );
        return g;   // atlas full — draws nothing, advance still applies
    }

    uint8_t* r8 = static_cast<uint8_t*>( SDL_malloc( static_cast<size_t>( w ) * static_cast<size_t>( h ) ) );

    // coverage lives in the alpha channel
    SDL_Surface* rgba = SDL_ConvertSurface( surf, SDL_PIXELFORMAT_RGBA32 );
    if( rgba != nullptr )
    {
        const uint8_t* base = static_cast<const uint8_t*>( rgba->pixels );
        for( int32_t y = 0; y < h; ++y )
        {
            const uint8_t* row = base + ( y * rgba->pitch );
            for( int32_t x = 0; x < w; ++x )
                r8[ ( y * w ) + x ] = row[ ( x * 4 ) + 3 ];
        }
        SDL_DestroySurface( rgba );
    }
    else
    {
        SDL_memset( r8, 0, static_cast<size_t>( w ) * static_cast<size_t>( h ) );
    }

    glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );
    glBindTexture( GL_TEXTURE_2D, _pages.At( pack.page ).texture );
    glTexSubImage2D( GL_TEXTURE_2D, 0, pack.x, pack.y, w, h, GL_RED, GL_UNSIGNED_BYTE, r8 );
    glBindTexture( GL_TEXTURE_2D, 0 );

    SDL_free( r8 );
    SDL_DestroySurface( surf );

    const float inv = 1.0f / static_cast<float>( TEXT_ATLAS_PAGE_SIZE );
    g.page   = pack.page;
    g.sizePx = float2( static_cast<float>( w ), static_cast<float>( h ) );
    g.uvMin  = float2( static_cast<float>( pack.x ) * inv, static_cast<float>( pack.y ) * inv );
    g.uvMax  = float2( static_cast<float>( pack.x + w ) * inv, static_cast<float>( pack.y + h ) * inv );

    return g;
}
crFontAtlas::PackResult crFontAtlas::PackGlyph( int32_t w, int32_t h )
{
    if( ( w > TEXT_ATLAS_PAGE_SIZE ) || ( h > TEXT_ATLAS_PAGE_SIZE ) )    // guard: a single glyph larger than a page would loop forever
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_FONT, "crFontAtlas: glyph too large for a page (%dx%d > %d) — dropped", w, h, TEXT_ATLAS_PAGE_SIZE );
        PackResult r = { false, 0, 0, 0 };
        return r;
    }

    for( ;; )
    {
        if( _pages.Size() == 0 )
            OpenPage();

        Page& pg = _pages.At( _pages.Size() - 1 );

        if( ( pg.shelfX + w ) > TEXT_ATLAS_PAGE_SIZE ) // wrap to a new shelf
        {
            pg.shelfY     += ( pg.shelfHeight + GLYPH_PADDING );
            pg.shelfX      = 0;
            pg.shelfHeight = 0;
        }

        if( ( pg.shelfY + h ) <= TEXT_ATLAS_PAGE_SIZE )
        {
            PackResult r;
            r.ok   = true;
            r.page = static_cast<uint8_t>( _pages.Size() - 1 );
            r.x    = pg.shelfX;
            r.y    = pg.shelfY;

            pg.shelfX += ( w + GLYPH_PADDING );
            if( h > pg.shelfHeight )
                pg.shelfHeight = h;

            return r;
        }

        OpenPage(); // page full — grow (the font's glyph set is finite)
    }
}
void crFontAtlas::OpenPage()
{
    GLuint texture = 0;
    glGenTextures( 1, &texture );
    glBindTexture( GL_TEXTURE_2D, texture );

    void* zeros = SDL_calloc( 1, static_cast<size_t>( TEXT_ATLAS_PAGE_SIZE ) * static_cast<size_t>( TEXT_ATLAS_PAGE_SIZE ) );
    glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );
    glTexImage2D( GL_TEXTURE_2D, 0, GL_R8, TEXT_ATLAS_PAGE_SIZE, TEXT_ATLAS_PAGE_SIZE, 0, GL_RED, GL_UNSIGNED_BYTE, zeros );
    SDL_free( zeros );

    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );   // fixed-size 1:1 text; native-px raster, no mips
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
    glBindTexture( GL_TEXTURE_2D, 0 );

    Page pg = { texture, 0, 0, 0 };
    _pages.Add( pg );

    if( _pages.Size() > 1 )
        SDL_LogWarn( CR_LOG_CATEGORY_ASSET_FONT, "crFontAtlas: opened extra glyph page (%d total)", _pages.Size() );
}

/*static*/ uint64_t crFontAtlas::GlyphKey( uint32_t codepoint, int32_t font )
{
    return ( static_cast<uint64_t>( codepoint ) << 32 )
         | static_cast<uint64_t>( static_cast<uint32_t>( font ) );
}
