/* ~~~~~~licensing~~~~~~
 * base-support
 * ==========
 * Copyright (C) 2020 - 2021 EmDev LLC
 * ==========
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ~~~~~~/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.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 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 JsonObject getJsonRootObject(URL url) throws IOException {
		String metadata = new BufferedReader (
				  new InputStreamReader(
						  url.openStream(), StandardCharsets.UTF_8))
					.lines()
					.collect(Collectors.joining("\n"));
		return JSONUtils.getJsonRootObject(metadata);
	}
	
	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 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) {
		String preparedPath = path.replaceAll("(\\[\\d+\\])", ".$1");
		String[] pathSplitted = preparedPath.split("\\.");
		JsonElement currentElement = origin;
		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()) {
					currentElement = currentElement.getAsJsonObject().get(fragment);
			} 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;
	}	
	
	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();
		};
		
	}
	
}
