package org.jboss.errai.jpa.client.local;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Date;

import org.jboss.errai.common.client.util.Base64Util;

import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONBoolean;
import com.google.gwt.json.client.JSONNull;
import com.google.gwt.json.client.JSONNumber;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;

/**
 * JSON-related operations required by Errai JPA.
 *
 * @author Jonathan Fuerth <jfuerth@gmail.com>
 */
public class JsonUtil {

  /**
   * Returns a JSONValue that represents the given value, which must be one of
   * the JPA2 basic types. JPA2 basic types are Java primitive types, wrappers
   * of the primitive types, java.lang.String, java.math.BigInteger,
   * java.math.BigDecimal, java.util.Date, java.sql.Date, java.sql.Time,
   * java.sql.Timestamp, byte[], Byte[], char[], Character[], and enums. Note
   * that JPA2 also specifies java.util.Calendar and all serializable types as
   * basic types, but these are not supported because they are not
   * GWT-compatible.
   *
   * @param value
   *          The value to convert to JSON. Must be a JPA2 basic type. Null is
   *          permitted.
   * @return The JSON representation of {@code value}. Never null (but might be
   *         an instance of JSONNull).
   */
  public static JSONValue basicValueToJson(Object value) {
    if (value == null) {
      return JSONNull.getInstance();
    }
    else if (value instanceof String
            || value instanceof BigInteger || value instanceof BigDecimal

            // Long doesn't fit in a JSONNumber
            || value instanceof Long

            // Character isn't a java.lang.Number
            || value instanceof Character

            // Timestamp includes nanoseconds, and has a special String representation that is parseable
            || value instanceof Timestamp) {
      return new JSONString(value.toString());
    }
    else if (value instanceof Boolean) {
      return JSONBoolean.getInstance((Boolean) value);
    }
    else if (value instanceof Number) {
      return new JSONNumber(((Number) value).doubleValue());
    }
    else if (value instanceof Enum) {
      return new JSONString(((Enum<?>) value).name());
    }
    else if (value instanceof Date) {  // covers java.sql.[Date,Time,Timestamp]
      return new JSONString(String.valueOf(((Date) value).getTime()));
    }
    else if (value instanceof byte[]) {
      byte[] v = (byte[]) value;
      return new JSONString(Base64Util.encode(v, 0, v.length));
    }
    else if (value instanceof Byte[]) {
      Byte[] v = (Byte[]) value;
      return new JSONString(Base64Util.encode(v, 0, v.length));
    }
    else if (value instanceof char[]) {
      return new JSONString(String.copyValueOf(((char[]) value)));
    }
    else if (value instanceof Character[]) {
      Character[] v = (Character[]) value;
      StringBuilder sb = new StringBuilder(v.length);
      for (Character c : v) {
        sb.append((char) c);
      }
      return new JSONString(sb.toString());
    }
    else {
      throw new RuntimeException("I don't know how JSONify " + value + " (" + value.getClass() + ")");
    }
  }

  /**
   * Converts the given JSONValue to the Java value of the given type. No type
   * coercion is performed; the given JSONValue and Class must correspond with
   * what would have been produced by {@link #basicValueToJson(Object)}.
   *
   * @param jsonValue
   *          The value to convert from JSON. Not null.
   * @param expectedType
   *          The Java type to convert to. Not null.
   * @return The JSON representation of {@code value}. Will be null if the given
   *         JSONValue is a JSONNull.
   */
  @SuppressWarnings("unchecked")
  public static <Y> Y basicValueFromJson(JSONValue jsonValue, Class<Y> expectedType) {
    Y value;
    if (jsonValue == null || jsonValue.isNull() != null) {
      value = null;
    }
    else if (expectedType == String.class) {
      value = (Y) jsonValue.isString().stringValue();
    }
    else if (expectedType == boolean.class || expectedType == Boolean.class) {
      value = (Y) Boolean.valueOf(jsonValue.isBoolean().booleanValue());
    }
    else if (expectedType == BigInteger.class) {
      value = (Y) new BigInteger(jsonValue.isString().stringValue());
    }
    else if (expectedType == BigDecimal.class) {
      value = (Y) new BigDecimal(jsonValue.isString().stringValue());
    }
    else if (expectedType == byte.class || expectedType == Byte.class) {
      value = (Y) Byte.valueOf((byte) jsonValue.isNumber().doubleValue());
    }
    else if (expectedType == char.class || expectedType == Character.class) {
      value = (Y) Character.valueOf(jsonValue.isString().stringValue().charAt(0));
    }
    else if (expectedType == short.class || expectedType == Short.class) {
      value = (Y) Short.valueOf((short) jsonValue.isNumber().doubleValue());
    }
    else if (expectedType == int.class || expectedType == Integer.class) {
      value = (Y) Integer.valueOf((int) jsonValue.isNumber().doubleValue());
    }
    else if (expectedType == long.class || expectedType == Long.class) {
      value = (Y) Long.valueOf(jsonValue.isString().stringValue());
    }
    else if (expectedType == float.class || expectedType == Float.class) {
      value = (Y) Float.valueOf((float) jsonValue.isNumber().doubleValue());
    }
    else if (expectedType == double.class || expectedType == Double.class) {
      value = (Y) Double.valueOf(jsonValue.isNumber().doubleValue());
    }
    else if (expectedType == Date.class) {
      value = (Y) new Date(Long.parseLong(jsonValue.isString().stringValue()));
    }
    else if (expectedType == java.sql.Date.class) {
      value = (Y) new java.sql.Date(Long.parseLong(jsonValue.isString().stringValue()));
    }
    else if (expectedType == Time.class) {
      value = (Y) new Time(Long.parseLong(jsonValue.isString().stringValue()));
    }
    else if (expectedType == Timestamp.class) {
      value = (Y) Timestamp.valueOf(jsonValue.isString().stringValue());
    }
    else if (expectedType.isEnum()) {
      @SuppressWarnings("rawtypes") Class enumType = expectedType;
      value = (Y) Enum.valueOf(enumType, jsonValue.isString().stringValue());
    }
    else if (expectedType == byte[].class) {
      value = (Y) Base64Util.decode(jsonValue.isString().stringValue());
    }
    else if (expectedType == Byte[].class) {
      value = (Y) Base64Util.decodeAsBoxed(jsonValue.isString().stringValue());
    }
    else if (expectedType == char[].class) {
      value = (Y) jsonValue.isString().stringValue().toCharArray();
    }
    else if (expectedType == Character[].class) {
      String str = jsonValue.isString().stringValue();
      Character[] boxedArray = new Character[str.length()];
      for (int i = 0; i < str.length(); i++) {
        boxedArray[i] = str.charAt(i);
      }
      value = (Y) boxedArray;
    }
    else {
      throw new RuntimeException("I don't know how unJSONify " + jsonValue + " (expected type "+ expectedType + ")");
    }
    return value;
  }

  /**
   * Compares two JSON values for equality by value.
   * <p>
   * If the actual type of v1 and v2 differs, the values are considered unequal
   * (that is, type coercion is never performed when doing a comparison).
   * <p>
   * This method returns true under the following conditions:
   * <ul>
   *  <li>If v1 and v2 are both JSONString, and their String values compare equal;
   *  <li>If v1 and v2 are both JSONBoolean, and they compare equal;
   *  <li>If v1 and v2 are both JSONNumber, and their double values compare equal;
   *  <li>If v1 and v2 are both JSONNull;
   *  <li>If v1 and v2 are both JSONArray, they have the same length, and recursive
   *      application of this check to each pair of corresponding elements finds them all equal;
   *  <li>If v1 and v2 are both JSONObject, they have the same key sets as each other,
   *      and recursive application of this check to each pair of corresponding
   *      values finds them all equal;
   * </ul>
   *
   * @param v1
   *          One of the values to compare. Must not be null (but JSONNull is
   *          permitted).
   * @param v2
   *          The other value to compare. Must not be null (but JSONNull is
   *          permitted).
   * @return true if v1 and v2 have identical values according to the above criteria.
   */
  public static boolean equals(JSONValue v1, JSONValue v2) {
    if (v1.equals(v2)) {
      return true;
    }
    else if (v1.getClass() != v2.getClass()) {
      return false;
    }
    else if (v1.isArray() != null) {
      JSONArray a1 = v1.isArray();
      JSONArray a2 = v2.isArray();
      if (a1.size() != a2.size()) {
        return false;
      }
      for (int i = 0, n = a1.size(); i < n; i++) {
        if (!equals(a1.get(i), a2.get(i))) {
          return false;
        }
      }
      return true;
    }
    else if (v1.isBoolean() != null) {
      JSONBoolean b1 = v1.isBoolean();
      JSONBoolean b2 = v2.isBoolean();
      return b1.booleanValue() == b2.booleanValue();
    }
    else if (v1.isNull() != null) {
      // this case should never be triggered, because of the getClass() precheck above
      return v2.isNull() != null;
    }
    else if (v1.isNumber() != null) {
      JSONNumber n1 = v1.isNumber();
      JSONNumber n2 = v2.isNumber();
      return n1.doubleValue() == n2.doubleValue();
    }
    else if (v1.isObject() != null) {
      JSONObject o1 = v1.isObject();
      JSONObject o2 = v2.isObject();
      if (!o1.keySet().equals(o2.keySet())) {
        return false;
      }
      for (String key : o1.keySet()) {
        if (!equals(o1.get(key), o2.get(key))) {
          return false;
        }
      }
      return true;
    }
    else if (v1.isString() != null) {
      JSONString s1 = v1.isString();
      JSONString s2 = v2.isString();
      return s1.stringValue().equals(s2.stringValue());
    }
    else {
      throw new AssertionError("Found unexpected subtype of JSONValue: " + v1.getClass());
    }
    // NOTREACHED
  }
}
