/*
 * ~~~~~~licensing~~~~~~ base-support ========== Copyright (C) 2020 - 2023 EmDev LLC ========== You
 * may not use this file except in accordance with the License Terms of the Copyright Holder located
 * at: https://entaxy.ru/eula . All copyrights, all intellectual property rights to the Software and
 * any copies are the property of the Copyright Holder. Unless it is explicitly allowed the
 * Copyright Holder, the User is prohibited from using the Software for commercial purposes to
 * provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS". Under no
 * circumstances does the Copyright Holder guarantee or promise that the Software provided by him
 * will be suitable or not suitable for the specific purposes of the User, that the Software will
 * meet all commercial and personal subjective expectations of the User, that the Software will work
 * properly, without technical errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable to the User for
 * any direct or indirect losses of the User, his expenses or actual damage, including, downtime;
 * loss of bussines; lost profit; lost earnings; loss or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.platform.base.support;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;

public class JSONUtils {

    public static final Gson GSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
    public static final String PROP_VALUE = "_value";

    public static Map<String, Object> element2map(JsonElement element) {
        Map<String, Object> result = new HashMap<>();
        if (element.isJsonObject()) {
            JsonObject jsonObject = element.getAsJsonObject();
            for (Entry<String, JsonElement> entry : jsonObject.entrySet()) {
                result.put(entry.getKey(), element2object(entry.getValue()));
            }
        } else if (element.isJsonArray()) {
            JsonArray array = element.getAsJsonArray();
            result.put(PROP_VALUE, element2list(element));
        } else if (element.isJsonNull()) {
            result.put(PROP_VALUE, null);
        } else if (element.isJsonPrimitive()) {
            result.put(PROP_VALUE, element2value(element));
        }

        return result;
    }

    public static Object element2object(JsonElement element) {
        if (element.isJsonNull() || element.isJsonPrimitive())
            return element2value(element);
        if (element.isJsonArray())
            return element2list(element);
        if (element.isJsonObject())
            return element2map(element);
        return null;
    }

    public static Object element2value(JsonElement element) {
        Object result = null;
        if (element.isJsonNull() || !element.isJsonPrimitive())
            return result;
        try {
            JsonPrimitive primitive = element.getAsJsonPrimitive();
            if (primitive.isNumber())
                result = primitive.getAsNumber();
            else if (primitive.isBoolean())
                result = primitive.getAsBoolean();
            else
                result = primitive.getAsString();
        } catch (Exception e1) {
            try {
                result = element.getAsBoolean();
            } catch (Exception e2) {
                result = element.getAsString();
            }
        }
        return result;
    }

    public static List<Object> element2list(JsonElement element) {
        List<Object> result = new ArrayList<>();
        JsonArray array = element.getAsJsonArray();
        for (int i = 0; i < array.size(); i++)
            result.add(element2object(array.get(i)));
        return result;
    }

    public static String getJsonRootObjectString(URL url) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(
                url.openStream(), StandardCharsets.UTF_8))) {
            String data = reader
                    .lines()
                    .collect(Collectors.joining("\n"));
            return data;
        }

    }

    public static JsonObject getJsonRootObject(URL url) throws IOException {
        return JSONUtils.getJsonRootObject(getJsonRootObjectString(url));
    }

    public static JsonObject getJsonRootObject(String jsonData) {
        try {
            JsonElement je = (new JsonParser()).parse(jsonData);
            JsonObject root = je.getAsJsonObject();
            return root;
        } catch (Exception e) {
            return new JsonObject();
        }
    }

    public static JsonObject getJsonRootObjectUnsafe(String jsonData) throws Exception {
        JsonElement je = (new JsonParser()).parse(jsonData);
        JsonObject root = je.getAsJsonObject();
        return root;
    }

    public static void mergeObjects(JsonObject source, JsonObject target) {
        if (source == null)
            return;
        if (target == null)
            return;
        for (Entry<String, JsonElement> entry : source.entrySet()) {
            if (target.has(entry.getKey())) {
                if (entry.getValue().isJsonObject())
                    if (target.get(entry.getKey()).isJsonObject()) {
                        mergeObjects(entry.getValue().getAsJsonObject(), target.get(entry.getKey()).getAsJsonObject());
                        continue;
                    }
                target.remove(entry.getKey());
            }
            target.add(entry.getKey(), entry.getValue().deepCopy());
        }
    }

    public static boolean replaceValue(JsonObject origin, String path, JsonElement replacement) {
        return setValue(origin, path, replacement, false);
    }

    public static boolean setValue(JsonObject origin, String path, JsonElement replacement, boolean ifMissing) {
        return setValue(origin, path, replacement, ifMissing, false);
    }

    public static boolean setValue(JsonObject origin, String path, JsonElement replacement, boolean ifMissing,
            boolean createFragments) {
        String separator = path.indexOf('/') >= 0
                ? "/"
                : ".";
        String preparedPath = path.replaceAll("(\\[\\d+\\])", separator + "$1");

        List<String> fragmentsList = new ArrayList<>();
        int sepIndex = preparedPath.indexOf(separator);
        while (sepIndex >= 0) {
            if (sepIndex == 0) {
                preparedPath = preparedPath.substring(separator.length());
                continue;
            }
            fragmentsList.add(preparedPath.substring(0, sepIndex));
            preparedPath = preparedPath.substring(sepIndex + separator.length());
            sepIndex = preparedPath.indexOf(separator);
        }
        if (CommonUtils.isValid(preparedPath))
            fragmentsList.add(preparedPath);

        String[] pathSplitted = fragmentsList.toArray(new String[] {});
        // String[] pathSplitted = preparedPath.split("\\\\" + separator);
        JsonElement currentElement = origin;
        JsonElement previousElement = null;
        for (int i = 0; i < pathSplitted.length - 1; i++) {
            String fragment = pathSplitted[i];
            if (fragment.startsWith("[") && fragment.endsWith("]")) {
                // array index
                if (currentElement.isJsonArray()) {
                    int index = Integer.parseInt(fragment.substring(1, fragment.length() - 1));
                    JsonArray arr = currentElement.getAsJsonArray();
                    if (arr.size() > index)
                        currentElement = arr.get(index);
                    else {
                        // TODO process
                        // System.out.println("ERROR: index out of bounds");
                    }
                } else {
                    // TODO process
                    // System.out.println("ERROR: found indexed property on non-array value");
                }
            } else if (currentElement.isJsonObject()) {
                previousElement = currentElement;
                currentElement = currentElement.getAsJsonObject().get(fragment);
                if (currentElement == null) {
                    if (!createFragments)
                        break;
                    if (i == pathSplitted.length - 2) {
                        // it's last fragment -> create object
                        currentElement = new JsonObject();
                    } else {
                        // check the next element
                        String nextFragment = pathSplitted[i + 1];
                        if (!CommonUtils.isValid(nextFragment)) {
                            currentElement = new JsonObject();
                        } else {
                            if (nextFragment.startsWith("[") && nextFragment.endsWith("]")) {
                                // next element is array index, so we create array
                                currentElement = new JsonObject();
                            } else {
                                currentElement = new JsonObject();
                            }
                        }
                    }

                    previousElement.getAsJsonObject().add(fragment, currentElement);
                }
            } else {
                // TODO process
                // System.out.println("ERROR: currentElement can't be traversed");
            }
        }
        if (currentElement == null) {
            // System.out.println("ERROR: currentElement is null");
            return false;
        }
        String finalFragment = pathSplitted[pathSplitted.length - 1];
        if (finalFragment.startsWith("[") && finalFragment.endsWith("]")) {
            // array index
            if (currentElement.isJsonArray()) {
                // System.out.println("INDEX: [" + finalFragment.substring(1,
                // finalFragment.length()-1) + "]");
                int index = Integer.parseInt(finalFragment.substring(1, finalFragment.length() - 1));
                JsonArray arr = currentElement.getAsJsonArray();
                if (arr.size() > index) {
                    // arr.remove(index);
                    arr.set(index, replacement);
                } else {
                    // TODO process
                    // System.out.println("ERROR: index out of bounds");
                }
            } else {
                // TODO process
                // System.out.println("ERROR: found indexed property on non-array value");
            }
        } else if (currentElement.isJsonObject()) {
            if (ifMissing && currentElement.getAsJsonObject().has(finalFragment))
                return false;
            currentElement.getAsJsonObject().remove(finalFragment);
            currentElement.getAsJsonObject().add(finalFragment, replacement);
        } else {
            // System.out.println("ERROR: currentElement can't be traversed");
            return false;
        }

        // System.out.println("\n -- found --\n" + currentElement.toString() + "\n");
        return true;
    }

    @Deprecated(forRemoval = true, since = "1.9")
    public static JsonElement findElement(JsonObject jsonObject, String pathFragment) {
        JsonElement result = findElement(jsonObject, pathFragment, "");
        return result;
    }

    public static JsonElement findElementExt(JsonObject jsonObject, String pathFragment) {
        JsonElement result = null;
        if (pathFragment.startsWith("?.")) {
            result = findElement(jsonObject, pathFragment.substring(2), "");
        } else {
            result = getElement(jsonObject, pathFragment);
        }

        return result;

    }

    public static JsonElement findElement(JsonObject jsonObject, String pathFragment, String parentPath) {

        for (Entry<String, JsonElement> entry : jsonObject.entrySet()) {

            String currentPath = parentPath + "." + entry.getKey();
            if (currentPath.endsWith(pathFragment))
                return entry.getValue();

        }

        for (Entry<String, JsonElement> entry : jsonObject.entrySet()) {

            if (!entry.getValue().isJsonObject())
                continue;

            String currentPath = parentPath + "." + entry.getKey();

            JsonElement currentResult = findElement(entry.getValue().getAsJsonObject(), pathFragment, currentPath);

            if (currentResult != null)
                return currentResult;
        }


        return null;
    }

    public static JsonElement getElement(JsonObject jsonObject, String pathFragment) {

        int ind = 0;
        String currentFragment = "";
        String newFragment = null;

        if (pathFragment.startsWith("'")) {
            int closing = pathFragment.indexOf("'", 1);
            if (closing > 1) {
                currentFragment = pathFragment.substring(1, closing);
                ind = pathFragment.indexOf('.', closing);
                if (ind > 0) {
                    newFragment = pathFragment.substring(ind + 1);
                } else {
                    newFragment = null;
                }
            }
        } else {
            ind = pathFragment.indexOf(".");
            if (ind > 0) {
                currentFragment = pathFragment.substring(0, ind);
                newFragment = pathFragment.substring(ind + 1);
            } else {
                currentFragment = pathFragment;
                newFragment = null;
            }
        }

        if (!jsonObject.has(currentFragment))
            return null;
        JsonElement je = jsonObject.get(currentFragment);
        if (newFragment == null)
            return je;
        if (!je.isJsonObject())
            return null;
        return getElement(je.getAsJsonObject(), newFragment);
    }

    public static class JsonTraverse {

        List<ObjectChecker> checkers = new ArrayList<>();

        public JsonTraverse checker(ObjectChecker checker) {
            this.checkers.add(checker);
            return this;
        }

        public Object traverse(JsonObject rootObject) {
            Object result = null;

            Map<String, Object> context = new HashMap<>();

            result = element2object(rootObject, context, "$");

            return result;
        }

        protected Object element2object(JsonElement element, Map<String, Object> context, String path) {
            if (element.isJsonNull() || element.isJsonPrimitive())
                return element2value(element, context, path);
            if (element.isJsonArray())
                return element2list(element, context, path);
            if (element.isJsonObject()) {
                JsonObject jsonObject = element.getAsJsonObject();
                ObjectWrapper ow = null;
                for (ObjectChecker oc : checkers) {
                    ow = oc.checkObject(jsonObject);
                    if (ow != null)
                        break;
                }

                if (ow != null) {
                    ow.wrap(jsonObject, context, path);
                    if (ow.continueTraverse())
                        ow.setTraverseMap(element2map(element, context, path), context);
                    return ow;
                }

                return element2map(element, context, path);
            }
            return null;
        }

        protected Map<String, Object> element2map(JsonElement element, Map<String, Object> context, String path) {
            Map<String, Object> result = new HashMap<>();
            if (element.isJsonObject()) {
                JsonObject jsonObject = element.getAsJsonObject();
                for (Entry<String, JsonElement> entry : jsonObject.entrySet()) {
                    result.put(entry.getKey(), element2object(entry.getValue(), context, path + "." + entry.getKey()));
                }
            } else if (element.isJsonArray()) {
                JsonArray array = element.getAsJsonArray();
                result.put(PROP_VALUE, element2list(element, context, path));
            } else if (element.isJsonNull()) {
                result.put(PROP_VALUE, null);
            } else if (element.isJsonPrimitive()) {
                result.put(PROP_VALUE, element2value(element, context, path));
            }

            return result;
        }

        protected Object element2value(JsonElement element, Map<String, Object> context, String path) {
            Object result = null;
            if (element.isJsonNull() || !element.isJsonPrimitive())
                return result;
            try {
                JsonPrimitive primitive = element.getAsJsonPrimitive();
                if (primitive.isNumber())
                    result = primitive.getAsNumber();
                else if (primitive.isBoolean())
                    result = primitive.getAsBoolean();
                else
                    result = primitive.getAsString();
            } catch (Exception e1) {
                try {
                    result = element.getAsBoolean();
                } catch (Exception e2) {
                    result = element.getAsString();
                }
            }
            return result;
        }

        protected List<Object> element2list(JsonElement element, Map<String, Object> context, String path) {
            List<Object> result = new ArrayList<>();
            JsonArray array = element.getAsJsonArray();
            for (int i = 0; i < array.size(); i++)
                result.add(element2object(array.get(i), context, path + "[" + i + "]"));
            return result;
        }

    }

    public static abstract class ObjectChecker {

        public abstract ObjectWrapper checkObject(JsonObject object);

    }

    public static abstract class ObjectWrapper implements Map<String, Object> {

        protected Map<String, Object> data = new HashMap<>();

        public abstract void wrap(JsonObject object, Map<String, Object> context, String path);

        public boolean continueTraverse() {
            return true;
        }

        public void setTraverseMap(Map<String, Object> traverseMap, Map<String, Object> context) {
            this.data = traverseMap;
        }

        @Override
        public int size() {
            return this.data.size();
        }

        @Override
        public boolean isEmpty() {
            return this.data.isEmpty();
        }

        @Override
        public boolean containsKey(Object key) {
            return this.data.containsKey(key);
        }

        @Override
        public boolean containsValue(Object value) {
            return this.data.containsValue(value);
        }

        @Override
        public Object get(Object key) {
            return this.data.get(key);
        }

        @Override
        public Object put(String key, Object value) {
            return this.data.put(key, value);
        }

        @Override
        public Object remove(Object key) {
            return this.data.remove(key);
        }

        @Override
        public void putAll(Map<? extends String, ? extends Object> m) {
            this.data.putAll(m);
        }

        @Override
        public void clear() {
            this.data.clear();
        }

        @Override
        public Set<String> keySet() {
            return this.data.keySet();
        }

        @Override
        public Collection<Object> values() {
            return this.data.values();
        }

        @Override
        public Set<Entry<String, Object>> entrySet() {
            return this.data.entrySet();
        };

    }

    public static class JsonBuilder {

        public static JsonObjectBuilder create(JsonObject jsonObject) {
            return new JsonObjectBuilder(jsonObject);
        }

        protected static abstract class CommonObjectBuilder<T extends CommonObjectBuilder<T>> {

            protected JsonObject currentObject;

            protected CommonObjectBuilder(JsonObject jsonObject) {
                this.currentObject = jsonObject;
            }

            protected T me() {
                return (T) this;
            }

            public T string(String name, String value) {
                return this.string(name, value, true);
            }

            public T string(String name, String value, boolean replace) {
                JSONUtils.setValue(currentObject, name, new JsonPrimitive(value), replace);
                return me();
            }

            public T bool(String name, boolean value) {
                return this.bool(name, value, true);
            }

            public T bool(String name, boolean value, boolean replace) {
                JSONUtils.setValue(currentObject, name, new JsonPrimitive(value), replace);
                return me();
            }

            public T number(String name, Number value) {
                return this.number(name, value, true);
            }

            public T number(String name, Number value, boolean replace) {
                JSONUtils.setValue(currentObject, name, new JsonPrimitive(value), replace);
                return me();
            }

            public JsonArrayBuilder<T> array(String name) {
                return this.array(name, true);
            }

            public JsonArrayBuilder<T> array(String name, boolean replace) {
                if (replace)
                    this.currentObject.remove(name);
                if (!this.currentObject.has(name)) {
                    this.currentObject.add(name, new JsonArray());
                }
                JsonElement je = this.currentObject.get(name);
                if (!je.isJsonArray()) {
                    this.currentObject.remove(name);
                    this.currentObject.add(name, new JsonArray());
                }

                return new JsonArrayBuilder<T>(this.currentObject.get(name).getAsJsonArray(), me());
            }

            protected JsonObject getCreateobject(String name, boolean replace) {
                if (replace)
                    this.currentObject.remove(name);
                if (!this.currentObject.has(name)) {
                    this.currentObject.add(name, new JsonObject());
                }
                JsonElement je = this.currentObject.get(name);
                if (!je.isJsonObject()) {
                    this.currentObject.remove(name);
                    this.currentObject.add(name, new JsonObject());
                }

                return this.currentObject.get(name).getAsJsonObject();
            };


        }

        public static class JsonObjectBuilder extends CommonObjectBuilder<JsonObjectBuilder> {

            protected JsonObjectBuilder parentBuilder = null;

            protected JsonObjectBuilder(JsonObject jsonObject) {
                super(jsonObject);
            }

            protected JsonObjectBuilder(JsonObject jsonObject, JsonObjectBuilder parent) {
                this(jsonObject);
                this.parentBuilder = parent;
            }

            public JsonObjectBuilder object(String name) {
                return this.object(name, true);
            }

            public JsonObjectBuilder object(String name, boolean replace) {

                return new JsonObjectBuilder(getCreateobject(name, replace), this);
            }

            public JsonObjectBuilder up() {
                return this.parentBuilder;
            }

        }

        public static class JsonObjectInArrayBuilder<T extends AbstractJsonArrayBuilder<T>>
                extends CommonObjectBuilder<JsonObjectInArrayBuilder<T>> {

            protected T parentBuilder = null;

            protected JsonObjectInArrayBuilder(JsonObject jsonObject, T parent) {
                super(jsonObject);
                this.parentBuilder = parent;
            }

            public JsonObjectBuilder object(String name) {
                return this.object(name, true);
            }

            public JsonObjectBuilder object(String name, boolean replace) {

                return new JsonObjectBuilder(getCreateobject(name, replace));
            }

            public T up() {
                return this.parentBuilder;
            }

        }

        protected static class AbstractJsonArrayBuilder<T extends AbstractJsonArrayBuilder<T>> {

            JsonArray currentArray;

            protected AbstractJsonArrayBuilder(JsonArray jsonArray) {
                this.currentArray = jsonArray;
            }

            protected T me() {
                return (T) this;
            }

            public T clear() {
                for (int i = 0; i < this.currentArray.size(); i++)
                    this.currentArray.remove(0);
                return me();
            }

            public T string(String value) {
                this.currentArray.add(value);
                return me();
            }

            public T bool(boolean value) {
                this.currentArray.add(value);
                return me();
            }

            public T number(Number value) {
                this.currentArray.add(value);
                return me();
            }

            public JsonObjectInArrayBuilder<T> object() {
                JsonObject jo = new JsonObject();
                this.currentArray.add(jo);
                return new JsonObjectInArrayBuilder<>(jo, me());
            }

        }

        public static class JsonArrayBuilder<T extends CommonObjectBuilder<T>>
                extends AbstractJsonArrayBuilder<JsonArrayBuilder<T>> {

            T parentBuilder;

            protected JsonArrayBuilder(JsonArray jsonArray, T parent) {
                super(jsonArray);
                this.parentBuilder = parent;
            }

            public JsonArrayInArrayBuilder<JsonArrayBuilder<T>> array() {
                JsonArray ja = new JsonArray();
                this.currentArray.add(ja);
                return new JsonArrayInArrayBuilder<JsonArrayBuilder<T>>(ja, this);
            }

            public T up() {
                return parentBuilder;
            }
        }

        public static class JsonArrayInArrayBuilder<T extends AbstractJsonArrayBuilder<T>>
                extends AbstractJsonArrayBuilder<JsonArrayInArrayBuilder<T>> {

            T parentBuilder;

            protected JsonArrayInArrayBuilder(JsonArray jsonArray, T parent) {
                super(jsonArray);
                this.parentBuilder = parent;
            }

            public JsonArrayInArrayBuilder<JsonArrayInArrayBuilder<T>> array() {
                JsonArray ja = new JsonArray();
                this.currentArray.add(ja);
                return new JsonArrayInArrayBuilder<JsonArrayInArrayBuilder<T>>(ja, this);
            }

            public T up() {
                return parentBuilder;
            }
        }
    }
}
