// Game.cpp - Implementation of core Tetris game logic
#include "Game.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <SDL3/SDL.h>

// Piece rotation bitmasks (row-major 4x4). Bit 0 = (0,0).
static const std::array<Shape, PIECE_COUNT> SHAPES = {{
    Shape{ 0x0F00, 0x2222, 0x00F0, 0x4444 }, // I
    Shape{ 0x0660, 0x0660, 0x0660, 0x0660 }, // O
    Shape{ 0x0E40, 0x4C40, 0x4E00, 0x4640 }, // T
    Shape{ 0x06C0, 0x4620, 0x06C0, 0x4620 }, // S
    Shape{ 0x0C60, 0x2640, 0x0C60, 0x2640 }, // Z
    Shape{ 0x08E0, 0x6440, 0x0E20, 0x44C0 }, // J
    Shape{ 0x02E0, 0x4460, 0x0E80, 0xC440 }, // L
}};

// NES (NTSC) gravity table: frames per grid cell for each level.
// Based on: 0-9, 10-12: 5, 13-15: 4, 16-18: 3, 19-28: 2, 29+: 1
namespace {
    constexpr double NES_FPS = 60.0988;
    constexpr double FRAME_MS = 1000.0 / NES_FPS;
    
    struct LevelGravity { int framesPerCell; double levelMultiplier; };

    // Default table following NES values; levelMultiplier starts at 1.0 and can be tuned per-level
    LevelGravity LEVEL_TABLE[30] = {
        {48,1.0}, {43,1.0}, {38,1.0}, {33,1.0}, {28,1.0}, {23,1.0}, {18,1.0}, {13,1.0}, {8,1.0}, {6,1.0},
        {5,1.0}, {5,1.0}, {5,1.0}, {4,1.0}, {4,1.0}, {4,1.0}, {3,1.0}, {3,1.0}, {3,1.0}, {2,1.0},
        {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {2,1.0}, {1,1.0}
    };

    inline double gravityMsForLevel(int level, double globalMultiplier)
    {
        int idx = level < 0 ? 0 : (level >= 29 ? 29 : level);
        const LevelGravity &lg = LEVEL_TABLE[idx];
        double frames = lg.framesPerCell * lg.levelMultiplier;
        double result = frames * FRAME_MS * globalMultiplier;

        static int debug_calls = 0;
        /*
        if (debug_calls < 3) {
            printf("Level %d: %d frames per cell (mult %.2f) = %.1f ms per cell (global x%.2f)\\n",
                   level, lg.framesPerCell, lg.levelMultiplier, result, globalMultiplier);
            debug_calls++;
        }
        */
        return result;
    }
}

void Game::reset(int startLevel_) {
    // Standard reset is primarily for endless; Challenge reuses the same pipeline and then
    // immediately sets up its own level state.
    std::fill(board.begin(), board.end(), 0);
    clearAsteroidGrid();
    recentAsteroidExplosions.clear();
    std::fill(blockCounts.begin(), blockCounts.end(), 0);
    bag.clear();
    _score = 0; _lines = 0; _level = startLevel_; startLevel = startLevel_;
    _tetrisesMade = 0;
    _currentCombo = 0;
    _maxCombo = 0;
    _comboCount = 0;
    challengeComplete = false;
    challengeLevelActive = false;
    challengeAdvanceQueued = false;
    challengeQueuedLevel = 0;
    // Initialize gravity using NES timing table (ms per cell by level)
    gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
    fallAcc = 0; gameOver=false; paused=false;
    hardDropShakeTimerMs = 0.0;
    hardDropCells.clear();
    hardDropFxId = 0;
    _startTime = SDL_GetPerformanceCounter();
    _pausedTime = 0;
    _lastPauseStart = 0;
    hold = Piece{}; hold.type = PIECE_COUNT; canHold=true;
    refillBag();
    pieceSequence = 0;
    spawn();

    if (mode == GameMode::Challenge) {
        int lvl = startLevel_ <= 0 ? 1 : startLevel_;
        startChallengeRun(lvl);
    }
}

void Game::clearAsteroidGrid() {
    for (auto &cell : asteroidGrid) {
        cell.reset();
    }
    asteroidsRemainingCount = 0;
    asteroidsTotalThisLevel = 0;
}

void Game::startChallengeRun(int startingLevel) {
    mode = GameMode::Challenge;
    int lvl = std::clamp(startingLevel, 1, ASTEROID_MAX_LEVEL);
    // Reset all stats and timers like a fresh run
    _score = 0; _lines = 0; _level = lvl; startLevel = lvl;
    _tetrisesMade = 0;
    _currentCombo = 0;
    _maxCombo = 0;
    _comboCount = 0;
    _startTime = SDL_GetPerformanceCounter();
    _pausedTime = 0;
    _lastPauseStart = 0;
    // Reseed challenge RNG so levels are deterministic per run but distinct per session
    if (challengeSeedBase == 0) {
        challengeSeedBase = static_cast<uint32_t>(SDL_GetTicks());
    }
    challengeRng.seed(challengeSeedBase + static_cast<uint32_t>(lvl));
    challengeAdvanceQueued = false;
    challengeQueuedLevel = 0;
    setupChallengeLevel(lvl, false);
}

void Game::beginNextChallengeLevel() {
    if (mode != GameMode::Challenge || challengeComplete) {
        return;
    }
    challengeAdvanceQueued = false;
    challengeQueuedLevel = 0;
    int next = challengeLevelIndex + 1;
    if (next > ASTEROID_MAX_LEVEL) {
        challengeComplete = true;
        challengeLevelActive = false;
        return;
    }
    setupChallengeLevel(next, true);
}

void Game::setupChallengeLevel(int level, bool preserveStats) {
    challengeLevelIndex = std::clamp(level, 1, ASTEROID_MAX_LEVEL);
    _level = challengeLevelIndex;
    startLevel = challengeLevelIndex;
    challengeComplete = false;
    challengeLevelActive = true;
    challengeAdvanceQueued = false;
    challengeQueuedLevel = 0;
    // Refresh deterministic RNG for this level
    challengeRng.seed(challengeSeedBase + static_cast<uint32_t>(challengeLevelIndex));

    // Optionally reset cumulative stats (new run) or keep them (between levels)
    if (!preserveStats) {
        std::fill(blockCounts.begin(), blockCounts.end(), 0);
        _score = 0;
        _lines = 0;
        _tetrisesMade = 0;
        _currentCombo = 0;
        _comboCount = 0;
        _maxCombo = 0;
        _startTime = SDL_GetPerformanceCounter();
        _pausedTime = 0;
        _lastPauseStart = 0;
    } else {
        _currentCombo = 0;
    }

    // Clear playfield and piece state
    std::fill(board.begin(), board.end(), 0);
    clearAsteroidGrid();
    completedLines.clear();
    hardDropCells.clear();
    hardDropFxId = 0;
    recentAsteroidExplosions.clear();
    fallAcc = 0.0;
    gameOver = false;
    paused = false;
    softDropping = false;
    hold = Piece{};
    hold.type = PIECE_COUNT;
    canHold = true;
    bag.clear();
    refillBag();
    pieceSequence = 0;
    spawn();

    // Challenge gravity scales upward per level (faster = smaller ms per cell)
    double baseMs = gravityMsForLevel(0, gravityGlobalMultiplier);
    double speedFactor = 1.0 + static_cast<double>(challengeLevelIndex) * 0.02;
    gravityMs = (speedFactor > 0.0) ? (baseMs / speedFactor) : baseMs;

    // Place asteroids for this level
    placeAsteroidsForLevel(challengeLevelIndex);

    if (levelUpCallback) {
        levelUpCallback(_level);
    }
}

AsteroidType Game::chooseAsteroidTypeForLevel(int level) {
    // Simple weight distribution by level bands
    int normalWeight = 100;
    int armoredWeight = 0;
    int fallingWeight = 0;
    int coreWeight = 0;

    if (level >= 10) {
        armoredWeight = 20;
        normalWeight = 80;
    }
    if (level >= 20) {
        fallingWeight = 20;
        normalWeight = 60;
    }
    if (level >= 40) {
        fallingWeight = 30;
        armoredWeight = 25;
        normalWeight = 45;
    }
    if (level >= 60) {
        coreWeight = 20;
        fallingWeight = 30;
        armoredWeight = 25;
        normalWeight = 25;
    }

    int total = normalWeight + armoredWeight + fallingWeight + coreWeight;
    if (total <= 0) return AsteroidType::Normal;
    std::uniform_int_distribution<int> dist(0, total - 1);
    int pick = dist(challengeRng);
    if (pick < normalWeight) return AsteroidType::Normal;
    pick -= normalWeight;
    if (pick < armoredWeight) return AsteroidType::Armored;
    pick -= armoredWeight;
    if (pick < fallingWeight) return AsteroidType::Falling;
    return AsteroidType::Core;
}

AsteroidCell Game::makeAsteroidForType(AsteroidType t) const {
    AsteroidCell cell{};
    cell.type = t;
    switch (t) {
        case AsteroidType::Normal:
            cell.hitsRemaining = 1;
            cell.gravityEnabled = false;
            break;
        case AsteroidType::Armored:
            cell.hitsRemaining = 2;
            cell.gravityEnabled = false;
            break;
        case AsteroidType::Falling:
            cell.hitsRemaining = 2;
            cell.gravityEnabled = false;
            break;
        case AsteroidType::Core:
            cell.hitsRemaining = 3;
            cell.gravityEnabled = false;
            break;
    }
    cell.visualState = 0;
    return cell;
}

void Game::placeAsteroidsForLevel(int level) {
    int desired = std::clamp(level, 1, ASTEROID_MAX_LEVEL);
    // Placement window grows upward with level but caps at half board
    int height = std::clamp(2 + level / 3, 2, ROWS / 2);
    int minRow = ROWS - 1 - height;
    int maxRow = ROWS - 1;
    minRow = std::max(0, minRow);

    std::uniform_int_distribution<int> xDist(0, COLS - 1);
    std::uniform_int_distribution<int> yDist(minRow, maxRow);

    int attempts = 0;
    const int maxAttempts = desired * 16;
    while (asteroidsRemainingCount < desired && attempts < maxAttempts) {
        int x = xDist(challengeRng);
        int y = yDist(challengeRng);
        int idx = y * COLS + x;
        attempts++;
        if (board[idx] != 0 || asteroidGrid[idx].has_value()) {
            continue;
        }
        AsteroidType type = chooseAsteroidTypeForLevel(level);
        AsteroidCell cell = makeAsteroidForType(type);
        board[idx] = asteroidBoardValue(type);
        asteroidGrid[idx] = cell;
        ++asteroidsRemainingCount;
        ++asteroidsTotalThisLevel;
    }

    if (asteroidsRemainingCount < desired) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[CHALLENGE] Placed %d/%d asteroids for level %d", asteroidsRemainingCount, desired, level);
    }
}

// Helper implementations for asteroid board encoding
bool Game::isAsteroidValue(int boardValue) {
    return boardValue >= ASTEROID_BASE;
}

AsteroidType Game::asteroidTypeFromValue(int boardValue) {
    int idx = boardValue - ASTEROID_BASE;
    if (idx < 0) return AsteroidType::Normal;
    if (idx > static_cast<int>(AsteroidType::Core)) idx = static_cast<int>(AsteroidType::Core);
    return static_cast<AsteroidType>(idx);
}

int Game::asteroidBoardValue(AsteroidType t) {
    return ASTEROID_BASE + static_cast<int>(t);
}

double Game::elapsed() const {
    if (!_startTime) return 0.0;
    
    Uint64 currentTime = SDL_GetPerformanceCounter();
    Uint64 totalPausedTime = _pausedTime;
    
    // If currently paused, add time since pause started
    if (paused && _lastPauseStart > 0) {
        totalPausedTime += (currentTime - _lastPauseStart);
    }
    
    Uint64 activeTime = currentTime - _startTime - totalPausedTime;
    double seconds = (double)activeTime / (double)SDL_GetPerformanceFrequency();
    return seconds;
}

void Game::updateElapsedTime() {
    // This method is now just for API compatibility
    // Actual elapsed time is calculated on-demand in elapsed()
}

void Game::setPaused(bool p) {
    if (p == paused) return; // No change
    
    if (p) {
        // Pausing - record when pause started
        _lastPauseStart = SDL_GetPerformanceCounter();
    } else {
        // Unpausing - add elapsed pause time to total
        if (_lastPauseStart > 0) {
            Uint64 currentTime = SDL_GetPerformanceCounter();
            _pausedTime += (currentTime - _lastPauseStart);
            _lastPauseStart = 0;
        }
    }
    
    paused = p;
}

int Game::consumeQueuedChallengeLevel() {
    if (!challengeAdvanceQueued) {
        return 0;
    }
    int next = challengeQueuedLevel;
    challengeAdvanceQueued = false;
    challengeQueuedLevel = 0;
    return next;
}

void Game::setSoftDropping(bool on) {
    if (softDropping == on) {
        return;
    }

    double oldStep = softDropping ? (gravityMs / 5.0) : gravityMs;
    softDropping = on;
    double newStep = softDropping ? (gravityMs / 5.0) : gravityMs;

    if (oldStep <= 0.0 || newStep <= 0.0) {
        return;
    }

    double progress = fallAcc / oldStep;
    progress = std::clamp(progress, 0.0, 1.0);
    fallAcc = progress * newStep;
}

void Game::refillBag() {
    bag.clear();
    for (int i=0;i<PIECE_COUNT;++i) bag.push_back(static_cast<PieceType>(i));
    std::shuffle(bag.begin(), bag.end(), rng);
}

double Game::getGravityGlobalMultiplier() const { return gravityGlobalMultiplier; }
double Game::getGravityMs() const { return gravityMs; }

void Game::setLevelGravityMultiplier(int level, double m) {
    if (level < 0) return;
    int idx = level >= 29 ? 29 : level;
    LEVEL_TABLE[idx].levelMultiplier = m;
    // If current level changed, refresh gravityMs
    if (_level == idx) gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
}


void Game::spawn() {
    if (bag.empty()) refillBag();
    PieceType pieceType = bag.back();
    
    // I-piece needs to start one row higher due to its height when vertical
    int spawnY = (pieceType == I) ? -2 : -1;
    
    cur = Piece{ pieceType, 0, 3, spawnY };
    
    // Check if the newly spawned piece collides with existing blocks
    if (collides(cur)) {
        gameOver = true;
        return; // Don't proceed with spawning if it causes a collision
    }
    
    bag.pop_back();
    blockCounts[cur.type]++; // Increment count for this piece type
    canHold = true;
    
    // Prepare next piece
    if (bag.empty()) refillBag();
    PieceType nextType = bag.back();
    int nextSpawnY = (nextType == I) ? -2 : -1;
    nextPiece = Piece{ nextType, 0, 3, nextSpawnY };
        ++pieceSequence;
}

bool Game::cellFilled(const Piece& p, int cx, int cy) {
    if (p.type == PIECE_COUNT) return false;
    const uint16_t mask = SHAPES[p.type][p.rot];
    const int bit = cy*4 + cx;
    return (mask >> bit) & 1;
}

bool Game::collides(const Piece& p) const {
    for (int cy=0; cy<4; ++cy) {
        for (int cx=0; cx<4; ++cx) if (cellFilled(p,cx,cy)) {
            int gx = p.x + cx; int gy = p.y + cy;
            if (gx < 0 || gx >= COLS || gy >= ROWS) return true;
            if (gy >= 0 && board[gy*COLS + gx] != 0) return true;
        }
    }
    return false;
}

void Game::lockPiece() {
    for (int cy=0; cy<4; ++cy) {
        for (int cx=0; cx<4; ++cx) if (cellFilled(cur,cx,cy)) {
            int gx = cur.x + cx; int gy = cur.y + cy;
            if (gy >= 0 && gy < ROWS) board[gy*COLS + gx] = static_cast<int>(cur.type)+1;
            if (gy < 0) gameOver = true;
        }
    }
    
    // Check for completed lines but don't clear them yet - let the effect system handle it
    int cleared = checkLines();
    if (cleared > 0) {
        // JS scoring system: base points per clear, multiplied by (level+1) in JS.
        // Our _level is 1-based (JS level + 1), so multiplier == _level.
        int base = 0;
        switch (cleared) {
            case 1: base = 40; break;      // SINGLE
            case 2: base = 100; break;     // DOUBLE
            case 3: base = 300; break;     // TRIPLE
            case 4: base = 1200; break;    // TETRIS
            default: base = 0; break;
        }
    // multiplier is level+1 to match original scoring where level 0 => x1
    _score += base * (_level + 1);

        // Update total lines
        _lines += cleared;

        // Update combo counters: consecutive clears increase combo; reset when no clear
        _currentCombo += 1;
        if (_currentCombo > _maxCombo) _maxCombo = _currentCombo;
        // Count combos as any single clear that removes more than 1 line
        if (cleared > 1) {
            _comboCount += 1;
        }

        // Track tetrises made
        if (cleared == 4) {
            _tetrisesMade += 1;
        }

        if (mode != GameMode::Challenge) {
            // JS level progression (NES-like) using starting level rules
            // Both startLevel and _level are 0-based now.
            int targetLevel = startLevel;
            int firstThreshold = (startLevel + 1) * 10;

            if (_lines >= firstThreshold) {
                targetLevel = startLevel + 1 + (_lines - firstThreshold) / 10;
            }

            // If we haven't reached the first threshold yet, we are still at startLevel.
            // The above logic handles this (targetLevel initialized to startLevel).

            if (targetLevel > _level) {
                _level = targetLevel;
                // Update gravity to exact NES speed for the new level
                gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
                if (levelUpCallback) levelUpCallback(_level);
            }
        } else {
            // Challenge keeps level tied to the current challenge stage; gravity already set there
            _level = challengeLevelIndex;
        }

        // Trigger sound effect callback for line clears
        if (soundCallback) {
            soundCallback(cleared);
        }
    }
    else {
        // No clear -> reset combo
        _currentCombo = 0;
    }
    if (!gameOver) spawn();
}

int Game::checkLines() {
    completedLines.clear();
    
    // Check each row from bottom to top
    for (int y = ROWS - 1; y >= 0; --y) {
        bool full = true;
        for (int x = 0; x < COLS; ++x) {
            if (board[y*COLS + x] == 0) {
                full = false;
                break;
            }
        }
        if (full) {
            completedLines.push_back(y);
        }
    }

    // Pre-play asteroid destroy SFX immediately when a clearing line contains asteroids (reduces latency)
    if (!completedLines.empty() && mode == GameMode::Challenge) {
        std::optional<AsteroidType> foundType;
        for (int y : completedLines) {
            for (int x = 0; x < COLS; ++x) {
                int idx = y * COLS + x;
                if (isAsteroidValue(board[idx])) {
                    foundType = asteroidTypeFromValue(board[idx]);
                } else if (idx >= 0 && idx < static_cast<int>(asteroidGrid.size()) && asteroidGrid[idx].has_value()) {
                    foundType = asteroidGrid[idx]->type;
                }
            }
        }
        if (foundType.has_value()) {
            pendingAsteroidDestroyType = foundType;
            if (!asteroidDestroySoundPreplayed && asteroidDestroyedCallback) {
                asteroidDestroySoundPreplayed = true;
                asteroidDestroyedCallback(*foundType);
            }
        }
    }
    
    return static_cast<int>(completedLines.size());
}

void Game::clearCompletedLines() {
    if (completedLines.empty()) return;
    
    actualClearLines();
    completedLines.clear();
}

void Game::actualClearLines() {
    if (completedLines.empty()) return;
        recentAsteroidExplosions.clear();

    std::array<int, COLS*ROWS> newBoard{};
    std::array<std::optional<AsteroidCell>, COLS*ROWS> newAst{};
    for (auto &cell : newAst) cell.reset();
    std::fill(newBoard.begin(), newBoard.end(), 0);

    handleAsteroidsOnClearedRows(completedLines, newBoard, newAst);

    board = newBoard;
    asteroidGrid = newAst;

    // Apply asteroid-specific gravity after the board collapses
    applyAsteroidGravity();

    // Reset preplay latch so future destroys can fire again
    pendingAsteroidDestroyType.reset();
    asteroidDestroySoundPreplayed = false;

    if (mode == GameMode::Challenge) {
        if (asteroidsRemainingCount <= 0) {
            int nextLevel = challengeLevelIndex + 1;
            if (nextLevel > ASTEROID_MAX_LEVEL) {
                challengeComplete = true;
                challengeLevelActive = false;
                challengeAdvanceQueued = false;
                challengeQueuedLevel = 0;
            } else {
                challengeAdvanceQueued = true;
                challengeQueuedLevel = nextLevel;
                challengeLevelActive = false;
                setPaused(true);
            }
        }
    }
}

void Game::handleAsteroidsOnClearedRows(const std::vector<int>& clearedRows,
                                         std::array<int, COLS*ROWS>& outBoard,
                                         std::array<std::optional<AsteroidCell>, COLS*ROWS>& outAsteroids) {
    std::vector<bool> clearedFlags(ROWS, false);
    for (int r : clearedRows) {
        if (r >= 0 && r < ROWS) {
            clearedFlags[r] = true;
        }
    }

    // Track asteroid count updates during processing
    int destroyedThisPass = 0;
    std::optional<AsteroidType> lastDestroyedType;

    // Precompute how many cleared rows are at or below each row to reposition survivors
    std::array<int, ROWS> clearedBelow{};
    int running = 0;
    for (int y = ROWS - 1; y >= 0; --y) {
        clearedBelow[y] = running;
        if (clearedFlags[y]) {
            ++running;
        }
    }

    for (int y = ROWS - 1; y >= 0; --y) {
        for (int x = 0; x < COLS; ++x) {
            int srcIdx = y * COLS + x;
            bool rowCleared = clearedFlags[y];
            bool isAsteroid = asteroidGrid[srcIdx].has_value();

            if (rowCleared) {
                if (!isAsteroid) {
                    continue; // normal blocks in cleared rows vanish
                }

                AsteroidCell cell = *asteroidGrid[srcIdx];
                if (cell.hitsRemaining > 0) {
                    --cell.hitsRemaining;
                }
                if (cell.hitsRemaining == 0) {
                    destroyedThisPass++;
                    lastDestroyedType = cell.type;
                    continue;
                }

                // Update visual/gravity state for surviving asteroids
                cell.visualState = static_cast<uint8_t>(std::min<int>(3, cell.visualState + 1));
                if (cell.type == AsteroidType::Falling || cell.type == AsteroidType::Core) {
                    cell.gravityEnabled = true;
                }

                int destY = y + clearedBelow[y]; // shift down by cleared rows below
                if (destY >= ROWS) {
                    continue; // off the board after collapse
                }
                int destIdx = destY * COLS + x;
                outBoard[destIdx] = asteroidBoardValue(cell.type);
                outAsteroids[destIdx] = cell;
            } else {
                int destY = y + clearedBelow[y];
                if (destY >= ROWS) {
                    continue;
                }
                int destIdx = destY * COLS + x;
                outBoard[destIdx] = board[srcIdx];
                if (isAsteroid) {
                    outAsteroids[destIdx] = asteroidGrid[srcIdx];
                }
            }
        }
    }

    if (destroyedThisPass > 0) {
        asteroidsRemainingCount = std::max(0, asteroidsRemainingCount - destroyedThisPass);
        if (!asteroidDestroySoundPreplayed && asteroidDestroyedCallback && lastDestroyedType.has_value()) {
            asteroidDestroyedCallback(*lastDestroyedType);
        }
    }
}

void Game::applyAsteroidGravity() {
    if (asteroidsRemainingCount <= 0) {
        return;
    }
    bool moved = false;
    do {
        moved = false;
        for (int y = ROWS - 2; y >= 0; --y) {
            for (int x = 0; x < COLS; ++x) {
                int idx = y * COLS + x;
                if (!asteroidGrid[idx].has_value()) {
                    continue;
                }
                if (!asteroidGrid[idx]->gravityEnabled) {
                    continue;
                }
                int belowIdx = (y + 1) * COLS + x;
                if (board[belowIdx] == 0) {
                    // Move asteroid down one cell
                    board[belowIdx] = board[idx];
                    asteroidGrid[belowIdx] = asteroidGrid[idx];
                    board[idx] = 0;
                    asteroidGrid[idx].reset();
                    moved = true;
                }
            }
        }
    } while (moved);
}

bool Game::tryMoveDown() {
    Piece p = cur; p.y += 1; if (!collides(p)) { cur = p; return true; } return false;
}

void Game::tickGravity(double frameMs) {
    if (paused) return; // Don't tick gravity when paused
    
    // Soft drop: 20x faster for rapid continuous dropping
    double effectiveGravityMs = softDropping ? (gravityMs / 5.0) : gravityMs;
    
    fallAcc += frameMs;
    
    while (fallAcc >= effectiveGravityMs) {
        // Attempt to move down by one row
        if (tryMoveDown()) {
            // Award soft drop points only if player is actively holding Down
            if (softDropping) {
                _score += 1;
            }
        } else {
            // Can't move down further, lock piece
            lockPiece();
            if (gameOver) break;
        }
        fallAcc -= effectiveGravityMs;
    }
}

void Game::softDropBoost(double frameMs) { 
    // This method is now deprecated - soft drop is handled in tickGravity
    // Kept for API compatibility but does nothing
    (void)frameMs;
}

void Game::updateVisualEffects(double frameMs) {
    if (frameMs <= 0.0) {
        return;
    }

    if (hardDropShakeTimerMs <= 0.0) {
        hardDropShakeTimerMs = 0.0;
        if (!hardDropCells.empty()) {
            hardDropCells.clear();
        }
        return;
    }

    hardDropShakeTimerMs = std::max(0.0, hardDropShakeTimerMs - frameMs);
    if (hardDropShakeTimerMs <= 0.0 && !hardDropCells.empty()) {
        hardDropCells.clear();
    }
}

void Game::hardDrop() { 
    if (paused) return;
    // Count how many rows we drop for scoring parity with JS
    int rows = 0;
    while (tryMoveDown()) { rows++; }
    // JS: POINTS.HARD_DROP = 1 per cell
    if (rows > 0) {
        _score += rows * 1;
    }
    hardDropCells.clear();
    hardDropCells.reserve(8);
    for (int cy = 0; cy < 4; ++cy) {
        for (int cx = 0; cx < 4; ++cx) {
            if (!cellFilled(cur, cx, cy)) {
                continue;
            }
            int gx = cur.x + cx;
            int gy = cur.y + cy;
            if (gx < 0 || gx >= COLS || gy >= ROWS) {
                continue;
            }
            if (gy >= 0) {
                hardDropCells.push_back(SDL_Point{gx, gy});
            }
        }
    }

    ++hardDropFxId;
    lockPiece();
    hardDropShakeTimerMs = HARD_DROP_SHAKE_DURATION_MS;
}

double Game::hardDropShakeStrength() const {
    if (hardDropShakeTimerMs <= 0.0) {
        return 0.0;
    }
    return std::clamp(hardDropShakeTimerMs / HARD_DROP_SHAKE_DURATION_MS, 0.0, 1.0);
}

void Game::rotate(int dir) {
    if (paused) return;
    
    Piece p = cur;
    p.rot = (p.rot + dir + 4) % 4;
    
    // Try rotation at current position first
    if (!collides(p)) {
        // If rotation at current position would place cells above the top,
        // attempt to shift it down so the topmost block sits at gy == 0.
        int minGy = std::numeric_limits<int>::max();
        for (int cy = 0; cy < 4; ++cy) {
            for (int cx = 0; cx < 4; ++cx) {
                if (!cellFilled(p, cx, cy)) continue;
                minGy = std::min(minGy, p.y + cy);
            }
        }

        if (minGy < 0) {
            Piece adj = p;
            adj.y += -minGy;
            if (!collides(adj)) {
                cur = adj;
                return;
            }
            // Can't shift into place without collision - fall through to kicks
        } else {
            cur = p;
            return;
        }
    }
    
    // Standard SRS Wall Kicks
    // See: https://tetris.wiki/Super_Rotation_System#Wall_kicks
    
    // JLSTZ Wall Kicks (0->R, R->2, 2->L, L->0)
    // We only implement the clockwise (0->1, 1->2, 2->3, 3->0) and counter-clockwise (0->3, 3->2, 2->1, 1->0)
    // For simplicity in this codebase, we'll use a unified set of tests that covers most cases
    // or we can implement the full table.
    
    // Let's use a robust set of kicks that covers most standard situations
    std::vector<std::pair<int,int>> kicks;
    
    if (p.type == I) {
        // I-piece kicks
        kicks = {
            {0, 0},   // Basic rotation
            {-2, 0}, {1, 0}, {-2, -1}, {1, 2}, // 0->1 (R)
            {2, 0}, {-1, 0}, {2, 1}, {-1, -2}, // 1->0 (L)
            {-1, 0}, {2, 0}, {-1, 2}, {2, -1}, // 1->2 (R)
            {1, 0}, {-2, 0}, {1, -2}, {-2, 1}, // 2->1 (L)
            {2, 0}, {-1, 0}, {2, 1}, {-1, -2}, // 2->3 (R)
            {-2, 0}, {1, 0}, {-2, -1}, {1, 2}, // 3->2 (L)
            {1, 0}, {-2, 0}, {1, -2}, {-2, 1}, // 3->0 (R)
            {-1, 0}, {2, 0}, {-1, 2}, {2, -1}  // 0->3 (L)
        };
        // The above is a superset; for a specific rotation state transition we should pick the right row.
        // However, since we don't track "last rotation state" easily here (we just have p.rot),
        // we'll try a generally permissive set of kicks that works for I-piece.
        // A simplified "try everything" approach for I-piece:
        kicks = {
            {0, 0},
            {-2, 0}, { 2, 0},
            {-1, 0}, { 1, 0},
            { 0,-1}, { 0, 1}, // Up/Down
            {-2,-1}, { 2,-1}, // Diagonal up
            { 1, 2}, {-1, 2}, // Diagonal down
            {-2, 1}, { 2, 1}
        };
    } else {
        // JLSTZ kicks
        kicks = {
            {0, 0},
            {-1, 0}, { 1, 0}, // Left/Right
            { 0,-1},          // Up (floor kick)
            {-1,-1}, { 1,-1}, // Diagonal up
            { 0, 1}           // Down (rare but possible)
        };
    }

    for (auto kick : kicks) {
        Piece test = p;
        test.x = cur.x + kick.first;
        test.y = cur.y + kick.second;
        if (!collides(test)) {
            // Prevent rotated piece from ending up above the visible playfield.
            // If any cell of `test` is above the top (gy < 0), try shifting it
            // downward so the highest block sits at row 0. Accept the shift
            // only if it doesn't collide; otherwise keep searching kicks.
            int minGy = std::numeric_limits<int>::max();
            for (int cy = 0; cy < 4; ++cy) {
                for (int cx = 0; cx < 4; ++cx) {
                    if (!cellFilled(test, cx, cy)) continue;
                    minGy = std::min(minGy, test.y + cy);
                }
            }

            if (minGy < 0) {
                Piece adj = test;
                adj.y += -minGy; // shift down so topmost block is at gy == 0
                if (!collides(adj)) {
                    cur = adj;
                    return;
                }
                // couldn't shift without collision, try next kick
            } else {
                cur = test;
                return;
            }
        }
    }
}

void Game::move(int dx) {
    if (paused) return;
    Piece p = cur; p.x += dx; if (!collides(p)) cur = p;
}

void Game::holdCurrent() {
    if (paused || !canHold) return;
    
    if (hold.type == PIECE_COUNT) {
        // First hold - just store current piece and spawn new one
        hold = cur;
        // I-piece needs to start one row higher due to its height when vertical
        int holdSpawnY = (hold.type == I) ? -2 : -1;
        hold.x = 3; hold.y = holdSpawnY; hold.rot = 0;
        spawn();
    } else {
        // Swap current with held piece
        Piece temp = hold;
        hold = cur;
        // I-piece needs to start one row higher due to its height when vertical
        int holdSpawnY = (hold.type == I) ? -2 : -1;
        int currentSpawnY = (temp.type == I) ? -2 : -1;
        hold.x = 3; hold.y = holdSpawnY; hold.rot = 0;
        cur = temp;
        cur.x = 3; cur.y = currentSpawnY; cur.rot = 0;
    }
    
    canHold = false; // Can only hold once per piece spawn
}
