#pragma once

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

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

class crGraphics;

// Fullscreen post-processing chain: the scene renders into an offscreen HDR target (linear RGBA16F),
// enabled passes ping-pong over it, and the final composite pass tone-maps + sRGB-encodes to the backbuffer.
// RGBA16F renderability requires EXT_color_buffer_(half_)float — enforced in crGraphics::Init.
class crPostProcess
{
    static constexpr float2  RENDER_SCALE_MINMAX = float2( 0.20f, 2.0f );
    // ~28 B a slot, so the pool is cheap. the real budget is fill: each live ripple rasterizes a
    // blended quad into the half-res distort map, making the cost their total SCREEN AREA, not count
    static constexpr int32_t QUEUED_RIPPLE_CAP   = 1024;

public:
    struct BloomSettings
    {
        bool  enabled   = true;
        float threshold = 1.0f;    // linear HDR — bloom only what exceeds display white
        float knee      = 0.1f;
        float intensity = 0.8f;
        // what "bright" measures. 0 = Rec.709 luminance, which is perceptually right but weights
        // blue at 7% and green at 72% — a saturated blue then needs roughly ten times a green's
        // value to qualify, so on a neon palette whole hues silently never bloom. 1 = the largest
        // channel, which makes the test hue-blind: every saturated colour glows at the same value.
        // 0 keeps the pre-existing behaviour, so games that never set it are unaffected
        float chromaBias = 0.0f;
    };
    struct TonemapSettings
    {
        bool    enabled  = true;   // HDR pipeline — off means values past 1.0 hard-clip at the encode
        float   exposure = 1.0f;
        int32_t mode     = 1;      // 0 = Reinhard, 1 = ACES
    };
    struct ColorGradeSettings
    {
        bool   enabled = false;
        float3 lift    = { 0.0f, 0.0f, 0.0f };
        float3 gamma   = { 1.0f, 1.0f, 1.0f };
        float3 gain    = { 1.0f, 1.0f, 1.0f };
    };
    struct RadialBlurSettings
    {
        bool    enabled  = false;
        float2  center   = { 0.5f, 0.5f };
        float   strength = 0.15f;
        int32_t samples  = 8;
    };
    struct ChromaticSettings
    {
        bool  enabled  = false;
        float strength = 0.004f;
    };
    struct VignetteSettings
    {
        bool  enabled   = false;
        float radius    = 0.7f;
        float softness  = 0.35f;
        float intensity = 0.6f;
    };
    struct FogSettings
    {
        bool   enabled = true;
        float3 color   = { 0.12f, 0.12f, 0.12f };   // authored sRGB — default matches clearColor so distance melts into the background
        float  start   = 32.0f;                     // view-space meters
        float  end     = 64.0f;
    };
    struct SmaaSettings
    {
        bool    enabled   = true;
        int32_t debugView = 0;   // 0 = off, 1 = edges, 2 = blend weights
    };
    struct DistortSettings
    {
        bool  enabled  = true;
        float strength = 1.0f;   // global multiplier on every pushed ripple
    };
    struct GlitchSettings
    {
        float strength = 0.0f;   // 0 = off; horizontal slice tearing + jitter, scaled by this
    };

    bool  enabled    = true;   // master — off disables every effect; the scene still routes through the final encode pass
    float saturation = 1.0f;   // < 1 drains color at the final composite (low-hp dread); game-driven per frame

    BloomSettings      bloom;
    TonemapSettings    tonemap;
    ColorGradeSettings colorGrade;
    RadialBlurSettings radialBlur;
    ChromaticSettings  chromatic;
    VignetteSettings   vignette;
    FogSettings        fog;
    SmaaSettings       smaa;
    DistortSettings    distort;
    GlitchSettings     glitch;

private:
    struct RenderTarget
    {
        GLuint fbo;
        GLuint texture;
        GLuint depthTexture;
        int2   size;
    };

    // per-frame value uniforms, resolved once at Init (CacheUniformLocations)
    struct UniformLocations
    {
        GLint bloomThreshold;
        GLint bloomKnee;
        GLint bloomChromaBias;
        GLint bloomDirection;
        GLint bloomIntensity;
        GLint radialCenter;
        GLint radialStrength;
        GLint radialSamples;
        GLint tonemapEnabled;
        GLint exposure;
        GLint tonemapMode;
        GLint gradeEnabled;
        GLint lift;
        GLint gamma;
        GLint gain;
        GLint chromaticEnabled;
        GLint chromaticStrength;
        GLint vignetteEnabled;
        GLint vignetteRadius;
        GLint vignetteSoftness;
        GLint vignetteIntensity;
        GLint smaaEdgeRtMetrics;
        GLint smaaWeightRtMetrics;
        GLint smaaBlendRtMetrics;
        GLint distortEnabled;
        GLint glitchStrength;
        GLint glitchTime;
        GLint saturation;
    };

    crGraphics* _graphics = nullptr;

    int2  _outputSize  = {};     // backbuffer pixels — the final pass viewport
    float _renderScale = 1.0f;

    RenderTarget _scene  = {};   // full res, the only target with depth (3D pass) — depth is a texture so overlays can sample it
    RenderTarget _ping   = {};
    RenderTarget _pong   = {};
    RenderTarget _bloomA = {};   // half res
    RenderTarget _bloomB = {};

    RenderTarget _ldr       = {};   // composite output when SMAA follows — SMAA needs the tonemapped, gamma-encoded image
    RenderTarget _smaaEdges = {};
    RenderTarget _smaaBlend = {};

    RenderTarget _distortMap = {};   // half res RG16F — screen-space UV offsets, ripples sum additively

    crBatchedBillboards _rippleBatch;   // world-space ripple quads rendered into the offset map
    crArray<crBatchedBillboards::BillboardInstance> _ripples;   // pushed per frame, consumed by Execute

    // multi-frame shockwaves (QueueRipple) — advanced inside Execute on the render clock
    struct QueuedRipple
    {
        float3 pos      = {};
        float  age      = 0.0f;
        float  life     = 0.4f;
        float  radius   = 4.0f;
        float  strength = 0.015f;
    };

    QueuedRipple _queuedRipples[ QUEUED_RIPPLE_CAP ] = {};
    int32_t      _queuedCount  = 0;
    int32_t      _queuedCursor = 0;
    uint64_t     _rippleTicks  = 0;

    GLuint _smaaAreaTex   = 0;   // precomputed SMAA LUTs (iryoku reference data)
    GLuint _smaaSearchTex = 0;

    GLuint _quadVao = 0;   // fullscreen quad matching fullscreen.vert (aPos, aTexCoord)
    GLuint _quadVbo = 0;

    UniformLocations _locations = {};

public:
    bool Init( crGraphics* graphics, int2 viewport );
    void Cleanup();

    void Resize( int2 viewport );

    // offscreen targets recreate at scale x output size; the always-on final pass upscales to the backbuffer
    void  SetRenderScale( float scale );
    float RenderScale() const;

    void BeginScene( color4 clearColor );    // binds + clears the scene target
    void Execute();                          // effects + the always-on final encode pass (+ SMAA); ends with the backbuffer bound

    // queue one ripple quad for this frame's distortion map (im-mode — cleared by Execute).
    // phase01 = ring expansion 0 (center) .. 1 (rim, faded out); strength in UV units (~0.01)
    void PushRipple( float3 worldPos, float radius, float phase01, float strength );

    // queue a shockwave that expands over `life` seconds — Execute() advances it on the render
    // clock and feeds PushRipple each frame. full pool overwrites the oldest in ring order
    void QueueRipple( float3 worldPos, float radius, float life, float strength );
    void ClearQueuedRipples();   // session boundary — drop every live shockwave

    GLuint SceneDepthTexture() const;        // always valid — the scene always renders offscreen
    int2   SceneSize() const;

private:
    void ExecuteSmaa();   // edge -> weights -> neighborhood blend, LDR in / backbuffer out

    bool CreateTargets();
    void DestroyTargets();
    void CreateSmaaLuts();
    void CacheUniformLocations();   // fills _locations; sampler-unit uniforms never change, so they are set here and never per frame

    void BindTarget( const RenderTarget& rt );
    void DrawQuad();

    static bool CreateTarget( RenderTarget& rt, int2 size, bool depth, GLenum internalFormat, GLenum format, GLenum type );
    static void DestroyTarget( RenderTarget& rt );
};
