/*
 * Copyright 2010 Google Inc.
 * 
 * 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 com.google.web.bindery.autobean.vm.impl;

import com.google.gwt.core.client.impl.WeakMapping;
import com.google.gwt.thirdparty.json.JSONArray;
import com.google.gwt.thirdparty.json.JSONException;
import com.google.gwt.thirdparty.json.JSONObject;
import com.google.web.bindery.autobean.shared.Splittable;
import com.google.web.bindery.autobean.shared.impl.HasSplittable;
import com.google.web.bindery.autobean.shared.impl.StringQuoter;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Uses the org.json packages to slice and dice request payloads.
 */
public class JsonSplittable implements Splittable, HasSplittable {

  public static JsonSplittable create() {
    return new JsonSplittable(new JSONObject());
  }

  public static Splittable create(String payload) {
    try {
      switch (payload.charAt(0)) {
        case '{':
          return new JsonSplittable(new JSONObject(payload));
        case '[':
          return new JsonSplittable(new JSONArray(payload));
        case '"':
          return new JsonSplittable(new JSONArray("[" + payload + "]").getString(0));
        case '-':
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
          return new JsonSplittable(Double.parseDouble(payload));
        case 't':
        case 'f':
          return new JsonSplittable(Boolean.parseBoolean(payload));
        case 'n':
          return null;
        default:
          throw new RuntimeException("Could not parse payload: payload[0] = " + payload.charAt(0));
      }
    } catch (JSONException e) {
      throw new RuntimeException("Could not parse payload", e);
    }
  }

  public static Splittable createIndexed() {
    return new JsonSplittable(new JSONArray());
  }

  public static Splittable createNull() {
    return new JsonSplittable();
  }

  /**
   * Private equivalent of com.google.gwt.thirdparty.json.JSONObject.getNames(JSONObject) since that
   * method is not available in Android 2.2. Used to represent a null value.
   */
  private static String[] getNames(JSONObject json) {
    int length = json.length();
    if (length == 0) {
      return null;
    }
    String[] names = new String[length];
    Iterator<?> i = json.keys();
    int j = 0;
    while (i.hasNext()) {
      names[j++] = (String) i.next();
    }
    return names;
  }

  private JSONArray array;
  private Boolean bool;
  /**
   * Used to represent a null value.
   */
  private boolean isNull;
  private Double number;
  private JSONObject obj;
  private String string;
  private final Map<String, Object> reified = new HashMap<String, Object>();

  /**
   * Constructor for a null value.
   */
  private JsonSplittable() {
    isNull = true;
  }

  private JsonSplittable(boolean value) {
    this.bool = value;
  }

  private JsonSplittable(double value) {
    this.number = value;
  }

  private JsonSplittable(JSONArray array) {
    this.array = array;
  }

  private JsonSplittable(JSONObject obj) {
    this.obj = obj;
  }

  private JsonSplittable(String string) {
    this.array = null;
    this.obj = null;
    this.string = string;
  }

  public boolean asBoolean() {
    return bool;
  }

  public double asNumber() {
    return number;
  }

  public void assign(Splittable parent, int index) {
    try {
      ((JsonSplittable) parent).array.put(index, value());
    } catch (JSONException e) {
      throw new RuntimeException(e);
    }
  }

  public void assign(Splittable parent, String propertyName) {
    try {
      ((JsonSplittable) parent).obj.put(propertyName, value());
    } catch (JSONException e) {
      throw new RuntimeException(e);
    }
  }

  public String asString() {
    return string;
  }

  public Splittable deepCopy() {
    return create(getPayload());
  }

  public Splittable get(int index) {
    try {
      return makeSplittable(array.get(index));
    } catch (JSONException e) {
      throw new RuntimeException(e);
    }
  }

  public Splittable get(String key) {
    try {
      return makeSplittable(obj.get(key));
    } catch (JSONException e) {
      throw new RuntimeException(key, e);
    }
  }

  public String getPayload() {
    if (isNull) {
      return "null";
    }
    if (obj != null) {
      return obj.toString();
    }
    if (array != null) {
      return array.toString();
    }
    if (string != null) {
      return StringQuoter.quote(string);
    }
    if (number != null) {
      return String.valueOf(number);
    }
    if (bool != null) {
      return String.valueOf(bool);
    }
    throw new RuntimeException("No data in this JsonSplittable");
  }

  public List<String> getPropertyKeys() {
    String[] names = getNames(obj);
    if (names == null) {
      return Collections.emptyList();
    } else {
      return Collections.unmodifiableList(Arrays.asList(names));
    }
  }

  public Object getReified(String key) {
    return reified.get(key);
  }

  public Splittable getSplittable() {
    return this;
  }

  public boolean isBoolean() {
    return bool != null;
  }

  public boolean isIndexed() {
    return array != null;
  }

  public boolean isKeyed() {
    return obj != null;
  }

  public boolean isNull(int index) {
    return array.isNull(index);
  }

  public boolean isNull(String key) {
    // Treat undefined and null as the same
    return !obj.has(key) || obj.isNull(key);
  }

  public boolean isNumber() {
    return number != null;
  }

  public boolean isReified(String key) {
    return reified.containsKey(key);
  }

  public boolean isString() {
    return string != null;
  }

  public boolean isUndefined(String key) {
    return !obj.has(key);
  }

  @Override
  public void removeReified(String key) {
    reified.remove(key);
  }

  public void setReified(String key, Object object) {
    reified.put(key, object);
  }

  public void setSize(int size) {
    // This is terrible, but there's no API support for resizing or splicing
    JSONArray newArray = new JSONArray();
    for (int i = 0; i < size; i++) {
      try {
        newArray.put(i, array.get(i));
      } catch (JSONException e) {
        throw new RuntimeException(e);
      }
    }
    array = newArray;
  }

  public int size() {
    return array.length();
  }

  private synchronized JsonSplittable makeSplittable(Object object) {
    if (JSONObject.NULL.equals(object)) {
      return null;
    }
    /*
     * Maintain a 1:1 mapping between object instances and JsonSplittables.
     * Doing this with a WeakHashMap doesn't work, since our c.g.g.thirdparty.json
     * arrays, coming from Android, appear to have value-based equality.
     */
    JsonSplittable seen = (JsonSplittable) WeakMapping.get(object, JsonSplittable.class.getName());
    if (seen == null) {
      if (object instanceof JSONObject) {
        seen = new JsonSplittable((JSONObject) object);
        WeakMapping.setWeak(object, JsonSplittable.class.getName(), seen);
      } else if (object instanceof JSONArray) {
        seen = new JsonSplittable((JSONArray) object);
        WeakMapping.setWeak(object, JsonSplittable.class.getName(), seen);
      } else if (object instanceof String) {
        seen = new JsonSplittable(object.toString());
      } else if (object instanceof Number) {
        seen = new JsonSplittable(((Number) object).doubleValue());
      } else if (object instanceof Boolean) {
        seen = new JsonSplittable((Boolean) object);
      } else {
        throw new RuntimeException("Unhandled type " + object.getClass());
      }
    }
    return seen;
  }

  private Object value() {
    if (isNull) {
      return null;
    }
    if (obj != null) {
      return obj;
    }
    if (array != null) {
      return array;
    }
    if (string != null) {
      return string;
    }
    if (number != null) {
      return number;
    }
    if (bool != null) {
      return bool;
    }
    throw new RuntimeException("No data");
  }
}
