/*
 * Copyright 2006 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 java.lang;

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

import java.lang.reflect.Type;
import javaemul.internal.annotations.DoNotInline;

/**
 * Generally unsupported. This class is provided so that the GWT compiler can
 * choke down class literal references.
 * <p>
 * NOTE: The code in this class is very sensitive and should keep its dependencies upon other
 * classes to a minimum.
 *
 * @param <T> the type of the object
 */
public final class Class<T> implements Type {

  private static final int PRIMITIVE = 0x00000001;
  private static final int INTERFACE = 0x00000002;
  private static final int ARRAY = 0x00000004;
  private static final int ENUM = 0x00000008;

  /**
   * Create a Class object for an array.<p>
   *
   * Arrays are not registered in the prototype table and get the class literal explicitly at
   * construction.<p>
   *
   * NOTE: this method is accessed through JSNI (Violator pattern) to avoid changing the public API.
   */
  private static native <T> Class<T> getClassLiteralForArray(Class<?> leafClass,
      int dimensions) /*-{
    var arrayLiterals = leafClass.@Class::arrayLiterals = leafClass.@Class::arrayLiterals || [];
    return arrayLiterals[dimensions] || (arrayLiterals[dimensions] =
            leafClass.@Class::createClassLiteralForArray(I)(dimensions));
  }-*/;

  private <T> Class<T> createClassLiteralForArray(int dimensions) {
    Class<T> clazz = new Class<T>();
    clazz.modifiers = ARRAY;
    clazz.superclass = Object.class;
    if (dimensions > 1) {
      clazz.componentType = getClassLiteralForArray(this, dimensions - 1);
    } else {
      clazz.componentType = this;
    }
    return clazz;
  }

  /**
   * Create a Class object for a class.
   *
   * @skip
   */
  @DoNotInline
  static <T> Class<T> createForClass(String packageName, String compoundClassName,
      JavaScriptObject typeId, Class<? super T> superclass) {
    Class<T> clazz = createClassObject(packageName, compoundClassName, typeId);
    maybeSetClassLiteral(typeId, clazz);
    clazz.superclass = superclass;
    return clazz;
  }

  /**
   * Create a Class object for an enum.
   *
   * @skip
   */
  @DoNotInline
  static <T> Class<T> createForEnum(String packageName, String compoundClassName,
      JavaScriptObject typeId, Class<? super T> superclass,
      JavaScriptObject enumConstantsFunc, JavaScriptObject enumValueOfFunc) {
    Class<T> clazz = createClassObject(packageName, compoundClassName, typeId);
    maybeSetClassLiteral(typeId, clazz);
    clazz.modifiers = (enumConstantsFunc != null) ? ENUM : 0;
    clazz.superclass = clazz.enumSuperclass = superclass;
    clazz.enumConstantsFunc = enumConstantsFunc;
    clazz.enumValueOfFunc = enumValueOfFunc;
    return clazz;
  }

  /**
   * Create a Class object for an interface.
   *
   * @skip
   */
  @DoNotInline
  static <T> Class<T> createForInterface(String packageName, String compoundClassName) {
    Class<T> clazz = createClassObject(packageName, compoundClassName, null);
    clazz.modifiers = INTERFACE;
    return clazz;
  }

  /**
   * Create a Class object for a primitive.
   *
   * @skip
   */
  @DoNotInline
  static Class<?> createForPrimitive(String className, JavaScriptObject primitiveTypeId) {
    Class<?> clazz = createClassObject("", className, primitiveTypeId);
    clazz.typeId = primitiveTypeId;
    clazz.modifiers = PRIMITIVE;
    return clazz;
  }

  /**
    * Used by {@link WebModePayloadSink} to create uninitialized instances.
    */
  @DoNotInline
  static native JavaScriptObject getPrototypeForClass(Class<?> clazz) /*-{
    if (clazz.@Class::isPrimitive()()) {
      return null;
    }
    var typeId = clazz.@Class::typeId;
    return @com.google.gwt.lang.Runtime::prototypesByTypeId[typeId];
  }-*/;

  public static boolean isClassMetadataEnabled() {
    // This body may be replaced by the compiler
    return true;
  }

  /**
   * null implies non-instantiable type, with no entries in
   * {@link Runtime::prototypesByTypeId}.
   */
  private static native boolean isInstantiable(JavaScriptObject typeId) /*-{
    return !!typeId;
  }-*/;

  /**
   * Creates the class object for a type and initiliazes its fields.
   */
  private static <T> Class<T> createClassObject(String packageName, String compoundClassName,
      JavaScriptObject typeId) {
    Class<T> clazz = new Class<T>();
    if (isClassMetadataEnabled()) {
      clazz.packageName = packageName;
      clazz.compoundName = compoundClassName;
    } else {
      synthesizeClassNamesFromTypeId(clazz, typeId);
    }
    return clazz;
  }

  /**
   * Install class literal into prototype.clazz field (if type is instantiable) such that
   * Object.getClass() returning this.clazz returns the literal. Also stores typeId on class literal
   * for looking up prototypes given a literal. This is used for deRPC at the moment, but may be
   * used to implement Class.newInstance() in the future.
   *
   * If the prototype for typeId has not yet been created, then install the literal into a
   * placeholder array to differentiate the two cases.
   */
  private static native void maybeSetClassLiteral(JavaScriptObject typeId, Class<?> clazz) /*-{
    var proto;
    if (!typeId) {
      // Type is not instantiable, hence not registered in the metadata table.
      return;
    }
    clazz.@Class::typeId = typeId;
    // Guarantees virtual method won't be pruned by using a JSNI ref
    // This is required because deRPC needs to call it.
    var prototype  = @Class::getPrototypeForClass(Ljava/lang/Class;)(clazz);
    // A class literal may be referenced prior to an async-loaded vtable setup
    // For example, class literal lives in inital fragment,
    // but type is instantiated in another fragment
    if (!prototype) {
      // Leave a place holder for now to be filled in by __defineClass__ later.
      // TODO(rluble): Do not rely on the fact that if the entry is an array it is a placeholder.
      @com.google.gwt.lang.Runtime::prototypesByTypeId[typeId] = [clazz];
      return;
    }
    // Type already registered in the metadata table, install the class literal in the appropriate
    // prototype field.
    prototype.@java.lang.Object::___clazz = clazz;
  }-*/;

  /**
   * Initiliazes {@code clazz} names from metadata.
   * <p>
   * Written in JSNI to minimize dependencies (on String.+).
   */
  private static native void initializeNames(Class<?> clazz) /*-{
    if (clazz.@Class::isArray(*)()) {
      var componentType = clazz.@Class::componentType;
      if (componentType.@Class::isPrimitive()()) {
        clazz.@Class::typeName = "[" + componentType.@Class::typeId;
      } else  if (!componentType.@Class::isArray()()) {
        clazz.@Class::typeName = "[L" + componentType.@Class::getName()() + ";";
      } else {
        clazz.@Class::typeName = "[" + componentType.@Class::getName()();
      }
      clazz.@Class::canonicalName = componentType.@Class::getCanonicalName()() + "[]";
      clazz.@Class::simpleName = componentType.@Class::getSimpleName()() + "[]";
      return;
    }

    var packageName = clazz.@Class::packageName;
    var compoundName = clazz.@Class::compoundName;
    compoundName =  compoundName.split("/");
    clazz.@Class::typeName =
        @Class::join(*)('.', [packageName , @Class::join(*)("$", compoundName)]);
    clazz.@Class::canonicalName =
        @Class::join(*)('.', [packageName , @Class::join(*)(".", compoundName)]);
    clazz.@Class::simpleName = compoundName[compoundName.length - 1];
  }-*/;

  /**
   * Joins an array of strings with the specified separator.
   * <p>
   * Written in JSNI to minimize dependencies (on String.+).
   */
  private static native String join(String separator, JavaScriptObject strings) /*-{
    var i = 0;
    while (!strings[i] || strings[i] == "") {
      i++;
    }
    var result = strings[i++];
    for (; i < strings.length; i++) {
      if  (!strings[i] || strings[i] == "") {
        continue;
      }
      result += separator + strings[i];
    }
    return result;
  }-*/;

  /**
   * Initiliazes {@code clazz} names from typeIds.
   * <p>
   * Only called if metadata IS disabled.
   * <p>
   * Written in JSNI to minimize dependencies (on toString() and String.+).
   */
  static native void synthesizeClassNamesFromTypeId(Class<?> clazz, JavaScriptObject typeId) /*-{
     // The initial "" + in the below code is to prevent clazz.getAnonymousId from
     // being autoboxed. The class literal creation code is run very early
     // during application start up, before class Integer has been initialized.

    clazz.@Class::typeName = "Class$" +
        (!!typeId ? "S" + typeId : "" + clazz.@Class::sequentialId);
    clazz.@Class::canonicalName = clazz.@Class::typeName;
    clazz.@Class::simpleName = clazz.@Class::typeName;
  }-*/;

  /**
   * Sets the class object for primitives.
   * <p>
   * Written in JSNI to minimize dependencies (on (String)+).
   */
  static native void synthesizePrimitiveNamesFromTypeId(Class<?> clazz,
      JavaScriptObject primitiveTypeId) /*-{
    clazz.@Class::typeName = "Class$" + primitiveTypeId;
    clazz.@Class::canonicalName = clazz.@Class::typeName;
    clazz.@Class::simpleName = clazz.@Class::typeName;
  }-*/;

  JavaScriptObject enumValueOfFunc;

  int modifiers;

  private Class<?> componentType;

  @SuppressWarnings("unused")
  private JavaScriptObject enumConstantsFunc;

  private Class<? super T> enumSuperclass;

  private Class<? super T> superclass;

  private String simpleName;

  private String typeName;

  private String canonicalName;

  private String packageName;

  private String compoundName;

  private JavaScriptObject typeId;

  private JavaScriptObject arrayLiterals;

  private JavaScriptObject jsConstructor;

  // Assign a sequential id to each class literal to avoid calling hashCode which bring Impl as
  // a dependency.
  private int sequentialId = nextSequentialId++;

  private static int nextSequentialId = 1;

  /**
   * Not publicly instantiable.
   *
   * @skip
   */
  private Class() {
    // Initialize in constructor to avoid V8 invalidating hidden classes.
    typeName = null;
    simpleName = null;
    packageName = null;
    compoundName = null;
    canonicalName = null;
    typeId = null;
    arrayLiterals = null;
  }

  public boolean desiredAssertionStatus() {
    // This body is ignored by the JJS compiler and a new one is
    // synthesized at compile-time based on the actual compilation arguments.
    return false;
  }

  private void ensureNamesAreInitialized() {
    if (typeName != null) {
      return;
    }
    initializeNames(this);
  }

  public String getCanonicalName() {
    ensureNamesAreInitialized();
    return canonicalName;
  }

  public Class<?> getComponentType() {
    return componentType;
  }

  public native T[] getEnumConstants() /*-{
    return this.@Class::enumConstantsFunc
        && (this.@Class::enumConstantsFunc)();
  }-*/;

  public String getName() {
    ensureNamesAreInitialized();
    return typeName;
  }

  public String getSimpleName() {
    ensureNamesAreInitialized();
    return simpleName;
  }

  public Class<? super T> getSuperclass() {
    if (isClassMetadataEnabled()) {
      return superclass;
    } else {
      return null;
    }
  }

  public boolean isArray() {
    return (modifiers & ARRAY) != 0;
  }

  public boolean isEnum() {
    return (modifiers & ENUM) != 0;
  }

  public boolean isInterface() {
    return (modifiers & INTERFACE) != 0;
  }

  public boolean isPrimitive() {
    return (modifiers & PRIMITIVE) != 0;
  }

  @Override
  public String toString() {
    return (isInterface() ? "interface " : (isPrimitive() ? "" : "class "))
        + getName();
  }

  /**
   * Used by Enum to allow getSuperclass() to be pruned.
   */
  Class<? super T> getEnumSuperclass() {
    return enumSuperclass;
  }
}
