/*
 * Copyright 2008 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.gwt.lang;

import static javaemul.internal.InternalPreconditions.checkType;

import com.google.gwt.core.client.JavaScriptObject;

import javaemul.internal.annotations.HasNoSideEffects;

/**
 * This is a magic class the compiler uses to perform any cast operations that require code.<br />
 *
 * The cast operations are only as accurate as the contents of the castableTypeMaps and should not
 * be used directly by user code. The compiler takes care to record most cast operations in user
 * code so that it can build limited but accurate castableTypeMaps.
 */
@SuppressWarnings("checkstyle:MethodName")
final class Cast {

  /**
   * As plain JavaScript Strings (not monkey patched) are used to model Java Strings,
   * {@code  stringCastMap} stores runtime type info for cast purposes for string objects.
   * Same applies to Double and Boolean.
   *
   * NOTE: it is important that the field is left uninitialized so that Cast does not
   * require a clinit.
   */
  // NOTE: if any of these three are edited, update JProgram.DispatchType's constructor
  private static JavaScriptObject stringCastMap;
  private static JavaScriptObject doubleCastMap;
  private static JavaScriptObject booleanCastMap;

  @HasNoSideEffects
  static native boolean canCast(Object src, JavaScriptObject dstId) /*-{
    if (@com.google.gwt.lang.Cast::instanceOfString(*)(src)) {
      return !!@com.google.gwt.lang.Cast::stringCastMap[dstId];
    } else if (src.@java.lang.Object::castableTypeMap) {
      return !!src.@java.lang.Object::castableTypeMap[dstId];
    } else if (@com.google.gwt.lang.Cast::instanceOfDouble(*)(src)) {
      return !!@com.google.gwt.lang.Cast::doubleCastMap[dstId];
    } else if (@com.google.gwt.lang.Cast::instanceOfBoolean(*)(src)) {
      return !!@com.google.gwt.lang.Cast::booleanCastMap[dstId];
    }
    return false;
  }-*/;

  @HasNoSideEffects
  static native boolean canCastClass(Class<?> srcClazz, Class<?> dstClass) /*-{
    var srcTypeId = srcClazz.@java.lang.Class::typeId;
    var dstTypeId = dstClass.@java.lang.Class::typeId;
    var prototype = @com.google.gwt.lang.Runtime::prototypesByTypeId[srcTypeId];
    return @com.google.gwt.lang.Cast::canCast(*)(prototype, dstTypeId);
  }-*/;

  static native String charToString(char x) /*-{
    return String.fromCharCode(x);
  }-*/;

  static Object castTo(Object src, JavaScriptObject dstId) {
    checkType(src == null || canCast(src, dstId));
    return src;
  }

  // NOTE: if any of these three are edited, update JProgram.DispatchType's constructor
  static Object castToString(Object src) {
    checkType(src == null || instanceOfString(src));
    return src;
  }

  static Object castToDouble(Object src) {
    checkType(src == null || instanceOfDouble(src));
    return src;
  }

  static Object castToBoolean(Object src) {
    checkType(src == null || instanceOfBoolean(src));
    return src;
  }

  /**
   * Allow a cast to an java.lang.Object array, accepting also untyped arrays.
   */
  static Object castToArray(Object src) {
    checkType(src == null || instanceOfArray(src));
    return src;
  }

  /**
   * Allow a cast to an array of Jsos, accepting also untyped arrays.
   */
  static Object castToJsoArray(Object src, JavaScriptObject dstId) {
    checkType(src == null || instanceOfJsoArray(src, dstId));
    return src;
  }

  /**
   * Allow a cast to (untyped) array. This case covers single and multidimensional JsType arrays.
   */
  static Object castToJsArray(Object src) {
    checkType(src == null || instanceOfJsArray(src));
    return src;
  }

  /**
   * Allow a dynamic cast to an object, always succeeding if it's a JSO.
   */
  static Object castToAllowJso(Object src, JavaScriptObject dstId) {
    checkType(src == null || isJavaScriptObject(src) || canCast(src, dstId));
    return src;
  }

  /**
   * Allow a cast to JSO only if there's no type ID.
   */
  static Object castToJso(Object src) {
    checkType(src == null || isJavaScriptObject(src));
    return src;
  }

  /**
   * Allow a dynamic cast to a JsFunction interface only if it is a function.
   */
  static Object castToFunction(Object src) {
    checkType(src == null || isFunction(src));
    return src;
  }

  /**
   * Allow a dynamic cast to a native GLOBAL.Object if it is JavaScript object.
   */
  static Object castToJsObject(Object src) {
    checkType(src == null || isJsObject(src));
    return src;
  }

  /**
   * A dynamic cast that optionally checks for JsType prototypes.
   */
  static Object castToNative(Object src, JavaScriptObject jsType) {
    checkType(src == null ||  jsinstanceOf(src, jsType));
    return src;
  }

  static Object castToUnknownNative(Object src) {
    return src;
  }

  static boolean instanceOf(Object src, JavaScriptObject dstId) {
    return (src != null) && canCast(src, dstId);
  }

  @HasNoSideEffects
  static native boolean instanceOfString(Object src) /*-{
    return typeof(src) === "string";
  }-*/;

  @HasNoSideEffects
  static native boolean instanceOfDouble(Object src) /*-{
    return typeof(src) === "number";
  }-*/;

  @HasNoSideEffects
  static native boolean instanceOfBoolean(Object src) /*-{
    return typeof(src) === "boolean";
  }-*/;

  /**
   * Returns true if {@code src} is Java object array or an untyped array.
   */
  static boolean instanceOfArray(Object src) {
    return isArray(src) && !Array.isPrimitiveArray(src);
  }

  /**
   * Returns true if {@code src} is Java object array or an untyped array.
   */
  static boolean instanceOfJsoArray(Object src, JavaScriptObject dstId) {
    return canCast(src, dstId) || !Util.hasTypeMarker(src) && isArray(src);
  }

  /**
   * Returns true if {@code src} is an array (native or not).
   */
  static boolean instanceOfJsArray(Object src) {
    return isArray(src);
  }

  static boolean instanceOfNative(Object src, JavaScriptObject jsType) {
    return jsinstanceOf(src, jsType);
  }

  static boolean instanceOfUnknownNative(Object src) {
    return src != null;
  }

  static boolean instanceOfJso(Object src) {
    return (src != null) && isJavaScriptObject(src);
  }

  /**
   * Returns true if the object is a Java object and can be cast, or if it's a
   * non-null JSO.
   */
  static boolean instanceOfAllowJso(Object src, JavaScriptObject dstId) {
    return (src != null) && (isJavaScriptObject(src) || canCast(src, dstId));
  }

  /**
   * Returns true if the object is a function.
   */
  static boolean instanceOfFunction(Object src) {
    return (src != null) && isFunction(src);
  }

  /**
   * Returns true if the object is a JS object.
   */
  static boolean instanceOfJsObject(Object src) {
    return (src != null) && isJsObject(src);
  }

  /**
   * Returns whether the Object is a function.
   */
  @HasNoSideEffects
  private static native boolean isFunction(Object src)/*-{
    return typeof(src) === "function";
  }-*/;

  @HasNoSideEffects
  private static native boolean isJsObject(Object src)/*-{
    return typeof(src) === "object" || typeof (src) == "function";
  }-*/;

  @HasNoSideEffects
  static boolean isJavaScriptObject(Object src) {
    // TODO(rluble): should return false to for plain arrays.
    return isJsObjectOrFunction(src) && !Util.hasTypeMarker(src);
  }

  @HasNoSideEffects
  private static native boolean isJsObjectOrFunction(Object src) /*-{
    return typeof(src) === "object" || typeof(src) === "function";
  }-*/;

  /**
   * Uses the not operator to perform a null-check; do NOT use on anything that
   * could be a String, 'unboxed' Double, or 'unboxed' Boolean.
   */
  static native boolean isNotNull(Object src) /*-{
    // Coerce to boolean.
    return !!src;
  }-*/;

  /**
   * Uses the not operator to perform a null-check; do NOT use on anything that
   * could be a String, 'unboxed' Double, or 'unboxed' Boolean.
   */
  static native boolean isNull(Object src) /*-{
    return !src;
  }-*/;

  static native boolean jsEquals(Object a, Object b) /*-{
    return a == b;
  }-*/;

  /**
   * Determine if object is an instanceof jsType regardless of window or frame.
   */
  @HasNoSideEffects
  private static native boolean jsinstanceOf(Object obj, JavaScriptObject jsType) /*-{
    return obj && jsType && obj instanceof jsType;
  }-*/;

  static native boolean jsNotEquals(Object a, Object b) /*-{
    return a != b;
  }-*/;

  static native Object maskUndefined(Object src) /*-{
    return (src == null) ? null : src;
  }-*/;

  /**
   * See JLS 5.1.3.
   */
  static native byte narrow_byte(double x) /*-{
    return x << 24 >> 24;
  }-*/;

  /**
   * See JLS 5.1.3.
   */
  static native char narrow_char(double x) /*-{
    return x & 0xFFFF;
  }-*/;

  /**
   * See JLS 5.1.3.
   */
  static native int narrow_int(double x) /*-{
    return x | 0;
  }-*/;

  /**
   * See JLS 5.1.3.
   */
  static native short narrow_short(double x) /*-{
    return x << 16 >> 16;
  }-*/;

  /**
   * See JLS 5.1.3 for why we do a two-step cast. First we round to int, then
   * narrow to byte.
   */
  static byte round_byte(double x) {
    return narrow_byte(round_int(x));
  }

  /**
   * See JLS 5.1.3 for why we do a two-step cast. First we round to int, then
   * narrow to char.
   */
  static char round_char(double x) {
    return narrow_char(round_int(x));
  }

  /**
   * See JLS 5.1.3.
   */
  static native int round_int(double x) /*-{
    // TODO: reference java.lang.Integer::MAX_VALUE when we get clinits fixed
    return Math.max(Math.min(x, 2147483647), -2147483648) | 0;
  }-*/;

  /**
   * See JLS 5.1.3 for why we do a two-step cast. First we rount to int, then
   * narrow to short.
   */
  static short round_short(double x) {
    return narrow_short(round_int(x));
  }

  /**
   * Check a statically false cast, which can succeed if the argument is null.
   * Called by compiler-generated code based on static type information.
   */
  static Object throwClassCastExceptionUnlessNull(Object o) throws ClassCastException {
    checkType(o == null);
    return o;
  }

  /**
   * Returns true if Object can dispatch instance methods and does not need a compiler
   * provided trampoline.
   *
   * Java non primitive objects fall into 3 classes: Strings, arrays (of primitive or non primitive
   * types) and regular Java Objects (all others).
   *
   * Only regular Java object have dynamic instance dispatch, strings and arrays need compiler
   * generated trampolines to implement instance dispatch.
   */
  static boolean hasJavaObjectVirtualDispatch(Object src) {
    return !isArray(src) && Util.hasTypeMarker(src);
  }

  @HasNoSideEffects
  /**
   * Returns true if {@code src} is a Java array.
   */
  static native boolean isArray(Object src) /*-{
    return Array.isArray(src);
  }-*/;

  @HasNoSideEffects
  static native Class<?> getClass(Object array) /*-{
    return array.@java.lang.Object::___clazz
        || Array.isArray(array) && @JavaScriptObject[]::class
        || @JavaScriptObject::class;
  }-*/;
}
