// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0

package dan200.computercraft.shared.platform;

import com.google.auto.service.AutoService;
import com.mojang.authlib.GameProfile;
import com.mojang.brigadier.arguments.ArgumentType;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.api.media.MediaCapability;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.network.wired.WiredElementCapability;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.PeripheralCapability;
import dan200.computercraft.impl.Peripherals;
import dan200.computercraft.shared.command.UserLevel;
import dan200.computercraft.shared.config.ConfigFile;
import dan200.computercraft.shared.network.container.ContainerData;
import dan200.computercraft.shared.util.InventoryUtil;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.commands.synchronization.ArgumentTypeInfos;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey;
import net.minecraft.world.Container;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuConstructor;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.*;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.ColorCollection;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.neoforged.fml.ModList;
import net.neoforged.neoforge.capabilities.BlockCapability;
import net.neoforged.neoforge.capabilities.BlockCapabilityCache;
import net.neoforged.neoforge.capabilities.Capabilities;
import net.neoforged.neoforge.common.CommonHooks;
import net.neoforged.neoforge.common.ItemAbilities;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.common.Tags;
import net.neoforged.neoforge.common.extensions.IMenuTypeExtension;
import net.neoforged.neoforge.event.EventHooks;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;
import net.neoforged.neoforge.server.permission.PermissionAPI;
import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent;
import net.neoforged.neoforge.server.permission.nodes.PermissionNode;
import net.neoforged.neoforge.server.permission.nodes.PermissionType;
import net.neoforged.neoforge.server.permission.nodes.PermissionTypes;
import net.neoforged.neoforge.transfer.item.VanillaContainerWrapper;
import org.jspecify.annotations.Nullable;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;

@AutoService(PlatformHelper.class)
public class PlatformHelperImpl implements PlatformHelper {
    @Override
    public boolean isModLoaded(String id) {
        return ModList.get().isLoaded(id);
    }

    @Override
    public ConfigFile.Builder createConfigBuilder() {
        return new ForgeConfigFile.Builder();
    }

    @Override
    public <T> RegistrationHelper<T> createRegistrationHelper(ResourceKey<Registry<T>> registry) {
        return new RegistrationHelperImpl<>(DeferredRegister.create(registry, ComputerCraftAPI.MOD_ID));
    }

    @Override
    public PermissionRegistry createPermissionRegistry() {
        return new PermissionRegistryImpl();
    }

    @Override
    public <A extends ArgumentType<?>, T extends ArgumentTypeInfo.Template<A>, I extends ArgumentTypeInfo<A, T>> I registerArgumentTypeInfo(Class<A> klass, I info) {
        return ArgumentTypeInfos.registerByClass(klass, info);
    }

    @Override
    public <C extends AbstractContainerMenu, T extends ContainerData> MenuType<C> createMenuType(StreamCodec<RegistryFriendlyByteBuf, T> codec, ContainerData.Factory<C, T> factory) {
        return IMenuTypeExtension.create((id, player, data) -> factory.create(id, player, codec.decode(data)));
    }

    @Override
    public void openMenu(Player player, Component title, MenuConstructor menu, ContainerData data) {
        player.openMenu(new SimpleMenuProvider(menu, title), data::toBytes);
    }

    @Override
    public void invalidateComponent(BlockEntity owner) {
        owner.invalidateCapabilities();
    }

    @Override
    public ComponentAccess<IPeripheral> createPeripheralAccess(BlockEntity owner, Consumer<Direction> invalidate) {
        return new PeripheralAccess(owner, invalidate);
    }

    @Override
    public ComponentAccess<WiredElement> createWiredElementAccess(BlockEntity owner, Consumer<Direction> invalidate) {
        return new ComponentAccessImpl<>(owner, WiredElementCapability.get(), invalidate);
    }

    @Override
    public boolean hasWiredElementIn(Level level, BlockPos pos, Direction direction) {
        if (!level.isLoaded(pos)) return false;
        return level.getCapability(WiredElementCapability.get(), pos.relative(direction), direction.getOpposite()) != null;
    }

    @Override
    public ContainerTransfer.Slotted wrapContainer(Container container) {
        return new ForgeContainerTransfer(VanillaContainerWrapper.of(container));
    }

    @Nullable
    @Override
    public ContainerTransfer getContainer(ServerLevel level, BlockPos pos, Direction side) {
        var inventory = level.getCapability(Capabilities.Item.BLOCK, pos, side);
        if (inventory != null) return new ForgeContainerTransfer(inventory);

        var entity = InventoryUtil.getEntityContainer(level, pos, side);
        return entity == null ? null : new ForgeContainerTransfer(VanillaContainerWrapper.of(entity));
    }

    @Override
    public RecipeIngredients getRecipeIngredients() {
        return new RecipeIngredients(
            Tags.Items.DUSTS_REDSTONE,
            Tags.Items.STRINGS,
            Tags.Items.LEATHERS,
            Tags.Items.GLASS_PANES,
            Tags.Items.INGOTS_GOLD,
            Tags.Items.STORAGE_BLOCKS_GOLD,
            Tags.Items.INGOTS_IRON,
            Tags.Items.DYES,
            Tags.Items.ENDER_PEARLS,
            Tags.Items.CHESTS_WOODEN
        );
    }

    @Override
    public ColorCollection<TagKey<Item>> getDyeTags() {
        return ColorCollection.VALUES.map(DyeColor::getTag);
    }

    @Override
    public int getBurnTime(MinecraftServer server, ItemStack stack) {
        return stack.getBurnTime(null, server.fuelValues());
    }

    @Override
    public CreativeModeTab.Builder newCreativeModeTab() {
        return CreativeModeTab.builder();
    }

    @Override
    public @Nullable ItemStackTemplate getCraftingRemainingItem(ItemStack stack) {
        return stack.getCraftingRemainder();
    }

    @Override
    public boolean onNotifyNeighbour(Level level, BlockPos pos, BlockState block, Direction direction) {
        return !EventHooks.onNeighborNotify(level, pos, block, EnumSet.of(direction), false).isCanceled();
    }

    @Override
    public ServerPlayer createFakePlayer(ServerLevel world, GameProfile profile) {
        return new FakePlayerExt(world, profile);
    }

    @Override
    public boolean hasToolUsage(ItemStack stack) {
        return stack.canPerformAction(ItemAbilities.SHOVEL_FLATTEN) || stack.canPerformAction(ItemAbilities.HOE_TILL);
    }

    @Override
    public InteractionResult canAttackEntity(ServerPlayer player, Entity entity) {
        return CommonHooks.onPlayerAttackTarget(player, entity) ? InteractionResult.PASS : InteractionResult.SUCCESS;
    }

    @Override
    public boolean interactWithEntity(ServerPlayer player, EntityHitResult hit) {
        var entity = hit.getEntity();
        var hitPos = hit.getLocation().subtract(entity.position());
        return player.interactOn(entity, InteractionHand.MAIN_HAND, hitPos).consumesAction();
    }

    @Override
    public UseOnResult useOn(ServerPlayer player, ItemStack stack, BlockHitResult hit) {
        var pos = hit.getBlockPos();
        var event = CommonHooks.onRightClickBlock(player, InteractionHand.MAIN_HAND, pos, hit);
        if (event.isCanceled()) return new UseOnResult.Handled(event.getCancellationResult());

        var context = new UseOnContext(player, InteractionHand.MAIN_HAND, hit);
        if (!event.getUseItem().isFalse()) {
            var result = stack.onItemUseFirst(context);
            if (result != InteractionResult.PASS) return new UseOnResult.Handled(event.getCancellationResult());
        }

        return new UseOnResult.Continue(!event.getUseBlock().isFalse(), !event.getUseItem().isFalse());
    }

    @Override
    public @Nullable IMedia getMedia(ItemStack stack) {
        return stack.getCapability(MediaCapability.get());
    }

    @Override
    public ClickEvent createOpenFolderAction(Path path) {
        return new ClickEvent.OpenFile(path.toAbsolutePath().toString());
    }

    private record RegistrationHelperImpl<R>(DeferredRegister<R> registry) implements RegistrationHelper<R> {
        @Override
        public <T extends R> RegistryEntry<T> register(String name, Supplier<T> create) {
            return new RegistryEntryImpl<>(registry().register(name, create));
        }

        @Override
        public <U extends R> RegistryEntry<U> register(ResourceKey<R> id, Supplier<U> create) {
            if (!id.identifier().getNamespace().equals(ComputerCraftAPI.MOD_ID)) {
                throw new IllegalArgumentException("Can only register items for ComputerCraft");
            }

            return register(id.identifier().getPath(), create);
        }

        @Override
        public void register() {
            registry().register(ComputerCraft.getEventBus());
        }
    }

    private record RegistryEntryImpl<R, T extends R>(DeferredHolder<R, T> object) implements RegistryEntry<T> {
        @Override
        public Identifier id() {
            return object().getId();
        }

        @Override
        public T get() {
            return object().get();
        }
    }

    private static class ComponentAccessImpl<T> implements ComponentAccess<T> {
        private final BlockEntity owner;
        private final BlockCapability<T, Direction> capability;
        private final Consumer<Direction> invalidate;
        @SuppressWarnings({ "unchecked", "rawtypes" })
        final BlockCapabilityCache<T, Direction>[] caches = new BlockCapabilityCache[6];

        ComponentAccessImpl(BlockEntity owner, BlockCapability<T, Direction> capability, Consumer<Direction> invalidate) {
            this.owner = owner;
            this.capability = capability;
            this.invalidate = invalidate;
        }

        @Nullable
        @Override
        public T get(Direction direction) {
            var level = getLevel();
            var cache = caches[direction.ordinal()];
            if (cache == null) {
                cache = caches[direction.ordinal()] = BlockCapabilityCache.create(
                    capability, level, owner.getBlockPos().relative(direction),
                    direction.getOpposite(), () -> !owner.isRemoved(), () -> this.invalidate.accept(direction)
                );
            }

            return cache.getCapability();
        }

        final ServerLevel getLevel() {
            return Objects.requireNonNull((ServerLevel) owner.getLevel(), "Block entity is not in a level");
        }
    }

    private static class PeripheralAccess extends ComponentAccessImpl<IPeripheral> {
        PeripheralAccess(BlockEntity owner, Consumer<Direction> invalidate) {
            super(owner, PeripheralCapability.get(), invalidate);
        }

        @Nullable
        @Override
        public IPeripheral get(Direction direction) {
            var result = super.get(direction);
            if (result != null) return result;

            var cache = caches[direction.ordinal()];
            return Peripherals.getGenericPeripheral(cache.level(), cache.pos(), cache.context(), cache.level().getBlockEntity(cache.pos()));
        }
    }

    /**
     * An implementation of {@link PermissionRegistry} using Forge's {@link PermissionAPI}.
     */
    private static final class PermissionRegistryImpl extends PermissionRegistry {
        private final List<PermissionNode<?>> nodes = new ArrayList<>();

        private <T> PermissionNode<T> registerNode(String nodeName, PermissionType<T> type, PermissionNode.PermissionResolver<T> defaultResolver) {
            checkNotFrozen();
            var node = new PermissionNode<>(ComputerCraftAPI.MOD_ID, nodeName, type, defaultResolver);
            nodes.add(node);
            return node;
        }

        @Override
        public Predicate<CommandSourceStack> registerCommand(String command, UserLevel fallback) {
            var node = registerNode(
                "command." + command, PermissionTypes.BOOLEAN,
                (player, uuid, context) -> player != null && fallback.test(player)
            );

            return source -> {
                var player = source.getPlayer();
                return player == null ? fallback.test(source) : PermissionAPI.getPermission(player, node);
            };
        }

        @Override
        public void register() {
            super.register();
            NeoForge.EVENT_BUS.addListener((PermissionGatherEvent.Nodes event) -> event.addNodes(nodes));
        }
    }
}
