#include "crGraphics.h"

#include <SDL3/SDL.h>

#include "crSpriteAtlas.h"
#include "crFontAtlas.h"
#include "crGraphicsUniforms.h"
#include "crPostProcess.h"
#include "crShadowMap.h"
#include "crText.h"
#include "crTextureLoader.h"

#include "crBatchedSprites.h"
#include "crBatchedTexts.h"

bool crGraphics::Init( int2 initialSize )
{
    static_assert( SDL_arraysize( SHADER_DEFS ) == static_cast<size_t>( EShader::_SIZE ),
                   "SHADER_DEFS out of sync with EShader" );

    // HDR is a hard requirement (no LDR fallback path) — RGBA16F render targets are not core
    // until ES 3.2, so ES 3.0 / WebGL2 needs one of these extensions
    if( ( HasGLExtension( "EXT_color_buffer_half_float" ) == false ) &&
        ( HasGLExtension( "EXT_color_buffer_float" ) == false ) )
    {
        SDL_LogError( SDL_LOG_CATEGORY_RENDER, "crGraphics: HDR render targets unsupported — EXT_color_buffer_(half_)float missing" );
        return false;
    }

    int32_t len = static_cast<int32_t>( EShader::_SIZE );

    int32_t countCompiled = 0;
    for( int32_t i = 0; i < len; ++i )
    {
        GLuint program = CompileProgram( SHADER_DEFS[ i ].vert, SHADER_DEFS[ i ].frag );

        _programs[ i ] = program;

        if( program != 0 )
        {
            ++countCompiled;
        }
    }

    SDL_LogTrace( SDL_LOG_CATEGORY_GPU, "crGraphics: compiled shader( %d/%d )", countCompiled, len );

    if( countCompiled != len )
    {
        SDL_LogError( SDL_LOG_CATEGORY_RENDER, "crGraphics: shader compilation failed( %d/%d )", countCompiled, len );
        return false;
    }

    _uniforms = new crGraphicsUniforms;
    _uniforms->Init();
    for( int32_t i = 0; i < len; ++i )
        _uniforms->BindTo( _programs[ i ] );

    _shadowMap = new crShadowMap;
    bool rv = _shadowMap->Init();
    if( rv == false )
        return false;

    if( LoadSpriteAtlas( "builtin_atlas_0" ) != BUILTIN_ATLAS )
        return false;

    _fontAtlas = new crFontAtlas;
    rv = _fontAtlas->Init();
    if( rv == false )
        return false;

    _postProcess = new crPostProcess;
    rv = _postProcess->Init( this, initialSize );
    if( rv == false )
        return false;

    _sprites = new crBatchedSprites;
    _sprites->Init( "world" );

    _texts = new crBatchedTexts;
    _texts->Init();

    return true;
}
void crGraphics::Cleanup()
{
    if( _sprites != nullptr )
    {
        _sprites->Cleanup();
        delete _sprites;
        _sprites = nullptr;
    }

    if( _texts != nullptr )
    {
        _texts->Cleanup();
        delete _texts;
        _texts = nullptr;
    }

    const int32_t szAtlases = _spriteAtlases.Size();
    for( int32_t i = 0; i < szAtlases; ++i )
    {
        _spriteAtlases.At( i )->Cleanup();
        delete _spriteAtlases.At( i );
    }
    _spriteAtlases.Clear();

    const int32_t szTextures = _textures.Size();
    for( int32_t i = 0; i < szTextures; ++i )
        glDeleteTextures( 1, &_textures.At( i ).texture );
    _textures.Clear();

    if( _fontAtlas != nullptr )
    {
        _fontAtlas->Cleanup();
        delete _fontAtlas;
        _fontAtlas = nullptr;
    }

    if( _uniforms != nullptr )
    {
        _uniforms->Cleanup();
        delete _uniforms;
        _uniforms = nullptr;
    }

    if( _shadowMap != nullptr )
    {
        _shadowMap->Cleanup();
        delete _shadowMap;
        _shadowMap = nullptr;
    }

    if( _postProcess != nullptr )
    {
        _postProcess->Cleanup();
        delete _postProcess;
        _postProcess = nullptr;
    }

    int32_t szShaders = static_cast<int32_t>( EShader::_SIZE );
    for( int32_t i = 0; i < szShaders; ++i )
    {
        glDeleteProgram( _programs[ i ] );
        _programs[ i ] = 0;
    }
}

void crGraphics::BeginBatches()
{
    _sprites->Begin();
    _texts->Begin();
}
void crGraphics::FlushSprites( float4x4 proj )
{
    _sprites->Flush( Program( EShader::SPRITE ), proj, this );
}
void crGraphics::FlushTexts( float4x4 screenProj )
{
    _texts->Flush( Program( EShader::TEXT ), screenProj, _fontAtlas );
}

GLuint crGraphics::Texture( const char* baseName )
{
    const int32_t n = _textures.Size();
    for( int32_t i = 0; i < n; ++i )
    {
        if( SDL_strcmp( _textures.At( i ).name, baseName ) == 0 )
            return _textures.At( i ).texture;
    }

    NamedTexture entry = {};
    if( SDL_strlcpy( entry.name, baseName, sizeof( entry.name ) ) >= sizeof( entry.name ) )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_TEXTURE, "crGraphics: texture name too long( %s ) — max %d chars", baseName, static_cast<int32_t>( sizeof( entry.name ) - 1 ) );
        SDL_assert( false );
    }

    char path[ 256 ];
    SDL_snprintf( path, sizeof( path ), "crassets/texture/%s.ktx2", baseName );

    entry.texture = crTextureLoader::Load( path ).handle;
    _textures.Add( entry );   // 0 stays cached — do not retry (and re-log) every frame

    if( entry.texture != 0 )
        SDL_LogTrace( CR_LOG_CATEGORY_ASSET_TEXTURE, "crGraphics: texture loaded( %s )", baseName );

    return entry.texture;
}

int32_t crGraphics::LoadSpriteAtlas( const char* baseName )
{
    SDL_assert( _spriteAtlases.Size() < 256 );   // atlas id must fit the packed sprite handle's 8 bits

    crSpriteAtlas* atlas = new crSpriteAtlas;
    if( atlas->Init( baseName ) == false )
    {
        atlas->Cleanup();
        delete atlas;
        return -1;
    }

    _spriteAtlases.Add( atlas );
    return ( _spriteAtlases.Size() - 1 );
}

int32_t crGraphics::FindSprite( int32_t atlasId, const char* name ) const
{
    const int32_t index = SpriteAtlas( atlasId )->Find( name );
    if( index < 0 )
        return -1;

    return ( ( atlasId << 24 ) | index );
}

const crAtlasSprite& crGraphics::Sprite( int32_t handle ) const
{
    return SpriteAtlas( SpriteHandleAtlas( handle ) )->Sprite( SpriteHandleIndex( handle ) );
}

crSpriteAtlas* crGraphics::SpriteAtlas( int32_t id ) const
{
    SDL_assert( ( id >= 0 ) && ( id < _spriteAtlases.Size() ) );

    if( id < 0 )
        id = 0;
    else if( id >= _spriteAtlases.Size() )
        id = ( _spriteAtlases.Size() - 1 );

    return _spriteAtlases.At( id );
}
crFontAtlas* crGraphics::FontAtlas() const
{
    return _fontAtlas;
}
crGraphicsUniforms* crGraphics::Uniforms() const
{
    return _uniforms;
}
crPostProcess* crGraphics::PostProcess() const
{
    return _postProcess;
}
crShadowMap* crGraphics::ShadowMap() const
{
    return _shadowMap;
}

crBatchedSprites* crGraphics::Sprites() const
{
    return _sprites;
}
crBatchedTexts* crGraphics::Texts() const
{
    return _texts;
}

GLuint crGraphics::Program( EShader shader ) const
{
    return _programs[ static_cast<int32_t>( shader ) ];
}

/*static*/ bool crGraphics::HasGLExtension( const char* name )
{
    GLint count = 0;
    glGetIntegerv( GL_NUM_EXTENSIONS, &count );

    for( GLint i = 0; i < count; ++i )
    {
        const char* ext = reinterpret_cast<const char*>( glGetStringi( GL_EXTENSIONS, static_cast<GLuint>( i ) ) );
        if( ( ext != nullptr ) && ( SDL_strstr( ext, name ) != nullptr ) )
            return true;
    }

    return false;
}
/*static*/ char* crGraphics::PreprocessShaderSource( const char* source, const char* name )
{
    static constexpr int32_t MAX_INCLUDES = 8;   // cycle guard — a file including itself would loop forever

    char* current = SDL_strdup( source );

    for( int32_t pass = 0; pass < MAX_INCLUDES; ++pass )
    {
        // a directive only counts at the start of a line (whitespace allowed) —
        // the literal text inside a comment must not trigger an expansion
        const char* directive = nullptr;
        const char* search    = current;
        while( directive == nullptr )
        {
            const char* hit = SDL_strstr( search, "#include \"" );
            if( hit == nullptr )
                break;

            bool lineStart = true;
            for( const char* p = hit; ( p > current ) && ( p[ -1 ] != '\n' ); --p )
            {
                if( ( p[ -1 ] != ' ' ) && ( p[ -1 ] != '\t' ) )
                {
                    lineStart = false;
                    break;
                }
            }

            if( lineStart )
                directive = hit;
            else
                search = hit + 1;
        }

        if( directive == nullptr )
            return current;

        const char* nameStart = directive + SDL_strlen( "#include \"" );
        const char* nameEnd   = SDL_strchr( nameStart, '"' );
        if( nameEnd == nullptr )
        {
            SDL_LogError( SDL_LOG_CATEGORY_RENDER, "crGraphics: malformed #include in %s", name );
            return current;
        }

        char includeName[ 64 ];
        const size_t nameLen = static_cast<size_t>( nameEnd - nameStart );
        if( nameLen >= sizeof( includeName ) )
        {
            SDL_LogError( SDL_LOG_CATEGORY_RENDER, "crGraphics: #include name too long in %s", name );
            return current;
        }
        SDL_memcpy( includeName, nameStart, nameLen );
        includeName[ nameLen ] = '\0';

        char includePath[ 256 ];
        SDL_snprintf( includePath, sizeof( includePath ), "crassets/shaders/%s", includeName );

        char* included = static_cast<char*>( SDL_LoadFile( includePath, nullptr ) );
        if( included == nullptr )
        {
            SDL_LogError( SDL_LOG_CATEGORY_RENDER, "crGraphics: #include load failed( %s ) in %s", includePath, name );
            return current;
        }
        if( crText::HasUtf8Bom( included, includeName, SDL_LOG_CATEGORY_RENDER ) )
        {
            SDL_free( included );
            SDL_free( current );
            return nullptr;
        }

        // line number of the line following the directive — restored via #line so compile errors
        // keep reporting positions in the original file (source string 0; the include is string 1)
        int32_t lineAfter = 1;
        for( const char* p = current; p < directive; ++p )
        {
            if( *p == '\n' )
                ++lineAfter;
        }
        ++lineAfter;

        const char* rest = SDL_strchr( directive, '\n' );
        if( rest == nullptr )
            rest = "";

        char lineEnter[ 24 ];
        char lineRestore[ 32 ];
        SDL_snprintf( lineEnter, sizeof( lineEnter ), "#line 1 1\n" );
        SDL_snprintf( lineRestore, sizeof( lineRestore ), "\n#line %d 0\n", lineAfter );

        const size_t prefixLen = static_cast<size_t>( directive - current );
        const size_t total     = prefixLen + SDL_strlen( lineEnter ) + SDL_strlen( included ) + SDL_strlen( lineRestore ) + SDL_strlen( rest ) + 1;

        char* merged = static_cast<char*>( SDL_malloc( total ) );
        merged[ 0 ] = '\0';
        SDL_memcpy( merged, current, prefixLen );
        merged[ prefixLen ] = '\0';
        SDL_strlcat( merged, lineEnter, total );
        SDL_strlcat( merged, included, total );
        SDL_strlcat( merged, lineRestore, total );
        SDL_strlcat( merged, rest, total );

        SDL_free( included );
        SDL_free( current );
        current = merged;
    }

    SDL_LogError( SDL_LOG_CATEGORY_RENDER, "crGraphics: too many #include expansions in %s — cyclic include?", name );
    return current;
}
GLuint crGraphics::CompileShader( GLenum type, const char* source, const char* name )
{
    GLuint shader = glCreateShader( type );
    glShaderSource( shader, 1, &source, nullptr );
    glCompileShader( shader );

    GLint ok = 0;
    glGetShaderiv( shader, GL_COMPILE_STATUS, &ok );
    if( ok == 0 )
    {
        char log[ 1024 ];
        glGetShaderInfoLog( shader, sizeof( log ), nullptr, log );
        SDL_LogError( SDL_LOG_CATEGORY_RENDER, "Shader compile failed (%s): %s", name, log );
        glDeleteShader( shader );
        return 0;
    }

    return shader;
}
GLuint crGraphics::LoadProgram( const char* vertFile, const char* fragFile )
{
    GLuint program = CompileProgram( vertFile, fragFile );
    if( program != 0 )
        _uniforms->BindTo( program );   // same GraphicsUniforms UBO the builtin programs get
    return program;
}
GLuint crGraphics::CompileProgram( const char* vertPath, const char* fragPath )
{
    char vfull[ 256 ];
    char ffull[ 256 ];
    SDL_snprintf( vfull, sizeof( vfull ), "crassets/shaders/%s", vertPath );
    SDL_snprintf( ffull, sizeof( ffull ), "crassets/shaders/%s", fragPath );

    void* vsrc = SDL_LoadFile( vfull, nullptr );   // SDL_LoadFile null-terminates the buffer
    void* fsrc = SDL_LoadFile( ffull, nullptr );
    if( ( vsrc == nullptr ) || ( fsrc == nullptr ) )
    {
        SDL_LogError( SDL_LOG_CATEGORY_RENDER, "crGraphics: Shader load failed: %s / %s", vfull, ffull );
        SDL_free( vsrc );
        SDL_free( fsrc );
        return 0;
    }

    const bool vertBom = crText::HasUtf8Bom( static_cast<const char*>( vsrc ), vertPath, SDL_LOG_CATEGORY_RENDER );   // no short circuit — one run names every offender
    const bool fragBom = crText::HasUtf8Bom( static_cast<const char*>( fsrc ), fragPath, SDL_LOG_CATEGORY_RENDER );
    if( vertBom || fragBom )
    {
        SDL_free( vsrc );
        SDL_free( fsrc );
        return 0;
    }

    char* vprep = PreprocessShaderSource( static_cast<const char*>( vsrc ), vertPath );
    char* fprep = PreprocessShaderSource( static_cast<const char*>( fsrc ), fragPath );
    SDL_free( vsrc );
    SDL_free( fsrc );

    if( ( vprep == nullptr ) || ( fprep == nullptr ) )
    {
        SDL_free( vprep );
        SDL_free( fprep );
        return 0;
    }

    GLuint vsh = CompileShader( GL_VERTEX_SHADER,   vprep, vertPath );
    GLuint fsh = CompileShader( GL_FRAGMENT_SHADER, fprep, fragPath );
    SDL_free( vprep );
    SDL_free( fprep );

    if( ( vsh == 0 ) || ( fsh == 0 ) )
    {
        glDeleteShader( vsh );
        glDeleteShader( fsh );
        return 0;
    }

    GLuint program = glCreateProgram();
    glAttachShader( program, vsh );
    glAttachShader( program, fsh );
    glLinkProgram( program );
    glDeleteShader( vsh );
    glDeleteShader( fsh );

    GLint ok = 0;
    glGetProgramiv( program, GL_LINK_STATUS, &ok );
    if( ok == 0 )
    {
        char log[ 1024 ];
        glGetProgramInfoLog( program, sizeof( log ), nullptr, log );
        SDL_LogError( SDL_LOG_CATEGORY_RENDER, "crGraphics: Program link failed (%s + %s): %s", vertPath, fragPath, log );
        glDeleteProgram( program );
        return 0;
    }

    return program;
}
