#include <Block/SpriteBlock/FarmLand/FarmLand.hpp>
#include <Utilities/Utilities.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <BlockRegistry/BlockRegistry.hpp>
#include <Item/IFluidContainingItem.hpp>
#include <Block/FluidBlock/FluidBlock.hpp>

std::vector<FarmLand::Sprite> FarmLand::makeSprites()
{
    static const char *orientNames[2] = {
        "xorient",
        "yorient",
    };

    std::vector<Sprite> sprites;

    for (int axis=0; axis<2; axis++)
    {
        for (int water=0; water<4; water++)
        {
            for (int fertility=0; fertility<4; fertility++)
            {
                std::string sideIcon = formatString("farmland/$+/water0/fertility$+", orientNames[axis], fertility);
                std::string topIcon = formatString("farmland/$+/water$+/fertility$+", orientNames[axis], water, fertility);
                std::string wetSideIcon = formatString("farmland/$+/wetSide/fertility$+", orientNames[axis], fertility);
                std::string stateName = formatString("$+/water$+/fertility$+", orientNames[axis], water, fertility);

                int waterWidth = water == 3 ? 4 : water;
                if (water == 0)
                {
                    wetSideIcon = sideIcon;
                }

                sprites.push_back(
                    Sprite{
                        {wetSideIcon, wetSideIcon, sideIcon, sideIcon, sideIcon, topIcon},
                        {
                            BoundingBox<int>{0, 0, 0, 8, 8, 7},

                            // dirt lines
                            BoundingBox<int>{0, 1, 7, 8, 2, 8},
                            BoundingBox<int>{0, 3, 7, 8, 4, 8},
                            BoundingBox<int>{0, 5, 7, 8, 6, 8},
                            BoundingBox<int>{0, 7, 7, 8, 8, 8},

                            // water lines
                            BoundingBox<int>{4-waterWidth, 0, 7, 4+waterWidth, 1, 8},
                            BoundingBox<int>{4-waterWidth, 2, 7, 4+waterWidth, 3, 8},
                            BoundingBox<int>{4-waterWidth, 4, 7, 4+waterWidth, 5, 8},
                            BoundingBox<int>{4-waterWidth, 6, 7, 4+waterWidth, 7, 8},
                        },
                        stateName,
                    }
                );
            }
        }
    }

    return sprites;
}

void FarmLand::scheduleWaterCheck(LocalWorld &world, glm::ivec3 pos)
{
    world.cancelSignals(pos);
    world.sendSignal(pos, sigCheckWater, waterWaitMin + world.generateRandom() % waterWaitRange);
}

FarmLand::FarmLand(std::string name) :
    SpriteBlock(
        name,
        "Farm land",
        90.0f,
        "dirt",
        makeSprites()
    )
{
    withDustSounds();
}

int FarmLand::getWaterLevelFromMeta(uint8_t meta)
{
    return (meta >> 2) & 3;
}

int FarmLand::getNutritionFromMeta(uint8_t meta)
{
    return meta & 3;
}

uint8_t FarmLand::makeUpdatedMeta(uint8_t meta, int nutrition, int waterLevel)
{
    meta &= ~0xF;
    meta |= nutrition;
    meta |= (waterLevel << 2);
    return meta;
}

bool FarmLand::canManuallyDig()
{
    return true;
}

bool FarmLand::isFluidInteractable()
{
    return true;
}

void FarmLand::getCollisionBoxes(World &world, glm::ivec3 pos, std::vector<BoundingBox<wfixed>> &boxes)
{
    boxes.push_back(BoundingBox<wfixed>{
        (wfixed) pos.x,
        (wfixed) pos.y,
        (wfixed) pos.z,
        (wfixed) (pos.x+1),
        (wfixed) (pos.y+1),
        (wfixed) (pos.z+1),
    });
}

void FarmLand::getHarvestedStacks(LocalWorld &world, glm::ivec3 pos, std::vector<ItemStack> &stacks)
{
    uint8_t nutrition = getNutritionFromMeta(*world.getBlockMeta(pos));
    static const char *dropNames[] = {"main:sand", "main:dirt", "main:dirt", "main:grass"};
    stacks.push_back(blockRegistry.getBlockByName(dropNames[nutrition]).toItemStack(1));
}

void FarmLand::onInteract(LocalWorld &world, Player &player, glm::ivec3 pos, Side side, const RGBABytes &collisionKey)
{
    ItemStack &stack = player.holdingStack();
    IFluidContainingItem *fluidCont = stack.getItemAs<IFluidContainingItem>();

    uint8_t meta = *world.getBlockMeta(pos);
    int nutrition = getNutritionFromMeta(meta);
    int waterLevel = getWaterLevelFromMeta(meta);

    if (waterLevel < 3 && fluidCont != nullptr && fluidCont->pullFluid(stack, "main:water", 64) == 64)
    {
        world.setBlockMeta(pos, makeUpdatedMeta(meta, nutrition, waterLevel+1), true);
        return;
    }

    SpriteBlock::onInteract(world, player, pos, side, collisionKey);
}

void FarmLand::onPlaced(LocalWorld &world, Chunk &chunk, glm::ivec3 pos)
{
    scheduleWaterCheck(world, pos);
}

void FarmLand::onSignal(LocalWorld &world, glm::ivec3 pos, uint32_t code)
{
    if (code == sigCheckWater)
    {
        uint8_t currentMeta = *world.getBlockMeta(pos);
        int waterLevel = getWaterLevelFromMeta(currentMeta);
        int nutrition = getNutritionFromMeta(currentMeta);

        if (waterLevel == 3)
        {
            return;
        }

        glm::ivec3 delta(0, 0, 0);
        for (delta.y=-1; delta.y<=1; delta.y++)
        {
            for (delta.x=-1; delta.x<=1; delta.x++)
            {
                if (delta.x+delta.y != -1 && delta.x+delta.y != 1)
                {
                    continue;
                }
                
                Block *block = world.getBlockAt(pos + delta);
                if (block == nullptr)
                {
                    // Not loaded yet, try again soon.
                    world.sendSignal(pos, code, sigCheckWater);
                    return;
                }

                FluidBlock *fluidBlock = dynamic_cast<FluidBlock*>(block);
                if (fluidBlock != nullptr)
                {
                    if (fluidBlock->fluidName != "main:water")
                    {
                        continue;
                    }

                    int amountInBlock = (int) *world.getBlockMeta(pos + delta) + 1;
                    if (amountInBlock < waterUnitsPerLevel)
                    {
                        continue;
                    }

                    int amountLeft = amountInBlock - waterUnitsPerLevel;
                    if (amountLeft == 0)
                    {
                        world.setBlockAt(pos + delta, 0);
                    }
                    else
                    {
                        world.setBlockMeta(pos + delta, (uint8_t) (amountLeft-1), true);
                        world.sendSignal(pos + delta, FluidBlock::sigSink, 1);
                    }

                    world.setBlockMeta(pos, makeUpdatedMeta(currentMeta, nutrition, waterLevel+1), true);
                    scheduleWaterCheck(world, pos);
                    return;
                }
                else if (dynamic_cast<FarmLand*>(block) != nullptr)
                {
                    uint8_t otherMeta = *world.getBlockMeta(pos + delta);
                    int otherNutrition = getNutritionFromMeta(otherMeta);
                    int otherWaterLevel = getWaterLevelFromMeta(otherMeta);

                    if (otherWaterLevel < waterLevel+2)
                    {
                        continue;
                    }

                    world.setBlockMeta(pos + delta, makeUpdatedMeta(otherMeta, otherNutrition, otherWaterLevel-1), true);
                    world.setBlockMeta(pos, makeUpdatedMeta(currentMeta, nutrition, waterLevel+1), true);
                    scheduleWaterCheck(world, pos);
                    return;
                }
            }
        }
    }
    else
    {
        SpriteBlock::onSignal(world, pos, code);
    }
}

void FarmLand::onNeighborUpdated(LocalWorld &world, glm::ivec3 pos, Side side)
{
    if (!side.isZ())
    {
        scheduleWaterCheck(world, pos);
    }
}