#pragma once

#if defined( __EMSCRIPTEN__ ) || defined( __ANDROID__ )
#include <GLES3/gl3.h>
#else
#include <glad/glad.h>
#endif

#include <stdint.h>

#include "crArray.h"
#include "crStruct.h"

struct crAtlasSprite;
class crSpriteAtlas;
class crFontAtlas;
class crGraphicsUniforms;
class crPostProcess;
class crShadowMap;

class crBatchedSprites;
class crBatchedTexts;

enum class EShader : uint8_t
{
    BILLBOARD = 0,
    LINE_2D,
    LINE_3D,
    LINE_3D_OCCLUDED,
    PRIMITIVE,
    RIBBON_TRAIL,
    SHADOW,
    SPRITE,
    TEXT,
    VERTEX_COLOR,

    PP_BLOOM_BRIGHT,
    PP_BLOOM_BLUR,
    PP_BLOOM_COMPOSITE,
    PP_COMPOSITE,
    PP_RADIAL_BLUR,
    PP_SMAA_EDGE,
    PP_SMAA_WEIGHT,
    PP_SMAA_BLEND,
    PP_BLIT,
    PP_DISTORT_RIPPLE,

    _SIZE
};

class crGraphics
{
public:
    static constexpr int32_t BUILTIN_ATLAS = 0;   // loaded by Init; game atlases follow via LoadSpriteAtlas

    color4 clearColor = color4( 0.12f, 0.12f, 0.12f, 1.00f );

private:
    // standalone texture — atlas sub-regions cannot hardware-REPEAT, so surfaces
    // that tile (ribbon trails) load their texture whole through this cache
    struct NamedTexture
    {
        char   name[ 64 ];
        GLuint texture;   // 0 = load failed (cached so the error logs once, not every frame)
    };

    crArray<crSpriteAtlas*> _spriteAtlases;   // pointer elements — atlas addresses stay stable across growth
    crFontAtlas*            _fontAtlas     = nullptr;

    crGraphicsUniforms* _uniforms    = nullptr;
    crPostProcess*      _postProcess = nullptr;
    crShadowMap*        _shadowMap   = nullptr;

    crBatchedSprites* _sprites = nullptr;
    crBatchedTexts*   _texts   = nullptr;

    crArray<NamedTexture> _textures;

    struct ShaderDef
    {
        const char* vert;
        const char* frag;
    };

    // index-aligned with EShader — keep in sync
    inline static const ShaderDef SHADER_DEFS[] =
    {
        { "billboard.vert",      "textured_additive.frag" },
        { "line2d.vert",         "line2d.frag" },
        { "line3d.vert",         "vertex_color.frag" },      // LINE_3D
        { "line3d.vert",         "line3d_occluded.frag" },   // LINE_3D_OCCLUDED
        { "primitive.vert",      "primitive.frag" },
        { "ribbon_trail.vert",   "ribbon_trail.frag" },
        { "shadow.vert",         "shadow.frag" },
        { "quad_instanced.vert", "sprite.frag" },         // SPRITE
        { "quad_instanced.vert", "text.frag" },           // TEXT
        { "vertex_color.vert",   "vertex_color.frag" },

        { "fullscreen.vert",   "pp_bloom_bright.frag" },
        { "fullscreen.vert",   "pp_bloom_blur.frag" },
        { "fullscreen.vert",   "pp_bloom_composite.frag" },
        { "fullscreen.vert",   "pp_composite.frag" },
        { "fullscreen.vert",   "pp_radial_blur.frag" },
        { "pp_smaa_edge.vert",   "pp_smaa_edge.frag" },
        { "pp_smaa_weight.vert", "pp_smaa_weight.frag" },
        { "pp_smaa_blend.vert",  "pp_smaa_blend.frag" },
        { "fullscreen.vert",   "pp_blit.frag" },
        { "distort_ripple.vert", "pp_distort_ripple.frag" },
    };

    GLuint _programs[ static_cast<int32_t>( EShader::_SIZE ) ] = {};

public:
    bool Init( int2 initialSize );
    void Cleanup();

    void BeginBatches();
    void FlushSprites( float4x4 proj );
    void FlushTexts( float4x4 screenProj );   // world-anchored, fixed-pixel text

    GLuint Texture( const char* baseName );   // crassets/texture/<base>.ktx2, sampler state from baked crtex_* metadata; loaded once, cached by name

    static constexpr int32_t SpriteHandleAtlas( int32_t handle ) { return ( ( handle >> 24 ) & 0xFF ); }
    static constexpr int32_t SpriteHandleIndex( int32_t handle ) { return ( handle & 0x00FFFFFF ); }

    int32_t LoadSpriteAtlas( const char* baseName );   // -> atlas id, -1 on failure; owned + released by crGraphics
    int32_t FindSprite( int32_t atlasId, const char* name ) const;   // -> packed handle; -1 if missing. spawn/Init-time lookup only

    const crAtlasSprite& Sprite( int32_t handle ) const;

    crSpriteAtlas*      SpriteAtlas( int32_t id ) const;
    crFontAtlas*        FontAtlas() const;
    crGraphicsUniforms* Uniforms() const;
    crPostProcess*      PostProcess() const;
    crShadowMap*        ShadowMap() const;

    crBatchedSprites* Sprites() const;
    crBatchedTexts*   Texts() const;

    GLuint Program( EShader shader ) const;

    // game-owned shader hook: compile a program from crassets/shaders/ through the engine's include
    // preprocessor + UBO wiring. the caller owns the returned program (glDeleteProgram on teardown).
    // 0 on failure. keeps game shaders out of the builtin EShader table
    GLuint LoadProgram( const char* vertFile, const char* fragFile );

private:
    static bool   HasGLExtension( const char* name );   // substring match — covers the GL_ prefix difference between native and WebGL2
    static char*  PreprocessShaderSource( const char* source, const char* name );   // splices #include "file" (shader-dir relative), fixes line numbers via #line; nullptr if a file is refused
    static GLuint CompileShader( GLenum type, const char* source, const char* name );
    GLuint        CompileProgram( const char* vertPath, const char* fragPath );
};
