package org.openlca.app.cloud.ui.compare.json;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;

public class JsonUtil {

	public static JsonObject toJsonObject(JsonElement element) {
		if (element == null)
			return null;
		if (!element.isJsonObject())
			return null;
		return element.getAsJsonObject();
	}

	public static JsonArray toJsonArray(JsonElement element) {
		if (element == null)
			return null;
		if (!element.isJsonArray())
			return null;
		return element.getAsJsonArray();
	}

	public static JsonPrimitive toJsonPrimitive(JsonElement element) {
		if (element == null)
			return null;
		if (!element.isJsonPrimitive())
			return null;
		return element.getAsJsonPrimitive();
	}

	public static JsonElement deepCopy(JsonElement element) {
		if (element == null)
			return null;
		if (element.isJsonPrimitive())
			return deepCopy(element.getAsJsonPrimitive());
		if (element.isJsonArray())
			return deepCopy(element.getAsJsonArray());
		if (element.isJsonObject())
			return deepCopy(element.getAsJsonObject());
		return JsonNull.INSTANCE;
	}

	private static JsonArray deepCopy(JsonArray element) {
		JsonArray copy = new JsonArray();
		element.forEach((child) -> copy.add(deepCopy(child)));
		return copy;
	}

	private static JsonObject deepCopy(JsonObject element) {
		JsonObject copy = new JsonObject();
		for (Entry<String, JsonElement> entry : element.entrySet())
			copy.add(entry.getKey(), deepCopy(entry.getValue()));
		return copy;
	}

	private static JsonPrimitive deepCopy(JsonPrimitive element) {
		if (element.isBoolean())
			return new JsonPrimitive(element.getAsBoolean());
		if (element.isNumber())
			return new JsonPrimitive(element.getAsNumber());
		return new JsonPrimitive(element.getAsString());
	}

	public static boolean equal(String property, JsonElement e1, JsonElement e2, ElementFinder finder) {
		if (isNull(e1) && isNull(e2))
			return true;
		if (isNull(e1) || isNull(e2))
			return false;
		if (e1.isJsonPrimitive() && e2.isJsonPrimitive())
			return equal(e1.getAsJsonPrimitive(), e2.getAsJsonPrimitive());
		if (e1.isJsonArray() && e2.isJsonArray())
			return equal(property, e1.getAsJsonArray(), e2.getAsJsonArray(), finder);
		if (e1.isJsonObject() && e2.isJsonObject())
			return equal(property, e1.getAsJsonObject(), e2.getAsJsonObject(), finder);
		return false;
	}

	private static boolean equal(String property, JsonArray a1, JsonArray a2, ElementFinder finder) {
		if (a1.size() != a2.size())
			return false;
		Iterator<JsonElement> it1 = a1.iterator();
		Set<Integer> used = new HashSet<>();
		while (it1.hasNext()) {
			JsonElement e1 = it1.next();
			int index = finder.find(property, e1, a2, used);
			if (index == -1)
				return false;
			JsonElement e2 = a2.get(index);
			if (!equal(property, e1, e2, finder))
				return false;
			used.add(index);
		}
		return true;
	}

	private static boolean equal(String property, JsonObject e1, JsonObject e2, ElementFinder finder) {
		Set<String> checked = new HashSet<>();
		for (Entry<String, JsonElement> entry : e1.entrySet()) {
			checked.add(entry.getKey());
			if (finder.skipOnEqualsCheck(property, e1, entry.getKey()))
				continue;
			JsonElement element = entry.getValue();
			JsonElement other = e2.get(entry.getKey());
			if (!equal(entry.getKey(), element, other, finder))
				return false;
		}
		for (Entry<String, JsonElement> entry : e2.entrySet()) {
			if (checked.contains(entry.getKey()))
				continue;
			if (finder.skipOnEqualsCheck(property, e1, entry.getKey()))
				continue;
			JsonElement element = e1.get(entry.getKey());
			JsonElement other = entry.getValue();
			if (!equal(entry.getKey(), element, other, finder))
				return false;
		}
		return true;
	}

	private static boolean equal(JsonPrimitive e1, JsonPrimitive e2) {
		if (e1.isBoolean() && e2.isBoolean())
			return e1.getAsBoolean() == e2.getAsBoolean();
		if (e1.isNumber() && e2.isNumber())
			return e1.getAsNumber().doubleValue() == e2.getAsNumber().doubleValue();
		return e1.getAsString().equals(e2.getAsString());
	}

	public static boolean isNull(JsonElement element) {
		if (element == null)
			return true;
		if (element.isJsonNull())
			return true;
		if (element.isJsonArray())
			return element.getAsJsonArray().size() == 0;
		if (element.isJsonObject())
			return element.getAsJsonObject().entrySet().size() == 0;
		if (element.isJsonPrimitive())
			if (element.getAsJsonPrimitive().isNumber())
				return element.getAsJsonPrimitive().getAsNumber() == null;
			else if (element.getAsJsonPrimitive().isString())
				return element.getAsJsonPrimitive().getAsString() == null;
		return false;
	}

	public static String getString(JsonElement element, String property) {
		JsonElement value = getValue(element, property);
		if (value == null)
			return null;
		return value.getAsString();
	}

	public static double getDouble(JsonElement element, String property) {
		return getDouble(element, property, 0d);
	}

	public static Double getDouble(JsonElement element, String property, Double defaultValue) {
		JsonElement value = getValue(element, property);
		if (!value.isJsonPrimitive())
			return defaultValue;
		JsonPrimitive primitive = value.getAsJsonPrimitive();
		if (primitive.isNumber())
			return primitive.getAsDouble();
		if (!primitive.isString())
			return defaultValue;
		try {
			return Double.parseDouble(primitive.getAsString());
		} catch (NumberFormatException e) {
			return defaultValue;
		}
	}

	private static JsonElement getValue(JsonElement element, String property) {
		if (element == null)
			return null;
		if (!element.isJsonObject())
			return null;
		JsonObject object = element.getAsJsonObject();
		if (property.contains(".")) {
			String next = property.substring(0, property.indexOf('.'));
			String rest = property.substring(property.indexOf('.') + 1);
			return getValue(object.get(next), rest);
		}
		if (!object.has(property))
			return null;
		return object.get(property);
	}

	public static JsonArray replace(int index, JsonArray original, JsonElement toReplace) {
		JsonArray copy = new JsonArray();
		for (int i = 0; i < original.size(); i++)
			if (index == i)
				copy.add(toReplace);
			else
				copy.add(original.get(i));
		return copy;
	}

	public static JsonArray remove(int index, JsonArray original) {
		JsonArray copy = new JsonArray();
		for (int i = 0; i < original.size(); i++)
			if (index != i)
				copy.add(original.get(i));
		return copy;
	}

	public static int find(JsonElement element, JsonArray array, Set<Integer> exclude, String... fields) {
		if (array == null || array.size() == 0)
			return -1;
		if (element == null)
			return -1;
		if (element.isJsonPrimitive())
			return findPrimitive(element.getAsJsonPrimitive(), array);
		if (fields == null)
			return -1;
		if (!element.isJsonObject())
			return -1;
		JsonObject object = element.getAsJsonObject();
		String[] values = getValues(object, fields);
		if (values == null)
			return -1;
		Iterator<JsonElement> iterator = array.iterator();
		int index = 0;
		while (iterator.hasNext()) {
			JsonElement other = iterator.next();
			if (!other.isJsonObject()) {
				index++;
				continue;
			}
			String[] otherValues = getValues(other.getAsJsonObject(), fields);
			if (equal(values, otherValues) && (exclude == null || !exclude.contains(index)))
				return index;
			index++;
		}
		return -1;
	}

	private static int findPrimitive(JsonPrimitive element, JsonArray array) {
		Iterator<JsonElement> iterator = array.iterator();
		int index = 0;
		while (iterator.hasNext()) {
			JsonElement next = iterator.next();
			if (!next.isJsonPrimitive()) {
				index++;
				continue;
			}
			JsonPrimitive other = next.getAsJsonPrimitive();
			if (other.equals(element))
				return index;
			index++;
		}
		return -1;
	}

	private static String get(JsonObject object, String... path) {
		if (path == null)
			return null;
		if (path.length == 0)
			return null;
		Stack<String> stack = new Stack<>();
		for (int i = path.length - 1; i >= 0; i--)
			stack.add(path[i]);
		while (stack.size() > 1) {
			String next = stack.pop();
			if (!object.has(next))
				return null;
			object = object.get(next).getAsJsonObject();
		}
		JsonElement value = object.get(stack.pop());
		if (value == null || value.isJsonNull())
			return null;
		if (!value.isJsonPrimitive())
			return null;
		if (value.getAsJsonPrimitive().isNumber())
			return Double.toString(value.getAsNumber().doubleValue());
		if (value.getAsJsonPrimitive().isBoolean())
			return value.getAsBoolean() ? "true" : "false";
		return value.getAsString();
	}

	private static String[] getValues(JsonObject object, String[] fields) {
		String[] values = new String[fields.length];
		for (int i = 0; i < fields.length; i++)
			values[i] = get(object, fields[i].split("\\."));
		return values;
	}

	private static boolean equal(String[] a1, String[] a2) {
		if (a1 == null && a2 == null)
			return true;
		if (a1 == null || a2 == null)
			return false;
		if (a1.length != a2.length)
			return false;
		for (int i = 0; i < a1.length; i++)
			if (a1[i] == a2[i])
				continue;
			else if (a1[i] == null)
				return false;
			else if (!a1[i].equals(a2[i]))
				return false;
		return true;
	}

	public static abstract class ElementFinder {

		protected abstract String[] getComparisonFields(String property);

		protected abstract boolean skipOnEqualsCheck(String parentProperty, JsonElement element, String property);

		public int find(String property, JsonElement element, JsonArray array, Set<Integer> exclude) {
			return JsonUtil.find(element, array, exclude, getComparisonFields(property));
		}

	}

}