#include <AssetManager/AssetManager.hpp>
#include <Block/SpriteBlock/SpriteBlock.hpp>
#include <BoundingBox/BoundingBox.hpp>
#include <Utilities/FixedPoint.hpp>
#include <World/World.hpp>
#include <TextureVoxelizer/TextureVoxelizer.hpp>
#include <random>

SpriteBlock::SpriteBlock(
    std::string name,
    std::string displayName,
    float hardness,
    std::string iconName,
    std::vector<Sprite> sprites
) :
    Block(name, displayName, hardness),
    sprites(sprites),
    iconName(iconName)
{
    for (Sprite &sprite : sprites)
    {
        stateNames.push_back(sprite.stateName);
    }
}

SpriteBlock::SpriteBlock(
    std::string name,
    std::string displayName,
    float hardness,
    const std::vector<std::string> &stateNames
) :
    Block(name, displayName, hardness),
    stateNames(stateNames),
    iconName("<invalid>")
{
    for (const auto &name : stateNames)
    {
        sprites.push_back({{}, {}, name});
    }
}

void SpriteBlock::onDesignerInitializing(BlockSpriteDesigner &designer, ScriptEngine &engine)
{
    // NOP
}

void SpriteBlock::loadBlockSprites(BlockAtlas &atlas, Bitmap &icon, VoxelSprite &defaultSprite)
{
    static std::random_device rd;
    static std::mt19937 eng(rd());
    static std::uniform_real_distribution<float> distr(-1.0f, 1.0f);

    std::unordered_map<std::string, bool> dumpedStates;
    bool dumpingSprites = getenv("VOXELVERSE_DUMP_SPRITES") != nullptr;

    BlockSpriteDesigner designer(getPureName());
    onDesignerInitializing(designer, designer.engine);
    designer.execute();

    bool isFirst = true;
    for (Sprite &sprite : sprites)
    {
        std::shared_ptr<BlockSprite> blockSprite = std::make_shared<BlockSprite>();
        std::string stateName = stateNames[spriteIds.size()];
        std::string spriteDir = formatString("blocksprites/$+/$+", getPureName(), stateName);
        std::shared_ptr<BlockSprite> exportedSprite = designer.tryGetSprite(stateName);

        std::string firstColorMap = formatString("$+/frame0/0-colormap.vsp", spriteDir);
        std::string collisonMaskFile = formatString("$/collisonMask.vsp");
        if (stateName.empty())
        {
            BlockSprite::Frame &frame = blockSprite->addFrame();
            frame.colorMap.fill({{0xFF, 0x00, 0xFF, 0xFF}});
        }
        else if (exportedSprite != nullptr)
        {
            blockSprite = exportedSprite;
        }
        else if (assets.exists(firstColorMap) || assets.exists(collisonMaskFile))
        {
            blockSprite->readFromDir(spriteDir);
        }
        else if (sprite.segments.empty())
        {
            Log::warn(
                formatString(
                    "No design or blocksprite is defined for state '$+' of '$+', and it does not define "
                    "rules for generating from textures. Using a dummy sprite.",
                    stateName,
                    getPureName()
                )
            );

            BlockSprite::Frame &frame = blockSprite->addFrame();

            for (int z = 0; z < 8; z++)
            {
                for (int y = 0; y < 8; y++)
                {
                    for (int x = 0; x < 8; x++)
                    {
                        if ((x >> 2) ^ (y >> 2) ^ (z >> 2))
                        {
                            frame.colorMap.at(x, y, z) = VoxelSprite::Color{{0xFF, 0xFF, 0xFF, 0xFF}};
                        }
                        else
                        {
                            frame.colorMap.at(x, y, z) = VoxelSprite::Color{{0xFF, 0x00, 0xFF, 0xFF}};
                        }
                    }
                }
            }
        }
        else
        {
            TextureVoxelizer voxelizer(sprite.texturesOnSides);
            for (size_t i = 0; i < sprite.segments.size(); i++)
            {
                const BoundingBox<int> &segment = sprite.segments[i];
                if (sprite.segmentTextureOverrides.count(i) != 0)
                {
                    TextureVoxelizer innerVoxelizer(sprite.segmentTextureOverrides[i]);
                    innerVoxelizer.makeBox(*blockSprite, segment);
                }
                else
                {
                    voxelizer.makeBox(*blockSprite, segment);
                }
            }
        }

        if (!stateName.empty() && blockSprite->collisionMask.getVolume() == 0)
        {
            blockSprite->collisionMask.clear(glm::ivec3(8));

            glm::ivec3 offset;
            for (offset.z = 0; offset.z < 8; offset.z++)
            {
                for (offset.y = 0; offset.y < 8; offset.y++)
                {
                    for (offset.x = 0; offset.x < 8; offset.x++)
                    {
                        if (blockSprite->frames[0].colorMap.isFullVoxel(offset))
                        {
                            blockSprite->collisionMask.at(offset) = {{0xFF, 0xFF, 0xFF, 0xFF}};
                        }
                    }
                }
            }
        }

        if (!stateName.empty() && dumpingSprites && !dumpedStates[stateName])
        {
            std::string stateDir = formatString("dumps/sprites/$+/$+", getPureName(), stateNames[spriteIds.size()]);
            blockSprite->dumpToDir(stateDir);
        }

        areAllSpritesFull = areAllSpritesFull && blockSprite->isFullyOpaque();
        generatedSprites.push_back(std::make_shared<BlockSprite>(*blockSprite));
        spriteIds.push_back(atlas.addSprite(*blockSprite));

        std::vector<BoundingBox<double>> spriteBoxes;
        blockSprite->collisionMask.computeBoundingBoxes(spriteBoxes);

        for (auto &bbox : spriteBoxes)
        {
            bbox.x0 /= 8.0;
            bbox.x1 /= 8.0;
            bbox.y0 /= 8.0;
            bbox.y1 /= 8.0;
            bbox.z0 /= 8.0;
            bbox.z1 /= 8.0;
        }

        boxes.push_back(spriteBoxes);

        if (isFirst)
        {
            isFirst = false;

            glm::ivec3 pos;
            for (pos.z = 0; pos.z < 8; pos.z++)
            {
                for (pos.y = 0; pos.y < 8; pos.y++)
                {
                    for (pos.x = 0; pos.x < 8; pos.x++)
                    {
                        VoxelSprite::Color &color = blockSprite->frames[0].colorMap.at(pos);
                        glm::vec4 rgba = glm::vec4(color.red, color.green, color.blue, color.alpha) / 255.0f;
                        glm::vec3 hsv = Bitmap::rgb2hsv(Bitmap::sRGBToLinear(glm::vec3(rgba)));

                        VoxelSprite::Color &biasSpec = blockSprite->frames[0].biasMap.at(pos);
                        glm::vec3 bias = glm::vec3(biasSpec.red, biasSpec.green, biasSpec.blue) / 255.0f;

                        glm::vec3 noise = glm::vec3(distr(eng), distr(eng), distr(eng));

                        hsv += bias * noise;

                        rgba = glm::vec4(Bitmap::linearToSRGB(Bitmap::hsv2rgb(hsv)), rgba.a);
                        rgba = glm::clamp(rgba, glm::vec4(0.0f), glm::vec4(1.0f));

                        VoxelSprite::Color &outColor = defaultSprite.at(pos);
                        outColor.red = 255.0f * rgba.r;
                        outColor.green = 255.0f * rgba.g;
                        outColor.blue = 255.0f * rgba.b;
                        outColor.alpha = 255.0f * rgba.a;
                    }
                }
            }
        }
    }

    std::optional<Bitmap> optIcon = Bitmap::fromFile(assets.resolve(formatString(
        "blocksprites/$+/$+Icon.png",
        getPureName(),
        getPureName()
    )));

    if (optIcon.has_value())
    {
        icon = *optIcon;
    }
    else
    {
        std::string iconPath = assets.resolve(formatString("blocktex/$+.png", iconName));
        std::optional<Bitmap> maybeIcon = Bitmap::fromFile(iconPath);

        if (maybeIcon.has_value())
        {
            icon = *maybeIcon;
        }
        else
        {
            icon = generatedSprites[0]->frames[0].colorMap.getSideProfileBitmap();
        }

        std::string biasMapPath = assets.resolve(formatString("blockbias/$+.png", iconName));
        std::optional<Bitmap> biasMap = Bitmap::fromFile(biasMapPath);

        if (biasMap.has_value())
        {
            icon.applyNoiseHSV(*biasMap);
        }
        else
        {
            Bitmap generatedBiasMap = generatedSprites[0]->frames[0].biasMap.getSideProfileBitmap();
            icon.applyNoiseHSV(generatedBiasMap);
        }
    }

    if (dumpingSprites)
    {
        std::string iconDumpPath = formatString("dumps/sprites/$+/$+Icon.png", getPureName(), getPureName());
        icon.writeToPNG(iconDumpPath);
    }
}

BlockAtlas::SpriteID SpriteBlock::getBlockSprite(glm::ivec3 pos, uint8_t meta, World &world)
{
    return spriteIds[meta % spriteIds.size()];
}

bool SpriteBlock::isFullyOpaque()
{
    return areAllSpritesFull;
}

void SpriteBlock::getCollisionBoxes(World &world, glm::ivec3 pos, std::vector<BoundingBox<wfixed>> &boxes)
{
    uint8_t meta = *world.getBlockMeta(pos);
    for (BoundingBox<double> &box : this->boxes[meta % this->boxes.size()])
    {
        boxes.push_back((BoundingBox<wfixed>) box + wfvec3(pos));
    }
}

bool SpriteBlock::isIntersectedByRay(World &world, glm::ivec3 pos, wfvec3 start, wfvec3 dir, Side &side, RGBABytes &collisionKey)
{
    uint8_t meta = *world.getBlockMeta(pos);
    VoxelSprite &collisionMask = generatedSprites[meta % generatedSprites.size()]->collisionMask;

    // Recalculate into voxel coordinates.
    start = (start - wfvec3(pos)) * wfvec3(wfixed(8));

    glm::uvec3 coords = glm::floor(start);
    glm::dvec3 tDelta = glm::dvec3(1.0) / glm::abs(glm::dvec3(dir));

    glm::ivec3 step;
    glm::dvec3 tMax;

    for (int i = 0; i < 3; i++)
    {
        step[i] = dir[i] > wfixed(0) ? 1 : -1;

        if (dir[i] != wfixed(0))
        {
            tMax[i] = ((wfixed) (step[i] > 0 ? (coords[i] + 1) : coords[i]) - start[i]) / dir[i];
        }
        else
        {
            tMax[i] = std::numeric_limits<double>::infinity();
        }
    }

    constexpr double epsilon = 1e-5;
    double currentDistance = 0.0;

    Side internalSide = side;
    while (currentDistance < 16.0)
    {
        if ((coords & glm::uvec3(~7)) == glm::uvec3(0))
        {
            collisionKey = collisionMask.at(coords);
            if (collisionKey.alpha != 0)
            {
                glm::uvec3 testCoords;
                bool sideAcceptable = true;
                for (
                    testCoords = coords + glm::uvec3(internalSide.out());
                    (testCoords & glm::uvec3(~7)) == glm::uvec3(0);
                    testCoords += internalSide.out()
                )
                {
                    RGBABytes &key = collisionMask.at(testCoords);
                    if (key.alpha != 0)
                    {
                        sideAcceptable = false;
                        break;
                    }
                }
        
                if (sideAcceptable)
                {
                    side = internalSide;
                }

                return true;
            }
        }

        static const glm::ivec3 axisNumbers = glm::ivec3(0, 2, 4);

        glm::ivec3 traversal = glm::ivec3(
            tMax.x < tMax.y + epsilon && tMax.x < tMax.z + epsilon,
            tMax.x >= tMax.y + epsilon && tMax.y < tMax.z + epsilon,
            0
        );

        traversal.z = !traversal.x && !traversal.y;

        glm::ivec3 sideVec = traversal * (axisNumbers | ((step + glm::ivec3(1)) >> 1) ^ glm::ivec3(1));

        coords += step * traversal;
        internalSide = Side(sideVec.x + sideVec.y + sideVec.z);
        currentDistance = tMax[internalSide.axis()];
        tMax += tDelta * glm::dvec3(traversal);
    }

    return false;
}