#include <Controller/InventoryController/InventoryController.hpp>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Item/IBag.hpp>
#include <ItemFilter/ItemFilter.hpp>
#include <ItemFilter/ItemFilterByName/ItemFilterByName.hpp>
#include <ItemRegistry/ItemRegistry.hpp>
#include <ItemStack/ItemStack.hpp>
#include <Log/Log.hpp>
#include <PlayerTriggerRegistry/PlayerTriggerRegistry.hpp>
#include <RuntimeStateCache/RuntimeStateCache.hpp>
#include <Widget/Label/Label.hpp>
#include <Widget/Layout/CompoundWidget/Deck/Deck.hpp>
#include <Widget/Layout/CompoundWidget/FormSection/FormSection.hpp>
#include <Widget/Layout/CompoundWidget/IconText/IconText.hpp>
#include <Widget/Layout/ScrollPanel/ScrollPanel.hpp>
#include <Widget/StaticItemSlot/ItemSlot/ItemSlot.hpp>
#include <Widget/Layout/Row/Row.hpp>
#include <Widget/Layout/Column/Column.hpp>
#include <Widget/Layout/Center/Center.hpp>
#include <Entity/PhysicsEntity/CameraEntity/Player/Player.hpp>
#include <Widget/Layout/Frame/Frame.hpp>
#include <Utilities/Utilities.hpp>
#include <BlockRegistry/BlockRegistry.hpp>
#include <Widget/Panel/Panel.hpp>
#include <Widget/StaticItemSlot/ItemSlotButton/ItemSlotButton.hpp>
#include <Widget/TabList/TabList.hpp>
#include <Widget/TextField/TextField.hpp>
#include <Widget/Builder/Builder.hpp>
#include <ItemRecipeRegistry/ItemRecipeRegistry.hpp>
#include <Widget/Button/Button.hpp>
#include <World/World.hpp>
#include <Widget/Layout/CompoundWidget/Explorer/Explorer.hpp>
#include <Widget/StaticItemSlot/RecipeInputView/RecipeInputView.hpp>
#include <Widget/IconLabel/IconLabel.hpp>
#include <Widget/Layout/Row/VerticalSeparator/VerticalSeparator.hpp>
#include <tuple>

Widget* InventoryController::createRecipeView(ItemRecipe *recipe)
{
    std::vector<Widget*> rows;
    std::vector<Widget*> nextRow;

    for (ItemStack &stack : recipe->identity.inputStacks)
    {
        if (nextRow.size() == 6)
        {
            rows.push_back(
                new Row(
                    Layout::WithProportion<1>(),
                    new Panel(),
                    nextRow
                )
            );

            nextRow.clear();
        }

        nextRow.push_back(new RecipeInputView(this->player->items.store("inventory"), stack));
    }

    if (nextRow.size() != 0)
    {
        rows.push_back(
            new Row(
                Layout::WithProportion<1>(),
                new Panel(),
                nextRow
            )
        );
    }

    uint32_t recipeID = recipe->identity.id;

    return new Row(
        Layout::WithProportion<1>(),
        new Panel(),
        new Column(rows),
        new Center(new IconLabel(Material::rightArrow)),
        new Center(
            new StaticItemSlot([recipe]()
            {
                return recipe->identity.outputStack;
            })
        ),
        new Center(
            (new Button(">>", [this, recipeID]()
                {
                    execRecipe(recipeID);
                }))
                ->withToolTip(
                    formatString(
                        "Make $+", recipe->identity.outputStack.getDisplayName()
                    )
                )
                ->enabledIf([recipe, this]() { return recipe->canBeMade(*this->player); })
        )
    );
}

Widget* InventoryController::createItemCheatBook()
{
    auto state = RuntimeStateCache::get<ItemCheatBookState>("/item-cheat-book");

    return new Column(
        (new TextField(state->query))
            ->withPlaceholder("Search...")
            ->withAutoSelect(),
        
        Layout::WithProportion<1>(),
        new Builder<std::string>(
            state->query,
            [this, state](std::string query)
            {
                return new ScrollPanel(
                    state->scrollPos,
                    1,
                    createCheatItemGrid(query)
                );
            }
        )
    );
}

Widget* InventoryController::createCheatItemGrid(const std::string &query)
{
    std::vector<ItemStack> allStacks;
    std::vector<ItemStack> filteredStacks;

    itemRegistry.getAllStacks(allStacks);
    filteredStacks.reserve(allStacks.size());

    ItemFilterByName filter(query);
    for (ItemStack &stack : allStacks)
    {
        if (filter.matches(stack))
        {
            stack.getItem()->getCheatStacks(stack, filteredStacks);
        }
    }

    std::vector<Widget*> rows;

    for (size_t rowBaseIndex = 0; rowBaseIndex < filteredStacks.size(); rowBaseIndex += 4)
    {
        std::vector<Widget*> row;
        for (size_t i = 0; i < 4; i++)
        {
            size_t stackIndex = rowBaseIndex + i;
            if (stackIndex < filteredStacks.size())
            {
                ItemStack &stack = filteredStacks[stackIndex];
                row.push_back(
                    (new ItemSlotButton(
                        stack,
                        [this, stack]()
                        {
                            Sound::itemClick.play();

                            ItemStack newStack = stack;
                            if (SDL_GetModState() & SDL_KMOD_SHIFT)
                            {
                                newStack.setStackSize(newStack.getMaxStackSize());
                            }

                            newStack.getItem()->onPickedUpInCheatBook(newStack);

                            player->movingStack.leftMergeFrom(newStack);
                        }
                    ))
                );
            }
        }

        rows.push_back(new Row(row));
    }

    return new Column(rows);
}

Widget* InventoryController::createBagBuilder(int index)
{
    return new Builder<std::tuple<Item*, ItemStack*, const void*>>(
        [this, index]()
        {
            auto &bound = boundInventories["player.equipment"];
            if (index >= bound.lastKnownStacks.size())
            {
                return std::tuple<Item*, ItemStack*, const void*>{nullptr, nullptr, nullptr};
            }

            ItemStack &stack = bound.lastKnownStacks[index];
            return std::tuple{stack.getItem(), &stack, stack.getConstStatePtr()};
        },
        [this, index](std::tuple<Item*, ItemStack*, const void*> data) -> Widget*
        {
            auto &[ item, stack, _ ] = data;
            auto bag = dynamic_cast<IBag*>(item);
            
            if (bag == nullptr)
            {
                return new Panel();
            }

            return bag->createBagWidget(*this, formatString("player.bag$+", index), *stack, index);
        }
    );
}

void InventoryController::bindMachine(std::shared_ptr<Machine> machine)
{
    boundMachine = machine;

    std::shared_ptr<IInventory> inv = std::dynamic_pointer_cast<IInventory>(machine);
    if (inv != nullptr)
    {
        bindInventory("machine", inv.get());
    }

    bindItemContainer("player", player->items);
}

void InventoryController::bindItemContainer(const std::string &id, ItemContainer &container)
{
    for (auto &[subid, store] : container)
    {
        bindInventory(formatString("$+.$+", id, subid), &store);
    }
}

void InventoryController::bindInventory(const std::string &id, IInventory* inv)
{
    auto &bound = boundInventories[id];
    bound.inv = inv;
    bound.lastKnownStacks.resize(inv == nullptr ? 0 : inv->getNumSlots());

    setBoundInventoryStackCount(id, bound.lastKnownStacks.size());
}

InventoryController::InventoryController(ControllerChannel &channel, ItemRecipe::Level level) :
    Controller(channel),
    isBusyPriv(false),
    level(level),
    leftClickWithStack(this, RunsOn::Server,
        [this](std::string id, uint32_t slotIndex, ItemStack movingStack) -> ItemStack
    {
        if (id == "player.trash")
        {
            auto trashStack = this->player->items.store("trash").begin();
            if (movingStack.isEmpty())
            {
                ItemStack result = *trashStack;
                *trashStack = ItemStack();
                return result;
            }

            *trashStack = movingStack;
            return ItemStack();
        }

        auto &bound = boundInventories[id];
        if (bound.inv == nullptr)
        {
            return movingStack;
        }

        if (slotIndex >= bound.inv->getNumSlots())
        {
            return movingStack;
        }

        ItemStack over = movingStack;
        ItemStack under = bound.inv->getStackInSlot(slotIndex);
        ItemStack result;
        under.leftMergeFrom(over);

        if (bound.inv->isAllowedStackInSlot(slotIndex, under))
        {
            bound.inv->setStackInSlot(slotIndex, under);
            over.onMovedBy(*this->player);
            result = over;
        }
        else
        {
            result = movingStack;
        }

        if (id == "player.equipment")
        {
            bindInventory(formatString("player.bag$+", slotIndex), nullptr);
        }

        return result;
    }),
    rightClickWithStack(this, RunsOn::Server,
        [this](std::string id, uint32_t slotIndex, ItemStack movingStack) -> ItemStack
    {
        auto &bound = boundInventories[id];
        if (bound.inv == nullptr)
        {
            return movingStack;
        }

        if (slotIndex > bound.inv->getNumSlots())
        {
            return movingStack;
        }

        ItemStack over = movingStack;
        ItemStack under = bound.inv->getStackInSlot(slotIndex);
        ItemStack result;
        under.rightMergeFrom(over);

        if (bound.inv->isAllowedStackInSlot(slotIndex, under))
        {
            bound.inv->setStackInSlot(slotIndex, under);
            over.onMovedBy(*this->player);
            result = over;
        }
        else
        {
            result = movingStack;
        }

        if (id == "player.equipment")
        {
            bindInventory(formatString("player.bag$+", slotIndex), nullptr);
        }

        return result;
    }),
    shiftClick(this, RunsOn::Server,
        [this](std::string srcId, uint32_t slotIndex, std::string destId) -> uint32_t
    {
        auto &src = boundInventories[srcId];
        if (src.inv == nullptr)
        {
            return 1;
        }

        if (slotIndex >= src.inv->getNumSlots())
        {
            return 2;
        }

        auto &dest = boundInventories[destId];
        if (dest.inv == nullptr)
        {
            return 3;
        }

        ItemStack selectedStack = src.inv->getStackInSlot(slotIndex);
        selectedStack.onMovedBy(*this->player);
        dest.inv->insertStack(selectedStack);
        src.inv->setStackInSlot(slotIndex, selectedStack);

        if (srcId == "player.equipment")
        {
            bindInventory(formatString("player.bag$+", slotIndex), nullptr);
        }

        return 0;
    }),
    execRecipe(this, RunsOn::Server,
        [this](uint32_t id) -> uint32_t
    {
        ItemRecipe *recipe = itemRecipeRegistry.getRecipeByID(id);
        if (recipe == nullptr)
        {
            return 1;
        }

        if (!recipe->canBeMade(*this->player))
        {
            return 2;
        }

        recipe->make(*this->player);
        world.playSound(Sound::construct, glm::floor(this->player->getEyePos()));
        return 0;
    }),
    setBoundInventoryStackCount(this, RunsOn::Client,
        [this](std::string invID, uint32_t count) -> uint32_t
    {
        auto &bound = boundInventories[invID];
        bound.lastKnownStacks.resize(count);

        return 0;
    }),
    updateBoundInventoryStack(this, RunsOn::Client,
        [this](std::string id, uint32_t slotIndex, ItemStack stack) -> uint32_t
    {
        if (!this->world.isLocal())
        {
            auto &bound = boundInventories[id];
            if (slotIndex <= (uint32_t) bound.lastKnownStacks.size())
            {
                bound.lastKnownStacks[slotIndex] = stack;
            }
        }

        return 0;
    }),
    enableGiveSelfItems(this, RunsOn::Client,
        [this](uint32_t)
    {
        this->canGiveSelfItems = true;
        return 0;
    }),
    applyBagTransform(this, RunsOn::Server,
        [this](uint32_t slotIndex, std::string cmd, std::vector<uint8_t> data)
    {
        auto &bound = boundInventories["player.equipment"];
        if (bound.inv == nullptr || slotIndex >= bound.lastKnownStacks.size())
        {
            return 1;
        }

        auto applyTransform = [&cmd, &data](ItemStack &stack)
        {
            auto bag = stack.getItemAs<IBag>();
            if (bag == nullptr)
            {
                return;
            }

            bag->applyBagTransform(stack, cmd, data);
        };

        applyTransform(bound.lastKnownStacks[slotIndex]);
        applyTransform(*this->player->items.store("equipment").at(slotIndex));

        return 0;
    })
{}

void InventoryController::onConnect()
{
    bindItemContainer("player", player->items);
}

void InventoryController::onConnect(ItemRecipe::Level level)
{
    this->level = level;
    bindItemContainer("player", player->items);
}

Widget* InventoryController::makeItemSlotColumn(const std::string &invId, int base, int count, std::string emptyToolTip)
{
    std::vector<Widget*> slots;
    for (int i = 0; i < count; i++)
    {
        slots.push_back((new ItemSlot(this, invId, base + i))->withEmptyToolTip(emptyToolTip));
    }

    return new Column(slots);
}

ItemStack InventoryController::getStack(const std::string &id, int slot)
{
    auto &bound = boundInventories[id];
    if (bound.lastKnownStacks.size() <= slot)
    {
        return ItemStack();
    }

    return bound.lastKnownStacks[slot];
}

void InventoryController::onSlotLeftClick(const std::string &id, int slot, const std::string &invIdForShiftClick)
{
    if (!isBusyPriv)
    {
        isBusyPriv = true;

        if (SDL_GetModState() & SDL_KMOD_SHIFT)
        {
            shiftClick(id, slot, invIdForShiftClick)->then([this](uint32_t)
            {
                this->isBusyPriv = false;
            });
        }
        else
        {
            leftClickWithStack(id, slot, player->movingStack)->then([this](ItemStack result)
            {
                this->player->movingStack = result;
                this->isBusyPriv = false;
            });
        }
    }
}

void InventoryController::onSlotRightClick(const std::string &id, int slot)
{
    if (!isBusyPriv)
    {
        isBusyPriv = true;

        rightClickWithStack(id, slot, player->movingStack)->then([this](ItemStack result)
        {
            this->player->movingStack = result;
            this->isBusyPriv = false;
        });
    }
}

ItemStack& InventoryController::getMovingStack()
{
    return player->movingStack;
}

Widget* InventoryController::getRightPanel()
{
    return new Column(
        new IconText(
            level == ItemRecipe::Level::Table ?
                blockRegistry.getBlockByName("main:constructTable").iconMaterial :
                Material::kevinAvatar,
            level == ItemRecipe::Level::Table ? "Construction Table" : "Manual Construction"
        ),

        Layout::WithProportion<1>(),
        new Explorer<ItemRecipe*>(
            "/inventory/construct-panel",
            [this](std::string query)
            {
                return itemRecipeRegistry.search(this->level, query);
            },

            [this](ItemRecipe *recipe) -> Widget*
            {
                return this->createRecipeView(recipe);
            }
        )
    );
}

glm::vec4 InventoryController::getSlotHighlight(const std::string &id, int slot, ItemStack &stack)
{
    return glm::vec4(0.0f);
}

void InventoryController::getExtraToolTipLines(const std::string &id, int slot, ItemStack &stack, ToolTip &tip)
{
    // NOP
}

void InventoryController::update()
{
    Controller::update();

    if (world.isLocal())
    {
        for (auto &[id, bound] : boundInventories)
        {
            if (bound.inv == nullptr)
            {
                continue;
            }

            uint32_t i;
            for (i=0; i<(uint32_t)bound.lastKnownStacks.size(); i++)
            {
                ItemStack currentStack = bound.inv->getStackInSlot(i);
                if (bound.lastKnownStacks[i] != currentStack)
                {
                    bound.lastKnownStacks[i] = currentStack;
                    updateBoundInventoryStack(id, i, currentStack);

                    if (id == "player.equipment")
                    {
                        auto bagSlot = player->items.store("equipment").at(i);

                        std::string bagInvId = formatString("player.bag$+", i);
                        IBag *bag = bagSlot->getItemAs<IBag>();

                        if (bag != nullptr)
                        {
                            bindInventory(bagInvId, &bag->bagStore(*bagSlot));
                        }
                        else
                        {
                            bindInventory(bagInvId, nullptr);
                        }
                    }
                }
            }
        }

        if (!canGiveSelfItems && player->hasPermission(UserPermission::GiveSelfItems))
        {
            canGiveSelfItems = true;
            enableGiveSelfItems(0);
        }
    }
}

Widget* InventoryController::getWidget()
{
    std::string equipmentToolTip = GraphicsContext::colorstr(Color::equipmentHighlight) + "Equipment";
    std::string trashToolTip = GraphicsContext::colorstr(Color::error) + "Drop unwanted items here\n"
        "Dropping an item over another\n"
        "discards the old one.";

    return new Row(
        new Center(
            makeItemSlotColumn("player.inventory", 0)
        ),

        Layout::WithProportion<1>(),
        new Center(
            new Frame(
                new Row(
                    new Column(
                        new Deck<Row, Center>(
                            new Column(
                                new FormSection(
                                    new Deck<Column, Center>(
                                        new IconLabel(Material::screwdriver),
                                        makeItemSlotColumn(
                                            "player.equipment",
                                            0,
                                            Player::numEquipmentSlots,
                                            equipmentToolTip
                                        )
                                    )
                                ),
                                new Panel(glm::ivec2(0, 10)),
                                new FormSection(
                                    new Deck<Column, Center>(
                                        new IconLabel(Material::trash),
                                        new Center(
                                            (new ItemSlot(this, "player.trash", 0))
                                                ->withEmptyToolTip(trashToolTip)
                                        )
                                    )
                                )
                            ),
                            Layout::WithProportion<1>(),
                            new Builder<bool>(
                                canGiveSelfItems,
                                [this](bool canGiveSelfItems)
                                {
                                    std::vector<Widget*> bagBuilders;
                                    for (size_t i = 0; i < Player::numEquipmentSlots; i++)
                                    {
                                        bagBuilders.push_back(createBagBuilder(i));
                                    }

                                    return new TabList(
                                        "/inventory/main",
                                        new TabList::Page(
                                            Material::kevinAvatar,
                                            "Kevin",
                                            new ScrollPanel(
                                                "/inventory-controller/inv-scroll-panel",
                                                213,
                                                new Column(
                                                    new Row(
                                                        makeItemSlotColumn("player.inventory", 10),
                                                        makeItemSlotColumn("player.inventory", 20),
                                                        makeItemSlotColumn("player.inventory", 30),
                                                        makeItemSlotColumn("player.inventory", 40)
                                                    ),
                                                    bagBuilders
                                                )
                                            )

                                        ),

                                        !canGiveSelfItems ? nullptr : new TabList::Page(
                                            Material::itemCheatBookIcon,
                                            GraphicsContext::colorstr(glm::vec4(1.0f, 0.0f, 1.0f, 1.0f)) +
                                                "Cheat Item Book",
                                            createItemCheatBook()
                                        )
                                    );
                                }
                            )
                        )
                    ),
                    new VerticalSeparator(),
                    getRightPanel()
                )
            )
        )
    );
}

bool InventoryController::isBusy()
{
    return isBusyPriv;
}