#pragma once

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

#include "crArray.h"
#include "crEcsComponents.h"
#include "crPrimitiveGeometry.h"
#include "crShadowMap.h"   // CASCADE_COUNT — the shadow instance lists are indexed per cascade
#include "crStruct.h"

// instanced solid primitives (unit box / unit sphere) in world space — depth-tested, back-face culled;
// owns the opaque, shadow, and transparent (multiply/alpha/additive) instance streams
class crBatchedPrimitives
{
private:
    // per-instance stream — must match primitive.vert locations 2..7
    struct PrimitiveInstance
    {
        quat4  rot;             // vec4 in the shader
        float3 pos;
        float3 scale;
        color4 color;
        float  emissive;        // location 6 = vec4( emissive, receiveShadow, fresnel, custom0 )
        float  receiveShadow;   // 0 or 1
        float  fresnel;
        float  custom0;
        float  custom1;         // location 7 = vec4( custom1, custom2, custom3, custom4 )
        float  custom2;
        float  custom3;
        float  custom4;
        float  custom5;         // location 8 = float
    };

    struct PrimitiveBuffers
    {
        GLuint  vao;
        GLuint  vbo;
        GLuint  ibo;
        GLuint  instanceVbo;
        int32_t indexCount;
    };

    // alpha instances merge across primitives so the back-to-front sort is GLOBAL; the sorted list
    // is then drawn as instanced "runs" — one draw per consecutive same-primitive stretch
    struct AlphaEntry
    {
        PrimitiveInstance inst;
        uint8_t      shape;
    };

    static constexpr int32_t MAX_MATERIALS   = 4;       // 0 = builtin lit; 1.. = RegisterMaterial ids (custom draw programs)

    // cull volumes set each frame by Begin(); camera-visibility and shadow-visibility are independent,
    // so an off-screen caster still populates the shadow lists and never touches the camera lists
    crFrustum             _cameraFrustum = {};
    crFrustum             _cascadeFrustums[ crShadowMap::CASCADE_COUNT ] = {};

    // opaque/multiply/additive are indexed [material][shape]: a custom material (RegisterMaterial)
    // gets its own draw program. shadow and alpha have no material dimension and draw through one
    // program each — an opaque custom material DOES still cast, but through the generic depth
    // shader, so a vertex stage that displaces (warp, reshape) leaves an unmatched silhouette
    PrimitiveBuffers           _buffers[ static_cast<int32_t>( EPrimitive::_SIZE ) ] = {};
    GLuint                _materialProgram[ MAX_MATERIALS ] = {};   // slot 0 = builtin (set by Flush); 1.. = RegisterMaterial
    int32_t               _materialCount = 1;                       // slot 0 reserved for builtin
    crArray<PrimitiveInstance> _opaque[ MAX_MATERIALS ][ static_cast<int32_t>( EPrimitive::_SIZE ) ];       // opaque ∩ camera frustum — camera pass
    crArray<PrimitiveInstance> _shadow[ crShadowMap::CASCADE_COUNT ][ static_cast<int32_t>( EPrimitive::_SIZE ) ];   // casters ∩ each cascade's light frustum — shadow pass
    crArray<PrimitiveInstance> _multiply[ MAX_MATERIALS ][ static_cast<int32_t>( EPrimitive::_SIZE ) ];     // transparent groups — camera pass only, never cast
    crArray<PrimitiveInstance> _additive[ MAX_MATERIALS ][ static_cast<int32_t>( EPrimitive::_SIZE ) ];
    crArray<AlphaEntry>   _alpha;                                                     // merged across primitives (see AlphaEntry)
    crArray<PrimitiveInstance> _alphaSorted[ static_cast<int32_t>( EPrimitive::_SIZE ) ];  // scratch: per-primitive upload buffers in global-sorted order

public:
    void Init();
    void Cleanup();

    // register a custom draw program (game-owned, from crGraphics::LoadProgram); returns its material id
    // for CPrimitive.material. custom-material instances receive the same PrimitiveInstance stream (locations 2..8)
    // and GraphicsUniforms UBO as the builtin — the custom shader must declare that layout. ALPHA
    // instances ignore the material (they join one global depth sort) and so does the shadow pass.
    // a full registry is a programming error: it asserts, and returns 0 so release still draws
    uint8_t RegisterMaterial( GLuint program );

    void Begin( const crFrustum& cameraFrustum, const crFrustum* cascadeFrustums );   // stores the cull volumes + clears; Add() culls against them (cascadeFrustums = CASCADE_COUNT entries)
    void Add( EPrimitive shape, EBlendMode blend, quat4 rot, float3 pos, float3 scale, color4 color, float emissive, float receiveShadow, bool castShadow, float fresnel, float custom0, float custom1, float custom2, float custom3, float custom4, float custom5, uint8_t material );
    void Flush( GLuint builtinProgram );       // opaque camera pass; view/light data comes from the GraphicsUniforms UBO
    void FlushShadow( GLuint program, int32_t cascade );   // light-space depth pass — casters within this cascade's volume (front faces; acne via slope-scaled offset)
    void FlushTransparent( GLuint builtinProgram, float3 eye, float3 fwd );   // multiply -> alpha (sorted back-to-front) -> additive; depth read only
    void ClearUnusedMemory();

    // dev stats — valid after the fill until the next Begin; alpha only after FlushTransparent
    int32_t MaterialCount() const
    {
        return _materialCount;
    }
    int32_t CameraInstanceCount( int32_t material, int32_t shape ) const
    {
        return _opaque[ material ][ shape ].Size() + _multiply[ material ][ shape ].Size() + _additive[ material ][ shape ].Size();
    }
    int32_t AlphaInstanceCount( int32_t shape ) const
    {
        return _alphaSorted[ shape ].Size();
    }
    int32_t ShadowInstanceCount( int32_t cascade, int32_t shape ) const
    {
        return _shadow[ cascade ][ shape ].Size();
    }

private:
    void        DrawInstances( const PrimitiveBuffers& buffers, const crArray<PrimitiveInstance>& instances );
    void        DrawInstanceRange( const PrimitiveBuffers& buffers, int32_t first, int32_t count );         // draw a slice of the already-uploaded instance VBO

    static void SetInstanceAttribPointers( int32_t firstInstance );   // instance attribs 2..6 based at the given VBO element (ES 3.0 has no baseInstance)
    static bool AlphaDepthGreater( const AlphaEntry& a, const AlphaEntry& b );   // std::sort comparator — view context is file-static
    static void TrimInstances( crArray<PrimitiveInstance>* instances );

    PrimitiveBuffers CreateBuffers( const float* verts, int32_t floatCount, const uint16_t* indices, int32_t indexCount );
};
