#include <Block/FluidBlock/FluidBlock.hpp>
#include <BoundingBox/BoundingBox.hpp>
#include <FluidRegistry/FluidRegistry.hpp>
#include <Log/Log.hpp>
#include <World/World.hpp>
#include <Utilities/Utilities.hpp>
#include <TextureVoxelizer/TextureVoxelizer.hpp>

void FluidBlock::sinkOneUnit(LocalWorld &world, glm::ivec3 pos)
{
    uint8_t meta = *world.getBlockMeta(pos);

    glm::ivec3 posBelow = pos + glm::ivec3(0, 0, -1);
    Block *blockBelow = world.getBlockAt(posBelow);

    if (blockBelow->canSweepAway())
    {
        world.breakBlock(posBelow, false);
        world.setBlockAt(posBelow, name);
        world.sendSignal(posBelow, sigSink, 1);

        if (meta == 0)
        {
            world.setBlockAt(pos, 0);
            return;
        }
        else
        {
            meta--;
        }
    }
    else if (blockBelow->name == name)
    {
        uint8_t metaBelow = *world.getBlockMeta(posBelow);
        if (metaBelow < 0xFF)
        {
            world.sendSignal(posBelow, sigSink, 1);
        }
    }

    for (const Side &side : Side::all)
    {
        if (meta == 0xFF)
        {
            break;
        }

        if (side == Side::Bottom)
        {
            continue;
        }

        glm::ivec3 otherPos = pos + side.out();
        Block *otherBlock = world.getBlockAt(otherPos);

        if (otherBlock->name != name)
        {
            continue;
        }

        uint8_t otherMeta = *world.getBlockMeta(otherPos);
        if (otherMeta == 0)
        {
            world.sendSignal(otherPos, sigSink, 1);
        }

        if (side != Side::Top && otherMeta <= meta + 1)
        {
            continue;
        }

        if (otherMeta == 0)
        {
            world.setBlockAt(otherPos, 0);
        }
        else
        {
            world.setBlockMeta(otherPos, otherMeta - 1);
            world.sendSignal(otherPos, sigSink, 1);
        }

        world.sendSignal(pos, sigSink, 1);
        meta++;
    }

    world.setBlockMeta(pos, meta, true);

    if (meta == 0)
    {
        return;
    }

    static std::array<Side, 5> spillSides{Side::West, Side::East, Side::North, Side::South};
    for (const Side &side : spillSides)
    {
        glm::ivec3 otherPos = pos + side.out();
        Block *otherBlock = world.getBlockAt(otherPos);

        if (otherBlock->canSweepAway())
        {
            world.breakBlock(otherPos, false);
            world.setBlockAt(otherPos, name);
            world.setBlockMeta(pos, meta - 1);

            world.sendSignal(otherPos, sigSink, 1);
            world.sendSignal(pos, sigSink, 1);

            return;
        }
    }
}

FluidBlock::FluidBlock(std::string name, std::string fluidName) :
    Block(name, "<FLUID BLOCK>", 0.0f),
    swimPhysics(nullptr),
    fluidName(fluidName)
{
    mapColor = fluidRegistry.getFluidByName(fluidName)->mapColor;
}

void FluidBlock::loadBlockSprites(BlockAtlas &atlas, Bitmap &icon, VoxelSprite &defaultSprite)
{
    for (size_t i = 0; i < 8; i++)
    {
        std::string texName = fluidRegistry.getFluidByName(fluidName)->textureName;
        BlockSprite sprite;
        TextureVoxelizer voxelizer({texName, texName, texName, texName, texName, texName});
        BoundingBox<int32_t> bounds{0, 0, 0, 8, 8, (int32_t) i + 1};
        voxelizer.makeBox(sprite, bounds);

        levelSprites[i] = atlas.addSprite(sprite);
    }
}

BlockAtlas::SpriteID FluidBlock::getBlockSprite(glm::ivec3 pos, uint8_t meta, World &world)
{
    return levelSprites[meta >> 5];
}

bool FluidBlock::isFullyOpaque()
{
    return false;
}

bool FluidBlock::concealsFluidBelow()
{
    return true;
}

bool FluidBlock::isIntersectedByRay(World &world, glm::ivec3 pos, wfvec3 start, wfvec3 dir, Side &side, RGBABytes &collisionKey)
{
    // Fluid blocks do not collide.
    return false;
}

bool FluidBlock::isIntersectedBy(World &world, glm::ivec3 pos, const BoundingBox<wfixed> &box)
{
    // Fluid blocks do not collide.
    return false;
}

bool FluidBlock::isFluid()
{
    // This one should be obvious
    return true;
}

void FluidBlock::onNeighborUpdated(LocalWorld &world, glm::ivec3 pos, Side side)
{
    if (side == Side::Top)
    {
        // We don't care about updates above us.
        return;
    }

    glm::ivec3 otherPos = pos + side.out();

    Block *block = world.getBlockAt(otherPos);
    uint8_t meta = *world.getBlockMeta(pos);
    uint8_t otherMeta = *world.getBlockMeta(otherPos);

    if (meta == 0)
    {
        return;
    }

    if (block->canSweepAway())
    {
        world.breakBlock(otherPos, false);
        world.setBlockAt(otherPos, name);
        world.setBlockMeta(pos, meta - 1);

        world.sendSignal(pos, sigSink, 1);
        world.sendSignal(otherPos, sigSink, 1);
    }
    else if (block->name == name && meta < otherMeta)
    {
        world.sendSignal(pos, sigSink, 1);
        world.sendSignal(otherPos, sigSink, 1);
    }
}

void FluidBlock::onSignal(LocalWorld &world, glm::ivec3 pos, uint32_t code)
{
    int flowRate = fluidRegistry.getFluidByName(fluidName)->flowRate;

    if (code == sigSink)
    {
        // Ensure all neighbors are loaded.
        for (const Side &side : Side::all)
        {
            if (world.getBlockAt(pos + side.out()) == nullptr)
            {
                world.sendSignal(pos, sigSink, 1);
                return;
            }
        }

        for (int i = 0; i < flowRate; i++)
        {
            sinkOneUnit(world, pos);
        }
    }
    else
    {
        Block::onSignal(world, pos, code);
    }
}

bool FluidBlock::canReplaceWithPlacedBlock()
{
    return true;
}

MovementPhysics& FluidBlock::getMovementPhysicsInside()
{
    if (swimPhysics == nullptr)
    {
        swimPhysics = new SwimPhysics(*fluidRegistry.getFluidByName(fluidName));
    }

    return *swimPhysics;
}

void FluidBlock::onEntering(World &world, PhysicsEntity &entity)
{
    if (!world.isLocal())
    {
        return;
    }

    world.playSound(fluidRegistry.getFluidByName(fluidName)->enterSound, glm::floor(entity.pos), entity.vel);
}

void FluidBlock::onLeaving(World &world, PhysicsEntity &entity)
{
    if (!world.isLocal())
    {
        return;
    }

    world.playSound(fluidRegistry.getFluidByName(fluidName)->leaveSound, glm::floor(entity.pos), entity.vel);
}