#include <Controller/InventoryController/BeltJunctionController/BeltJunctionController.hpp>
#include <Controller/InventoryController/InventoryController.hpp>
#include <Log/Log.hpp>
#include <Machine/MBeltJunction/MBeltJunction.hpp>
#include <Widget/IconToggle/IconToggle.hpp>
#include <Widget/Layout/Center/Center.hpp>
#include <Widget/Layout/CompoundWidget/MachineConfigPanel/MachineConfigPanel.hpp>
#include <Widget/Layout/Frame/Frame.hpp>
#include <Widget/Label/Label.hpp>
#include <Widget/Button/Button.hpp>
#include <Widget/Layout/Margin/Margin.hpp>
#include <Widget/Layout/Row/VerticalSeparator/VerticalSeparator.hpp>
#include <Widget/Layout/ScrollPanel/ScrollPanel.hpp>
#include <Widget/Panel/Panel.hpp>
#include <Widget/Layout/Column/Column.hpp>
#include <Widget/Builder/Builder.hpp>
#include <Utilities/Utilities.hpp>
#include <Widget/StaticItemSlot/StaticItemSlot.hpp>
#include <Widget/Switch/Switch.hpp>
#include <Widget/Layout/Row/Row.hpp>
#include <Widget/Layout/Column/HorizontalSeparator/HorizontalSeparator.hpp>
#include <Widget/TabList/TabList.hpp>
#include <World/World.hpp>
#include <Widget/Layout/CompoundWidget/DataInputTable/DataInputTable.hpp>
#include <Widget/ItemListSelector/ItemListSelector.hpp>
#include <Widget/Layout/CompoundWidget/DataChannelPicker/DataChannelPicker.hpp>
#include <Widget/ItemFilterDropdown/ItemFilterDropdown.hpp>
#include <Widget/Layout/CompoundWidget/SpinBox/SpinBox.hpp>
#include <cstdint>

Widget* BeltJunctionController::makeFilterTab()
{
    return new Column(
        Layout::WithProportion<1>(),
        new ScrollPanel(
            "/belt-junction/rules/scroll-panel",
            rulesNaturalHeight,
            makeRuleList()
        ),

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

        new Row(
            (new Button("", [this]()
                {
                    addRule(0);
                }))
                ->withIcon(Material::create)
                ->withToolTip("Add filter rule")
                ->enabledIf([this](){ return rules.size() < MBeltJunction::maxRules; }),
            
            Layout::WithProportion<1>(),
            new Panel(),

            new Center(
                new Builder<int>(
                    ruleUpdateCounter,
                    [this](int) -> Widget*
                    {
                        return new Label(formatString("$+/$+", rules.size(), MBeltJunction::maxRules), Color::faded);
                    }
                )
            )
        )
    );
}

Widget* BeltJunctionController::makeOutputBeltPicker(MBeltJunction::OutputBelt &belt, std::function<void()> onChanged)
{
    return new Row(
        (new IconToggle(belt, MBeltJunction::OutputBelt::Neg, Material::outputBeltNeg))->withChangedCallback(onChanged),
        (new IconToggle(belt, MBeltJunction::OutputBelt::Pos, Material::outputBeltPos))->withChangedCallback(onChanged),
        (new IconToggle(belt, MBeltJunction::OutputBelt::Def, Material::outputBeltDefault))->withChangedCallback(onChanged)
    );
}

Widget* BeltJunctionController::makeRule(size_t index)
{
    MBeltJunction::Rule &rule = rules[index];

    return new Frame(
        (index & 1) ? Color::tableRowOdd : Color::tableRowEven,
        new Row(
            new Center(
                new Column(
                    new Row(
                        new Center(new Label("WHEN")),
                        new Center(
                            new DataChannelPicker(
                                rule.controlChannel,
                                [this, &rule, index]()
                                {
                                    setRuleControlChannel(index, rule.controlChannel);
                                }
                            )
                        )
                    ),

                    new Row(
                        new Center(new Label("EVERY")),
                        new Center(
                            new SpinBox<uint32_t>(rule.interval, 1u, MBeltJunction::maxInterval, [this, index, &rule]()
                            {
                                setRuleInterval(index, rule.interval);
                            })
                        ),
                        new Panel(glm::ivec2(2, 0)),
                        new Center(
                            new ItemFilterDropdown(
                                rule.refStack,
                                [this, index, &rule]()
                                {
                                    setRuleRefStack(index, rule.refStack);
                                }
                            )
                        )
                    ),

                    new Row(
                        new Center(new Label("GOES TO")),
                        new Center(
                            makeOutputBeltPicker(
                                rule.outputBelt,
                                [this, &rule, index]()
                                {
                                    setRuleOutputBelt(index, rule.outputBelt);
                                }
                            )
                        )
                    )
                )
            ),

            new VerticalSeparator(),

            new Center(
                (new Button("", [this, index]()
                    {
                        swapRules(index, index-1);
                    }))
                    ->withToolTip("Move up")
                    ->enabledIf([index]() { return index != 0; })
                    ->withIcon(Material::moveUp)
            ),

            new Center(
                (new Button("", [this, index]()
                    {
                        swapRules(index+1, index);
                    }))
                    ->withToolTip("Move down")
                    ->enabledIf([this, index]() { return index != rules.size() - 1; })
                    ->withIcon(Material::moveDown)
            ),

            new Center(
                (new Button("", [this, index]()
                    {
                        deleteRule(index);
                    }))
                    ->withToolTip("Delete rule")
                    ->withFaceColor(Color::danger)
                    ->withIcon(Material::trash)
            )
        )
    );
}

Widget* BeltJunctionController::makeRuleList()
{
    return new Builder<int>(
        ruleUpdateCounter,
        [this](int) -> Widget*
        {
            std::vector<Widget*> rows;

            for (size_t i = 0; i < rules.size(); i++)
            {
                rows.push_back(makeRule(i));
            }

            return new Column(rows);
        }
    );
}

Widget* BeltJunctionController::makeStatsTab()
{
    return new Column(
        Layout::WithProportion<1>(),
        new ScrollPanel(
            "/belt-junction/stats/scroll-panel",
            rulesNaturalHeight,
            makeItemStatsList()
        ),

        new Margin<2>(
            new Builder<uint64_t>(
                stats.totalItemsProcessed,
                [](uint64_t count)
                {
                    return new Label(formatString("Total items processed: $+", count));
                }
            )
        )
    );
}

Widget* BeltJunctionController::makeItemStatsList()
{
    return new Builder<int>(
        statUpdateCounter,
        [this](int) -> Widget*
        {
            std::vector<Widget*> rows;

            for (size_t i = 0; i < stats.itemStats.size(); i++)
            {
                rows.push_back(makeItemStat(i));
            }

            return new Column(rows);
        }
    );
}

Widget* BeltJunctionController::makeItemStat(size_t index)
{
    MBeltJunction::ItemStats &itemStats = stats.itemStats[index];

    return new Frame(
        (index & 1) ? Color::tableRowOdd : Color::tableRowEven,
        new Row(
            new Center(new StaticItemSlot([&itemStats](){ return itemStats.refStack; })),
            new VerticalSeparator(),
            Layout::WithProportion<1>(),
            new Center(new Label(formatString("$+", itemStats.sentToNeg), glm::vec4(1.0f, 0.0f, 0.0f, 1.0f))),
            new VerticalSeparator(),
            Layout::WithProportion<1>(),
            new Center(new Label(formatString("$+", itemStats.sentToPos), glm::vec4(0.0f, 1.0f, 0.0f, 1.0f))),
            new VerticalSeparator(),
            Layout::WithProportion<1>(),
            new Center(new Label(formatString("$+", itemStats.sentToDef), glm::vec4(0.0f, 0.0f, 1.0f, 1.0f)))
        )
    );
}

BeltJunctionController::BeltJunctionController(ControllerChannel &channel) :
    InventoryController(channel),
    updateJunctionState(this, RunsOn::Client,
        [this](
            uint32_t ruleUpdateCounter,
            std::vector<MBeltJunction::Rule> rules,
            uint32_t statUpdateCounter,
            MBeltJunction::Stats stats
    ) -> uint32_t
    {
        if (this->ruleUpdateCounter != ruleUpdateCounter)
        {
            this->ruleUpdateCounter = ruleUpdateCounter;
            this->rules = rules;
        }

        if (this->statUpdateCounter != statUpdateCounter)
        {
            this->statUpdateCounter = statUpdateCounter;
            this->stats = stats;
        }

        return 0;
    }),
    addRule(this, RunsOn::Server,
        [this](
            uint32_t ignore
    ) -> uint32_t
    {
        if (junction != nullptr)
        {
            junction->addRule();
        }

        return 0;
    }),
    deleteRule(this, RunsOn::Server,
        [this](
            uint32_t index
    ) -> uint32_t
    {
        if (junction != nullptr)
        {
            junction->deleteRule(index);
        }

        return 0;
    }),
    setRuleControlChannel(this, RunsOn::Server,
        [this](
            uint32_t index,
            std::optional<DataLink::Channel> controlChannel
    ) -> uint32_t
    {
        if (junction != nullptr)
        {
            junction->setRuleControlChannel(index, controlChannel);
        }

        return 0;
    }),
    setRuleOutputBelt(this, RunsOn::Server,
        [this](
            uint32_t index,
            MBeltJunction::OutputBelt belt
    ) -> uint32_t
    {
        if (junction != nullptr)
        {
            junction->setRuleOutputBelt(index, belt);
        }

        return 0;
    }),
    setRuleInterval(this, RunsOn::Server,
        [this](
            uint32_t index,
            uint32_t interval
    ) -> uint32_t
    {
        if (junction != nullptr)
        {
            junction->setRuleInterval(index, interval);
        }

        return 0;
    }),
    setRuleRefStack(this, RunsOn::Server,
        [this](
            uint32_t index,
            ItemStack stack
    ) -> uint32_t
    {
        if (junction != nullptr)
        {
            junction->setRuleRefStack(index, stack);
        }

        return 0;
    }),
    swapRules(this, RunsOn::Server,
        [this](
            uint32_t a,
            uint32_t b
    ) -> uint32_t
    {
        if (junction != nullptr)
        {
            junction->swapRules(a, b);
        }

        return 0;
    })
{}

void BeltJunctionController::onConnect(std::shared_ptr<Machine> beltJunctionMachine)
{
    bindMachine(junction = std::dynamic_pointer_cast<MBeltJunction>(beltJunctionMachine));
}

Widget* BeltJunctionController::getRightPanel()
{
    return new Column(
        new Center(new Label("Belt Junction")),

        Layout::WithProportion<1>(),
        new TabList(
            "/belt-junction/tab-list",
            new TabList::Page(
                Material::filterIcon,
                "Filters",
                makeFilterTab()
            ),

            new TabList::Page(
                Material::statsIcon,
                "Stats",
                makeStatsTab()
            ),

            MachineConfigPanel::page(
                player,
                "main:beltJunction",
                "/belt-junction/config",
                Material::filterIcon,
                *this
            )
        )
    );
}

bool BeltJunctionController::isBusy()
{
    return false;
}

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

    if (world.isLocal() && junction != nullptr)
    {
        bool shouldUpdate = false;

        if (ruleUpdateCounter != junction->getRuleUpdateCounter())
        {
            ruleUpdateCounter = junction->getRuleUpdateCounter();
            rules = junction->getRules();

            shouldUpdate = true;
        }

        if (statUpdateCounter != junction->getStatUpdateCounter())
        {
            statUpdateCounter = junction->getStatUpdateCounter();
            stats = junction->getStats();

            shouldUpdate = true;
        }

        if (shouldUpdate)
        {
            updateJunctionState(ruleUpdateCounter, rules, statUpdateCounter, stats);
        }
    }
}