/*
 * Copyright 2007 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.ast.Context;
import com.google.gwt.dev.jjs.ast.JArrayLength;
import com.google.gwt.dev.jjs.ast.JArrayRef;
import com.google.gwt.dev.jjs.ast.JBinaryOperation;
import com.google.gwt.dev.jjs.ast.JBinaryOperator;
import com.google.gwt.dev.jjs.ast.JCastOperation;
import com.google.gwt.dev.jjs.ast.JConditional;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JFieldRef;
import com.google.gwt.dev.jjs.ast.JIntLiteral;
import com.google.gwt.dev.jjs.ast.JLocalRef;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JNewArray;
import com.google.gwt.dev.jjs.ast.JNewInstance;
import com.google.gwt.dev.jjs.ast.JParameterRef;
import com.google.gwt.dev.jjs.ast.JPostfixOperation;
import com.google.gwt.dev.jjs.ast.JPrefixOperation;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JThisRef;
import com.google.gwt.dev.jjs.ast.JVisitor;

/**
 * Analyzes an expression and make a number of static analysis flags available
 * based on the information available solely through the expression.
 *
 * TODO: make this even smarter when we have real null analysis.
 */
public class ExpressionAnalyzer extends JVisitor {
  private boolean accessesField;
  private boolean accessesFieldNonFinal;
  private boolean accessesLocal;
  private boolean accessesParameter;
  private boolean assignmentToField;
  private boolean assignmentToLocal;
  private boolean assignmentToParameter;
  private boolean canThrowException;
  private boolean createsObject;
  private int inConditional;

  /**
   * Does this expression read or write fields within the scope of the
   * expression?
   */
  public boolean accessesField() {
    return accessesField;
  }

  /**
   * Does this expression read or write non-final fields within the scope of the
   * expression?
   */
  public boolean accessesFieldNonFinal() {
    return accessesFieldNonFinal;
  }

  /**
   * Does this expression read or write locals within the scope of the
   * expression?
   */
  public boolean accessesLocal() {
    return accessesLocal;
  }

  /**
   * Does this expression read or write parameters within the scope of the
   * expression?
   */
  public boolean accessesParameter() {
    return accessesParameter;
  }

  public boolean canThrowException() {
    return canThrowException;
  }

  public boolean createsObject() {
    return createsObject;
  }

  @Override
  public void endVisit(JArrayLength x, Context ctx) {
    // TODO: Is setting accessesField necessary for array.length access?
    accessesField = true;
    // Can throw an NPE when the array instance is null at runtime.
    JReferenceType refType = (JReferenceType) x.getInstance().getType();
    canThrowException = refType.canBeNull();
  }

  @Override
  public void endVisit(JArrayRef x, Context ctx) {
    /*
     * In Java, array references can throw IndexOutOfBoundsExceptions, but this
     * isn't the case for current GWT generated code. If we add a strict array
     * bounds check later, this flag would need to reflect it.
     */

    // If JArrayRef is null, this can throw a NullPointerException.
    canThrowException = true;
  }

  @Override
  public void endVisit(JBinaryOperation x, Context ctx) {
    if (x.isAssignment()) {
      JExpression lhs = x.getLhs();
      if (lhs instanceof JArrayRef) {
        // Array store operations can throw ArrayStoreExceptions
        canThrowException = true;
      } else {
        analyzeStore(lhs);
      }
    }
  }

  @Override
  public void endVisit(JCastOperation x, Context ctx) {
    // Can throw ClassCastException
    canThrowException = true;
  }

  @Override
  public void endVisit(JFieldRef x, Context ctx) {
    accessesField = true;
    if (!x.getTarget().isFinal()) {
      accessesFieldNonFinal = true;
    }

    if (x.hasClinit()) {
      recordMethodCall();
    }

    JExpression instance = x.getInstance();
    if (instance == null) {
      return;
    }

    // Field references using this are always safe
    if (instance instanceof JThisRef) {
      return;
    }

    if (x.getField().isStatic()) {
      // Can throw exceptions IFF a clinit is triggered.
      canThrowException = x.hasClinit();
    } else {
      // Can throw exceptions IFF the instance is null.
      JReferenceType refType = (JReferenceType) instance.getType();
      canThrowException = refType.canBeNull();
    }
  }

  @Override
  public void endVisit(JLocalRef x, Context ctx) {
    accessesLocal = true;
  }

  @Override
  public void endVisit(JMethodCall x, Context ctx) {
    recordMethodCall();
  }

  @Override
  public void endVisit(JNewArray x, Context ctx) {
    createsObject = true;
    /*
     * If no array bounds, the new array is being automatically initialized. If
     * there are side-effects, they'll show up when we visit the initializers.
     */
    if (x.getDimensionExpressions() == null) {
      return;
    }

    /*
     * Can throw NegativeArraySizeException if we initialize an array with
     * negative dimensions.
     */
    for (JExpression expression : x.getDimensionExpressions()) {
      if (expression instanceof JIntLiteral) {
        int value = ((JIntLiteral) expression).getValue();
        if (value >= 0) {
          continue;
        }
      }
      canThrowException = true;
    }
  }

  @Override
  public void endVisit(JNewInstance x, Context ctx) {
    createsObject = true;
    endVisit((JMethodCall) x, ctx);
  }

  @Override
  public void endVisit(JParameterRef x, Context ctx) {
    accessesParameter = true;
  }

  @Override
  public void endVisit(JPostfixOperation x, Context ctx) {
    // Unary operations that are modifying cause assignment side-effects.
    if (x.getOp().isModifying()) {
      analyzeStore(x.getArg());
    }
  }

  @Override
  public void endVisit(JPrefixOperation x, Context ctx) {
    // Unary operations that are modifying cause assignment side-effects.
    if (x.getOp().isModifying()) {
      analyzeStore(x.getArg());
    }
  }

  /**
   * Does this expression make assignments to variables within the scope of the
   * expression?
   */
  public boolean hasAssignment() {
    return assignmentToField || assignmentToLocal || assignmentToParameter;
  }

  /**
   * Does this expression make assignments to fields within the scope of the
   * expression?
   */
  public boolean hasAssignmentToField() {
    return assignmentToField;
  }

  /**
   * Does this expression make assignments to locals within the scope of the
   * expression?
   */
  public boolean hasAssignmentToLocal() {
    return assignmentToLocal;
  }

  /**
   * Does this expression make assignments to parameters within the scope of the
   * expression?
   */
  public boolean hasAssignmentToParameter() {
    return assignmentToParameter;
  }

  @Override
  public boolean visit(JBinaryOperation x, Context ctx) {
    if (x.getOp() == JBinaryOperator.AND || x.getOp() == JBinaryOperator.OR) {
      accept(x.getLhs());
      inConditional++;
      accept(x.getRhs());
      inConditional--;
      return false;
    }
    return true;
  }

  @Override
  public boolean visit(JConditional x, Context ctx) {
    accept(x.getIfTest());
    inConditional++;
    accept(x.getThenExpr());
    accept(x.getElseExpr());
    inConditional--;

    return false;
  }

  /**
   * Determined if the current expression conditionally executes, based on its
   * parent expressions.
   */
  protected boolean isInConditional() {
    return inConditional > 0;
  }

  private void analyzeStore(JExpression expr) {
    if (expr instanceof JFieldRef) {
      assignmentToField = true;
    } else if (expr instanceof JParameterRef) {
      assignmentToParameter = true;
    } else if (expr instanceof JLocalRef) {
      assignmentToLocal = true;
    }
  }

  /**
   * We can't assume anything about method calls right now, except that it can't
   * access any of our locals or parameters.
   *
   * TODO: what about accessing arrays? Should be treated like field refs I
   * guess.
   */
  private void recordMethodCall() {
    assignmentToField = true;
    accessesField = true;
    accessesFieldNonFinal = true;
    canThrowException = true;
    createsObject = true;
  }
}
