#include <BlockRegistry/BlockRegistry.hpp>
#include <Machine/MNuclearReactor/MNuclearReactor.hpp>
#include <MachineComponent/BeltComponent/BeltComponent.hpp>
#include <MachineComponent/DataCableComponent/DataCableComponent.hpp>
#include <MachineComponent/FluidComponent/FluidComponent.hpp>
#include <Serialize/Serialize.hpp>
#include <Item/INuclearReactorComponent.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <MachineComponent/MultiBlockComponent/MultiBlockComponent.hpp>
#include <MachineComponent/ElectricComponent/ElectricComponent.hpp>
#include <Block/SpriteBlock/MultiBlock/NuclearReactor/NuclearReactor.hpp>

ItemStack& MNuclearReactor::rechargeSlot()
{
    return stacks[8*8];
}

bool MNuclearReactor::updateComponents()
{
    if (!isSwitchedOnPriv)
    {
        return false;
    }

    bool reacted = false;
    for (size_t i = 0; i < 8*8; i++)
    {
        ItemStack &stack = stacks[i];
        INuclearReactorComponent *component = components[i];

        if (component == nullptr)
        {
            continue;
        }

        glm::ivec2 cellPos(i % 8, i / 8);

        if (component->updateNuclearReactor(stack, *this, cellPos))
        {
            reacted = true;
        }
    }

    return reacted;
}

void MNuclearReactor::updateHeat()
{
    int temperatureDeltas[8*8] = {};

    glm::ivec2 cellPos;
    for (cellPos.y = 0; cellPos.y < 8; cellPos.y++)
    {
        for (cellPos.x = 0; cellPos.x < 8; cellPos.x++)
        {
            static std::array<glm::ivec2, 4> sideOffsets = {
                glm::ivec2(-1, 0),
                glm::ivec2(1, 0),
                glm::ivec2(0, -1),
                glm::ivec2(0, 1),
            };

            int currentTemp = getCellTemperature(cellPos);
            float myHeatDeltaFloat = 0.0f;

            for (const glm::ivec2 &offset : sideOffsets)
            {
                myHeatDeltaFloat += getCellHeatConductivity(cellPos + offset) *
                    (float) (getCellTemperature(cellPos + offset) - currentTemp);
            }

            myHeatDeltaFloat *= getCellHeatConductivity(cellPos);

            int myHeatDelta = myHeatDeltaFloat < 0.0f ? std::floor(myHeatDeltaFloat) : std::ceil(myHeatDeltaFloat);

            temperatureDeltas[cellPos.y * 8 + cellPos.x] += myHeatDelta;
        }
    }

    for (int i = 0; i < 8*8; i++)
    {
        slotTemperatures[i] += temperatureDeltas[i];

        if (slotTemperatures[i] > criticalTemperature)
        {
            INuclearReactorComponent *comp = components[i];
            if (comp != nullptr)
            {
                comp->onOverheating(stacks[i], *this, glm::ivec2(i % 8, i / 8));
            }
        }
    }
}

void MNuclearReactor::updateExplosion()
{
    if (explosion == nullptr)
    {
        explosion = std::make_unique<Explosion>(m_world, pos, explosionPower, explosionRadius);
    }

    if (!explosion->isReady())
    {
        return;
    }

    m_world.setBlockAt(pos, 0);
    explosion->explode();
}

MNuclearReactor::MNuclearReactor(LocalWorld &world, glm::ivec3 pos) :
    Machine(world, pos),
    components(8*8),
    stacks(8*8 + 1),
    slotTemperatures(8*8, 300)
{
    attach<MultiBlockComponent>();
    attach<ElectricComponent>();
    attach<FluidComponent>();
    attach<BeltComponent>();
    attach<DataCableComponent>();
}

bool MNuclearReactor::isSwitchedOn()
{
    return isSwitchedOnPriv;
}

void MNuclearReactor::setSwitch(bool on)
{
    isSwitchedOnPriv = on;
}

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

const std::vector<int>& MNuclearReactor::getSlotTemperatures()
{
    return slotTemperatures;
}

void MNuclearReactor::setCellTemperature(glm::ivec2 cellPos, int temperature)
{
    if (cellPos.x < 0 || cellPos.x >= 8 || cellPos.y < 0 || cellPos.y >= 8)
    {
        return;
    }

    slotTemperatures[cellPos.y * 8 + cellPos.x] = temperature;
}

void MNuclearReactor::heatCell(glm::ivec2 cellPos, int dt)
{
    if (cellPos.x < 0 || cellPos.x >= 8 || cellPos.y < 0 || cellPos.y >= 8)
    {
        return;
    }

    slotTemperatures[cellPos.y * 8 + cellPos.x] += dt;
}

int MNuclearReactor::getCellTemperature(glm::ivec2 cellPos)
{
    if (cellPos.x < 0 || cellPos.x >= 8 || cellPos.y < 0 || cellPos.y >= 8)
    {
        return 300;
    }

    return slotTemperatures[cellPos.y * 8 + cellPos.x];
}

float MNuclearReactor::getCellHeatConductivity(glm::ivec2 cellPos)
{
    if (cellPos.x < 0 || cellPos.x >= 8 || cellPos.y < 0 || cellPos.y >= 8)
    {
        return airHeatConductivity;
    }

    INuclearReactorComponent *component = components[cellPos.y * 8 + cellPos.x];
    if (component == nullptr)
    {
        return airHeatConductivity;
    }

    return component->getHeatConductivity(stacks[cellPos.y * 8 + cellPos.x]);
}

bool MNuclearReactor::tryConsumeFluid(const std::string &name, int amount)
{
    auto fluidLink = getComponent<MultiBlockComponent>().getFluidInput();
    auto &fluidComp = getComponent<FluidComponent>();

    int drained = fluidComp.drainFluid(name, amount);
    if (fluidLink != nullptr && drained < amount)
    {
        drained += fluidLink->pullFluid(name, amount - drained);
    }

    if (drained < amount)
    {
        // Buffer the already-drained fluid in the reactor.
        fluidComp.addFluid(name, drained);
        return false;
    }

    return true;
}

void MNuclearReactor::generateEnergy(int amount)
{
    getComponent<ElectricComponent>().rechargeBy(amount, energyCapacity);
}

void MNuclearReactor::meltdown()
{
    newMeta |= NuclearReactor::metaMeltdown;
}

void MNuclearReactor::explode()
{
    exploding = true;
}

void MNuclearReactor::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, stacks, isSwitchedOnPriv, slotTemperatures, exploding);
}

void MNuclearReactor::deserializeState(const uint8_t *data, size_t size)
{
    deserialize(data, size, stacks, isSwitchedOnPriv, slotTemperatures, exploding);
    
    for (size_t i = 0; i < 8*8; i++)
    {
        components[i] = stacks[i].getItemAs<INuclearReactorComponent>();
    }
}

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

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

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

    if (slot < 8*8)
    {
        components[slot] = stack.getItemAs<INuclearReactorComponent>();
    }
}

bool MNuclearReactor::isAllowedStackInSlot(int slot, ItemStack stack)
{
    return true;
}

void MNuclearReactor::update()
{
    auto &fluidComp = getComponent<FluidComponent>();

    uint8_t oldMeta = *m_world.getBlockMeta(pos);
    newMeta = oldMeta & 3;

    if (exploding)
    {
        newMeta = oldMeta | NuclearReactor::metaReacting | NuclearReactor::metaMeltdown;

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

        updateExplosion();
        return;
    }

    uraniumBurned = 0;
    plutoniumCaptureCells = 0;

    getComponent<ElectricComponent>().drainEnergyInto(rechargeSlot());

    if (updateComponents())
    {
        newMeta |= NuclearReactor::metaReacting;
    }

    updateHeat();

    if (fluidComp.getFluidAmount("main:plutonium") == 0 && plutoniumCaptureCells > 0)
    {
        int amountGenerated = std::min(m_world.generateRandom() % (1 + plutoniumCaptureCells * 1), (uint64_t) (25 * uraniumBurned));
        fluidComp.addFluid("main:plutonium", amountGenerated);
    }

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

bool MNuclearReactor::canConnectElectricWire(Side side)
{
    return false;
}

void MNuclearReactor::onItemStackEntering(Side side, ItemStack &stack)
{
    insertStack(stack);
}

ItemStack MNuclearReactor::tryExtractItem(Side side, ItemFilter &filter, uint32_t modeCode)
{
    for (size_t i = 0; i < stacks.size(); i++)
    {
        ItemStack &stack = stacks[i];
        if (!stack.isEmpty() && filter.matches(stack))
        {
            return stack.extractOne();
        }
    }

    return ItemStack();
}

bool MNuclearReactor::canConnectExtractor(Side side)
{
    return true;
}

bool MNuclearReactor::canConnectFluidPipe(Side side)
{
    return true;
}

bool MNuclearReactor::canDrainEnergyFromSide(Side side)
{
    return true;
}

Side MNuclearReactor::getDataInputSide()
{
    return Side::Top;
}

void MNuclearReactor::onDataInputVerb(std::string verb)
{
    if (verb == Verbs::TurnOn)
    {
        isSwitchedOnPriv = true;
    }
    else if (verb == Verbs::TurnOff)
    {
        isSwitchedOnPriv = false;
    }
    else
    {
        Log::warn(formatString("MNuclearReactor::onDataInputVerb(): received an unknown verb '$+'", verb));
    }
}

int MNuclearReactor::getMaxFluidToRelease(std::optional<Side> side, std::string fluidName)
{
    return INT_MAX;
}

void MNuclearReactor::saveMainConfig(MachineConfig &outConfig)
{
    outConfig.blockName = "main:nuclearReactor";
    outConfig.label = "Nuclear Reactor";

    serializeInto(outConfig.components[""], isSwitchedOnPriv);
}

void MNuclearReactor::loadMainConfig(const MachineConfig &config, bool consumed)
{
    const auto &data = config.components.at("");
    deserialize(data.data(), data.size(), isSwitchedOnPriv);
}