package net.minecraft.commands;

import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.ParseResults;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.context.ContextChain;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.ArgumentCommandNode;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.RootCommandNode;
import com.mojang.logging.LogUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import net.minecraft.ChatFormatting;
import net.minecraft.SharedConstants;
import net.minecraft.Util;
import net.minecraft.commands.execution.ExecutionContext;
import net.minecraft.commands.synchronization.ArgumentTypeInfos;
import net.minecraft.commands.synchronization.ArgumentUtils;
import net.minecraft.commands.synchronization.SuggestionProviders;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.data.registries.VanillaRegistries;
import net.minecraft.gametest.framework.TestCommand;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.ComponentUtils;
import net.minecraft.network.chat.HoverEvent;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.protocol.game.ClientboundCommandsPacket;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.commands.AdvancementCommands;
import net.minecraft.server.commands.AttributeCommand;
import net.minecraft.server.commands.BanIpCommands;
import net.minecraft.server.commands.BanListCommands;
import net.minecraft.server.commands.BanPlayerCommands;
import net.minecraft.server.commands.BossBarCommands;
import net.minecraft.server.commands.ClearInventoryCommands;
import net.minecraft.server.commands.CloneCommands;
import net.minecraft.server.commands.DamageCommand;
import net.minecraft.server.commands.DataPackCommand;
import net.minecraft.server.commands.DeOpCommands;
import net.minecraft.server.commands.DebugCommand;
import net.minecraft.server.commands.DebugConfigCommand;
import net.minecraft.server.commands.DebugMobSpawningCommand;
import net.minecraft.server.commands.DebugPathCommand;
import net.minecraft.server.commands.DefaultGameModeCommands;
import net.minecraft.server.commands.DialogCommand;
import net.minecraft.server.commands.DifficultyCommand;
import net.minecraft.server.commands.EffectCommands;
import net.minecraft.server.commands.EmoteCommands;
import net.minecraft.server.commands.EnchantCommand;
import net.minecraft.server.commands.ExecuteCommand;
import net.minecraft.server.commands.ExperienceCommand;
import net.minecraft.server.commands.FillBiomeCommand;
import net.minecraft.server.commands.FillCommand;
import net.minecraft.server.commands.ForceLoadCommand;
import net.minecraft.server.commands.FunctionCommand;
import net.minecraft.server.commands.GameModeCommand;
import net.minecraft.server.commands.GameRuleCommand;
import net.minecraft.server.commands.GiveCommand;
import net.minecraft.server.commands.HelpCommand;
import net.minecraft.server.commands.ItemCommands;
import net.minecraft.server.commands.JfrCommand;
import net.minecraft.server.commands.KickCommand;
import net.minecraft.server.commands.KillCommand;
import net.minecraft.server.commands.ListPlayersCommand;
import net.minecraft.server.commands.LocateCommand;
import net.minecraft.server.commands.LootCommand;
import net.minecraft.server.commands.MsgCommand;
import net.minecraft.server.commands.OpCommand;
import net.minecraft.server.commands.PardonCommand;
import net.minecraft.server.commands.PardonIpCommand;
import net.minecraft.server.commands.ParticleCommand;
import net.minecraft.server.commands.PerfCommand;
import net.minecraft.server.commands.PermissionCheck;
import net.minecraft.server.commands.PlaceCommand;
import net.minecraft.server.commands.PlaySoundCommand;
import net.minecraft.server.commands.PublishCommand;
import net.minecraft.server.commands.RaidCommand;
import net.minecraft.server.commands.RandomCommand;
import net.minecraft.server.commands.RecipeCommand;
import net.minecraft.server.commands.ReloadCommand;
import net.minecraft.server.commands.ReturnCommand;
import net.minecraft.server.commands.RideCommand;
import net.minecraft.server.commands.RotateCommand;
import net.minecraft.server.commands.SaveAllCommand;
import net.minecraft.server.commands.SaveOffCommand;
import net.minecraft.server.commands.SaveOnCommand;
import net.minecraft.server.commands.SayCommand;
import net.minecraft.server.commands.ScheduleCommand;
import net.minecraft.server.commands.ScoreboardCommand;
import net.minecraft.server.commands.SeedCommand;
import net.minecraft.server.commands.ServerPackCommand;
import net.minecraft.server.commands.SetBlockCommand;
import net.minecraft.server.commands.SetPlayerIdleTimeoutCommand;
import net.minecraft.server.commands.SetSpawnCommand;
import net.minecraft.server.commands.SetWorldSpawnCommand;
import net.minecraft.server.commands.SpawnArmorTrimsCommand;
import net.minecraft.server.commands.SpectateCommand;
import net.minecraft.server.commands.SpreadPlayersCommand;
import net.minecraft.server.commands.StopCommand;
import net.minecraft.server.commands.StopSoundCommand;
import net.minecraft.server.commands.SummonCommand;
import net.minecraft.server.commands.TagCommand;
import net.minecraft.server.commands.TeamCommand;
import net.minecraft.server.commands.TeamMsgCommand;
import net.minecraft.server.commands.TeleportCommand;
import net.minecraft.server.commands.TellRawCommand;
import net.minecraft.server.commands.TickCommand;
import net.minecraft.server.commands.TimeCommand;
import net.minecraft.server.commands.TitleCommand;
import net.minecraft.server.commands.TransferCommand;
import net.minecraft.server.commands.TriggerCommand;
import net.minecraft.server.commands.VersionCommand;
import net.minecraft.server.commands.WardenSpawnTrackerCommand;
import net.minecraft.server.commands.WaypointCommand;
import net.minecraft.server.commands.WeatherCommand;
import net.minecraft.server.commands.WhitelistCommand;
import net.minecraft.server.commands.WorldBorderCommand;
import net.minecraft.server.commands.data.DataCommands;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey;
import net.minecraft.util.profiling.Profiler;
import net.minecraft.util.profiling.jfr.JvmProfiler;
import net.minecraft.world.flag.FeatureFlagSet;
import net.minecraft.world.flag.FeatureFlags;
import net.minecraft.world.level.GameRules;
import org.slf4j.Logger;

public class Commands {
    public static final String COMMAND_PREFIX = "/";
    private static final ThreadLocal<ExecutionContext<CommandSourceStack>> CURRENT_EXECUTION_CONTEXT = new ThreadLocal<>();
    private static final Logger LOGGER = LogUtils.getLogger();
    public static final int LEVEL_ALL = 0;
    public static final int LEVEL_MODERATORS = 1;
    public static final int LEVEL_GAMEMASTERS = 2;
    public static final int LEVEL_ADMINS = 3;
    public static final int LEVEL_OWNERS = 4;
    private static final ClientboundCommandsPacket.NodeInspector<CommandSourceStack> COMMAND_NODE_INSPECTOR = new ClientboundCommandsPacket.NodeInspector<CommandSourceStack>() {
        @Nullable
        @Override
        public ResourceLocation suggestionId(ArgumentCommandNode<CommandSourceStack, ?> node) {
            SuggestionProvider<CommandSourceStack> customSuggestions = node.getCustomSuggestions();
            return customSuggestions != null ? SuggestionProviders.getName(customSuggestions) : null;
        }

        @Override
        public boolean isExecutable(CommandNode<CommandSourceStack> node) {
            return node.getCommand() != null;
        }

        @Override
        public boolean isRestricted(CommandNode<CommandSourceStack> node) {
            if (node.getRequirement() instanceof PermissionSource.RestrictedMarker) return true; // Paper - restricted api
            return node.getRequirement() instanceof PermissionCheck<?> permissionCheck && permissionCheck.requiredLevel() > 0;
        }
    };
    private final CommandDispatcher<CommandSourceStack> dispatcher = new CommandDispatcher<>();

    public Commands(Commands.CommandSelection selection, CommandBuildContext context) {
    // Paper start - Brigadier API - modern minecraft overloads that do not use redirects but are copies instead
        this(selection, context, false);
    }
    public Commands(Commands.CommandSelection selection, CommandBuildContext context, final boolean modern) {
    // Paper end - Brigadier API - modern minecraft overloads that do not use redirects but are copies instead
        AdvancementCommands.register(this.dispatcher);
        AttributeCommand.register(this.dispatcher, context);
        ExecuteCommand.register(this.dispatcher, context);
        //BossBarCommands.register(this.dispatcher, context); // Folia - region threading - TODO
        ClearInventoryCommands.register(this.dispatcher, context);
        //CloneCommands.register(this.dispatcher, context); // Folia - region threading - TODO
        DamageCommand.register(this.dispatcher, context);
        if(me.earthme.luminol.config.modules.experiment.CommandConfig.data) {
            DataCommands.register(this.dispatcher); // Folia - region threading - TODO
        }
        DataPackCommand.register(this.dispatcher, context); // Folia - region threading - TODO // Luminol - Add back read-only datapack command
        //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO
        DefaultGameModeCommands.register(this.dispatcher);
        //DialogCommand.register(this.dispatcher, context); // Folia - region threading - TODO
        DifficultyCommand.register(this.dispatcher);
        EffectCommands.register(this.dispatcher, context);
        EmoteCommands.register(this.dispatcher);
        EnchantCommand.register(this.dispatcher, context);
        ExperienceCommand.register(this.dispatcher);
        FillCommand.register(this.dispatcher, context);
        FillBiomeCommand.register(this.dispatcher, context);
        ForceLoadCommand.register(this.dispatcher);
        //FunctionCommand.register(this.dispatcher); // Folia - region threading - TODO
        GameModeCommand.register(this.dispatcher);
        GameRuleCommand.register(this.dispatcher, context);
        GiveCommand.register(this.dispatcher, context);
        HelpCommand.register(this.dispatcher);
        //ItemCommands.register(this.dispatcher, context); // Folia - region threading - TODO later
        KickCommand.register(this.dispatcher);
        KillCommand.register(this.dispatcher);
        ListPlayersCommand.register(this.dispatcher);
        LocateCommand.register(this.dispatcher, context);
        //LootCommand.register(this.dispatcher, context); // Folia - region threading - TODO later
        MsgCommand.register(this.dispatcher);
        ParticleCommand.register(this.dispatcher, context);
        PlaceCommand.register(this.dispatcher);
        PlaySoundCommand.register(this.dispatcher);
        RandomCommand.register(this.dispatcher);
        //ReloadCommand.register(this.dispatcher); // Folia - region threading
        RecipeCommand.register(this.dispatcher);
        //ReturnCommand.register(this.dispatcher); // Folia - region threading - TODO later
        //RideCommand.register(this.dispatcher); // Folia - region threading - TODO later
        //RotateCommand.register(this.dispatcher); // Folia - region threading - TODO later
        SayCommand.register(this.dispatcher);
        //ScheduleCommand.register(this.dispatcher); // Folia - region threading
        //ScoreboardCommand.register(this.dispatcher, context); // Folia - region threading
        SeedCommand.register(this.dispatcher, selection != Commands.CommandSelection.INTEGRATED);
        VersionCommand.register(this.dispatcher, selection != Commands.CommandSelection.INTEGRATED);
        SetBlockCommand.register(this.dispatcher, context);
        SetSpawnCommand.register(this.dispatcher);
        SetWorldSpawnCommand.register(this.dispatcher);
        //SpectateCommand.register(this.dispatcher); // Folia - region threading - TODO later
        //SpreadPlayersCommand.register(this.dispatcher); // Folia - region threading - TODO later
        StopSoundCommand.register(this.dispatcher);
        SummonCommand.register(this.dispatcher, context);
        //TagCommand.register(this.dispatcher); // Folia - region threading - TODO later
        //TeamCommand.register(this.dispatcher, context); // Folia - region threading - TODO later
        //TeamMsgCommand.register(this.dispatcher); // Folia - region threading - TODO later
        TeleportCommand.register(this.dispatcher);
        TellRawCommand.register(this.dispatcher, context);
        //TestCommand.register(this.dispatcher, context); // Folia - region threading
        //TickCommand.register(this.dispatcher); // Folia - region threading - TODO later
        TimeCommand.register(this.dispatcher);
        TitleCommand.register(this.dispatcher, context);
        //TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later
        //WaypointCommand.register(this.dispatcher, context); // Folia - region threading - TODO later
        WeatherCommand.register(this.dispatcher);
        WorldBorderCommand.register(this.dispatcher);
        if (JvmProfiler.INSTANCE.isAvailable()) {
            JfrCommand.register(this.dispatcher);
        }

        if (SharedConstants.IS_RUNNING_IN_IDE) {
            RaidCommand.register(this.dispatcher, context);
            DebugPathCommand.register(this.dispatcher);
            DebugMobSpawningCommand.register(this.dispatcher);
            WardenSpawnTrackerCommand.register(this.dispatcher);
            SpawnArmorTrimsCommand.register(this.dispatcher);
            ServerPackCommand.register(this.dispatcher);
            if (selection.includeDedicated) {
                DebugConfigCommand.register(this.dispatcher, context);
            }
        }

        if (selection.includeDedicated) {
            BanIpCommands.register(this.dispatcher);
            BanListCommands.register(this.dispatcher);
            BanPlayerCommands.register(this.dispatcher);
            DeOpCommands.register(this.dispatcher);
            OpCommand.register(this.dispatcher);
            PardonCommand.register(this.dispatcher);
            PardonIpCommand.register(this.dispatcher);
            //PerfCommand.register(this.dispatcher); // Folia - region threading - TODO later
            //SaveAllCommand.register(this.dispatcher); // Folia - region threading - TODO later
            SaveOffCommand.register(this.dispatcher);
            SaveOnCommand.register(this.dispatcher);
            SetPlayerIdleTimeoutCommand.register(this.dispatcher);
            StopCommand.register(this.dispatcher);
            TransferCommand.register(this.dispatcher);
            WhitelistCommand.register(this.dispatcher);
        }

        if (selection.includeIntegrated) {
            PublishCommand.register(this.dispatcher);
        }

        // Paper start - Vanilla command permission fixes
        for (final CommandNode<CommandSourceStack> node : this.dispatcher.getRoot().getChildren()) {
            if (node.getRequirement() == com.mojang.brigadier.builder.ArgumentBuilder.<CommandSourceStack>defaultRequirement()) {
                node.requirement = stack -> stack.source == CommandSource.NULL || stack.getBukkitSender().hasPermission(org.bukkit.craftbukkit.command.VanillaCommandWrapper.getPermission(node));
            } else if (node.getRequirement() instanceof net.minecraft.commands.PermissionSource.Check<CommandSourceStack> check) {
                check.vanillaNode().set(node);
            }
        }
        // Paper end - Vanilla command permission fixes
        // Paper start - Brigadier Command API
        // Create legacy minecraft namespace commands
        for (final CommandNode<CommandSourceStack> node : new java.util.ArrayList<>(this.dispatcher.getRoot().getChildren())) {
            if (modern) {
                // Modern behaviour that simply creates a full copy of the commands node.
                // Avoids plenty of issues around registering redirects *to* these nodes from the API
                this.dispatcher.getRoot().addChild(
                    io.papermc.paper.command.brigadier.PaperBrigadier.copyLiteral(
                        "minecraft:" + node.getName(),
                        (com.mojang.brigadier.tree.LiteralCommandNode<CommandSourceStack>) node
                    )
                );
                continue;
            }

            // Legacy behaviour of creating a flattened redirecting node.
            // Used by CommandArgumentUpgrader
            CommandNode<CommandSourceStack> flattenedAliasTarget = node;
            while (flattenedAliasTarget.getRedirect() != null) flattenedAliasTarget = flattenedAliasTarget.getRedirect();

            this.dispatcher.register(
                com.mojang.brigadier.builder.LiteralArgumentBuilder.<CommandSourceStack>literal("minecraft:" + node.getName())
                    .executes(flattenedAliasTarget.getCommand())
                    .requires(flattenedAliasTarget.getRequirement())
                    .redirect(flattenedAliasTarget));
        }
        // Paper end - Brigadier Command API
        this.dispatcher.setConsumer(ExecutionCommandSource.resultConsumer());
    }

    public static <S> ParseResults<S> mapSource(ParseResults<S> parseResults, UnaryOperator<S> mapper) {
        CommandContextBuilder<S> context = parseResults.getContext();
        CommandContextBuilder<S> commandContextBuilder = context.withSource(mapper.apply(context.getSource()));
        return new ParseResults<>(commandContextBuilder, parseResults.getReader(), parseResults.getExceptions());
    }

    public void performPrefixedCommand(CommandSourceStack source, String command) {
        command = trimOptionalPrefix(command);
        this.performCommand(this.dispatcher.parse(command, source), command);
    }

    public static String trimOptionalPrefix(String command) {
        return command.startsWith("/") ? command.substring(1) : command;
    }

    public void performCommand(ParseResults<CommandSourceStack> parseResults, String command) {
        // Paper start
        this.performCommand(parseResults, command, false);
    }

    public void performCommand(ParseResults<CommandSourceStack> parseResults, String command, boolean throwCommandError) {
        org.spigotmc.AsyncCatcher.catchOp("Cannot perform command async");
        // Paper end
        CommandSourceStack commandSourceStack = parseResults.getContext().getSource();
        Profiler.get().push(() -> "/" + command);
        ContextChain<CommandSourceStack> contextChain = finishParsing(parseResults, command, commandSourceStack);

        try {
            if (contextChain != null) {
                executeCommandInContext(
                    commandSourceStack,
                    executionContext -> ExecutionContext.queueInitialCommandExecution(
                        executionContext, command, contextChain, commandSourceStack, CommandResultCallback.EMPTY
                    )
                );
            }
            // Paper start
        } catch (Throwable var12) { // always gracefully handle it, no matter how bad:tm:
            if (throwCommandError) throw var12; // rethrow directly if requested
            // Paper end
            MutableComponent mutableComponent = Component.literal(var12.getMessage() == null ? var12.getClass().getName() : var12.getMessage());
            LOGGER.error("Command exception: /{}", command, var12); // Paper - always show execution exception in console log
            if (commandSourceStack.getServer().isDebugging() || LOGGER.isDebugEnabled()) { // Paper - Debugging
                StackTraceElement[] stackTrace = var12.getStackTrace();

                for (int i = 0; i < Math.min(stackTrace.length, 3); i++) {
                    mutableComponent.append("\n\n")
                        .append(stackTrace[i].getMethodName())
                        .append("\n ")
                        .append(stackTrace[i].getFileName())
                        .append(":")
                        .append(String.valueOf(stackTrace[i].getLineNumber()));
                }
            }

            commandSourceStack.sendFailure(
                Component.translatable("command.failed").withStyle(style -> style.withHoverEvent(new HoverEvent.ShowText(mutableComponent)))
            );
            if (SharedConstants.IS_RUNNING_IN_IDE) {
                commandSourceStack.sendFailure(Component.literal(Util.describeError(var12)));
                LOGGER.error("'/{}' threw an exception", command, var12);
            }
        } finally {
            Profiler.get().pop();
        }
    }

    @Nullable
    private ContextChain<CommandSourceStack> finishParsing(ParseResults<CommandSourceStack> parseResults, String command, CommandSourceStack source) {
        try {
            validateParseResults(parseResults);
            return ContextChain.tryFlatten(parseResults.getContext().build(command))
                .orElseThrow(() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parseResults.getReader()));
        } catch (CommandSyntaxException var7) {
            // Paper start - Add UnknownCommandEvent
            final net.kyori.adventure.text.TextComponent.Builder builder = net.kyori.adventure.text.Component.text();
            // source.sendFailure(ComponentUtils.fromMessage(var7.getRawMessage()));
            builder.color(net.kyori.adventure.text.format.NamedTextColor.RED).append(io.papermc.paper.command.brigadier.MessageComponentSerializer.message().deserialize(var7.getRawMessage()));
            // Paper end - Add UnknownCommandEvent
            if (var7.getInput() != null && var7.getCursor() >= 0) {
                int min = Math.min(var7.getInput().length(), var7.getCursor());
                MutableComponent mutableComponent = Component.empty()
                    .withStyle(ChatFormatting.GRAY)
                    .withStyle(style -> style.withClickEvent(new ClickEvent.SuggestCommand("/" + command)));
                if (min > 10) {
                    mutableComponent.append(CommonComponents.ELLIPSIS);
                }

                mutableComponent.append(var7.getInput().substring(Math.max(0, min - 10), min));
                if (min < var7.getInput().length()) {
                    Component component = Component.literal(var7.getInput().substring(min)).withStyle(ChatFormatting.RED, ChatFormatting.UNDERLINE);
                    mutableComponent.append(component);
                }

                mutableComponent.append(Component.translatable("command.context.here").withStyle(ChatFormatting.RED, ChatFormatting.ITALIC));
                // Paper start - Add UnknownCommandEvent
                // source.sendFailure(mutableComponent);
                builder
                    .append(net.kyori.adventure.text.Component.newline())
                    .append(io.papermc.paper.adventure.PaperAdventure.asAdventure(mutableComponent));
            }
            org.bukkit.event.command.UnknownCommandEvent event = new org.bukkit.event.command.UnknownCommandEvent(source.getBukkitSender(), command, org.spigotmc.SpigotConfig.unknownCommandMessage.isEmpty() ? null : builder.build());
            org.bukkit.Bukkit.getServer().getPluginManager().callEvent(event);
            if (event.message() != null) {
                source.sendFailure(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.message()), false);
                // Paper end - Add UnknownCommandEvent
            }

            return null;
        }
    }

    public static void executeCommandInContext(CommandSourceStack source, Consumer<ExecutionContext<CommandSourceStack>> contextConsumer) {
        MinecraftServer server = source.getServer();
        ExecutionContext<CommandSourceStack> executionContext = CURRENT_EXECUTION_CONTEXT.get();
        boolean flag = executionContext == null;
        if (flag) {
            int max = Math.max(1, server.getGameRules().getInt(GameRules.RULE_MAX_COMMAND_CHAIN_LENGTH));
            int _int = server.getGameRules().getInt(GameRules.RULE_MAX_COMMAND_FORK_COUNT);

            try (ExecutionContext<CommandSourceStack> executionContext1 = new ExecutionContext<>(max, _int, Profiler.get())) {
                CURRENT_EXECUTION_CONTEXT.set(executionContext1);
                contextConsumer.accept(executionContext1);
                executionContext1.runCommandQueue();
            } finally {
                CURRENT_EXECUTION_CONTEXT.set(null);
            }
        } else {
            contextConsumer.accept(executionContext);
        }
    }

    public void sendCommands(ServerPlayer player) {
        // Paper start - Send empty commands if tab completion is disabled
        if (org.spigotmc.SpigotConfig.tabComplete < 0) {
            player.connection.send(new ClientboundCommandsPacket(new RootCommandNode<>(), COMMAND_NODE_INSPECTOR));
            return;
        }
        // Paper end - Send empty commands if tab completion is disabled
        // CraftBukkit start
        // Register Vanilla commands into builtRoot as before
        // Paper start - Perf: Async command map building
        // Copy root children to avoid concurrent modification during building
        final java.util.Collection<CommandNode<CommandSourceStack>> commandNodes = new java.util.ArrayList<>(this.dispatcher.getRoot().getChildren());
        COMMAND_SENDING_POOL.execute(() -> this.sendAsync(player, commandNodes));
    }

    // Fixed pool, but with discard policy
    public static final java.util.concurrent.ExecutorService COMMAND_SENDING_POOL = new java.util.concurrent.ThreadPoolExecutor(
        2, 2, 0, java.util.concurrent.TimeUnit.MILLISECONDS,
        new java.util.concurrent.LinkedBlockingQueue<>(),
        new com.google.common.util.concurrent.ThreadFactoryBuilder()
            .setNameFormat("Paper Async Command Builder Thread Pool - %1$d")
            .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER))
            .build(),
        new java.util.concurrent.ThreadPoolExecutor.DiscardPolicy()
    );

    private void sendAsync(ServerPlayer player, java.util.Collection<CommandNode<CommandSourceStack>> dispatcherRootChildren) {
        // Paper end - Perf: Async command map building
        Map<CommandNode<CommandSourceStack>, CommandNode<CommandSourceStack>> map = new HashMap<>();
        RootCommandNode<CommandSourceStack> rootCommandNode = new RootCommandNode<>();
        map.put(this.dispatcher.getRoot(), rootCommandNode);
        fillUsableCommands(dispatcherRootChildren, rootCommandNode, player.createCommandSourceStack(), map); // Paper - Perf: Async command map building; pass copy of children

        java.util.Collection<String> bukkit = new java.util.LinkedHashSet<>();
        for (CommandNode node : rootCommandNode.getChildren()) {
            bukkit.add(node.getName());
        }
        // Paper start - Perf: Async command map building
        new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent<CommandSourceStack>(player.getBukkitEntity(), (RootCommandNode) rootCommandNode, false).callEvent(); // Paper - Brigadier API
        // Folia start - region threading
        // ignore if retired
        player.getBukkitEntity().taskScheduler.schedule((ServerPlayer updatedPlayer) -> {
            runSync((ServerPlayer)updatedPlayer, bukkit, rootCommandNode);
        }, null, 1L);
        // Folia end - region threading
    }

    private void runSync(ServerPlayer player, java.util.Collection<String> bukkit, RootCommandNode<CommandSourceStack> rootCommandNode) {
        // Paper end - Perf: Async command map building
        new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent<CommandSourceStack>(player.getBukkitEntity(), (RootCommandNode) rootCommandNode, true).callEvent(); // Paper - Brigadier API
        org.bukkit.event.player.PlayerCommandSendEvent event = new org.bukkit.event.player.PlayerCommandSendEvent(player.getBukkitEntity(), new java.util.LinkedHashSet<>(bukkit));
        event.getPlayer().getServer().getPluginManager().callEvent(event);

        // Remove labels that were removed during the event
        for (String orig : bukkit) {
            if (!event.getCommands().contains(orig)) {
                rootCommandNode.removeCommand(orig);
            }
        }
        // CraftBukkit end
        player.connection.send(new ClientboundCommandsPacket(rootCommandNode, COMMAND_NODE_INSPECTOR));
    }

    private static <S> void fillUsableCommands(java.util.Collection<CommandNode<S>> children, CommandNode<S> current, S source, Map<CommandNode<S>, CommandNode<S>> output) { // Paper - Perf: Async command map building; pass copy of children
        for (CommandNode<S> commandNode : children) {  // Paper - Perf: Async command map building; pass copy of children
            // Paper start - Brigadier API
            if (commandNode.clientNode != null) {
                commandNode = commandNode.clientNode;
            }
            // Paper end - Brigadier API
            if (!org.spigotmc.SpigotConfig.sendNamespaced && commandNode.getName().contains(":")) continue; // Spigot
            if (commandNode.canUse(source)) {
                ArgumentBuilder<S, ?> argumentBuilder = commandNode.createBuilder();
                // Paper start
                /*
                Because of how commands can be yeeted right left and center due to bad bukkit practices
                we need to be able to ensure that ALL commands are registered (even redirects).

                What this will do is IF the redirect seems to be "dead" it will create a builder and essentially populate (flatten)
                all the children from the dead redirect to the node.

                So, if minecraft:msg redirects to msg but the original msg node has been overriden minecraft:msg will now act as msg and will explicilty inherit its children.

                The only way to fix this is to either:
                - Send EVERYTHING flattened, don't use redirects
                - Don't allow command nodes to be deleted
                - Do this :)
                 */
                // Is there an invalid command redirect?
                if (argumentBuilder.getRedirect() != null && output.get(argumentBuilder.getRedirect()) == null) {
                    // Create the argument builder with the same values as the specified node, but with a different literal and populated children

                    final CommandNode<S> redirect = argumentBuilder.getRedirect();
                    // Diff copied from LiteralCommand#createBuilder
                    final com.mojang.brigadier.builder.LiteralArgumentBuilder<S> builder = com.mojang.brigadier.builder.LiteralArgumentBuilder.literal(commandNode.getName());
                    builder.requires(redirect.getRequirement());
                    // builder.forward(redirect.getRedirect(), redirect.getRedirectModifier(), redirect.isFork()); We don't want to migrate the forward, since it's invalid.
                    if (redirect.getCommand() != null) {
                        builder.executes(redirect.getCommand());
                    }
                    // Diff copied from LiteralCommand#createBuilder
                    for (final CommandNode<S> child : redirect.getChildren()) {
                        builder.then(child);
                    }

                    argumentBuilder = builder;
                }
                // Paper end
                if (argumentBuilder.getRedirect() != null) {
                    argumentBuilder.redirect(output.get(argumentBuilder.getRedirect()));
                }

                CommandNode<S> commandNode1 = argumentBuilder.build();
                output.put(commandNode, commandNode1);
                current.addChild(commandNode1);
                if (!commandNode.getChildren().isEmpty()) {
                    fillUsableCommands(commandNode.getChildren(), commandNode1, source, output); // Paper - Perf: Async command map building; pass copy of children
                }
            }
        }
    }

    public static LiteralArgumentBuilder<CommandSourceStack> literal(String name) {
        return LiteralArgumentBuilder.literal(name);
    }

    public static <T> RequiredArgumentBuilder<CommandSourceStack, T> argument(String name, ArgumentType<T> type) {
        return RequiredArgumentBuilder.argument(name, type);
    }

    public static Predicate<String> createValidator(Commands.ParseFunction parser) {
        return string -> {
            try {
                parser.parse(new StringReader(string));
                return true;
            } catch (CommandSyntaxException var3) {
                return false;
            }
        };
    }

    public CommandDispatcher<CommandSourceStack> getDispatcher() {
        return this.dispatcher;
    }

    public static <S> void validateParseResults(ParseResults<S> parseResults) throws CommandSyntaxException {
        CommandSyntaxException parseException = getParseException(parseResults);
        if (parseException != null) {
            throw parseException;
        }
    }

    @Nullable
    public static <S> CommandSyntaxException getParseException(ParseResults<S> result) {
        if (!result.getReader().canRead()) {
            return null;
        } else if (result.getExceptions().size() == 1) {
            return result.getExceptions().values().iterator().next();
        } else {
            return result.getContext().getRange().isEmpty()
                ? CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(result.getReader())
                : CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(result.getReader());
        }
    }

    public static CommandBuildContext createValidationContext(final HolderLookup.Provider provider) {
        return new CommandBuildContext() {
            @Override
            public FeatureFlagSet enabledFeatures() {
                return FeatureFlags.REGISTRY.allFlags();
            }

            @Override
            public Stream<ResourceKey<? extends Registry<?>>> listRegistryKeys() {
                return provider.listRegistryKeys();
            }

            @Override
            public <T> Optional<HolderLookup.RegistryLookup<T>> lookup(ResourceKey<? extends Registry<? extends T>> registryKey) {
                return provider.lookup(registryKey).map(this::createLookup);
            }

            private <T> HolderLookup.RegistryLookup.Delegate<T> createLookup(final HolderLookup.RegistryLookup<T> registryLookup) {
                return new HolderLookup.RegistryLookup.Delegate<T>() {
                    @Override
                    public HolderLookup.RegistryLookup<T> parent() {
                        return registryLookup;
                    }

                    @Override
                    public Optional<HolderSet.Named<T>> get(TagKey<T> tagKey) {
                        return Optional.of(this.getOrThrow(tagKey));
                    }

                    @Override
                    public HolderSet.Named<T> getOrThrow(TagKey<T> tagKey) {
                        Optional<HolderSet.Named<T>> optional = this.parent().get(tagKey);
                        return optional.orElseGet(() -> HolderSet.emptyNamed(this.parent(), tagKey));
                    }
                };
            }
        };
    }

    public static void validate() {
        CommandBuildContext commandBuildContext = createValidationContext(VanillaRegistries.createLookup());
        CommandDispatcher<CommandSourceStack> dispatcher = new Commands(Commands.CommandSelection.ALL, commandBuildContext).getDispatcher();
        RootCommandNode<CommandSourceStack> root = dispatcher.getRoot();
        dispatcher.findAmbiguities(
            (commandNode, commandNode1, commandNode2, collection) -> LOGGER.warn(
                "Ambiguity between arguments {} and {} with inputs: {}", dispatcher.getPath(commandNode1), dispatcher.getPath(commandNode2), collection
            )
        );
        Set<ArgumentType<?>> set = ArgumentUtils.findUsedArgumentTypes(root);
        Set<ArgumentType<?>> set1 = set.stream()
            .filter(argumentType -> !ArgumentTypeInfos.isClassRecognized(argumentType.getClass()))
            .collect(Collectors.toSet());
        if (!set1.isEmpty()) {
            LOGGER.warn(
                "Missing type registration for following arguments:\n {}",
                set1.stream().map(argumentType -> "\t" + argumentType).collect(Collectors.joining(",\n"))
            );
            throw new IllegalStateException("Unregistered argument types");
        }
    }

    public static <T extends PermissionSource> PermissionCheck<T> hasPermission(int level) {
        return new PermissionSource.Check<>(level);
    }

    public static enum CommandSelection {
        ALL(true, true),
        DEDICATED(false, true),
        INTEGRATED(true, false);

        final boolean includeIntegrated;
        final boolean includeDedicated;

        private CommandSelection(final boolean includeIntegrated, final boolean includeDedicated) {
            this.includeIntegrated = includeIntegrated;
            this.includeDedicated = includeDedicated;
        }
    }

    @FunctionalInterface
    public interface ParseFunction {
        void parse(StringReader input) throws CommandSyntaxException;
    }
}
