#include "world.h"

#include "chunk_generator.h"
#include "planet.h"
#include "world_save.h"

#include <algorithm>
#include <cstddef>
#include <cstdint>

std::size_t ChunkPosHash::operator()(const ChunkPos& p) const noexcept {
    std::size_t hx = static_cast<std::size_t>(static_cast<std::uint32_t>(p.x * 73856093));
    std::size_t hy = static_cast<std::size_t>(static_cast<std::uint32_t>(p.y * 19349663));
    std::size_t hz = static_cast<std::size_t>(static_cast<std::uint32_t>(p.z * 83492791));
    return hx ^ hy ^ hz;
}

void World::setSeed(uint64_t seed) {
    ChunkGenerator::seed = seed;
    PlanetManager::init(seed);
}

uint64_t World::getSeed() {
    return ChunkGenerator::seed;
}

World::World() = default;

const Chunk* World::getChunk(const ChunkPos& pos) const {
    auto it = chunks_.find(pos);
    if (it == chunks_.end()) {
        return nullptr;
    }
    return &it->second;
}

Chunk* World::getChunk(const ChunkPos& pos) {
    auto it = chunks_.find(pos);
    if (it == chunks_.end()) {
        return nullptr;
    }
    return &it->second;
}

bool World::hasChunk(const ChunkPos& pos) const {
    return getChunk(pos) != nullptr;
}

void World::ensureChunk(const ChunkPos& pos) {
    if (hasChunk(pos)) {
        return;
    }

    auto [it, inserted] = chunks_.emplace(pos, Chunk(pos));
    if (inserted) {
        // Try loading saved data; fall back to procedural generation
        if (!WorldSave::load(it->second)) {
            ChunkGenerator::generate(it->second);
        }
        markChunkDirty(pos);
    }
}

void World::addPrebuiltChunk(Chunk chunk) {
    const ChunkPos pos = chunk.position();
    if (hasChunk(pos)) return;
    chunks_.emplace(pos, std::move(chunk));
    markChunkDirty(pos);
}

void World::markDirty(const ChunkPos& pos) {
    markChunkDirty(pos);
}

void World::saveAllModified() {
    for (const ChunkPos& pos : playerModifiedChunks_) {
        auto it = chunks_.find(pos);
        if (it != chunks_.end())
            WorldSave::save(it->second);
    }
}

void World::unloadChunk(const ChunkPos& pos) {
    auto it = chunks_.find(pos);
    if (it != chunks_.end()) {
        // Only save chunks the player has actually modified.
        // Procedurally generated chunks are reproduced on demand.
        if (playerModifiedChunks_.count(pos)) {
            WorldSave::save(it->second);
        }
        chunks_.erase(it);
    }
    dirtyChunkSet_.erase(pos);
    playerModifiedChunks_.erase(pos);
}

void World::worldToLocal(
    int worldX, int worldY, int worldZ,
    ChunkPos& chunkPos,
    int& lx, int& ly, int& lz
) {
    int cx = worldX / Chunk::SIZE;
    int cy = worldY / Chunk::SIZE;
    int cz = worldZ / Chunk::SIZE;

    lx = worldX % Chunk::SIZE;
    ly = worldY % Chunk::SIZE;
    lz = worldZ % Chunk::SIZE;

    if (lx < 0) { lx += Chunk::SIZE; --cx; }
    if (ly < 0) { ly += Chunk::SIZE; --cy; }
    if (lz < 0) { lz += Chunk::SIZE; --cz; }

    chunkPos = {cx, cy, cz};
}

ChunkPos World::worldToChunkPos(int worldX, int worldY, int worldZ) const {
    ChunkPos pos{};
    int lx = 0;
    int ly = 0;
    int lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);
    return pos;
}

void World::markChunkDirty(const ChunkPos& pos) {
    if (dirtyChunkSet_.insert(pos).second) {
        dirtyChunks_.push_back(pos);
    }
}

void World::markChunkDirtyByWorld(int worldX, int worldY, int worldZ) {
    markChunkDirty(worldToChunkPos(worldX, worldY, worldZ));
}

void World::markChunkAndNeighborsDirtyByWorld(int worldX, int worldY, int worldZ) {
    ChunkPos center = worldToChunkPos(worldX, worldY, worldZ);
    markChunkDirty(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)                markChunkDirty({center.x - 1, center.y, center.z});
    if (lx == Chunk::SIZE - 1)  markChunkDirty({center.x + 1, center.y, center.z});
    if (ly == 0)                markChunkDirty({center.x, center.y - 1, center.z});
    if (ly == Chunk::SIZE - 1)  markChunkDirty({center.x, center.y + 1, center.z});
    if (lz == 0)                markChunkDirty({center.x, center.y, center.z - 1});
    if (lz == Chunk::SIZE - 1)  markChunkDirty({center.x, center.y, center.z + 1});
}

BlockType World::getBlock(int worldX, int worldY, int worldZ) const {
    ChunkPos pos{};
    int lx = 0;
    int ly = 0;
    int lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);

    const Chunk* chunk = getChunk(pos);
    if (!chunk) {
        return BlockType::Air;
    }

    return chunk->get(lx, ly, lz);
}

void World::setBlock(int worldX, int worldY, int worldZ, BlockType type) {
    ChunkPos pos{};
    int lx = 0;
    int ly = 0;
    int lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);

    ensureChunk(pos);
    Chunk* chunk = getChunk(pos);
    chunk->set(lx, ly, lz, type);

    playerModifiedChunks_.insert(pos); // mark for persistence
    markChunkAndNeighborsDirtyByWorld(worldX, worldY, worldZ);
    enqueueWaterUpdateAround(worldX, worldY, worldZ);
}

bool World::isBlockSubdivided(int worldX, int worldY, int worldZ) const {
    ChunkPos pos{};
    int lx = 0;
    int ly = 0;
    int lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);

    const Chunk* chunk = getChunk(pos);
    if (!chunk) {
        return false;
    }

    return chunk->isSubdivided(lx, ly, lz);
}

BlockType World::getVoxel(int worldX, int worldY, int worldZ, int voxelX, int voxelY, int voxelZ) const {
    ChunkPos pos{};
    int lx = 0;
    int ly = 0;
    int lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);

    const Chunk* chunk = getChunk(pos);
    if (!chunk) {
        return BlockType::Air;
    }

    return chunk->getMicroBlock(lx, ly, lz, voxelX, voxelY, voxelZ);
}

void World::setVoxel(int worldX, int worldY, int worldZ, int voxelX, int voxelY, int voxelZ, BlockType type) {
    ChunkPos pos{};
    int lx = 0;
    int ly = 0;
    int lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);

    ensureChunk(pos);
    Chunk* chunk = getChunk(pos);
    chunk->setMicroBlock(lx, ly, lz, voxelX, voxelY, voxelZ, type);

    playerModifiedChunks_.insert(pos); // mark for persistence
    markChunkAndNeighborsDirtyByWorld(worldX, worldY, worldZ);
}

std::uint8_t World::getWaterLevel(int worldX, int worldY, int worldZ) const {
    ChunkPos pos{};
    int lx = 0;
    int ly = 0;
    int lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);

    const Chunk* chunk = getChunk(pos);
    if (!chunk) {
        return 0;
    }

    return chunk->getWaterLevel(lx, ly, lz);
}

void World::setWaterLevel(int worldX, int worldY, int worldZ, std::uint8_t level) {
    ChunkPos pos{};
    int lx = 0;
    int ly = 0;
    int lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);

    ensureChunk(pos);
    Chunk* chunk = getChunk(pos);
    chunk->setWaterLevel(lx, ly, lz, level);

    markChunkAndNeighborsDirtyByWorld(worldX, worldY, worldZ);
    enqueueWaterUpdateAround(worldX, worldY, worldZ);
}

bool World::hasWater(int worldX, int worldY, int worldZ) const {
    return getWaterLevel(worldX, worldY, worldZ) > 0;
}

void World::removeWater(int worldX, int worldY, int worldZ) {
    ChunkPos pos{};
    int lx = 0;
    int ly = 0;
    int lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);

    Chunk* chunk = getChunk(pos);
    if (!chunk) {
        return;
    }

    chunk->removeWater(lx, ly, lz);
    markChunkAndNeighborsDirtyByWorld(worldX, worldY, worldZ);
    enqueueWaterUpdateAround(worldX, worldY, worldZ);
}

void World::enqueueWaterUpdate(int worldX, int worldY, int worldZ) {
    WorldCell cell{worldX, worldY, worldZ};

    if (pendingWaterUpdateSet_.find(cell) != pendingWaterUpdateSet_.end()) {
        return;
    }

    pendingWaterUpdates_.push_back(cell);
    pendingWaterUpdateSet_.insert(cell);
}

void World::enqueueWaterUpdateAround(int worldX, int worldY, int worldZ) {
    enqueueWaterUpdate(worldX, worldY, worldZ);
    enqueueWaterUpdate(worldX + 1, worldY, worldZ);
    enqueueWaterUpdate(worldX - 1, worldY, worldZ);
    enqueueWaterUpdate(worldX, worldY + 1, worldZ);
    enqueueWaterUpdate(worldX, worldY - 1, worldZ);
    enqueueWaterUpdate(worldX, worldY, worldZ + 1);
    enqueueWaterUpdate(worldX, worldY, worldZ - 1);
}

void World::processWaterCell(const WorldCell& cell) {
    if (getBlock(cell.x, cell.y, cell.z) != BlockType::Water) {
        return;
    }

    int selfLevel = static_cast<int>(getWaterLevel(cell.x, cell.y, cell.z));
    if (selfLevel <= 0) {
        return;
    }

    // Gravity-aware water flow: determine "down" and horizontal axes from PlanetManager.
    float gx = 0.0f, gy = 1.0f, gz = 0.0f;
    PlanetManager::gravityUp(static_cast<float>(cell.x), static_cast<float>(cell.y),
                             static_cast<float>(cell.z), gx, gy, gz);
    // Integer down step: -up direction (e.g. gy=1 → fall to y-1; gx=-1 → fall to x+1)
    const int downX = -static_cast<int>(gx);
    const int downY = -static_cast<int>(gy);
    const int downZ = -static_cast<int>(gz);

    // Try to fall in gravity direction first
    const int bx = cell.x + downX, by = cell.y + downY, bz = cell.z + downZ;
    if (!isSolid(getBlock(bx, by, bz))) {
        const int belowLevel = static_cast<int>(getWaterLevel(bx, by, bz));
        const int capacity = Chunk::MAX_WATER_LEVEL - belowLevel;
        const int transfer = std::min(selfLevel, capacity);
        if (transfer > 0) {
            setWaterLevel(bx, by, bz, static_cast<std::uint8_t>(belowLevel + transfer));
            selfLevel -= transfer;
            if (selfLevel > 0)
                setWaterLevel(cell.x, cell.y, cell.z, static_cast<std::uint8_t>(selfLevel));
            else
                removeWater(cell.x, cell.y, cell.z);
        }
        return;
    }

    // Spread horizontally on the 4 axes perpendicular to gravity.
    // The vertical axis has |g| = 1; the two horizontal axes have g = 0.
    int horiz[4][3];
    int k = 0;
    for (int a = 0; a < 3 && k < 4; ++a) {
        const int ga = (a == 0) ? static_cast<int>(gx) :
                       (a == 1) ? static_cast<int>(gy) : static_cast<int>(gz);
        if (ga != 0) continue; // vertical axis — skip
        for (int s : {-1, 1}) {
            horiz[k][0] = (a == 0) ? s : 0;
            horiz[k][1] = (a == 1) ? s : 0;
            horiz[k][2] = (a == 2) ? s : 0;
            ++k;
        }
    }

    for (int i = 0; i < 4 && selfLevel > 0; ++i) {
        const int nx = cell.x + horiz[i][0];
        const int ny = cell.y + horiz[i][1];
        const int nz = cell.z + horiz[i][2];

        const BlockType neighborType = getBlock(nx, ny, nz);
        if (neighborType != BlockType::Air && neighborType != BlockType::Water) continue;

        const int neighborLevel = static_cast<int>(getWaterLevel(nx, ny, nz));
        if (selfLevel - neighborLevel < 2) continue;

        const int transfer = (selfLevel - neighborLevel) / 2;
        if (transfer <= 0) continue;

        const int actualTransfer = std::min({transfer, selfLevel,
                                             static_cast<int>(Chunk::MAX_WATER_LEVEL) - neighborLevel});
        if (actualTransfer <= 0) continue;

        setWaterLevel(nx, ny, nz, static_cast<std::uint8_t>(neighborLevel + actualTransfer));
        selfLevel -= actualTransfer;
        setWaterLevel(cell.x, cell.y, cell.z, static_cast<std::uint8_t>(selfLevel));
    }

    if (selfLevel <= 0) {
        removeWater(cell.x, cell.y, cell.z);
    }
}

void World::tickWater(int maxUpdatesPerTick) {
    int updates = 0;

    while (!pendingWaterUpdates_.empty() && updates < maxUpdatesPerTick) {
        const WorldCell cell = pendingWaterUpdates_.front();
        pendingWaterUpdates_.pop_front();
        pendingWaterUpdateSet_.erase(cell);
        processWaterCell(cell);
        ++updates;
    }
}

std::vector<ChunkPos> World::consumeDirtyChunks(int limit) {
    std::vector<ChunkPos> result;
    if (dirtyChunks_.empty()) return result;

    if (limit <= 0 || static_cast<int>(dirtyChunks_.size()) <= limit) {
        result.swap(dirtyChunks_);
        dirtyChunkSet_.clear();
    } else {
        result.assign(dirtyChunks_.begin(), dirtyChunks_.begin() + limit);
        dirtyChunks_.erase(dirtyChunks_.begin(), dirtyChunks_.begin() + limit);
        for (const ChunkPos& pos : result) {
            dirtyChunkSet_.erase(pos);
        }
    }
    return result;
}

const std::unordered_map<ChunkPos, Chunk, ChunkPosHash>& World::chunks() const {
    return chunks_;
}

void World::markBlockPlayerPlaced(int worldX, int worldY, int worldZ) {
    ChunkPos pos{}; int lx = 0, ly = 0, lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);
    Chunk* chunk = getChunk(pos);
    if (chunk) chunk->markLeafPlayerPlaced(lx, ly, lz);
}

bool World::isBlockPlayerPlaced(int worldX, int worldY, int worldZ) const {
    ChunkPos pos{}; int lx = 0, ly = 0, lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);
    const Chunk* chunk = getChunk(pos);
    if (!chunk) return false;
    return chunk->isLeafPlayerPlaced(lx, ly, lz);
}

void World::clearBlockPlayerPlaced(int worldX, int worldY, int worldZ) {
    ChunkPos pos{}; int lx = 0, ly = 0, lz = 0;
    worldToLocal(worldX, worldY, worldZ, pos, lx, ly, lz);
    Chunk* chunk = getChunk(pos);
    if (chunk) chunk->clearLeafPlayerPlaced(lx, ly, lz);
}
