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

import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JArrayRef;
import com.google.gwt.dev.jjs.ast.JArrayType;
import com.google.gwt.dev.jjs.ast.JBinaryOperation;
import com.google.gwt.dev.jjs.ast.JBinaryOperator;
import com.google.gwt.dev.jjs.ast.JCastMap;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JIntLiteral;
import com.google.gwt.dev.jjs.ast.JLiteral;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JModVisitor;
import com.google.gwt.dev.jjs.ast.JNewArray;
import com.google.gwt.dev.jjs.ast.JProgram;
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.JsonArray;

import java.util.Collections;
import java.util.List;

/**
 * Replace array accesses and instantiations with calls to the Array class.
 * Depends on {@link CompoundAssignmentNormalizer} and {@link ImplementCastsAndTypeChecks}
 * having already run.
 */
public class ArrayNormalizer {

  private class ArrayVisitor extends JModVisitor {

    @Override
    public void endVisit(JBinaryOperation x, Context ctx) {
      JArrayRef arrayRef = needsSetCheck(x);
      if (arrayRef == null) {
        return;
      }

      // replace this assignment with a call to setCheck()
      JMethodCall call = new JMethodCall(x.getSourceInfo(), null, setCheckMethod);
      call.addArgs(arrayRef.getInstance(), arrayRef.getIndexExpr(), x.getRhs());
      ctx.replaceMe(call);
    }

    @Override
    public void endVisit(JNewArray x, Context ctx) {
      JArrayType type = x.getArrayType();

      List<JExpression> initializers = x.getInitializers();
      if (initializers != null) {
        JsonArray initializerArray = getInitializerArray(x);
        if (program.isUntypedArrayType(type)) {
          ctx.replaceMe(initializerArray);
          return;
        }
        ctx.replaceMe(createArrayFromInitializers(x, type));
        return;
      }

      if (program.isUntypedArrayType(type) && type.getDims() == 1) {
          // Create a plain array.
        ctx.replaceMe(new JMethodCall(x.getSourceInfo(), null,
            program.getIndexedMethod(RuntimeConstants.ARRAY_NEW_ARRAY),
            x.getDimensionExpressions().get(0)));
        return;
      }

      int suppliedDimensions = x.getDimensionExpressions().size();
      assert (suppliedDimensions >= 1);

      if (suppliedDimensions == 1) {
        ctx.replaceMe(initializeUnidimensionalArray(x, type));
        return;
      }

      ctx.replaceMe(initializeMultidimensionalArray(x, type));
    }

    private JRuntimeTypeReference getElementRuntimeTypeReference(SourceInfo sourceInfo,
        JArrayType arrayType) {
      JType elementType = arrayType.getElementType();
      if (!(elementType instanceof JReferenceType)) {
        // elementType is a primitive type, store check will be performed statically.
        elementType = JReferenceType.NULL_TYPE;
      }

      if (program.typeOracle.isEffectivelyJavaScriptObject(elementType)) {
        /*
         * treat types that are effectively JSO's as JSO's, for the purpose of
         * castability checking
         */
        elementType = program.getJavaScriptObject();
      } else {
        elementType = elementType.getUnderlyingType();
      }

      elementType = program.normalizeJsoType(elementType);
      return new JRuntimeTypeReference(sourceInfo, program.getTypeJavaLangObject(),
          (JReferenceType) elementType);
    }

    private JExpression getOrCreateCastMap(SourceInfo sourceInfo, JArrayType arrayType) {
      JCastMap castableTypeMap = program.getCastMap(arrayType);
      if (castableTypeMap == null) {
        return new JCastMap(sourceInfo, program.getTypeJavaLangObject(),
            Collections.<JReferenceType>emptyList());
      }
      return castableTypeMap;
    }

    private JExpression initializeUnidimensionalArray(JNewArray x, JArrayType arrayType) {
      // override the type of the called method with the array's type
      SourceInfo sourceInfo = x.getSourceInfo();
      JLiteral classLit = x.getLeafTypeClassLiteral();
      JExpression castableTypeMap = getOrCreateCastMap(sourceInfo, arrayType);
      JRuntimeTypeReference arrayElementRuntimeTypeReference =
          getElementRuntimeTypeReference(sourceInfo, arrayType);
      JType elementType = arrayType.getElementType();
      JIntLiteral elementTypeCategory = getTypeCategoryLiteral(elementType);
      JExpression dim = x.getDimensionExpressions().get(0);
      JMethodCall call =
          new JMethodCall(sourceInfo, null, initializeUnidimensionalArrayMethod);
      call.overrideReturnType(arrayType);
      call.addArgs(classLit, castableTypeMap, arrayElementRuntimeTypeReference, dim,
          elementTypeCategory, program.getLiteralInt(arrayType.getDims()));
      return call;
    }

    private JExpression initializeMultidimensionalArray(JNewArray x, JArrayType arrayType) {
      // override the type of the called method with the array's type
      SourceInfo sourceInfo = x.getSourceInfo();
      JsonArray castableTypeMaps = new JsonArray(sourceInfo, program.getJavaScriptObject());
      JsonArray elementTypeReferences = new JsonArray(sourceInfo, program.getJavaScriptObject());
      JsonArray dimList = new JsonArray(sourceInfo, program.getJavaScriptObject());
      JType currentElementType = arrayType;
      JLiteral classLit = x.getLeafTypeClassLiteral();
      for (int i = 0; i < x.getDimensionExpressions().size(); ++i) {
        // Walk down each type from most dims to least.
        JArrayType curArrayType = (JArrayType) currentElementType;

        JExpression castableTypeMap = getOrCreateCastMap(sourceInfo, curArrayType);
        castableTypeMaps.getExpressions().add(castableTypeMap);

        JRuntimeTypeReference elementTypeIdLit = getElementRuntimeTypeReference(sourceInfo,
            curArrayType);
        elementTypeReferences.getExpressions().add(elementTypeIdLit);

        dimList.getExpressions().add(x.getDimensionExpressions().get(i));
        currentElementType = curArrayType.getElementType();
      }
      JType leafElementType = currentElementType;
      JIntLiteral leafElementTypeCategory = getTypeCategoryLiteral(leafElementType);
      JMethodCall call =
          new JMethodCall(sourceInfo, null, initializeMultidimensionalArrayMethod);
      call.overrideReturnType(arrayType);
      call.addArgs(classLit, castableTypeMaps, elementTypeReferences, leafElementTypeCategory,
          dimList, program.getLiteralInt(x.getDimensionExpressions().size()));
      return call;
    }

    private JExpression createArrayFromInitializers(JNewArray x, JArrayType arrayType) {
      // override the type of the called method with the array's type
      SourceInfo sourceInfo = x.getSourceInfo();
      JExpression classLitExpression = program.createArrayClassLiteralExpression(x.getSourceInfo(),
          x.getLeafTypeClassLiteral(), arrayType.getDims());
      JExpression castableTypeMap = getOrCreateCastMap(sourceInfo, arrayType);
      JRuntimeTypeReference elementTypeIds = getElementRuntimeTypeReference(sourceInfo, arrayType);
      JsonArray initializers =
          new JsonArray(sourceInfo, program.getJavaScriptObject(), x.getInitializers());
      JIntLiteral leafElementTypeCategory = getTypeCategoryLiteral(arrayType.getElementType());
      JMethodCall call = new JMethodCall(sourceInfo, null, stampJavaTypeInfoMethod);
      call.overrideReturnType(arrayType);
      call.addArgs(classLitExpression, castableTypeMap, elementTypeIds, leafElementTypeCategory,
          initializers);
      return call;
    }

    /**
     * Returns a literal that represent the type category for a type.
     */
    private JIntLiteral getTypeCategoryLiteral(JType type) {
      return JIntLiteral.get(TypeCategory.typeCategoryForType(type, program).ordinal());
    }
  }

  private JArrayRef needsSetCheck(JBinaryOperation x) {
    if (x.getOp() != JBinaryOperator.ASG || !(x.getLhs() instanceof JArrayRef)) {
      return null;
    }
    JArrayRef arrayRef = (JArrayRef) x.getLhs();
    JType elementType = arrayRef.getType();
    JExpression arrayInstance = arrayRef.getInstance();
    if (elementType.isNullType()) {
      // JNullType will generate a null pointer exception instead,
      return null;
    } else if (!(elementType instanceof JReferenceType)) {
      // Primitive array types are statically correct, no need to set check.
      return null;
    } else if (!arrayInstance.getType().canBeSubclass() &&
        program.typeOracle.castSucceedsTrivially((JReferenceType) x.getRhs().getType(),
            (JReferenceType) elementType)) {
      // There is no need to check as the static check already proved the cast is correct.
      return null;
    }
    // TODO(rluble): native[] setCheck can also be omitted.
    return arrayRef;
  }

  public static JsonArray getInitializerArray(JNewArray x) {
    return new JsonArray(x.getSourceInfo(), x.getType(), x.getInitializers());
  }

  public static void exec(JProgram program) {
    new ArrayNormalizer(program).execImpl();
  }

  private final JMethod initializeUnidimensionalArrayMethod;
  private final JMethod initializeMultidimensionalArrayMethod;
  private final JMethod stampJavaTypeInfoMethod;
  private final JMethod setCheckMethod;
  private final JProgram program;

  private ArrayNormalizer(JProgram program) {
    this.program = program;
    setCheckMethod = program.getIndexedMethod(RuntimeConstants.ARRAY_SET_CHECK);
    initializeUnidimensionalArrayMethod = program.getIndexedMethod(
        RuntimeConstants.ARRAY_INITIALIZE_UNIDIMENSIONAL_ARRAY);
    initializeMultidimensionalArrayMethod = program.getIndexedMethod(
        RuntimeConstants.ARRAY_INITIALIZE_MULTIDIMENSIONAL_ARRAY);
    stampJavaTypeInfoMethod = program.getIndexedMethod(RuntimeConstants.ARRAY_STAMP_JAVA_TYPE_INFO);
  }

  private void execImpl() {
    new ArrayVisitor().accept(program);
  }
}
