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.util.*;
import java.util.concurrent.atomic.AtomicReference;

/*
 NightJson v0.1 by Martin Prokoph
 Extremely lightweight (and incomplete) JSON library
 Concept inspired by GSON
 */
public class NightJson {
    private static final String KEY_PATTERN = "\"(-?[0-9a-zA-Z]*)\":";
    Class<?> jsonClass;
    String fileLocation;

    public NightJson(Class<?> jsonClass, String fileLocation) {
        this.jsonClass = jsonClass;
        this.fileLocation = fileLocation;
    }

    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();
                jsonFile.write("\t");
                if (field.getType() == Comment.class) {
                    jsonFile.write(String.format("// %s\n", ((Comment) field.get(null)).commentString));
                    continue;
                }
                jsonFile.write(String.format("\"%s\": ", field.getName()));
                jsonFile.write(objToString(field.get(null), field.getType()));
                jsonFile.write(it.hasNext() ? ",\n" : "\n");
            }
            jsonFile.write("}");
            jsonFile.close();
        } catch (IOException | IllegalAccessException e) {
            System.out.println("Oh no! An Error occurred whilst writing the JSON file :(");
            e.fillInStackTrace();
        }
    }

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

            Scanner jsonFile = new Scanner(file);
            Map<String, String> jsonKeyValuePairs = new HashMap<>();
            AtomicReference<String> lastKey = new AtomicReference<>();
            jsonFile.forEachRemaining(s -> {
                if (!s.matches("[{}]") && !s.matches("//+")) {
                    if (s.matches(KEY_PATTERN)) {
                        lastKey.set(s.replaceAll("([\":])", ""));
                        jsonKeyValuePairs.put(lastKey.get(), "");
                    }
                    else {
                        String val = s.replaceAll("(\")", "");
                        if (val.endsWith(",")) val = val.substring(0, val.length()-1);
                        jsonKeyValuePairs.put(lastKey.get(), (jsonKeyValuePairs.get(
                                lastKey.get()).isEmpty() ? "" : jsonKeyValuePairs.get(lastKey.get()) + " "
                        ) + val);
                    }
                }
            });

            for (String key : jsonKeyValuePairs.keySet()) {
                String currentString = jsonKeyValuePairs.get(key);
                //System.out.printf("Key: %s Value: %s%n", key, currentString);
                Field field;
                try { field = jsonClass.getField(key);
                } catch (NoSuchFieldException e) {continue;}

                field.set(field, stringToFieldObj(currentString, field));
            }
            jsonFile.close();
        } catch (IOException | IllegalAccessException | NoSuchElementException e) {
            System.out.println("Oh no! An Error occurred whilst reading the JSON file :(");
            e.fillInStackTrace();
        }
    }

    private String objToString(Object value, Class<?> type) throws IllegalAccessException {
        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(String.format("%s: %s", objToString(key, key.getClass()), objToString(val, val.getClass())));
                if (it.hasNext()) mapPairs.append(",");
                else mapPairs.append("}");
            }
            return mapPairs.toString();
        }
        return String.format(type == String.class || type.isEnum() ? "\"%s\"" : "%s", value);
    }

    private Object stringToFieldObj(String currentString, Field field) {
        if (field.getType() == Map.class) {
            Map<Object, Object> map = new HashMap<>();
            Iterator<String> it = Arrays.stream(currentString.substring(1, currentString.length()-1).split(",")).iterator();
            while (it.hasNext()) {
                String pair = it.next();
                System.out.println(pair);
                int semicolonPos = pair.indexOf(":");
                Class<?> keyType = getPrimitiveType((Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]);
                Class<?> valType = getPrimitiveType((Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[1]);
                map.put(stringToObj(pair.substring(0, semicolonPos), keyType),
                        stringToObj(pair.substring(semicolonPos+2), valType));
            }
            return map;
        }
        return stringToObj(currentString, field.getType());
    }

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

    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; }
    }

    public static class Comment {
        final String commentString;
        public Comment(String commentString) {
            this.commentString = commentString;
        }
    }
}
