#include <Machine/MSteamGenerator/MSteamGenerator.hpp>
#include <Serialize/Serialize.hpp>
#include <Utilities/Utilities.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <FuelRuleRegistry/FuelRuleRegistry.hpp>
#include <Item/IFluidContainingItem.hpp>
#include <Item/IEnergyContainingItem.hpp>
#include <MachineComponent/ElectricComponent/ElectricComponent.hpp>
#include <MachineComponent/FluidComponent/FluidComponent.hpp>

ItemStack& MSteamGenerator::fuelSlot()
{
    return stacks[0];
}

ItemStack& MSteamGenerator::waterSlot()
{
    return stacks[1];
}

ItemStack& MSteamGenerator::rechargeSlot()
{
    return stacks[2];
}

Side MSteamGenerator::getOutputSide()
{
    return Side(*m_world.getBlockMeta(pos) & 3);
}

MSteamGenerator::MSteamGenerator(LocalWorld &world, glm::ivec3 pos) :
    Machine(world, pos),
    stacks(3),
    temperature(0),
    fuelCurrent(0),
    fuelBatch(0)
{
    attach<ElectricComponent>();
    attach<FluidComponent>();
}

uint32_t MSteamGenerator::getTemperature()
{
    return temperature;
}

int MSteamGenerator::getWaterLevel()
{
    return getComponent<FluidComponent>().getFluidAmount("main:water");
}

uint32_t MSteamGenerator::getFuelCurrent()
{
    return fuelCurrent;
}

uint32_t MSteamGenerator::getFuelBatch()
{
    return fuelBatch;
}

int MSteamGenerator::getStoredEnergy()
{
    return getComponent<ElectricComponent>().getEnergy();
}

void MSteamGenerator::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, stacks, temperature, fuelCurrent, fuelBatch);
}

void MSteamGenerator::deserializeState(const uint8_t *data, size_t size)
{
    deserialize(data, size, stacks, temperature, fuelCurrent, fuelBatch);
}

int MSteamGenerator::getNumSlots()
{
    return (int) stacks.size();
}

ItemStack MSteamGenerator::getStackInSlot(int slot)
{
    return stacks[slot];
}

void MSteamGenerator::setStackInSlot(int slot, ItemStack stack)
{
    stacks[slot] = stack;
}

bool MSteamGenerator::isAllowedStackInSlot(int slot, ItemStack stack)
{
    if (stack.isEmpty())
    {
        // All slots can be cleared.
        return true;
    }

    if (slot == 0)
    {
        // Fuel slot: only fuel can be inserted.
        return fuelRuleRegistry.isValidFuel(stack);
    }
    else if (slot == 1)
    {
        // Water collection slot: only fluid containing items can be inserted.
        return stack.getItemAs<IFluidContainingItem>() != nullptr;
    }
    else if (slot == 2)
    {
        // Recharge slot: only energy-containing items can be inserted.
        return stack.getItemAs<IEnergyContainingItem>() != nullptr;
    }
    else
    {
        return false;
    }
}

void MSteamGenerator::update()
{
    ElectricComponent &electricComp = getComponent<ElectricComponent>();
    FluidComponent &fluidComp = getComponent<FluidComponent>();

    fluidComp.refillFrom(waterSlot());

    // Set up heating paramters.
    uint32_t heatRate = 80;
    float heatResistance = 0.01f;

    // If we currently have no fuel left, try refuelling from the slot.
    if (fuelCurrent == 0)
    {
        ItemStack &fuelStack = fuelSlot();
        if (!fuelStack.isEmpty() && fuelRuleRegistry.isValidFuel(fuelStack))
        {
            fuelBatch = fuelCurrent = fuelRuleRegistry.getFuelAmount(fuelStack);
            fuelStack.extractOne();
        }
    }

    // If we do have fuel now, decrease it by 1 unit and add to the heat.
    bool hadFuel = false;
    if (fuelCurrent != 0)
    {
        hadFuel = true;

        fuelCurrent--;
        temperature += heatRate;
    }

    // Cool down based on the current heat resistance.
    if (temperature != 0)
    {
        temperature -= std::max<uint32_t>(1, temperature * heatResistance);
    }

    // Set the metadata based on whether we are fueled.
    uint8_t oldMeta = *m_world.getBlockMeta(pos);
    uint8_t newMeta = oldMeta & 3;
    
    if (hadFuel || temperature >= 1000)
    {
        newMeta |= (1 << 2);
    }

    if (oldMeta != newMeta)
    {
        m_world.setBlockMeta(pos, newMeta);
    }

    int waterUsed = fluidComp.drainFluid("main:water", temperature / 1000);

    electricComp.rechargeBy(waterUsed, energyCapacity);
    electricComp.drainEnergyInto(rechargeSlot());
}

bool MSteamGenerator::canConnectFluidPipe(Side side)
{
    return side == Side::Top;
}

int MSteamGenerator::getMaxFluidToAccept(std::optional<Side> side, std::string fluidName)
{
    if (side.has_value() && *side != Side::Top)
    {
        return 0;
    }

    if (fluidName != "main:water")
    {
        return 0;
    }

    return waterCapacity - getWaterLevel();
}

bool MSteamGenerator::canConnectElectricWire(Side side)
{
    return side == getOutputSide();
}

bool MSteamGenerator::canDrainEnergyFromSide(Side side)
{
    return side == getOutputSide();
}