#include <Controller/InventoryController/EngineeringTableController/EngineeringTableController.hpp>
#include <Widget/Layout/Center/Center.hpp>
#include <Widget/Layout/CompoundWidget/IconText/IconText.hpp>
#include <Widget/Layout/CompoundWidget/MachineConfigPanel/MachineConfigPanel.hpp>
#include <Widget/Layout/Row/Row.hpp>
#include <Widget/Layout/Column/Column.hpp>
#include <Widget/Label/Label.hpp>
#include <Widget/TabList/MachineHelpTab/MachineHelpTab.hpp>
#include <Widget/TabList/TabList.hpp>
#include <Widget/Button/Button.hpp>
#include <Widget/Panel/Panel.hpp>
#include <Widget/Switch/Switch.hpp>
#include <Widget/StaticItemSlot/ItemSlot/ItemSlot.hpp>
#include <Entity/PhysicsEntity/CameraEntity/Player/Player.hpp>
#include <World/World.hpp>
#include <Widget/Layout/CompoundWidget/Explorer/Explorer.hpp>
#include <Widget/StaticItemSlot/StaticItemSlot.hpp>
#include <Widget/ProgressIndicator/ProgressIndicator.hpp>
#include <Widget/InfoIcon/InfoIcon.hpp>
#include <Widget/BatteryView/BatteryView.hpp>
#include <ItemRecipeRegistry/ItemRecipeRegistry.hpp>
#include <Item/IEnergyContainingItem.hpp>
#include <Widget/IconLabel/IconLabel.hpp>
#include <Widget/Layout/Column/HorizontalSeparator/HorizontalSeparator.hpp>
#include <Widget/Layout/CompoundWidget/DataChannelPicker/DataChannelPicker.hpp>
#include <Widget/Layout/CompoundWidget/DataOutputTable/DataOutputTable.hpp>
#include <Widget/Layout/CompoundWidget/DataInputTable/DataInputTable.hpp>
#include <MachineComponent/DataCableComponent/DataCableComponent.hpp>
#include <MachineComponent/DataCableComponent/DataCableComponent.hpp>
#include <Machine/MEngineeringTable/MEngineeringTable.hpp>
#include <BlockRegistry/BlockRegistry.hpp>

Widget* EngineeringTableController::makeTable()
{
    std::list<Widget*> cols;

    int i;
    for (i=0; i<6; i++)
    {
        if (i == 3)
        {
            cols.push_back(new Center(new ProgressIndicator<int>(progress, completedProgress)));
        }

        std::list<Widget*> slots;
        int j;
        for (j=0; j<6; j++)
        {
            slots.push_back(new ItemSlot(this, "machine", 6*i+j));
        }

        cols.push_back(new Column(slots));
    }

    return new Column(
        Layout::WithProportion<1>(),
        new Center(new Row(cols)),

        new HorizontalSeparator(),
        new Center(
            new Row(
                new Center(
                    (new ItemSlot(this, "machine", 6*6))
                        ->withEmptyToolTip(GraphicsContext::colorstr(Color::rechargeHighlight) +
                            "Recharge machine from battery")
                ),
                new Panel(glm::ivec2(2, 0)),
                new Center(new IconLabel(Material::rightArrow)),
                new Panel(glm::ivec2(2, 0)),
                new Center(new BatteryView(storedEnergy, energyCapacity))
            )
        )
    );
}

Widget* EngineeringTableController::makeRecipeBook()
{
    return new Explorer<ItemRecipe*>(
        "/eng-table/recipe-explorer",
        [](std::string query)
        {
            return itemRecipeRegistry.search(ItemRecipe::Level::EngTable, query);
        },

        [this](ItemRecipe *recipe) -> Widget*
        {
            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 StaticItemSlot([stack]()
                    {
                        return 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("SET", [this, recipeID]()
                {
                    this->setRecipe(recipeID);
                }))
            );
        }
    );
}

Widget* EngineeringTableController::makeDataOutputsTab()
{
    return new Builder<int>(
        recipeUpdateCounter,
        [this](int) -> Widget*
        {
            std::list<Widget*> inputItemChannelSetters;
            for (size_t i=0; i<itemInputChannelTable.size(); i++)
            {
                inputItemChannelSetters.push_back(makeInputItemChannelSetter(i));
            }

            return new DataOutputTable(
                makeTriggerSetter("Not enough power:", MEngineeringTable::Trigger::NotEnoughPower),
                inputItemChannelSetters
            );
        }
    );
}

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

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

Widget* EngineeringTableController::makeInputItemChannelSetter(size_t index)
{
    return new DataOutputTable::Entry(
        new Row(
            new Center(new Label("Not enough")),
            new Center(new StaticItemSlot([this, index](){ return inputStacks[index]; })),
            new Center(new Label(":"))
        ),
        itemInputChannelTable[index],
        [this, index]()
        {
            onInputItemChannelUpdated(index);
        }
    );
}

void EngineeringTableController::onInputItemChannelUpdated(uint32_t index)
{
    setInputItemToChannel(recipeUpdateCounter, index, itemInputChannelTable[index]);
}

EngineeringTableController::EngineeringTableController(ControllerChannel &channel) :
    InventoryController(channel),
    energyCapacity(MEngineeringTable::energyCapacity),
    completedProgress(MEngineeringTable::completedProgress),
    isSwitchedOnPriv(false),
    updateTableState(this, RunsOn::Client,
        [this](
            int storedEnergy,
            int progress,
            bool isSwitchedOn,
            std::vector<ItemStack> inputStacks,
            ItemStack outputStack,
            int recipeUpdateCounter,
            MEngineeringTable::ChannelTable channelTable,
            MEngineeringTable::ItemInputChannelTable itemInputChannelTable,
            std::vector<DataCableComponent::InputRule> inputRules
    ) -> uint32_t
    {
        this->storedEnergy = storedEnergy;
        this->progress = progress;
        this->isSwitchedOnPriv = isSwitchedOn;
        this->inputStacks = inputStacks;
        this->outputStack = outputStack;
        this->recipeUpdateCounter = recipeUpdateCounter;
        this->channelTable = channelTable;
        this->itemInputChannelTable = itemInputChannelTable;
        this->inputRules = inputRules;

        return 0;
    }),
    setSwitch(this, RunsOn::Server,
        [this](bool on) -> uint32_t
    {
        if (this->table != nullptr)
        {
            this->table->setSwitch(on);
        }

        return 0;
    }),
    setRecipe(this, RunsOn::Server,
        [this](uint32_t id) -> uint32_t
    {
        if (this->table != nullptr)
        {
            this->table->setRecipe(id);
        }

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

        return 0;
    }),
    setInputItemToChannel(this, RunsOn::Server,
        [this](uint32_t recipeUpdateCounter, uint32_t index, std::optional<DataLink::Channel> channel) -> uint32_t
    {
        if (recipeUpdateCounter != this->recipeUpdateCounter)
        {
            return 0;
        }

        this->itemInputChannelTable[index] = channel;
        this->table->setInputItemToChannel(recipeUpdateCounter, index, channel);

        return 0;
    }),
    remoteDataComponent(this, [this]() -> DataCableComponent& { return table->getComponent<DataCableComponent>(); })
{}

void EngineeringTableController::onConnect(std::shared_ptr<Machine> tableMachine)
{
    bindMachine(tableMachine);
    table = std::dynamic_pointer_cast<MEngineeringTable>(tableMachine);
}

Widget* EngineeringTableController::getRightPanel()
{
    return new Column(
        new Row(
            new IconText(blockRegistry.getBlockByName("main:engineeringTable").iconMaterial, "Engineering table"),

            Layout::WithProportion<1>(),
            new Panel(),

            new Switch(isSwitchedOnPriv, [this](bool on)
            {
                this->setSwitch(on);
            })
        ),

        new Panel(glm::ivec2(0, 2)),

        Layout::WithProportion<1>(),
        new TabList(
            "/eng-table/tab-list",
            new TabList::Page(
                blockRegistry.getBlockByName("main:engineeringTable").iconMaterial,
                "Table",
                makeTable()
            ),

            new TabList::Page(
                Material::recipeBookIcon,
                "Recipes",
                makeRecipeBook()
            ),

            new TabList::Page(
                Material::dataInputs,
                "Data inputs",
                new DataInputTable(
                    inputRules,
                    remoteDataComponent,
                    std::vector<DataInputTable::VerbOption>{
                        {std::string(MEngineeringTable::Verbs::TurnOn), "Turn on"},
                        {std::string(MEngineeringTable::Verbs::TurnOff), "Turn off"},
                    }
                )
            ),

            new TabList::Page(
                Material::dataOutputs,
                "Data outputs",
                makeDataOutputsTab()
            ),

            MachineConfigPanel::page(
                player,
                "main:engineeringTable",
                "/eng-table/config",
                blockRegistry.getBlockByName("main:engineeringTable").iconMaterial,
                *this
            ),

            new MachineHelpTab("articles/engineering-table", "/eng-table/help")
        ),

        new HorizontalSeparator(),

        new Label("Selected recipe:"),
        new Builder<int>(recipeUpdateCounter, [this](const int &) -> Widget*
        {
            if (this->outputStack.isEmpty())
            {
                return new Label("None", Color::faded);
            }

            std::vector<Widget*> rows;
            std::vector<Widget*> nextRow;

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

                    nextRow.clear();
                }

                nextRow.push_back(
                    new StaticItemSlot([stack]()
                    {
                        return stack;
                    })
                );
            }

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

            return new Row(
                new Column(rows),
                new Center(new IconLabel(Material::rightArrow)),
                new Center(
                    new StaticItemSlot([this]()
                    {
                        return outputStack;
                    })
                )
            );
        })
    );
}

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

    if (world.isLocal() && table != nullptr)
    {
        bool channelTableUpdated = table->syncChannelTable(channelTable);
        bool itemInputChannelTableUpdated = table->syncItemInputChannelTable(itemInputChannelTable);
        bool inputRulesUpdated = table->getComponent<DataCableComponent>().syncInputRules(inputRules);

        if (
            channelTableUpdated ||
            itemInputChannelTableUpdated ||
            inputRulesUpdated ||
            storedEnergy != table->getStoredEnergy() ||
            progress != table->getProgress() ||
            isSwitchedOnPriv != table->isSwitchedOn() ||
            recipeUpdateCounter != table->getRecipeUpdateCounter()
        )
        {
            storedEnergy = table->getStoredEnergy();
            progress = table->getProgress();
            isSwitchedOnPriv = table->isSwitchedOn();
            recipeUpdateCounter = table->getRecipeUpdateCounter();
            
            updateTableState(
                storedEnergy,
                progress,
                isSwitchedOnPriv,
                table->getInputStacks(),
                table->getOutputStack(),
                recipeUpdateCounter,
                channelTable,
                itemInputChannelTable,
                inputRules
            );
        }
    }
}

glm::vec4 EngineeringTableController::getSlotHighlight(const std::string &id, int slot, ItemStack &stack)
{
    if (id == "machine" && slot == 6*6)
    {
        return Color::rechargeHighlight;
    }

    if (id == "machine")
    {
        return InventoryController::getSlotHighlight(id, slot, stack);
    }

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

    for (ItemStack &inputStack : inputStacks)
    {
        if (stack.canMergeFrom(inputStack))
        {
            return Color::inputHighlight;
        }
    }

    IEnergyContainingItem *energyContainer = stack.getItemAs<IEnergyContainingItem>();
    if (energyContainer != nullptr)
    {
        return Color::rechargeHighlight;
    }

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