#include <MachineComponent/FluidComponent/FluidComponent.hpp>
#include <Machine/Machine.hpp>
#include <Serialize/Serialize.hpp>
#include <WorldComponent/Wiring/FluidWiring/FluidWiring.hpp>
#include <Item/IFluidContainingItem.hpp>

std::string FluidComponent::tagName = "main:fluidComponent";

FluidComponent::FluidComponent(Machine &machine) :
    MachineComponent(machine),
    fluidMachine(dynamic_cast<IFluidMachine&>(machine))
{}

std::optional<std::string> FluidComponent::getDefaultFluidOnSide(Side side)
{
    return defaultFluidOnSide[side.asNumber()];
}

void FluidComponent::setDefaultFluidOnSide(Side side, std::optional<std::string> fluidName)
{
    defaultFluidOnSide[side.asNumber()] = fluidName;
}

int FluidComponent::pullFluid(Side side, std::string fluidName, int amount)
{
    int maxToRelease = fluidMachine.getMaxFluidToRelease(side, fluidName);

    std::unordered_map<std::string, int>::iterator fluidIter = fluidAmounts.find(fluidName);
    if (fluidIter == fluidAmounts.end())
    {
        return 0;
    }

    int amountToPull = std::min(maxToRelease, std::min(amount, fluidIter->second));
    
    fluidIter->second -= amountToPull;
    if (fluidIter->second == 0)
    {
        fluidAmounts.erase(fluidIter);
    }

    return amountToPull;
}

int FluidComponent::pushFluid(Side side, std::string fluidName, int amount)
{
    int maxToAccept = fluidMachine.getMaxFluidToAccept(side, fluidName);
    int amountToPush = std::min(maxToAccept, amount);

    if (amountToPush == 0)
    {
        return 0;
    }

    fluidAmounts[fluidName] += amountToPush;

    return amountToPush;
}

int FluidComponent::getFluidAmount(std::string fluidName)
{
    std::unordered_map<std::string, int>::iterator fluidIter = fluidAmounts.find(fluidName);
    if (fluidIter == fluidAmounts.end())
    {
        return 0;
    }
    else
    {
        return fluidIter->second;
    }
}

void FluidComponent::refillFrom(ItemStack &stack)
{
    IFluidContainingItem *input = stack.getItemAs<IFluidContainingItem>();
    if (input == nullptr)
    {
        return;
    }

    std::string candidateFluid = input->getFluidName(stack);
    if (candidateFluid == "")
    {
        return;
    }

    int amountToRefill = fluidMachine.getMaxFluidToAccept(std::nullopt, candidateFluid);
    if (amountToRefill == 0)
    {
        return;
    }

    fluidAmounts[candidateFluid] += input->pullFluid(stack, candidateFluid, amountToRefill);
}

void FluidComponent::drainInto(ItemStack &stack, std::string fluidName)
{
    IFluidContainingItem *output = stack.getItemAs<IFluidContainingItem>();
    if (output == nullptr)
    {
        return;
    }

    std::unordered_map<std::string, int>::iterator fluidIter = fluidAmounts.find(fluidName);
    if (fluidIter == fluidAmounts.end())
    {
        return;
    }

    int amountToDrain = std::min(fluidIter->second, fluidMachine.getMaxFluidToRelease(std::nullopt, fluidName));

    fluidIter->second -= output->pushFluid(stack, fluidName, amountToDrain);
    if (fluidIter->second == 0)
    {
        fluidAmounts.erase(fluidIter);
    }
}

int FluidComponent::drainFluid(std::string fluidName, int amount)
{
    std::unordered_map<std::string, int>::iterator fluidIter = fluidAmounts.find(fluidName);
    if (fluidIter == fluidAmounts.end())
    {
        return 0;
    }

    int amountToDrain = std::min(amount, fluidIter->second);
    fluidIter->second -= amountToDrain;
    return amountToDrain;
}

void FluidComponent::addFluid(std::string fluidName, int amount)
{
    fluidAmounts[fluidName] += amount;
}

void FluidComponent::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, sideBitmap, fluidAmounts, defaultFluidOnSide);
}

void FluidComponent::deserializeState(const uint8_t *bytes, size_t size)
{
    deserialize(bytes, size, sideBitmap, fluidAmounts, defaultFluidOnSide);
}

void FluidComponent::onPreUpdate()
{
    std::shared_ptr<WorldGridNode> node = machine.getChunk().getNode();
    FluidWiring &wiring = node->getComponent<FluidWiring>();

    for (const Side &side : Side::all)
    {
        if (fluidMachine.canConnectFluidPipe(side))
        {
            if ((sideBitmap & side.mask()) == 0)
            {
                sideBitmap |= side.mask();
                wiring.onMachineSidePlaced(machine.getWorld(), node, machine.pos, side);
            }
        }
        else
        {
            if (sideBitmap & side.mask())
            {
                sideBitmap &= ~side.mask();
                wiring.onMachineSideRemoved(machine.getWorld(), node, machine.pos, side);
            }
        }
    }
}

void FluidComponent::onRemoving()
{
    std::shared_ptr<WorldGridNode> node = machine.getChunk().getNode();
    FluidWiring &wiring = node->getComponent<FluidWiring>();

    for (const Side &side : Side::all)
    {
        if (sideBitmap & side.mask())
        {
            wiring.onMachineSideRemoved(machine.getWorld(), node, machine.pos, side);
        }
    }
}