package eu.midnightdust.yaytris.util;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.nio.file.Files;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/*
 NightJson v0.2 by Martin Prokoph
 Extremely lightweight (and incomplete) JSON library
 Concept inspired by GSON
 */
public class NightJson {
    private static final String KEY_PATTERN = "\"(-?[A-Za-z-_.]*)\":";
    Class<?> jsonClass;
    Field jsonMap;
    String fileLocation;

    public NightJson(Class<?> jsonClass, String fileLocation) {
        this.jsonClass = jsonClass;
        this.fileLocation = fileLocation;
        getField("jsonMap").ifPresent(f -> {
            if (f.getType() == Map.class && getTypeArgument(f, 0) == String.class) jsonMap = f;
        });
    }

    @SuppressWarnings("unchecked")
    public void writeJson() {
        if (fileLocation == null) return;
        try {
            FileWriter jsonFile = new FileWriter(fileLocation);

            jsonFile.write("{\n");
            Iterator<Field> it = Arrays.stream(jsonClass.getFields()).iterator();
            while (it.hasNext()) {
                Field field = it.next();
                if (field != jsonMap) writeElement(jsonFile, field.get(null), field.getType(), field.getName(), it.hasNext());
            }
            if (jsonMap != null) {
                Iterator<String> mapIt = ((Map<String,?>)jsonMap.get(null)).keySet().iterator();
                while (mapIt.hasNext()) {
                    String key = mapIt.next();
                    Object value = jsonMap.get(key);
                    writeElement(jsonFile, value, value.getClass(), key, mapIt.hasNext());
                }
            }
            jsonFile.write("}");
            jsonFile.close();
        } catch (IOException | IllegalAccessException e) {
            System.out.println("Oh no! An Error occurred whilst writing the JSON file :(");
            //noinspection CallToPrintStackTrace
            e.printStackTrace();
        }
    }

    private void writeElement(FileWriter jsonFile, Object value, Class<?> type, String name, boolean hasNext) throws IOException, IllegalAccessException {
        jsonFile.write("\t");
        if (type == Comment.class) {
            jsonFile.write(String.format("// %s\n", ((Comment) value).commentString));
            return;
        }
        jsonFile.write(String.format("\"%s\": ", name));
        jsonFile.write(objToString(value, type));
        jsonFile.write(hasNext ? ",\n" : "\n");
    }

    private String objToString(Object value, Class<?> type) {
        if (type == Map.class) {
            StringBuilder mapPairs = new StringBuilder();
            Map<?, ?> map = (Map<?, ?>) value;
            Iterator<?> it = map.keySet().iterator();
            if (it.hasNext()) mapPairs.append("{");
            while (it.hasNext()) {
                Object key = it.next();
                Object val = map.get(key);
                mapPairs.append("\n\t\t");
                mapPairs.append(String.format("%s: %s", objToString(key, key.getClass()), objToString(val, val.getClass())));
                mapPairs.append(it.hasNext() ? "," : "\n\t}");
            }
            return mapPairs.toString();
        }
        return String.format(type == String.class || type.isEnum() ? "\"%s\"" : "%s", value);
    }

    @SuppressWarnings("unchecked")
    public void readJson() {
        if (fileLocation == null) return;
        try {
            File file = new File(fileLocation);
            if (!file.exists()) {
                writeJson();
                return;
            }

            Map<String, Object> asMap = jsonToMap(
                    Files.readString(file.toPath()).replaceAll("(//)+.*\n", ""), // Replace comment lines (Json5)
                    (key) -> getField(key).isPresent() ? getField(key).get().getType() : String.class); // Determine data type

            for (String key : asMap.keySet()) {
                Object value = asMap.get(key);
                Optional<Field> field = getField(key);
                if (field.isPresent()) {
                    field.get().set(null, value);
                }
                else if (jsonMap != null) {
                    ((Map<String, Object>)jsonMap.get(null)).put(key, value);
                }
            }
        } catch (IOException | IllegalAccessException | NoSuchElementException | ClassCastException e) {
            System.out.println("Oh no! An Error occurred whilst reading the JSON file :(");
            //noinspection CallToPrintStackTrace
            e.printStackTrace();
        }
    }

    private Map<String, Object> jsonToMap(String jsonString, Function<String, Class<?>> keyToType) {
        Map<String, Object> map = new HashMap<>();
        Iterator<String> pairIterator = Arrays.stream(jsonString.replaceAll("(//)+.*\n", "").replaceFirst("[{]", "").split(",")).iterator();
        while (pairIterator.hasNext()) {
            String s = pairIterator.next();

            Matcher matcher = Pattern.compile(KEY_PATTERN).matcher(s);
            if (matcher.find()) {
                String key = matcher.group().replaceAll("([\":])", "");
                map.put(key, getValue(s, key, keyToType, pairIterator));
            }
        }
        return map;
    }

    private Object getValue(String s, String key, Function<String, Class<?>> keyToType, Iterator<String> pairIterator) {
        String val = s.split(KEY_PATTERN, 2)[1];

        if (s.contains("{")) {
            StringBuilder submapString = new StringBuilder();
            int level = charAmount(s, '{');
            submapString.append(val);
            if (pairIterator.hasNext()) submapString.append(",");
            while (pairIterator.hasNext()) {
                String next = pairIterator.next();
                submapString.append(next);
                level += charAmount(next, '{');
                level -= charAmount(next, '}');
                if (level <= 0) break;
                if (pairIterator.hasNext()) submapString.append(",");
            }
            Optional<Field> field = getField(key);
            return jsonToMap(String.valueOf(submapString), k -> field.isPresent() ? getTypeArgument(field.get(), 1) : String.class);
        }
        else {
            while (val.startsWith(" ")) val = val.substring(1);
            val = val.replaceAll("[\"}\n]", "");
            while (val.endsWith(",") || val.endsWith("\n") || val.endsWith("\t")) val = val.substring(0, val.length() - 1);

            return stringToObj(val, keyToType.apply(key));
        }
    }

    private int charAmount(String input, char c) {
        return (int) input.chars().filter(ch -> ch == c).count();
    }

    private Object stringToObj(String value, Class<?> type) {
        switch (type.getName()) {
            case "byte": return Byte.parseByte(value);
            case "int": return Integer.parseInt(value);
            case "long": return Long.parseLong(value);
            case "float": return Float.parseFloat(value);
            case "double": return Double.parseDouble(value);
            case "boolean": return Boolean.parseBoolean(value);
        }
        if (type.isEnum()) return Arrays.stream(type.getEnumConstants())
                .filter(enumConstant -> Objects.equals(enumConstant.toString(), value)).findFirst().orElseThrow();
        else return value;
    }

    private static Class<?> getTypeArgument(Field field, int index) {
        return getPrimitiveType((Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[index]);
    }

    public static Class<?> getPrimitiveType(Class<?> rawType) {
        try { return (Class<?>) rawType.getField("TYPE").get(null); // Tries to get primitive types from non-primitives (e.g. Boolean -> boolean)
        } catch (NoSuchFieldException | IllegalAccessException ignored) { return rawType; }
    }

    private Optional<Field> getField(String name) {
        try {
            return Optional.of(jsonClass.getField(name));
        } catch (NoSuchFieldException e) {
            return Optional.empty();
        }
    }

    public static class Comment {
        final String commentString;
        public Comment(String commentString, Object... args) {
            this.commentString = String.format(commentString, args);
        }
    }
}
