#include <AssetManager/AssetManager.hpp>
#include <BlockSpriteDesigner/BlockSpriteDesigner.hpp>
#include <BoundingBox/BoundingBox.hpp>
#include <Utilities/Utilities.hpp>
#include <filesystem>

void BlockSpriteDesigner::addScriptFuncs()
{
    engine.addFunc("debug", [](std::string line)
    {
        Log::debug(formatString("[Design script] $+", line));
    });

    engine.addFunc("vsp_new", [this](int xsize, int ysize, int zsize)
    {
        int id = nextSpriteId++;
        voxelSprites[id] = std::make_shared<VoxelSprite>(glm::ivec3(xsize, ysize, zsize));
        return id;
    });

    engine.addFunc("vsp_clone", [this](int oldSpriteId)
    {
        if (voxelSprites.count(oldSpriteId) == 0)
        {
            Log::error("vsp_clone() called with an invalid sprite ID.");
            return 0;
        }

        int id = nextSpriteId++;
        voxelSprites[id] = std::make_shared<VoxelSprite>(*voxelSprites[oldSpriteId]);
        return id;
    });

    engine.addFunc("vsp_fill", [this](int spriteId, VoxelSprite::Color color)
    {
        if (voxelSprites.count(spriteId) == 0)
        {
            Log::error("vsp_fill() called with an invalid sprite ID.");
            return;
        }

        voxelSprites[spriteId]->fill(color);
    });

    engine.addFunc("vsp_dump", [this](int spriteId, std::string name)
    {
        bool isNameValid = !name.empty() && name[0] != '.' && std::all_of(name.begin(), name.end(), [](unsigned char c)
        {
            return std::isalnum(c) || c == '_' || c == '-' || c == '.';
        });

        if (!isNameValid)
        {
            Log::error(formatString("Invalid name passed to vsp_dump(): '$+'.", name));
        }

        if (voxelSprites.count(spriteId) == 0)
        {
            Log::error("vsp_dump() called with an invalid sprite ID.");
            return;
        }

        std::filesystem::create_directories(dumpDir);
        voxelSprites[spriteId]->writeToVSP(formatString("$+/$+", dumpDir, name));
    });

    engine.addFunc("vsp_set_voxel", [this](int spriteId, int x, int y, int z, VoxelSprite::Color color)
    {
        if (voxelSprites.count(spriteId) == 0)
        {
            Log::error("vsp_set_voxel() called with an invalid sprite ID.");
            return;
        }

        voxelSprites[spriteId]->at(glm::ivec3(x, y, z)) = color;
    });

    engine.addFunc("vsp_get_voxel", [this](int spriteId, int x, int y, int z) -> VoxelSprite::Color
    {
        if (voxelSprites.count(spriteId) == 0)
        {
            Log::error("vsp_get_voxel() called with an invalid sprite ID.");
            return {};
        }

        return voxelSprites[spriteId]->at(glm::ivec3(x, y, z));
    });

    engine.addFunc("set_collision_mask", [this](std::string stateName, int spriteId)
    {
        std::shared_ptr<BlockSprite> &sprite = exportedSprites[stateName];
        if (sprite == nullptr)
        {
            sprite = std::make_shared<BlockSprite>();
        }

        if (voxelSprites.count(spriteId) == 0)
        {
            Log::error("set_collision_mask() called with an invalid sprite ID.");
            return;
        }

        sprite->collisionMask = *voxelSprites[spriteId];
    });

    engine.addFunc("set_frame_duration", [this](std::string stateName, int duration)
    {
        std::shared_ptr<BlockSprite> &sprite = exportedSprites[stateName];
        if (sprite == nullptr)
        {
            sprite = std::make_shared<BlockSprite>();
        }

        sprite->frameDuration = duration;
    });

    engine.addFunc("export_frame", [this](std::string stateName, std::unordered_map<std::string, int> maps)
    {
        std::shared_ptr<BlockSprite> &sprite = exportedSprites[stateName];
        if (sprite == nullptr)
        {
            sprite = std::make_shared<BlockSprite>();
        }

        BlockSprite::Frame &frame = sprite->addFrame();
        if (maps.count("colormap") == 0)
        {
            Log::warn(
                formatString(
                    "export_frame(): frame added to state '$+' of '$+' without a colormap.",
                    stateName,
                    blockPureName
                )
            );
        }

        for (const auto &[mapName, spriteId] : maps)
        {
            if (voxelSprites.count(spriteId) == 0)
            {
                Log::error(formatString("export_frame(): received map '$+' with invalid sprite ID.", mapName));
                continue;
            }

            VoxelSprite sprite = *voxelSprites[spriteId];

            if (mapName == "colormap")
            {
                frame.colorMap = sprite;
            }
            else if (mapName == "biasmap")
            {
                frame.biasMap = sprite;
            }
            else if (mapName == "refractmap")
            {
                frame.refractMap = sprite;
            }
            else if (mapName == "lightspecmap")
            {
                frame.lightSpecMap = sprite;
            }
            else
            {
                Log::error(formatString("export_frame(): received unknown map name '$+'.", mapName));
            }
        }
    });

    engine.addFunc("vsp_from_assets", [this](std::string relPath) -> int
    {
        std::string fullPath = assets.resolve(relPath);
        std::shared_ptr<VoxelSprite> sprite = std::make_shared<VoxelSprite>();
        if (!sprite->loadFromVSP(fullPath))
        {
            Log::error(formatString("Failed to load VSP file requested by script: '$+'.", fullPath));
            return 0;
        }

        int id = nextSpriteId++;
        voxelSprites[id] = sprite;
        return id;
    });

    engine.addFunc("vsp_color_remap", [this](int spriteId, std::unordered_map<int, int> translationTable)
    {
        if (voxelSprites.count(spriteId) == 0)
        {
            Log::error("vsp_color_remap() called with an invalid sprite ID.");
            return;
        }

        voxelSprites[spriteId]->colorRemap(translationTable);
    });

    engine.addFunc(
        "vsp_color_remap_with_default",
        [this](int spriteId, std::unordered_map<int, int> translationTable, int defaultColor)
        {
            if (voxelSprites.count(spriteId) == 0)
            {
                Log::error("vsp_color_remap_with_default() called with an invalid sprite ID.");
                return;
            }

            voxelSprites[spriteId]->colorRemapWithDefault(translationTable, defaultColor);
        }
    );

    engine.addFunc("vsp_delete", [this](int spriteId)
    {
        auto it = voxelSprites.find(spriteId);
        if (it != voxelSprites.end())
        {
            voxelSprites.erase(it);
        }
    });

    engine.addFunc("vsp_pluck_sides", [this](int spriteId, int sideBitmap, VoxelSprite::Color color)
    {
        if (voxelSprites.count(spriteId) == 0)
        {
            Log::error("vsp_pluck_sides() called with an invalid sprite ID.");
            return;
        }

        voxelSprites[spriteId]->pluckSides(sideBitmap, color);
    });

    engine.addFunc("vsp_from_cropped", [this](int spriteId, int x, int y, int z, int xsz, int ysz, int zsz)
    {
        if (voxelSprites.count(spriteId) == 0)
        {
            Log::error("vsp_from_cropped() called with an invalid sprite ID.");
            return 0;
        }

        int id = nextSpriteId++;
        voxelSprites[id] = voxelSprites[spriteId]->cropped(glm::ivec3(x, y, z), glm::ivec3(xsz, ysz, zsz));
        return id;
    });

    engine.addFunc("vsp_get_size", [this](int spriteId) -> glm::ivec3
    {
        if (voxelSprites.count(spriteId) == 0)
        {
            Log::error("vsp_get_size() called with an invalid sprite ID.");
            return glm::ivec3(0);
        }

        return voxelSprites[spriteId]->getSize();
    });

    engine.addFunc("vsp_fill_box", [this](int spriteId, std::unordered_map<int, int> sideBounds, VoxelSprite::Color color)
    {
        BoundingBox<int> box;
        for (auto &[sideNum, value] : sideBounds)
        {
            box.getSideValue(Side(sideNum)) = value;
        }

        voxelSprites[spriteId]->fillBox(box, color);
    });

    engine.addFunc("vsp_merge", [this](int underSpriteId, int overSpriteId)
    {
        if (voxelSprites.count(underSpriteId) == 0)
        {
            Log::error("vsp_merge() called with an invalid sprite ID.");
            return;
        }

        if (voxelSprites.count(overSpriteId) == 0)
        {
            Log::error("vsp_merge() called with an invalid sprite ID.");
            return;
        }

        voxelSprites[underSpriteId]->merge(*voxelSprites[overSpriteId]);
    });
}

BlockSpriteDesigner::BlockSpriteDesigner(std::string blockPureName) : blockPureName(blockPureName)
{
    blockDir = formatString("blocksprites/$+", blockPureName);
    scriptPath = formatString("$+/$+Design.lua", blockDir, blockPureName);
    dumpDir = formatString("dumps/from-design/$+", blockPureName);

    addScriptFuncs();

    engine.push(blockPureName);
    engine.setGlobal("BLOCK_PURE_NAME");
}

void BlockSpriteDesigner::execute()
{
    if (!assets.exists(scriptPath))
    {
        return;
    }

    Log::info(formatString("Executing block sprite design script '$+'...", scriptPath));
    if (!engine.execFromFile(assets.resolve(scriptPath)))
    {
        crash(formatString("Failed to execute design script: $+", engine.pop<std::string>()));
    }
}

std::shared_ptr<BlockSprite> BlockSpriteDesigner::tryGetSprite(std::string exportName)
{
    if (exportedSprites.count(exportName) == 0)
    {
        return nullptr;
    }

    return exportedSprites[exportName];
}

int BlockSpriteDesigner::addSprite(std::shared_ptr<VoxelSprite> vsp)
{
    int id = nextSpriteId++;
    voxelSprites[id] = vsp;
    return id;
}

void BlockSpriteDesigner::useFallbackScript(std::string path)
{
    if (!assets.exists(scriptPath))
    {
        scriptPath = formatString("spritegen/$+", path);
    }
}