#include "app.h"
#include "chunk_generator.h"
#include "world_save.h"

#include <algorithm>
#include <cmath>
#include <stdexcept>
#include <string>

// ── World lifecycle ────────────────────────────────────────────────────────

void App::placePlayerAtSpawn(uint64_t seed) {
    const Planet sp = PlanetManager::spawnPlanet();
    const float  sr = static_cast<float>(sp.baseRadius);

    const Noise terrN { sp.seed ^ 0x4F3A1B2CULL };
    const Noise decorN{ sp.seed ^ 0x3C71A9F2ULL };
    constexpr float SEA_LEVEL    = 16.0f;
    constexpr float RELIEF_AMP   = 57.5f;
    constexpr float TERRAIN_BIAS = 28.0f;

    auto surfaceH = [&](float wx, float wz) -> float {
        float dy  = sr;
        float len = std::sqrt(wx*wx + dy*dy + wz*wz);
        if (len < 1.f) len = 1.f;
        float su = wx/len*sr, sv = dy/len*sr, sw = wz/len*sr;
        float th = terrN.octaves3(su*0.006f, sv*0.006f, sw*0.006f, 5, 0.5f)
                   * RELIEF_AMP + TERRAIN_BIAS;
        float pn = decorN.octaves2(su*0.05f + 500.f, sw*0.05f + 500.f, 2, 0.5f);
        if (pn > 0.82f) th -= (pn - 0.82f) * 8.f * SEA_LEVEL;
        float rn = decorN.octaves2(su*0.003f + 777.f, sw*0.003f + 333.f, 3, 0.5f);
        if (std::abs(rn) < 0.055f) th -= (0.055f - std::abs(rn)) / 0.055f * SEA_LEVEL * 2.8f;
        return th;
    };

    float spawnX = sp.cx, spawnZ = sp.cz;
    float spawnTh = surfaceH(spawnX, spawnZ);
    if (spawnTh < SEA_LEVEL + 2.0f) {
        float bestDist = 1e30f;
        for (int r = 10; r <= 200; r += 10) {
            for (int angle = 0; angle < 360; angle += 15) {
                float rad = static_cast<float>(angle) * 3.14159265f / 180.0f;
                float cx  = sp.cx + static_cast<float>(r) * std::cos(rad);
                float cz  = sp.cz + static_cast<float>(r) * std::sin(rad);
                float th  = surfaceH(cx, cz);
                if (th >= SEA_LEVEL + 2.0f) {
                    float d = static_cast<float>(r);
                    if (d < bestDist) { bestDist=d; spawnX=cx; spawnZ=cz; spawnTh=th; }
                }
            }
            if (bestDist < 1e30f) break;
        }
    }
    if (spawnTh < SEA_LEVEL + 2.0f) spawnTh = SEA_LEVEL + 5.0f;

    const float surfaceY  = sp.cy + sr + spawnTh;
    camera_.position      = {spawnX, surfaceY + 2.5f, spawnZ};
    camera_.worldUp       = {0.0f, 1.0f, 0.0f};
    camera_.orientForward = {0.0f, 0.0f, -1.0f};
    gravityUp_            = {0.0f, 1.0f, 0.0f};
    pendingGravityUp_     = {0.0f, 1.0f, 0.0f};
    cameraX_ = camera_.position.x;
    cameraY_ = camera_.position.y;
    cameraZ_ = camera_.position.z;
}



void App::startWorld(const WorldInfo& info) {
    // Set seed and save path for new world
    World::setSeed(info.seed);
    WorldSave::saveDir = WorldManager::chunksDir(info.name);
    activeWorldName_   = info.name;

    // Clear all loaded chunks and GPU meshes
    vkDeviceWaitIdle(device_);
    destroyChunkMeshes();
    world_ = World{};
    currentPlayerChunk_ = {999999, 999999, 999999};
    pendingChunkLoads_.clear();
    pendingChunkLoadSet_.clear();
    desiredChunkPositions_.clear();
    desiredChunkSet_.clear();

    // Reset inventory and hotbar
    for (auto& s : inventory_) { s.type = BlockType::Air; s.count = 0; }
    heldItem_       = {};
    hotbarSlot_     = 0;
    inventoryOpen_  = false;
    verticalVelocity_ = 0.0f;
    flyModeActive_  = false;
    flySpeedLevel_  = 0;
    airPercent_     = 100.0f;
    breakProgress_  = 0.0f;
    drops_.clear();
    for (auto& s : craftGrid_) { s.type = BlockType::Air; s.count = 0; }
    craftOutput_ = {};

    // Try to restore saved position and inventory
    double sx, sy, sz; float fx, fy, fz;
    if (WorldManager::loadPlayerPos(info.name, sx, sy, sz, fx, fy, fz)) {
        cameraX_ = sx; cameraY_ = sy; cameraZ_ = sz;
        camera_.position      = {(float)sx, (float)sy, (float)sz};
        camera_.orientForward = {fx, fy, fz};
        camera_.worldUp       = {0.0f, 1.0f, 0.0f};
        gravityUp_            = {0.0f, 1.0f, 0.0f};
        pendingGravityUp_     = {0.0f, 1.0f, 0.0f};
        // Load saved inventory
        WorldManager::SaveSlot slots[16];
        if (WorldManager::loadInventory(info.name, slots)) {
            for (int i = 0; i < 16; ++i) {
                inventory_[i].type  = static_cast<BlockType>(slots[i].type);
                inventory_[i].count = slots[i].count;
            }
        }
    } else {
        placePlayerAtSpawn(info.seed);
    }

    updateChunkStreaming();
    spawnMobs();

    // Load persisted playtime for this world
    worldPlayTime_ = WorldManager::loadPlayTime(info.name);

    // Capture cursor and switch state
    glfwSetInputMode(window_, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    firstMouse_ = true;
    gameState_  = GameState::Playing;
    updateWindowTitle();
}

void App::leaveWorld() {
    if (!activeWorldName_.empty()) {
        world_.saveAllModified();
        WorldManager::savePlayerPos(activeWorldName_,
            cameraX_, cameraY_, cameraZ_,
            camera_.orientForward.x,
            camera_.orientForward.y,
            camera_.orientForward.z);
        // Save inventory
        WorldManager::SaveSlot slots[16];
        for (int i = 0; i < 16; ++i) {
            slots[i].type  = static_cast<int32_t>(inventory_[i].type);
            slots[i].count = inventory_[i].count;
        }
        WorldManager::saveInventory(activeWorldName_, slots);
        WorldManager::savePlayTime(activeWorldName_, worldPlayTime_);
    }
    activeWorldName_.clear();
    mobs_.clear();
    cityReady_     = false;
    cityDone_      = false;
    cityNextBlock_ = 0;
    wolfKilledPlayer_ = false;
    playerHp_       = 16.0f;
    attackCooldown_ = 0.0f;
    worldList_ = WorldManager::listWorlds();
    menuSubState_ = 0;
    menuSel_      = 0;
    glfwSetInputMode(window_, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
    gameState_ = GameState::MainMenu;
}

// ── Inventory helpers ─────────────────────────────────────────────────────

int App::getLodLevel(const ChunkPos& pos) const {
    // Use 3D Chebyshev distance so planets on all faces get proper LOD
    const int dx = std::abs(pos.x - currentPlayerChunk_.x);
    const int dy = std::abs(pos.y - currentPlayerChunk_.y);
    const int dz = std::abs(pos.z - currentPlayerChunk_.z);
    const int dist = std::max({dx, dy, dz});
    return (dist > LOD_NEAR_RADIUS) ? 1 : 0;
}

void App::buildDesiredChunkSet(const ChunkPos& center) {
    desiredChunkPositions_.clear();
    desiredChunkSet_.clear();
    const int R = CHUNK_STREAM_RADIUS;
    const std::size_t n = static_cast<std::size_t>((2*R+1)*(2*R+1)*(2*R+1));
    desiredChunkPositions_.reserve(n);
    desiredChunkSet_.reserve(n);

    for (int dz = -R; dz <= R; ++dz)
    for (int dy = -R; dy <= R; ++dy)
    for (int dx = -R; dx <= R; ++dx) {
        ChunkPos p{center.x+dx, center.y+dy, center.z+dz};
        desiredChunkPositions_.push_back(p);
        desiredChunkSet_.insert(p);
    }

    // Sort by distance so nearest chunks get enqueued first
    std::sort(desiredChunkPositions_.begin(), desiredChunkPositions_.end(),
        [&center](const ChunkPos& a, const ChunkPos& b) {
            auto d2 = [&](const ChunkPos& p) {
                int x = p.x-center.x, y = p.y-center.y, z = p.z-center.z;
                return x*x + y*y + z*z;
            };
            return d2(a) < d2(b);
        }
    );
}

bool App::isDesiredChunk(const ChunkPos& pos) const {
    return desiredChunkSet_.count(pos) > 0;
}

void App::enqueueMissingChunks() {
    for (const ChunkPos& pos : desiredChunkPositions_) {
        if (!world_.hasChunk(pos) &&
            pendingChunkLoadSet_.find(pos) == pendingChunkLoadSet_.end()) {
            pendingChunkLoads_.push_back(pos);
            pendingChunkLoadSet_.insert(pos);
        }
    }
}

void App::processChunkLoadQueue() {
    // Dispatch pending positions to background generation workers.
    // Batched into one lock acquisition per frame to avoid thousands of
    // individual lock/notify cycles when the player enters a new area.
    if (pendingChunkLoads_.empty()) return;

    // Cap how many we send to workers per frame — prevents genJobQueue_ from
    // growing unbounded and keeps dispatch cost predictable.
    constexpr int DISPATCH_PER_FRAME = 64;
    int dispatched = 0;

    std::vector<ChunkPos> batch;
    batch.reserve(DISPATCH_PER_FRAME);

    while (!pendingChunkLoads_.empty() && dispatched < DISPATCH_PER_FRAME) {
        const ChunkPos pos = pendingChunkLoads_.front();
        pendingChunkLoads_.pop_front();
        pendingChunkLoadSet_.erase(pos);

        if (!isDesiredChunk(pos) || world_.hasChunk(pos)) continue;
        batch.push_back(pos);
        ++dispatched;
    }

    if (batch.empty()) return;

    {
        std::lock_guard lock(genMutex_);
        for (const ChunkPos& p : batch) {
            genJobQueue_.push_back(p);
        }
    }
    genCv_.notify_all(); // wake workers once for the whole batch
}

void App::processChunkUnloadQueue() {
    int budget = CHUNK_UNLOADS_PER_FRAME;

    std::vector<ChunkPos> toUnload;
    for (const auto& [pos, chunk] : world_.chunks()) {
        (void)chunk;
        if (!isDesiredChunk(pos)) {
            toUnload.push_back(pos);
            if (static_cast<int>(toUnload.size()) >= budget) {
                break;
            }
        }
    }

    if (toUnload.empty()) {
        return;
    }

    for (const ChunkPos& pos : toUnload) {
        world_.unloadChunk(pos);
        rebuildChunkMesh(pos); // uses destroyBufferDeferred — no stall needed
    }

    if (currentHit_.hit && !isDesiredChunk(world_.worldToChunkPos(
        currentHit_.blockX, currentHit_.blockY, currentHit_.blockZ))) {
        destroyBufferDeferred(outlineMesh_.vertexBuffer, outlineMesh_.vertexBufferMemory);
        destroyBufferDeferred(outlineMesh_.indexBuffer, outlineMesh_.indexBufferMemory);
        outlineMesh_.indexCount = 0;
        outlineMesh_.valid = false;
        currentHit_ = RaycastHit{};
        previousHit_ = RaycastHit{};
    }
}

void App::updateChunkStreaming() {
    const int wx = static_cast<int>(std::floor(camera_.position.x));
    const int wy = static_cast<int>(std::floor(camera_.position.y));
    const int wz = static_cast<int>(std::floor(camera_.position.z));

    const ChunkPos playerChunk = world_.worldToChunkPos(wx, wy, wz);

    if (!(playerChunk == currentPlayerChunk_)) {
        currentPlayerChunk_ = playerChunk;
        buildDesiredChunkSet(playerChunk);
        enqueueMissingChunks();

        // Mark LOD-border chunks dirty so they get re-meshed at correct detail level
        for (auto& [pos, mesh] : chunkMeshes_) {
            const int newLod = getLodLevel(pos);
            if (newLod != mesh.lodLevel) {
                world_.markDirty(pos);
            }
        }

        processChunkUnloadQueue();
    }

    processChunkLoadQueue();
}

void App::rebuildDirtyWorldChunks() {
    // Consume a bounded batch of dirty chunks per frame and sort by distance
    // so the area near the player is always meshed first.
    std::vector<ChunkPos> dirty = world_.consumeDirtyChunks(CHUNK_MESH_REBUILDS_PER_FRAME);
    if (dirty.empty()) return;

    const ChunkPos& pc = currentPlayerChunk_;
    std::sort(dirty.begin(), dirty.end(), [&](const ChunkPos& a, const ChunkPos& b) {
        const int da = std::max({std::abs(a.x-pc.x), std::abs(a.y-pc.y), std::abs(a.z-pc.z)});
        const int db = std::max({std::abs(b.x-pc.x), std::abs(b.y-pc.y), std::abs(b.z-pc.z)});
        return da < db;
    });

    // Chunks very close to the player (Chebyshev distance <= 3) are rebuilt
    // unconditionally — never defer them, to prevent visible pop-in near terrain.
    // Distant chunks share a 12ms time budget so they don't stall the frame.
    const auto start = std::chrono::steady_clock::now();
    constexpr auto BUDGET = std::chrono::microseconds(12000);
    constexpr int  NEAR_DIST = 3;

    std::size_t i = 0;
    for (; i < dirty.size(); ++i) {
        const ChunkPos& cp = dirty[i];
        const int dist = std::max({std::abs(cp.x-pc.x), std::abs(cp.y-pc.y), std::abs(cp.z-pc.z)});
        if (dist > NEAR_DIST && std::chrono::steady_clock::now() - start > BUDGET) break;
        rebuildChunkMesh(cp);
    }
    // Anything we skipped goes back into the queue for the next frame.
    for (; i < dirty.size(); ++i) {
        world_.markDirty(dirty[i]);
    }

    rebuildOutlineMesh();
}

void App::rebuildChunksAroundBlock(int worldX, int worldY, int worldZ) {
    ChunkPos center = world_.worldToChunkPos(worldX, worldY, worldZ);
    rebuildChunkMesh(center);

    int lx = worldX % Chunk::SIZE; if (lx < 0) lx += Chunk::SIZE;
    int ly = worldY % Chunk::SIZE; if (ly < 0) ly += Chunk::SIZE;
    int lz = worldZ % Chunk::SIZE; if (lz < 0) lz += Chunk::SIZE;

    if (lx == 0) rebuildChunkMesh({center.x - 1, center.y, center.z});
    if (lx == Chunk::SIZE - 1) rebuildChunkMesh({center.x + 1, center.y, center.z});

    if (ly == 0) rebuildChunkMesh({center.x, center.y - 1, center.z});
    if (ly == Chunk::SIZE - 1) rebuildChunkMesh({center.x, center.y + 1, center.z});

    if (lz == 0) rebuildChunkMesh({center.x, center.y, center.z - 1});
    if (lz == Chunk::SIZE - 1) rebuildChunkMesh({center.x, center.y, center.z + 1});
}


void App::workerLoop() {
    try {
        while (true) {
            ChunkPos pos;
            {
                std::unique_lock lock(genMutex_);
                genCv_.wait(lock, [&]{
                    return !genJobQueue_.empty() || !genRunning_;
                });
                if (!genRunning_ && genJobQueue_.empty()) return;
                pos = genJobQueue_.front();
                genJobQueue_.pop_front();
            }

            Chunk chunk(pos);
            if (!WorldSave::load(chunk)) {
                ChunkGenerator::generate(chunk);
            }

            {
                std::lock_guard lock(readyMutex_);
                readyChunks_.push_back({pos, std::move(chunk)});
            }
        }
    } catch (...) {
        // Worker must not propagate exceptions — swallow and exit cleanly.
    }
}

void App::drainReadyChunks() {
    int budget = CHUNK_LOADS_PER_FRAME;
    while (budget > 0) {
        std::optional<ReadyChunk> rc;
        {
            std::lock_guard lock(readyMutex_);
            if (readyChunks_.empty()) break;
            rc = std::move(readyChunks_.front());
            readyChunks_.pop_front();
        }
        if (!isDesiredChunk(rc->pos)) continue;
        if (world_.hasChunk(rc->pos)) continue;

        const ChunkPos pos = rc->pos;

        // Skip spawn animation for chunks close enough to be visible immediately.
        // Distant chunks may slide in; nearby chunks must appear at the correct
        // position right away to prevent the player seeing through terrain.
        {
            const int dx = std::abs(pos.x - currentPlayerChunk_.x);
            const int dy = std::abs(pos.y - currentPlayerChunk_.y);
            const int dz = std::abs(pos.z - currentPlayerChunk_.z);
            if (std::max({dx, dy, dz}) <= 4) {
                // Force spawnTime far in the past so animation is already done
                auto& gpuMesh = chunkMeshes_[pos]; // create if absent
                gpuMesh.spawnTime = currentTime_ - 1.0f; // 1 s past = animation finished
            }
        }

        world_.addPrebuiltChunk(std::move(rc->chunk));
        rebuildChunkMesh(pos); // new chunk: no old buffer → deferred deletion is a no-op
        // Mark loaded neighbors dirty so their face visibility updates
        for (const ChunkPos nb : {
            ChunkPos{pos.x-1,pos.y,pos.z}, ChunkPos{pos.x+1,pos.y,pos.z},
            ChunkPos{pos.x,pos.y-1,pos.z}, ChunkPos{pos.x,pos.y+1,pos.z},
            ChunkPos{pos.x,pos.y,pos.z-1}, ChunkPos{pos.x,pos.y,pos.z+1}
        }) {
            if (world_.hasChunk(nb)) world_.markDirty(nb);
        }
        --budget;
    }
}

// ── Leaf decay system ─────────────────────────────────────────────────────

void App::enqueueLeafDecayAround(int wx, int wy, int wz) {
    // Enqueue all Leaves neighbors (6-connected) that are not player-placed
    constexpr int DX[] = {1,-1,0,0,0,0};
    constexpr int DY[] = {0,0,1,-1,0,0};
    constexpr int DZ[] = {0,0,0,0,1,-1};
    for (int d = 0; d < 6; ++d) {
        const int nx = wx + DX[d], ny = wy + DY[d], nz = wz + DZ[d];
        if (world_.getBlock(nx, ny, nz) != BlockType::Leaves) continue;
        if (world_.isBlockPlayerPlaced(nx, ny, nz)) continue;
        auto key = std::make_tuple(nx, ny, nz);
        if (leafDecaySet_.insert(key).second)
            leafDecayQueue_.push_back({nx, ny, nz, currentTime_ + 0.5f});
    }
}

void App::tickLeafDecay() {
    if (leafDecayQueue_.empty()) return;

    // Use a simple LCG for 50% probability (fast, no std::random needed)
    static uint32_t rng = 0xDEAD1234u;
    auto rand50 = [&]() -> bool {
        rng = rng * 1664525u + 1013904223u;
        return (rng & 1u) == 0;
    };

    // Process items whose time has come (bounded to 64 per frame)
    constexpr int MAX_PER_FRAME = 64;
    int processed = 0;
    const int total = static_cast<int>(leafDecayQueue_.size());

    for (int i = 0; i < total && processed < MAX_PER_FRAME; ++i) {
        LeafDecayItem& item = leafDecayQueue_.front();
        if (item.nextCheck > currentTime_) {
            // Move to back — not ready yet
            leafDecayQueue_.push_back(item);
            leafDecayQueue_.pop_front();
            ++processed;
            continue;
        }

        const int x = item.x, y = item.y, z = item.z;
        leafDecayQueue_.pop_front();
        leafDecaySet_.erase(std::make_tuple(x, y, z));
        ++processed;

        // If block is no longer leaves, skip
        if (world_.getBlock(x, y, z) != BlockType::Leaves) continue;
        // Player-placed leaves never decay
        if (world_.isBlockPlayerPlaced(x, y, z)) continue;

        // Check if adjacent to any Wood block — if so, keep alive
        constexpr int DX[] = {1,-1,0,0,0,0};
        constexpr int DY[] = {0,0,1,-1,0,0};
        constexpr int DZ[] = {0,0,0,0,1,-1};
        bool nearWood = false;
        for (int d = 0; d < 6; ++d) {
            if (world_.getBlock(x+DX[d], y+DY[d], z+DZ[d]) == BlockType::Wood) {
                nearWood = true;
                break;
            }
        }
        if (nearWood) continue; // keep this leaf, don't re-enqueue

        // 50% chance to vanish each tick
        if (rand50()) {
            vkDeviceWaitIdle(device_);
            world_.setBlock(x, y, z, BlockType::Air);
            rebuildChunksAroundBlock(x, y, z);
            // Spread decay to adjacent leaf neighbours
            enqueueLeafDecayAround(x, y, z);
        } else {
            // Retry in 0.5 s
            auto key = std::make_tuple(x, y, z);
            if (leafDecaySet_.insert(key).second)
                leafDecayQueue_.push_back({x, y, z, currentTime_ + 0.5f});
        }
    }
}

// ── Mob system ────────────────────────────────────────────────────────────────

