#include <Controller/InventoryController/LogicBoxController/LogicBoxController.hpp>
#include <Machine/MLogicBox/MLogicBox.hpp>
#include <Material/Material.hpp>
#include <Widget/EnumSelector/EnumSelector.hpp>
#include <Widget/IconLabel/IconLabel.hpp>
#include <Widget/Layout/Center/Center.hpp>
#include <Widget/Layout/CompoundWidget/DataChannelSelector/DataChannelSelector.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/Panel/Panel.hpp>
#include <Widget/Layout/Column/Column.hpp>
#include <Widget/Builder/Builder.hpp>
#include <Utilities/Utilities.hpp>
#include <Widget/Layout/Row/Row.hpp>
#include <Widget/Layout/Column/HorizontalSeparator/HorizontalSeparator.hpp>
#include <World/World.hpp>

Widget* LogicBoxController::makeChannelRow(Material &channelIcon, size_t channelNumber)
{
    return new Margin<2>(
        new Row(
            new Center(new IconLabel(channelIcon)),
            new Center(new Label("=")),
            new Center(
                new Builder<bool>(
                    [this, channelNumber]()
                    {
                        return static_cast<int>(channelOps[channelNumber].op) >= static_cast<int>(MLogicBox::Operator::And);
                    },
                    [this, channelNumber](bool hasLeftOperand) -> Widget*
                    {
                        if (!hasLeftOperand)
                        {
                            return new Panel(glm::ivec2(48, 12), Color::disabled);
                        }

                        return new DataChannelSelector(
                            channelOps[channelNumber].left,
                            [this, channelNumber](){ setLeftInput(channelNumber, channelOps[channelNumber].left); }
                        );
                    }
                )
            ),
            new Panel(glm::ivec2(2, 0)),
            new Center(
                new EnumSelector<MLogicBox::Operator>(
                    channelOps[channelNumber].op,
                    {
                        {MLogicBox::Operator::AlwaysOff, "Always off"},
                        {MLogicBox::Operator::AlwaysOn, "Always on"},
                        {MLogicBox::Operator::Copy, "Copy input:"},
                        {MLogicBox::Operator::Not, "NOT"},
                        {MLogicBox::Operator::And, "AND"},
                        {MLogicBox::Operator::Or, "OR"},
                        {MLogicBox::Operator::Xor, "XOR"},
                    },
                    [this, channelNumber]() { setOperator(channelNumber, channelOps[channelNumber].op); }
                )
            ),
            new Panel(glm::ivec2(2, 0)),
            new Center(
                new Builder<bool>(
                    [this, channelNumber]()
                    {
                        return static_cast<int>(channelOps[channelNumber].op) >= static_cast<int>(MLogicBox::Operator::Copy);
                    },
                    [this, channelNumber](bool hasRightOperand) -> Widget*
                    {
                        if (!hasRightOperand)
                        {
                            return new Panel(glm::ivec2(48, 12), Color::disabled);
                        }

                        return new DataChannelSelector(
                            channelOps[channelNumber].right,
                            [this, channelNumber](){ setRightInput(channelNumber, channelOps[channelNumber].right); }
                        );
                    }
                )
            )
        )
    );
}

LogicBoxController::LogicBoxController(ControllerChannel &channel) :
    InventoryController(channel),
    updateLogicBoxState(this, RunsOn::Client,
        [this](std::array<MLogicBox::Operation, 4> channelOps) -> uint32_t
    {
        this->channelOps = channelOps;

        return 0;
    }),
    setLeftInput(this, RunsOn::Server,
        [this](uint8_t channelNumber, DataLink::Channel left) -> uint32_t
    {
        if (logicBox == nullptr)
        {
            return 1;
        }

        this->channelOps[channelNumber].left = left;
        logicBox->channelOps[channelNumber].left = left;
        logicBox->updateLookupTable();

        return 0;
    }),
    setRightInput(this, RunsOn::Server,
        [this](uint8_t channelNumber, DataLink::Channel right) -> uint32_t
    {
        if (logicBox == nullptr)
        {
            return 1;
        }

        this->channelOps[channelNumber].right = right;
        logicBox->channelOps[channelNumber].right = right;
        logicBox->updateLookupTable();

        return 0;
    }),
    setOperator(this, RunsOn::Server,
        [this](uint8_t channelNumber, MLogicBox::Operator op) -> uint32_t
    {
        if (logicBox == nullptr)
        {
            return 1;
        }

        this->channelOps[channelNumber].op = op;
        logicBox->channelOps[channelNumber].op = op;
        logicBox->updateLookupTable();

        return 0;
    })
{}

void LogicBoxController::onConnect(std::shared_ptr<Machine> logicBoxMachine)
{
    bindMachine(logicBox = std::dynamic_pointer_cast<MLogicBox>(logicBoxMachine));
}

Widget* LogicBoxController::getRightPanel()
{
    return new Column(
        new Center(new Label("Logic Box")),

        makeChannelRow(Material::redChannel, 0),
        makeChannelRow(Material::greenChannel, 1),
        makeChannelRow(Material::blueChannel, 2),
        makeChannelRow(Material::magentaChannel, 3),

        Layout::WithProportion<1>(),
        new MachineConfigPanel(
            player,
            "main:logicBox",
            "/logic-box/config",
            Material::dataOutputs,
            *this
        )
    );
}

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

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

    if (world.isLocal() && logicBox != nullptr)
    {
        if (!logicBox->isChannelOpListSame(channelOps))
        {
            channelOps = logicBox->channelOps;
            
            updateLogicBoxState(channelOps);
        }
    }
}