/*
 * 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.ast.Context;
import com.google.gwt.dev.jjs.ast.JBinaryOperation;
import com.google.gwt.dev.jjs.ast.JExpression;
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.JNode;
import com.google.gwt.dev.jjs.ast.JParameter;
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.JProgram;
import com.google.gwt.dev.jjs.ast.JValueLiteral;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.dev.jjs.ast.js.JsniMethodBody;
import com.google.gwt.dev.jjs.ast.js.JsniMethodRef;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Sets;

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

/**
 * Detects when same literal is passed as parameter value, and uses literal
 * instead of parameter. The unused parameter will be removed by other analyses.
 */
// TODO: this optimization can mistakenly act on methods such as LongLib.fromInt
// since only one call is seen in LongLib itself.
public class SameParameterValueOptimizer {
  /**
   * Fill parameterValues map.
   */
  private class AnalysisVisitor extends JVisitor {
    @Override
    public void endVisit(JBinaryOperation x, Context ctx) {
      if (x.isAssignment() && x.getLhs() instanceof JParameterRef) {
        parameterValues.put(((JParameterRef) x.getLhs()).getParameter(), null);
      }
    }

    @Override
    public void endVisit(JMethodCall x, Context ctx) {
      JMethod method = x.getTarget();

      // A quick check to reduce extra work needed otherwise...
      if (isNotOptimizable(method)) {
        return;
      }

      List<JExpression> arguments = x.getArgs();
      List<JParameter> parameters = method.getParams();

      for (int i = 0; i < arguments.size() && i < parameters.size(); i++) {
        JParameter parameter = parameters.get(i);
        JExpression argument = arguments.get(i);

        if (!(argument instanceof JValueLiteral)) {
          parameterValues.put(parameter, null);
          continue;
        }

        if (!parameterValues.containsKey(parameter)) {
          parameterValues.put(parameter, (JValueLiteral) argument);
          continue;
        }

        JValueLiteral commonParamValue = parameterValues.get(parameter);
        if (commonParamValue == null) {
          continue;
        }

        if (!equalLiterals(commonParamValue, (JValueLiteral) argument)) {
          parameterValues.put(parameter, null);
        }
      }
    }

    @Override
    public void endVisit(JPostfixOperation x, Context ctx) {
      if (x.getArg() instanceof JParameterRef) {
        parameterValues.put(((JParameterRef) x.getArg()).getParameter(), null);
      }
    }

    @Override
    public void endVisit(JPrefixOperation x, Context ctx) {
      if (x.getArg() instanceof JParameterRef) {
        parameterValues.put(((JParameterRef) x.getArg()).getParameter(), null);
      }
    }

    @Override
    public void endVisit(JsniMethodBody x, Context ctx) {
      for (JsniMethodRef methodRef : x.getJsniMethodRefs()) {
        nonOptimizableMethods.add(methodRef.getTarget());
      }
    }

    @Override
    public boolean visit(JMethod x, Context ctx) {
      if (isNotOptimizable(x)) {
        nonOptimizableMethods.add(x);
      }
      return true;
    }

    private boolean isNotOptimizable(JMethod x) {
      return x.needsDynamicDispatch() || x.canBeReferencedExternally();
    }

    private boolean equalLiterals(JValueLiteral l1, JValueLiteral l2) {
      Object v1 = l1.getValueObj();
      Object v2 = l2.getValueObj();

      if (v1 == v2) {
        return true;
      }

      if (v1 == null || v2 == null) {
        return false;
      }

      return v1.equals(v2);
    }
  }

  /**
   * Substitute all parameter references with expression.
   */
  private class SubstituteParameterVisitor extends JChangeTrackingVisitor {
    private final CloneExpressionVisitor cloner;

    public SubstituteParameterVisitor(OptimizerContext optimizerCtx) {
      super(optimizerCtx);
      cloner = new CloneExpressionVisitor();
    }

    @Override
    public boolean enter(JMethod x, Context ctx) {
      if (nonOptimizableMethods.contains(x)) {
        return false;
      }
      for (JParameter parameter : x.getParams()) {
        if (parameterValues.get(parameter) != null) {
          return true;
        }
      }
      return false;
    }

    @Override
    public void endVisit(JParameterRef x, Context ctx) {
      JParameter parameter = x.getParameter();
      JLiteral value = parameterValues.get(parameter);
      if (value != null) {
        ctx.replaceMe(cloner.cloneExpression(Simplifier.cast(parameter.getType(), value)));
      }
    }
  }

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

  public static int exec(JProgram program, OptimizerContext optimizerCtx) {
    return new SameParameterValueOptimizer(program).execImpl(program, optimizerCtx);
  }

  /**
   * Parameter values.
   *
   * If doesn't contain a parameter, then its value is unknown. If contains
   * parameter, and value is null - the parameter's value is not the same across
   * all calls. If value is not null - the parameter's value is the same across
   * all calls.
   */
  private final Map<JParameter, JValueLiteral> parameterValues = Maps.newIdentityHashMap();
  private final JProgram program;

  /**
   * These methods should not be tried to be optimized, either because they are polymorphic or we
   * cannot see all the calls.
   */
  private final Set<JMethod> nonOptimizableMethods = Sets.newHashSet();

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

  private int execImpl(JNode node, OptimizerContext optimizerCtx) {
    try (OptimizerStats stats = OptimizerStats.optimization(NAME);) {
      new AnalysisVisitor().accept(node);
      SubstituteParameterVisitor substituteParameterVisitor =
          new SubstituteParameterVisitor(optimizerCtx);
      substituteParameterVisitor.accept(node);
      stats.recordModified(substituteParameterVisitor.getNumMods());

      JavaAstVerifier.assertProgramIsConsistent(program);

      optimizerCtx.incOptimizationStep();

      return stats.getNumMods();
    }
  }
}
