#include "CoopGame.h"

#include <algorithm>
#include <cmath>
#include <cstring>

namespace {
// NES (NTSC) gravity table reused from single-player for level progression (ms per cell)
constexpr double NES_FPS = 60.0988;
constexpr double FRAME_MS = 1000.0 / NES_FPS;

struct LevelGravity { int framesPerCell; double levelMultiplier; };

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 gravityMsForLevelInternal(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;
    return frames * FRAME_MS * globalMultiplier;
}
}

namespace {
// 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
}};
}

CoopGame::CoopGame(int startLevel_) {
    reset(startLevel_);
}

namespace {
uint64_t fnv1a64(uint64_t h, const void* data, size_t size) {
    const uint8_t* p = static_cast<const uint8_t*>(data);
    for (size_t i = 0; i < size; ++i) {
        h ^= static_cast<uint64_t>(p[i]);
        h *= 1099511628211ull;
    }
    return h;
}

template <typename T>
uint64_t hashPod(uint64_t h, const T& v) {
    return fnv1a64(h, &v, sizeof(T));
}
}

void CoopGame::resetInternal(int startLevel_, const std::optional<uint32_t>& seedOpt) {
    std::fill(board.begin(), board.end(), Cell{});
    rowStates.fill(RowHalfState{});
    completedLines.clear();
    hardDropCells.clear();
    hardDropFxId = 0;
    hardDropShakeTimerMs = 0.0;
    _score = 0;
    _lines = 0;
    _level = startLevel_;
    startLevel = startLevel_;
    gravityMs = gravityMsForLevel(_level);
    gameOver = false;
    pieceSequence = 0;
    elapsedMs = 0.0;

    left = PlayerState{};
    right = PlayerState{ PlayerSide::Right };

    auto initPlayer = [&](PlayerState& ps, uint32_t seed) {
        ps.canHold = true;
        ps.hold.type = PIECE_COUNT;
        ps.softDropping = false;
        ps.toppedOut = false;
        ps.fallAcc = 0.0;
        ps.lockAcc = 0.0;
        ps.pieceSeq = 0;
        ps.score = 0;
        ps.lines = 0;
        ps.level = startLevel_;
        ps.tetrisesMade = 0;
        ps.currentCombo = 0;
        ps.maxCombo = 0;
        ps.comboCount = 0;
        ps.bag.clear();
        ps.next.type = PIECE_COUNT;
        ps.rng.seed(seed);
        refillBag(ps);
    };

    if (seedOpt.has_value()) {
        const uint32_t seed = seedOpt.value();
        initPlayer(left, seed);
        initPlayer(right, seed ^ 0x9E3779B9u);
    } else {
        // Preserve existing behavior: random seed when not in deterministic mode.
        std::random_device rd;
        initPlayer(left, static_cast<uint32_t>(rd()));
        initPlayer(right, static_cast<uint32_t>(rd()));
    }

    spawn(left);
    spawn(right);
    updateRowStates();
}

void CoopGame::reset(int startLevel_) {
    resetInternal(startLevel_, std::nullopt);
}

void CoopGame::resetDeterministic(int startLevel_, uint32_t seed) {
    resetInternal(startLevel_, seed);
}

void CoopGame::setSoftDropping(PlayerSide side, bool on) {
    PlayerState& ps = player(side);
    auto stepFor = [&](bool soft)->double { return soft ? std::max(5.0, gravityMs / 5.0) : gravityMs; };
    double oldStep = stepFor(ps.softDropping);
    double newStep = stepFor(on);
    if (oldStep <= 0.0 || newStep <= 0.0) {
        ps.softDropping = on;
        return;
    }

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

uint64_t CoopGame::computeStateHash() const {
    uint64_t h = 1469598103934665603ull;

    // Board
    for (const auto& c : board) {
        const uint8_t occ = c.occupied ? 1u : 0u;
        const uint8_t owner = (c.owner == PlayerSide::Left) ? 0u : 1u;
        const uint8_t val = static_cast<uint8_t>(std::clamp(c.value, 0, 255));
        h = hashPod(h, occ);
        h = hashPod(h, owner);
        h = hashPod(h, val);
    }

    auto hashPiece = [&](const Piece& p) {
        const uint8_t type = static_cast<uint8_t>(p.type);
        const int32_t rot = p.rot;
        const int32_t x = p.x;
        const int32_t y = p.y;
        h = hashPod(h, type);
        h = hashPod(h, rot);
        h = hashPod(h, x);
        h = hashPod(h, y);
    };

    auto hashPlayer = [&](const PlayerState& ps) {
        const uint8_t side = (ps.side == PlayerSide::Left) ? 0u : 1u;
        h = hashPod(h, side);
        hashPiece(ps.cur);
        hashPiece(ps.next);
        hashPiece(ps.hold);
        const uint8_t canHoldB = ps.canHold ? 1u : 0u;
        const uint8_t toppedOutB = ps.toppedOut ? 1u : 0u;
        h = hashPod(h, canHoldB);
        h = hashPod(h, toppedOutB);
        h = hashPod(h, ps.score);
        h = hashPod(h, ps.lines);
        h = hashPod(h, ps.level);
        h = hashPod(h, ps.tetrisesMade);
        h = hashPod(h, ps.currentCombo);
        h = hashPod(h, ps.maxCombo);
        h = hashPod(h, ps.comboCount);
        h = hashPod(h, ps.pieceSeq);

        const uint32_t bagSize = static_cast<uint32_t>(ps.bag.size());
        h = hashPod(h, bagSize);
        for (auto t : ps.bag) {
            const uint8_t tt = static_cast<uint8_t>(t);
            h = hashPod(h, tt);
        }
    };

    hashPlayer(left);
    hashPlayer(right);

    // Session-wide counters/stats
    h = hashPod(h, _score);
    h = hashPod(h, _lines);
    h = hashPod(h, _level);
    h = hashPod(h, _tetrisesMade);
    h = hashPod(h, _currentCombo);
    h = hashPod(h, _maxCombo);
    h = hashPod(h, _comboCount);
    h = hashPod(h, startLevel);
    h = hashPod(h, pieceSequence);

    return h;
}

void CoopGame::move(PlayerSide side, int dx) {
    PlayerState& ps = player(side);
    if (gameOver || ps.toppedOut) return;
    tryMove(ps, dx, 0);
}

void CoopGame::rotate(PlayerSide side, int dir) {
    PlayerState& ps = player(side);
    if (gameOver || ps.toppedOut) return;

    auto minOccupiedY = [&](const Piece& p) -> int {
        int minY = 999;
        for (int cy = 0; cy < 4; ++cy) {
            for (int cx = 0; cx < 4; ++cx) {
                if (!cellFilled(p, cx, cy)) continue;
                minY = std::min(minY, p.y + cy);
            }
        }
        return (minY == 999) ? p.y : minY;
    };

    auto tryApplyWithTopKick = [&](const Piece& candidate) -> bool {
        // If rotation would place any occupied cell above the visible grid,
        // kick it down just enough to keep all blocks visible.
        int minY = minOccupiedY(candidate);
        int baseDy = (minY < 0) ? -minY : 0;

        // Try minimal adjustment first; allow a couple extra pixels/rows for safety.
        for (int dy = baseDy; dy <= baseDy + 2; ++dy) {
            Piece test = candidate;
            test.y += dy;
            if (!collides(ps, test)) {
                ps.cur = test;
                return true;
            }
        }
        return false;
    };

    Piece rotated = ps.cur;
    rotated.rot = (rotated.rot + dir + 4) % 4;

    // Simple wall kick: try in place, then left, then right.
    if (tryApplyWithTopKick(rotated)) return;
    rotated.x -= 1;
    if (tryApplyWithTopKick(rotated)) return;
    rotated.x += 2;
    if (tryApplyWithTopKick(rotated)) return;
}

void CoopGame::hardDrop(PlayerSide side) {
    PlayerState& ps = player(side);
    if (gameOver || ps.toppedOut) return;

    hardDropCells.clear();
    bool moved = false;
    int dropped = 0;
    while (tryMove(ps, 0, 1)) {
        moved = true;
        dropped++;
        // Record path for potential effects
        for (int cy = 0; cy < 4; ++cy) {
            for (int cx = 0; cx < 4; ++cx) {
                if (!cellFilled(ps.cur, cx, cy)) continue;
                int px = ps.cur.x + cx;
                int py = ps.cur.y + cy;
                if (py >= 0) {
                    hardDropCells.push_back(SDL_Point{ px, py });
                }
            }
        }
    }
    if (moved) {
        _score += dropped; // 1 point per cell, matches single-player hard drop
        ps.score += dropped;
        hardDropShakeTimerMs = HARD_DROP_SHAKE_DURATION_MS;
        hardDropFxId++;
    }
    lock(ps);
}

void CoopGame::holdCurrent(PlayerSide side) {
    PlayerState& ps = player(side);
    if (gameOver || ps.toppedOut) return;
    if (!ps.canHold) return;
    if (ps.hold.type == PIECE_COUNT) {
        ps.hold = ps.cur;
        spawn(ps);
    } else {
        std::swap(ps.cur, ps.hold);
        ps.cur.rot = 0;
        ps.cur.x = columnMin(ps.side) + 3;
        // Match single-player spawn height (I starts higher)
        ps.cur.y = (ps.cur.type == PieceType::I) ? -2 : -1;
        ps.pieceSeq++;
        pieceSequence++;
    }
    ps.canHold = false;
    ps.lockAcc = 0.0;
}

void CoopGame::tickGravity(double frameMs) {
    if (gameOver) return;

    elapsedMs += frameMs;

    auto stepPlayer = [&](PlayerState& ps) {
        if (ps.toppedOut) return;
        double step = ps.softDropping ? std::max(5.0, gravityMs / 5.0) : gravityMs;
        ps.fallAcc += frameMs;
        while (ps.fallAcc >= step) {
            ps.fallAcc -= step;
            if (!tryMove(ps, 0, 1)) {
                ps.lockAcc += step;
                if (ps.lockAcc >= LOCK_DELAY_MS) {
                    lock(ps);
                    break;
                }
            } else {
                // Award soft drop points when actively holding down
                if (ps.softDropping) {
                    _score += 1;
                    ps.score += 1;
                }
                ps.lockAcc = 0.0;
            }
        }
    };

    stepPlayer(left);
    stepPlayer(right);

    updateRowStates();
}

void CoopGame::updateVisualEffects(double frameMs) {
    if (hardDropShakeTimerMs > 0.0) {
        hardDropShakeTimerMs = std::max(0.0, hardDropShakeTimerMs - frameMs);
    }
}

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

double CoopGame::gravityMsForLevel(int level) const {
    return gravityMsForLevelInternal(level, gravityGlobalMultiplier);
}

bool CoopGame::cellFilled(const Piece& p, int cx, int cy) {
    if (p.type >= PIECE_COUNT) return false;
    const Shape& shape = SHAPES[p.type];
    uint16_t mask = shape[p.rot % 4];
    int bitIndex = cy * 4 + cx;
    // Masks are defined row-major 4x4 with bit 0 = (0,0) (same convention as classic).
    return (mask >> bitIndex) & 1;
}

void CoopGame::clearCompletedLines() {
    if (completedLines.empty()) return;
    clearLinesInternal();
    completedLines.clear();
    updateRowStates();
}

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

CoopGame::Piece CoopGame::drawFromBag(PlayerState& ps) {
    if (ps.bag.empty()) {
        refillBag(ps);
    }
    PieceType t = ps.bag.back();
    ps.bag.pop_back();
    Piece p{};
    p.type = t;
    return p;
}

void CoopGame::spawn(PlayerState& ps) {
    if (ps.next.type == PIECE_COUNT) {
        ps.next = drawFromBag(ps);
    }
    ps.cur = ps.next;
    ps.cur.rot = 0;
    ps.cur.x = columnMin(ps.side) + 3; // center within side
    // Match single-player spawn height (I starts higher)
    ps.cur.y = (ps.cur.type == PieceType::I) ? -2 : -1;
    ps.next = drawFromBag(ps);
    ps.canHold = true;
    ps.softDropping = false;
    ps.lockAcc = 0.0;
    ps.fallAcc = 0.0;
    ps.pieceSeq++;
    pieceSequence++;
    if (collides(ps, ps.cur)) {
        ps.toppedOut = true;
        // Cooperative mode: game ends when any player tops out.
        gameOver = true;
    }
}

bool CoopGame::collides(const PlayerState& ps, const Piece& p) const {
    int minX = columnMin(ps.side);
    int maxX = columnMax(ps.side);
    for (int cy = 0; cy < 4; ++cy) {
        for (int cx = 0; cx < 4; ++cx) {
            if (!cellFilled(p, cx, cy)) continue;
            int px = p.x + cx;
            int py = p.y + cy;
            if (px < minX || px > maxX) return true;
            if (py >= ROWS) return true;
            if (py < 0) continue; // allow spawn above board
            int idx = py * COLS + px;
            if (board[idx].occupied) return true;
        }
    }
    return false;
}

bool CoopGame::tryMove(PlayerState& ps, int dx, int dy) {
    Piece test = ps.cur;
    test.x += dx;
    test.y += dy;
    if (collides(ps, test)) return false;
    ps.cur = test;
    if (dy > 0) {
        ps.lockAcc = 0.0;
    }
    return true;
}

void CoopGame::lock(PlayerState& ps) {
    // Write piece into the board
    for (int cy = 0; cy < 4; ++cy) {
        for (int cx = 0; cx < 4; ++cx) {
            if (!cellFilled(ps.cur, cx, cy)) continue;
            int px = ps.cur.x + cx;
            int py = ps.cur.y + cy;
            if (py < 0 || py >= ROWS) continue;
            int idx = py * COLS + px;
            board[idx].occupied = true;
            board[idx].owner = ps.side;
            board[idx].value = static_cast<int>(ps.cur.type) + 1;
        }
    }
    // Detect completed lines and apply rewards but DO NOT clear them here.
    // Clearing is deferred to the visual `LineEffect` system (as in single-player)
    findCompletedLines();
    if (!completedLines.empty()) {
        int cleared = static_cast<int>(completedLines.size());
        applyLineClearRewards(ps, cleared);
        // Notify audio layer if present (matches single-player behavior)
        if (soundCallback) soundCallback(cleared);
        // Leave `completedLines` populated; `clearCompletedLines()` will be
        // invoked by the state when the LineEffect finishes.
    } else {
        _currentCombo = 0;
        ps.currentCombo = 0;
    }
    spawn(ps);
}

void CoopGame::findCompletedLines() {
    completedLines.clear();
    for (int r = 0; r < ROWS; ++r) {
        bool leftFull = true;
        bool rightFull = true;
        for (int c = 0; c < COLS; ++c) {
            const Cell& cell = board[r * COLS + c];
            if (!cell.occupied) {
                if (c < 10) leftFull = false; else rightFull = false;
            }
        }
        rowStates[r].leftFull = leftFull;
        rowStates[r].rightFull = rightFull;
        if (leftFull && rightFull) {
            completedLines.push_back(r);
        }
    }
}

void CoopGame::applyLineClearRewards(PlayerState& creditPlayer, int cleared) {
    if (cleared <= 0) return;

    // Base NES scoring scaled by shared level (level 0 => 1x multiplier)
    int base = 0;
    switch (cleared) {
        case 1: base = 40; break;
        case 2: base = 100; break;
        case 3: base = 300; break;
        case 4: base = 1200; break;
        default: base = 0; break;
    }
    _score += base * (_level + 1);
    creditPlayer.score += base * (creditPlayer.level + 1);

    // Also award a trivial per-line bonus to both players so clears benefit
    // both participants equally (as requested).
    if (cleared > 0) {
        left.score += cleared;
        right.score += cleared;
    }

    _lines += cleared;
    // Credit both players with the cleared lines so cooperative play counts for both
    left.lines += cleared;
    right.lines += cleared;

    _currentCombo += 1;
    if (_currentCombo > _maxCombo) _maxCombo = _currentCombo;
    if (cleared > 1) {
        _comboCount += 1;
    }
    if (cleared == 4) {
        _tetrisesMade += 1;
    }

    creditPlayer.currentCombo += 1;
    if (creditPlayer.currentCombo > creditPlayer.maxCombo) creditPlayer.maxCombo = creditPlayer.currentCombo;
    if (cleared > 1) {
        creditPlayer.comboCount += 1;
    }
    if (cleared == 4) {
        creditPlayer.tetrisesMade += 1;
    }

    // Level progression mirrors single-player: threshold after (startLevel+1)*10 then every 10 lines
    int targetLevel = startLevel;
    int firstThreshold = (startLevel + 1) * 10;
    if (_lines >= firstThreshold) {
        targetLevel = startLevel + 1 + (_lines - firstThreshold) / 10;
    }
    if (targetLevel > _level) {
        _level = targetLevel;
        gravityMs = gravityMsForLevel(_level);
        if (levelUpCallback) levelUpCallback(_level);
    }

    // Per-player level progression mirrors the shared rules but is driven by
    // that player's credited line clears.
    {
        int pTargetLevel = startLevel;
        int pFirstThreshold = (startLevel + 1) * 10;
        if (creditPlayer.lines >= pFirstThreshold) {
            pTargetLevel = startLevel + 1 + (creditPlayer.lines - pFirstThreshold) / 10;
        }
        creditPlayer.level = std::max(creditPlayer.level, pTargetLevel);
    }
}

void CoopGame::clearLinesInternal() {
    if (completedLines.empty()) return;
    std::sort(completedLines.begin(), completedLines.end());
    for (int idx = static_cast<int>(completedLines.size()) - 1; idx >= 0; --idx) {
        int row = completedLines[idx];
        for (int y = row; y > 0; --y) {
            for (int x = 0; x < COLS; ++x) {
                board[y * COLS + x] = board[(y - 1) * COLS + x];
            }
        }
        for (int x = 0; x < COLS; ++x) {
            board[x] = Cell{};
        }
    }
}

// Sound callback (optional) - invoked when lines are detected so audio can play
// (set via setSoundCallback)
// NOTE: defined inline in header as a std::function member; forward usage above

void CoopGame::updateRowStates() {
    for (int r = 0; r < ROWS; ++r) {
        bool leftFull = true;
        bool rightFull = true;
        for (int c = 0; c < COLS; ++c) {
            const Cell& cell = board[r * COLS + c];
            if (!cell.occupied) {
                if (c < 10) leftFull = false; else rightFull = false;
            }
        }
        rowStates[r].leftFull = leftFull;
        rowStates[r].rightFull = rightFull;
    }
}
