/*
 *       _____  _       _    _____                                _
 *      |  __ \| |     | |  / ____|                              | |
 *      | |__) | | ___ | |_| (___   __ _ _   _  __ _ _ __ ___  __| |
 *      |  ___/| |/ _ \| __|\___ \ / _` | | | |/ _` | '__/ _ \/ _` |
 *      | |    | | (_) | |_ ____) | (_| | |_| | (_| | | |  __/ (_| |
 *      |_|    |_|\___/ \__|_____/ \__, |\__,_|\__,_|_|  \___|\__,_|
 *                                    | |
 *                                    |_|
 *            PlotSquared plot management system for Minecraft
 *                  Copyright (C) 2020 IntellectualSites
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.plotsquared.core.configuration;

import com.plotsquared.core.PlotSquared;
import com.plotsquared.core.configuration.Settings.Enabled_Components;
import com.plotsquared.core.configuration.file.YamlConfiguration;
import com.plotsquared.core.util.StringMan;

import java.io.File;
import java.io.PrintWriter;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Config {

    /**
     * Get the value for a node<br>
     * Probably throws some error if you try to get a non existent key
     *
     * @param key
     * @param root
     * @param <T>
     * @return
     */
    public static <T> T get(String key, Class<?> root) {
        String[] split = key.split("\\.");
        Object instance = getInstance(split, root);
        if (instance != null) {
            Field field = getField(split, instance);
            if (field != null) {
                try {
                    return (T) field.get(instance);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
        PlotSquared.debug("Failed to get config option: " + key);
        return null;
    }

    /**
     * Set the value of a specific node<br>
     * Probably throws some error if you supply non existing keys or invalid values
     *
     * @param key   config node
     * @param value value
     * @param root
     */
    public static void set(String key, Object value, Class<? extends Config> root) {
        String[] split = key.split("\\.");
        Object instance = getInstance(split, root);
        if (instance != null) {
            Field field = getField(split, instance);
            if (field != null) {
                try {
                    if (field.getAnnotation(Final.class) != null) {
                        return;
                    }
                    if (field.getType() == String.class && !(value instanceof String)) {
                        value = value + "";
                    }
                    field.set(instance, value);
                    return;
                } catch (Throwable e) {
                    PlotSquared.debug(
                        "Invalid configuration value: " + key + ": " + value + " in " + root
                            .getSimpleName());
                    e.printStackTrace();
                }
            }
        }
        PlotSquared.debug("Failed to set config option: " + key + ": " + value + " | " + instance);
    }

    public static boolean load(File file, Class<? extends Config> root) {
        if (!file.exists()) {
            return false;
        }
        YamlConfiguration yml = YamlConfiguration.loadConfiguration(file);
        for (String key : yml.getKeys(true)) {
            Object value = yml.get(key);
            if (value instanceof MemorySection) {
                continue;
            }
            set(key, value, root);
        }
        return true;
    }

    /**
     * Set all values in the file (load first to avoid overwriting)
     *
     * @param file
     * @param root
     */
    public static void save(File file, Class<? extends Config> root) {
        try {
            if (!file.exists()) {
                file.getParentFile().mkdirs();
                file.createNewFile();
            }
            try (PrintWriter writer = new PrintWriter(file)) {
                Object instance = root.newInstance();
                save(writer, root, instance, 0);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /**
     * Get the static fields in a section.
     *
     * @param clazz
     * @return
     */
    public static Map<String, Object> getFields(Class<Enabled_Components> clazz) {
        HashMap<String, Object> map = new HashMap<>();
        for (Field field : clazz.getFields()) {
            if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
                try {
                    map.put(toNodeName(field.getName()), field.get(null));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
        return map;
    }

    private static String toYamlString(Object value, String spacing) {
        if (value instanceof List) {
            Collection<?> listValue = (Collection<?>) value;
            if (listValue.isEmpty()) {
                return "[]";
            }
            StringBuilder m = new StringBuilder();
            for (Object obj : listValue) {
                m.append(System.lineSeparator() + spacing + "- " + toYamlString(obj, spacing));
            }
            return m.toString();
        }
        if (value instanceof String) {
            String stringValue = (String) value;
            if (stringValue.isEmpty()) {
                return "''";
            }
            return "\"" + stringValue + "\"";
        }
        return value != null ? value.toString() : "null";
    }

    private static void save(PrintWriter writer, Class clazz, Object instance, int indent) {
        try {
            String lineSeparator = System.lineSeparator();
            String spacing = StringMan.repeat(" ", indent);
            for (Field field : clazz.getFields()) {
                if (field.getAnnotation(Ignore.class) != null) {
                    continue;
                }
                Comment comment = field.getAnnotation(Comment.class);
                if (comment != null) {
                    for (String commentLine : comment.value()) {
                        writer.write(spacing + "# " + commentLine + lineSeparator);
                    }
                }
                Create create = field.getAnnotation(Create.class);
                if (create != null) {
                    Object value = field.get(instance);
                    if (value == null && field.getType() != ConfigBlock.class) {
                        setAccessible(field);
                        Class<?>[] classes = clazz.getDeclaredClasses();
                        for (Class current : classes) {
                            if (StringMan.isEqual(current.getSimpleName(), field.getName())) {
                                field.set(instance, current.newInstance());
                                break;
                            }
                        }
                    }
                } else {
                    writer.write(spacing + toNodeName(field.getName() + ": ") + toYamlString(
                        field.get(instance), spacing) + lineSeparator);
                }
            }
            for (Class<?> current : clazz.getClasses()) {
                if (current.isInterface() || current.getAnnotation(Ignore.class) != null) {
                    continue;
                }
                if (indent == 0) {
                    writer.write(lineSeparator);
                }
                Comment comment = current.getAnnotation(Comment.class);
                if (comment != null) {
                    for (String commentLine : comment.value()) {
                        writer.write(spacing + "# " + commentLine + lineSeparator);
                    }
                }
                writer.write(spacing + toNodeName(current.getSimpleName()) + ":" + lineSeparator);
                BlockName blockNames = current.getAnnotation(BlockName.class);
                if (blockNames != null) {
                    Field instanceField =
                        clazz.getDeclaredField(toFieldName(current.getSimpleName()));
                    setAccessible(instanceField);
                    ConfigBlock value = (ConfigBlock) instanceField.get(instance);
                    if (value == null) {
                        value = new ConfigBlock();
                        instanceField.set(instance, value);
                        for (String blockName : blockNames.value()) {
                            value.put(blockName, current.newInstance());
                        }
                    }
                    // Save each instance
                    for (Map.Entry<String, Object> entry : ((Map<String, Object>) value.getRaw())
                        .entrySet()) {
                        String key = entry.getKey();
                        writer.write(spacing + "  " + toNodeName(key) + ":" + lineSeparator);
                        save(writer, current, entry.getValue(), indent + 4);
                    }
                    continue;
                } else {
                    save(writer, current, current.newInstance(), indent + 2);
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /**
     * Get the field for a specific config node.
     *
     * @param split the node (split by period)
     * @param root
     * @return
     */
    private static Field getField(String[] split, Class<?> root) {
        Object instance = getInstance(split, root);
        if (instance == null) {
            return null;
        }
        return getField(split, instance);
    }

    /**
     * Get the field for a specific config node and instance<br>
     * Note: As expiry can have multiple blocks there will be multiple instances
     *
     * @param split    the node (split by period)
     * @param instance the instance
     * @return
     */
    private static Field getField(String[] split, Object instance) {
        try {
            Field field = instance.getClass().getField(toFieldName(split[split.length - 1]));
            setAccessible(field);
            return field;
        } catch (Throwable e) {
            PlotSquared.debug(
                "Invalid config field: " + StringMan.join(split, ".") + " for " + toNodeName(
                    instance.getClass().getSimpleName()));
            return null;
        }
    }

    /**
     * Get the instance for a specific config node.
     *
     * @param split the node (split by period)
     * @param root
     * @return The instance or null
     */
    private static Object getInstance(String[] split, Class<?> root) {
        try {
            Class<?> clazz = root == null ? MethodHandles.lookup().lookupClass() : root;
            Object instance = clazz.newInstance();
            while (split.length > 0) {
                if (split.length == 1) {
                    return instance;
                }
                Class<?> found = null;
                Class<?>[] classes = clazz.getDeclaredClasses();
                for (Class<?> current : classes) {
                    if (current.getSimpleName().equalsIgnoreCase(toFieldName(split[0]))) {
                        found = current;
                        break;
                    }
                }
                try {
                    Field instanceField = clazz.getDeclaredField(toFieldName(split[0]));
                    setAccessible(instanceField);
                    if (instanceField.getType() != ConfigBlock.class) {
                        Object value = instanceField.get(instance);
                        if (value == null) {
                            value = found.newInstance();
                            instanceField.set(instance, value);
                        }
                        clazz = found;
                        instance = value;
                        split = Arrays.copyOfRange(split, 1, split.length);
                        continue;
                    }
                    ConfigBlock value = (ConfigBlock) instanceField.get(instance);
                    if (value == null) {
                        value = new ConfigBlock();
                        instanceField.set(instance, value);
                    }
                    instance = value.get(split[1]);
                    if (instance == null) {
                        instance = found.newInstance();
                        value.put(split[1], instance);
                    }
                    clazz = found;
                    split = Arrays.copyOfRange(split, 2, split.length);
                    continue;
                } catch (NoSuchFieldException ignore) {
                }
                if (found != null) {
                    split = Arrays.copyOfRange(split, 1, split.length);
                    clazz = found;
                    instance = clazz.newInstance();
                    continue;
                }
                return null;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Translate a node to a java field name.
     *
     * @param node
     * @return
     */
    private static String toFieldName(String node) {
        return node.toUpperCase().replaceAll("-", "_");
    }

    /**
     * Translate a field to a config node.
     *
     * @param field
     * @return
     */
    private static String toNodeName(String field) {
        return field.toLowerCase().replace("_", "-");
    }

    /**
     * Set some field to be accessible.
     *
     * @param field
     */
    private static void setAccessible(Field field) {
        field.setAccessible(true);
    }

    /**
     * Indicates that a field should be instantiated / created.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD})
    public @interface Create {
    }


    /**
     * Indicates that a field cannot be modified.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD})
    public @interface Final {
    }


    /**
     * Creates a comment.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.TYPE})
    public @interface Comment {
        String[] value();
    }


    /**
     * The names of any default blocks.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.TYPE})
    public @interface BlockName {
        String[] value();
    }


    /**
     * Any field or class with is not part of the config.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.TYPE})
    public @interface Ignore {
    }


    @Ignore // This is not part of the config
    public static class ConfigBlock<T> {

        private HashMap<String, T> INSTANCES = new HashMap<>();

        public T get(String key) {
            return INSTANCES.get(key);
        }

        public void put(String key, T value) {
            INSTANCES.put(key, value);
        }

        public Collection<T> getInstances() {
            return INSTANCES.values();
        }

        public Collection<String> getSections() {
            return INSTANCES.keySet();
        }

        private Map<String, T> getRaw() {
            return INSTANCES;
        }
    }
}
