/*
 * Copyright 2013 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.dev.jjs.impl;

import com.google.gwt.dev.javac.JsInteropUtil;
import com.google.gwt.dev.jjs.ast.JArrayType;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JPrimitiveType;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JType;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;

import java.util.Map;

/**
 * TypeCategory classifies Java types into different categories. <p>
 *
 * These are used in Cast checking and array implementation.
 */
public enum TypeCategory {
  /*
   * Enum list is kept in sync with the one in Array.java.
   *
   * TYPE_JAVA_LANG_* is kept in sync with JProgram.DispatchType.
   *
   * TypeCategory.castInstanceOfQualifier is kept in sync with Cast.java.
   *
   * Also note that we have separate categories for boxed types (e.g. JAVA_LANG_DOUBLE vs.
   * PRIMITIVE_NUMBER), as we need the separation of the two for array initialization purposes (e.g.
   * initialize to zero vs. null).
   */

  TYPE_JAVA_OBJECT("", true, false),
  TYPE_JAVA_OBJECT_OR_JSO("AllowJso", true, false),
  TYPE_JSO("Jso"),
  TYPE_ARRAY("Array"),
  TYPE_JSO_ARRAY("JsoArray", true, false),
  TYPE_JAVA_LANG_OBJECT("AllowJso", true, false),
  TYPE_JAVA_LANG_STRING("String"),
  TYPE_JAVA_LANG_DOUBLE("Double"),
  TYPE_JAVA_LANG_BOOLEAN("Boolean"),
  TYPE_JS_NATIVE("Native", false, true),
  TYPE_JS_UNKNOWN_NATIVE("UnknownNative"),
  TYPE_JS_FUNCTION("Function"),
  TYPE_JS_OBJECT("JsObject"),
  TYPE_JS_ARRAY("JsArray"),
  // Primitive types are meant to be consecutive.
  TYPE_PRIMITIVE_LONG,
  TYPE_PRIMITIVE_NUMBER,
  TYPE_PRIMITIVE_BOOLEAN;

  private final String castInstanceOfQualifier;
  private final boolean requiresTypeId;
  private final boolean requiresConstructor;

  TypeCategory() {
    this(null, false, false);
  }

  TypeCategory(String castInstanceOfQualifier) {
    this(castInstanceOfQualifier, false, false);
  }

  TypeCategory(
      String castInstanceOfQualifier, boolean requiresTypeId, boolean requiresConstructor) {
    this.castInstanceOfQualifier = castInstanceOfQualifier;
    this.requiresTypeId = requiresTypeId;
    this.requiresConstructor = requiresConstructor;
  }

  public String castInstanceOfQualifier() {
    return castInstanceOfQualifier;
  }

  public boolean requiresTypeId() {
    return requiresTypeId;
  }

  public boolean requiresJsConstructor() {
    return requiresConstructor;
  }
  /**
   * Determines the type category for a specific type.
   */
  public static TypeCategory typeCategoryForType(JType type, JProgram program) {
    if (type.isPrimitiveType()) {
      if (type == JPrimitiveType.BOOLEAN) {
        return TypeCategory.TYPE_PRIMITIVE_BOOLEAN;
      } else if (type == JPrimitiveType.LONG) {
        return TypeCategory.TYPE_PRIMITIVE_LONG;
      } else {
        return TypeCategory.TYPE_PRIMITIVE_NUMBER;
      }
    }

    assert type instanceof JReferenceType;
    type = type.getUnderlyingType();
    if (type == program.getTypeJavaLangObjectArray()) {
      return TypeCategory.TYPE_ARRAY;
    } else if (isJsoArray(type)) {
      return TypeCategory.TYPE_JSO_ARRAY;
    } else if (getJsSpecialType(type) != null) {
      return getJsSpecialType(type);
    } else if (program.isUntypedArrayType(type)) {
      return TypeCategory.TYPE_JS_ARRAY;
    } else if (type == program.getTypeJavaLangObject()) {
      return TypeCategory.TYPE_JAVA_LANG_OBJECT;
    } else if (program.getRepresentedAsNativeTypesDispatchMap().containsKey(type)) {
      return program.getRepresentedAsNativeTypesDispatchMap().get(type).getTypeCategory();
    } else if (program.typeOracle.isEffectivelyJavaScriptObject(type)) {
      return TypeCategory.TYPE_JSO;
    } else if (program.typeOracle.isDualJsoInterface(type)) {
      return TypeCategory.TYPE_JAVA_OBJECT_OR_JSO;
    } else if (program.typeOracle.isNoOpCast(type)) {
      return TypeCategory.TYPE_JS_UNKNOWN_NATIVE;
    } else if (type instanceof JClassType && type.isJsNative()) {
      return TypeCategory.TYPE_JS_NATIVE;
    } else if (type.isJsFunction() || type.isJsFunctionImplementation()) {
      return TypeCategory.TYPE_JS_FUNCTION;
    }
    return TypeCategory.TYPE_JAVA_OBJECT;
  }

  private static Map<String, TypeCategory> specialGlobalNames =
      ImmutableMap.<String,TypeCategory>builder()
          .put("Object", TYPE_JS_OBJECT)
          .put("Function", TYPE_JS_FUNCTION)
          .put("Array", TYPE_JS_ARRAY)
          .put("Number", TYPE_JAVA_LANG_DOUBLE)
          .put("String", TYPE_JAVA_LANG_STRING)
          .build();

  private static TypeCategory getJsSpecialType(JType type) {
    if (!(type instanceof JClassType) || !type.isJsNative()) {
      return null;
    }

    JClassType classType = (JClassType) type;
    if (!JsInteropUtil.isGlobal(classType.getJsNamespace())) {
      return null;
    }

    return specialGlobalNames.get(classType.getJsName());
  }

  private static boolean isJsoArray(JType type) {
    if (!type.isArrayType()) {
      return false;
    }

    JArrayType arrayType = (JArrayType) type;
    return arrayType.getLeafType().isJsoType();
  }
}
