#include <DataLink/DataLink.hpp>
#include <Log/Log.hpp>
#include <Machine/MLogicBox/MLogicBox.hpp>
#include <MachineComponent/DataCableComponent/DataCableComponent.hpp>
#include <World/LocalWorld/LocalWorld.hpp>

bool MLogicBox::Operation::operator==(const Operation &other)
{
    return left == other.left && op == other.op && right == other.right;
}

bool MLogicBox::computeOutput(bool a, Operator op, bool b)
{
    switch (op)
    {
        case Operator::AlwaysOff: return false;
        case Operator::AlwaysOn: return true;
        case Operator::Copy: return b;
        case Operator::Not: return !b;
        case Operator::And: return a && b;
        case Operator::Or: return a || b;
        case Operator::Xor: return a != b;
        default:
        {
            Log::warn(
                formatString(
                    "MLogicBox at $+: unrecognised operator ID '$+' (returning false as fallback)",
                    pos,
                    static_cast<int>(op)
                )
            );
            return false;
        }
    }
}

uint8_t MLogicBox::computeCombinedOutput(uint8_t input)
{
    uint8_t result = 0;

    for (int channel = 0; channel < 4; channel++)
    {
        auto &op = channelOps[channel];
        bool a = input & (1 << static_cast<int>(op.left));
        bool b = input & (1 << static_cast<int>(op.right));

        if (computeOutput(a, op.op, b))
        {
            result |= 1 << channel;
        }
    }

    return result;
}

void MLogicBox::updateLookupTable()
{
    for (uint8_t offset = 0; offset < 8; offset++)
    {
        uint8_t lookupTableByte = computeCombinedOutput((offset << 1) | 1);
        lookupTableByte <<= 4;
        lookupTableByte |= computeCombinedOutput(offset << 1);

        lookupTable[offset] = lookupTableByte;
    }
}

bool MLogicBox::isChannelOpListSame(const std::array<Operation, 4> &ops)
{
    for (size_t i = 0; i < 4; i++)
    {
        if (ops[i] != channelOps[i])
        {
            return false;
        }
    }

    return true;
}

MLogicBox::MLogicBox(LocalWorld &world, glm::ivec3 pos) : Machine(world, pos)
{
    attach<DataCableComponent>();
}

void MLogicBox::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, channelOps);
}

void MLogicBox::deserializeState(const uint8_t *data, size_t size)
{
    deserialize(data, size, channelOps);
    updateLookupTable();
}

void MLogicBox::saveMainConfig(MachineConfig &outConfig)
{
    outConfig.blockName = "main:logicBox";
    outConfig.label = "Logic Box";

    serializeInto(outConfig.components[""], channelOps);
}

void MLogicBox::loadMainConfig(const MachineConfig &config, bool consumed)
{
    const auto &data = config.components.at("");
    deserialize(data.data(), data.size(), channelOps);
    updateLookupTable();
}

Side MLogicBox::getDataInputSide()
{
    return Side(*m_world.getBlockMeta(pos));
}

Side MLogicBox::getDataOutputSide()
{
    return getDataInputSide().opposite();
}

DataLink::ChannelBitmap MLogicBox::getActiveChannels()
{
    auto &dataComponent = getComponent<DataCableComponent>();
    uint8_t inputs = dataComponent.getLastKnownInputs().unpack();

    uint8_t result = lookupTable[inputs >> 1];
    if (inputs & 1)
    {
        result >>= 4;
    }

    result &= 0xF;
    
    DataLink::ChannelBitmap bitmap;
    bitmap = result;
    return bitmap;
}