#ifndef VOXEL_SPRITE_HPP_
#define VOXEL_SPRITE_HPP_

#include <BoundingBox/BoundingBox.hpp>
#include <vector>
#include <Utilities/Extern.hpp>
#include <Serialize/Serialize.hpp>
#include <Utilities/RGBABytes.hpp>
#include <Bitmap/Bitmap.hpp>
#include <Font/Font.hpp>

/**
 * @brief Represents a voxel sprite (a 3D grid of colors).
 */
class VoxelSprite
{
public:
    typedef RGBABytes Color;

private:
    static constexpr int voxelTextMargin = 1;

    struct SpriteFile
    {
        char magic[3];
        uint8_t zero;
        uint32_t xsize;
        uint32_t ysize;
        uint32_t zsize;
        Color colors[];
    };

    glm::ivec3 size;
    std::vector<Color> colors;

public:
    VoxelSprite(glm::ivec3 size = glm::ivec3(0));

    glm::ivec3 getSize();
    VoxelSprite::Color& at(int x, int y, int z);
    VoxelSprite::Color& at(glm::ivec3 coords);
    void clear(glm::ivec3 newSize);
    bool isSingularity();
    bool loadFromVSP(std::string filename);
    void loadFromAssets(std::string name);
    int getVolume();
    void writeToVSP(std::string filename);
    void fill(Color color);
    void colorRemap(const std::unordered_map<int, int> &translationTable);
    void colorRemapWithDefault(const std::unordered_map<int, int> &translationTable, int defaultColor);
    void pluckSides(uint8_t sideBitmap, Color color);
    std::shared_ptr<VoxelSprite> cropped(glm::ivec3 start, glm::ivec3 size);
    void fillBox(const BoundingBox<int> &box, Color color);
    void computeBoundingBoxes(std::vector<BoundingBox<double>> &boxes);
    bool isFullVoxel(glm::ivec3 pos);
    Bitmap getSideProfileBitmap();
    void merge(VoxelSprite &over);

    static VoxelSprite fromText(Font &font, std::string text, Color bg, Color fg);

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

    friend struct SerialCodec<VoxelSprite>;
};

template<>
struct SerialCodec<VoxelSprite>
{
    static void encode(std::vector<uint8_t> &outBytes, const VoxelSprite &sprite)
    {
        // Allocate space for the serialized sprite, and output the size.
        int volume = sprite.size.x * sprite.size.y * sprite.size.z;
        size_t fileSize = sizeof(VoxelSprite::SpriteFile) + sizeof(VoxelSprite::Color) * volume;

        char *data = new char[fileSize];

        // Fill in the header.
        VoxelSprite::SpriteFile *file = (VoxelSprite::SpriteFile*) data;
        std::memcpy(file->magic, "VSP", 3);
        file->zero = 0;
        file->xsize = sprite.size.x;
        file->ysize = sprite.size.y;
        file->zsize = sprite.size.z;

        // Copy the colors.
        if (volume != 0)
        {
            std::memcpy(file->colors, &sprite.colors[0], sizeof(VoxelSprite::Color) * volume);
        }

        // And we are done.
        outBytes.insert(outBytes.end(), data, data + fileSize);
        delete[] data;
    }

    static size_t decode(const uint8_t *data, size_t size, VoxelSprite &sprite)
    {
        // Ensure we have at least a header.
        if (size < sizeof(VoxelSprite::SpriteFile))
        {
            return 0;
        }

        VoxelSprite::SpriteFile header;
        std::memcpy(&header, data, sizeof(VoxelSprite::SpriteFile));

        // Validate the header.
        if (std::memcmp(header.magic, "VSP", 3) != 0)
        {
            return 0;
        }

        if (header.zero != 0)
        {
            return 0;
        }

        // Calculate the volume and file size expected, and make sure we have it.
        int volume = header.xsize * header.ysize * header.zsize;
        size_t expectedSize = sizeof(VoxelSprite::SpriteFile) + sizeof(VoxelSprite::Color) * volume;

        if (size < expectedSize)
        {
            return 0;
        }

        if (header.xsize > 64 || header.ysize > 64 || header.zsize > 64)
        {
            return 0;
        }

        // Size and clear the output sprite.
        sprite.clear(glm::ivec3(header.xsize, header.ysize, header.zsize));

        // Copy the colors.
        if (volume != 0)
        {
            std::memcpy(&sprite.colors[0], data + sizeof(VoxelSprite::SpriteFile), sizeof(VoxelSprite::Color) * volume);
        }

        // And we are done.
        return expectedSize;
    }
};

#endif