#include "crShadowMap.h"

#include "crCamera.h"
#include "crMath.h"

bool crShadowMap::Init()
{
    glGenTextures( 1, &_depthTexture );
    glBindTexture( GL_TEXTURE_2D_ARRAY, _depthTexture );
    glTexImage3D( GL_TEXTURE_2D_ARRAY, 0, GL_DEPTH_COMPONENT24, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, CASCADE_COUNT, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr );
    glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR );  // GL_LINEAR is legal with compare mode — free 2x2 softening on most hardware
    glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE );
    glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL );
    glBindTexture( GL_TEXTURE_2D_ARRAY, 0 );

    glGenFramebuffers( 1, &_fbo );
    glBindFramebuffer( GL_FRAMEBUFFER, _fbo );
    glFramebufferTextureLayer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, _depthTexture, 0, 0 );

    const GLenum none = GL_NONE;
    glDrawBuffers( 1, &none );   // depth-only — WebGL2 requires the empty draw-buffer set explicitly
    glReadBuffer( GL_NONE );

    const GLenum status = glCheckFramebufferStatus( GL_FRAMEBUFFER );
    if( status != GL_FRAMEBUFFER_COMPLETE )
        SDL_LogError( SDL_LOG_CATEGORY_RENDER, "crShadowMap: framebuffer incomplete( 0x%x )", status );

    glBindFramebuffer( GL_FRAMEBUFFER, 0 );

    return ( status == GL_FRAMEBUFFER_COMPLETE );
}
void crShadowMap::Cleanup()
{
    glDeleteFramebuffers( 1, &_fbo );
    glDeleteTextures( 1, &_depthTexture );
    _fbo          = 0;
    _depthTexture = 0;
}

float4x4 crShadowMap::LightVP( float3 lightDir, const crCamera* camera, int32_t cascade ) const
{
    const float3 dir = crMath::Normalize3( lightDir );

    // up +Y, fall back to +Z when the light points straight up/down
    float3 up = float3( 0.0f, 1.0f, 0.0f );
    if( crMath::Abs( dir.y ) > 0.99f )
        up = float3( 0.0f, 0.0f, 1.0f );

    // frustum slice of this cascade, in view-space distances
    const float d0 = SplitDistance( cascade, camera->NearPlane() );
    float       d1 = SplitDistance( cascade + 1, camera->NearPlane() );
    if( d1 < ( d0 + 0.5f ) )
        d1 = d0 + 0.5f;   // the split sliders are free-form — keep the slice non-degenerate

    // lateral radius of the slice's corner rings: r = d * tan(fovY/2) * sqrt(1 + aspect^2)
    const int2  vp     = camera->Viewport();
    const float aspect = static_cast<float>( vp.x ) / static_cast<float>( vp.y );
    const float spread = crMath::Tan( camera->EffectiveFovY() * 0.5f ) * crMath::Sqrt( 1.0f + ( aspect * aspect ) );
    const float r0     = d0 * spread;
    const float r1     = d1 * spread;

    // bounding sphere on the view axis, equidistant from both corner rings —
    // a sphere (unlike a tight box) keeps its size while the camera rotates, so texels stay put
    float t = ( ( d1 * d1 ) + ( r1 * r1 ) - ( d0 * d0 ) - ( r0 * r0 ) ) / ( 2.0f * ( d1 - d0 ) );
    t = crMath::Clamp( t, d0, d1 );

    const float radius = crMath::Sqrt( ( ( t - d1 ) * ( t - d1 ) ) + ( r1 * r1 ) );

    const float3 eyeCam = camera->EyePosition();
    const float3 fwd    = camera->Forward();

    float3 center = float3( eyeCam.x + ( fwd.x * t ),
                            eyeCam.y + ( fwd.y * t ),
                            eyeCam.z + ( fwd.z * t ) );

    {// texel snapping: quantize the center on the light's right/up axes so the volume moves in
     // whole-texel steps — otherwise shadow edges shimmer while the camera pans/orbits
        const float3 f = float3( -dir.x, -dir.y, -dir.z );
        const float3 s = crMath::Normalize3( crMath::Cross3( f, up ) );
        const float3 u = crMath::Cross3( s, f );

        const float texel = ( 2.0f * radius ) / static_cast<float>( SHADOW_MAP_SIZE );

        const float cx = crMath::Dot3( s, center );
        const float cy = crMath::Dot3( u, center );
        const float dx = ( crMath::Round( cx / texel ) * texel ) - cx;
        const float dy = ( crMath::Round( cy / texel ) * texel ) - cy;

        center = float3( center.x + ( s.x * dx ) + ( u.x * dy ),
                         center.y + ( s.y * dx ) + ( u.y * dy ),
                         center.z + ( s.z * dx ) + ( u.z * dy ) );
    }

    // per-cascade Z sized to the volume: the sphere itself plus margin toward the light.
    // the depth range therefore differs per cascade — the shader converts the meter bias
    // using this range, recovered from the ortho matrix ( m[2][2] = -2 / range )
    const float eyeDist = Z_NEAR + CASTER_MARGIN + radius;

    const float3 eye = float3( center.x + ( dir.x * eyeDist ),
                               center.y + ( dir.y * eyeDist ),
                               center.z + ( dir.z * eyeDist ) );

    const float4x4 proj = crMath::OrthoRH( radius, radius, Z_NEAR, eyeDist + radius );
    const float4x4 view = crMath::LookAtRH( eye, center, up );
    return crMath::Mult4x4( proj, view );
}
void crShadowMap::BeginPass( int32_t cascade )
{
    glBindFramebuffer( GL_FRAMEBUFFER, _fbo );
    glFramebufferTextureLayer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, _depthTexture, 0, cascade );
    glViewport( 0, 0, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE );
    glDepthMask( GL_TRUE );   // declare, don't inherit — glClear only clears depth while the write mask is on
    glClear( GL_DEPTH_BUFFER_BIT );
}

GLuint crShadowMap::DepthTexture() const
{
    return _depthTexture;
}

float crShadowMap::SplitDistance( int32_t boundary, float nearPlane ) const
{
    if( boundary <= 0 )
        return nearPlane;
    if( boundary >= CASCADE_COUNT )
        return maxDistance;

    return splitDistances[ boundary - 1 ];
}
