#include <Machine/MPot/MPot.hpp>
#include <Serialize/Serialize.hpp>
#include <FuelRuleRegistry/FuelRuleRegistry.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <Block/Block.hpp>
#include <SmeltingRuleRegistry/SmeltingRuleRegistry.hpp>

ItemStack& MPot::fuelSlot()
{
    return m_stacks[0];
}

ItemStack& MPot::inputSlot()
{
    return m_stacks[1];
}

ItemStack& MPot::outputSlot()
{
    return m_stacks[2];
}

MPot::MPot(LocalWorld &world, glm::ivec3 pos) :
    Machine(world, pos),
    m_stacks(3),
    m_temperature(0),
    progress(0),
    m_fuelCurrent(0),
    m_fuelBatch(0)
{}

uint32_t MPot::getTemperature()
{
    return m_temperature;
}

uint32_t MPot::getProgress()
{
    return progress;
}

uint32_t MPot::getFuelCurrent()
{
    return m_fuelCurrent;
}

uint32_t MPot::getFuelBatch()
{
    return m_fuelBatch;
}

bool MPot::isIron()
{
    return m_world.getBlockAt(pos)->name == "main:ironPot";
}

uint32_t MPot::getCompletedProgress(bool isIron)
{
    return isIron ? 900 : 1800;
}

void MPot::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, m_stacks, m_temperature, progress, m_fuelCurrent, m_fuelBatch);
}

void MPot::deserializeState(const uint8_t *data, size_t size)
{
    deserialize(data, size, m_stacks, m_temperature, progress, m_fuelCurrent, m_fuelBatch);
}

int MPot::getNumSlots()
{
    return (int) m_stacks.size();
}

ItemStack MPot::getStackInSlot(int slot)
{
    return m_stacks[slot];
}

void MPot::setStackInSlot(int slot, ItemStack stack)
{
    m_stacks[slot] = stack;

    if (slot == 1)
    {
        // Input slot updated, cancel smelting progress.
        progress = 0;
    }
}

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

    if (slot == 0)
    {
        // Only fuels can be placed in the fuel slot.
        return fuelRuleRegistry.isValidFuel(stack);
    }
    else if (slot == 1)
    {
        // Only smeltable items can be placed in the input slot.
        return smeltingRuleRegistry.canSmelt(stack);
    }
    else
    {
        // Cannot place anything in the output slot.
        return false;
    }
}

void MPot::update()
{
    // Figure out the heat rate and heat resistance based on the type of pot.
    uint32_t heatRate;
    float heatResistance;

    if (isIron())
    {
        // Iron pot.
        heatRate = 40;
        heatResistance = 0.01f;
    }
    else
    {
        // Stone pot.
        heatRate = 10;
        heatResistance = 0.0033f;
    }

    // If we currently have no fuel left, try refuelling from the slot.
    if (m_fuelCurrent == 0)
    {
        ItemStack &fuelStack = fuelSlot();
        if (!fuelStack.isEmpty() && fuelRuleRegistry.isValidFuel(fuelStack))
        {
            m_fuelBatch = m_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 (m_fuelCurrent != 0)
    {
        hadFuel = true;

        m_fuelCurrent--;
        m_temperature += heatRate;
    }

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

    // Set metadata based on temperature and fuel level.
    uint8_t newMeta = (m_temperature / 1000) & 3;
    if (hadFuel)
    {
        newMeta |= 0x4;
    }

    m_world.setBlockMeta(pos, newMeta);

    // Get the input/output stacks.
    ItemStack &inputStack = inputSlot();
    ItemStack &outputStack = outputSlot();

    // If the current input is not smeltable, reset progress to zero.
    if (!smeltingRuleRegistry.canSmelt(inputStack))
    {
        progress = 0;
        return;
    }

    // Get the result stack from the rule.
    ItemStack resultStack = smeltingRuleRegistry.getSmeltingResult(inputStack);

    // If the output slot is not empty and also can't merge the result, reset progress to zero.
    if (!outputStack.isEmpty() && !outputStack.canMergeFrom(resultStack))
    {
        progress = 0;
        return;
    }

    // Make progress based on temperature, merge result if completed.
    progress += m_temperature / 1000;
    if (progress >= getCompletedProgress(isIron()))
    {
        progress = 0;

        outputStack.leftMergeFrom(resultStack);
        inputStack.extractOne();
    }

    // If too cold, cancel progress.
    if (m_temperature < 1000)
    {
        progress = 0;
    }
}
