#include "crTextureLoader.h"

#include <SDL3/SDL.h>
#include <ktx.h>

struct KeyValueWrap
{
    bool  ok;
    GLint mode;
};

struct KeyValueFilter
{
    bool  ok;
    GLint minFilter;
    GLint magFilter;
};

static bool HasGlExtension( const char* name )
{
    // exact token match — substring alone would confuse ..._etc with ..._etc1
    const char* ext = reinterpret_cast<const char*>( glGetString( GL_EXTENSIONS ) );
    if( ext == nullptr )
        return false;

    const size_t len = SDL_strlen( name );
    for( const char* p = SDL_strstr( ext, name ); p != nullptr; p = SDL_strstr( p + len, name ) )
    {
        const bool head = ( p == ext ) || ( p[ -1 ] == ' ' );
        const bool tail = ( p[ len ] == ' ' ) || ( p[ len ] == '\0' );
        if( head && tail )
            return true;
    }
    return false;
}

static ktx_transcode_fmt_e PickTranscodeTarget()
{
    static bool                s_picked = false;
    static ktx_transcode_fmt_e s_target = KTX_TTF_RGBA32;
    if( s_picked )
        return s_target;
    s_picked = true;

#ifdef __EMSCRIPTEN__
    const bool hasEtc2 = HasGlExtension( "GL_WEBGL_compressed_texture_etc" );
#else
    const bool hasEtc2 = true;   // ETC2 is GLES 3.0 core (may be driver-emulated, hence lowest priority)
#endif

    const char* name = "RGBA32";
    if( HasGlExtension( "GL_EXT_texture_compression_bptc" ) )
    {
        s_target = KTX_TTF_BC7_RGBA;
        name     = "BC7";
    }
    else if( HasGlExtension( "GL_KHR_texture_compression_astc_ldr" ) || HasGlExtension( "GL_WEBGL_compressed_texture_astc" ) )
    {
        s_target = KTX_TTF_ASTC_4x4_RGBA;
        name     = "ASTC_4x4";
    }
    else if( hasEtc2 )
    {
        s_target = KTX_TTF_ETC2_RGBA;
        name     = "ETC2";
    }

    SDL_LogInfo( CR_LOG_CATEGORY_ASSET_TEXTURE, "crTextureLoader: transcode target( %s )", name );
    return s_target;
}

static KeyValueWrap ReadWrapKv( ktxTexture2* tex, const char* key )
{
    KeyValueWrap result = { false, GL_CLAMP_TO_EDGE };

    unsigned int len   = 0;
    void*        value = nullptr;
    if( ktxHashList_FindValue( &tex->kvDataHead, key, &len, &value ) != KTX_SUCCESS )
        return result;

    const char* s = static_cast<const char*>( value );   // NUL-terminated by ktx_compress.py
    if( SDL_strcmp( s, "clamp" ) == 0 )
        result = { true, GL_CLAMP_TO_EDGE };
    else if( SDL_strcmp( s, "repeat" ) == 0 )
        result = { true, GL_REPEAT };
    else if( SDL_strcmp( s, "mirror" ) == 0 )
        result = { true, GL_MIRRORED_REPEAT };
    return result;
}

static KeyValueFilter ReadFilterKv( ktxTexture2* tex, bool hasMips )
{
    KeyValueFilter result = { false, GL_LINEAR, GL_LINEAR };

    unsigned int len   = 0;
    void*        value = nullptr;
    if( ktxHashList_FindValue( &tex->kvDataHead, "crtex_filter", &len, &value ) != KTX_SUCCESS )
        return result;

    const char* s = static_cast<const char*>( value );
    if( SDL_strcmp( s, "linear" ) == 0 )
        result = { true, hasMips ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR, GL_LINEAR };
    else if( SDL_strcmp( s, "nearest" ) == 0 )
        result = { true, hasMips ? GL_NEAREST_MIPMAP_NEAREST : GL_NEAREST, GL_NEAREST };
    return result;
}

crTexture crTextureLoader::Load( const char* path )
{
    crTexture result = {};

    static bool s_glLoaded = false;
    if( s_glLoaded == false )
    {
        ktxLoadOpenGL( reinterpret_cast<PFNGLGETPROCADDRESS>( SDL_GL_GetProcAddress ) );
        s_glLoaded = true;
    }

    // LOAD_IMAGE_DATA_BIT makes libktx take its own copy, so the buffer is ours to free
    size_t size  = 0;
    void*  bytes = SDL_LoadFile( path, &size );
    if( bytes == nullptr )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_TEXTURE, "crTextureLoader: ktx2 read failed( %s ): %s", path, SDL_GetError() );
        return result;
    }

    ktxTexture2* tex = nullptr;
    KTX_error_code rc = ktxTexture2_CreateFromMemory( static_cast<const ktx_uint8_t*>( bytes ), size,
                                                      KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &tex );
    SDL_free( bytes );
    if( rc != KTX_SUCCESS )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_TEXTURE, "crTextureLoader: ktx2 load failed( %s ): %s", path, ktxErrorString( rc ) );
        return result;
    }

    if( ktxTexture2_NeedsTranscoding( tex ) )
    {
        rc = ktxTexture2_TranscodeBasis( tex, PickTranscodeTarget(), 0 );
        if( rc != KTX_SUCCESS )
        {
            SDL_LogError( CR_LOG_CATEGORY_ASSET_TEXTURE, "crTextureLoader: transcode failed( %s ): %s", path, ktxErrorString( rc ) );
            ktxTexture_Destroy( ktxTexture( tex ) );
            return result;
        }
    }

    GLuint texture  = 0;
    GLenum glTarget = 0;
    GLenum glError  = 0;
    rc = ktxTexture_GLUpload( ktxTexture( tex ), &texture, &glTarget, &glError );
    if( rc != KTX_SUCCESS )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_TEXTURE, "crTextureLoader: gl upload failed( %s ): %s, glError:0x%x", path, ktxErrorString( rc ), glError );
        ktxTexture_Destroy( ktxTexture( tex ) );
        return result;
    }
    if( glTarget != GL_TEXTURE_2D )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_TEXTURE, "crTextureLoader: unexpected target( %s ): 0x%x", path, glTarget );
        glDeleteTextures( 1, &texture );
        ktxTexture_Destroy( ktxTexture( tex ) );
        return result;
    }

    const bool     hasMips = ( tex->numLevels > 1 );
    const KeyValueWrap   wrapS   = ReadWrapKv( tex, "crtex_wrap_s" );
    const KeyValueWrap   wrapT   = ReadWrapKv( tex, "crtex_wrap_t" );
    const KeyValueFilter filter  = ReadFilterKv( tex, hasMips );
    if( ( wrapS.ok == false ) || ( wrapT.ok == false ) || ( filter.ok == false ) )
    {
        // every ktx_compress.py output carries these keys — absence means a foreign/stale file
        SDL_LogError( CR_LOG_CATEGORY_ASSET_TEXTURE, "crTextureLoader: crtex_* metadata missing( %s )", path );
        glDeleteTextures( 1, &texture );
        ktxTexture_Destroy( ktxTexture( tex ) );
        return result;
    }

    glBindTexture( GL_TEXTURE_2D, texture );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter.minFilter );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter.magFilter );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapS.mode );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapT.mode );
    glBindTexture( GL_TEXTURE_2D, 0 );

    result.handle = texture;
    result.size    = { static_cast<int32_t>( tex->baseWidth ), static_cast<int32_t>( tex->baseHeight ) };

    ktxTexture_Destroy( ktxTexture( tex ) );
    return result;
}
