#ifndef ITEM_STACK_HPP_
#define ITEM_STACK_HPP_

#include <string>
#include <Item/Item.hpp>
#include <Serialize/Serialize.hpp>
#include <GraphicsContext/GraphicsContext.hpp>

/**
 * @brief Represents an item stack.
 */
class ItemStack final
{
public:
    using Key = uint64_t;

private:
    Item *item;
    int stackSize;
    void *state;
    Key currentKey = 0;

public:
    /**
     * @brief Construct an empty stack.
     */
    ItemStack();

    /**
     * @brief Copy an item stack.
     * 
     * @param other The stack we are copying from.
     */
    ItemStack(const ItemStack &other);

    /**
     * @brief Construct a stack with a single item.
     * 
     * @param item The item.
     */
    ItemStack(Item &item);

    /**
     * @brief Construct a stack with the specified number of items.
     * 
     * @param item The item.
     * @param stackSize The number of items.
     */
    ItemStack(Item &item, int stackSize);

    /**
     * @brief Construct a stack with the specified item name.
     * 
     * @param itemName The item name.
     * @param stackSize The stack size.
     */
    ItemStack(std::string itemName, int stackSize = 1);

    ~ItemStack();

    ItemStack& operator=(const ItemStack &rhs);

    /**
     * @brief Returns a const reference to this stack's key, which changes on each stack change.
     */
    const Key& key() const;

    /**
     * @brief Give the stack a new key, marking it modified.
     */
    void rekey();

    /**
     * @brief Get the item in this stack.
     */
    Item* getItem() const;

    /**
     * @brief Get the item in this stack, dynamically cast to `TItem`.
     */
    template<typename TItem>
    TItem* getItemAs() const
    {
        return dynamic_cast<TItem*>(item);
    }

    /**
     * @brief Returns the number of items in the stack.
     */
    int getStackSize() const;

    /**
     * @brief Set the number of items in the stack.
     */
    void setStackSize(int size);

    /**
     * @brief Get the display name for this item stack.
     */
    std::string getDisplayName() const;

    /**
     * @brief Returns the maximum stack size for the current item (0 if empty stack).
     */
    int getMaxStackSize() const;

    /**
     * @brief Returns a const state pointer.
     */
    const void* getConstStatePtr() const;

    /**
     * @brief Returns a mutable state pointer.
     */
    void* getMutableStatePtr();

    /**
     * @brief Sets the state pointer; takes ownership of the state.
     */
    void setStatePtr(void *state);

    /**
     * @brief Add any extra lines required in the tooltip beyond the name.
     * 
     * @param tip The tooltip to update.
     */
    void getExtraToolTipLines(ToolTip &tip) const;

    /**
     * @brief Draw this item stack (the item and the value label).
     */
    void draw(GraphicsContext &context, int x, int y) const;

    /**
     * @brief Returns true if this stack is empty.
     */
    bool isEmpty() const;

    /**
     * @brief Left-merge into this stack from the specified stack.
     * 
     * @param over The stack 'over' (which we are merging from).
     */
    void leftMergeFrom(ItemStack &over);

    /**
     * @brief Right-merge into this stack from the specified stack.
     * 
     * @param over The stack 'over' (which we are merging from).
     */
    void rightMergeFrom(ItemStack &over);

    /**
     * @brief Return a new item stack containing one item from this one.
     * 
     * Returns an empty stack if nothing could be removed.
     */
    ItemStack extractOne();

    /**
     * @brief Insert into this stack from the specified source stack.
     */
    void insertFrom(ItemStack &source);

    /**
     * @brief Returns the maximum number of items which can be inserted from the source stack into this one.
     */
    int getMaxInsertableFrom(const ItemStack &source) const;

    /**
     * @brief Returns true if the source stack can be merged into this one.
     *
     * If this is an empty stack, always returns true.
     */
    bool canMergeFrom(const ItemStack &source) const;

    /**
     * @brief Try to set an option based on an option string in the form `option=value`.
     * 
     * Options are item-specific. The empty stack reports an error on trying to set any
     * option.
     *
     * @returns Empty string if successful, error string otherwise.
     */
    std::string trySetOption(const std::string &optionStr);

    /**
     * @brief Report to the item that it was moved by a player.
     */
    void onMovedBy(Player &player) const;

    /**
     * @brief Report to the item that it was collected by a player.
     */
    void onCollectedBy(Player &player) const;

    /**
     * @brief Compare this stack with another stack.
     * 
     * @param other The other stack.
     */
    bool operator==(const ItemStack &other) const;
    bool operator!=(const ItemStack &other) const;
    
    /**
     * @brief Try constructing an `ItemStack` given an item or block name, and a stack size.
     */
    static std::optional<ItemStack> fromItemOrBlockName(std::string name, int stackSize = 1);

    friend struct SerialCodec<ItemStack>;
};

template<>
struct SerialCodec<ItemStack>
{
    static void encode(std::vector<uint8_t> &outBytes, const ItemStack &stack);
    static size_t decode(const uint8_t *data, size_t size, ItemStack &stack);
};

#endif