#pragma once

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

#include <entt/entt.hpp>
#include <SDL3/SDL_events.h>

#include "crArray.h"
#include "crEcsComponents.h"
#include "crRandom.h"

class crApp;

enum class EBrickKind : uint8_t
{
    NORMAL = 0,
    TOUGH,      // several times the hp
    RICH,       // pays out a multiple
    VOLATILE,   // detonates on death, damaging its neighbours

    _SIZE
};

enum class EUpgrade : uint8_t
{
    NIBS = 0,
    PRESS,
    SPEED,
    GREED,

    _SIZE
};

// which beat of the planet loop is running. the phase decides what the balls can reach, where the
// containment sits and what the HUD reads
enum class EPhase : uint8_t
{
    SHELLS = 0,   // peeling the crust, outermost shell first
    CORE,         // crust gone — every nib on the exposed core
    CRACK,        // core dead — the spectacle, then the next planet

    _SIZE
};

// tag — one grinding nib. nibs are filtered against each other, so the count scales without the
// annulus jamming solid
struct CBall
{
    int32_t colorIndex = 0;
};

// tag — one crust brick
struct CBrick
{
    color4     baseColor;
    float      seed  = 0.0f;   // paper-tooth offset; two bricks must never be the same sheet
    int32_t    hp    = 1;
    int32_t    hpMax = 1;
    float      flash = 0.0f;   // 1 on hit — the wet blot, decays render-side
    EBrickKind kind  = EBrickKind::NORMAL;
};

// what the run has earned so far — nothing persists it, so a restart starts over
struct Progress
{
    double  gold      = 0.0;
    int32_t depth     = 1;
    int32_t bestDepth = 1;
    int32_t level[ static_cast<int32_t>( EUpgrade::_SIZE ) ] = {};
};

// an expanding shockwave ring — pooled, parked at scale zero when idle. drawn MULTIPLY, so it fades
// by walking towards white rather than towards black
struct PulseRing
{
    entt::entity entity = entt::null;
    color4       color;
    float3       pos    = {};
    float        age    = 0.0f;
    float        life   = 0.0f;   // 0 = the slot is free
    float        radius = 0.0f;
};

// one number drifting over the field — the per-hit feedback the gold counter cannot give, because a
// counter says the total and never says WHERE it came from. pooled, life 0 = the slot is free
struct FloatText
{
    char   text[ 24 ] = {};
    color4 color;
    float3 pos   = {};
    float  age   = 0.0f;
    float  life  = 0.0f;
    float  scale = 1.0f;
    float  rise  = 4.0f;   // world units per second
};

// live-tunable knobs — edited from the dev panel (RenderDevUi)
struct Tuning
{
    float   simSpeed        = 1.00f;   // scales how OFTEN a step runs, never what one contains

    float   ballSpeed       = 17.0f;   // base; the SPEED upgrade multiplies it
    float   ballRadius      = 0.34f;

    // depth adds SHELLS before it adds hp — the planet gets visibly fatter, so progress reads off the
    // silhouette instead of off a number nobody watches. the hp curves are MULTIPLICATIVE because the
    // economy is: a linear wall against exponential income stops being a wall within a few planets
    float   shellHpBase     = 1.0f;
    float   shellHpGrowth   = 1.14f;   // per depth
    float   shellHpInner    = 0.30f;   // extra hp fraction per shell further in
    float   coreHpBase      = 90.0f;
    float   coreHpGrowth    = 1.22f;   // per depth — the core is the wall the planet is paced by

    float   goldBase        = 2.0f;
    float   goldPerDepth    = 1.20f;   // multiplicative
    float   shellBonus      = 25.0f;   // shell clear, in bricks-worth of gold
    float   coreBonus       = 400.0f;  // planet crack, in bricks-worth of gold

    // FLOW — the sub-second feedback. every brick feeds it, it bleeds constantly, and it drives both
    // the payout and how wet the whole page runs, so the player sees the swarm winning or stalling
    // about a second after it happens
    float   heatPerBrick    = 0.050f;
    float   heatDecay       = 0.50f;   // per second
    float   heatGold        = 2.00f;   // gold multiplier at full flow = 1 + this

    float   strikeCooldown  = 8.0f;
    float   strikeRadius    = 6.0f;
    int32_t strikePower     = 12;      // multiplier on nib damage — scales with the run, never goes stale

    // INK. a brick's line weight IS how far it has been worked, and the blot cashes that weight in:
    // damage is multiplied per brick by inkMin + (inkGain x ink). a fresh patch barely marks, a patch
    // the swarm has been grinding goes through — so the line-weight map on the crust is the aim
    float   inkFloor        = 0.12f;   // ink an untouched brick still carries — nothing is drawn with no line at all
    float   inkMin          = 0.25f;
    float   inkGain         = 2.25f;

    // the containment tracks the live shell, so the annulus the nibs work in stays TIGHT no matter
    // how far the crust has been peeled back. without it the hit rate collapses as the planet shrinks
    float   annulus         = 3.6f;
    float   wallLerp        = 0.09f;   // fraction of the remaining gap closed per sim step

    float   camFill         = 3.30f;   // camera distance per unit of containment radius
    float   camTargetFrac   = -0.21f;  // field lift, as a fraction of distance — keeps it clear of the shop
    float   camPitch        = 0.16f;   // 0 = dead-on 2D; the tilt is what shows the field has depth
    float   camShake        = 1.0f;
};

// the game module — the framework's single entry point into src/game/ (bound through Game.h).
//
// INKWELL — CORE BREAK drawn instead of lit. the loop and the economy are the ones from CORE BREAK;
// everything spent here went into making the frame look like a printed illustration that happens to
// be running, and into one rule that makes the drawing worth reading:
//
//   a cel material (primitive_inkwell.vert/.frag) quantises the lit term into three hard bands with
//   nothing between them, draws a CONTOUR from a fresnel band tightened until it is a line, and lays
//   diagonal hatching in SCREEN space — a pen hatches the page, so three hundred bricks at three
//   hundred angles still share one stroke direction and the crust reads as one drawing
//
//   INK is the hook. a brick's line weight is how spent it is: hatching thickens, the contour widens,
//   a second hatch set crosses in. the whole crust is therefore a line-weight map of where the swarm
//   has been, and the BLOT multiplies its damage by exactly that weight — so placing it is reading
//   the drawing, not tapping the middle
//
//   the page is warm off-white and the ink darkens it by MULTIPLY, which is the only blend that means
//   anything on paper. bloom is all but off, the vignette is a page edge, the backdrop is ruled lines
//   and spatter, and the palettes were re-authored as flat inks and washes — the HDR ones read as
//   radioactive the moment the background stops being black
class Game
{
private:
    // nibs meet the crust and the core, never each other — hundreds of colliding balls would jam the
    // annulus solid long before the count became a spectacle
    static constexpr uint64_t CAT_BRICK = 1 << 2;
    static constexpr uint64_t CAT_BALL  = 1 << 3;

    static constexpr int32_t BALL_BASE     = 4;     // before any upgrade
    static constexpr int32_t BALL_CAP      = 400;
    static constexpr int32_t SHELL_MAX     = 9;
    static constexpr int32_t SHELL_RESERVE = 96;    // per-shell array reserve — the sector count decides the real one
    static constexpr int32_t FLECK_COUNT   = 170;   // spatter behind the field
    static constexpr int32_t BLOT_COUNT    = 9;     // ... and the few larger ones that give it a rhythm
    static constexpr int32_t RULE_COUNT    = 15;    // ruled lines — the page the drawing sits on
    static constexpr int32_t PULSE_RINGS   = 6;
    static constexpr int32_t CONTAIN_RINGS = 3;     // layered hairline bands drawn at the containment radius
    static constexpr int32_t HALO_COUNT    = 2;
    static constexpr int32_t FLOAT_TEXTS   = 20;
    static constexpr int32_t UPGRADE_COUNT = static_cast<int32_t>( EUpgrade::_SIZE );

    static constexpr int32_t STRIKE_SLOT = 0;   // crInputSystem button slot — key and pad land here; touch aims by tapping the field

    crApp*   _app = nullptr;
    crRandom _rng;        // sim stream — consumed only inside fixed steps
    crRandom _decorRng;   // cosmetic stream — backdrop layout only

    Tuning   _tune;
    Progress _progress;

    EPhase   _phase      = EPhase::SHELLS;
    int32_t  _shellCount = 0;
    int32_t  _liveShell  = -1;   // the outermost shell still standing; < 0 once the crust is gone

    crArray<entt::entity> _shells[ SHELL_MAX ];
    crArray<entt::entity> _balls;
    crArray<entt::entity> _dead;   // brick teardown queue — grows while it drains, so volatile chains resolve iteratively

    // cached per planet so the sim never runs a growth curve per brick — and never calls pow(), whose
    // results IEEE-754 does not pin across platforms (see crMath)
    int32_t _shellHp[ SHELL_MAX ] = {};
    double  _brickGold            = 1.0;

    entt::entity _core      = entt::null;   // the physics core — a plain static sphere, no primitive of its own
    entt::entity _coreShell = entt::null;   // what the core LOOKS like; render-clock animated, so it is its own entity
    entt::entity _halos[ HALO_COUNT ] = {};
    entt::entity _atmosphere = entt::null;  // the planet's drawn outline — a shell whose interior multiplies by white
    int32_t      _coreHp     = 1;
    int32_t      _coreHpMax  = 1;
    float        _coreFlash  = 0.0f;

    // the containment is analytic, not a body: a circle reflects a sphere exactly, and a radius that
    // is only a number can be animated every step for free (48 wall bodies could not)
    entt::entity _containRings[ CONTAIN_RINGS ] = {};
    float        _wallRadius = 27.0f;
    float        _wallTarget = 27.0f;

    PulseRing _pulseRings[ PULSE_RINGS ];

    GLuint  _fxProgram  = 0;   // primitive_inkwell — game-owned, deleted in Cleanup
    uint8_t _fxMaterial = 0;   // its crBatchedPrimitives material id; everything drawn goes through it

    uint64_t _crackEndStep   = 0;
    uint64_t _nextStrikeStep = 0;
    float    _heat           = 0.0f;   // sim state: it multiplies the payout, so it cannot live on the render clock

    // the UI and the console run on the render clock; the world may only change on the sim clock, so
    // what they want is queued here and drained in FixedUpdatePre (CLAUDE.determinism.md)
    int32_t _queuedBuy       = -1;   // EUpgrade the UI clicked this frame, or -1
    bool    _queuedRebuild   = false;
    bool    _queuedStrike    = false;
    float3  _queuedStrikePos = {};
    float3  _pointerWorld    = {};   // last pointer position on the play plane — where a key/pad blot lands

    int32_t  _bricksBroken   = 0;
    uint64_t _planetStartStep = 0;
    bool     _stampPending    = false;

    // render-side presentation state — the sim never reads any of it
    float _renderDt    = 0.0f;
    float _fxTime      = 0.0f;
    float _shakeMag    = 0.0f;
    float _shakePhase  = 0.0f;
    float _flashPulse  = 0.0f;   // radial-blur kick — a smear across wet ink, decays render-side
    float _soakPulse   = 0.0f;   // the page drinking a big beat: colour drains for a moment
    float _heatSmooth  = 0.0f;   // what the PRESENTATION reads — the sim's _heat, chased at render rate
    float _camDistance = 90.0f;

    double _goldEarnedTotal = 0.0;   // never decreases — spending must not read as a drop in income
    double _incomeMark      = 0.0;
    float  _incomeTimer     = 0.0f;
    float  _incomeDisplay   = 0.0f;

    FloatText _floatTexts[ FLOAT_TEXTS ];
    double    _textGoldAccum = 0.0;   // gold banked since the last popup — one legible number beats 100 illegible ones
    float3    _textGoldPos   = {};
    uint64_t  _nextTextStep  = 0;

    int32_t _uiButton = -1;
    int32_t _uiRound  = -1;
    int32_t _uiBar    = -1;
    int32_t _fxSpark  = -1;

    int32_t _font24 = -1;
    int32_t _font16 = -1;
    int32_t _font12 = -1;

    int32_t  _sfxBreak  = -1;
    int32_t  _sfxBlast  = -1;
    int32_t  _sfxBuy    = -1;
    int32_t  _sfxShell  = -1;
    int32_t  _sfxCrack  = -1;
    int32_t  _sfxDenied = -1;
    uint64_t _nextBreakSfxStep = 0;   // break audio is throttled — hundreds of nibs would machine-gun it

public:
    void Init( crApp* app );
    void Cleanup();

    void Update( crApp* app );                  // render frame — income readout, presentation, camera
    void FixedUpdatePre( const crApp* app );    // sim step — queued player actions, containment servo, nib servo
    void FixedUpdatePost( const crApp* app );   // sim step — brick hits, flow, phase advance
    void HandleEvent( const crApp* app, const SDL_Event* event );
    void Render( const crApp* app );            // world-anchored floating numbers
    void RenderUi( crApp* app );
    void RenderDevUi( const crApp* app );

private:
    void SpawnArena( const crApp* app );        // containment bands + halos + the page + shockwave pool
    void SpawnPlanet( const crApp* app );       // clear and rebuild every shell + the core for the current depth
    void SpawnShell( const crApp* app, int32_t shell );
    void SpawnBrick( const crApp* app, int32_t shell, float radius, float angle, float halfArc );
    void SpawnBall( const crApp* app );
    void ClearPlanet( const crApp* app );

    void UpdateBalls( const crApp* app );          // count servo + speed servo + analytic containment
    void UpdateBrickHits( const crApp* app );      // contact events -> damage -> teardown
    void UpdatePlayerActions( const crApp* app );  // drain the queued buy and blot
    void UpdateContainment( const crApp* app );    // chase the radius the live shell asks for
    void UpdatePhase( const crApp* app );          // shell clear -> next shell -> core -> crack -> next planet
    void StampDeterminism( const crApp* app );

    bool DamageBrick( const crApp* app, entt::entity e, int32_t damage );   // true = hp depleted (queued, not destroyed here)
    void DestroyBrick( const crApp* app, entt::entity e );
    void DamageCore( const crApp* app, int32_t damage );
    void Detonate( const crApp* app, float3 pos, float radius, int32_t damage, float kick, color4 color, bool inkScaled );

    void BuyUpgrade( const crApp* app, EUpgrade upgrade );
    void FireStrike( const crApp* app, float3 pos );
    void ApplyProgress( const crApp* app );   // recompute the per-planet curves the sim reads (shell hp, core hp, payout)
    void AddGold( double amount );
    void PushPulseRing( float3 pos, float radius, float life, color4 color );
    void PushFloatText( const char* text, float3 pos, color4 color, float life, float scale, float rise );

    static void CmdDepth( void* context, int32_t argc, const char** argv );
    static void CmdGold( void* context, int32_t argc, const char** argv );

    void UpdatePresentation( crApp* app );   // brick ink weight, core state, containment bands, post chain
    void UpdateCamera( crApp* app );

    void RenderHud( crApp* app );
    void RenderShop( crApp* app );

    float   BallSpeed() const;
    int32_t BallDamage() const;   // includes the full-flow bonus — this is what a hit actually deals
    int32_t BallTarget() const;
    double  GoldPerBrick() const;   // includes the flow multiplier
    bool    IsOverdrive() const;

    float InkWeight( int32_t hp, int32_t hpMax ) const;   // 0 = untouched, 1 = one hit from gone — the line weight AND the blot multiplier

    float ShellRadius( int32_t shell ) const;
    float PlanetRadius() const;      // outer face of the live shell, or the core once the crust is gone
    float ContainmentTarget() const;

    static double UpgradeCost( EUpgrade upgrade, int32_t level );
};
