/*
 * Copyright 2012 LinkedIn Corp.
 * 
 * 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.
 */

package azkaban.utils;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ObjectWriter;

public class JSONUtils {

	/**
	 * The constructor. Cannot construct this class.
	 */
	private JSONUtils() {
	}
	
	public static String toJSON(Object obj) {
		return toJSON(obj, false);
	}

	public static String toJSON(Object obj, boolean prettyPrint) {
		ObjectMapper mapper = new ObjectMapper();

		try {
			if (prettyPrint) {
				ObjectWriter writer = mapper.writerWithDefaultPrettyPrinter();
				return writer.writeValueAsString(obj);
			}
			return mapper.writeValueAsString(obj);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static void toJSON(Object obj, OutputStream stream) {
		toJSON(obj, stream, false);
	}
	
	public static void toJSON(Object obj, OutputStream stream, boolean prettyPrint) {
		ObjectMapper mapper = new ObjectMapper();
		try {
			if (prettyPrint) {
				ObjectWriter writer = mapper.writerWithDefaultPrettyPrinter();
				writer.writeValue(stream, obj);
				return;
			}
			mapper.writeValue(stream, obj);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	public static void toJSON(Object obj, File file) throws IOException {
		toJSON(obj, file, false);
	}
	
	public static void toJSON(Object obj, File file, boolean prettyPrint) throws IOException {
		BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(file));
		try {
			toJSON(obj, stream, prettyPrint);
		} finally {
			stream.close();
		}
	}
	
	public static Object parseJSONFromStringQuiet(String json) {
		try {
			return parseJSONFromString(json);
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}
	}
	
	public static Object parseJSONFromString(String json) throws IOException {
		ObjectMapper mapper = new ObjectMapper();
		JsonFactory factory = new JsonFactory();
		JsonParser parser = factory.createJsonParser(json);
		JsonNode node = mapper.readTree(parser);

		return toObjectFromJSONNode(node);
	}

	public static Object parseJSONFromFile(File file) throws IOException {
		ObjectMapper mapper = new ObjectMapper();
		JsonFactory factory = new JsonFactory();
		JsonParser parser = factory.createJsonParser(file);
		JsonNode node = mapper.readTree(parser);

		return toObjectFromJSONNode(node);
	}
	
	public static Object parseJSONFromReader(Reader reader) throws IOException {
		ObjectMapper mapper = new ObjectMapper();
		JsonFactory factory = new JsonFactory();
		JsonParser parser = factory.createJsonParser(reader);
		JsonNode node = mapper.readTree(parser);

		return toObjectFromJSONNode(node);
	}

	private static Object toObjectFromJSONNode(JsonNode node) {
		if (node.isObject()) {
			HashMap<String, Object> obj = new HashMap<String, Object>();
			Iterator<String> iter = node.getFieldNames();
			while (iter.hasNext()) {
				String fieldName = iter.next();
				JsonNode subNode = node.get(fieldName);
				Object subObj = toObjectFromJSONNode(subNode);
				obj.put(fieldName, subObj);
			}

			return obj;
		} else if (node.isArray()) {
			ArrayList<Object> array = new ArrayList<Object>();
			Iterator<JsonNode> iter = node.getElements();
			while (iter.hasNext()) {
				JsonNode element = iter.next();
				Object subObject = toObjectFromJSONNode(element);
				array.add(subObject);
			}
			return array;
		} else if (node.isTextual()) {
			return node.asText();
		} else if (node.isNumber()) {
			if (node.isInt()) {
				return node.asInt();
			} else if (node.isLong()) {
				return node.asLong();
			} else if (node.isDouble()) {
				return node.asDouble();
			} else {
				System.err.println("ERROR What is this!? "
						+ node.getNumberType());
				return null;
			}
		} else if (node.isBoolean()) {
			return node.asBoolean();
		} else {
			return null;
		}
	}
	
	public static long getLongFromObject(Object obj) {
		if (obj instanceof Integer) {
			return Long.valueOf((Integer)obj);
		}
		
		return (Long)obj;
	}
	
	/*
	 * Writes json to a stream without using any external dependencies.
	 * 
	 * This is useful for plugins or extensions that want to write properties to a writer
	 * without having to import the jackson, or json libraries. The properties are expected
	 * to be a map of String keys and String values.
	 * 
	 * The other json writing methods are more robust and will handle more cases.
	 */
	public static void writePropsNoJarDependency(Map<String, String> properties, Writer writer) throws IOException {
		writer.write("{\n");
		int size = properties.size();
		
		for (Map.Entry<String, String> entry: properties.entrySet()) {
			// tab the space
			writer.write('\t');
			// Write key
			writer.write(quoteAndClean(entry.getKey()));
			writer.write(':');
			writer.write(quoteAndClean(entry.getValue()));
			
			size -= 1;
			// Add comma only if it's not the last one
			if (size > 0) {
				writer.write(',');
			}
			writer.write('\n');
		}
		writer.write("}");
	}
	
	private static String quoteAndClean(String str) {
		if (str == null || str.isEmpty()) {
			return "\"\"";
		}
		
		StringBuffer buffer = new StringBuffer(str.length());
		buffer.append('"');
		for (int i = 0; i < str.length(); ++i) {
			char ch = str.charAt(i);
			
			switch(ch) {
				case '\b':
					buffer.append("\\b");
					break;
				case '\t':
					buffer.append("\\t");
					break;
				case '\n':
					buffer.append("\\n");
					break;
				case '\f':
					buffer.append("\\f");
					break;
				case '\r':
					buffer.append("\\r");
					break;
				case '"':
				case '\\':
				case '/':
					buffer.append('\\');
					buffer.append(ch);
					break;
				default:
					if (isCharSpecialUnicode(ch)) {
						buffer.append("\\u");
						String hexCode = Integer.toHexString(ch);
						int lengthHexCode = hexCode.length();
						if (lengthHexCode < 4){
							buffer.append("0000".substring(0, 4 - lengthHexCode));
						}
						buffer.append(hexCode);
					}
					else {
						buffer.append(ch);
					}
			}
		}
		buffer.append('"');
		return buffer.toString();
	}
	
	private static boolean isCharSpecialUnicode(char ch) {
		if (ch < ' ') {
			return true;
		}
		else if ( ch >= '\u0080' && ch < '\u00a0') {
			return true;
		}
		else if ( ch >= '\u2000' && ch < '\u2100') {
			return true;
		}
		
		return false;
	}
}
