#include <Block/SpriteBlock/MultiBlock/MultiBlock.hpp>
#include <Utilities/Utilities.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <MachineComponent/MultiBlockComponent/MultiBlockComponent.hpp>

std::vector<MultiBlock::Sprite> MultiBlock::makeSprites(std::string pureName, const std::vector<std::string> &stateNames)
{
    std::vector<Sprite> sprites;

    for (uint8_t meta = 0; meta < stateNames.size() << 2; meta++)
    {
        std::string stateName = stateNames[meta >> 2];
        std::string texName = formatString("$+/$+", pureName, stateName);

        sprites.push_back({
            {texName, texName, texName, texName, texName, texName},
            {{0, 0, 0, 8, 8, 8}},
            stateName,
        });
    }

    return sprites;
}

glm::ivec3 MultiBlock::Structure::getRightVector(int orientation)
{
    switch (orientation)
    {
    case 0: return glm::ivec3(1, 0, 0);
    case 1: return glm::ivec3(0, -1, 0);
    case 2: return glm::ivec3(-1, 0, 0);
    case 3: return glm::ivec3(0, 1, 0);
    default: crash(formatString("Invalid multi-block structure orientation $+", orientation));
    }
}

glm::ivec3 MultiBlock::Structure::getIntoVector(int orientation)
{
    switch (orientation)
    {
    case 0: return glm::ivec3(0, 1, 0);
    case 1: return glm::ivec3(1, 0, 0);
    case 2: return glm::ivec3(0, -1, 0);
    case 3: return glm::ivec3(-1, 0, 0);
    default: crash(formatString("Invalid multi-block structure orientation $+", orientation));;
    }
}

void MultiBlock::Structure::add(int u, int v, int z, std::string blockName)
{
    Element elem{u, v, z, blockName};
    elements.push_back(elem);
}

MultiBlock::Structure::State MultiBlock::Structure::getStateAt(
    World &world,
    glm::ivec3 origin,
    int orientation,
    MultiBlockComponent *multiBlockComp
)
{
    std::vector<MultiBlockComponent::CasingConnector> outConnectors;

    glm::ivec3 right = getRightVector(orientation);
    glm::ivec3 into = getIntoVector(orientation);

    std::optional<ChunkHandle> chunk;
    for (Element &elem : elements)
    {
        glm::ivec3 blockPos = origin + elem.u * right + elem.v * into;
        blockPos.z += elem.z;

        glm::ivec3 chunkBase = blockPos & glm::ivec3(~0xF);
        if (!chunk.has_value() || (*chunk)->base != chunkBase)
        {
            chunk = world.requestChunk(chunkBase);
            if (!(*chunk)->isReady())
            {
                return State::NotReady;
            }
        }

        if (multiBlockComp != nullptr)
        {
            multiBlockComp->chunkMonitor.monitor(*chunk);
        }

        const auto [ blockId, meta ] = (*chunk)->getBlockAndMeta(blockPos & glm::ivec3(0xF));
        Block &block = world.getBlockById(blockId);
        if (block.name != elem.blockName)
        {
            return State::Absent;
        }

        auto connectorType = static_cast<MultiBlockComponent::CasingConnectorType>(meta & 0xF);
        if (connectorType == MultiBlockComponent::CasingConnectorType::None)
        {
            continue;
        }

        bool found = false;
        for (MultiBlockComponent::CasingConnector &existingConnector : outConnectors)
        {
            if (existingConnector.type == connectorType)
            {
                found = true;
                break;
            }
        }

        if (!found)
        {
            glm::ivec3 offset = blockPos - origin;
            Side side(meta >> 4);

            MultiBlockComponent::CasingConnector connector{
                connectorType,
                (int8_t) offset[0],
                (int8_t) offset[1],
                (int8_t) offset[2],
                side
            };

            outConnectors.push_back(connector);

            if (multiBlockComp != nullptr)
            {
                multiBlockComp->onConnectorAdded(connectorType, (int8_t) offset[0], (int8_t) offset[1], (int8_t) offset[2], side);
            }
        }
    }

    return State::Present;
}

bool MultiBlock::Structure::containsBlock(
    World &world,
    glm::ivec3 origin, int orientation,
    glm::ivec3 queryPos
)
{
    glm::ivec3 right = getRightVector(orientation);
    glm::ivec3 into = getIntoVector(orientation);

    for (Element &elem : elements)
    {
        glm::ivec3 blockPos = origin + elem.u * right + elem.v * into;
        blockPos.z += elem.z;

        if (blockPos == queryPos)
        {
            return true;
        }
    }

    return false;
}

MultiBlock::MultiBlock(std::string name, std::string displayName, std::vector<std::string> stateNames) :
    SpriteBlock(
        name,
        displayName,
        60.0f,
        formatString("$+/$+", name.substr(name.find(':')+1), stateNames[0]),
        makeSprites(name.substr(name.find(':')+1), stateNames)
    )
{}

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

bool MultiBlock::canPlace(LocalWorld &world, IWorldActor &actor, glm::ivec3 origin, Side side, uint8_t &meta)
{
    int orientation;
    for (orientation=0; orientation<4; orientation++)
    {        
        Structure::State state = structure.getStateAt(world, origin, orientation, nullptr);
        if (state == Structure::State::Present)
        {
            meta = orientation;
            return true;
        }
    }

    return false;
}

bool MultiBlock::isProtectorOf(World &world, glm::ivec3 myPos, glm::ivec3 otherPos)
{
    int orientation = *world.getBlockMeta(myPos) & 3;

    return structure.containsBlock(world, myPos, orientation, otherPos);
}

void MultiBlock::onPlaced(LocalWorld &world, Chunk &chunk, glm::ivec3 pos)
{
    int orientation = *world.getBlockMeta(pos) & 3;
    std::shared_ptr<Machine> machine = world.getMachine(pos);
    MultiBlockComponent &multiBlockComp = machine->getComponent<MultiBlockComponent>();
    structure.getStateAt(world, pos, orientation, &multiBlockComp);
}