/*
 * 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.CanBeSetFinal;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JBinaryOperation;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JConstructor;
import com.google.gwt.dev.jjs.ast.JDeclarationStatement;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JFieldRef;
import com.google.gwt.dev.jjs.ast.JLocal;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodBody;
import com.google.gwt.dev.jjs.ast.JParameter;
import com.google.gwt.dev.jjs.ast.JPostfixOperation;
import com.google.gwt.dev.jjs.ast.JPrefixOperation;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JVariable;
import com.google.gwt.dev.jjs.ast.JVariableRef;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.dev.jjs.ast.js.JsniFieldRef;
import com.google.gwt.thirdparty.guava.common.collect.Sets;

import java.util.Set;

/**
 * Finds all items are effectively final. That is, methods that are never  overridden and variables
 * that are never reassigned.
 * <p>
 * NOTE: Classes should not be marked final, since the analysis for classes is done through
 * {@link JAnalysisDecoratedType}.
 */
public class Finalizer {
  /**
   * Any items that weren't marked during MarkVisitor can be set final.
   *
   * Open question: What does it mean if an interface/abstract method becomes
   * final? Is this possible after Pruning? I guess it means that someone tried
   * to make a call to method that wasn't actually implemented anywhere in the
   * program. But if it wasn't implemented, then the enclosing class should have
   * come up as not instantiated and been culled. So I think it's not possible.
   */
  private class FinalizeVisitor extends JChangeTrackingVisitor {

    public FinalizeVisitor(OptimizerContext optimizerCtx) {
      super(optimizerCtx);
    }

    @Override
    public void exit(JConstructor x, Context ctx) {
      // Not applicable.
    }

    @Override
    public void exit(JField x, Context ctx) {
      if (x.isVolatile() || x.canBeImplementedExternally() || x.canBeReferencedExternally()) {
        return;
      }
      maybeFinalize(x);
    }

    @Override
    public void endVisit(JLocal x, Context ctx) {
      maybeFinalize(x);
    }

    @Override
    public void exit(JMethod x, Context ctx) {
      if (!x.isFinal() && x.getOverridingMethods().isEmpty()) {
        setFinal(x);
      }
    }

    @Override
    public void endVisit(JParameter x, Context ctx) {
      maybeFinalize(x);
    }

    @Override
    public boolean visit(JClassType x, Context ctx) {
      // Don't visit external types, because we can't change their final
      // specifiers.
      return !x.isExternal();
    }

    @Override
    public boolean visit(JMethodBody x, Context ctx) {
      for (JLocal local : x.getLocals()) {
        maybeFinalize(local);
      }
      return false;
    }

    private void maybeFinalize(JVariable x) {
      if (!x.isFinal() && !isReassigned.contains(x)) {
        setFinal(x);
      }
    }

    private void setFinal(CanBeSetFinal x) {
      x.setFinal();
      assert x.isFinal();
      madeChanges();
    }
  }

  /**
   * Find all items that ARE overridden/subclassed/reassigned.
   */
  private class MarkVisitor extends JVisitor {
    @Override
    public void endVisit(JBinaryOperation x, Context ctx) {
      if (x.getOp().isAssignment()) {
        recordAssignment(x.getLhs());
      }
    }

    @Override
    public void endVisit(JConstructor x, Context ctx) {
      // Never overridden.
    }

    @Override
    public void endVisit(JDeclarationStatement x, Context ctx) {
      // This is should only be considered a reassignment if the uninitialized value for this field
      // is observable.
      // TODO(rluble): do static analysis to improve this pass.
      if (x.getVariableRef() instanceof JFieldRef) {
        JField field = ((JFieldRef) x.getVariableRef()).getField();
        if (field.getLiteralInitializer() != null &&
            !field.getLiteralInitializer().equals(field.getType().getDefaultValue())) {
          recordAssignment(x.getVariableRef());
        }
      }
    }

    @Override
    public void endVisit(JPostfixOperation x, Context ctx) {
      if (x.getOp().isModifying()) {
        recordAssignment(x.getArg());
      }
    }

    @Override
    public void endVisit(JPrefixOperation x, Context ctx) {
      if (x.getOp().isModifying()) {
        recordAssignment(x.getArg());
      }
    }

    @Override
    public void endVisit(JsniFieldRef x, Context ctx) {
      if (x.isLvalue()) {
        recordAssignment(x);
      }
    }

    @Override
    public boolean visit(JClassType x, Context ctx) {
      // Don't visit external types, because we can't change their final
      // specifiers.
      return !x.isExternal();
    }

    private void recordAssignment(JExpression lhs) {
      if (lhs instanceof JVariableRef) {
        JVariableRef variableRef = (JVariableRef) lhs;
        isReassigned.add(variableRef.getTarget());
      }
    }
  }

  private static final String NAME = Finalizer.class.getSimpleName();

  private Finalizer(JProgram program) {
    this.program = program;
  }

  public static int exec(JProgram program, OptimizerContext optimizerCtx) {
    OptimizerStats stats = new Finalizer(program).execImpl(optimizerCtx);
    optimizerCtx.incOptimizationStep();
    return stats.getNumMods();
  }

  private final Set<JVariable> isReassigned = Sets.newHashSet();

  private final JProgram program;

  private OptimizerStats execImpl(OptimizerContext optimizerCtx) {
    try (OptimizerStats stats = OptimizerStats.optimization(NAME)) {
      MarkVisitor marker = new MarkVisitor();
      marker.accept(program);

      FinalizeVisitor finalizer = new FinalizeVisitor(optimizerCtx);
      finalizer.accept(program);

      JavaAstVerifier.assertProgramIsConsistent(program);

      return stats.recordModified(finalizer.getNumMods());
    }
  }
}
