#ifndef ITEM_PROCESSOR_CONTROLLER_HPP_
#define ITEM_PROCESSOR_CONTROLLER_HPP_

#include <BlockRegistry/BlockRegistry.hpp>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Item/IEnergyContainingItem.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/CompoundWidget/ItemProcRuleExplorer/ItemProcRuleExplorer.hpp>
#include <Widget/Layout/CompoundWidget/MachineConfigPanel/MachineConfigPanel.hpp>
#include <Widget/TabList/MachineHelpTab/MachineHelpTab.hpp>
#include <World/World.hpp>
#include <optional>
#include <Controller/InventoryController/InventoryController.hpp>
#include <Machine/Machine.hpp>
#include <Widget/Label/Label.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 <Widget/ProgressIndicator/ProgressIndicator.hpp>
#include <Entity/PhysicsEntity/CameraEntity/Player/Player.hpp>
#include <Widget/BatteryView/BatteryView.hpp>
#include <Widget/Panel/Panel.hpp>
#include <Item/StatelessItem/BasicItem/MachineUpgrade/MachineUpgrade.hpp>
#include <Widget/IconLabel/IconLabel.hpp>
#include <Widget/Layout/Column/HorizontalSeparator/HorizontalSeparator.hpp>
#include <DataLink/DataLink.hpp>
#include <Widget/TabList/TabList.hpp>
#include <Widget/Layout/CompoundWidget/DataChannelPicker/DataChannelPicker.hpp>
#include <Widget/Layout/CompoundWidget/DataOutputTable/DataOutputTable.hpp>

/**
 * @brief The controller for item processors.
 */
template<typename TMachine>
class ItemProcessorController : public InventoryController
{
private:
    std::shared_ptr<TMachine> machine;
    int progress = 0;
    int storedEnergy = 0;
    typename TMachine::ChannelTable channelTable;

    Command<uint32_t, int, int, typename TMachine::ChannelTable> updateProcessorState;
    Command<uint32_t, typename TMachine::Trigger, std::optional<DataLink::Channel>> setTriggerToChannel;

    glm::vec4 getUpgradeStatColor(int value)
    {
        if (value < 0)
        {
            return Color::negativeStat;
        }
        else if (value == 0)
        {
            return Color::faded;
        }
        else
        {
            return Color::positiveStat;
        }
    }

    std::string formatUpgradeStat(int value, const char *statName)
    {
        if (value < 0)
        {
            return formatString("-$+ $+", value, statName);
        }
        else
        {
            return formatString("+$+ $+", value, statName);
        }
    }

    void onTriggerUpdated(typename TMachine::Trigger trigger)
    {
        setTriggerToChannel(trigger, channelTable[static_cast<size_t>(trigger)]);
    }

    Widget* makeTriggerSetter(std::string label, typename TMachine::Trigger trigger)
    {
        return new DataOutputTable::Entry(
            new Label(label),
            channelTable[static_cast<size_t>(trigger)],
            [this, trigger]()
            {
                onTriggerUpdated(trigger);
            }
        );
    }

    Widget* makeUpgradeSlot(size_t index)
    {
        return (new ItemSlot(this, "machine", 3 + index))
            ->withEmptyToolTip(GraphicsContext::colorstr(Color::upgradeHighlight) + "Upgrades");
    }

public:
    ItemProcessorController(ControllerChannel &channel) :
        InventoryController(channel),
        updateProcessorState(this, RunsOn::Client,
            [this](int progress, int storedEnergy, typename TMachine::ChannelTable channelTable) -> uint32_t
        {
            this->progress = progress;
            this->storedEnergy = storedEnergy;
            this->channelTable = channelTable;

            return 0;
        }),
        setTriggerToChannel(this, RunsOn::Server,
            [this](typename TMachine::Trigger trigger, std::optional<DataLink::Channel> channel) -> uint32_t
        {
            this->channelTable[static_cast<size_t>(trigger)] = channel;
            this->machine->setTriggerToChannel(trigger, channel);

            return 0;
        })
    {}

    void onConnect(std::shared_ptr<Machine> machine)
    {
        bindMachine(machine);
        this->machine = std::dynamic_pointer_cast<TMachine>(machine);
    }

    virtual Widget* getRightPanel() override
    {
        return new Column(
            new Center(new IconText(
                blockRegistry.getBlockByName(TMachine::machineBlockName).iconMaterial,
                TMachine::displayName
            )),

            Layout::WithProportion<1>(),
            new TabList(
                formatString("/item-proc/$+/tab-list", TMachine::displayName),
                new TabList::Page(
                    blockRegistry.getBlockByName(TMachine::machineBlockName).iconMaterial,
                    "General",
                    new Row(
                        new Center(
                            new FormSection(
                                new Column(
                                    new Center(
                                        (new ItemSlot(this, "machine", 2))
                                            ->withEmptyToolTip(GraphicsContext::colorstr(Color::rechargeHighlight) +
                                                "Recharge machine from battery")
                                    ),
                                    new Center(new IconLabel(Material::downArrow)),
                                    new Panel(glm::ivec2(0, 2)),
                                    new Center(new BatteryView(storedEnergy, TMachine::energyCapacity, 1000))
                                )
                            )
                        ),

                        Layout::WithProportion<1>(),
                        new Center(
                            new FormSection(
                                new Row(
                                    new Center(
                                        (new ItemSlot(this, "machine", 0))
                                            ->withEmptyToolTip(GraphicsContext::colorstr(Color::inputHighlight) + "Input")
                                    ),
                                    new Center(new ProgressIndicator<int>(progress, TMachine::completedProgress)),
                                    new Center(
                                        (new ItemSlot(this, "machine", 1))
                                            ->withEmptyToolTip(GraphicsContext::colorstr(Color::faded) + "Output")
                                    )
                                )
                            )
                        ),

                        new Center(
                            new FormSection(
                                new Deck<Column, Center>(
                                    new IconLabel(Material::upgradeIcon),
                                    makeUpgradeSlot(0),
                                    makeUpgradeSlot(1),
                                    makeUpgradeSlot(2),
                                    makeUpgradeSlot(3)
                                )
                            )
                        )
                    )
                ),

                new TabList::Page(
                    Material::recipeBookIcon,
                    "Recipes",
                    new ItemProcRuleExplorer(
                        formatString("/item-proc/$+/rule-explorer", TMachine::displayName),
                        [](ItemStack &input) -> ItemStack
                        {
                            return TMachine::getOutputItemS(input);
                        }
                    )
                ),

                new TabList::Page(
                    Material::dataOutputs,
                    "Data outputs",
                    new DataOutputTable(
                        makeTriggerSetter("Input slot empty:", TMachine::Trigger::InputSlotEmpty),
                        makeTriggerSetter("Not enough power:", TMachine::Trigger::NotEnoughPower)
                    )
                ),

                MachineConfigPanel::page(
                    player,
                    TMachine::machineBlockName,
                    formatString("/item-processor/$+/config", TMachine::machineBlockName),
                    blockRegistry.getBlockByName(TMachine::machineBlockName).iconMaterial,
                    *this
                ),

                new MachineHelpTab(TMachine::helpUrl, formatString("/item-proc/$+/help", TMachine::displayName))
            )
        );
    }

    virtual void update() override
    {
        InventoryController::update();

        if (world.isLocal() && machine != nullptr)
        {
            if (
                machine->syncChannelTable(channelTable) ||
                progress != machine->getProgress() ||
                storedEnergy != machine->getStoredEnergy()
            )
            {
                progress = machine->getProgress();
                storedEnergy = machine->getStoredEnergy();
                
                updateProcessorState(progress, storedEnergy, channelTable);
            }
        }
    }

    virtual glm::vec4 getSlotHighlight(const std::string &id, int slot, ItemStack &stack) override
    {
        if (id == "machine" && slot == 0)
        {
            if (!player->movingStack.isEmpty() && !TMachine::getOutputItemS(player->movingStack).isEmpty())
            {
                return Color::inputHighlight;
            }

            return glm::vec4(0.0f);
        }
        else if (id == "machine" && slot == 1)
        {
            return InventoryController::getSlotHighlight(id, slot, stack);
        }
        else if (id == "machine" && slot == 2)
        {
            IEnergyContainingItem *energyContainer = player->movingStack.getItemAs<IEnergyContainingItem>();
            return energyContainer == nullptr ? glm::vec4(0.0f) : Color::rechargeHighlight;
        }
        else if (id == "machine" && slot >= 3)
        {
            MachineUpgrade *upgrade = player->movingStack.getItemAs<MachineUpgrade>();
            return upgrade == nullptr ? glm::vec4(0.0f) : Color::upgradeHighlight;
        }

        if (stack.isEmpty())
        {
            return InventoryController::getSlotHighlight(id, slot, stack);
        }

        ItemStack output = TMachine::getOutputItemS(stack);
        MachineUpgrade *upgrade = stack.getItemAs<MachineUpgrade>();
        IEnergyContainingItem *energyContainer = stack.getItemAs<IEnergyContainingItem>();

        if (!output.isEmpty())
        {
            return Color::inputHighlight;
        }

        if (upgrade != nullptr)
        {
            return Color::upgradeHighlight;
        }

        if (energyContainer != nullptr)
        {
            return Color::rechargeHighlight;
        }

        return InventoryController::getSlotHighlight(id, slot, stack);
    }

    virtual void getExtraToolTipLines(const std::string &id, int slot, ItemStack &stack, ToolTip &tip) override
    {
        if (stack.isEmpty())
        {
            return;
        }

        MachineUpgrade *upgrade = stack.getItemAs<MachineUpgrade>();
        if (upgrade != nullptr)
        {
            tip.addLine(getUpgradeStatColor(upgrade->speedDelta), formatUpgradeStat(upgrade->speedDelta, "speed"));
            tip.addLine(getUpgradeStatColor(upgrade->efficiencyDelta), formatUpgradeStat(upgrade->efficiencyDelta, "efficiency"));
        }

        ItemStack output = TMachine::getOutputItemS(stack);
        if (!output.isEmpty())
        {
            tip.addLine(Color::inputHighlight, formatString(
                "$+ to $+ $+",
                TMachine::verb,
                output.getStackSize(),
                output.getDisplayName()
            ));
        }
    }
};

#endif