/*
 * 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.gwt.client.impl;

import com.google.gwt.core.client.GwtScriptOnly;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsonUtils;
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.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Implements the EntityCodex.Splittable interface using a raw JavaScriptObject.
 * <p>
 * A string value represented by a JsoSplittable can't use the string object
 * directly, since {@code String.prototype} is overridden, so instead a
 * temporary wrapper object is used to encapsulate the string data.
 */
@GwtScriptOnly
public final class JsoSplittable extends JavaScriptObject implements Splittable, HasSplittable {
  private static boolean stringifyFastTested;
  private static boolean stringifyFastResult;

  public static native JsoSplittable create() /*-{
    return {};
  }-*/;

  public static Splittable create(boolean value) {
    return create0(value);
  }

  public static Splittable create(double value) {
    return create0(value);
  }

  public static Splittable create(String value) {
    return create0(value);
  }

  public static native JsoSplittable createIndexed() /*-{
    return [];
  }-*/;

  public static Splittable nullValue() {
    return NullSplittable.INSTANCE;
  }

  private static native Splittable create0(boolean object) /*-{
    return Object(object);
  }-*/;

  private static native Splittable create0(double object) /*-{
    return Object(object);
  }-*/;

  private static native Splittable create0(String object) /*-{
    return {
      __s : object
    };
  }-*/;

  private static native boolean isUnwrappedString(JavaScriptObject obj) /*-{
    return Object.prototype.toString.call(obj) == '[object String]';
  }-*/;

  private static boolean stringifyFastSupported() {
    if (stringifyFastTested) {
      return stringifyFastResult;
    }
    stringifyFastTested = true;
    return stringifyFastResult = stringifyFastSupported0();
  }

  /**
   * Test that the JSON api is available and that it does not add function
   * objects to the output. The test for function objects is for old versions of
   * Safari.
   */
  private static native boolean stringifyFastSupported0() /*-{
    return $wnd.JSON && $wnd.JSON.stringify && $wnd.JSON.stringify({
      b : function() {
      }
    }) == '{}';
  }-*/;

  protected JsoSplittable() {
  };

  public native boolean asBoolean() /*-{
    return this && this.valueOf();
  }-*/;

  public native double asNumber() /*-{
    return this && this.valueOf();
  }-*/;

  public void assign(Splittable parent, int index) {
    if (isString()) {
      assign0(parent, index, asString());
    } else {
      assign0(parent, index, this);
    }
  }

  public void assign(Splittable parent, String index) {
    if (isString()) {
      assign0(parent, index, asString());
    } else {
      assign0(parent, index, this);
    }
  }

  public native String asString() /*-{
    return this.__s;
  }-*/;

  public Splittable deepCopy() {
    return StringQuoter.split(getPayload());
  }

  public JsoSplittable get(int index) {
    return getRaw(index);
  }

  public JsoSplittable get(String key) {
    return getRaw(key);
  }

  public String getPayload() {
    if (isString()) {
      return JsonUtils.escapeValue(asString());
    }
    if (stringifyFastSupported()) {
      return stringifyFast();
    }
    return stringifySlow();
  }

  public List<String> getPropertyKeys() {
    List<String> toReturn = new ArrayList<String>();
    getPropertyKeys0(toReturn);
    return Collections.unmodifiableList(toReturn);
  }

  public native Object getReified(String key) /*-{
    return this.__reified && this.__reified[':' + key];
  }-*/;

  public Splittable getSplittable() {
    return this;
  }

  public native boolean isBoolean() /*-{
    return this && typeof(this.valueOf()) === 'boolean';
  }-*/;

  public native boolean isFunction() /*-{
    return Object.prototype.toString.call(this) == '[object Function]';
  }-*/;

  public native boolean isIndexed() /*-{
    return Object.prototype.toString.call(this) == '[object Array]';
  }-*/;

  public boolean isKeyed() {
    return this != NULL && !isString() && !isIndexed() && !isFunction();
  }

  public native boolean isNull(int index) /*-{
    return this[index] == null;
  }-*/;

  public native boolean isNull(String key) /*-{
    return this[key] == null;
  }-*/;

  public native boolean isNumber() /*-{
    return this && typeof(this.valueOf()) === 'number';
  }-*/;

  public native boolean isReified(String key) /*-{
    return !!(this.__reified && this.__reified.hasOwnProperty(':' + key));
  }-*/;

  /**
   * Returns whether or not the current object is a string-carrier.
   */
  public native boolean isString() /*-{
    return this && this.__s != null;
  }-*/;

  public native boolean isUndefined(String key) /*-{
    return this[key] === undefined;
  }-*/;

  @Override
  public native void removeReified(String key) /*-{
    if (this.__reified) {
      delete this.__reified[':' + key];
    }
  }-*/;

  public native void setReified(String key, Object object) /*-{
    // Use a function object so native JSON.stringify will ignore
    (this.__reified || (this.__reified = function() {
    }))[':' + key] = object;
  }-*/;

  public native void setSize(int size) /*-{
    this.length = size;
  }-*/;

  public native int size() /*-{
    return this.length;
  }-*/;

  private native void assign0(Splittable parent, int index, Splittable value) /*-{
    parent[index] = value;
  }-*/;

  private native void assign0(Splittable parent, int index, String value) /*-{
    parent[index] = value;
  }-*/;

  private native void assign0(Splittable parent, String index, Splittable value) /*-{
    parent[index] = value;
  }-*/;

  private native void assign0(Splittable parent, String index, String value) /*-{
    parent[index] = value;
  }-*/;

  private native void getPropertyKeys0(List<String> list) /*-{
    for (key in this) {
      if (this.hasOwnProperty(key)) {
        list.@java.util.List::add(Ljava/lang/Object;)(key);
      }
    }
  }-*/;

  private native JsoSplittable getRaw(int index) /*-{
    _ = this[index];
    if (_ == null) {
      return null;
    }
    if (@com.google.web.bindery.autobean.gwt.client.impl.JsoSplittable::isUnwrappedString(*)(_)) {
      return @com.google.web.bindery.autobean.gwt.client.impl.JsoSplittable::create(Ljava/lang/String;)(_);
    }
    return Object(_);
  }-*/;

  private native JsoSplittable getRaw(String index) /*-{
    _ = this[index];
    if (_ == null) {
      return null;
    }
    if (@com.google.web.bindery.autobean.gwt.client.impl.JsoSplittable::isUnwrappedString(*)(_)) {
      return @com.google.web.bindery.autobean.gwt.client.impl.JsoSplittable::create(Ljava/lang/String;)(_);
    }
    return Object(_);
  }-*/;

  /**
   * The test for {@code $H} removes the key in the emitted JSON, however making
   * a similar test for {@code __reified} causes the key to be emitted with an
   * explicit {@code null} value.
   */
  private native String stringifyFast() /*-{
    return $wnd.JSON.stringify(this, function(key, value) {
      if (key == "$H") {
        return;
      }
      return value;
    });
  }-*/;

  private String stringifySlow() {
    StringBuilder sb = new StringBuilder();
    stringifySlow(sb);
    return sb.toString();
  }

  private void stringifySlow(StringBuilder sb) {
    if (this == NULL) {
      sb.append("null");
      return;
    }
    if (isBoolean()) {
      sb.append(asBoolean());
      return;
    }
    if (isNumber()) {
      sb.append(asNumber());
      return;
    }
    if (isString()) {
      sb.append(JsonUtils.escapeValue(asString()));
      return;
    }
    if (isIndexed()) {
      sb.append("[");
      for (int i = 0, j = size(); i < j; i++) {
        if (i > 0) {
          sb.append(",");
        }
        get(i).stringifySlow(sb);
      }
      sb.append("]");
      return;
    }

    sb.append("{");
    boolean needsComma = false;
    for (String key : getPropertyKeys()) {
      if (needsComma) {
        sb.append(",");
      } else {
        needsComma = true;
      }
      JsoSplittable value = get(key);
      if (!value.isFunction()) {
        if ("$H".equals(key)) {
          // Ignore hashcode
          continue;
        }
        sb.append(JsonUtils.escapeValue(key));
        sb.append(":");
        value.stringifySlow(sb);
      }
    }
    sb.append("}");
  }
}
