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

package dan200.computercraft.shared.platform;

import com.mojang.authlib.GameProfile;
import com.mojang.brigadier.arguments.ArgumentType;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.impl.Services;
import dan200.computercraft.shared.config.ConfigFile;
import dan200.computercraft.shared.network.container.ContainerData;
import dan200.computercraft.shared.util.InventoryUtil;
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
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.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerPlayerGameMode;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.tags.TagKey;
import net.minecraft.world.Container;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.WorldlyContainer;
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.minecraft.world.phys.Vec3;
import org.jspecify.annotations.Nullable;

import java.nio.file.Path;
import java.util.function.Consumer;

/**
 * Abstraction layer for Forge and Fabric. See implementations for more details.
 */
public interface PlatformHelper {
    /**
     * Get the current {@link PlatformHelper} instance.
     *
     * @return The current instance.
     */
    static PlatformHelper get() {
        var instance = Instance.INSTANCE;
        return instance == null ? Services.raise(PlatformHelper.class, Instance.ERROR) : instance;
    }

    /**
     * Determine if the specified mod is loaded.
     *
     * @param id The id of the mod to check.
     * @return Whether this mod is loaded.
     */
    boolean isModLoaded(String id);

    /**
     * Create a new config builder.
     *
     * @return The newly created config builder.
     */
    ConfigFile.Builder createConfigBuilder();

    /**
     * Create a registration helper for a specific registry.
     *
     * @param registry The registry we'll add entries to.
     * @param <T>      The type of object stored in the registry.
     * @return The registration helper.
     */
    <T> RegistrationHelper<T> createRegistrationHelper(ResourceKey<Registry<T>> registry);

    /**
     * Create a new {@linkplain PermissionRegistry registry for permissions}.
     *
     * @return The newly created permission registry.
     */
    PermissionRegistry createPermissionRegistry();

    /**
     * Register a new argument type.
     *
     * @param klass The argument type we're registering.
     * @param info  The argument type info.
     * @param <A>   The argument type.
     * @param <T>   The argument type template.
     * @param <I>   Argument type info
     * @return The registered argument type.
     */
    <A extends ArgumentType<?>, T extends ArgumentTypeInfo.Template<A>, I extends ArgumentTypeInfo<A, T>> I registerArgumentTypeInfo(Class<A> klass, I info);

    /**
     * Create a menu type which sends additional data when opened.
     *
     * @param codec   Parse the additional container data into a usable type.
     * @param factory The factory to create the new menu.
     * @param <C>     The menu/container than we open.
     * @param <T>     The data that we send to the client.
     * @return The menu type for this container.
     */
    <C extends AbstractContainerMenu, T extends ContainerData> MenuType<C> createMenuType(StreamCodec<RegistryFriendlyByteBuf, T> codec, ContainerData.Factory<C, T> factory);

    /**
     * Open a container using a specific {@link ContainerData}.
     *
     * @param player The player to open the menu for.
     * @param title  The title for this menu.
     * @param menu   The underlying menu constructor.
     * @param data   The menu data.
     */
    void openMenu(Player player, Component title, MenuConstructor menu, ContainerData data);

    /**
     * Invalidate components on a block enitty.
     *
     * @param owner The block entity whose components should be invalidated.
     */
    default void invalidateComponent(BlockEntity owner) {
    }

    /**
     * Create a {@link ComponentAccess} for surrounding peripherals.
     *
     * @param owner      The block entity requesting surrounding peripherals.
     * @param invalidate The function to call when a neighbouring peripheral potentially changes. This <em>MAY NOT</em>
     *                   include all changes, and so block updates should still be listened to.
     * @return The peripheral component access.
     */
    ComponentAccess<IPeripheral> createPeripheralAccess(BlockEntity owner, Consumer<Direction> invalidate);

    /**
     * Create a {@link ComponentAccess} for surrounding wired nodes.
     *
     * @param owner      The block entity requesting surrounding wired elements.
     * @param invalidate The function to call when a neighbouring wired node potentially changes. This <em>MAY NOT</em>
     *                   include all changes, and so block updates should still be listened to.
     * @return The peripheral component access.
     */
    ComponentAccess<WiredElement> createWiredElementAccess(BlockEntity owner, Consumer<Direction> invalidate);

    /**
     * Determine if there is a wired element in the given direction. This is equivalent to
     * {@code createWiredElementAt(x -> {}).get(level, pos, dir) != null}, but is intended for when we don't need the
     * cache.
     *
     * @param level     The current level.
     * @param pos       The <em>current</em> block's position.
     * @param direction The direction to check in.
     * @return Whether there is a wired element in the given direction.
     */
    boolean hasWiredElementIn(Level level, BlockPos pos, Direction direction);

    /**
     * Wrap a vanilla Minecraft {@link Container} into a {@link ContainerTransfer}.
     *
     * @param container The container to wrap.
     * @return The container transfer.
     */
    ContainerTransfer.Slotted wrapContainer(Container container);

    /**
     * Get access to a {@link ContainerTransfer} for a given position. This should look up blocks, then fall back to
     * {@link InventoryUtil#getEntityContainer(ServerLevel, BlockPos, Direction)}
     *
     * @param level The current level.
     * @param pos   The current position.
     * @param side  The side of the block we're viewing the inventory from. Equivalent to the direction argument for
     *              {@link WorldlyContainer}.
     * @return The container, or {@code null} if none exists.
     */
    @Nullable
    ContainerTransfer getContainer(ServerLevel level, BlockPos pos, Direction side);

    /**
     * Get the {@link RecipeIngredients} for this loader.
     *
     * @return The loader-specific recipe ingredients.
     */
    RecipeIngredients getRecipeIngredients();

    /**
     * Get a list of tags representing each Minecraft dye. This should follow the same order as {@linkplain DyeColor
     * Minecraft's dyes}, starting with white and ending with black.
     *
     * @return A list of tags.
     */
    ColorCollection<TagKey<Item>> getDyeTags();

    /**
     * Get the amount of fuel an item provides.
     *
     * @param server The current server.
     * @param stack  The item to burn.
     * @return The amount of fuel it provides.
     * @see MinecraftServer#fuelValues()
     */
    int getBurnTime(MinecraftServer server, ItemStack stack);

    /**
     * Create a builder for a new creative tab.
     *
     * @return The creative tab builder.
     */
    CreativeModeTab.Builder newCreativeModeTab();

    /**
     * Get the "container" item to be returned after crafting. For instance, crafting with a lava bucket should return
     * an empty bucket.
     *
     * @param stack The original item.
     * @return The "remainder" item. May be {@link ItemStack#EMPTY}.
     */
    @Nullable ItemStackTemplate getCraftingRemainingItem(ItemStack stack);

    /**
     * Check whether we should notify neighbours in a particular direction.
     *
     * @param level     The current level.
     * @param pos       The position of the current block.
     * @param block     The block which is performing the notification, should be equal to {@code level.getBlockState(pos)}.
     * @param direction The direction we're notifying in.
     * @return {@code true} if neighbours should be notified or {@code false} otherwise.
     */
    boolean onNotifyNeighbour(Level level, BlockPos pos, BlockState block, Direction direction);

    /**
     * Create a new fake player.
     *
     * @param level   The level the player should be created in.
     * @param profile The user this player should mimic.
     * @return The newly constructed fake player.
     */
    ServerPlayer createFakePlayer(ServerLevel level, GameProfile profile);

    /**
     * Determine if a player is not a real player.
     *
     * @param player The player to check.
     * @return Whether this player is fake.
     */
    default boolean isFakePlayer(ServerPlayer player) {
        // Any subclass of ServerPlayer (i.e. Forge's FakePlayer) is assumed to be a fake.
        return player.connection == null || player.getClass() != ServerPlayer.class;
    }

    /**
     * Check if this item is a tool and has some secondary usage.
     * <p>
     * In practice, this only checks if a tool is a hoe or shovel. We don't want to include things like axes,
     *
     * @param stack The stack to check.
     * @return Whether this tool has a secondary usage.
     */
    boolean hasToolUsage(ItemStack stack);

    /**
     * Check if an entity can be attacked according to platform-specific events.
     *
     * @param player The player who is attacking.
     * @param entity The entity we're attacking.
     * @return If this entity can be attacked.
     * @see Player#attack(Entity)
     */
    InteractionResult canAttackEntity(ServerPlayer player, Entity entity);

    /**
     * Interact with an entity, for instance feeding cows.
     * <p>
     * Implementations should call {@link Player#interactOn(Entity, InteractionHand, Vec3)} and any loader-specific
     * hooks.
     *
     * @param player The player which is interacting with the entity.
     * @param hit    The entity and position we're interacting with. Note the hit's position is world-relative,
     *               unlike {@link Entity#interact(Player, InteractionHand, Vec3)} which is relative to the entity.
     * @return Whether any interaction occurred.
     * @see Player#interactOn(Entity, InteractionHand, Vec3)
     * @see ServerGamePacketListenerImpl#handleInteract
     */
    boolean interactWithEntity(ServerPlayer player, EntityHitResult hit);

    /**
     * The result of attempting to use an item on a block.
     */
    sealed interface UseOnResult {
        /**
         * This interaction was intercepted by an event, and handled.
         *
         * @param result The result of using an item on a block.
         */
        record Handled(InteractionResult result) implements UseOnResult {
        }

        /**
         * This result was not handled, and should be handled by the caller.
         *
         * @param block Whether the block may be used ({@link BlockState#useItemOn(ItemStack, Level, Player, InteractionHand, BlockHitResult)}).
         * @param item  Whether the item may be used on the block ({@link ItemStack#useOn(UseOnContext)}).
         * @see ServerPlayerGameMode#useItemOn(ServerPlayer, Level, ItemStack, InteractionHand, BlockHitResult)
         */
        record Continue(boolean block, boolean item) implements UseOnResult {
        }
    }

    /**
     * Run mod-loader specific code before placing an item against a block.
     * <p>
     * This should dispatch any mod-loader specific events that are fired when clicking a block. It does necessarily
     * handle the actual clicking of the block — see {@link UseOnResult.Handled} and {@link UseOnResult.Continue}.
     *
     * @param player The player which is placing this item.
     * @param stack  The item to place.
     * @param hit    The collision with the block we're placing against.
     * @return Whether any interaction occurred.
     */
    UseOnResult useOn(ServerPlayer player, ItemStack stack, BlockHitResult hit);


    final class Instance {
        static final @Nullable PlatformHelper INSTANCE;
        static final @Nullable Throwable ERROR;

        static {
            // We don't want class initialisation to fail here (as that results in confusing errors). Instead, capture
            // the error and rethrow it when accessing. This should be JITted away in the common case.
            var helper = Services.tryLoad(PlatformHelper.class);
            INSTANCE = helper.instance();
            ERROR = helper.error();
        }

        private Instance() {
        }
    }

    /**
     * Find a {@link IMedia} instance for an item stack.
     *
     * @param stack The stack to look up the media for.
     * @return The media instance, or {@code null} if not found.
     */
    @Nullable
    IMedia getMedia(ItemStack stack);

    /**
     * Construct a {@link ClickEvent} that opens a folder.
     *
     * @param path The folder to open.
     * @return The click event.
     */
    ClickEvent createOpenFolderAction(Path path);
}
