#include <WorldComponent/Wiring/FluidWiring/FluidWiring.hpp>
#include <WorldGridNode/WorldGridNode.hpp>
#include <Block/SpriteBlock/Casing/Casing.hpp>
#include <Utilities/Utilities.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <WorldComponent/Wiring/ElectricWiring/ElectricWiring.hpp>
#include <WorldComponent/Wiring/DataWiring/DataWiring.hpp>

void Casing::createPorts(LocalWorld &world, glm::ivec3 pos, uint8_t meta, glm::ivec3 machinePos)
{
    Side portSide(meta >> 4);
    MultiBlockComponent::CasingConnectorType connectorType = static_cast<MultiBlockComponent::CasingConnectorType>(meta & 0xF);
    std::shared_ptr<Chunk> chunk = world.getChunk(pos & ~0xF);

    switch (connectorType)
    {
    case MultiBlockComponent::CasingConnectorType::EnergyInput:
    case MultiBlockComponent::CasingConnectorType::EnergyOutput:
        chunk->getNode()->getComponent<ElectricWiring>().onMachineSidePlaced(world, chunk->getNode(), pos, portSide, machinePos);
        break;
    case MultiBlockComponent::CasingConnectorType::Data:
        chunk->getNode()->getComponent<DataWiring>().onMachineSidePlaced(world, chunk->getNode(), pos, portSide, machinePos);
        break;
    case MultiBlockComponent::CasingConnectorType::FluidInput:
    case MultiBlockComponent::CasingConnectorType::FluidOutput:
        chunk->getNode()->getComponent<FluidWiring>().onMachineSidePlaced(world, chunk->getNode(), pos, portSide, machinePos);
    default:
        break;
    }
}

void Casing::removePorts(LocalWorld &world, glm::ivec3 pos, uint8_t meta)
{
    Side portSide(meta >> 4);
    MultiBlockComponent::CasingConnectorType connectorType = static_cast<MultiBlockComponent::CasingConnectorType>(meta & 0xF);
    std::shared_ptr<Chunk> chunk = world.getChunk(pos & ~0xF);

    switch (connectorType)
    {
    case MultiBlockComponent::CasingConnectorType::EnergyInput:
    case MultiBlockComponent::CasingConnectorType::EnergyOutput:
        chunk->getNode()->getComponent<ElectricWiring>().onMachineSideRemoved(world, chunk->getNode(), pos, portSide);
        break;
    case MultiBlockComponent::CasingConnectorType::Data:
        chunk->getNode()->getComponent<DataWiring>().onMachineSideRemoved(world, chunk->getNode(), pos, portSide);
        break;
    case MultiBlockComponent::CasingConnectorType::FluidInput:
    case MultiBlockComponent::CasingConnectorType::FluidOutput:
        chunk->getNode()->getComponent<FluidWiring>().onMachineSideRemoved(world, chunk->getNode(), pos, portSide);
    default:
        break;
    }
}

std::vector<Casing::Sprite> Casing::makeSprites(std::string kindName)
{
    static const char *typeNames[] = {
        "blank",
        "electricInput",
        "electricOutput",
        "data",
        "fluidInput",
        "fluidOutput",
    };

    std::vector<Sprite> sprites;

    for (uint8_t meta = 0; meta < 16 * 6; meta++)
    {
        Side portSide(meta >> 4);
        uint8_t portType = meta & 0xF;

        if (portType >= sizeof(typeNames)/sizeof(char*))
        {
            sprites.push_back({});
            continue;
        }

        Sprite sprite = {
            {},
            {{0, 0, 0, 8, 8, 8}},
            formatString("$+$+", typeNames[portType], portSide),
        };

        for (const Side &side : Side::all)
        {
            if (side == portSide)
            {
                sprite.texturesOnSides[side.asNumber()] = formatString("casing/$+/$+", kindName, typeNames[portType]);
            }
            else
            {
                sprite.texturesOnSides[side.asNumber()] = formatString("casing/$+/blank", kindName);
            }
        }

        sprites.push_back(sprite);
    }

    return sprites;
}

Casing::Casing(std::string name, std::string displayName, std::string kindName) :
    SpriteBlock(
        name,
        displayName,
        60.0f,
        formatString("casing/$+/blank", kindName),
        makeSprites(kindName)
    )
{}

bool Casing::canManuallyDig()
{
    return true;
}

void Casing::onInteract(LocalWorld &world, Player &player, glm::ivec3 pos, Side side, const RGBABytes &collisionKey)
{
    glm::ivec3 protectorPos;
    for (protectorPos.z=pos.z-7; protectorPos.z<=pos.z+7; protectorPos.z++)
    {
        for (protectorPos.y=pos.y-7; protectorPos.y<=pos.y+7; protectorPos.y++)
        {
            for (protectorPos.x=pos.x-7; protectorPos.x<=pos.x+7; protectorPos.x++)
            {
                Block *protector = world.getBlockAt(protectorPos);
                if (protector == nullptr)
                {
                    SpriteBlock::onInteract(world, player, pos, side, collisionKey);
                    return;
                }

                if (protector->isProtectorOf(world, protectorPos, pos))
                {
                    if (player.isHoldingScrewdriver())
                    {
                        uint8_t currentMeta = *world.getBlockMeta(pos);
                        Side currentSide(currentMeta >> 4);
                        
                        uint8_t newMeta;

                        if (currentSide != side)
                        {
                            newMeta = (side.asNumber() << 4) | 1;
                        }
                        else
                        {
                            uint8_t currentType = currentMeta & 0xF;
                            uint8_t newType = (currentType + 1) % static_cast<uint8_t>(MultiBlockComponent::CasingConnectorType::TypesCount);
                            newMeta = (currentMeta & 0xF0) | newType;
                        }

                        std::shared_ptr<Machine> machine = world.getMachine(protectorPos);
                        MultiBlockComponent &multiBlockComp = machine->getComponent<MultiBlockComponent>();

                        int8_t offsetX = pos.x - protectorPos.x;
                        int8_t offsetY = pos.y - protectorPos.y;
                        int8_t offsetZ = pos.z - protectorPos.z;

                        multiBlockComp.onConnectorRemoved(offsetX, offsetY, offsetZ);
    
                        if ((newMeta & 0xF) != 0)
                        {
                            MultiBlockComponent::CasingConnectorType connectorType = static_cast<MultiBlockComponent::CasingConnectorType>(newMeta & 0xF);
                            multiBlockComp.onConnectorAdded(connectorType, offsetX, offsetY, offsetZ, side);
                        }

                        world.setBlockMeta(pos, newMeta, true);
                        removePorts(world, pos, currentMeta);
                        createPorts(world, pos, newMeta, protectorPos);

                        return;
                    }

                    protector->onInteract(world, player, protectorPos, side, collisionKey);
                    return;
                }
            }
        }
    }

    SpriteBlock::onInteract(world, player, pos, side, collisionKey);
}