#include <MachineComponent/ChunkMonitorComponent/ChunkMonitorComponent.hpp>
#include <MachineComponent/MultiBlockComponent/MultiBlockComponent.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <WorldComponent/Wiring/DataWiring/DataWiring.hpp>
#include <WorldComponent/Wiring/ElectricWiring/ElectricWiring.hpp>
#include <Block/SpriteBlock/MultiBlock/MultiBlock.hpp>
#include <WorldComponent/Wiring/FluidWiring/FluidWiring.hpp>

std::string MultiBlockComponent::tagName = "main:multiBlock";

MultiBlockComponent::MultiBlockComponent(Machine &machine) :
    MachineComponent(machine),
    chunkMonitor(machine.attach<ChunkMonitorComponent>())
{
    chunkMonitor.addCanBreakRule([this](glm::ivec3 queryPos)
    {
        MultiBlock &multiBlock = static_cast<MultiBlock&>(*this->machine.getWorld().getBlockAt(this->machine.pos));
        int orientation = *this->machine.getWorld().getBlockMeta(this->machine.pos) & 3;
        return !multiBlock.structure.containsBlock(this->machine.getWorld(), this->machine.pos, orientation, queryPos);
    });
}

void MultiBlockComponent::onConnectorAdded(CasingConnectorType type, int8_t offsetX, int8_t offsetY, int8_t offsetZ, Side side)
{
    connectors.push_back({type, offsetX, offsetY, offsetZ, side});
    connectorChunkHandles.push_back(
        machine
            .getWorld()
            .requestChunk(glm::ivec3(offsetX, offsetY, offsetZ) + machine.pos + side.out())
    );
}

void MultiBlockComponent::onConnectorRemoved(int8_t offsetX, int8_t offsetY, int8_t offsetZ)
{
    size_t i;
    for (i=0; i<connectors.size(); i++)
    {
        CasingConnector &connector = connectors[i];
        if (connector.offsetX == offsetX && connector.offsetY == offsetY && connector.offsetZ == offsetZ)
        {
            connectors.erase(connectors.begin() + i);
            connectorChunkHandles.erase(connectorChunkHandles.begin() + i);
            return;
        }
    }
}

bool MultiBlockComponent::getCasingConnector(MultiBlockComponent::CasingConnectorType type, glm::ivec3 &outPos, Side &outSide)
{
    for (MultiBlockComponent::CasingConnector &connector : connectors)
    {
        if (connector.type == type)
        {
            glm::ivec3 connectionPos(
                (int)connector.offsetX + machine.pos.x,
                (int)connector.offsetY + machine.pos.y,
                (int)connector.offsetZ + machine.pos.z
            );

            connectionPos += connector.side.out();

            outPos = connectionPos;
            outSide = connector.side.opposite();

            return true;
        }
    }

    return false;
}

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

void MultiBlockComponent::deserializeState(const uint8_t *bytes, size_t size)
{
    deserialize(bytes, size, connectors);

    connectorChunkHandles.clear();
    for (const CasingConnector &connector : connectors)
    {
        connectorChunkHandles.push_back(
            machine
                .getWorld()
                .requestChunk(machine.pos + connector.side.out() + glm::ivec3(
                    connector.offsetX & ~0xF,
                    connector.offsetY & ~0xF,
                    connector.offsetZ & ~0xF
                ))
        );
    }
}

bool MultiBlockComponent::isReady()
{
    for (ChunkHandle &chunk : connectorChunkHandles)
    {
        if (!chunk->isReady())
        {
            return false;
        }
    }

    return true;
}

std::shared_ptr<ElectricLink> MultiBlockComponent::getEnergyInput()
{
    LocalWorld &world = machine.getWorld();
    glm::ivec3 portPos;
    Side portSide;

    if (!getCasingConnector(CasingConnectorType::EnergyInput, portPos, portSide))
    {
        return nullptr;
    }

    return world
        .getRootNode()
        ->getComponent<ElectricWiring>()
        .getLink(world, portPos, portSide);
}

std::shared_ptr<FluidLink> MultiBlockComponent::getFluidInput()
{
    LocalWorld &world = machine.getWorld();
    glm::ivec3 portPos;
    Side portSide;

    if (!getCasingConnector(CasingConnectorType::FluidInput, portPos, portSide))
    {
        return nullptr;
    }

    return world
        .getRootNode()
        ->getComponent<FluidWiring>()
        .getLink(world, portPos, portSide);
}

std::shared_ptr<DataLink> MultiBlockComponent::getDataLink()
{
    LocalWorld &world = machine.getWorld();
    glm::ivec3 portPos;
    Side portSide;

    if (!getCasingConnector(CasingConnectorType::Data, portPos, portSide))
    {
        return nullptr;
    }

    return world
        .getRootNode()
        ->getComponent<DataWiring>()
        .getLink(world, portPos, portSide);
}

glm::ivec3 MultiBlockComponent::getCasingRelativePos(glm::ivec3 uvz)
{
    int orientation = *machine.getWorld().getBlockMeta(machine.pos) & 3;

    return uvz.x * MultiBlock::Structure::getRightVector(orientation) +
        uvz.y * MultiBlock::Structure::getIntoVector(orientation) +
        uvz.z * glm::ivec3(0, 0, 1);
}