/*
 * Copyright 2011 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.jjs.Correlation.Literal;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JArrayType;
import com.google.gwt.dev.jjs.ast.JClassLiteral;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JDeclarationStatement;
import com.google.gwt.dev.jjs.ast.JEnumType;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JField.Disposition;
import com.google.gwt.dev.jjs.ast.JFieldRef;
import com.google.gwt.dev.jjs.ast.JInterfaceType;
import com.google.gwt.dev.jjs.ast.JLiteral;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodBody;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JModVisitor;
import com.google.gwt.dev.jjs.ast.JNullLiteral;
import com.google.gwt.dev.jjs.ast.JParameter;
import com.google.gwt.dev.jjs.ast.JPrimitiveType;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JRecordType;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JRuntimeTypeReference;
import com.google.gwt.dev.jjs.ast.JType;
import com.google.gwt.dev.jjs.ast.RuntimeConstants;
import com.google.gwt.dev.jjs.ast.js.JsniClassLiteral;
import com.google.gwt.dev.jjs.ast.js.JsniMethodBody;
import com.google.gwt.dev.jjs.ast.js.JsniMethodRef;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsInvocation;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsNumberLiteral;
import com.google.gwt.thirdparty.guava.common.base.Joiner;
import com.google.gwt.thirdparty.guava.common.collect.ArrayListMultimap;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
import com.google.gwt.thirdparty.guava.common.collect.Iterables;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Multimap;
import com.google.gwt.thirdparty.guava.common.collect.Sets;

import java.util.Map;
import java.util.Set;

/**
 * Create fields to represent the mechanical implementation of class literals.
 * Must be done after all class literals are created, but before optimizations
 * begin. {@link ControlFlowAnalyzer} depends on this.
 * <p>
 * Class literals are implemented as static field references. The static fields
 * are all put into the special com.google.gwt.lang.ClassLiteralHolder class.
 * Ordinarily, accessing one of these fields would trigger a clinit to run, but
 * we've special-cased class literal fields to evaluate as top-level code before
 * the application starts running to avoid the clinit.
 *
 * Class literal factory methods are responsible for installing references
 * to themselves on the Object.clazz field of their JS runtime prototype
 * since getClass() is no longer an overridden method.  Prototypes can be
 * looked up via 'typeId' from the global prototypesByTypeId object, and so each
 * class literal factory method is passed the typeId of its type.
 * <p>
 */
public class ImplementClassLiteralsAsFields {
  public static final String NAME = ImplementClassLiteralsAsFields.class.getSimpleName();
  private static Map<Class<? extends JType>, ClassLiteralFactoryMethod>
      literalFactoryMethodByTypeClass  = new ImmutableMap.Builder()
          .put(JEnumType.class, ClassLiteralFactoryMethod.CREATE_FOR_ENUM)
          .put(JClassType.class, ClassLiteralFactoryMethod.CREATE_FOR_CLASS)
          .put(JRecordType.class, ClassLiteralFactoryMethod.CREATE_FOR_CLASS)
          .put(JInterfaceType.class, ClassLiteralFactoryMethod.CREATE_FOR_INTERFACE)
          .put(JPrimitiveType.class, ClassLiteralFactoryMethod.CREATE_FOR_PRIMITIVE)
          .build();

  /**
   * Class used to construct invocations to class literal factory methods.
   */
  public enum ClassLiteralFactoryMethod {
    CREATE_FOR_ENUM() {
      @Override
      JMethodCall createCall(SourceInfo info, JProgram program, JType type,
          JLiteral superclassLiteral) {
        JEnumType enumType = type.isEnumOrSubclass();
        assert enumType != null;

        // createForEnum(packageName, typeName, runtimeTypeReference, Enum.class,  type.values(),
        // type.valueOf(java/lang/String));
        JMethodCall call = createBaseCall(info, program, type, "Class.createForEnum");

        call.addArg(new JRuntimeTypeReference(info, program.getTypeJavaLangObject(),
            (JReferenceType) type));
        call.addArg(superclassLiteral);

        call.addArg(getStandardMethodAsArg(info, program, type, "values()"));
        call.addArg(getStandardMethodAsArg(info, program, type, "valueOf(Ljava/lang/String;)"));
        return call;
      }

      private JExpression getStandardMethodAsArg(SourceInfo info, JProgram program, JType type,
          String methodSignature) {
        JEnumType enumType = type.isEnumOrSubclass();

        if (enumType != type) {
          // The type is an anonymous subclass that represents one of the enum values
          return JNullLiteral.INSTANCE;
        }

        // This type is the base enum type not one of the anonymous classes that represent
        // enum values.
        for (JMethod method : enumType.getMethods()) {
          if (method.isStatic() && method.getSignature().startsWith(methodSignature)) {
            return new JsniMethodRef(info, method.getJsniSignature(true, false),
                method, program.getJavaScriptObject());
          }
        }

        // The method was pruned.
        return JNullLiteral.INSTANCE;
      }
    },
    CREATE_FOR_CLASS() {
      @Override
      JMethodCall createCall(SourceInfo info, JProgram program, JType type,
          JLiteral superclassLiteral) {

        // Class.createForClass(packageName, typeName, runtimeTypeReference, superclassliteral)
        JMethodCall call =
            createBaseCall(info, program, type, RuntimeConstants.CLASS_CREATE_FOR_CLASS);

        call.addArg(new JRuntimeTypeReference(info, program.getTypeJavaLangObject(),
            (JReferenceType) type));
        call.addArg(superclassLiteral);
        return call;
      }
    },
    CREATE_FOR_PRIMITIVE() {
      @Override
      JMethodCall createCall(SourceInfo info, JProgram program, JType type,
          JLiteral superclassLiteral) {

        // Class.createForPrimitive(typeName, typeSignature)
        JMethodCall call = new JMethodCall(info, null, program.getIndexedMethod(
                RuntimeConstants.CLASS_CREATE_FOR_PRIMITIVE));
        call.addArg(program.getStringLiteral(info, type.getShortName()));
        call.addArg(program.getStringLiteral(info, type.getJavahSignatureName()));
        return call;
      }
    },
    CREATE_FOR_INTERFACE() {
      @Override
      JMethodCall createCall(SourceInfo info, JProgram program, JType type,
          JLiteral superclassLiteral) {

        // Class.createForInterface(packageName, typeName)
        return createBaseCall(info, program, type, RuntimeConstants.CLASS_CREATE_FOR_INTERFACE);
      }
    };

    abstract JMethodCall createCall(SourceInfo info, JProgram program, JType type,
        JLiteral superclassLiteral);

    private static JMethodCall createBaseCall(SourceInfo info, JProgram program, JType type,
        String indexedMethodName) {

      String[] compoundName = maybeMangleJSOTypeName(type);
      JMethodCall call = new JMethodCall(info, null, program.getIndexedMethod(indexedMethodName),
          program.getStringLiteral(info, type.getPackageName()),
          getCompoundNameLiteral(program, info, compoundName));

      return call;
    }

    private static String[] maybeMangleJSOTypeName(JType type) {
      assert !(type instanceof JArrayType);
      String[] compoundName = type.getCompoundName();

      // Mangle the class name to match hosted mode.
      if (type.isJsoType()) {
        compoundName[compoundName.length - 1] = compoundName[compoundName.length - 1] + '$';
      }
      return compoundName;
    }

    private static JExpression getCompoundNameLiteral(final JProgram program, final SourceInfo info,
        String[] compoundName) {
      return program.getStringLiteral(info, Joiner.on('/').join(compoundName));
    }
  }

  private class NormalizeVisitor extends JModVisitor {
    @Override
    public void endVisit(JClassLiteral x, Context ctx) {
      JType type = x.getRefType();
      if (type instanceof JArrayType) {
        // Replace array class literals by an expression to obtain the class literal from the
        // leaf type of the array.
        JArrayType arrayType = (JArrayType) type;
        JClassLiteral leafTypeClassLiteral =
            new JClassLiteral(x.getSourceInfo(), arrayType.getLeafType());
        resolveClassLiteral(leafTypeClassLiteral);

        int dims = type.isJsNative() ? 1 : arrayType.getDims();
        JExpression arrayClassLiteralExpression = program.createArrayClassLiteralExpression(
            x.getSourceInfo(), leafTypeClassLiteral, dims);
        ctx.replaceMe(arrayClassLiteralExpression);
      } else {
        // Just resolve the class literal.
        resolveClassLiteral(x);
      }
    }

    @Override
    public void endVisit(JMethod x, Context ctx) {
      if (x.isJsMethodVarargs()) {
        // ImplementJsVarargs might insert an array creation for the varargs parameter which is not
        // seen by this pass.
        JParameter varargsParameter = Iterables.getLast(x.getParams());
        assert varargsParameter.isVarargs();
        resolveClassLiteralField(((JArrayType) varargsParameter.getType()).getLeafType());
      }
    }

    @Override
    public void endVisit(JsniClassLiteral x, Context ctx) {
      // JsniClassLiterals will be traversed explicitly in JsniMethodBody. For each JsniClassLiteral
      // in JsniMethodBody.classRefs there is at least a corresponding JsNameRef (with a jsni
      // identifier) that needs to be altered accordingly.
    }

    @Override
    public void endVisit(final JsniMethodBody jsniMethodBody, Context ctx) {
      if (jsniMethodBody.getClassRefs().size() == 0) {
        return;
      }

      final Multimap<String, JsniClassLiteral> jsniClassLiteralsByJsniReference =
          ArrayListMultimap.create();
      final JMethod getClassLiteralForArrayMethod =
          program.getIndexedMethod(RuntimeConstants.ARRAY_GET_CLASS_LITERAL_FOR_ARRAY);
      final String getClassLiteralForArrayMethodIdent =
          "@" + getClassLiteralForArrayMethod.getJsniSignature(true, false);

      boolean areThereArrayClassLiterals = false;
      for (JsniClassLiteral jsniClassLiteral : jsniMethodBody.getClassRefs()) {

        // Check for the presence of array class literals.
        if (jsniClassLiteral.getRefType() instanceof JArrayType) {
          areThereArrayClassLiterals = true;
        } else {
          // Resolve non array class literals.
          resolveClassLiteral(jsniClassLiteral);
        }

        // Map JSNI reference string to the actual JsniClassLiterals.
        jsniClassLiteralsByJsniReference.put(jsniClassLiteral.getIdent(), jsniClassLiteral);
      }

      if (!areThereArrayClassLiterals) {
        // No array class literal no need to explore the body.
        return;
      }

      final Set<JsniClassLiteral> newClassRefs = Sets.newLinkedHashSet();

      // Replace all arrays JSNI class literals with the its construction via the leaf type class
      // literal.
      JsModVisitor replaceJsniClassLiteralVisitor = new JsModVisitor() {

        @Override
        public void endVisit(JsNameRef x, JsContext ctx) {
          if (!x.isJsniReference()) {
            return;
          }

          if (jsniClassLiteralsByJsniReference.get(x.getIdent()).isEmpty()) {
            // The JsNameRef is not a class literal.
            return;
          }

          JsniClassLiteral jsniClassLiteral = jsniClassLiteralsByJsniReference.get(
              x.getIdent()).iterator().next();
          jsniClassLiteralsByJsniReference.remove(x.getIdent(), jsniClassLiteral);

          if (jsniClassLiteral.getRefType() instanceof JArrayType) {
            // Replace the array class literal by an expression that retrieves it from
            // that of the leaf type.
            JArrayType arrayType = (JArrayType) jsniClassLiteral.getRefType();
            JType leafType = arrayType.getLeafType();

            jsniClassLiteral = new JsniClassLiteral(jsniClassLiteral.getSourceInfo(), leafType);

            // Array.getClassLiteralForArray(leafType.class, dimensions)
            SourceInfo info = x.getSourceInfo();
            JsNameRef getArrayClassLiteralMethodNameRef =
                new JsNameRef(info, getClassLiteralForArrayMethodIdent);
            JsInvocation invocation = new JsInvocation(info, getArrayClassLiteralMethodNameRef,
                new JsNameRef(info, jsniClassLiteral.getIdent()),
                new JsNumberLiteral(info, arrayType.getDims()));
            // Finally resolve the class literal.
            resolveClassLiteral(jsniClassLiteral);
            ctx.replaceMe(invocation);
          }
          newClassRefs.add(jsniClassLiteral);
        }
      };
      replaceJsniClassLiteralVisitor.accept(jsniMethodBody.getFunc());
      if (replaceJsniClassLiteralVisitor.getNumMods() == 0) {
        // Nothing was changed, no need to replace JsniMethodBody.
        return;
      }

      JsniMethodBody newBody =
          new JsniMethodBody(jsniMethodBody.getSourceInfo(), jsniMethodBody.getFunc(),
          Lists.newArrayList(newClassRefs), jsniMethodBody.getJsniFieldRefs(),
          jsniMethodBody.getJsniMethodRefs(), jsniMethodBody.getUsedStrings());

      // Add getClassLiteralForArray as a JsniMethodRef.
      newBody.addJsniRef(
          new JsniMethodRef(jsniMethodBody.getSourceInfo(), getClassLiteralForArrayMethodIdent,
              getClassLiteralForArrayMethod, program.getJavaScriptObject()));

      ctx.replaceMe(newBody);
    }
  }

  public static void exec(JProgram program, boolean shouldOptimize) {
    new ImplementClassLiteralsAsFields(program, shouldOptimize).execImpl();
  }

  private final Map<JType, JField> classLiteralFields = Maps.newIdentityHashMap();
  private final JMethodBody classLiteralHolderClinitBody;
  private final JProgram program;
  private final JClassType typeClassLiteralHolder;
  private final boolean shouldOptimize;

  private ImplementClassLiteralsAsFields(JProgram program, boolean shouldOptimize) {
    this.program = program;
    this.typeClassLiteralHolder = program.getTypeClassLiteralHolder();
    this.classLiteralHolderClinitBody =
        (JMethodBody) typeClassLiteralHolder.getClinitMethod().getBody();
    this.shouldOptimize = shouldOptimize;
    assert program.getDeclaredTypes().contains(typeClassLiteralHolder);
  }

  private JLiteral getSuperclassClassLiteral(SourceInfo info, JType type) {
    if (!(type instanceof JClassType) ||  ((JClassType) type).getSuperClass() == null) {
      return JNullLiteral.INSTANCE;
    }

    JClassType superClass = ((JClassType) type).getSuperClass();

    if (superClass.isJsNative()) {
      // Class object for subclasses of native JsType will return Object.class as their super class
      // class literal; this is done so that in invariant that "super" class literals are literals
      // of a actual superclass.
      superClass = program.getTypeJavaLangObject();
    }

    return createDependentClassLiteral(info, superClass);
  }

  private JClassLiteral createDependentClassLiteral(SourceInfo info, JType type) {
    JClassLiteral classLiteral = new JClassLiteral(info.makeChild(), type);
    classLiteral.setField(resolveClassLiteralField(classLiteral.getRefType()));
    return classLiteral;
  }

  private void execImpl() {
    try (OptimizerStats stats = OptimizerStats.normalizer(NAME)) {
      if (!shouldOptimize) {
        // Create all class literals regardless of whether they are referenced or not.
        for (JPrimitiveType type : JPrimitiveType.types) {
          resolveClassLiteralField(type);
        }
        for (JType type : program.getDeclaredTypes()) {
          resolveClassLiteralField(type);
        }
      }
      NormalizeVisitor visitor = new NormalizeVisitor();
      visitor.accept(program);
      program.recordClassLiteralFields(classLiteralFields);

      stats.recordModified(visitor.getNumMods());
    }
  }

  /**
   * Create an expression that will evaluate, at run time, to the class literal.
   * Causes recursive literal create (super type, array element type). Examples:
   *
   * Class:
   *
   * <pre>
   * Class.createForClass("java.lang.", "Object", /JRuntimeTypeReference/"java.lang.Object", null)
   * Class.createForClass("java.lang.", "Exception", /JRuntimeTypeReference/"java.lang.Exception",
   *   Throwable.class)
   * </pre>
   *
   * Interface:
   *
   * <pre>
   * Class.createForInterface(&quot;java.lang.&quot;, &quot;Comparable&quot;)
   * </pre>
   *
   * Arrays are lazily created.
   *
   * Primitive:
   *
   * <pre>
   * Class.createForPrimitive(&quot;&quot;, &quot;int&quot;, &quot;I&quot;)
   * </pre>
   *
   * Enum:
   *
   * <pre>
   * Class.createForEnum("com.example.", "MyEnum", /JRuntimeTypeReference/"com.example.MyEnum",
   *     Enum.class, public static MyEnum[] values(), public static MyEnum valueOf(String name))
   * </pre>
   *
   * Enum subclass:
   *
   * <pre>
   * Class.createForEnum("com.example.", "MyEnum$1", /JRuntimeTypeReference/"com.example.MyEnum$1",
   *   MyEnum.class, null, null))
   * </pre>
   */
  private JMethodCall createLiteralCall(SourceInfo info, JProgram program, JType type) {
    type = type.getUnderlyingType();

    Class<? extends JType>  typeClass = type.getClass();
    if (type.isEnumOrSubclass() != null) {
      typeClass = JEnumType.class;
    }

    return literalFactoryMethodByTypeClass.get(typeClass).createCall(info, program, type,
        getSuperclassClassLiteral(info, type));
  }

  private void resolveClassLiteral(JClassLiteral x) {
    JField field = resolveClassLiteralField(x.getRefType());
    x.setField(field);
  }

  /**
   * Resolve a class literal field. Takes the form:
   *
   * <pre>
   * class ClassLiteralHolder {
   *   Class Ljava_lang_Object_2_classLit =
   *       Class.createForClass("java.lang.", "Object", /JNameOf/"java.lang.Object", null)
   * }
   * </pre>
   */
  private JField resolveClassLiteralField(JType type) {
    type = type.isJsNative() || type.isJsFunction() || type.isJsFunctionImplementation()
        ? program.getJavaScriptObject() : program.normalizeJsoType(type);
    JField field = classLiteralFields.get(type);
    if (field == null) {
      // Create the allocation expression FIRST since this may be recursive on
      // super type (this forces the super type classLit to be created first).
      SourceInfo info = type.getSourceInfo().makeChild();
      assert !(type instanceof JArrayType);

      JMethodCall classLiteralCreationExpression = createLiteralCall(info, program, type);

      // Create a field in the class literal holder to hold the object.
      field =
          new JField(info, getClassLiteralFieldName(type), typeClassLiteralHolder, program
              .getTypeJavaLangClass(), true, Disposition.FINAL);
      typeClassLiteralHolder.addField(field);
      info.addCorrelation(info.getCorrelator().by(Literal.CLASS));

      // Initialize the field.
      JFieldRef fieldRef = new JFieldRef(info, null, field, typeClassLiteralHolder);
      JDeclarationStatement decl =
          new JDeclarationStatement(info, fieldRef, classLiteralCreationExpression);
      classLiteralHolderClinitBody.getBlock().addStmt(decl);
      classLiteralFields.put(type, field);
    }
    return field;
  }

  private static String getClassLiteralFieldName(JType type) {
    return JjsUtils.classLiteralFieldNameFromJavahTypeSignatureName(type.getJavahSignatureName());
  }
}
