/*
 * 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;
  }
}
