#include "world_save.h"

#include "chunk.h"

#include <array>
#include <cstdint>
#include <cstdio>
#include <filesystem>
#include <string>
#include <vector>

// ---- Binary format (little-endian, no padding) ---------------------------
//  1 byte   : version = 1
//  4096 bytes: blocks (BlockType as uint8_t)
//  2 bytes  : water count
//  for each : 2 bytes index, 1 byte level
//  2 bytes  : subdiv count
//  for each : 2 bytes index, 4096 bytes voxels
// --------------------------------------------------------------------------

static constexpr uint8_t FORMAT_VERSION = 2;

std::string WorldSave::filePath(const ChunkPos& pos) {
    return saveDir + "/" + std::to_string(pos.x)
         + "_" + std::to_string(pos.y)
         + "_" + std::to_string(pos.z) + ".bin";
}

bool WorldSave::exists(const ChunkPos& pos) {
    return std::filesystem::exists(filePath(pos));
}

// ---- Write helpers -------------------------------------------------------

static bool writeU8(std::FILE* f, uint8_t v) {
    return std::fwrite(&v, 1, 1, f) == 1;
}
static bool writeU16(std::FILE* f, uint16_t v) {
    return std::fwrite(&v, sizeof(v), 1, f) == 1;
}
static bool writeBytes(std::FILE* f, const void* data, size_t n) {
    return std::fwrite(data, 1, n, f) == n;
}

// ---- Read helpers --------------------------------------------------------

static bool readU8(std::FILE* f, uint8_t& v) {
    return std::fread(&v, 1, 1, f) == 1;
}
static bool readU16(std::FILE* f, uint16_t& v) {
    return std::fread(&v, sizeof(v), 1, f) == 1;
}
static bool readBytes(std::FILE* f, void* data, size_t n) {
    return std::fread(data, 1, n, f) == n;
}

// --------------------------------------------------------------------------

void WorldSave::save(const Chunk& chunk) {
    std::filesystem::create_directories(saveDir);

    const std::string path = filePath(chunk.position());
    std::FILE* f = std::fopen(path.c_str(), "wb");
    if (!f) return;

    // Version
    writeU8(f, FORMAT_VERSION);

    // Blocks
    constexpr int N = Chunk::SIZE;
    uint8_t blocks[N * N * N];
    for (int z = 0; z < N; ++z)
        for (int y = 0; y < N; ++y)
            for (int x = 0; x < N; ++x)
                blocks[Chunk::index(x, y, z)] = static_cast<uint8_t>(chunk.get(x, y, z));
    writeBytes(f, blocks, sizeof(blocks));

    // Water levels
    std::vector<std::pair<uint16_t, uint8_t>> waters;
    for (int z = 0; z < N; ++z)
        for (int y = 0; y < N; ++y)
            for (int x = 0; x < N; ++x) {
                const uint8_t lvl = chunk.getWaterLevel(x, y, z);
                if (lvl > 0)
                    waters.push_back({static_cast<uint16_t>(Chunk::index(x, y, z)), lvl});
            }
    writeU16(f, static_cast<uint16_t>(waters.size()));
    for (auto& [idx, lvl] : waters) {
        writeU16(f, idx);
        writeU8(f, lvl);
    }

    // Micro-voxel subdivisions
    std::vector<std::pair<uint16_t, std::array<uint8_t, MicroVoxelGrid::COUNT>>> subdivs;
    for (int z = 0; z < N; ++z)
        for (int y = 0; y < N; ++y)
            for (int x = 0; x < N; ++x) {
                if (!chunk.isSubdivided(x, y, z)) continue;
                std::array<uint8_t, MicroVoxelGrid::COUNT> vox{};
                for (int mz = 0; mz < MicroVoxelGrid::SIZE; ++mz)
                    for (int my = 0; my < MicroVoxelGrid::SIZE; ++my)
                        for (int mx = 0; mx < MicroVoxelGrid::SIZE; ++mx)
                            vox[MicroVoxelGrid::index(mx, my, mz)] =
                                static_cast<uint8_t>(chunk.getMicroBlock(x, y, z, mx, my, mz));
                subdivs.push_back({static_cast<uint16_t>(Chunk::index(x, y, z)), vox});
            }
    writeU16(f, static_cast<uint16_t>(subdivs.size()));
    for (auto& [idx, vox] : subdivs) {
        writeU16(f, idx);
        writeBytes(f, vox.data(), vox.size());
    }

    // Player-placed leaves (version 2+)
    const auto& ppl = chunk.playerPlacedLeaves();
    writeU16(f, static_cast<uint16_t>(ppl.size()));
    for (const auto& [idx, _] : ppl) {
        writeU16(f, static_cast<uint16_t>(idx));
    }

    std::fclose(f);
}

bool WorldSave::load(Chunk& chunk) {
    const std::string path = filePath(chunk.position());
    std::FILE* f = std::fopen(path.c_str(), "rb");
    if (!f) return false;

    uint8_t version = 0;
    if (!readU8(f, version) || (version != 1 && version != FORMAT_VERSION)) {
        std::fclose(f);
        return false;
    }

    constexpr int N = Chunk::SIZE;

    // Blocks
    uint8_t blocks[N * N * N];
    if (!readBytes(f, blocks, sizeof(blocks))) { std::fclose(f); return false; }
    for (int z = 0; z < N; ++z)
        for (int y = 0; y < N; ++y)
            for (int x = 0; x < N; ++x)
                chunk.set(x, y, z, static_cast<BlockType>(blocks[Chunk::index(x, y, z)]));

    // Water levels
    uint16_t waterCount = 0;
    if (!readU16(f, waterCount)) { std::fclose(f); return false; }
    for (uint16_t i = 0; i < waterCount; ++i) {
        uint16_t idx = 0; uint8_t lvl = 0;
        if (!readU16(f, idx) || !readU8(f, lvl)) { std::fclose(f); return false; }
        const int x = idx % N;
        const int y = (idx / N) % N;
        const int z = idx / (N * N);
        chunk.setWaterLevel(x, y, z, lvl);
    }

    // Micro-voxel subdivisions
    uint16_t subdivCount = 0;
    if (!readU16(f, subdivCount)) { std::fclose(f); return false; }
    for (uint16_t i = 0; i < subdivCount; ++i) {
        uint16_t idx = 0;
        if (!readU16(f, idx)) { std::fclose(f); return false; }
        std::array<uint8_t, MicroVoxelGrid::COUNT> vox{};
        if (!readBytes(f, vox.data(), vox.size())) { std::fclose(f); return false; }

        const int bx = idx % N;
        const int by = (idx / N) % N;
        const int bz = idx / (N * N);
        for (int mz = 0; mz < MicroVoxelGrid::SIZE; ++mz)
            for (int my = 0; my < MicroVoxelGrid::SIZE; ++my)
                for (int mx = 0; mx < MicroVoxelGrid::SIZE; ++mx)
                    chunk.setMicroBlock(bx, by, bz, mx, my, mz,
                        static_cast<BlockType>(vox[MicroVoxelGrid::index(mx, my, mz)]));
    }

    // Player-placed leaves (version 2+)
    if (version >= 2) {
        uint16_t pplCount = 0;
        if (!readU16(f, pplCount)) { std::fclose(f); return false; }
        for (uint16_t i = 0; i < pplCount; ++i) {
            uint16_t idx = 0;
            if (!readU16(f, idx)) { std::fclose(f); return false; }
            const int bx2 = idx % N;
            const int by2 = (idx / N) % N;
            const int bz2 = idx / (N * N);
            chunk.markLeafPlayerPlaced(bx2, by2, bz2);
        }
    }

    std::fclose(f);
    return true;
}
