#include <memory>

#include <FluidLink/FluidLink.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <Machine/Machine.hpp>
#include <WorldComponent/Wiring/FluidWiring/FluidWiring.hpp>
#include <MachineComponent/FluidComponent/FluidComponent.hpp>
#include <Block/FluidBlock/FluidBlock.hpp>

std::shared_ptr<FluidLink> FluidLink::dummyLink = std::make_shared<FluidLink>();

FluidLink::FluidLink(
    LocalWorld &world,
    std::shared_ptr<WorldGridNode> node,
    std::vector<Wiring::Port> &ports,
    size_t startingPort
) : world(&world),
    pos(ports[startingPort].pos),
    side(ports[startingPort].side)
{
    Wiring::Port &ourPort = ports[startingPort];
    if (ourPort.connections.size() == 0)
    {
        std::shared_ptr<Machine> machine = world.getMachine(ourPort.pos);

        if (machine != nullptr)
        {
            endpoints.push_back({machine, ourPort.side});
            return;
        }

        return;
    }

    for (Wiring::Connection &connection : ourPort.connections)
    {
        Wiring::Port &targetPort = ports[connection.targetPort];
        std::shared_ptr<Machine> machine = world.getMachine(targetPort.machinePos);

        if (std::dynamic_pointer_cast<IFluidMachine>(machine) != nullptr)
        {
            endpoints.push_back({machine, targetPort.side.opposite()});
        }
    }
}

std::shared_ptr<FluidLink> FluidLink::get(Machine &machine, Side side)
{
    LocalWorld &world = machine.getWorld();
    return world
        .getRootNode()
        ->getComponent<FluidWiring>()
        .getLink(world, machine.pos + side.out(), side.opposite());
}

std::optional<std::string> FluidLink::getDefaultFluid()
{
    for (Endpoint &endpoint : endpoints)
    {
        std::shared_ptr<Machine> machine = endpoint.machine.lock();
        if (machine == nullptr)
        {
            continue;
        }

        std::optional<std::string> optFluid = machine
            ->getComponent<FluidComponent>()
            .getDefaultFluidOnSide(endpoint.side);
        
        if (optFluid.has_value())
        {
            return optFluid;
        }
    }

    FluidBlock *fluidBlock = dynamic_cast<FluidBlock*>(world->getBlockAt(pos));
    if (fluidBlock == nullptr)
    {
        return std::nullopt;
    }

    return fluidBlock->fluidName;
}

int FluidLink::pullFluid(std::string fluidName, int amount)
{
    if (amount == 0)
    {
        return 0;
    }

    int pulledSoFar = 0;

    for (Endpoint &endpoint : endpoints)
    {
        std::shared_ptr<Machine> machine = endpoint.machine.lock();
        if (machine == nullptr)
        {
            continue;
        }

        pulledSoFar += machine
            ->getComponent<FluidComponent>()
            .pullFluid(endpoint.side, fluidName, amount - pulledSoFar);

        if (pulledSoFar == amount)
        {
            return pulledSoFar;
        }
    }

    FluidBlock *fluidBlock = dynamic_cast<FluidBlock*>(world->getBlockAt(pos));
    if (fluidBlock == nullptr)
    {
        return pulledSoFar;
    }

    int amountInBlock = (int) *world->getBlockMeta(pos) + 1;
    int amountToPull = std::min(amountInBlock, amount - pulledSoFar);

    amountInBlock -= amountToPull;
    pulledSoFar += amountToPull;

    if (amountInBlock == 0)
    {
        world->setBlockAt(pos, 0);
    }
    else
    {
        world->setBlockMeta(pos, (uint8_t) (amountInBlock - 1));
    }

    return pulledSoFar;
}

int FluidLink::pushFluid(std::string fluidName, int amount)
{
    int pushedSoFar = 0;

    for (Endpoint &endpoint : endpoints)
    {
        if (pushedSoFar == amount)
        {
            break;
        }

        std::shared_ptr<Machine> machine = endpoint.machine.lock();
        if (machine == nullptr)
        {
            continue;
        }

        pushedSoFar += machine
            ->getComponent<FluidComponent>()
            .pushFluid(endpoint.side, fluidName, amount - pushedSoFar);
    }

    return pushedSoFar;
}