package dev.alangomes.springspigot.util;

import org.apache.commons.lang3.tuple.Pair;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.util.StringUtil;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.PositionalParamSpec;

import java.util.Map;
import java.util.Optional;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class CommandUtils {

    private CommandUtils() {
    }

    /**
     * Find all possible subcommand names for autocompletion
     *
     * @param commandSpec The {@link picocli.CommandLine.Model.CommandSpec CommandSpec} to be analyzed
     * @param args        The partial arguments to be used on analysis
     * @return All possible subcommands (names and aliases considered)
     */
    public static Stream<String> getPossibleSubcommands(CommandSpec commandSpec, String[] args) {
        return getPossibleSubcommands(commandSpec, args, 0);
    }

    /**
     * Find suggestions of positional arguments for autocompletion
     *
     * @param commandSpec The {@link picocli.CommandLine.Model.CommandSpec CommandSpec} to be analyzed
     * @param args        The partial arguments to be used on analysis
     * @return A stream of suggested values for the last argument (respecting
     * {@link PositionalParamSpec#completionCandidates() completionCandidates})
     */
    public static Stream<String> getPossibleArguments(CommandSpec commandSpec, String[] args) {
        int requestedIndex = args.length - 1;
        return getPossibleCommands(commandSpec, args, 0)
                .map(specPair -> specPair.getKey().positionalParameters().stream()
                        .filter(paramSpec -> paramSpec.index().contains(Math.max(0,
                                requestedIndex - (specPair.getValue() + 1))))
                        .findFirst())
                .filter(Optional::isPresent)
                .map(Optional::get)
                .flatMap(CommandUtils::getSuggestedValues)
                .filter(value -> StringUtil.startsWithIgnoreCase(value, args[requestedIndex]));
    }

    private static Stream<String> getSuggestedValues(PositionalParamSpec paramSpec) {
        Iterable<String> completionCandidates = paramSpec.completionCandidates();
        if (completionCandidates != null) {
            return StreamSupport.stream(Spliterators.spliteratorUnknownSize(completionCandidates.iterator(), 0), false);
        } else {
            return getSuggestedValues(paramSpec.type());
        }
    }

    private static Stream<String> getSuggestedValues(Class<?> type) {
        if (CommandSender.class.isAssignableFrom(type)) {
            return Bukkit.getOnlinePlayers().stream().map(Player::getName);
        } else if (World.class.isAssignableFrom(type)) {
            return Bukkit.getWorlds().stream().map(World::getName);
        }
        return Stream.empty();
    }

    private static Stream<String> getPossibleSubcommands(CommandSpec spec, String[] args, int index) {
        if (args.length == 0) return Stream.empty();
        Stream<Map.Entry<String, CommandLine>> subcommandsStream = spec.subcommands().entrySet().stream();
        if (index + 1 == args.length) {
            return subcommandsStream
                    .filter(entry -> StringUtil.startsWithIgnoreCase(entry.getKey(), args[index]))
                    .flatMap(entry -> entry.getValue().getCommandSpec().names().stream());
        }
        return subcommandsStream
                .filter(entry -> entry.getKey().equalsIgnoreCase(args[index]))
                .flatMap(entry -> getPossibleSubcommands(entry.getValue().getCommandSpec(), args, index + 1));
    }

    private static Stream<Pair<CommandSpec, Integer>> getPossibleCommands(CommandSpec spec, String[] args, int index) {
        if (args.length <= 1) return Stream.of(Pair.of(spec, index));
        return spec.subcommands().entrySet().stream()
                .filter(entry -> entry.getKey().equalsIgnoreCase(args[index]))
                .flatMap(entry -> {
                    if (index + 2 >= args.length || !hasSubcommand(entry.getValue().getCommandSpec(),
                            args[index + 1])) {
                        return Stream.of(Pair.of(entry.getValue().getCommandSpec(), index));
                    } else {
                        return getPossibleCommands(entry.getValue().getCommandSpec(), args, index + 1);
                    }
                });
    }

    private static boolean hasSubcommand(CommandSpec spec, String subcommand) {
        return spec.subcommands().keySet().stream()
                .anyMatch(sub -> sub.equalsIgnoreCase(subcommand));
    }

}