package com.gregtechceu.gtceu.integration.ae2.machine;

import com.gregtechceu.gtceu.api.blockentity.BlockEntityCreationInfo;
import com.gregtechceu.gtceu.api.capability.recipe.FluidRecipeCapability;
import com.gregtechceu.gtceu.api.capability.recipe.IFilteredHandler;
import com.gregtechceu.gtceu.api.capability.recipe.IO;
import com.gregtechceu.gtceu.api.capability.recipe.IRecipeHandler;
import com.gregtechceu.gtceu.api.capability.recipe.ItemRecipeCapability;
import com.gregtechceu.gtceu.api.capability.recipe.RecipeCapability;
import com.gregtechceu.gtceu.api.machine.MetaMachine;
import com.gregtechceu.gtceu.api.machine.MultiblockMachineDefinition;
import com.gregtechceu.gtceu.api.machine.TickableSubscription;
import com.gregtechceu.gtceu.api.machine.feature.IDataStickInteractable;
import com.gregtechceu.gtceu.api.machine.mui.MachineUIPanelBuilder;
import com.gregtechceu.gtceu.api.machine.multiblock.MultiblockControllerMachine;
import com.gregtechceu.gtceu.api.machine.trait.notifiable.NotifiableFluidTank;
import com.gregtechceu.gtceu.api.machine.trait.notifiable.NotifiableItemStackHandler;
import com.gregtechceu.gtceu.api.machine.trait.notifiable.NotifiableRecipeHandlerTrait;
import com.gregtechceu.gtceu.api.machine.trait.recipe.RecipeHandlerGroupDistinctness;
import com.gregtechceu.gtceu.api.machine.trait.recipe.RecipeHandlerList;
import com.gregtechceu.gtceu.api.recipe.GTRecipe;
import com.gregtechceu.gtceu.api.recipe.ingredient.FluidIngredient;
import com.gregtechceu.gtceu.api.recipe.ingredient.SizedIngredient;
import com.gregtechceu.gtceu.api.sync_system.annotations.SaveField;
import com.gregtechceu.gtceu.api.sync_system.annotations.SyncToClient;
import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler;
import com.gregtechceu.gtceu.common.data.machines.GTAEMachines;
import com.gregtechceu.gtceu.common.item.behavior.IntCircuitBehaviour;
import com.gregtechceu.gtceu.common.mui.GTGuiTextures;
import com.gregtechceu.gtceu.common.mui.GTMuiMachineUtil;
import com.gregtechceu.gtceu.common.mui.widgets.PopupPanel;
import com.gregtechceu.gtceu.utils.GTMath;
import com.gregtechceu.gtceu.utils.ItemStackHashStrategy;

import net.minecraft.MethodsReturnNonnullByDefault;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.Component;
import net.minecraft.util.Mth;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.level.material.Fluid;
import net.minecraftforge.common.util.INBTSerializable;
import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.fluids.FluidType;

import appeng.api.crafting.IPatternDetails;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.implementations.blockentities.PatternContainerGroup;
import appeng.api.inventories.InternalInventory;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridNodeListener;
import appeng.api.networking.crafting.ICraftingProvider;
import appeng.api.stacks.*;
import appeng.api.storage.MEStorage;
import appeng.api.storage.StorageHelper;
import appeng.crafting.pattern.EncodedPatternItem;
import appeng.crafting.pattern.ProcessingPatternItem;
import appeng.helpers.patternprovider.PatternContainer;
import brachy.modularui.api.IPanelHandler;
import brachy.modularui.api.drawable.Text;
import brachy.modularui.drawable.DrawableStack;
import brachy.modularui.drawable.DynamicDrawable;
import brachy.modularui.drawable.ItemDrawable;
import brachy.modularui.factory.PosGuiData;
import brachy.modularui.screen.RichTooltip;
import brachy.modularui.screen.UISettings;
import brachy.modularui.value.sync.BooleanSyncValue;
import brachy.modularui.value.sync.PanelSyncManager;
import brachy.modularui.value.sync.SyncHandlers;
import brachy.modularui.widget.ParentWidget;
import brachy.modularui.widgets.ButtonWidget;
import brachy.modularui.widgets.layout.Flow;
import brachy.modularui.widgets.layout.Grid;
import brachy.modularui.widgets.slot.ItemSlot;
import brachy.modularui.widgets.slot.SlotGroup;
import brachy.modularui.widgets.textfield.TextFieldWidget;
import com.mojang.blaze3d.platform.InputConstants;
import it.unimi.dsi.fastutil.objects.*;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
import org.jetbrains.annotations.VisibleForTesting;

import java.util.*;

import javax.annotation.ParametersAreNonnullByDefault;

@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
public class MEPatternBufferPartMachine extends MEBusPartMachine
                                        implements ICraftingProvider, PatternContainer, IDataStickInteractable {

    protected static final int MAX_PATTERN_COUNT = 27;
    private final InternalInventory internalPatternInventory = new InternalInventory() {

        @Override
        public int size() {
            return MAX_PATTERN_COUNT;
        }

        @Override
        public ItemStack getStackInSlot(int slotIndex) {
            return patternInventory.getStackInSlot(slotIndex);
        }

        @Override
        public void setItemDirect(int slotIndex, ItemStack stack) {
            patternInventory.setStackInSlot(slotIndex, stack);
            patternInventory.onContentsChanged(slotIndex);
            onPatternChange(slotIndex);
        }
    };

    @Getter
    @SaveField
    @SyncToClient
    // Maybe an Expansion Option in the future? a bit redundant for rn. Maybe Packdevs want to add their own
    // version.
    private final CustomItemStackHandler patternInventory = new CustomItemStackHandler(MAX_PATTERN_COUNT);

    @Getter
    @SaveField
    protected final NotifiableItemStackHandler shareInventory;

    @Getter
    @SaveField
    protected final NotifiableFluidTank shareTank;

    @SaveField
    protected final InternalSlot[] internalInventory = new InternalSlot[MAX_PATTERN_COUNT];

    private final @Nullable IPatternDetails[] patternSlotDetails = new IPatternDetails[MAX_PATTERN_COUNT];

    @SyncToClient
    @SaveField
    private String customName = "";

    private boolean needPatternSync;

    @SaveField
    private final Set<BlockPos> proxies = new ObjectOpenHashSet<>();
    private final Set<MEPatternBufferProxyPartMachine> proxyMachines = new ReferenceOpenHashSet<>();

    @Nullable
    protected TickableSubscription updateSubs;

    private final List<Worker> workers = new ArrayList<>();

    @Getter
    private final AggregateItemHandler aggregateItemHandler;
    @Getter
    private final AggregateFluidHandler aggregateFluidHandler;
    @Getter
    private final RecipeHandlerList bufferRecipeHandler;
    private final List<RecipeHandlerList> bufferHandlers;

    public MEPatternBufferPartMachine(BlockEntityCreationInfo info) {
        super(info, IO.IN, new NotifiableItemStackHandler(9, IO.IN, IO.NONE));
        patternInventory.setOnContentsChanged(() -> getSyncDataHolder().markClientSyncFieldDirty("patternInventory"));
        this.patternInventory.setFilter(stack -> stack.getItem() instanceof ProcessingPatternItem);

        for (int i = 0; i < this.internalInventory.length; i++) {
            this.internalInventory[i] = new InternalSlot();
        }

        getMainNode().addService(ICraftingProvider.class, this);

        this.shareInventory = attachTrait(new NotifiableItemStackHandler(9, IO.IN, IO.NONE));
        this.shareTank = attachTrait(new NotifiableFluidTank(9, 8 * FluidType.BUCKET_VOLUME, IO.IN, IO.NONE));
        this.aggregateItemHandler = attachTrait(new AggregateItemHandler());
        this.aggregateFluidHandler = attachTrait(new AggregateFluidHandler());
        this.bufferRecipeHandler = new BufferRecipeHandlerList();
        this.bufferHandlers = List.of(bufferRecipeHandler);
        addWorker();
    }

    @Override
    public void onLoad() {
        super.onLoad();
        if (!isRemote()) {
            for (int i = 0; i < patternInventory.getSlots(); i++) {
                patternSlotDetails[i] = PatternDetailsHelper.decodePattern(patternInventory.getStackInSlot(i),
                        getLevel());
            }
            needPatternSync = true;
            syncWorkerCount();
        }
    }

    private void syncWorkerCount() {
        int target = Mth.clamp(proxies.size() + 1, 1, MAX_PATTERN_COUNT);
        while (workers.size() < target) {
            addWorker();
        }
        while (workers.size() > target) {
            removeLastWorker();
        }
    }

    private void addWorker() {
        int idx = workers.size();
        if (idx >= MAX_PATTERN_COUNT) {
            return;
        }
        InternalSlot slot = internalInventory[idx];
        slot.setOnContentsChanged(this::onSlotChanged);
        workers.add(new Worker(slot));
    }

    private void removeLastWorker() {
        int last = workers.size() - 1;
        if (last < 0) return;
        Worker worker = workers.get(last);
        worker.slot.refund();
        worker.slot.setOnContentsChanged(() -> {});
        workers.remove(last);
        onSlotChanged();
    }

    private void onSlotChanged() {
        aggregateItemHandler.notifyListeners();
        aggregateFluidHandler.notifyListeners();
    }

    static boolean couldSlotMatchContents(InternalSlot slot, Map<RecipeCapability<?>, List<Object>> contents) {
        List<Object> itemContents = contents.get(ItemRecipeCapability.CAP);
        if (itemContents != null && !slot.isItemEmpty()) {
            Set<Item> itemTypes = slot.getItemTypes();
            for (Object obj : itemContents) {
                if (!(obj instanceof Ingredient ing) || ing.isEmpty()) {
                    continue;
                }
                for (ItemStack stack : ing.getItems()) {
                    if (itemTypes.contains(stack.getItem())) {
                        return true;
                    }
                }
            }
        }
        List<Object> fluidContents = contents.get(FluidRecipeCapability.CAP);
        if (fluidContents != null && !slot.isFluidEmpty()) {
            Set<Fluid> fluidTypes = slot.getFluidTypes();
            for (Object obj : fluidContents) {
                if (!(obj instanceof FluidIngredient ing) || ing.isEmpty()) {
                    continue;
                }
                for (FluidStack stack : ing.getStacks()) {
                    if (fluidTypes.contains(stack.getFluid())) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    static class Worker {

        final InternalSlot slot;
        @Nullable
        IPatternDetails pattern;
        final SlotItemHandler itemHandler;
        final SlotFluidHandler fluidHandler;

        Worker(InternalSlot slot) {
            this.slot = slot;
            this.itemHandler = new SlotItemHandler(slot);
            this.fluidHandler = new SlotFluidHandler(slot);
        }
    }

    static class SlotItemHandler implements IRecipeHandler<Ingredient> {

        private final InternalSlot slot;

        SlotItemHandler(InternalSlot slot) {
            this.slot = slot;
        }

        @Override
        public List<Ingredient> handleRecipeInner(IO io, GTRecipe recipe, List<Ingredient> left, boolean simulate) {
            if (io != IO.IN || slot.isItemEmpty()) {
                return left;
            }
            return slot.handleItemInternal(left, simulate);
        }

        @Override
        public List<Object> getContents() {
            return new ArrayList<>(slot.getItems());
        }

        @Override
        public double getTotalContentAmount() {
            return slot.getItems().stream().mapToLong(ItemStack::getCount).sum();
        }

        @Override
        public RecipeCapability<Ingredient> getCapability() {
            return ItemRecipeCapability.CAP;
        }

        @Override
        public boolean isDistinct() {
            return true;
        }

        @Override
        public int getPriority() {
            return IFilteredHandler.HIGH;
        }
    }

    static class SlotFluidHandler implements IRecipeHandler<FluidIngredient> {

        private final InternalSlot slot;

        SlotFluidHandler(InternalSlot slot) {
            this.slot = slot;
        }

        @Override
        public List<FluidIngredient> handleRecipeInner(IO io, GTRecipe recipe, List<FluidIngredient> left,
                                                       boolean simulate) {
            if (io != IO.IN || slot.isFluidEmpty()) return left;
            return slot.handleFluidInternal(left, simulate);
        }

        @Override
        public List<Object> getContents() {
            return new ArrayList<>(slot.getFluids());
        }

        @Override
        public double getTotalContentAmount() {
            return slot.getFluids().stream().mapToLong(FluidStack::getAmount).sum();
        }

        @Override
        public RecipeCapability<FluidIngredient> getCapability() {
            return FluidRecipeCapability.CAP;
        }

        @Override
        public boolean isDistinct() {
            return true;
        }

        @Override
        public int getPriority() {
            return IFilteredHandler.HIGH;
        }
    }

    @Getter
    class AggregateItemHandler extends NotifiableRecipeHandlerTrait<Ingredient> {

        private final RecipeCapability<Ingredient> capability = ItemRecipeCapability.CAP;
        private final IO handlerIO = IO.IN;
        private final boolean isDistinct = true;

        @Override
        public List<Ingredient> handleRecipeInner(IO io, GTRecipe recipe, List<Ingredient> left, boolean simulate) {
            return left;
        }

        @Override
        public List<Object> getContents() {
            List<Object> contents = new ArrayList<>();
            for (Worker worker : workers) {
                contents.addAll(worker.slot.getItems());
            }
            return contents;
        }

        @Override
        public double getTotalContentAmount() {
            double sum = 0;
            for (Worker worker : workers) {
                sum += worker.slot.getItems().stream().mapToLong(ItemStack::getCount).sum();
            }
            return sum;
        }

        @Override
        public int getPriority() {
            return IFilteredHandler.HIGH;
        }
    }

    @Getter
    class AggregateFluidHandler extends NotifiableRecipeHandlerTrait<FluidIngredient> {

        private final RecipeCapability<FluidIngredient> capability = FluidRecipeCapability.CAP;
        private final IO handlerIO = IO.IN;
        private final boolean isDistinct = true;

        @Override
        public List<FluidIngredient> handleRecipeInner(IO io, GTRecipe recipe, List<FluidIngredient> left,
                                                       boolean simulate) {
            return left;
        }

        @Override
        public List<Object> getContents() {
            List<Object> contents = new ArrayList<>();
            for (Worker worker : workers) {
                contents.addAll(worker.slot.getFluids());
            }
            return contents;
        }

        @Override
        public double getTotalContentAmount() {
            double sum = 0;
            for (Worker worker : workers) {
                sum += worker.slot.getFluids().stream().mapToLong(FluidStack::getAmount).sum();
            }
            return sum;
        }

        @Override
        public int getPriority() {
            return IFilteredHandler.HIGH;
        }
    }

    class BufferRecipeHandlerList extends RecipeHandlerList {

        BufferRecipeHandlerList() {
            super(IO.IN);
            addHandlers(getCircuitSlot(), getShareInventory(), getShareTank(), aggregateItemHandler,
                    aggregateFluidHandler);
            setGroup(RecipeHandlerGroupDistinctness.BUS_DISTINCT);
        }

        @Override
        public Map<RecipeCapability<?>, List<Object>> handleRecipe(IO io, GTRecipe recipe,
                                                                   Map<RecipeCapability<?>, List<Object>> contents,
                                                                   boolean simulate) {
            if (io != IO.IN || contents.isEmpty()) {
                return contents;
            }

            for (Worker worker : workers) {
                InternalSlot slot = worker.slot;
                if (slot.isItemEmpty() && slot.isFluidEmpty()) {
                    continue;
                }
                if (!couldSlotMatchContents(slot, contents)) {
                    continue;
                }

                Map<RecipeCapability<?>, List<Object>> left = consumeWorker(worker, io, recipe, contents, true);
                if (!left.isEmpty()) {
                    continue;
                }

                if (!simulate) {
                    return consumeWorker(worker, io, recipe, contents, false);
                }
                return left;
            }
            return contents;
        }

        private Map<RecipeCapability<?>, List<Object>> consumeWorker(Worker worker, IO io, GTRecipe recipe,
                                                                     Map<RecipeCapability<?>, List<Object>> contents,
                                                                     boolean simulate) {
            var copy = new Reference2ObjectOpenHashMap<>(contents);
            for (var it = copy.reference2ObjectEntrySet().fastIterator(); it.hasNext();) {
                var entry = it.next();
                List<IRecipeHandler<?>> handlers = handlersFor(entry.getKey(), worker);
                for (var handler : handlers) {
                    if (io == IO.IN && handler.getTotalContentAmount() == 0 &&
                            !handler.getCapability().skipEmptyContentCheck()) {
                        continue;
                    }
                    var handlerLeft = handler.handleRecipe(io, recipe, entry.getValue(), simulate);
                    if (handlerLeft.isEmpty()) {
                        it.remove();
                        break;
                    } else {
                        entry.setValue(new ArrayList<>(handlerLeft));
                    }
                }
            }
            return copy;
        }

        private List<IRecipeHandler<?>> handlersFor(RecipeCapability<?> cap, Worker worker) {
            if (cap == ItemRecipeCapability.CAP) {
                return List.of(getCircuitSlot(), getShareInventory(), worker.itemHandler);
            }
            if (cap == FluidRecipeCapability.CAP) {
                return List.of(getShareTank(), worker.fluidHandler);
            }
            return List.of();
        }

        @Override
        public boolean isDistinct() {
            return true;
        }

        @Override
        public void setDistinct(boolean ignored, boolean notify) {}
    }

    @Override
    public @UnmodifiableView List<RecipeHandlerList> getRecipeHandlers() {
        return bufferHandlers;
    }

    @Override
    public boolean isWorkingEnabled() {
        return true;
    }

    public void setCustomName(String newName) {
        customName = newName;
        syncDataHolder.markClientSyncFieldDirty("customName");
        markAsChanged();
    }

    @Override
    public void setWorkingEnabled(boolean ignored) {}

    @Override
    public boolean isDistinct() {
        return true;
    }

    @Override
    public void setDistinct(boolean ignored) {}

    @Override
    public void onMainNodeStateChanged(IGridNodeListener.State reason) {
        super.onMainNodeStateChanged(reason);
        this.updateSubscription();
    }

    protected void updateSubscription() {
        if (getMainNode().isOnline()) {
            updateSubs = subscribeServerTick(updateSubs, this::update);
        } else if (updateSubs != null) {
            updateSubs.unsubscribe();
            updateSubs = null;
        }
    }

    protected void update() {
        if (needPatternSync) {
            ICraftingProvider.requestUpdate(getMainNode());
            this.needPatternSync = false;
        }
    }

    public void addProxy(MEPatternBufferProxyPartMachine proxy) {
        proxies.add(proxy.getBlockPos());
        proxyMachines.add(proxy);
        syncWorkerCount();
    }

    public void removeProxy(MEPatternBufferProxyPartMachine proxy) {
        proxies.remove(proxy.getBlockPos());
        proxyMachines.remove(proxy);
        syncWorkerCount();
    }

    @UnmodifiableView
    public Set<MEPatternBufferProxyPartMachine> getProxies() {
        if (proxyMachines.size() != proxies.size()) {
            proxyMachines.clear();
            for (var pos : proxies) {
                if (MetaMachine.getMachine(getLevel(), pos) instanceof MEPatternBufferProxyPartMachine proxy) {
                    proxyMachines.add(proxy);
                }
            }
        }
        return Collections.unmodifiableSet(proxyMachines);
    }

    private void refundAll() {
        for (InternalSlot slot : internalInventory) {
            slot.refund();
        }
    }

    @VisibleForTesting
    public void onPatternChange(int index) {
        if (isRemote()) return;

        IPatternDetails oldPattern = patternSlotDetails[index];
        IPatternDetails newPatternDetails = PatternDetailsHelper.decodePattern(patternInventory.getStackInSlot(index),
                getLevel());
        patternSlotDetails[index] = newPatternDetails;
        if (oldPattern != null && !oldPattern.equals(newPatternDetails)) {
            for (Worker worker : workers) {
                if (oldPattern.equals(worker.pattern)) {
                    worker.slot.refund();
                    worker.pattern = null;
                    break;
                }
            }
        }

        needPatternSync = true;
    }

    //////////////////////////////////////
    // ********** GUI ***********//
    //////////////////////////////////////

    @Override
    public MachineUIPanelBuilder getPanelBuilder(PosGuiData data, PanelSyncManager syncManager, UISettings settings) {
        IPanelHandler renamingPanelHandler = syncManager.syncedPanel("renaming", true,
                ((syncManager1, syncHandler) -> PopupPanel.createPopupPanel("renaming_panel", 110, 40)
                        .child(Flow.col()
                                .coverChildren()
                                .child(Text.lang("gtceu.gui.pattern_buffer.set_custom_name").asWidget())
                                .child(new TextFieldWidget()
                                        .size(90, 20)
                                        .value(SyncHandlers.string(() -> this.customName, this::setCustomName)
                                                .allowC2S()))
                                .margin(5))));

        IPanelHandler sharedItemsPanelHandler = syncManager.syncedPanel("shared_items", true,
                (syncManager1, panelHandler) -> {
                    SlotGroup sharedItemSlotGroup = new SlotGroup("shared_item_slots", 3, false);

                    return PopupPanel.createPopupPanel("shared_items_panel", 80, 86)
                            .child(Text.lang("gui.gtceu.share_inventory.title").asWidget().margin(4))
                            .child(new Grid()
                                    .name("shared_item_grid")
                                    .top(26)
                                    .height(18 * 3)
                                    .minElementMargin(0, 0)
                                    .minColWidth(18).minRowHeight(18)
                                    .leftRel(0.5f)
                                    .gridOfSizeWidth(9, 3, (x, y, index) -> new ItemSlot()
                                            .slot(SyncHandlers.itemSlot(shareInventory.storage, index)
                                                    .slotGroup(sharedItemSlotGroup)
                                                    .accessibility(true, true))));
                });

        IPanelHandler sharedFluidsPanelHandler = syncManager.syncedPanel("shared_fluids", true,
                (syncManager1, panelHandler) -> PopupPanel.createPopupPanel("shared_fluids_panel", 85, 86)
                        .child(Text.lang("gui.gtceu.share_tank.title").asWidget().margin(4))
                        .child(GTMuiMachineUtil.createSlotGroupFromInventory(syncManager1, shareTank,
                                "shared_fluid_slots", 9, 'F',
                                GTMuiMachineUtil.createSquareMatrix(9, 'F'))
                                .top(26)
                                .leftRel(0.5f)));

        BooleanSyncValue canRefundValue = SyncHandlers.bool(this::canRefund, b -> {});
        syncManager.syncValue("can_refund", canRefundValue);

        syncManager.registerServerSyncedAction("refundButtonPressed", packet -> refundAll());

        return MachineUIPanelBuilder.panelBuilder(this).leftConfigurators(f -> {
            f.child(new ButtonWidget<>() // Shared items subpanel
                    .size(18)
                    .onMousePressed((context, b) -> {
                        if (b == InputConstants.MOUSE_BUTTON_LEFT) {
                            sharedItemsPanelHandler.openPanel();
                            return true;
                        }
                        return false;
                    })
                    .overlay(GTGuiTextures.BUTTON_ITEM_OUTPUT)
                    .tooltip(new RichTooltip()
                            .addLine(Text.lang("gui.gtceu.share_inventory.desc.0"))
                            .addLine(Text.lang("gui.gtceu.share_inventory.desc.1"))))
                    .child(new ButtonWidget<>() // Shared fluids subpanel
                            .size(18)
                            .onMousePressed((context, b) -> {
                                if (b == InputConstants.MOUSE_BUTTON_LEFT) {
                                    sharedFluidsPanelHandler.openPanel();
                                    return true;
                                }
                                return false;
                            })
                            .overlay(GTGuiTextures.BUTTON_FLUID_OUTPUT)
                            .tooltip(new RichTooltip()
                                    .addLine(Text.lang("gui.gtceu.share_tank.desc.0"))
                                    .addLine(Text.lang("gui.gtceu.share_inventory.desc.1"))))
                    .child(new ButtonWidget<>() // Refund button
                            .size(18)
                            .onMousePressed((context, b) -> {
                                if (canRefundValue.getBoolValue() && b == InputConstants.MOUSE_BUTTON_LEFT) {
                                    syncManager.callSyncedAction("refundButtonPressed");
                                    return true;
                                }
                                return false;
                            })
                            .overlay(new DynamicDrawable(() -> {
                                if (canRefundValue.getBoolValue()) {
                                    return GTGuiTextures.REFUND_OVERLAY
                                            .asIcon().size(16);
                                } else {
                                    return new DrawableStack(GTGuiTextures.REFUND_OVERLAY,
                                            new ItemDrawable(Items.BARRIER))
                                            .asIcon().size(16);
                                }
                            }))
                            .tooltip(new RichTooltip()
                                    .addLine(Text.lang("gui.gtceu.refund_all.desc"))))
                    .child(new ButtonWidget<>() // Renaming button
                            .size(18)
                            .onMousePressed((context, b) -> {
                                if (b == InputConstants.MOUSE_BUTTON_LEFT) {
                                    renamingPanelHandler.openPanel();
                                    return true;
                                }
                                return false;
                            })
                            .overlay(Text.str("✎")
                                    .asIcon()
                                    .size(16))
                            .tooltip(new RichTooltip()
                                    .addLine(Text.lang("gui.gtceu.rename.desc"))));
        });
    }

    @Override
    public void buildMainUI(ParentWidget<?> mainWidget, PosGuiData guiData, PanelSyncManager syncManager,
                            UISettings settings) {
        SlotGroup patternSlotGroup = new SlotGroup("pattern_slots", 9, 0, true);

        BooleanSyncValue isOnlineValue = new BooleanSyncValue(this::isOnline, this::setOnline);
        syncManager.syncValue("is_online", isOnlineValue);

        var flow = Flow.col().coverChildren();

        flow.child(Text.dynamic(() -> isOnlineValue.getBoolValue() ?
                Component.translatable("gtceu.gui.me_network.online") :
                Component.translatable("gtceu.gui.me_network.offline"))
                .asWidget().marginTop(2).marginBottom(4));

        flow.child(new Grid()
                .height(18 * (MAX_PATTERN_COUNT / 9))
                .minElementMargin(0, 0)
                .minColWidth(18).minRowHeight(18)
                .leftRel(0.5f)
                .gridOfSizeWidth(MAX_PATTERN_COUNT, 9, (x, y, index) -> new ItemSlot()
                        .slot(SyncHandlers.itemSlot(patternInventory, index)
                                .slotGroup(patternSlotGroup)
                                .accessibility(true, true)
                                .filter(stack -> stack.getItem() instanceof EncodedPatternItem)
                                .changeListener((i, o, c, init) -> onPatternChange(index)))
                        .background(GTGuiTextures.SLOT, GTGuiTextures.PATTERN_OVERLAY)));

        mainWidget.child(flow.center());
    }

    public boolean canRefund() {
        return Arrays.stream(internalInventory).anyMatch(slot -> !slot.isEmpty());
    }

    @Override
    public List<IPatternDetails> getAvailablePatterns() {
        ArrayList<IPatternDetails> result = new ArrayList<>(MAX_PATTERN_COUNT);
        for (IPatternDetails p : patternSlotDetails) {
            if (p != null) result.add(p);
        }
        return result;
    }

    @Override
    public boolean pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolder) {
        if (!isFormed() || !getMainNode().isActive() || !checkInput(inputHolder)) {
            return false;
        }
        boolean knownPattern = false;
        for (IPatternDetails p : patternSlotDetails) {
            if (patternDetails.equals(p)) {
                knownPattern = true;
                break;
            }
        }
        if (!knownPattern) return false;

        for (Worker worker : workers) {
            if (worker.slot.isEmpty()) worker.pattern = null;
        }
        Worker free = null;
        for (Worker worker : workers) {
            if (patternDetails.equals(worker.pattern)) {
                worker.slot.pushPattern(patternDetails, inputHolder);
                return true;
            }
            if (free == null && worker.slot.isEmpty()) free = worker;
        }
        if (free != null) {
            free.pattern = patternDetails;
            free.slot.pushPattern(patternDetails, inputHolder);
            return true;
        }
        return false;
    }

    @Override
    public boolean isBusy() {
        return false;
    }

    private boolean checkInput(KeyCounter[] inputHolder) {
        for (KeyCounter input : inputHolder) {
            var illegal = input.keySet().stream()
                    .map(AEKey::getType)
                    .map(AEKeyType::getId)
                    .anyMatch(id -> !id.equals(AEKeyType.items().getId()) && !id.equals(AEKeyType.fluids().getId()));
            if (illegal) return false;
        }
        return true;
    }

    @Override
    public @Nullable IGrid getGrid() {
        return getMainNode().getGrid();
    }

    @Override
    public InternalInventory getTerminalPatternInventory() {
        return internalPatternInventory;
    }

    @Override
    public PatternContainerGroup getTerminalGroup() {
        // Has controller
        if (isFormed()) {
            MultiblockControllerMachine controller = getControllers().first();
            MultiblockMachineDefinition controllerDefinition = controller.getDefinition();
            // has customName
            if (!customName.isEmpty()) {
                return new PatternContainerGroup(
                        AEItemKey.of(controllerDefinition.asStack()),
                        Component.literal(customName),
                        Collections.emptyList());
            } else {
                ItemStack circuitStack = circuitSlot.isEnabled() ? circuitSlot.storage.getStackInSlot(0) :
                        ItemStack.EMPTY;
                int circuitConfiguration = circuitStack.isEmpty() ? -1 :
                        IntCircuitBehaviour.getCircuitConfiguration(circuitStack);

                Component groupName = circuitConfiguration != -1 ?
                        Component.translatable(controllerDefinition.getDescriptionId())
                                .append(" - " + circuitConfiguration) :
                        Component.translatable(controllerDefinition.getDescriptionId());

                return new PatternContainerGroup(
                        AEItemKey.of(controllerDefinition.asStack()), groupName, Collections.emptyList());
            }
        } else {
            if (!customName.isEmpty()) {
                return new PatternContainerGroup(
                        AEItemKey.of(GTAEMachines.ME_PATTERN_BUFFER.getItem()),
                        Component.literal(customName),
                        Collections.emptyList());
            } else {
                return new PatternContainerGroup(
                        AEItemKey.of(GTAEMachines.ME_PATTERN_BUFFER.getItem()),
                        GTAEMachines.ME_PATTERN_BUFFER.get().getDefinition().getItem().getDescription(),
                        Collections.emptyList());
            }
        }
    }

    @Override
    public void onMachineDestroyed() {
        patternInventory.dropInventoryInWorld(getLevel(), getBlockPos());
        shareInventory.dropInventoryInWorld();
    }

    @Override
    public InteractionResult onDataStickShiftUse(Player player, ItemStack dataStick) {
        dataStick.getOrCreateTag().putIntArray("pos",
                new int[] { getBlockPos().getX(), getBlockPos().getY(), getBlockPos().getZ() });
        return InteractionResult.SUCCESS;
    }

    public record BufferData(Object2LongMap<ItemStack> items, Object2LongMap<FluidStack> fluids) {}

    public BufferData mergeInternalSlots() {
        var items = new Object2LongOpenCustomHashMap<>(ItemStackHashStrategy.comparingAllButCount());
        var fluids = new Object2LongOpenHashMap<FluidStack>();
        for (InternalSlot slot : internalInventory) {
            slot.itemInventory.object2LongEntrySet().fastForEach(e -> items.addTo(e.getKey(), e.getLongValue()));
            slot.fluidInventory.object2LongEntrySet().fastForEach(e -> fluids.addTo(e.getKey(), e.getLongValue()));
        }
        return new BufferData(items, fluids);
    }

    public class InternalSlot implements INBTSerializable<CompoundTag> {

        @Getter
        @Setter
        private Runnable onContentsChanged = () -> {};

        private final Object2LongOpenCustomHashMap<ItemStack> itemInventory = new Object2LongOpenCustomHashMap<>(
                ItemStackHashStrategy.comparingAllButCount());
        private final Object2LongOpenHashMap<FluidStack> fluidInventory = new Object2LongOpenHashMap<>();
        private @Nullable List<ItemStack> itemStacks = null;
        private @Nullable List<FluidStack> fluidStacks = null;
        private @Nullable Set<Item> cachedItemTypes = null;
        private @Nullable Set<Fluid> cachedFluidTypes = null;

        public InternalSlot() {}

        public boolean isItemEmpty() {
            return itemInventory.isEmpty();
        }

        public boolean isFluidEmpty() {
            return fluidInventory.isEmpty();
        }

        public boolean isEmpty() {
            return isItemEmpty() && isFluidEmpty();
        }

        public Set<Item> getItemTypes() {
            if (cachedItemTypes == null) {
                cachedItemTypes = new ReferenceOpenHashSet<>(itemInventory.size());
                itemInventory.keySet().forEach(s -> cachedItemTypes.add(s.getItem()));
            }
            return cachedItemTypes;
        }

        public Set<Fluid> getFluidTypes() {
            if (cachedFluidTypes == null) {
                cachedFluidTypes = new ReferenceOpenHashSet<>(fluidInventory.size());
                fluidInventory.keySet().forEach(s -> cachedFluidTypes.add(s.getFluid()));
            }
            return cachedFluidTypes;
        }

        public void onContentsChanged() {
            itemStacks = null;
            fluidStacks = null;
            cachedItemTypes = null;
            cachedFluidTypes = null;
            onContentsChanged.run();
        }

        private void add(AEKey what, long amount) {
            if (amount <= 0L) return;
            if (what instanceof AEItemKey itemKey) {
                var stack = itemKey.toStack();
                itemInventory.addTo(stack, amount);
            } else if (what instanceof AEFluidKey fluidKey) {
                var stack = fluidKey.toStack(1);
                fluidInventory.addTo(stack, amount);
            }
        }

        public List<ItemStack> getItems() {
            if (itemStacks == null) {
                itemStacks = new ArrayList<>();
                itemInventory.object2LongEntrySet().stream()
                        .map(e -> GTMath.splitStacks(e.getKey(), e.getLongValue()))
                        .forEach(itemStacks::addAll);
            }
            return itemStacks;
        }

        public List<FluidStack> getFluids() {
            if (fluidStacks == null) {
                fluidStacks = new ArrayList<>();
                fluidInventory.object2LongEntrySet().stream()
                        .map(e -> GTMath.splitFluidStacks(e.getKey(), e.getLongValue()))
                        .forEach(fluidStacks::addAll);
            }
            return fluidStacks;
        }

        public void refund() {
            var network = getMainNode().getGrid();
            if (network != null) {
                MEStorage networkInv = network.getStorageService().getInventory();
                var energy = network.getEnergyService();

                for (var it = itemInventory.object2LongEntrySet().iterator(); it.hasNext();) {
                    var entry = it.next();
                    var stack = entry.getKey();
                    var count = entry.getLongValue();
                    if (stack.isEmpty() || count == 0) {
                        it.remove();
                        continue;
                    }

                    var key = AEItemKey.of(stack);
                    if (key == null) continue;

                    long inserted = StorageHelper.poweredInsert(energy, networkInv, key, count, actionSource);
                    if (inserted > 0) {
                        count -= inserted;
                        if (count == 0) it.remove();
                        else entry.setValue(count);
                    }
                }

                for (var it = fluidInventory.object2LongEntrySet().iterator(); it.hasNext();) {
                    var entry = it.next();
                    var stack = entry.getKey();
                    var amount = entry.getLongValue();
                    if (stack.isEmpty() || amount == 0) {
                        it.remove();
                        continue;
                    }

                    var key = AEFluidKey.of(stack);
                    if (key == null) continue;

                    long inserted = StorageHelper.poweredInsert(energy, networkInv, key, amount, actionSource);
                    if (inserted > 0) {
                        amount -= inserted;
                        if (amount == 0) it.remove();
                        else entry.setValue(amount);
                    }
                }
                onContentsChanged();
            }
        }

        public void pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolder) {
            patternDetails.pushInputsToExternalInventory(inputHolder, this::add);
            onContentsChanged();
        }

        public List<Ingredient> handleItemInternal(List<Ingredient> left, boolean simulate) {
            boolean changed = false;
            for (var it = left.listIterator(); it.hasNext();) {
                var ingredient = it.next();
                if (ingredient.isEmpty()) {
                    it.remove();
                    continue;
                }

                var items = ingredient.getItems();
                if (items.length == 0 || items[0].isEmpty()) {
                    it.remove();
                    continue;
                }

                int amount = items[0].getCount();
                for (var it2 = itemInventory.object2LongEntrySet().iterator(); it2.hasNext();) {
                    var entry = it2.next();
                    var stack = entry.getKey();
                    var count = entry.getLongValue();
                    if (stack.isEmpty() || count == 0) {
                        it2.remove();
                        continue;
                    }
                    if (!ingredient.test(stack)) continue;
                    int extracted = Math.min(GTMath.saturatedCast(count), amount);
                    if (!simulate && extracted > 0) {
                        changed = true;
                        count -= extracted;
                        if (count == 0) it2.remove();
                        else entry.setValue(count);
                    }
                    amount -= extracted;

                    if (amount <= 0) {
                        it.remove();
                        break;
                    }
                }

                if (amount > 0) {
                    if (ingredient instanceof SizedIngredient si) {
                        si.setAmount(amount);
                    } else {
                        items[0].setCount(amount);
                    }
                }
            }
            if (changed) onContentsChanged();
            return left;
        }

        public List<FluidIngredient> handleFluidInternal(List<FluidIngredient> left, boolean simulate) {
            boolean changed = false;
            for (var it = left.listIterator(); it.hasNext();) {
                var ingredient = it.next();
                if (ingredient.isEmpty()) {
                    it.remove();
                    continue;
                }

                var fluids = ingredient.getStacks();
                if (fluids.length == 0 || fluids[0].isEmpty()) {
                    it.remove();
                    continue;
                }

                int amount = fluids[0].getAmount();
                for (var it2 = fluidInventory.object2LongEntrySet().iterator(); it2.hasNext();) {
                    var entry = it2.next();
                    var stack = entry.getKey();
                    var count = entry.getLongValue();
                    if (stack.isEmpty() || count == 0) {
                        it2.remove();
                        continue;
                    }
                    if (!ingredient.test(stack)) continue;
                    int extracted = Math.min(GTMath.saturatedCast(count), amount);
                    if (!simulate && extracted > 0) {
                        changed = true;
                        count -= extracted;
                        if (count == 0) it2.remove();
                        else entry.setValue(count);
                    }
                    amount -= extracted;

                    if (amount <= 0) {
                        it.remove();
                        break;
                    }
                }

                if (amount > 0) {
                    ingredient.setAmount(amount);
                }
            }

            if (changed) onContentsChanged();
            return left;
        }

        @Override
        public CompoundTag serializeNBT() {
            CompoundTag tag = new CompoundTag();

            ListTag itemsTag = new ListTag();
            for (var entry : itemInventory.object2LongEntrySet()) {
                var ct = entry.getKey().serializeNBT();
                ct.putLong("real", entry.getLongValue());
                itemsTag.add(ct);
            }
            if (!itemsTag.isEmpty()) tag.put("inventory", itemsTag);

            ListTag fluidsTag = new ListTag();
            for (var entry : fluidInventory.object2LongEntrySet()) {
                var ct = entry.getKey().writeToNBT(new CompoundTag());
                ct.putLong("real", entry.getLongValue());
                fluidsTag.add(ct);
            }
            if (!fluidsTag.isEmpty()) tag.put("fluidInventory", fluidsTag);

            return tag;
        }

        @Override
        public void deserializeNBT(CompoundTag tag) {
            ListTag items = tag.getList("inventory", Tag.TAG_COMPOUND);
            for (Tag t : items) {
                if (!(t instanceof CompoundTag ct)) continue;
                var stack = ItemStack.of(ct);
                var count = ct.getLong("real");
                if (!stack.isEmpty() && count > 0) {
                    itemInventory.put(stack, count);
                }
            }

            ListTag fluids = tag.getList("fluidInventory", Tag.TAG_COMPOUND);
            for (Tag t : fluids) {
                if (!(t instanceof CompoundTag ct)) continue;
                var stack = FluidStack.loadFluidStackFromNBT(ct);
                var amount = ct.getLong("real");
                if (!stack.isEmpty() && amount > 0) {
                    fluidInventory.put(stack, amount);
                }
            }
        }
    }
}
