#include <Item/StatefulItem/Capsule/Capsule.hpp>
#include <ItemStack/ItemStack.hpp>
#include <Log/Log.hpp>
#include <Model/Model.hpp>
#include <FluidRegistry/FluidRegistry.hpp>
#include <BlockRegistry/BlockRegistry.hpp>
#include <PlayerTriggerRegistry/PlayerTriggerRegistry.hpp>
#include <Utilities/Utilities.hpp>
#include <Block/FluidBlock/FluidBlock.hpp>
#include <BlockRegistry/BlockRegistry.hpp>
#include <Block/AirBlock/AirBlock.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>

#include <sstream>

int Capsule::getCapacity()
{
    return m_size == CapsuleSize::Small ? smallCapsuleCapacity : largeCapsuleCapacity;
}

bool Capsule::canStoreFluid(std::string name)
{
    Fluid *fluid = fluidRegistry.getFluidByName(name);
    return m_tier >= fluid->minCapsuleTier;
}

void Capsule::consumeFromBlock(LocalWorld &world, IWorldActor &actor, ItemStack &stack, glm::ivec3 pos)
{
    CapsuleState &capsuleState = state(stack);
    Player *player = dynamic_cast<Player*>(&actor);

    FluidBlock *fluidBlock = dynamic_cast<FluidBlock*>(world.getBlockAt(pos));
    if (fluidBlock != nullptr)
    {
        if (capsuleState.fluidName == "" || capsuleState.amount == 0)
        {
            if (canStoreFluid(fluidBlock->fluidName))
            {
                capsuleState.fluidName = fluidBlock->fluidName;
            }
        }

        if (fluidBlock->fluidName != capsuleState.fluidName)
        {
            return;
        }

        int amountInBlock = (int) *world.getBlockMeta(pos) + 1;
        int amountToTake = getCapacity() - capsuleState.amount;

        if (amountToTake == 0)
        {
            return;
        }

        if (amountToTake > amountInBlock)
        {
            amountToTake = amountInBlock;
        }

        int amountLeft = amountInBlock - amountToTake;

        capsuleState.amount += amountToTake;

        if (amountLeft == 0)
        {
            world.setBlockAt(pos, 0);
        }
        else
        {
            world.setBlockMeta(pos, (uint8_t) (amountLeft - 1), true);
        }

        if (amountToTake > 0 && player != nullptr)
        {
            playerTriggerRegistry.raise(
                *player,
                formatString("main:FluidCollected::$+", fluidBlock->fluidName)
            );
        }
    }
}

void Capsule::spillOntoBlock(LocalWorld &world, ItemStack &stack, glm::ivec3 pos)
{
    if (!world.getBlockAt(pos)->canReplaceWithPlacedBlock())
    {
        return;
    }

    CapsuleState &capsuleState = state(stack);
    if (capsuleState.amount == 0)
    {
        return;
    }

    std::string blockName = capsuleState.fluidName + "Block";
    if (!blockRegistry.isValidBlock(blockName))
    {
        return;
    }

    int existingAmount = 0;
    if (world.getBlockAt(pos)->name == name)
    {
        existingAmount = *world.getBlockMeta(pos) + 1;
    }

    int amountToSpill = std::min(capsuleState.amount, 256 - existingAmount);
    if (amountToSpill == 0)
    {
        return;
    }

    capsuleState.amount -= amountToSpill;

    world.setBlockAt(pos, blockName, existingAmount + amountToSpill - 1);
    world.sendSignal(pos, FluidBlock::sigSink, 1);
}

Capsule::Capsule(std::string name, CapsuleSize size, CapsuleTier tier, Material &icon) :
    StatefulItem(name),
    m_size(size),
    m_tier(tier),
    m_icon(icon)
{
    setMaxStackSize(1);

    addOption<std::string>("fluid", [this](ItemStack &stack, const std::string &fluidName) -> std::string
    {
        if (fluidRegistry.getFluidByName(fluidName) == nullptr)
        {
            return formatString("Invalid fluid name '$+'.", fluidName);
        }

        state(stack).fluidName = fluidName;
        return "";
    });

    addOption<int>("ml", [this](ItemStack &stack, const int &ml) -> std::string
    {
        if (ml < 0 || ml > getCapacity())
        {
            return formatString("Fluid ml must be between 0 and $+.", getCapacity());
        }

        state(stack).amount = ml;
        return "";
    });
}

int Capsule::getFluidAmount(const ItemStack &stack)
{
    const CapsuleState &capsuleState = state(stack);
    return capsuleState.amount;
}

void Capsule::draw(GraphicsContext &context, int x, int y, const ItemStack &stack)
{
    const CapsuleState &capsuleState = state(stack);

    context.color = glm::vec4(1.0f);
    context.drawMaterial(m_icon, x, y);

    Fluid *fluid = fluidRegistry.getFluidByName(capsuleState.fluidName);
    if (fluid == nullptr)
    {
        return;
    }

    int fluidX = m_size == CapsuleSize::Small ? 3 : 2;
    int fluidWidth = m_size == CapsuleSize::Small ? 2 : 4;
    int fluidHeight = 5 * capsuleState.amount / getCapacity();

    context.drawMaterial(
        fluid->icon,
        0, fluid->icon.height() - (fluidHeight % fluid->icon.height()),
        fluidWidth, fluidHeight,
        x + fluidX, y + 7 - fluidHeight,
        fluidWidth, fluidHeight
    );
}

std::string Capsule::getDisplayName(const ItemStack &stack)
{
    std::stringstream ss;
    
    if (m_size == CapsuleSize::Small)
    {
        ss << "Small ";
    }
    else
    {
        ss << "Large ";
    }

    if (m_tier == CapsuleTier::Tin)
    {
        ss << "Tin ";
    }
    else
    {
        ss << "Lead ";
    }

    ss << "Capsule";
    return ss.str();
}

void Capsule::draw3D(GraphicsContext &context, WorldDisplay &worldDisplay, glm::vec3 pos, ItemStack &stack)
{
    worldDisplay.drawMeshedIcon(context, m_icon, pos);
}

void Capsule::drawFlat(GraphicsContext &context, WorldDisplay &worldDisplay, glm::mat4 matrix, ItemStack &stack)
{
    worldDisplay.drawMeshedIconFlat(context, m_icon, matrix);
}

void Capsule::getExtraToolTipLines(const ItemStack &stack, ToolTip &tip)
{
    const CapsuleState &capsuleState = state(stack);
    Fluid *fluid = fluidRegistry.getFluidByName(capsuleState.fluidName);

    std::string displayName = fluid == nullptr ? "" : fluid->displayName;
    int capacity = getCapacity();

    tip.addLine(formatString("$+/$+ ml $+", capsuleState.amount, capacity, displayName));
}

void Capsule::onUse(LocalWorld &world, IWorldActor &actor, ItemStack &stack, glm::ivec3 pos, Side side)
{
    glm::ivec3 targetPos = pos + side.out();
    if (actor.isUsingItemDirectly())
    {
        spillOntoBlock(world, stack, targetPos);
    }
    else
    {
        consumeFromBlock(world, actor, stack, targetPos);
    }
}

bool Capsule::canMerge(const ItemStack &a, const ItemStack &b)
{
    return a.getItem() == b.getItem() && state(a).amount == 0 && state(b).amount == 0;
}

std::string Capsule::getFluidName(const ItemStack &stack)
{
    return state(stack).fluidName;
}

int Capsule::pullFluid(ItemStack &stack, std::string fluidName, int maxToPull)
{
    CapsuleState &capsuleState = state(stack);
    if (fluidName != capsuleState.fluidName)
    {
        return 0;
    }

    int willPull = capsuleState.amount > maxToPull ? maxToPull : capsuleState.amount;
    capsuleState.amount -= willPull;
    return willPull;
}

int Capsule::pushFluid(ItemStack &stack, std::string fluidName, int amount)
{
    CapsuleState &capsuleState = state(stack);
    if (fluidName != capsuleState.fluidName)
    {
        if (capsuleState.fluidName == "" || capsuleState.amount == 0)
        {
            if (canStoreFluid(fluidName))
            {
                capsuleState.fluidName = fluidName;
            }
            else
            {
                return 0;
            }
        }
        else
        {
            return 0;
        }
    }

    int maxAccept = getCapacity() - capsuleState.amount;
    int willPush = amount > maxAccept ? maxAccept : amount;
    capsuleState.amount += willPush;
    return willPush;
}

int Capsule::pullFluidForEquipment(ItemStack &stack, std::string fluidName, int maxAmount)
{
    return pullFluid(stack, fluidName, maxAmount);
}

void SerialCodec<CapsuleState>::encode(std::vector<uint8_t> &outBytes, const CapsuleState &state)
{
    serializeInto(outBytes, state.fluidName, state.amount);
}

size_t SerialCodec<CapsuleState>::decode(const uint8_t *data, size_t size, CapsuleState &state)
{
    return deserialize(data, size, state.fluidName, state.amount);
}