/*
MIT Licence

Copyright 2021 Steven Wooding

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
*/

package slite.lib_java;

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;

import java.util.Map;

/**
 *
 * @author steve
 */
public class GsonUtil
{
	public static final int MERGE_MODE_ARRAY_APPEND = 1;
	public static final int MERGE_MODE_ARRAY_MERGE = 2;
	public static final int MERGE_MODE_ARRAY_OVERWRITE = 4;
	public static final int MERGE_MODE_OBJECT_MERGE = 8;
	public static final int MERGE_MODE_OBJECT_OVERWRITE = 16;
	
	/**
	 * This method does a deep merge on two JsonObjects. It is NOT reference safe. 
	 * So after the merge any references to the 'from' must be disgarded. 
	 * If it encounters an array, it would append the 'from' array to the 'to' array
	 * @param to
	 * @param from 

	 */
	public static void deepMerge(JsonObject to, JsonObject from)
	{
		deepMerge(to, from, MERGE_MODE_ARRAY_APPEND | MERGE_MODE_OBJECT_MERGE);
	}
	
	/**
	 * This method does a deep merge on two JsonObjects. It is NOT reference safe.
	 * So after the merge any references to the 'from' must be disgarded.
	 * @param to
	 * @param from 
	 * @param mergeModes Conditions of how to handle certain types of conflicts
	 */
	public static void deepMerge(JsonObject to, JsonObject from, int mergeModes)
	{		
		for(Map.Entry<String, JsonElement> entry : from.entrySet())
		{
			String key = entry.getKey();
			JsonElement fromValue = entry.getValue();
			if(to.has(key)) // only if the destination (to) actually has the same key do we need to make a fuss about it.
			{
				JsonElement toValue = to.get(key);
				if(!deepMergeAttempt(toValue, fromValue, mergeModes))
					to.add(key, fromValue);
			}
			else // if the destination (to) does not have the key, then just transfer it from the 'from'
				to.add(key, fromValue);
		}
	}
	
	/**
	 * This method merges the indexes of two JsonArrays. It basically iterates 
	 * over the 'from' array and does a deepMerge on each index that matches with 
	 * the 'to', if the 'to' does not have that index then the value of the  index 
	 * is simply transferred to the 'to'. 
	 * @param to
	 * @param from
	 * @param mergeModes 
	 */
	public static void deepMerge(JsonArray to, JsonArray from, int mergeModes)
	{
		int fromSize = from.size();
		int toSize = to.size();
		if(fromSize>0)
		{
			for(int i=0;i<fromSize;i++)
			{
				JsonElement fromValue = from.get(i);
				if(i>=toSize)
					to.add(fromValue);
				else
				{
					JsonElement toValue = to.get(i);
					if(!deepMergeAttempt(toValue, fromValue, mergeModes))
						to.set(i, fromValue);
				}
			}
		}
	}
	
	/**
	 * Attempts to merge the two values supplied based on the merge mode rules.
	 * If the merge was successfull then true is returned. If no merge has taken
	 * place the false is returned
	 * @param toValue
	 * @param fromValue
	 * @param mergeModes
	 * @return 
	 */
	private static boolean deepMergeAttempt(JsonElement toValue, JsonElement fromValue, int mergeModes)
	{
		if(fromValue.isJsonObject() && toValue.isJsonObject()) // We have 2 objects, so do a deepMerge on them
		{
			if((mergeModes & MERGE_MODE_OBJECT_MERGE) >0)
				deepMerge(toValue.getAsJsonObject(), fromValue.getAsJsonObject(), mergeModes);
			else
				return false;
		}
		else if(toValue.isJsonArray() && fromValue.isJsonArray()) // We have 2 arrays so look at the mergeModes and decide what to do
		{
			JsonArray toArray = toValue.getAsJsonArray();
			JsonArray fromArray = fromValue.getAsJsonArray();
			if((mergeModes & MERGE_MODE_ARRAY_APPEND) > 0) // mergeMode dictates that we should append the two arrays together
				toArray.addAll(fromArray);
			else if((mergeModes & MERGE_MODE_ARRAY_MERGE) > 0) // mergeMode dictates that we need to merge where indexes match
				deepMerge(toArray, fromArray, mergeModes);
//			else if((mergeModes & MERGE_MODE_ARRAY_OVERWRITE) > 0) // mergeMode dictates that we should overwrite the array completely.
			else 
				return false;
		}
		else
			return false;
		
		return true;
	}
	
	/**
	 * This method produces a json string from a given json element. So you can supply this with a json value, a json object or a json array
	 * @param element
	 * @param pretty
	 * @return 
	 */
	public static String toJson(JsonElement element, boolean pretty)
	{
		if(pretty)
		{
			Gson gson = new GsonBuilder().setPrettyPrinting().create();
			return gson.toJson(element);
		}
		else
			return element.toString();
	}
	
	/**
	 * Takes the given string and converts it to a JsonObject. If the string is
	 * not an object, like a primitive or an array, this method will return null
	 * @param json
	 * @return
	 * @throws Exception
	 */
	public static JsonObject fromJsonObject(String json) throws Exception
	{
		JsonElement element = JsonParser.parseString(json);
		if(element.isJsonObject()) return element.getAsJsonObject();		
		return null;
	}

	/**
	 * Takes the given string and converts it to a JsonElement. This method is a
	 * lower level implentation than the fromJsonObject method since it can parse
	 * any type of top level json type.
	 * @param json
	 * @return
	 * @throws Exception 
	 */
	public static JsonElement fromJson(String json) throws Exception
	{
		return JsonParser.parseString(json);
	}


	
	public static Boolean getBoolean(JsonObject obj,String ...keys)
	{
		JsonElement element = getElement(obj, keys);
		return element==null ? null : element.getAsBoolean();
	}
	
	public static Byte getByte(JsonObject obj,String ...keys)
	{
		JsonElement element = getElement(obj, keys);
		return element==null ? null : element.getAsByte();
	}
	
	public static Short getShort(JsonObject obj,String ...keys)
	{
		JsonElement element = getElement(obj, keys);
		return element==null ? null : element.getAsShort();
	}

	public static Integer getInt(JsonObject obj,String ...keys)
	{
		JsonElement element = getElement(obj, keys);
		return element==null ? null : element.getAsInt();
	}

	public static Long getLong(JsonObject obj,String ...keys)
	{
		JsonElement element = getElement(obj, keys);
		return element==null ? null : element.getAsLong();
	}

	public static Float getFloat(JsonObject obj,String ...keys)
	{
		JsonElement element = getElement(obj, keys);
		return element==null ? null : element.getAsFloat();
	}

	public static Double getDouble(JsonObject obj,String ...keys)
	{
		JsonElement element = getElement(obj, keys);
		return element==null ? null : element.getAsDouble();
	}

	public static String getString(JsonObject obj,String ...keys)
	{
		JsonElement element = getElement(obj, keys);
		return element==null ? null : element.getAsString();
	}
	
	public static JsonElement getElement(JsonObject obj,String ...keys)
	{
		if(keys==null || keys.length == 0) return null;

		int keyDepth = keys.length;
		int count = 0;

		for(String key : keys)
		{
			if(obj==null) return null;
			JsonElement subObj = obj.get(key);
				
			if(subObj==null) return null;
			if(count < keyDepth - 1)
			{
				if(subObj.isJsonObject()) obj = (JsonObject) obj.get(key);
			}
			else return subObj;
			
			count ++;
		}
		
		return null; // Technically this is unreachable. But we have to keep the compiler happy. So that's why it's here.
	}
	
	public static JsonObject getObject(JsonObject data,String ...keys)
	{
		if(keys==null || keys.length == 0) return null;

		if(data==null) return null;
		
		int keyDepth = keys.length;
		int count = 0;
		for(String key : keys)
		{
			JsonObject obj = data;

			if(count < keyDepth - 1)
			{
				if(obj.get(key).isJsonObject())
					data = (JsonObject) obj.get(key);
			}else
			{
				data = new JsonObject();
				data.add(key, obj.get(key));
			}
			count ++;
		}

		return data;
	}

	public static JsonObject set(JsonObject data, boolean value, String ...keys)
	{
		return set(data, new JsonPrimitive((Boolean)value), keys);
	}

	public static JsonObject set(JsonObject data, byte value, String ...keys)
	{
		return set(data, new JsonPrimitive((Byte)value), keys);
	}

	public static JsonObject set(JsonObject data, short value, String ...keys)
	{
		return set(data, new JsonPrimitive((Short)value), keys);
	}

	public static JsonObject set(JsonObject data, int value, String ...keys)
	{
		return set(data, new JsonPrimitive((Integer)value), keys);
	}

	public static JsonObject set(JsonObject data, long value, String ...keys)
	{
		return set(data, new JsonPrimitive((Long)value), keys);
	}

	public static JsonObject set(JsonObject data, float value, String ...keys)
	{
		return set(data, new JsonPrimitive((Float)value), keys);
	}

	public static JsonObject set(JsonObject data, double value, String ...keys)
	{
		return set(data, new JsonPrimitive((Double)value), keys);
	}

	public static JsonObject set(JsonObject data, String value, String ...keys)
	{
		return set(data, new JsonPrimitive(value), keys);
	}

	public static JsonObject set(JsonObject data, JsonElement value, String ...keys)
	{
		if(keys==null || keys.length == 0) return null;

		if(data==null) data = new JsonObject();

		if(keys.length>1) // Multi dimensions, do some depth looping.
		{
			JsonObject obj = data;
			int keyDepth = keys.length;
			int count = 0;
			for(String key : keys)
			{
				if(count < keyDepth - 1) // Not the last key, so we are only dealing with objects, no primitives
				{
					if(!obj.has(key) || !obj.get(key).isJsonObject()) // the sub-key doesn't exist or it's not an object
						obj.add(key, new JsonObject()); // So let's create a sub-key that is an object

					obj = (JsonObject) obj.get(key);
				}
				else // It's the last key so we are dealing with a primitive
				{
					obj.add(key, value);
				}

				count ++;
			}
		}
		else // only one dimension. Keep it simple.
			data.add(keys[0], value);

		return data;
	}	

	public static void main(String[] args) throws Exception
	{
		// To Test : slite/lib_java/compile.sh && java -classpath ".:slite/lib_java/lib/*" slite.lib_java.GsonUtil
		JsonObject to = JsonParser.parseString(Parse.fromFileString("slite/lib_java/jsontest1.json")).getAsJsonObject();
		JsonObject from = JsonParser.parseString(Parse.fromFileString("slite/lib_java/jsontest2.json")).getAsJsonObject();
		
		deepMerge(to, from);
	}

	public static void debug(JsonElement element)
	{
		System.out.println(toJson(element, true));
	}
}
