package javajs.util;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

import javajs.J2SIgnoreImport;



/**
 * a very simple JSON parser for JSON objects that are compatible with JavaScript
 * A gross simplification of https://github.com/douglascrockford/JSON-java
 * 
 * A SUBSET of JSON with similarly to window.JSON.parse():
 * 
 * In JavaScript returns "null" for a null value, not null
 * 
 *  -- requires quoted strings for keys and values
 *  
 *  -- does not allow /xxx/ objects
 *  
 *  @author Bob Hanson
 *  
 */
@J2SIgnoreImport({ HashMap.class })
public class JSJSONParser {

  private String str;
  private int index;
  private int len;
  private boolean asHashTable;

  public JSJSONParser () {
    // for reflection
  }
  
  /**
   * requires { "key":"value", "key":"value",....}
   * 
   * @param str
   * @param asHashTable TODO
   * 
   * @return Map or null
   */
  @SuppressWarnings("unchecked")
  public Map<String, Object> parseMap(String str, boolean asHashTable) {
    index = 0;
    this.asHashTable = asHashTable;
    this.str = str;
    len = str.length();
    if (getChar() != '{')
      return null;
    returnChar();
    return (Map<String, Object>) getValue(false);
  }
  
  /**
   * Could return Integer, Float, Boolean, String, Map<String, Object>, Lst<Object>, or null
   * 
   * @param str
   * @param asHashTable 
   * @return a object equivalent to the JSON string str
   * 
   */
  public Object parse(String str, boolean asHashTable) {
    index = 0;
    this.asHashTable = asHashTable;
    this.str = str;
    len = str.length();
    return getValue(false);
  }

  private char next() {
    return (index < len ? str.charAt(index++) : '\0');
  }

  private void returnChar() {
    index--;
  }

  /**
   * Get the next char in the string, skipping whitespace.
   * 
   * @throws JSONException
   * @return one character, or 0 if there are no more characters.
   */
  private char getChar() throws JSONException {
    for (;;) {
      char c = next();
      if (c == 0 || c > ' ') {
        return c;
      }
    }
  }

  /**
   * only allowing the following values:
   * 
   * {...} object
   * 
   * [...] array
   * 
   * Integer
   * 
   * Float
   * 
   * "quoted string"
   * 
   * 
   * @param isKey if we should allow {...} and [...]
   * @return a subclass of Object
   * @throws JSONException
   */
  private Object getValue(boolean isKey) throws JSONException {
    int i = index;
    char c = getChar();
    switch (c) {
    case '\0':
      return null;
    case '"':
    case '\'':
      return getString(c);
    case '{':
      if (!isKey)
        return getObject();
      c = 0;
      break;
    case '[':
      if (!isKey)
        return getArray();
      c = 0;
      break;
    default:
      // standard syntax is assumed; not checking all possible invalid keys
      // for example, "-" is not allowed in JavaScript, which is what this is for
      returnChar();
      while (c >= ' ' && "[,]{:}'\"".indexOf(c) < 0)
        c = next();
      returnChar();
      if (isKey && c != ':')
        c = 0;
      break;
    }
    if (isKey && c == 0)
      throw new JSONException("invalid key");

    String string = str.substring(i, index).trim();

    // check for the only valid simple words: true, false, null (lower case)
    // and in this case, only for 

    if (!isKey) {
      if (string.equals("true")) {
        return Boolean.TRUE;
      }
      if (string.equals("false")) {
        return Boolean.FALSE;
      }
      if (string.equals("null")) {
        return (asHashTable ? string : null);
      }
    }
    //  only numbers from here on:
    c = string.charAt(0);
    if (c >= '0' && c <= '9' || c == '-')
      try {
        if (string.indexOf('.') < 0 && string.indexOf('e') < 0
            && string.indexOf('E') < 0)
          return new Integer(string);
        // not allowing infinity or NaN
        // using float here because Jmol does not use Double
        Float d = Float.valueOf(string);
        if (!d.isInfinite() && !d.isNaN())
          return d;
      } catch (Exception e) {
      }
    // not a valid number
    System.out.println("JSON parser cannot parse " + string);
    throw new JSONException("invalid value");
  }

  private String getString(char quote) throws JSONException {
    char c;
    SB sb = null;
    int i0 = index;
    for (;;) {
      int i1 = index;
      switch (c = next()) {
      case '\0':
      case '\n':
      case '\r':
        throw syntaxError("Unterminated string");
      case '\\':
        switch (c = next()) {
        case '"':
        case '\'':
        case '\\':
        case '/':
          break;
        case 'b':
          c = '\b';
          break;
        case 't':
          c = '\t';
          break;
        case 'n':
          c = '\n';
          break;
        case 'f':
          c = '\f';
          break;
        case 'r':
          c = '\r';
          break;
        case 'u':
          int i = index;
          index += 4;
          try {
            c = (char) Integer.parseInt(str.substring(i, index), 16);
          } catch (Exception e) {
            throw syntaxError("Substring bounds error");
          }
          break;
        default:
          throw syntaxError("Illegal escape.");
        }
        break;
      default:
        if (c == quote)
          return (sb == null ? str.substring(i0, i1) : sb.toString());
        break;
      }
      if (index > i1 + 1) {
        if (sb == null) {
          sb = new SB();
          sb.append(str.substring(i0, i1));
        }
      }
      if (sb != null)
        sb.appendC(c);
    }
  }

  private Object getObject() {
    Map<String, Object> map = (asHashTable ? new Hashtable<String, Object>() : new HashMap<String, Object>());
    String key = null;
    switch (getChar()) {
    case '}':
      return map;
    case 0:
      throw new JSONException("invalid object");
    }
    returnChar();
    boolean isKey = false;
    for (;;) {
      if ((isKey = !isKey) == true)
        key = getValue(true).toString();
      else
        map.put(key, getValue(false));
      switch (getChar()) {
      case '}':
        return map;
      case ':':
        if (isKey)
          continue;
        isKey = true;
        //$FALL-THROUGH$
      case ',':
        if (!isKey)
          continue;
        //$FALL-THROUGH$
      default:
        throw syntaxError("Expected ',' or ':' or '}'");
      }
    }
  }

  private Object getArray() {
    Lst<Object> l = new Lst<Object>();
    switch (getChar()) {
    case ']':
      return l;
    case '\0':
      throw new JSONException("invalid array");
    }
    returnChar();
    boolean isNull = false;
    for (;;) {
      if (isNull) {
        l.addLast(null);
        isNull = false;
      } else {
        l.addLast(getValue(false));
      }
      switch (getChar()) {
      case ',':
        switch (getChar()) {
        case ']':
          // terminal ,
          return l;
        case ',':
          // empty value
          isNull = true;
          //$FALL-THROUGH$
        default:
          returnChar();
        }
        continue;
      case ']':
        return l;
      default:
        throw syntaxError("Expected ',' or ']'");
      }
    }
  }

  /**
   * Make a JSONException to signal a syntax error.
   * 
   * @param message
   *        The error message.
   * @return A JSONException object, suitable for throwing
   */
  public JSONException syntaxError(String message) {
    return new JSONException(message + " for " + str.substring(0, Math.min(index,  len)));
  }

}
