package net.minestom.server.command.builder.parser;

import com.google.common.annotations.Beta;
import net.minestom.server.command.builder.arguments.*;
import net.minestom.server.command.builder.arguments.minecraft.*;
import net.minestom.server.command.builder.arguments.minecraft.registry.*;
import net.minestom.server.command.builder.arguments.number.ArgumentDouble;
import net.minestom.server.command.builder.arguments.number.ArgumentFloat;
import net.minestom.server.command.builder.arguments.number.ArgumentInteger;
import net.minestom.server.command.builder.arguments.relative.ArgumentRelativeBlockPosition;
import net.minestom.server.command.builder.arguments.relative.ArgumentRelativeVec2;
import net.minestom.server.command.builder.arguments.relative.ArgumentRelativeVec3;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

public class ArgumentParser {

    private static final Map<String, Function<String, Argument<?>>> ARGUMENT_FUNCTION_MAP = new ConcurrentHashMap<>();

    static {
        ARGUMENT_FUNCTION_MAP.put("Literal", ArgumentLiteral::new);
        ARGUMENT_FUNCTION_MAP.put("Boolean", ArgumentBoolean::new);
        ARGUMENT_FUNCTION_MAP.put("Integer", ArgumentInteger::new);
        ARGUMENT_FUNCTION_MAP.put("Double", ArgumentDouble::new);
        ARGUMENT_FUNCTION_MAP.put("Float", ArgumentFloat::new);
        ARGUMENT_FUNCTION_MAP.put("String", ArgumentString::new);
        ARGUMENT_FUNCTION_MAP.put("Word", ArgumentWord::new);
        ARGUMENT_FUNCTION_MAP.put("StringArray", ArgumentStringArray::new);
        ARGUMENT_FUNCTION_MAP.put("Command", ArgumentCommand::new);
        // TODO enum
        ARGUMENT_FUNCTION_MAP.put("Color", ArgumentColor::new);
        ARGUMENT_FUNCTION_MAP.put("Time", ArgumentTime::new);
        ARGUMENT_FUNCTION_MAP.put("Enchantment", ArgumentEnchantment::new);
        ARGUMENT_FUNCTION_MAP.put("Particle", ArgumentParticle::new);
        ARGUMENT_FUNCTION_MAP.put("ResourceLocation", ArgumentResourceLocation::new);
        ARGUMENT_FUNCTION_MAP.put("Potion", ArgumentPotionEffect::new);
        ARGUMENT_FUNCTION_MAP.put("EntityType", ArgumentEntityType::new);
        ARGUMENT_FUNCTION_MAP.put("BlockState", ArgumentBlockState::new);
        ARGUMENT_FUNCTION_MAP.put("IntRange", ArgumentIntRange::new);
        ARGUMENT_FUNCTION_MAP.put("FloatRange", ArgumentFloatRange::new);

        ARGUMENT_FUNCTION_MAP.put("Entity", s -> new ArgumentEntity(s).singleEntity(true));
        ARGUMENT_FUNCTION_MAP.put("Entities", ArgumentEntity::new);
        ARGUMENT_FUNCTION_MAP.put("Player", s -> new ArgumentEntity(s).singleEntity(true).onlyPlayers(true));
        ARGUMENT_FUNCTION_MAP.put("Players", s -> new ArgumentEntity(s).onlyPlayers(true));

        ARGUMENT_FUNCTION_MAP.put("ItemStack", ArgumentItemStack::new);
        ARGUMENT_FUNCTION_MAP.put("Component", ArgumentComponent::new);
        ARGUMENT_FUNCTION_MAP.put("UUID", ArgumentUUID::new);
        ARGUMENT_FUNCTION_MAP.put("NBT", ArgumentNbtTag::new);
        ARGUMENT_FUNCTION_MAP.put("NbtCompound", ArgumentNbtCompoundTag::new);
        ARGUMENT_FUNCTION_MAP.put("RelativeBlockPosition", ArgumentRelativeBlockPosition::new);
        ARGUMENT_FUNCTION_MAP.put("RelativeVec3", ArgumentRelativeVec3::new);
        ARGUMENT_FUNCTION_MAP.put("RelativeVec2", ArgumentRelativeVec2::new);
    }

    @Beta
    @NotNull
    public static Argument<?>[] generate(@NotNull String format) {
        List<Argument<?>> result = new ArrayList<>();

        // 0 = no state
        // 1 = inside angle bracket <>
        int state = 0;
        // function to create an argument from its identifier
        // not null during state 1
        Function<String, Argument<?>> argumentFunction = null;

        StringBuilder builder = new StringBuilder();

        // test: Integer<name> String<hey>
        for (int i = 0; i < format.length(); i++) {
            char c = format.charAt(i);

            // No state
            if (state == 0) {
                if (c == ' ')
                    continue;

                if (c == '<') {
                    // Retrieve argument type
                    final String argument = builder.toString();
                    argumentFunction = ARGUMENT_FUNCTION_MAP.get(argument);
                    if (argumentFunction == null) {
                        throw new IllegalArgumentException("error invalid argument name: " + argument);
                    }

                    builder = new StringBuilder();
                    state = 1;
                } else {
                    // Append to builder
                    builder.append(c);
                }

                continue;
            }

            // Inside bracket <>
            if (state == 1) {
                if (c == '>') {
                    final String param = builder.toString();
                    // TODO argument options
                    Argument<?> argument = argumentFunction.apply(param);
                    result.add(argument);

                    builder = new StringBuilder();
                    state = 0;
                } else {
                    builder.append(c);
                }

                continue;
            }

        }

        return result.toArray(Argument[]::new);
    }

    @Nullable
    public static ArgumentResult validate(@NotNull Argument<?> argument,
                                          @NotNull Argument<?>[] arguments, int argIndex,
                                          @NotNull String[] inputArguments, int inputIndex) {
        final boolean end = inputIndex == inputArguments.length;
        if (end) // Stop if there is no input to analyze left
            return null;

        // the parsed argument value, null if incorrect
        Object parsedValue = null;
        // the argument exception, null if the input is correct
        ArgumentSyntaxException argumentSyntaxException = null;
        // true if the arg is valid, false otherwise
        boolean correct = false;
        // The raw string value of the argument
        String rawArg = null;

        if (argument.useRemaining()) {
            final boolean hasArgs = inputArguments.length > inputIndex;
            // Verify if there is any string part available
            if (hasArgs) {
                StringBuilder builder = new StringBuilder();
                // Argument is supposed to take the rest of the command input
                for (int i = inputIndex; i < inputArguments.length; i++) {
                    final String arg = inputArguments[i];
                    if (builder.length() > 0)
                        builder.append(StringUtils.SPACE);
                    builder.append(arg);
                }

                rawArg = builder.toString();

                try {
                    parsedValue = argument.parse(rawArg);
                    correct = true;
                } catch (ArgumentSyntaxException exception) {
                    argumentSyntaxException = exception;
                }
            }
        } else {
            // Argument is either single-word or can accept optional delimited space(s)
            StringBuilder builder = new StringBuilder();
            for (int i = inputIndex; i < inputArguments.length; i++) {
                builder.append(inputArguments[i]);

                rawArg = builder.toString();

                try {
                    parsedValue = argument.parse(rawArg);

                    // Prevent quitting the parsing too soon if the argument
                    // does not allow space
                    final boolean lastArgumentIteration = argIndex + 1 == arguments.length;
                    if (lastArgumentIteration && i + 1 < inputArguments.length) {
                        if (!argument.allowSpace())
                            break;
                        builder.append(StringUtils.SPACE);
                        continue;
                    }

                    correct = true;

                    inputIndex = i + 1;
                    break;
                } catch (ArgumentSyntaxException exception) {
                    argumentSyntaxException = exception;

                    if (!argument.allowSpace()) {
                        // rawArg should be the remaining
                        for (int j = i + 1; j < inputArguments.length; j++) {
                            final String arg = inputArguments[j];
                            if (builder.length() > 0)
                                builder.append(StringUtils.SPACE);
                            builder.append(arg);
                        }
                        rawArg = builder.toString();
                        break;
                    }
                    builder.append(StringUtils.SPACE);
                }
            }
        }

        ArgumentResult argumentResult = new ArgumentResult();
        argumentResult.argument = argument;
        argumentResult.correct = correct;
        argumentResult.inputIndex = inputIndex;
        argumentResult.argumentSyntaxException = argumentSyntaxException;

        argumentResult.useRemaining = argument.useRemaining();

        argumentResult.rawArg = rawArg;

        argumentResult.parsedValue = parsedValue;
        return argumentResult;
    }

    public static class ArgumentResult {
        public Argument<?> argument;
        public boolean correct;
        public int inputIndex;
        public ArgumentSyntaxException argumentSyntaxException;

        public boolean useRemaining;

        public String rawArg;

        // If correct
        public Object parsedValue;
    }

}
