package de.srendi.advancedperipherals.common.addons.appliedenergistics;

import appeng.api.AECapabilities;
import appeng.api.crafting.IPatternDetails;
import appeng.api.inventories.InternalInventory;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridNode;
import appeng.api.networking.crafting.CraftingJobStatus;
import appeng.api.networking.crafting.ICraftingCPU;
import appeng.api.networking.crafting.ICraftingService;
import appeng.api.networking.storage.IStorageService;
import appeng.api.stacks.AEFluidKey;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.AEKeyType;
import appeng.api.stacks.GenericStack;
import appeng.api.stacks.KeyCounter;
import appeng.api.storage.AEKeyFilter;
import appeng.api.storage.IStorageProvider;
import appeng.api.storage.MEStorage;
import appeng.api.storage.cells.IBasicCellItem;
import appeng.blockentity.storage.DriveBlockEntity;
import appeng.core.definitions.AEItems;
import appeng.crafting.execution.CraftingCpuLogic;
import appeng.crafting.pattern.EncodedPatternItem;
import appeng.helpers.patternprovider.PatternContainer;
import appeng.items.storage.BasicStorageCell;
import appeng.me.cells.BasicCellHandler;
import appeng.me.cells.BasicCellInventory;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import appeng.parts.storagebus.StorageBusPart;
import de.srendi.advancedperipherals.AdvancedPeripherals;
import de.srendi.advancedperipherals.common.addons.APAddon;
import de.srendi.advancedperipherals.common.setup.BlockEntityTypes;
import de.srendi.advancedperipherals.common.util.LuaConverter;
import de.srendi.advancedperipherals.common.util.Pair;
import de.srendi.advancedperipherals.common.util.StatusConstants;
import de.srendi.advancedperipherals.common.util.inventory.ChemicalFilter;
import de.srendi.advancedperipherals.common.util.inventory.FluidFilter;
import de.srendi.advancedperipherals.common.util.inventory.FluidUtil;
import de.srendi.advancedperipherals.common.util.inventory.GenericFilter;
import de.srendi.advancedperipherals.common.util.inventory.InventoryUtil;
import de.srendi.advancedperipherals.common.util.inventory.ItemFilter;
import io.github.projectet.ae2things.item.DISKDrive;
import io.github.projectet.ae2things.storage.DISKCellHandler;
import io.github.projectet.ae2things.storage.DISKCellInventory;
import it.unimi.dsi.fastutil.objects.Object2LongMap;
import me.ramidzkh.mekae2.ae2.MekanismKey;
import me.ramidzkh.mekae2.ae2.MekanismKeyType;
import me.ramidzkh.mekae2.item.ChemicalStorageCell;
import mekanism.api.chemical.ChemicalStack;
import mekanism.common.tile.TileEntityChemicalTank;
import net.minecraft.core.BlockPos;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent;
import net.neoforged.neoforge.fluids.FluidStack;
import net.neoforged.neoforge.fluids.capability.IFluidHandler;
import net.neoforged.neoforge.items.IItemHandler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class AEApi {

    public static void registerCapabilities(RegisterCapabilitiesEvent event) {
        event.registerBlockEntity(AECapabilities.IN_WORLD_GRID_NODE_HOST, BlockEntityTypes.ME_BRIDGE.get(), (blockEntity, side) -> blockEntity);
    }

    @NotNull
    public static Pair<Long, AEItemKey> findAEStackFromStack(MEStorage monitor, @Nullable ICraftingService crafting, ItemStack item) {
        return findAEStackFromFilter(monitor, crafting, ItemFilter.fromStack(item));
    }

    @NotNull
    public static Pair<Long, AEItemKey> findAEStackFromFilter(MEStorage monitor, @Nullable ICraftingService crafting, ItemFilter filter) {
        for (Object2LongMap.Entry<AEKey> temp : monitor.getAvailableStacks()) {
            if (temp.getKey() instanceof AEItemKey key && filter.test(key.toStack()))
                return Pair.of(temp.getLongValue(), key);
        }

        if (crafting == null)
            return Pair.of(0L, null);

        for (var temp : crafting.getCraftables(param -> true)) {
            if (temp instanceof AEItemKey key && filter.test(key.toStack()))
                return Pair.of(0L, key);
        }

        return Pair.of(0L, null);
    }

    @NotNull
    public static List<Pair<Long, AEItemKey>> findAEStacksFromFilter(MEStorage monitor, ItemFilter filter) {
        List<Pair<Long, AEItemKey>> items = new ArrayList<>();
        for (Object2LongMap.Entry<AEKey> temp : monitor.getAvailableStacks()) {
            if (temp.getKey() instanceof AEItemKey key && filter.test(key.toStack())) {
                items.add(Pair.of(temp.getLongValue(), key));
            }
        }
        return items;
    }

    @NotNull
    public static Pair<Long, AEFluidKey> findAEFluidFromStack(MEStorage monitor, @Nullable ICraftingService crafting, FluidStack item) {
        return findAEFluidFromFilter(monitor, crafting, FluidFilter.fromStack(item));
    }

    @NotNull
    public static Pair<Long, AEFluidKey> findAEFluidFromFilter(MEStorage monitor, @Nullable ICraftingService crafting, FluidFilter filter) {
        for (Object2LongMap.Entry<AEKey> temp : monitor.getAvailableStacks()) {
            if (temp.getKey() instanceof AEFluidKey key && filter.test(key.toStack(1)))
                return Pair.of(temp.getLongValue(), key);
        }

        if (crafting == null)
            return Pair.of(0L, null);

        for (var temp : crafting.getCraftables(param -> true)) {
            if (temp instanceof AEFluidKey key && filter.test(key.toStack(1)))
                return Pair.of(0L, key);
        }

        return Pair.of(0L, null);
    }

    @NotNull
    public static List<Pair<Long, AEFluidKey>> findAEFluidsFromFilter(MEStorage monitor, FluidFilter filter) {
        List<Pair<Long, AEFluidKey>> fluids = new ArrayList<>();
        for (Object2LongMap.Entry<AEKey> temp : monitor.getAvailableStacks()) {
            if (temp.getKey() instanceof AEFluidKey key && filter.test(key.toStack(1))) {
                fluids.add(Pair.of(temp.getLongValue(), key));
            }
        }
        return fluids;
    }

    @NotNull
    public static Pair<Long, MekanismKey> findAEChemicalFromStack(MEStorage monitor, @Nullable ICraftingService crafting, ChemicalStack stack) {
        return findAEChemicalFromFilter(monitor, crafting, ChemicalFilter.fromStack(stack));
    }

    @NotNull
    public static Pair<Long, MekanismKey> findAEChemicalFromFilter(MEStorage monitor, @Nullable ICraftingService crafting, ChemicalFilter filter) {
        for (Object2LongMap.Entry<AEKey> temp : monitor.getAvailableStacks()) {
            if (temp.getKey() instanceof MekanismKey key && filter.test(key.getStack()))
                return Pair.of(temp.getLongValue(), key);
        }

        if (crafting == null)
            return Pair.of(0L, null);

        for (var temp : crafting.getCraftables(param -> true)) {
            if (temp instanceof MekanismKey key && filter.test(key.getStack()))
                return Pair.of(0L, key);
        }

        return Pair.of(0L, null);
    }

    @NotNull
    public static List<Pair<Long, MekanismKey>> findAEChemicalsFromFilter(MEStorage monitor, ChemicalFilter filter) {
        List<Pair<Long, MekanismKey>> chemicals = new ArrayList<>();
        for (Object2LongMap.Entry<AEKey> temp : monitor.getAvailableStacks()) {
            if (temp.getKey() instanceof MekanismKey key && filter.test(key.getStack())) {
                chemicals.add(Pair.of(temp.getLongValue(), key));
            }
        }
        return chemicals;
    }

    /**
     * Finds a pattern from filters.
     *
     * @param grid         The grid to search patterns from.
     * @param level        The level of the grid.
     * @param inputFilter  The input filter to apply, can be null to ignore input filter.
     * @param outputFilter The output filter to apply, can be null to ignore output filter.
     * @return A Pair object containing the matched pattern and an error message if no pattern is found.
     * The pattern can be null if no pattern is found.
     * The error message is "NOT_FOUND" if no pattern is found. See {@link StatusConstants#NOT_FOUND}
     */
    @NotNull
    public static Pair<Pair<EncodedPatternItem<?>, IPatternDetails>, String> findPatternFromFilters(IGrid grid, net.minecraft.world.level.Level level, @Nullable GenericFilter<?> inputFilter, @Nullable GenericFilter<?> outputFilter) {
        for (Pair<EncodedPatternItem<?>, IPatternDetails> pattern : getPatterns(grid, level)) {
            IPatternDetails patternDetails = pattern.getRight();
            if (patternDetails.getInputs().length == 0)
                continue;
            if (patternDetails.getOutputs().isEmpty())
                continue;

            boolean inputMatch = false;
            boolean outputMatch = false;

            if (inputFilter != null) {
                outerLoop:
                for (IPatternDetails.IInput input : patternDetails.getInputs()) {
                    for (GenericStack possibleInput : input.getPossibleInputs()) {
                        if (inputFilter.testAE(possibleInput)) {
                            inputMatch = true;
                            break outerLoop;
                        }
                    }
                }
            } else {
                inputMatch = true;
            }

            if (outputFilter != null) {
                for (GenericStack output : patternDetails.getOutputs()) {
                    if (outputFilter.testAE(output)) {
                        outputMatch = true;
                        break;
                    }
                }
            } else {
                outputMatch = true;
            }

            if (inputMatch && outputMatch)
                return Pair.of(Pair.of(pattern.getLeft(), patternDetails), null);
        }

        return Pair.of(null, StatusConstants.NOT_FOUND.toString());
    }

    public static List<Object> listItems(MEStorage monitor, ICraftingService service, ItemFilter filter) {
        List<Object> items = new ArrayList<>();
        KeyCounter keyCounter = monitor.getAvailableStacks();
        for (Object2LongMap.Entry<AEKey> aeKey : keyCounter) {
            if (aeKey.getKey() instanceof AEItemKey itemKey && filter.test(itemKey.getReadOnlyStack())) {
                items.add(parseAeStack(Pair.of(aeKey.getLongValue(), itemKey), service));
            }
        }
        return items;
    }

    public static List<Object> listCraftableItems(MEStorage monitor, ICraftingService service, ItemFilter filter) {
        List<Object> items = new ArrayList<>();
        KeyCounter keyCounter = monitor.getAvailableStacks();
        Set<AEKey> craftables = service.getCraftables(AEKeyFilter.none());
        for (AEKey aeKey : craftables) {
            if (aeKey instanceof AEItemKey itemKey && filter.test(itemKey.toStack())) {
                items.add(parseAeStack(Pair.of(keyCounter.get(aeKey), aeKey), service));
            }
        }
        return items;
    }

    public static List<Object> listFluids(MEStorage monitor, ICraftingService service, FluidFilter filter) {
        List<Object> items = new ArrayList<>();
        for (Object2LongMap.Entry<AEKey> aeKey : monitor.getAvailableStacks()) {
            if (aeKey.getKey() instanceof AEFluidKey fluidKey && filter.test(fluidKey.toStack(1))) {
                items.add(parseAeStack(Pair.of(aeKey.getLongValue(), fluidKey), service));
            }
        }
        return items;
    }

    public static List<Object> listChemicals(MEStorage monitor, ICraftingService service, ChemicalFilter filter) {
        List<Object> items = new ArrayList<>();
        for (Object2LongMap.Entry<AEKey> aeKey : monitor.getAvailableStacks()) {
            if (APAddon.APP_MEKANISTICS.isLoaded() && aeKey.getKey() instanceof MekanismKey mekanismKey && filter.test(mekanismKey.getStack())) {
                items.add(parseAeStack(Pair.of(aeKey.getLongValue(), mekanismKey), service));
            }
        }
        return items;
    }

    public static List<Object> listCraftableFluids(MEStorage monitor, ICraftingService service, FluidFilter filter) {
        List<Object> items = new ArrayList<>();
        KeyCounter keyCounter = monitor.getAvailableStacks();
        Set<AEKey> craftables = service.getCraftables(AEKeyFilter.none());
        for (AEKey aeKey : craftables) {
            if (aeKey instanceof AEFluidKey fluidKey && filter.test(fluidKey.toStack(1))) {
                items.add(parseAeStack(Pair.of(keyCounter.get(aeKey), aeKey), service));
            }
        }
        return items;
    }

    public static List<Object> listCraftableChemicals(MEStorage monitor, ICraftingService service, ChemicalFilter filter) {
        List<Object> items = new ArrayList<>();
        KeyCounter keyCounter = monitor.getAvailableStacks();
        Set<AEKey> craftables = service.getCraftables(AEKeyFilter.none());
        for (AEKey aeKey : craftables) {
            if (aeKey instanceof MekanismKey mekanismKey && filter.test(mekanismKey.getStack())) {
                items.add(parseAeStack(Pair.of(keyCounter.get(aeKey), aeKey), service));
            }
        }
        return items;
    }

    public static List<Pair<EncodedPatternItem<?>, IPatternDetails>> getPatterns(IGrid grid, Level level) {
        List<Pair<EncodedPatternItem<?>, IPatternDetails>> patterns = new ArrayList<>();

        for (var machineClass : grid.getMachineClasses()) {
            var containerClass = tryCastMachineToContainer(machineClass);
            if (containerClass == null)
                continue;

            for (var container : grid.getActiveMachines(containerClass)) {
                for (ItemStack patternItem : container.getTerminalPatternInventory()) {
                    if (patternItem.getItem() instanceof EncodedPatternItem<?> item) {
                        IPatternDetails patternDetails = item.decode(patternItem, level);
                        if (patternDetails == null)
                            continue;

                        patterns.add(Pair.of(item, patternDetails));
                    }
                }
            }
        }
        return patterns;
    }

    public static List<Object> listPatterns(IGrid grid, Level level) {
        return getPatterns(grid, level).stream().map(AEApi::parsePattern).collect(Collectors.toList());
    }

    public static List<Object> listDrives(IGrid grid) {
        List<Object> drives = new ArrayList<>();

        for (IGridNode node : grid.getMachineNodes(DriveBlockEntity.class)) {
            DriveBlockEntity drive = (DriveBlockEntity) node.getService(IStorageProvider.class);

            // A normal drive has a cellCount of 10
            if (drive == null || drive.getCellCount() != 10)
                continue;

            drives.add(parseDrive(drive));
        }

        return drives;
    }

    private static Class<? extends PatternContainer> tryCastMachineToContainer(Class<?> machineClass) {
        if (PatternContainer.class.isAssignableFrom(machineClass))
            return machineClass.asSubclass(PatternContainer.class);

        return null;
    }

    public static <T extends AEKey> Map<String, Object> parseAeStack(Pair<Long, T> stack, @Nullable ICraftingService service) {
        if (stack.getRight() == null)
            return null;
        if (stack.getRight() instanceof AEItemKey itemKey)
            return parseItemStack(Pair.of(stack.getLeft(), itemKey), service);
        if (stack.getRight() instanceof AEFluidKey fluidKey)
            return parseFluidStack(Pair.of(stack.getLeft(), fluidKey), service);
        if (APAddon.APP_MEKANISTICS.isLoaded() && (stack.getRight() instanceof MekanismKey gasKey))
            return parseChemStack(Pair.of(stack.getLeft(), gasKey), service);

        AdvancedPeripherals.debug("Could not create table from unknown stack " + stack.getRight().getClass() + " - Report this to the maintainer of ap", org.apache.logging.log4j.Level.WARN);
        return null;
    }

    public static Map<String, Object> parseGenericStack(GenericStack stack) {
        if (stack.what() == null)
            return null;
        if (stack.what() instanceof AEItemKey aeItemKey)
            return parseItemStack(Pair.of(stack.amount(), aeItemKey), null);
        if (stack.what() instanceof AEFluidKey aeFluidKey)
            return parseFluidStack(Pair.of(stack.amount(), aeFluidKey), null);

        AdvancedPeripherals.debug("Could not create table from unknown stack " + stack.getClass() + " - Report this to the maintainer of ap", org.apache.logging.log4j.Level.WARN);
        return null;
    }

    public static List<Object> parseKeyCounter(KeyCounter counter) {
        List<Object> parsedKeys = new ArrayList<>();
        for (AEKey key : counter.keySet()) {
            parsedKeys.add(parseGenericStack(new GenericStack(key, counter.get(key))));
        }

        return parsedKeys;
    }

    public static Map<Object, Object> parseDrive(DriveBlockEntity drive) {
        long totalBytes = 0;
        long usedBytes = 0;

        List<Object> driveCells = new ArrayList<>();
        for (ItemStack item : drive.getInternalInventory()) {
            if (item.getItem() instanceof BasicStorageCell cell) {
                BasicCellInventory cellInventory = BasicCellHandler.INSTANCE.getCellInventory(item, null);
                totalBytes += cellInventory.getTotalBytes();
                usedBytes += cellInventory.getUsedBytes();

                driveCells.add(parseCell(cell, item));
            }
        }

        Map<Object, Object> properties = new HashMap<>();

        properties.put("usedBytes", usedBytes);
        properties.put("totalBytes", totalBytes);
        properties.put("cells", driveCells);
        properties.put("priority", drive.getPriority());
        properties.put("menuIcon", LuaConverter.itemToObject(drive.getMainMenuIcon().getItem()));
        properties.put("position", LuaConverter.posToObject(drive.getBlockPos()));
        properties.put("name", drive.hasCustomName() ? drive.getCustomName().getString() : drive.getDisplayName().getString());

        return properties;
    }

    public static Map<Object, Object> parseCell(IBasicCellItem cell, ItemStack cellItem) {
        Map<Object, Object> properties = new HashMap<>();
        BasicCellInventory cellInventory = BasicCellHandler.INSTANCE.getCellInventory(cellItem, null);

        properties.put("item", LuaConverter.itemToObject(cellItem.getItem()));
        properties.put("type", cell.getKeyType().toString());
        properties.put("bytes", cell.getBytes(cellItem));
        properties.put("bytesPerType", cell.getBytesPerType(cellItem));
        properties.put("usedBytes", cellInventory.getUsedBytes());
        properties.put("totalTypes", cell.getTotalTypes(cellItem));
        properties.put("fuzzyMode", cell.getFuzzyMode(cellItem).toString());

        return properties;
    }

    private static Map<String, Object> parseDISKDrive(DISKDrive drive, ItemStack stack) {
        Map<String, Object> properties = new HashMap<>();
        DISKCellInventory cellInventory = DISKCellHandler.INSTANCE.getCellInventory(stack, null);

        if (cellInventory == null)
            return null;

        properties.put("item", LuaConverter.itemToObject(stack.getItem()));
        properties.put("type", drive.getKeyType().toString());
        properties.put("bytes", drive.getBytes(stack));
        properties.put("bytesPerType", 0);
        properties.put("usedBytes", cellInventory.getNbtItemCount());
        properties.put("totalTypes", 0);
        properties.put("fuzzyMode", drive.getFuzzyMode(stack).toString());

        return properties;
    }

    private static Map<String, Object> parseItemStack(Pair<Long, AEItemKey> stack, @Nullable ICraftingService craftingService) {
        Map<String, Object> properties = LuaConverter.itemStackToObject(stack.getRight().getReadOnlyStack());
        properties.put("count", stack.getLeft());
        properties.put("isCraftable", craftingService != null && craftingService.isCraftable(stack.getRight()));
        return properties;
    }

    private static Map<String, Object> parseFluidStack(Pair<Long, AEFluidKey> stack, @Nullable ICraftingService craftingService) {
        Map<String, Object> properties = LuaConverter.fluidStackToObject(stack.getRight().toStack(1));
        properties.put("count", stack.getLeft());
        properties.put("isCraftable", craftingService != null && craftingService.isCraftable(stack.getRight()));
        return properties;
    }

    private static Map<String, Object> parseChemStack(Pair<Long, MekanismKey> stack, @Nullable ICraftingService craftingService) {
        Map<String, Object> properties = LuaConverter.chemicalStackToObject(stack.getRight().withAmount(stack.getLeft()));
        properties.put("isCraftable", craftingService != null && craftingService.isCraftable(stack.getRight()));
        return properties;
    }

    public static Map<String, Object> parsePattern(Pair<EncodedPatternItem<?>, IPatternDetails> pattern) {
        Map<String, Object> properties = new HashMap<>();
        IPatternDetails patternDetails = pattern.getRight();
        String patternType = getPatternType(pattern.getLeft());

        properties.put("inputs", Arrays.stream(patternDetails.getInputs()).map(AEApi::parsePatternInput).collect(Collectors.toList()));
        properties.put("outputs", patternDetails.getOutputs().stream().map(AEApi::parseGenericStack).collect(Collectors.toList()));
        properties.put("primaryOutput", parseGenericStack(patternDetails.getPrimaryOutput()));
        properties.put("patternType", patternType);
        return properties;
    }

    private static String getPatternType(EncodedPatternItem<?> patternItem) {
        if (patternItem.equals(AEItems.CRAFTING_PATTERN.get())) return "crafting";
        if (patternItem.equals(AEItems.PROCESSING_PATTERN.get())) return "processing";
        if (patternItem.equals(AEItems.SMITHING_TABLE_PATTERN.get())) return "smithing";
        if (patternItem.equals(AEItems.STONECUTTING_PATTERN.get())) return "stonecutting";
        return "unknown";
    }

    public static Map<String, Object> parsePatternInput(IPatternDetails.IInput patternInput) {
        Map<String, Object> properties = new HashMap<>();
        GenericStack primaryInput = patternInput.getPossibleInputs()[0];
        properties.put("primaryInput", parseGenericStack(primaryInput));
        properties.put("possibleInputs", Arrays.stream(Arrays.copyOfRange(patternInput.getPossibleInputs(), 1, patternInput.getPossibleInputs().length)).map(AEApi::parseGenericStack));
        properties.put("multiplier", patternInput.getMultiplier());

        AEKey remainingKey = patternInput.getRemainingKey(patternInput.getPossibleInputs()[0].what());
        Map<String, Object> remainingKeyProperties = remainingKey == null ? null : parseGenericStack(new GenericStack(remainingKey, 1));
        properties.put("remaining", remainingKeyProperties);

        return properties;
    }

    public static Map<String, Object> parseCraftingCPU(ICraftingCPU cpu, boolean recursive) {
        Map<String, Object> properties = new HashMap<>();
        long storage = cpu.getAvailableStorage();
        int coProcessors = cpu.getCoProcessors();
        boolean isBusy = cpu.isBusy();
        properties.put("storage", storage);
        properties.put("coProcessors", coProcessors);
        properties.put("isBusy", isBusy);
        if (!recursive)
            properties.put("craftingJob", cpu.getJobStatus() != null ? parseCraftingJob(cpu.getJobStatus(), null, null) : null);
        properties.put("name", cpu.getName() != null ? cpu.getName().getString() : "Unnamed");
        properties.put("selectionMode", cpu.getSelectionMode().toString());

        return properties;
    }

    public static Object parseCraftingJob(CraftingJobStatus status, @Nullable AECraftJob craftJob, @Nullable ICraftingCPU cpu) {
        Map<String, Object> properties = new HashMap<>();

        properties.put("bridge_id", craftJob == null ? -1 : craftJob.getId());
        properties.put("quantity", status.crafting().amount());
        properties.put("resource", parseGenericStack(status.crafting()));

        if (cpu != null) {
            CraftingCpuLogic craftingCpuLogic = ((CraftingCPUCluster) cpu).craftingLogic;
            long pending = craftingCpuLogic.getPendingOutputs(status.crafting().what());
            long active = craftingCpuLogic.getWaitingFor(status.crafting().what());
            long crafted = status.crafting().amount() - (pending + active);
            properties.put("completion", crafted / (double) status.crafting().amount());
            properties.put("crafted", crafted);

            properties.put("id", craftingCpuLogic.getLastLink().getCraftingID().toString());

            properties.put("cpu", parseCraftingCPU(cpu, true));
        }

        return properties;
    }

    public static MEStorage getMonitor(IGridNode node) {
        return node.getGrid().getService(IStorageService.class).getInventory();
    }

    public static boolean isCrafting(ICraftingService grid, GenericFilter<?> filter, @Nullable ICraftingCPU craftingCPU) {

        // If the passed cpu is null, check all cpus
        if (craftingCPU == null) {
            // Loop through all crafting cpus and check if the item is being crafted.
            for (ICraftingCPU cpu : grid.getCpus()) {
                if (cpu.isBusy()) {
                    CraftingJobStatus jobStatus = cpu.getJobStatus();

                    // avoid null pointer exception
                    if (jobStatus == null)
                        continue;

                    if (filter.testAE(jobStatus.crafting()))
                        return true;
                }
            }
        } else {
            if (craftingCPU.isBusy()) {
                CraftingJobStatus jobStatus = craftingCPU.getJobStatus();

                // avoid null pointer exception
                if (jobStatus == null)
                    return false;

                return filter.testAE(jobStatus.crafting());
            }
        }

        return false;
    }

    /// External Storage
    /// Total

    public static long getTotalExternalItemStorage(IGridNode node) {
        long total = 0;

        for (IGridNode iGridNode : node.getGrid().getMachineNodes(StorageBusPart.class)) {
            StorageBusPart bus = (StorageBusPart) iGridNode.getService(IStorageProvider.class);
            BlockPos connectedInventoryPos = bus.getHost().getBlockEntity().getBlockPos().relative(bus.getSide());

            IItemHandler itemHandler = InventoryUtil.extractHandler(null, bus.getLevel(), connectedInventoryPos, bus.getSide());
            if (itemHandler != null) {
                for (int i = 0; i < itemHandler.getSlots(); i++) {
                    ItemStack stack = itemHandler.getStackInSlot(i);

                    total += stack.isEmpty() ? itemHandler.getSlotLimit(i) : stack.getMaxStackSize();
                }
            }
        }

        return total;
    }

    public static long getTotalExternalFluidStorage(IGridNode node) {
        long total = 0;

        for (IGridNode iGridNode : node.getGrid().getMachineNodes(StorageBusPart.class)) {
            StorageBusPart bus = (StorageBusPart) iGridNode.getService(IStorageProvider.class);
            BlockPos connectedInventoryPos = bus.getHost().getBlockEntity().getBlockPos().relative(bus.getSide());

            IFluidHandler fluidHandler = FluidUtil.extractHandler(null, bus.getLevel(), connectedInventoryPos, bus.getSide());
            if (fluidHandler != null) {
                for (int i = 0; i < fluidHandler.getTanks(); i++) {
                    total += fluidHandler.getTankCapacity(i);
                }
            }
        }

        return total;
    }

    public static long getTotalExternalChemicalStorage(IGridNode node) {
        long total = 0;

        if (!APAddon.APP_MEKANISTICS.isLoaded())
            return 0;

        for (IGridNode iGridNode : node.getGrid().getMachineNodes(StorageBusPart.class)) {
            StorageBusPart bus = (StorageBusPart) iGridNode.getService(IStorageProvider.class);
            net.minecraft.world.level.Level level = bus.getLevel();
            BlockPos connectedInventoryPos = bus.getHost().getBlockEntity().getBlockPos().relative(bus.getSide());
            BlockEntity connectedInventoryEntity = level.getBlockEntity(connectedInventoryPos);

            if (connectedInventoryEntity == null)
                continue;

            if (connectedInventoryEntity instanceof TileEntityChemicalTank tank) {
                total += tank.getChemicalTank().getCapacity();
            }
        }

        return total;
    }

    /// Used

    public static long getUsedExternalItemStorage(IGridNode node) {
        long used = 0;

        for (IGridNode iGridNode : node.getGrid().getMachineNodes(StorageBusPart.class)) {
            StorageBusPart bus = (StorageBusPart) iGridNode.getService(IStorageProvider.class);
            KeyCounter keyCounter = bus.getInternalHandler().getAvailableStacks();

            for (Object2LongMap.Entry<AEKey> aeKey : keyCounter) {
                if (aeKey.getKey() instanceof AEItemKey) used += aeKey.getLongValue();
            }
        }

        return used;
    }

    public static long getUsedExternalFluidStorage(IGridNode node) {
        long used = 0;

        for (IGridNode iGridNode : node.getGrid().getMachineNodes(StorageBusPart.class)) {
            StorageBusPart bus = (StorageBusPart) iGridNode.getService(IStorageProvider.class);
            KeyCounter keyCounter = bus.getInternalHandler().getAvailableStacks();

            for (Object2LongMap.Entry<AEKey> aeKey : keyCounter) {
                if (aeKey.getKey() instanceof AEFluidKey) used += aeKey.getLongValue();
            }
        }

        return used;
    }

    public static long getUsedExternalChemicalStorage(IGridNode node) {
        long used = 0;

        if (!APAddon.APP_MEKANISTICS.isLoaded())
            return 0;

        for (IGridNode iGridNode : node.getGrid().getMachineNodes(StorageBusPart.class)) {
            StorageBusPart bus = (StorageBusPart) iGridNode.getService(IStorageProvider.class);
            KeyCounter keyCounter = bus.getInternalHandler().getAvailableStacks();

            for (Object2LongMap.Entry<AEKey> aeKey : keyCounter) {
                if (aeKey.getKey() instanceof MekanismKey) used += aeKey.getLongValue();
            }
        }

        return used;
    }

    /// Internal Storage
    /// Total

    public static long getTotalItemStorage(IGridNode node) {
        long total = 0;

        // note: do not query DriveBlockEntity.class specifically here, because it will avoid subclasses, e.g. the ME Extended Drive from ExtendedAE
        for (IGridNode iGridNode : node.getGrid().getNodes()) {
            if (!(iGridNode.getService(IStorageProvider.class) instanceof DriveBlockEntity entity))
                continue;

            InternalInventory inventory = entity.getInternalInventory();

            for (int i = 0; i < inventory.size(); i++) {
                ItemStack stack = inventory.getStackInSlot(i);

                if (stack.isEmpty())
                    continue;

                if (stack.getItem() instanceof IBasicCellItem cell) {
                    if (cell.getKeyType().getClass().isAssignableFrom(AEKeyType.items().getClass())) {
                        total += cell.getBytes(null);
                    }
                } else if (APAddon.AE2_THINGS.isLoaded() && stack.getItem() instanceof DISKDrive disk) {
                    if (disk.getKeyType().toString().equals("ae2:i")) {
                        total += disk.getBytes(null);
                    }
                }
            }
        }
        return total;
    }

    public static long getTotalFluidStorage(IGridNode node) {
        long total = 0;

        for (IGridNode iGridNode : node.getGrid().getNodes()) {
            if (!(iGridNode.getService(IStorageProvider.class) instanceof DriveBlockEntity entity))
                continue;

            InternalInventory inventory = entity.getInternalInventory();

            for (int i = 0; i < inventory.size(); i++) {
                ItemStack stack = inventory.getStackInSlot(i);

                if (stack.isEmpty())
                    continue;

