#include "chunk.h"

#include <algorithm>
#include <stdexcept>

Chunk::Chunk(ChunkPos pos) : position_(pos) {
    blocks_.fill(BlockType::Air);
}

const ChunkPos& Chunk::position() const {
    return position_;
}

int Chunk::index(int x, int y, int z) {
    return x + y * SIZE + z * SIZE * SIZE;
}

void Chunk::validateBlockCoords(int x, int y, int z, const char* fn) const {
    if (x < 0 || x >= SIZE || y < 0 || y >= SIZE || z < 0 || z >= SIZE) {
        throw std::out_of_range(std::string(fn) + " out of range");
    }
}

void Chunk::validateMicroCoords(int mx, int my, int mz, const char* fn) const {
    if (mx < 0 || mx >= MicroVoxelGrid::SIZE || my < 0 || my >= MicroVoxelGrid::SIZE || mz < 0 || mz >= MicroVoxelGrid::SIZE) {
        throw std::out_of_range(std::string(fn) + " micro out of range");
    }
}

BlockType Chunk::get(int x, int y, int z) const {
    validateBlockCoords(x, y, z, "Chunk::get");
    return blocks_[index(x, y, z)];
}

void Chunk::set(int x, int y, int z, BlockType type) {
    validateBlockCoords(x, y, z, "Chunk::set");

    const int idx = index(x, y, z);
    blocks_[idx] = type;
    microVoxels_.erase(idx);

    if (type != BlockType::Water) {
        waterLevels_.erase(idx);
    } else if (waterLevels_.find(idx) == waterLevels_.end()) {
        waterLevels_[idx] = MAX_WATER_LEVEL;
    }
}

bool Chunk::hasWater(int x, int y, int z) const {
    if (x < 0 || x >= SIZE || y < 0 || y >= SIZE || z < 0 || z >= SIZE) {
        return false;
    }

    const int idx = index(x, y, z);
    return blocks_[idx] == BlockType::Water && waterLevels_.find(idx) != waterLevels_.end();
}

std::uint8_t Chunk::getWaterLevel(int x, int y, int z) const {
    if (x < 0 || x >= SIZE || y < 0 || y >= SIZE || z < 0 || z >= SIZE) {
        return 0;
    }

    const int idx = index(x, y, z);
    auto it = waterLevels_.find(idx);
    if (it == waterLevels_.end()) {
        return 0;
    }
    return it->second;
}

void Chunk::setWaterLevel(int x, int y, int z, std::uint8_t level) {
    validateBlockCoords(x, y, z, "Chunk::setWaterLevel");

    const int idx = index(x, y, z);
    microVoxels_.erase(idx);

    if (level == 0) {
        waterLevels_.erase(idx);
        if (blocks_[idx] == BlockType::Water) {
            blocks_[idx] = BlockType::Air;
        }
        return;
    }

    if (level > MAX_WATER_LEVEL) {
        level = MAX_WATER_LEVEL;
    }

    blocks_[idx] = BlockType::Water;
    waterLevels_[idx] = level;
}

void Chunk::removeWater(int x, int y, int z) {
    validateBlockCoords(x, y, z, "Chunk::removeWater");

    const int idx = index(x, y, z);
    waterLevels_.erase(idx);

    if (blocks_[idx] == BlockType::Water) {
        blocks_[idx] = BlockType::Air;
    }
}

bool Chunk::isSubdivided(int x, int y, int z) const {
    if (x < 0 || x >= SIZE || y < 0 || y >= SIZE || z < 0 || z >= SIZE) {
        return false;
    }
    return microVoxels_.find(index(x, y, z)) != microVoxels_.end();
}

MicroVoxelGrid& Chunk::ensureSubdivisionFromBlock(int x, int y, int z) {
    validateBlockCoords(x, y, z, "Chunk::ensureSubdivisionFromBlock");
    const int idx = index(x, y, z);
    auto it = microVoxels_.find(idx);
    if (it != microVoxels_.end()) {
        return it->second;
    }

    MicroVoxelGrid grid;
    const BlockType base = blocks_[idx];
    if (base != BlockType::Air && base != BlockType::Water) {
        grid.voxels.fill(base);
    }

    auto [insertedIt, inserted] = microVoxels_.emplace(idx, std::move(grid));
    (void)inserted;
    return insertedIt->second;
}

void Chunk::subdivideBlock(int x, int y, int z) {
    (void)ensureSubdivisionFromBlock(x, y, z);
}

void Chunk::clearSubdivision(int x, int y, int z) {
    validateBlockCoords(x, y, z, "Chunk::clearSubdivision");
    microVoxels_.erase(index(x, y, z));
}

BlockType Chunk::getMicroBlock(int x, int y, int z, int mx, int my, int mz) const {
    validateBlockCoords(x, y, z, "Chunk::getMicroBlock");
    validateMicroCoords(mx, my, mz, "Chunk::getMicroBlock");

    const int idx = index(x, y, z);
    auto it = microVoxels_.find(idx);
    if (it != microVoxels_.end()) {
        return it->second.get(mx, my, mz);
    }

    return blocks_[idx];
}

void Chunk::cleanupSubdivisionState(int x, int y, int z) {
    const int idx = index(x, y, z);
    auto it = microVoxels_.find(idx);
    if (it == microVoxels_.end()) {
        return;
    }

    bool anySolid = false;
    bool allSame = true;
    BlockType first = it->second.voxels[0];

    for (BlockType type : it->second.voxels) {
        if (type != BlockType::Air) {
            anySolid = true;
        }
        if (type != first) {
            allSame = false;
        }
    }

    if (!anySolid) {
        blocks_[idx] = BlockType::Air;
        microVoxels_.erase(it);
        return;
    }

    if (allSame && first != BlockType::Air && first != BlockType::Water) {
        blocks_[idx] = first;
        microVoxels_.erase(it);
        return;
    }

    blocks_[idx] = first == BlockType::Water ? BlockType::Air : first;
    if (blocks_[idx] == BlockType::Water) {
        waterLevels_.erase(idx);
    }
}

void Chunk::setMicroBlock(int x, int y, int z, int mx, int my, int mz, BlockType type) {
    validateBlockCoords(x, y, z, "Chunk::setMicroBlock");
    validateMicroCoords(mx, my, mz, "Chunk::setMicroBlock");

    if (type == BlockType::Water) {
        type = BlockType::Air;
    }

    const int idx = index(x, y, z);
    if (blocks_[idx] == BlockType::Water) {
        blocks_[idx] = BlockType::Air;
        waterLevels_.erase(idx);
    }

    MicroVoxelGrid& grid = ensureSubdivisionFromBlock(x, y, z);
    grid.set(mx, my, mz, type);
    cleanupSubdivisionState(x, y, z);
}

bool Chunk::isLeafPlayerPlaced(int x, int y, int z) const {
    const int idx = index(x, y, z);
    return playerPlacedLeaves_.count(idx) > 0;
}

void Chunk::markLeafPlayerPlaced(int x, int y, int z) {
    validateBlockCoords(x, y, z, "Chunk::markLeafPlayerPlaced");
    playerPlacedLeaves_[index(x, y, z)] = true;
}

void Chunk::clearLeafPlayerPlaced(int x, int y, int z) {
    playerPlacedLeaves_.erase(index(x, y, z));
}
