#include <BlockRegistry/BlockRegistry.hpp>
#include <Machine/MChemicalReactor/MChemicalReactor.hpp>
#include <MachineComponent/BeltComponent/BeltComponent.hpp>
#include <Serialize/Serialize.hpp>
#include <Item/IChemicalReactorComponent.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <MachineComponent/MultiBlockComponent/MultiBlockComponent.hpp>
#include <MachineComponent/ElectricComponent/ElectricComponent.hpp>

std::vector<MChemicalReactor::Recipe> MChemicalReactor::recipes;

bool MChemicalReactor::Recipe::canBeMade(MChemicalReactor &reactor)
{
    // Check we need fluid.
    if (fluidType != "")
    {
        int containerFluidAmount = 0;
        for (size_t i=0; i<reactor.fluidContainers.size(); i++)
        {
            if (reactor.fluidContainers[i] != nullptr && reactor.fluidContainers[i]->getFluidName(reactor.stacks[i]) == fluidType)
            {
                containerFluidAmount += reactor.fluidContainers[i]->getFluidAmount(reactor.stacks[i]);
            }
        }

        // Check that we have the required amount of fluid in the buffer.
        if (fluidAmount > reactor.bufferedFluid[fluidType] + containerFluidAmount)
        {
            return false;
        }
    }
    
    // Check that the input inventory contains enough of each input item.
    std::vector<ItemStack>::iterator it;
    for (it=inputs.begin(); it!=inputs.end(); ++it)
    {
        ItemStack &inputStack = *it;

        // Count this item.
        int amountInInput = 0;
        int i;
        for (i=0; i<reactor.getNumSlots(); i++)
        {
            ItemStack stack = reactor.getStackInSlot(i);
            if (!stack.isEmpty() && inputStack.canMergeFrom(stack))
            {
                amountInInput += stack.getStackSize();
            }
        }

        // If the amount in the input inventory is too low, we can't make.
        if (amountInInput < inputStack.getStackSize())
        {
            return false;
        }
    }

    // If there are any empty stacks in the output inventory, we can definitely make.
    int i;
    for (i=0; i<reactor.getNumSlots(); i++)
    {
        if (reactor.getStackInSlot(i).isEmpty())
        {
            return true;
        }
    }

    // Otherwise, make sure we can merge the output stack into existing stacks.
    int numCanMerge = 0;
    for (i=0; i<reactor.getNumSlots(); i++)
    {
        ItemStack stack = reactor.getStackInSlot(i);
        if (stack.canMergeFrom(output))
        {
            numCanMerge += (stack.getMaxStackSize() - stack.getStackSize());
        }
    }

    // We can make as long as we can merge enough.
    return numCanMerge >= output.getStackSize();
}

void MChemicalReactor::Recipe::make(MChemicalReactor &reactor)
{
    for (size_t i=0; i<reactor.fluidContainers.size(); i++)
    {
        // Check if the container we are trying to reference is actually valid and contains the correct fluid.
        if (reactor.fluidContainers[i] != nullptr && reactor.fluidContainers[i]->getFluidName(reactor.stacks[i]) == fluidType)
        {
            // Try to add the the correct fluid type and amount to the buffer.
            reactor.bufferedFluid[fluidType] += reactor.fluidContainers[i]->pullFluid(reactor.stacks[i],fluidType, fluidAmount - reactor.bufferedFluid[fluidType]);
            if (reactor.bufferedFluid[fluidType] >= fluidAmount)
            {
                break;
            }
        }
    }

    // Remove the correct amount and type of fluid from the buffer.
    reactor.bufferedFluid[fluidType] -= fluidAmount;

    // Remove the correct amount of each input stack from the input inventory.
    std::vector<ItemStack>::iterator it;
    for (it=inputs.begin(); it!=inputs.end(); ++it)
    {
        ItemStack &inputStack = *it;
        int numLeft = inputStack.getStackSize();

        int i;
        for (i=0; i<reactor.getNumSlots(); i++)
        {
            if (numLeft == 0)
            {
                break;
            }

            ItemStack stack = reactor.getStackInSlot(i);
            if (inputStack.canMergeFrom(stack))
            {
                int takingAway = std::min(stack.getStackSize(), numLeft);
                stack.setStackSize(stack.getStackSize() - takingAway);
                numLeft -= takingAway;

                if (stack.getStackSize() == 0) stack = ItemStack();
                reactor.setStackInSlot(i, stack);
            }
        }
    }

    // First try to merge as much as possible of the output stack into existing stacks.
    ItemStack outputStack = output;
    int i;
    for (i=0; i<reactor.getNumSlots(); i++)
    {
        if (outputStack.isEmpty())
        {
            break;
        }

        ItemStack stack = reactor.getStackInSlot(i);
        if (!stack.isEmpty() && stack.canMergeFrom(outputStack))
        {
            int unitsToMerge = std::min(stack.getMaxStackSize() - stack.getStackSize(), outputStack.getStackSize());
            stack.setStackSize(stack.getStackSize() + unitsToMerge);
            outputStack.setStackSize(outputStack.getStackSize() - unitsToMerge);

            reactor.setStackInSlot(i, stack);
        }
    }

    // If we have an empty stack then we are done.
    if (outputStack.isEmpty())
    {
        return;
    }

    // Otherwise, place this stack in an empty slot.
    for (i=0; i<reactor.getNumSlots(); i++)
    {
        if (reactor.getStackInSlot(i).isEmpty())
        {
            reactor.setStackInSlot(i, outputStack);
            return;
        }
    }
}

bool MChemicalReactor::Recipe::matchesQuery(std::string query)
{
    std::transform(query.begin(), query.end(), query.begin(),
        [](unsigned char c){ return std::tolower(c); });

    std::vector<ItemStack>::iterator it;
    for (it=inputs.begin(); it!=inputs.end(); ++it)
    {
        std::string displayName = it->getDisplayName();
        std::transform(displayName.begin(), displayName.end(), displayName.begin(),
            [](unsigned char c){ return std::tolower(c); });

        if (displayName.find(query) != std::string::npos)
        {
            return true;
        }
    }

    std::string displayName = output.getDisplayName();
    std::transform(displayName.begin(), displayName.end(), displayName.begin(),
        [](unsigned char c){ return std::tolower(c); });
    return displayName.find(query) != std::string::npos;
}

void MChemicalReactor::initRecipes()
{
    recipes.push_back(
        Recipe
        {
            1700,
            120,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:ironIngot", 1),
                ItemStack("main:coal", 3),
            },

            ItemStack("main:steelIngot", 1),
        }
    );

    recipes.push_back(
        Recipe
        {
            1700,
            30,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:ironDust", 1),
                ItemStack("main:coal", 3),
            },

            ItemStack("main:steelIngot", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            1200,
            60,
            "",
            0,


            std::vector<ItemStack>{
                ItemStack("main:copperIngot", 1),
                ItemStack("main:tinIngot", 1),
            },

            ItemStack("main:brassIngot", 1),
        }
    );

    recipes.push_back(
        Recipe
        {
            1200,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:copperDust", 1),
                ItemStack("main:tinDust", 1),
            },

            ItemStack("main:brassIngot", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            1310,
            120,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:brassIngot", 1),
                ItemStack("main:copperIngot", 1),
            },

            ItemStack("main:bronzeIngot", 1),
        }
    );

    recipes.push_back(
        Recipe
        {
            1310,
            30,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:brassDust", 1),
                ItemStack("main:copperDust", 1),
            },

            ItemStack("main:bronzeIngot", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            925,
            250,
            "",
            0,

            std::vector<ItemStack>{
                blockRegistry.getBlockByName("main:glass").toItemStack(3),
            },

            blockRegistry.getBlockByName("main:temperedGlass").toItemStack(1),
        }
    );

    recipes.push_back(
        Recipe
        {
            800,
            30,
            "main:water",
            1000,

            std::vector<ItemStack>{
                ItemStack("main:ironDust", 1),
            },

            ItemStack("main:blackDye", 4),
        }
    );

    recipes.push_back(
        Recipe
        {
            1080,
            30,
            "main:water",
            1000,

            std::vector<ItemStack>{
                ItemStack("main:ironDust", 1),
            },

            ItemStack("main:redDye", 4),
        }
    );

    recipes.push_back(
        Recipe
        {
            1200,
            30,
            "main:water",
            1000,

            std::vector<ItemStack>{
                ItemStack("main:ironDust", 1),
            },

            ItemStack("main:brownDye", 4),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:greenDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:limeDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:limeDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:greenDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:darkGreenDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:greenDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:greenDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:darkGreenDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:yellowDye", 1),
                ItemStack("main:greenDye", 1),
            },

            ItemStack("main:yellowGreenDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:redDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:lightRedDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:lightRedDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:redDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:darkRedDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:redDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:redDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:darkRedDye", 2),
        }
    );

    // ---

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:yellowDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:lightYellowDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:lightYellowDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:yellowDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:darkYellowDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:yellowDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:yellowDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:darkYellowDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:greenDye", 1),
                ItemStack("main:blueDye", 1),
            },

            ItemStack("main:tealDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:tealDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:cyanDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:cyanDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:tealDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:brownDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:darkBrownDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:darkBrownDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:brownDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:redDye", 1),
                ItemStack("main:orangeDye", 1),
            },

            ItemStack("main:redOrangeDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:blueDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:darkBlueDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:darkBlueDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:blueDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:redOrangeDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:coralDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:coralDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:redOrangeDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:redDye", 1),
                ItemStack("main:yellowDye", 1),
            },

            ItemStack("main:orangeDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:redDye", 1),
                ItemStack("main:blueDye", 1),
            },

            ItemStack("main:violetDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:redDye", 1),
                ItemStack("main:violetDye", 1),
            },

            ItemStack("main:purpleDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:redDye", 1),
                ItemStack("main:purpleDye", 1),
            },

            ItemStack("main:magentaDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:magentaDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:darkPinkDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:darkPinkDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:magentaDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:pinkDye", 1),
                ItemStack("main:blackDye", 1),
            },

            ItemStack("main:magentaDye", 2),
        }
    );

    recipes.push_back(
        Recipe
        {
            400,
            15,
            "",
            0,

            std::vector<ItemStack>{
                ItemStack("main:magentaDye", 1),
                ItemStack("main:whiteDye", 1),
            },

            ItemStack("main:pinkDye", 2),
        }
    );
}

std::vector<MChemicalReactor::Recipe*> MChemicalReactor::queryRecipes(std::string query)
{
    std::vector<Recipe*> matches;

    for (Recipe &recipe : recipes)
    {
        if (recipe.matchesQuery(query))
        {
            matches.push_back(&recipe);
        }
    }

    return matches;
}

MChemicalReactor::MChemicalReactor(LocalWorld &world, glm::ivec3 pos) :
    Machine(world, pos),
    components(6*6),
    energyContainers(6*6),
    fluidContainers(6*6),
    stacks(6*6),
    isSwitchedOnPriv(false),
    temperature(300),
    bufferedEnergy(0),
    ticks(0)
{
    attach<MultiBlockComponent>();
    attach<ElectricComponent>();
    attach<BeltComponent>();
}

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

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

int MChemicalReactor::getTemperature()
{
    return temperature;
}

void MChemicalReactor::heatUp(int dt)
{
    temperature += dt;
}

int MChemicalReactor::drainEnergy(int amount)
{
    if (bufferedEnergy < amount)
    {
        int i;
        for (i=0; i<(int) stacks.size(); i++)
        {
            IEnergyContainingItem *container = energyContainers[i];
            ItemStack &stack = stacks[i];

            if (container != nullptr)
            {
                bufferedEnergy += container->pullEnergy(stack, amount - bufferedEnergy);
            }

            if (bufferedEnergy == amount)
            {
                break;
            }
        }
    }

    if (amount > bufferedEnergy)
    {
        amount = bufferedEnergy;
    }

    bufferedEnergy -= amount;
    return amount;
}

void MChemicalReactor::returnEnergy(int energy)
{
    bufferedEnergy += energy;
}

void MChemicalReactor::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, stacks, isSwitchedOnPriv, temperature, bufferedEnergy, bufferedFluid);
}

void MChemicalReactor::deserializeState(const uint8_t *data, size_t size)
{
    deserialize(data, size, stacks, isSwitchedOnPriv, temperature, bufferedEnergy, bufferedFluid);

    components.clear();
    energyContainers.clear();
    fluidContainers.clear();
    
    for (ItemStack &stack : stacks)
    {
        components.push_back(stack.getItemAs<IChemicalReactorComponent>());
        energyContainers.push_back(stack.getItemAs<IEnergyContainingItem>());
        fluidContainers.push_back(stack.getItemAs<IFluidContainingItem>());
    }
}

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

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

void MChemicalReactor::setStackInSlot(int slot, ItemStack stack)
{
    stacks[slot] = stack;
    components[slot] = stack.getItemAs<IChemicalReactorComponent>();
    energyContainers[slot] = stack.getItemAs<IEnergyContainingItem>();
    fluidContainers[slot] = stack.getItemAs<IFluidContainingItem>();
}

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

void MChemicalReactor::update()
{
    ticks++;

    if (isSwitchedOnPriv)
    {
        // Let the components update.
        int i;
        for (i=0; i<(int) stacks.size(); i++)
        {
            IChemicalReactorComponent *component = components[i];
            ItemStack &stack = stacks[i];

            if (component != nullptr)
            {
                component->updateChemicalReactor(stack, *this);
            }
        }
    }

    // Cool down.
    float dtf = coolingRate * (300-temperature);
    int dt;

    if (dtf > 0.0f && dtf < 1.0f)
    {
        dt = 1;
    }
    else if (dtf > -1.0f && dtf < 0.0f)
    {
        dt = -1;
    }
    else
    {
        dt = (int) std::floor(dtf);
    }

    if (temperature == 300)
    {
        dt = 0;
    }

    temperature += dt;

    // Check if we overheated.
    if (temperature > maxTemperature)
    {
        m_world.playSound(Sound::chemicalReactorOverheated, pos);
        m_world.setBlockAt(pos, "main:burntChemicalReactor");
        return;
    }

    // Run the recipes.
    for (Recipe &recipe : recipes)
    {
        if (temperature >= recipe.minTemp && (ticks % recipe.ticksBetweenRuns) == 0)
        {
            if (recipe.canBeMade(*this))
            {
                recipe.make(*this);
            }
        }
    }

    // Update the metadata.
    uint8_t currentMeta = *m_world.getBlockMeta(pos);
    uint8_t newMeta = currentMeta & 3;

    if (temperature > 450)
    {
        newMeta |= (1 << 2);
    }

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

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

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

ItemStack MChemicalReactor::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 MChemicalReactor::canConnectExtractor(Side side)
{
    return true;
}

void MChemicalReactor::saveMainConfig(MachineConfig &outConfig)
{
    outConfig.blockName = "main:chemicalReactor";
    outConfig.label = "Chemical Reactor";

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

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