#include <AssetManager/AssetManager.hpp>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Serialize/Serialize.hpp>
#include <cstring>
#include <fstream>

#include <VoxelSprite/VoxelSprite.hpp>
#include <Log/Log.hpp>

#define DECL_VOXEL_SPRITE(name) VoxelSprite VoxelSprite::name
#include <VoxelSprite/VoxelSpriteList.hpp>

VoxelSprite::VoxelSprite(glm::ivec3 size) : size(size), colors(size.x * size.y * size.z) {}

glm::ivec3 VoxelSprite::getSize()
{
    return size;
}

VoxelSprite::Color& VoxelSprite::at(int x, int y, int z)
{
    return colors[size.x * size.y * z + size.x * y + x];
}

VoxelSprite::Color& VoxelSprite::at(glm::ivec3 coords)
{
    return at(coords.x, coords.y, coords.z);
}

void VoxelSprite::clear(glm::ivec3 newSize)
{
    size = newSize;
    colors.clear();
    colors.resize(size.x * size.y * size.z);
}

bool VoxelSprite::isSingularity()
{
    return size.x * size.y * size.z == 0;
}

bool VoxelSprite::loadFromVSP(std::string spriteFileName)
{
    std::ifstream ifs(spriteFileName, std::ios::ate | std::ios::binary);
    if (!ifs.good())
    {
        return false;
    }

    std::vector<uint8_t> data(ifs.tellg());
    ifs.seekg(0);

    ifs.read((char*) data.data(), data.size());
    if (!ifs.good())
    {
        return false;
    }

    ifs.close();

    if (deserialize(data.data(), data.size(), *this) != data.size())
    {
        return false;
    }

    return true;
}

void VoxelSprite::loadFromAssets(std::string name)
{
    Log::info(formatString("Loading sprite '$+' ...", name));
    std::string spriteFileName = assets.resolve(formatString("sprites/$+.vsp", name));
    
    if (!loadFromVSP(spriteFileName))
    {
        crash(formatString("Failed to load voxel sprite file '$+'!", spriteFileName));
    }
}

int VoxelSprite::getVolume()
{
    return size.x * size.y * size.z;
}

void VoxelSprite::writeToVSP(std::string filename)
{
    std::vector<uint8_t> bytes;
    serializeInto(bytes, *this);

    std::ofstream ofs(filename, std::ios::binary);
    ofs.write((char*) bytes.data(), bytes.size());
    ofs.close();
}

void VoxelSprite::fill(Color color)
{
    for (Color &current : colors)
    {
        current = color;
    }
}

void VoxelSprite::colorRemap(const std::unordered_map<int, int> &translationTable)
{
    for (Color &color : colors)
    {
        auto it = translationTable.find(color.asInt);
        if (it != translationTable.end())
        {
            color.asInt = it->second;
        }
    }
}

void VoxelSprite::colorRemapWithDefault(const std::unordered_map<int, int> &translationTable, int defaultColor)
{
    for (Color &color : colors)
    {
        auto it = translationTable.find(color.asInt);
        if (it != translationTable.end())
        {
            color.asInt = it->second;
        }
        else
        {
            color.asInt = defaultColor;
        }
    }
}

void VoxelSprite::pluckSides(uint8_t sideBitmap, Color color)
{
    glm::ivec3 pos;
    for (pos.z = 0; pos.z < size.z; pos.z++)
    {
        for (pos.y = 0; pos.y < size.y; pos.y++)
        {
            for (pos.x = 0; pos.x < size.x; pos.x++)
            {
                Color &current = at(pos);
                if (current.alpha != 0)
                {
                    continue;
                }

                uint8_t thisVoxelSides = 0;

                if (pos.x == 0) thisVoxelSides |= (1 << 0);
                if (pos.x == size.x-1) thisVoxelSides |= (1 << 1);
                if (pos.y == 0) thisVoxelSides |= (1 << 2);
                if (pos.y == size.y-1) thisVoxelSides |= (1 << 3);
                if (pos.z == 0) thisVoxelSides |= (1 << 4);
                if (pos.z == size.z-1) thisVoxelSides |= (1 << 5);

                if ((sideBitmap & thisVoxelSides) == thisVoxelSides)
                {
                    current = color;
                }
            }
        }
    }
}

std::shared_ptr<VoxelSprite> VoxelSprite::cropped(glm::ivec3 start, glm::ivec3 size)
{
    std::shared_ptr<VoxelSprite> result = std::make_shared<VoxelSprite>(size);

    if (glm::lessThan(start, glm::ivec3(0)) != glm::bvec3(false) || glm::greaterThan(size, this->size + start) != glm::bvec3(false))
    {
        Log::error("VoxelSprite::cropped: value out of bounds");
        return result;
    }

    glm::ivec3 offset;
    for (offset.z = 0; offset.z < size.z; offset.z++)
    {
        for (offset.y = 0; offset.y < size.y; offset.y++)
        {
            for (offset.x = 0; offset.x < size.x; offset.x++)
            {
                result->at(offset) = at(offset + start);
            }
        }
    }

    return result;
}

void VoxelSprite::fillBox(const BoundingBox<int> &box, Color color)
{
    glm::ivec3 pos;
    for (pos.z = box.z0; pos.z < box.z1; pos.z++)
    {
        for (pos.y = box.y0; pos.y < box.y1; pos.y++)
        {
            for (pos.x = box.x0; pos.x < box.x1; pos.x++)
            {
                at(pos) = color;
            }
        }
    }
}

void VoxelSprite::computeBoundingBoxes(std::vector<BoundingBox<double>> &boxes)
{
    glm::ivec3 size = getSize();
    std::vector<std::vector<std::vector<bool>>> visited(size.x, std::vector<std::vector<bool>>(size.y, std::vector<bool>(size.z, false)));

    auto isVisited = [&visited](glm::ivec3 pos) -> bool
    {
        return visited[pos.x][pos.y][pos.z];
    };

    auto markVisited = [&visited](glm::ivec3 pos)
    {
        visited[pos.x][pos.y][pos.z] = true;
    };

    glm::ivec3 startPos;
    for (startPos.x = 0; startPos.x < size.x; ++startPos.x)
    {
        for (startPos.y = 0; startPos.y < size.y; ++startPos.y)
        {
            for (startPos.z = 0; startPos.z < size.z; ++startPos.z)
            {
                if (isFullVoxel(startPos) && !isVisited(startPos))
                {
                    // Start a new bounding box.
                    glm::ivec3 endPos = startPos;

                    // Expand xEnd.
                    while (endPos.x + 1 < size.x && isFullVoxel({endPos.x + 1, endPos.y, endPos.z}) && !isVisited({endPos.x + 1, endPos.y, endPos.z}))
                    {
                        ++endPos.x;
                    }

                    // Expand yEnd.
                    bool canExpandY = true;
                    while (canExpandY && endPos.y + 1 < size.y)
                    {
                        for (int xPos = startPos.x; xPos <= endPos.x; ++xPos)
                        {
                            if (!isFullVoxel({xPos, endPos.y + 1, startPos.z}) || isVisited({xPos, endPos.y + 1, startPos.z}))
                            {
                                canExpandY = false;
                                break;
                            }
                        }
                        if (canExpandY)
                        {
                            ++endPos.y;
                        }
                    }

                    // Expand zEnd.
                    bool canExpandZ = true;
                    while (canExpandZ && endPos.z + 1 < size.z)
                    {
                        for (int xPos = startPos.x; xPos <= endPos.x; ++xPos)
                        {
                            for (int yPos = startPos.y; yPos <= endPos.y; ++yPos)
                            {
                                if (!isFullVoxel({xPos, yPos, endPos.z + 1}) || isVisited({xPos, yPos, endPos.z + 1}))
                                {
                                    canExpandZ = false;
                                    break;
                                }
                            }
                            if (!canExpandZ)
                            {
                                break;
                            }
                        }

                        if (canExpandZ)
                        {
                            ++endPos.z;
                        }
                    }

                    // Mark all voxels in this bounding box as visited.
                    for (int xPos = startPos.x; xPos <= endPos.x; ++xPos)
                    {
                        for (int yPos = startPos.y; yPos <= endPos.y; ++yPos)
                        {
                            for (int zPos = startPos.z; zPos <= endPos.z; ++zPos)
                            {
                                markVisited({xPos, yPos, zPos});
                            }
                        }
                    }

                    // Create the bounding box.
                    boxes.emplace_back(BoundingBox<double>{
                        static_cast<double>(startPos.x), static_cast<double>(startPos.y), static_cast<double>(startPos.z),
                        static_cast<double>(endPos.x + 1), static_cast<double>(endPos.y + 1), static_cast<double>(endPos.z + 1)
                    });
                }
            }
        }
    }
}

bool VoxelSprite::isFullVoxel(glm::ivec3 pos)
{
    return at(pos).alpha != 0;
}

Bitmap VoxelSprite::getSideProfileBitmap()
{
    Bitmap bitmap(glm::uvec2(8));

    glm::uvec2 pos;
    for (pos.y = 0; pos.y < 8; pos.y++)
    {
        for (pos.x = 0; pos.x < 8; pos.x++)
        {
            glm::ivec3 voxelPos(pos.x, 0, 7 - pos.y);
            for (voxelPos.y = 0; voxelPos.y < 8; voxelPos.y++)
            {
                Color voxel = at(voxelPos);
                if (voxel.alpha != 0)
                {
                    bitmap.setColor(pos, voxel);
                    break;
                }
            }
        }
    }

    return bitmap;
}

void VoxelSprite::merge(VoxelSprite &over)
{
    if (over.size != size)
    {
        crash(formatString("Trying to merge voxel sprites with mismatched sizes: $+ and $+", size, over.size));
    }

    glm::ivec3 pos;
    for (pos.z = 0; pos.z < size.z; pos.z++)
    {
        for (pos.y = 0; pos.y < size.y; pos.y++)
        {
            for (pos.x = 0; pos.x < size.x; pos.x++)
            {
                auto &color = over.at(pos);
                if (color.alpha == 0xFF)
                {
                    at(pos) = color;
                }
            }
        }
    }
}

VoxelSprite VoxelSprite::fromText(Font &font, std::string text, Color bg, Color fg)
{
    glm::ivec2 textSize = GraphicsContext::measureText(font, text);

    glm::ivec3 spriteSize = glm::ivec3(textSize.x + 2 * voxelTextMargin, 2, textSize.y + 2 * voxelTextMargin);
    VoxelSprite sprite(spriteSize);
    sprite.fill(bg);

    GraphicsContext::layoutText(
        font,
        text,
        [](glm::vec4) {},
        [&sprite, fg, spriteSize](Font::Sheet &sheet, glm::ivec2 pos, glm::ivec2 sheetBase, glm::ivec2 size, size_t)
        {
            pos += glm::ivec2(voxelTextMargin);

            glm::ivec2 offset;
            for (offset.y = 0; offset.y < size.y; offset.y++)
            {
                for (offset.x = 0; offset.x < size.x; offset.x++)
                {
                    if (sheet.bitmap.getColor(sheetBase + offset).alpha != 0)
                    {
                        glm::ivec3 spritePos = glm::uvec3(spriteSize.x - (pos.x + offset.x) - 1, 1, spriteSize.z - (pos.y + offset.y) - 1);
                        sprite.at(spritePos) = fg;
                    }
                }
            }
        },
        false
    );

    return sprite;
}