#ifndef ITEM_HPP_
#define ITEM_HPP_

#include <Utilities/Parse.hpp>
#include <functional>
#include <string>
#include <GraphicsContext/GraphicsContext.hpp>
#include <ToolTip/ToolTip.hpp>
#include <Side/Side.hpp>
#include <World/IWorldActor.hpp>
#include <unordered_map>

class ItemStack;
class World;
class LocalWorld;
class WorldDisplay;
class Player;

/**
 * @brief Represents an item.
 */
class Item
{
private:
    /**
     * @brief Takes a stack and the option value string, and updates the stack.
     * 
     * @return Empty string if successful, error message otherwise.
     */
    typedef std::function<std::string(ItemStack &stack, const std::string &value)> AbstractOptionSetter;

    int maxStackSize;
    std::unordered_map<std::string, AbstractOptionSetter> optionSetters;

protected:
    template<typename TValue>
    using Setter = std::function<std::string(ItemStack &stack, const TValue &value)>;

    template<typename TValue>
    void addOption(
        const std::string &option,
        Setter<TValue> setter
    )
    {
        if (optionSetters.count(option))
        {
            crash(formatString("Option '$+' registered twice.", option));
        }

        optionSetters[option] = [option, setter](ItemStack &stack, const std::string &valuestr)
        {
            TValue value;
            if (!Parse::fromString(valuestr, value))
            {
                return formatString("Cannot parse '$+' for option '$+'.", valuestr, option);
            }

            return setter(stack, value);
        };
    }

public:
    const std::string name;

    /**
     * @brief Construct an item with the specified name.
     * 
     * @param name The item name.
     */
    Item(std::string name);

    virtual ~Item() = default;

    /**
     * @brief Serialize this item's state.
     * 
     * @param outBytes The vector into which this item's state will be appended.
     * @param stack The item stack (containing this item).
     */
    virtual void serializeState(std::vector<uint8_t> &outBytes, const ItemStack &stack) = 0;

    /**
     * @brief Deserialize the item state.
     * 
     * @param data The data.
     * @param size The data size.
     * @param stack The stack.
     * @return Number of bytes consumed (0 meaning error).
     */
    virtual size_t deserializeState(const uint8_t *data, size_t size, ItemStack &stack) = 0;

    /**
     * @brief Copy the state.
     * 
     * @param dest Destination stack.
     * @param src Source stack.
     */
    virtual void copyState(ItemStack &dest, const ItemStack &src) = 0;

    /**
     * @brief Destroy the state.
     * 
     * @param state The state pointer.
     */
    virtual void destroyState(void *state) = 0;

    /**
     * @brief Decide whether the states in two stacks match.
     */
    virtual bool isStateEqual(const ItemStack &a, const ItemStack &b) = 0;

    /**
     * @brief Draw the item icon.
     */
    virtual void draw(GraphicsContext &context, int x, int y, const ItemStack &stack) = 0;

    /**
     * @brief Draw this item when dropped in the 3D world.
     * 
     * @param context The graphics context.
     * @param worldDisplay The world display within which we are drawing.
     * @param pos The position of the center of the `ItemEntity`.
     * @param stack The item stack containing this item.
     */
    virtual void draw3D(GraphicsContext &context, WorldDisplay &worldDisplay, glm::vec3 pos, ItemStack &stack) = 0;

    /**
     * @brief Draw this item when on a conveyor belt.
     *
     * The transformation matrix will translate the origin to the bottom-center of the belt element.
     * 
     * @param context The graphics context.
     * @param worldDisplay The world display within which we are drawing.
     * @param matrix The transformation matrix.
     * @param stack The item stack.
     */
    virtual void drawFlat(GraphicsContext &context, WorldDisplay &worldDisplay, glm::mat4 matrix, ItemStack &stack) = 0;

    /**
     * @brief Called when this item is used on a block.
     * 
     * This is only called on the server side. The default action is to do nothing.
     * 
     * @param world The world within which the interaction occured.
     * @param actor The world actor using this item.
     * @param stack Reference to the slot containing this item.
     * @param pos The coordinate of the block which was interacted with.
     * @param side The side on which the block was interacted with.
     */
    virtual void onUse(LocalWorld &world, IWorldActor &actor, ItemStack &stack, glm::ivec3 pos, Side side);

    /**
     * @brief Get the display name of this item for the given stack.
     * 
     * @param stack The stack containing this item.
     * 
     * @return The display name.
     */
    virtual std::string getDisplayName(const ItemStack &stack) = 0;

    /**
     * @brief Add any extra lines required in the tooltip beyond the name.
     * 
     * Does nothing by default.
     * 
     * @param stack The stack containing this item.
     * @param tip The tooltip to update.
     */
    virtual void getExtraToolTipLines(const ItemStack &stack, ToolTip &tip);

    /**
     * @brief Get the max stack size.
     * 
     * @return The maximum stack size.
     */
    int getMaxStackSize();

    /**
     * @brief Set the maximum stack size.
     * 
     * @param size The stack size.
     * @return The item instance itself.
     */
    Item& setMaxStackSize(int size);

    /**
     * @brief Check if the both item stacks contain "the same item" and can be stacked together.
     * 
     * By default this simply checks if the item is the same.
     * 
     * @param a The first stack, contains this item.
     * @param b The other stack, may or may not contain the same item.
     * 
     * @return true if the stacks can be merged.
     */
    virtual bool canMerge(const ItemStack &a, const ItemStack &b);

    /**
     * @brief Return the result of grinding this item in a bowl.
     * 
     * The grinding bowl is intented to be a simplistic machine, and so it does not have a
     * full-blown recipe registry like other machines. Only the most basic items can be produced
     * by grinding in a bowl.
     * 
     * By default, this function returns an empty stack. This indicates that this item cannot
     * be ground.
     * 
     * @param stack The stack containing this item (grinding bowl input stack).
     * @return The resulting stack (empty stack means grinding not possible).
     */
    virtual ItemStack getGrindingOutput(ItemStack &stack);

    /**
     * @brief Get the glow color when this item is in the HUD.
     * 
     * By default returns black (no light).
     */
    virtual glm::vec3 getGlowColor(ItemStack &stack);

    /**
     * @brief Called when the item is picked up (from an item entity) by a player.
     *
     * By default raises the `main:ItemCollected::<name>` trigger.
     */
    virtual void onCollectedBy(Player &player, const ItemStack &stack);

    /**
     * @brief Called when the item is picked up (from an item slot) by a player.
     * 
     * By default raises the `main:ItemMoved::<name>` trigger.
     */
    virtual void onMovedBy(Player &player, const ItemStack &stack);

    /**
     * @brief Called when the item is equipped by a player.
     * 
     * By default raises the `main:ItemEquipped::<name>` trigger.
     */
    virtual void onEquippedBy(Player &player, const ItemStack &stack);

    /**
     * @brief Get a set of stacks for cheat mode.
     * 
     * By default, just pushes the base stack.
     * 
     * @param baseStack A stack based on which cheat stacks should be generated.
     * @param outStacks The vector to append cheat stacks to.
     */
    virtual void getCheatStacks(ItemStack &baseStack, std::vector<ItemStack> &outStacks);

    /**
     * @brief Called when an item is picked up in the cheat book.
     * 
     * @param stack The stack to be given to the player; you can modify this.
     */
    virtual void onPickedUpInCheatBook(ItemStack &stack);

    /**
     * @brief Try setting an option based on an option string in the form `option=value`.
     * 
     * Returns an empty string on success, the error message otherwise.
     */
    std::string trySetOption(ItemStack &stack, const std::string &optionStr);
};

#endif