#include "File.hpp"

#include <filesystem>
#include <optional>

using namespace world::server;

#define REMOVE_CORRUPTED 1

FileRegion::FileRegion(const std::string &folderPath, const area_<region_pos> &pos) {
    path = folderPath + '/' + std::to_string(pos.first.index) + '.' + std::to_string(pos.second.x) + '.' +
        std::to_string(pos.second.y) + '.' + std::to_string(pos.second.z) + ".map";

    load();
}
FileRegion::~FileRegion() {
    file.close();
}

void FileRegion::load() {
    std::unique_lock lock(mutex);

    file.open(path, std::ios::in | std::ios::binary);
    if(!file.good()) {
        return;
    }
    // Read header
    uint16_t chunkCount; //NOTE: pretty useless
    file.read(reinterpret_cast<char *>(&chunkCount), sizeof(chunkCount));

    while (!file.eof()) {
        // Read pos
        region_chunk_pos pos;
        file.read(reinterpret_cast<char *>(&pos.x), sizeof(region_chunk_pos::value_type));
        file.read(reinterpret_cast<char *>(&pos.y), sizeof(region_chunk_pos::value_type));
        file.read(reinterpret_cast<char *>(&pos.z), sizeof(region_chunk_pos::value_type));

        std::optional<Voxel> avg;
        Flags flags = Flags::ZERO;
        file.read(reinterpret_cast<char *>(&flags), 1);
        if (flags & Flags::HAS_AVERAGE) {
            Voxel v;
            file.read(reinterpret_cast<char *>(&v), sizeof(v));
            avg = v;
        }

        // Read size
        uint16_t size = 0;
        if (!(flags & Flags::EMPTY)) {
            file.read(reinterpret_cast<char *>(&size), sizeof(size));
        }

        // Ignore content
        if(!index.emplace(pos, node{avg, size, file.tellg()}).second) {
            LOG_E("Duplicated chunk: " << path << ":" << (int)pos.x << "." << (int)pos.y << "." << (int)pos.z);
        }
        file.ignore(size);
        file.peek();
    }

    if(file.bad()) {
        LOG_E("region corrupted read " << path);
    }

    assert(index.size() == chunkCount);
}
bool FileRegion::read(const region_chunk_pos& pos, const zstd::read_ctx& ctx, std::vector<char>& out) {
    std::unique_lock lock(mutex);

    const auto it = index.find(pos);
    if (it == index.end() || it->second.size == 0)
        return false;

    std::vector<char> in;
    in.resize(it->second.size);
    file.seekg(it->second.offset);
    file.read(in.data(), in.size());

    if (auto err = ctx.decompress(in, out)) {
        LOG_E("Corrupted region chunk: " << path << ":" << (int)pos.x << "." << (int)pos.y << "." << (int)pos.z << " "
            << err.value());
#ifdef REMOVE_CORRUPTED
            LOG_W("Removing");
            index.erase(it);
            lock.unlock();
            save(std::nullopt);
#endif
        return false;
    }
    return true;
}
void FileRegion::write(const region_chunk_pos& pos, const zstd::write_ctx& ctx, const std::string_view& in, const std::optional<world::Voxel>& avg) {
    std::unique_ptr<std::vector<char>> buffer = nullptr;
    if (!in.empty()) {
        buffer = std::make_unique<std::vector<char>>();
        if (auto err = ctx.compress(in, *buffer)) {
            LOG_E("Corrupted chunk save: " << path << ":" << (int)pos.x << "." << (int)pos.y << "." << (int)pos.z << " "
                << err.value());
            return;
        }
    }
    save(std::make_optional(to_save{pos, std::move(buffer), avg}));
}

void FileRegion::save(std::optional<to_save> added) {
    std::unique_lock lock(mutex);

    const auto tmpPath = path + ".tmp";

    std::ofstream tmpFile(tmpPath, std::ios::out | std::ios::binary);
    if (!tmpFile.good()) {
        LOG_E("Corrupted region path: " << tmpPath);
        return;
    }

    { // Write header
        uint16_t size = index.size() + (added.has_value() ? 1 : 0);
        tmpFile.write(reinterpret_cast<char *>(&size), sizeof(size));
    }

    std::vector<char> tmp;
    for(const auto& chunk: index) {
        { // Write pos
            region_chunk_pos pos = chunk.first;
            tmpFile.write(reinterpret_cast<char *>(&pos.x), sizeof(region_chunk_pos::value_type));
            tmpFile.write(reinterpret_cast<char *>(&pos.y), sizeof(region_chunk_pos::value_type));
            tmpFile.write(reinterpret_cast<char *>(&pos.z), sizeof(region_chunk_pos::value_type));
        }

        // Write average if present
        Flags flags = Flags::ZERO;
        if (chunk.second.average.has_value())
            flags = (Flags)(flags | Flags::HAS_AVERAGE);
        if (chunk.second.size == 0)
            flags = (Flags)(flags | Flags::EMPTY);
        tmpFile.write(reinterpret_cast<const char *>(&flags), 1);
        if (flags & Flags::HAS_AVERAGE) {
            Voxel v = chunk.second.average.value();
            tmpFile.write(reinterpret_cast<char *>(&v), sizeof(v));
        }

        if (!(flags & Flags::EMPTY)) {
            // Write size
            auto size = chunk.second.size;
            tmpFile.write(reinterpret_cast<char *>(&size), sizeof(size));

            // Write content
            tmp.resize(size);
            file.seekg(chunk.second.offset);
            file.read(tmp.data(), size);
            tmpFile.write(tmp.data(), size);
        }
    }
    if(added.has_value()) {
        { // Write pos
            region_chunk_pos pos = added.value().pos;
            tmpFile.write(reinterpret_cast<char *>(&pos.x), sizeof(region_chunk_pos::value_type));
            tmpFile.write(reinterpret_cast<char *>(&pos.y), sizeof(region_chunk_pos::value_type));
            tmpFile.write(reinterpret_cast<char *>(&pos.z), sizeof(region_chunk_pos::value_type));
        }

        // Write average if present
        Flags flags = Flags::ZERO;
        if (added.value().average.has_value())
            flags = (Flags)(flags | Flags::HAS_AVERAGE);
        if (added.value().data == nullptr)
            flags = (Flags)(flags | Flags::EMPTY);
        tmpFile.write(reinterpret_cast<const char *>(&flags), 1);
        if (flags & Flags::HAS_AVERAGE) {
            Voxel v = added.value().average.value();
            tmpFile.write(reinterpret_cast<char *>(&v), sizeof(v));
        }

        if (!(flags & Flags::EMPTY)) {
            // Write size
            auto size = added.value().data->size();
            tmpFile.write(reinterpret_cast<char *>(&size), sizeof(size));

            // Write content
            tmpFile.write(added.value().data->data(), size);
        }
    }

    if (!tmpFile.good()) {
        LOG_E("Region corrupted write " << tmpPath);
        tmpFile.close();
        return;
    }
    tmpFile.close();

    index.clear();
    file.close();
    std::filesystem::rename(tmpPath, path);
    load();
}