package org.valkyrienskies.mod.common.command.config;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import lombok.SneakyThrows;
import mcp.MethodsReturnNonnullByDefault;
import net.minecraft.command.CommandBase;
import net.minecraft.command.ICommandSender;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.text.TextComponentString;
import org.valkyrienskies.mod.common.command.framework.VSCommandUtil;

/**
 * This class is used to generate a command for a VS/Forge config. It's used by
 * <code>/vsconfig</code>
 */
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
public class VSConfigCommandBase extends CommandBase {

    private String name;

    private ConfigCommandParentNode root;
    private Method sync;

    private List<String> aliases;

    /**
     * @param name        This is the name of the command that's going to be generated
     * @param configClass This is the class of the config that's going to have a command generated
     *                    for it. It should define a public static void sync().
     * @param aliases     These are the alternative names for the command that's going to be
     *                    generated
     */
    public VSConfigCommandBase(String name, Class<?> configClass, String... aliases) {
        try {
            this.sync = configClass.getMethod("sync");

            if (!Modifier.isStatic(this.sync.getModifiers())) {
                throw new IllegalArgumentException(
                    "That class does not have a public static sync method on it!");
            }
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException(
                "That class does not have a public static sync method on it!", e);
        }

        this.name = name;
        this.aliases = Arrays.asList(aliases);
        root = new ConfigCommandParentNode(name, Collections.emptyMap());

        processFields(configClass, root);
    }

    @Override
    public List<String> getAliases() {
        return this.aliases;
    }

    // TODO: allow usage of arrays
    private static void processFields(Class<?> configClass, ConfigCommandParentNode root) {
        List<Class<?>> subcategories = Arrays.asList(configClass.getDeclaredClasses());

        for (Field field : configClass.getFields()) {
            // Ensure the field is public static and supported
            if (Modifier.isStatic(field.getModifiers()) &&
                Modifier.isPublic(field.getModifiers())) {

                if (subcategories.contains(field.getType())) {
                    // If the field is the instance for a subcategory
                    processFieldForSubcategory(field.getType(), field, root);
                } else if (ConfigCommandUtils.isSupportedType(field.getType())) {
                    // Or its a normal field
                    root.addChild(new ConfigCommandEndNode(field.getName(),
                        str -> ConfigCommandUtils.setFieldFromString(str, field),
                        () -> ConfigCommandUtils.getStringFromField(field)));
                } // Ignore fields that aren't supported or a subcategory
            }
        }
    }

    // TODO: allow usage of arrays
    @SneakyThrows(IllegalAccessException.class)
    private static void processFieldForSubcategory(Class<?> subcategory, Field subcatField,
        ConfigCommandParentNode root) {
        // Note: subcatField should always be static
        Object subcategoryObj = subcatField.get(null);

        ShortName subcatShortName = subcatField.getAnnotation(ShortName.class);

        String subcatDisplayName = subcatShortName == null ?
            subcatField.getName() : subcatShortName.value();

        ConfigCommandParentNode subcategoryNode = new ConfigCommandParentNode(subcatDisplayName);
        root.addChild(subcategoryNode);

        for (Field field : subcategory.getFields()) {
            ShortName fieldShortName = field.getAnnotation(ShortName.class);
            String fieldDisplayName = fieldShortName == null ?
                field.getName() : fieldShortName.value();

            // Ensure field is public NOT static and supported
            if (!Modifier.isStatic(field.getModifiers()) &&
                Modifier.isPublic(field.getModifiers()) &&
                ConfigCommandUtils.isSupportedType(field.getType())) {

                subcategoryNode.addChild(new ConfigCommandEndNode(fieldDisplayName,
                    str -> ConfigCommandUtils.setFieldFromString(str, field, subcategoryObj),
                    () -> ConfigCommandUtils.getStringFromField(field, subcategoryObj)));
            }
        }
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getUsage(ICommandSender sender) {
        return String.format("/%s <option>", name);
    }

    @Override
    public void execute(MinecraftServer server, ICommandSender sender, String[] args) {
        args = VSCommandUtil.toProperArgs(args);

        ConfigCommandNode currentNode = root;

        for (int i = 0; i < args.length; i++) {
            if (currentNode instanceof ConfigCommandParentNode) {
                currentNode = ((ConfigCommandParentNode) currentNode).getChild(args[i]);
            }

            if (currentNode instanceof ConfigCommandEndNode && i < args.length - 1) {
                // setting the option
                ((ConfigCommandEndNode) currentNode).getOptionSetter().accept(args[i + 1]);
                try {
                    sync.invoke(null);
                } catch (Exception e) {
                    throw new RuntimeException(e); //blah blaah
                }
                sender.sendMessage(new TextComponentString("Set " + currentNode.getName() +
                    " = " + ((ConfigCommandEndNode) currentNode).getOptionGetter().get().toString()
                ));

                break;
            } else if (currentNode instanceof ConfigCommandEndNode && i == args.length - 1) {
                // getting the option
                sender.sendMessage(new TextComponentString(currentNode.getName() +
                    " = " + ((ConfigCommandEndNode) currentNode).getOptionGetter().get().toString()
                ));
                break;
            } else if (i == args.length - 1) {
                sender.sendMessage(new TextComponentString(
                    "That is a subcategory, please specify additional fields"));
            }

            if (currentNode == null) {
                sender.sendMessage(new TextComponentString(
                    String.format("Unrecognized option: %s", args[i])
                ));
            }
        }
    }

    // TODO autocomplete boolean values, enums, autocompletion annotation
    @Override
    public List<String> getTabCompletions(MinecraftServer server, ICommandSender sender,
        String[] args, @Nullable BlockPos targetPos) {

        args = VSCommandUtil.toProperArgs(args);

        ConfigCommandNode currentNode = root;

        if (args.length == 0) {
            return root.childrenNames();
        }

        for (int i = 0; i < args.length; i++) {
            System.out.println(args[i]);
            if (currentNode instanceof ConfigCommandParentNode) {
                ConfigCommandNode nextNode =
                    ((ConfigCommandParentNode) currentNode).getChild(args[i]);

                if (nextNode == null) {
                    return ((ConfigCommandParentNode) currentNode).getChildrenStartingWith(args[i])
                        .stream()
                        .map(ConfigCommandNode::getName)
                        .collect(Collectors.toList());
                } else if (i == args.length - 1 && nextNode instanceof ConfigCommandParentNode) {
                    // We have reached the last argument, so the user must be looking for all the
                    // values of this subcategory

                    return ((ConfigCommandParentNode) nextNode).childrenNames();
                } else {
                    currentNode = nextNode;
                }
            }
        }

        return Collections.emptyList();
    }
}