#include <AsyncWorldCloner/AsyncWorldCloner.hpp>
#include <AtomicDataStore/AtomicDataStore.hpp>
#include <Log/Log.hpp>
#include <Serialize/Base.hpp>
#include <Utilities/Async.hpp>
#include <Utilities/Utilities.hpp>
#include <WorldIndex/WorldIndex.hpp>
#include <WorldMigrator/WorldMigrator.hpp>
#include <cstdint>

AsyncWorldCloner::AsyncWorldCloner(const std::string &oldPath, const std::string &newPath)
{
    oldStore.open(oldPath);
    if (!oldStore.getError().empty())
    {
        error = formatString("Failed to open the old world file: $+", oldStore.getError());
        done = true;
        return;
    }

    std::vector<uint8_t> &windexData = plan["windex.dat"];
    if (!oldStore.read("windex.dat", windexData))
    {
        error = "This is not a Voxelverse world.";
        done = true;
        return;
    }

    if (!newStore.file.create(newPath))
    {
        error = "Failed to create the new world file.";
        done = true;
        return;
    }

    if (!newStore.file.lock())
    {
        newStore.file.close();
        error = "Failed to lock the new world file.";
        done = true;
        return;
    }

    newStore.nextIndexBlockIndex = 0;
    newStore.nextIndexGeneration = 0;
    newStore.freeRegionTable = oldStore.freeRegionTable;
    newStore.regionsToRelease = oldStore.regionsToRelease;
    newStore.rootHashTablePos = oldStore.rootHashTablePos;
    newStore.rootHashTableSize = oldStore.rootHashTableSize;
    newStore.headerNeedsWriting = true;
}

void AsyncWorldCloner::start(bool enableCheats, std::optional<WorldAvatar> avatar, std::string intoSinglePlayer)
{
    dispatchTask(
        [this, enableCheats, avatar, intoSinglePlayer]()
        {
            static constexpr size_t blockSize = 0x200000;

            std::vector<uint8_t> buffer(blockSize);

            size_t oldSize = oldStore.file.size();
            size_t offset = sizeof(AtomicDataStore::MainHeader) + 2 * sizeof(AtomicDataStore::IndexBlock);
            
            while (offset < oldSize)
            {
                size_t size = std::min(blockSize, oldSize - offset);
                if (!oldStore.file.read(offset, buffer.data(), size))
                {
                    error = "Failed to read the old world file.";
                    return;
                }

                if (!newStore.file.write(offset, buffer.data(), size))
                {
                    error = "Failed to write the new world file.";
                    return;
                }

                offset += size;
                progress = static_cast<float>(offset) / oldSize;
            }

            newStore.file.sync();

            WorldMigrator migrator(newStore, plan);
            migrator.migrate();

            if (!migrator.getError().empty())
            {
                error = formatString("Failed to migrate the world: $+", migrator.getError());
                done = true;
                return;
            }

            std::vector<uint8_t> &windexData = plan["windex.dat"];

            WorldIndex index;
            index.deserialize(windexData.data(), windexData.size(), error);

            if (!error.empty())
            {
                done = true;
                return;
            }

            index.generateNewID();
            
            if (avatar.has_value())
            {
                index.setAvatar(*avatar);
            }

            if (enableCheats)
            {
                index.enableCheats();
            }

            windexData.clear();
            serializeInto(windexData, index);

            if (!intoSinglePlayer.empty())
            {
                // Move the file from `players/` into `singleplayer.dat`.
                std::string path = formatString("players/$+.dat", intoSinglePlayer);

                auto &playerData = plan[path];
                if (playerData.empty())
                {
                    if (!oldStore.read(path, playerData))
                    {
                        error = formatString("Player '$+' does not exist.", intoSinglePlayer);
                        done = true;
                        return;
                    }
                }

                Log::info(formatString("'$+' converted to 'singleplayer.dat'", path));

                auto &singlePlayerData = plan["singleplayer.dat"];
                singlePlayerData = playerData;
                
                // Now clear all player files.
                std::vector<std::string> allFiles = oldStore.getAllFiles();
                for (const std::string &file : allFiles)
                {
                    if (file.substr(0, strlen("players/")) == "players/")
                    {
                        Log::info(formatString("Clearing player file '$+'", file));
                        plan[file].clear();
                    }
                }
            }

            newStore.update(plan, 0);
            done = true;
        }
    );
}

float AsyncWorldCloner::getProgress()
{
    return progress;
}

bool AsyncWorldCloner::isDone()
{
    return done;
}

const std::string& AsyncWorldCloner::getError()
{
    return error;
}