package de.ellpeck.prettypipes.terminal;

import de.ellpeck.prettypipes.PrettyPipes;
import de.ellpeck.prettypipes.Registry;
import de.ellpeck.prettypipes.Utility;
import de.ellpeck.prettypipes.misc.EquatableItemStack;
import de.ellpeck.prettypipes.misc.ItemEqualityType;
import de.ellpeck.prettypipes.misc.ItemOrder;
import de.ellpeck.prettypipes.network.*;
import de.ellpeck.prettypipes.packets.PacketHandler;
import de.ellpeck.prettypipes.packets.PacketNetworkItems;
import de.ellpeck.prettypipes.pipe.ConnectionType;
import de.ellpeck.prettypipes.pipe.IPipeConnectable;
import de.ellpeck.prettypipes.pipe.PipeTileEntity;
import de.ellpeck.prettypipes.terminal.containers.ItemTerminalContainer;
import net.minecraft.block.BlockState;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.inventory.container.Container;
import net.minecraft.inventory.container.INamedContainerProvider;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.CompoundNBT;
import net.minecraft.tileentity.ITickableTileEntity;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.tileentity.TileEntityType;
import net.minecraft.util.Direction;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.Style;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.util.text.TranslationTextComponent;
import net.minecraft.world.World;
import net.minecraft.world.server.ServerWorld;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.util.Constants;
import net.minecraftforge.common.util.Constants.NBT;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.items.IItemHandler;
import net.minecraftforge.items.ItemHandlerHelper;
import net.minecraftforge.items.ItemStackHandler;
import org.apache.commons.lang3.mutable.MutableInt;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

public class ItemTerminalTileEntity extends TileEntity implements INamedContainerProvider, ITickableTileEntity, IPipeConnectable {

    public final ItemStackHandler items = new ItemStackHandler(12) {
        @Override
        public boolean isItemValid(int slot, @Nonnull ItemStack stack) {
            return true;
        }
    };
    protected Map<EquatableItemStack, NetworkItem> networkItems;
    private final Queue<NetworkLock> existingRequests = new LinkedList<>();

    protected ItemTerminalTileEntity(TileEntityType<?> tileEntityTypeIn) {
        super(tileEntityTypeIn);
    }

    public ItemTerminalTileEntity() {
        this(Registry.itemTerminalTileEntity);
    }

    @Override
    public void tick() {
        if (this.world.isRemote)
            return;
        PipeNetwork network = PipeNetwork.get(this.world);
        PipeTileEntity pipe = this.getConnectedPipe();
        if (pipe == null)
            return;

        boolean update = false;
        int interval = pipe.pressurizer != null ? 2 : 10;
        if (this.world.getGameTime() % interval == 0) {
            for (int i = 6; i < 12; i++) {
                ItemStack extracted = this.items.extractItem(i, Integer.MAX_VALUE, true);
                if (extracted.isEmpty())
                    continue;
                ItemStack remain = network.routeItem(pipe.getPos(), this.pos, extracted, true);
                if (remain.getCount() == extracted.getCount())
                    continue;
                this.items.extractItem(i, extracted.getCount() - remain.getCount(), false);
                break;
            }

            if (!this.existingRequests.isEmpty()) {
                NetworkLock request = this.existingRequests.remove();
                network.resolveNetworkLock(request);
                network.requestExistingItem(request.location, pipe.getPos(), this.pos, request, request.stack, ItemEqualityType.NBT);
                update = true;
            }
        }

        if (this.world.getGameTime() % 100 == 0 || update) {
            PlayerEntity[] lookingPlayers = this.getLookingPlayers();
            if (lookingPlayers.length > 0)
                this.updateItems(lookingPlayers);
        }
    }

    @Override
    public void remove() {
        super.remove();
        PipeNetwork network = PipeNetwork.get(this.world);
        for (NetworkLock lock : this.existingRequests)
            network.resolveNetworkLock(lock);
    }

    public PipeTileEntity getConnectedPipe() {
        PipeNetwork network = PipeNetwork.get(this.world);
        for (Direction dir : Direction.values()) {
            PipeTileEntity pipe = network.getPipe(this.pos.offset(dir));
            if (pipe != null)
                return pipe;
        }
        return null;
    }

    public void updateItems(PlayerEntity... playersToSync) {
        PipeTileEntity pipe = this.getConnectedPipe();
        if (pipe == null)
            return;
        this.networkItems = this.collectItems();
        if (playersToSync.length > 0) {
            List<ItemStack> clientItems = this.networkItems.values().stream().map(NetworkItem::asStack).collect(Collectors.toList());
            List<ItemStack> clientCraftables = PipeNetwork.get(this.world).getAllCraftables(pipe.getPos()).stream().map(Pair::getRight).collect(Collectors.toList());
            for (PlayerEntity player : playersToSync) {
                if (!(player.openContainer instanceof ItemTerminalContainer))
                    continue;
                ItemTerminalTileEntity tile = ((ItemTerminalContainer) player.openContainer).tile;
                if (tile != this)
                    continue;
                PacketHandler.sendTo(player, new PacketNetworkItems(clientItems, clientCraftables));
            }
        }
    }

    public void requestItem(PlayerEntity player, ItemStack stack) {
        PipeNetwork network = PipeNetwork.get(this.world);
        network.startProfile("terminal_request_item");
        this.updateItems();
        int requested = this.requestItemImpl(stack, onItemUnavailable(player));
        if (requested > 0) {
            player.sendMessage(new TranslationTextComponent("info." + PrettyPipes.ID + ".sending", requested, stack.getDisplayName()).setStyle(Style.EMPTY.setFormatting(TextFormatting.GREEN)), UUID.randomUUID());
        } else {
            onItemUnavailable(player).accept(stack);
        }
        network.endProfile();
    }

    public int requestItemImpl(ItemStack stack, Consumer<ItemStack> unavailableConsumer) {
        NetworkItem item = this.networkItems.get(new EquatableItemStack(stack));
        Collection<NetworkLocation> locations = item == null ? Collections.emptyList() : item.getLocations();
        Pair<List<NetworkLock>, ItemStack> ret = requestItemLater(this.world, this.getConnectedPipe().getPos(), locations, unavailableConsumer, stack, ItemEqualityType.NBT);
        this.existingRequests.addAll(ret.getLeft());
        return stack.getCount() - ret.getRight().getCount();
    }

    protected PlayerEntity[] getLookingPlayers() {
        return this.world.getPlayers().stream()
                .filter(p -> p.openContainer instanceof ItemTerminalContainer)
                .filter(p -> ((ItemTerminalContainer) p.openContainer).tile == this)
                .toArray(PlayerEntity[]::new);
    }

    private Map<EquatableItemStack, NetworkItem> collectItems() {
        PipeNetwork network = PipeNetwork.get(this.world);
        network.startProfile("terminal_collect_items");
        PipeTileEntity pipe = this.getConnectedPipe();
        Map<EquatableItemStack, NetworkItem> items = new HashMap<>();
        for (NetworkLocation location : network.getOrderedNetworkItems(pipe.getPos())) {
            for (ItemStack stack : location.getItems(this.world).values()) {
                EquatableItemStack equatable = new EquatableItemStack(stack);
                NetworkItem item = items.computeIfAbsent(equatable, NetworkItem::new);
                item.add(location, stack);
            }
        }
        network.endProfile();
        return items;
    }

    @Override
    public CompoundNBT write(CompoundNBT compound) {
        compound.put("items", this.items.serializeNBT());
        compound.put("requests", Utility.serializeAll(this.existingRequests));
        return super.write(compound);
    }

    @Override
    public void read(BlockState state, CompoundNBT compound) {
        this.items.deserializeNBT(compound.getCompound("items"));
        this.existingRequests.clear();
        this.existingRequests.addAll(Utility.deserializeAll(compound.getList("requests", NBT.TAG_COMPOUND), NetworkLock::new));
        super.read(state, compound);
    }

    @Override
    public ITextComponent getDisplayName() {
        return new TranslationTextComponent("container." + PrettyPipes.ID + ".item_terminal");
    }

    @Nullable
    @Override
    public Container createMenu(int window, PlayerInventory inv, PlayerEntity player) {
        return new ItemTerminalContainer(Registry.itemTerminalContainer, window, player, this.pos);
    }

    @Override
    public <T> LazyOptional<T> getCapability(Capability<T> cap, Direction side) {
        if (cap == Registry.pipeConnectableCapability)
            return LazyOptional.of(() -> (T) this);
        return LazyOptional.empty();
    }

    @Override
    public ConnectionType getConnectionType(BlockPos pipePos, Direction direction) {
        return ConnectionType.CONNECTED;
    }

    @Override
    public ItemStack insertItem(BlockPos pipePos, Direction direction, ItemStack stack, boolean simulate) {
        BlockPos pos = pipePos.offset(direction);
        ItemTerminalTileEntity tile = Utility.getTileEntity(ItemTerminalTileEntity.class, world, pos);
        if (tile != null)
            return ItemHandlerHelper.insertItemStacked(tile.items, stack, simulate);
        return stack;
    }

    @Override
    public boolean allowsModules(BlockPos pipePos, Direction direction) {
        return true;
    }

    public static Pair<List<NetworkLock>, ItemStack> requestItemLater(World world, BlockPos destPipe, Collection<NetworkLocation> locations, Consumer<ItemStack> unavailableConsumer, ItemStack stack, ItemEqualityType... equalityTypes) {
        List<NetworkLock> requests = new ArrayList<>();
        ItemStack remain = stack.copy();
        PipeNetwork network = PipeNetwork.get(world);
        // check for existing items
        for (NetworkLocation location : locations) {
            int amount = location.getItemAmount(world, stack, ItemEqualityType.NBT);
            if (amount <= 0)
                continue;
            amount -= network.getLockedAmount(location.getPos(), stack, null, ItemEqualityType.NBT);
            if (amount > 0) {
                if (remain.getCount() < amount)
                    amount = remain.getCount();
                remain.shrink(amount);
                while (amount > 0) {
                    ItemStack copy = stack.copy();
                    copy.setCount(Math.min(stack.getMaxStackSize(), amount));
                    NetworkLock lock = new NetworkLock(location, copy);
                    network.createNetworkLock(lock);
                    requests.add(lock);
                    amount -= copy.getCount();
                }
                if (remain.isEmpty())
                    break;
            }
        }
        // check for craftable items
        if (!remain.isEmpty())
            remain = network.requestCraftedItem(destPipe, unavailableConsumer, remain, equalityTypes);
        return Pair.of(requests, remain);
    }

    public static Consumer<ItemStack> onItemUnavailable(PlayerEntity player) {
        return s -> player.sendMessage(new TranslationTextComponent("info." + PrettyPipes.ID + ".not_found", s.getDisplayName()).setStyle(Style.EMPTY.setFormatting(TextFormatting.RED)), UUID.randomUUID());
    }
}
