#include <BlockRegistry/BlockRegistry.hpp>
#include <Item/Item.hpp>
#include <optional>
#include <string>
#include <cstddef>
#include <ItemStack/ItemStack.hpp>
#include <ItemRegistry/ItemRegistry.hpp>
#include <Utilities/Utilities.hpp>

ItemStack::ItemStack() : item(nullptr), stackSize(0), state(nullptr) {}
ItemStack::ItemStack(Item &item) : item(&item), stackSize(1), state(nullptr) {}
ItemStack::ItemStack(Item &item, int stackSize) : item(&item), stackSize(stackSize), state(nullptr) {}
ItemStack::ItemStack(std::string itemName, int stackSize) :
    item(itemRegistry.getItemByName(itemName)),
    stackSize(stackSize),
    state(nullptr)
{}

ItemStack::ItemStack(const ItemStack &other) : item(other.item), stackSize(other.stackSize), state(nullptr)
{
    if (item != nullptr)
    {
        item->copyState(*this, other);
    }
}

ItemStack::~ItemStack()
{
    if (item != nullptr)
    {
        item->destroyState(state);
    }
}

ItemStack& ItemStack::operator=(const ItemStack &rhs)
{
    if (item != nullptr)
    {
        item->destroyState(state);
    }

    item = rhs.item;
    stackSize = rhs.stackSize;
    state = nullptr;

    if (item != nullptr)
    {
        item->copyState(*this, rhs);
    }

    rekey();
    return *this;
}

const ItemStack::Key& ItemStack::key() const
{
    return currentKey;
}

void ItemStack::rekey()
{
    currentKey++;
}

Item* ItemStack::getItem() const
{
    return item;
}

int ItemStack::getStackSize() const
{
    return stackSize;
}

void ItemStack::setStackSize(int size)
{
    stackSize = size;
    rekey();
}

std::string ItemStack::getDisplayName() const
{
    if (item == nullptr)
    {
        return "Empty";
    }

    return item->getDisplayName(*this);
}

int ItemStack::getMaxStackSize() const
{
    if (item == nullptr)
    {
        return 0;
    }

    return item->getMaxStackSize();
}

const void* ItemStack::getConstStatePtr() const
{
    return state;
}

void* ItemStack::getMutableStatePtr()
{
    rekey();
    return state;
}

void ItemStack::setStatePtr(void *state)
{
    this->state = state;
    rekey();
}

void ItemStack::getExtraToolTipLines(ToolTip &tip) const
{
    if (item == nullptr)
    {
        return;
    }

    item->getExtraToolTipLines(*this, tip);
}

void ItemStack::draw(GraphicsContext &context, int x, int y) const
{
    if (item == nullptr)
    {
        // Empty stack, nothing to draw.
        return;
    }

    item->draw(context, x, y, *this);

    context.color = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);

    if (item->getMaxStackSize() != 1 || stackSize != 1)
    {
        std::string label = formatString("$+", stackSize);
        int width = context.measureText(Font::generic, label)[0];

        int plotX = x-6+(20-width)/2;
        int plotY = y - 9;

        context.color = Color::solidShadow;
        context.drawText(Font::generic, plotX+1, plotY, label);
        context.drawText(Font::generic, plotX+1, plotY+1, label);
        context.drawText(Font::generic, plotX, plotY+1, label);

        context.color = glm::vec4(1.0f);
        context.drawText(Font::generic, plotX, plotY, label);
    }
}

bool ItemStack::isEmpty() const
{
    return stackSize == 0;
}

void ItemStack::leftMergeFrom(ItemStack &over)
{
    if (this->isEmpty() || over.isEmpty())
    {
        std::swap(*this, over);
        return;
    }

    Item *item = this->item;
    if (!item->canMerge(*this, over))
    {
        std::swap(*this, over);
        return;
    }

    int maxTransfer = std::min(item->getMaxStackSize() - this->stackSize, over.stackSize);
    if (maxTransfer == 0)
    {
        std::swap(*this, over);
        return;
    }

    this->stackSize += maxTransfer;
    over.stackSize -= maxTransfer;

    if (over.stackSize == 0)
    {
        over = ItemStack();
    }
    
    over.rekey();
    rekey();
}

void ItemStack::rightMergeFrom(ItemStack &over)
{
    if (this->isEmpty() && over.isEmpty())
    {
        // Nothing to be done.
        return;
    }

    if (over.isEmpty())
    {
        // If the stack "under" is not empty but "over" is, then split the stack in half.
        over = *this;
        this->stackSize /= 2;
        over.stackSize -= this->stackSize;

        if (this->stackSize == 0)
        {
            *this = ItemStack();
        }
        
        over.rekey();
        rekey();
        return;
    }

    if (this->isEmpty())
    {
        // If the "under" stack is empty but "over" isn't, drop 1 item.
        if (over.getMaxStackSize() == 1)
        {
            leftMergeFrom(over);
            return;
        }

        *this = over;
        over.stackSize--;
        this->stackSize = 1;
        
        if (over.stackSize == 0)
        {
            over = ItemStack();
        }

        over.rekey();
        rekey();
        return;
    }

    // If either stack is empty then they must be mergeable.
    Item *item = this->item;
    if (canMergeFrom(over) && item->getMaxStackSize() != this->stackSize)
    {
        this->stackSize++;
        over.stackSize--;

        if (over.stackSize == 0)
        {
            over = ItemStack();
        }

        over.rekey();
        rekey();
        return;
    }

    // In all other cases just act like a left-merge.
    leftMergeFrom(over);
}

ItemStack ItemStack::extractOne()
{
    ItemStack newStack;
    newStack.rightMergeFrom(*this);
    return newStack;
}

void ItemStack::insertFrom(ItemStack &source)
{
    if (isEmpty())
    {
        std::swap(*this, source);
        return;
    }

    if (stackSize == item->getMaxStackSize() || !canMergeFrom(source))
    {
        return;
    }
    
    leftMergeFrom(source);
}

int ItemStack::getMaxInsertableFrom(const ItemStack &source) const
{
    if (isEmpty())
    {
        return source.stackSize;
    }

    if (!canMergeFrom(source))
    {
        return 0;
    }

    return std::min(item->getMaxStackSize() - this->stackSize, source.stackSize);
}

bool ItemStack::canMergeFrom(const ItemStack &source) const
{
    if (isEmpty())
    {
        return true;
    }

    if (item->canMerge(*this, source))
    {
        return true;
    }

    return false;
}

std::string ItemStack::trySetOption(const std::string &optionStr)
{
    if (isEmpty())
    {
        return "Cannot set options on an empty stack.";
    }

    return item->trySetOption(*this, optionStr);
}

void ItemStack::onMovedBy(Player &player) const
{
    if (item == nullptr)
    {
        return;
    }

    item->onMovedBy(player, *this);
}

void ItemStack::onCollectedBy(Player &player) const
{
    if (item == nullptr)
    {
        return;
    }

    item->onCollectedBy(player, *this);
}

bool ItemStack::operator==(const ItemStack &other) const
{
    if (item == nullptr)
    {
        return other.isEmpty();
    }

    return item == other.item && stackSize == other.stackSize && item->isStateEqual(*this, other);
}

bool ItemStack::operator!=(const ItemStack &other) const
{
    return !(*this == other);
}

std::optional<ItemStack> ItemStack::fromItemOrBlockName(std::string name, int stackSize)
{
    Item *item = itemRegistry.getItemByName(name);
    if (item != nullptr)
    {
        return ItemStack(*item, stackSize);
    }
    else if (blockRegistry.isValidBlock(name))
    {
        return blockRegistry.getBlockByName(name).toItemStack(stackSize);
    }
    else
    {
        return std::nullopt;
    }
}

void SerialCodec<ItemStack>::encode(std::vector<uint8_t> &outBytes, const ItemStack &stack)
{
    if (stack.item == nullptr)
    {
        outBytes.push_back(0);
    }
    else
    {
        std::vector<uint8_t> itemData;
        uint8_t sizeByte = (uint8_t) (stack.stackSize - 1);

        stack.item->serializeState(itemData, stack);
        
        serializeInto(outBytes, stack.item->name, sizeByte, itemData);
    }
}

size_t SerialCodec<ItemStack>::decode(const uint8_t *data, size_t size, ItemStack &stack)
{
    std::string name;
    uint8_t sizeByte;
    std::vector<uint8_t> itemData;

    if (size == 0)
    {
        stack = ItemStack();
        return 0;
    }

    if (data[0] == 0)
    {
        stack = ItemStack();
        return 1;
    }

    size_t resultingSize = deserialize(data, size, name, sizeByte, itemData);

    if (name.empty())
    {
        stack = ItemStack();
        return resultingSize;
    }

    stack.item = itemRegistry.getItemByName(name);
    if (stack.item == nullptr)
    {
        stack = ItemStack();
        return resultingSize;
    }

    stack.stackSize = (int) sizeByte + 1;
    stack.item->deserializeState(itemData.data(), itemData.size(), stack);

    stack.rekey();
    return resultingSize;
}