#include <Entity/PhysicsEntity/CameraEntity/Player/Player.hpp>
#include <ItemContainer/ItemContainer.hpp>
#include <ItemStack/ItemStack.hpp>
#include <Log/Log.hpp>
#include <Utilities/FixedPoint.hpp>
#include <VoxelSprite/VoxelSprite.hpp>
#include <WorldIndex/WorldIndex.hpp>
#include <WorldMigrator/WorldMigrator.hpp>
#include <string>
#include <vector>

std::vector<WorldMigrator::Migration> WorldMigrator::migrations;

std::vector<uint8_t>& WorldMigrator::getFile(const std::string &path)
{
    if (plan.count(path) != 1)
    {
        store.read(path, plan[path]);
    }

    return plan[path];
}

void WorldMigrator::addMigration(Version sourceVersion, MigrationCallback migrate)
{
    migrations.push_back({sourceVersion, migrate});
}

WorldMigrator::WorldMigrator(AtomicDataStore &store, AtomicDataStore::Plan &plan) :
    store(store),
    plan(plan)
{}

void WorldMigrator::migrate()
{
    auto &windexBytes = getFile("windex.dat");
    WorldIndex::Signature sig;

    if (windexBytes.size() < sizeof(sig))
    {
        error = "This is not a Voxelverse world.";
        return;
    }

    memcpy(&sig, windexBytes.data(), sizeof(sig));

    if (memcmp(sig.magic, WORLD_INDEX_MAGIC, sizeof(sig.magic)) != 0)
    {
        error = "This is not a Voxelverse world.";
        return;
    }

    if (sig.gameVersion == Version::ofThisBuild)
    {
        // No migrations are necessary.
        return;
    }

    auto migration = std::find_if(migrations.begin(), migrations.end(), [&](const Migration &m)
    {
        return m.sourceVersion == sig.gameVersion;
    });

    if (migration == migrations.end())
    {
        error = formatString("No migration found for version $+", sig.gameVersion);
        return;
    }

    for (; migration != migrations.end(); migration++)
    {
        Log::info(formatString("Applying migrations for $+", migration->sourceVersion));
        if (!migration->migrate(*this))
        {
            error = "Migration failed.";
            return;
        }
    }

    sig.gameVersion = Version::ofThisBuild;

    memcpy(windexBytes.data(), &sig, sizeof(sig));
}

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

void WorldMigrator::initMigrations()
{
    // Migrations must be listed in order such that each migration migrates the
    // data from its source version to the source version of the *next* migration.
    //
    // Migration begins at the migrator defined for the version given in the world
    // index file. It *and all subsequent migrations* will be applied in order.
    // At the end, the world index file will have its version set to the current
    // version (`Version::ofThisBuild`).
    //
    // This means that if you are running a development build ("version 0.0.0"), it
    // will still work, because the last migrator should migrate from the previous
    // game version to the current one, regardless of whether we call it 0.0.0 or its
    // real name.

    // Migration: 0.0.15 -> 0.0.16
    addMigration({0, 0, 15}, [](WorldMigrator &wm) -> bool
    {
        // Scan for all player files.
        std::vector<std::string> allFiles = wm.store.getAllFiles();

        for (const std::string &file : allFiles)
        {
            if (file.substr(0, strlen("players/")) == "players/" || file == "singleplayer.dat")
            {
                // Convert from the old format with a legacy floating-point velocity field.
                auto &bytes = wm.getFile(file);

                wfvec3 pos(wfixed(0.0));
                wfvec3 vel(wfixed(0.0));
                glm::vec3 legacyVel;
                float phi, theta;
                std::vector<ItemStack> legacyStacks;
                std::string name;
                VoxelSprite sprite;
                std::unordered_map<std::string, uint64_t> milestoneBitmaps;
                bool crouching;
                std::vector<Player::MapPin> mapPins;

                deserialize(
                    bytes.data(),
                    bytes.size(),
                    pos,
                    legacyVel,
                    phi, theta,
                    legacyStacks,
                    name,
                    sprite,
                    milestoneBitmaps,
                    crouching,
                    mapPins,
                    vel
                );

                if (!std::isnan(legacyVel.x))
                {
                    Log::info(formatString("Converting player file '$+': from floating-point format", file));

                    glm::dvec3 legacyPos;
                    vel = legacyVel;
                    deserialize(bytes.data(), bytes.size(), legacyPos);

                    pos = legacyPos;
                }
                else
                {
                    Log::info(formatString("Converting player file '$+': from fixed-point format (legacyVel=nan)", file));
                }

                bytes.clear();

                ItemContainer items;
                items
                    .withStore("inventory", 50)
                    .withStore("equipment", 5)
                    .withStore("trash", 1);
                
                ItemStore &inventory = items.store("inventory");
                for (size_t i = 0; i < 50; i++)
                {
                    if (i < legacyStacks.size())
                    {
                        inventory.setStackInSlot(i, legacyStacks[i]);
                    }
                }

                ItemStore &equipment = items.store("equipment");
                for (size_t i = 0; i < 5; i++)
                {
                    size_t legacyIndex = 50 + i;
                    if (legacyIndex < legacyStacks.size())
                    {
                        equipment.setStackInSlot(i, legacyStacks[legacyIndex]);
                    }
                }

                serializeInto(
                    bytes,
                    pos,
                    vel,
                    phi, theta,
                    items,
                    name,
                    sprite,
                    milestoneBitmaps,
                    crouching,
                    mapPins
                );
            }
        }

        return true;
    });

    // Migration 0.0.16 -> 0.0.17
    addMigration({0, 0, 16}, [](WorldMigrator &wm)
    {
        // No changes needed; this migration exists to bump the version.
        return true;
    });

    // Migration 0.0.17 -> 0.0.18
    addMigration({0, 0, 17}, [](WorldMigrator &wm)
    {
        // No changes needed; this migration exists to bump the version.
        return true;
    });

    // Migration 0.0.18 -> 0.0.19
    addMigration({0, 0, 18}, [](WorldMigrator &wm)
    {
        // No changes needed; this migration exists to bump the version.
        return true;
    });

    // Migration 0.0.19 -> 0.0.20
    addMigration({0, 0, 19}, [](WorldMigrator &wm)
    {
        // No changes needed; this migration exists to bump the version.
        return true;
    });

    // Migration 0.0.20 -> 0.0.21
    addMigration({0, 0, 20}, [](WorldMigrator &wm)
    {
        // No changes needed; this migration exists to bump the version.
        return true;
    });

    // Migration 0.0.21 -> 0.0.22
    addMigration({0, 0, 21}, [](WorldMigrator &wm)
    {
        // No changes needed; this migration exists to bump the version.
        return true;
    });

    // Migration 0.0.22 -> 0.0.23
    addMigration({0, 0, 22}, [](WorldMigrator &wm)
    {
        // No changes needed; this migration exists to bump the version.
        return true;
    });

    // Migration 0.0.23 -> 0.0.24
    addMigration({0, 0, 23}, [](WorldMigrator &wm)
    {
        // Scan for all "grid.dat" files.
        std::vector<std::string> allFiles = wm.store.getAllFiles();
        size_t minLen = strlen("wgrid/*/grid.dat");

        size_t totalOldSize = 0;
        size_t totalNewSize = 0;

        for (const std::string &file : allFiles)
        {
            if (file.size() >= minLen && file.substr(0, 6) == "wgrid/" && file.substr(file.size() - 9) == "/grid.dat")
            {
                // Extract coordinates from the path.
                std::vector<std::string_view> parts;
                std::string_view sv(file);

                size_t start = 0;
                for (;;)
                {
                    size_t pos = sv.find('/', start);
                    if (pos == std::string_view::npos)
                    {
                        parts.emplace_back(sv.substr(start));
                        break;
                    }

                    parts.emplace_back(sv.substr(start, pos - start));
                    start = pos + 1;
                }
    
                auto parseAxis = [](std::string_view s) -> int
                {
                    // s is like "x1696" or "y-544"
                    return std::stoi(std::string(s.substr(1)));
                };

                int x = parseAxis(parts[2]);
                int y = parseAxis(parts[3]);
                int z = parseAxis(parts[4]);

                // Read and deserialize the old format.
                std::vector<uint32_t> blockPalette;
                DenseArray blockGrid;
                std::array<uint8_t, 16 * 16 * 16> metadata;
                std::vector<glm::ivec3> monitorPositions;

                auto &content = wm.getFile(file);
                size_t oldSize = content.size();

                totalOldSize += oldSize;

                deserialize(
                    content.data(), content.size(),
                    blockPalette,
                    blockGrid,
                    metadata,
                    monitorPositions
                );

                // Build the chunk from old data.
                Chunk chunk(nullptr, glm::ivec3(x, y, z));
                chunk.markLoadingComplete();
                chunk.monitorPositions = monitorPositions;

                glm::ivec3 offset;
                for (offset.z=0; offset.z<16; offset.z++)
                {
                    for (offset.y=0; offset.y<16; offset.y++)
                    {
                        for (offset.x=0; offset.x<16; offset.x++)
                        {
                            size_t index = 16 * 16 * offset.z + 16 * offset.y + offset.x;
                            uint32_t paletteIndex = blockGrid.get(index);

                            int32_t blockId = paletteIndex;
                            if (blockPalette.size() != 0)
                            {
                                blockId = blockPalette[paletteIndex];
                            }

                            uint8_t meta = metadata[index];

                            chunk.setBlockAndMeta(offset, blockId, meta);
                        }
                    }
                }

                content.clear();

                auto &newContent = wm.getFile(chunk.getGridFilePath());
                newContent.clear();
                chunk.saveInto(newContent);

                Log::info(
                    formatString(
                        "Converted '$+' ($+ bytes) -> '$+' ($+ bytes)",
                        file,
                        oldSize,
                        chunk.getGridFilePath(),
                        newContent.size()
                    )
                );

                totalNewSize += newContent.size();
            }
        }

        Log::info(formatString("Chunk conversion total: $+ bytes -> $+ bytes", totalOldSize, totalNewSize));

        return true;
    });

    // Migration 0.0.24 -> 0.0.25
    addMigration({0, 0, 24}, [](WorldMigrator &wm)
    {
        // No changes needed; this migration exists to bump the version.
        return true;
    });
}