/*
 * Copyright 2014 The Error Prone Authors.
 *
 * 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.errorprone.bugpatterns;

import static com.google.common.collect.Maps.newHashMap;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.fixes.SuggestedFix.replace;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.hasIdentifier;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.getType;
import static com.sun.source.tree.Tree.Kind.IDENTIFIER;
import static java.util.Collections.unmodifiableList;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Type;
import java.util.List;
import java.util.Map;

/**
 * Checks, if two constructors in a class both accept {@code Foo foo} and one calls the other, that
 * the caller passes {@code foo} as a parameter. The goal is to catch copy-paste errors:
 *
 * <pre>
 *   MissileLauncher(Location target, boolean askForConfirmation) {
 *     ...
 *   }
 *   MissileLauncher(Location target) {
 *     this(target, false);
 *   }
 *   MissileLauncher(boolean askForConfirmation) {
 *     this(TEST_TARGET, <b>false</b>); // should be askForConfirmation
 *   }</pre>
 *
 * @author cpovirk@google.com (Chris Povirk)
 */
@BugPattern(
    name = "ChainingConstructorIgnoresParameter",
    severity = ERROR,
    summary =
        "The called constructor accepts a parameter with the same name and type as one of "
            + "its caller's parameters, but its caller doesn't pass that parameter to it.  It's "
            + "likely that it was intended to.")
public final class ChainingConstructorIgnoresParameter extends BugChecker
    implements CompilationUnitTreeMatcher, MethodInvocationTreeMatcher, MethodTreeMatcher {
  private final Map<MethodSymbol, List<VariableTree>> paramTypesForMethod = newHashMap();
  private final Multimap<MethodSymbol, Caller> callersToEvaluate = ArrayListMultimap.create();

  @Override
  public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
    /*
     * Clear the collections to save memory. (I wonder if it also helps to handle weird cases when a
     * class has multiple definitions. But I would expect for multiple definitions within the same
     * compiler invocation to cause deeper problems.)
     */
    paramTypesForMethod.clear();
    callersToEvaluate.clear(); // should have already been cleared
    return NO_MATCH;
  }

  @Override
  public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
    MethodSymbol symbol = getSymbol(tree);
    // TODO(cpovirk): determine whether anyone might be calling Foo.this()
    if (!isIdentifierWithName(tree.getMethodSelect(), "this")) {
      return NO_MATCH;
    }
    callersToEvaluate.put(symbol, new Caller(tree, state));
    return evaluateCallers(symbol);
  }

  @Override
  public Description matchMethod(MethodTree tree, VisitorState state) {
    MethodSymbol symbol = getSymbol(tree);
    if (!symbol.isConstructor()) {
      return NO_MATCH;
    }
    paramTypesForMethod.put(symbol, unmodifiableList(tree.getParameters()));
    return evaluateCallers(symbol);
  }

  private Description evaluateCallers(MethodSymbol symbol) {
    List<VariableTree> paramTypes = paramTypesForMethod.get(symbol);
    if (paramTypes == null) {
      // We haven't seen the declaration yet. We'll evaluate the call when we do.
      return NO_MATCH;
    }

    for (Caller caller : callersToEvaluate.removeAll(symbol)) {
      VisitorState state = caller.state;
      MethodInvocationTree invocation = caller.tree;

      MethodTree callerConstructor = state.findEnclosing(MethodTree.class);
      if (callerConstructor == null) {
        continue; // impossible, at least in compilable code?
      }
      Map<String, Type> availableParams = indexTypeByName(callerConstructor.getParameters());

      /*
       * TODO(cpovirk): Better handling of varargs: If the last parameter type is varargs and it is
       * called as varargs (rather than by passing an array), then rewrite the parameter types to
       * (p0, p1, ..., p[n-2], p[n-1] = element type of varargs parameter if an argument is
       * supplied, p[n] = ditto, etc.). For now, we settle for not crashing in the face of a
       * mismatch between the number of parameters declared and the number supplied.
       *
       * (Use MethodSymbol.isVarArgs.)
       */
      for (int i = 0; i < paramTypes.size() && i < invocation.getArguments().size(); i++) {
        VariableTree formalParam = paramTypes.get(i);
        String formalParamName = formalParam.getName().toString();
        Type formalParamType = getType(formalParam.getType());

        Type availableParamType = availableParams.get(formalParamName);

        ExpressionTree actualParam = invocation.getArguments().get(i);

        if (
        /*
         * The caller has no param of this type. (Or if it did, we couldn't determine the type.
         * Does that ever happen?) If the param doesn't exist, the caller can't be failing to
         * pass it.
         */
        availableParamType == null

            /*
             * We couldn't determine the type of the formal parameter. (Does this ever happen?)
             */
            || formalParamType == null

            /*
             * The caller is passing the expected parameter (or "ImmutableList.copyOf(parameter),"
             * "new File(parameter)," etc.).
             */
            || referencesIdentifierWithName(formalParamName, actualParam, state)) {
          continue;
        }

        if (state.getTypes().isAssignable(availableParamType, formalParamType)) {
          reportMatch(invocation, state, actualParam, formalParamName);
        }
        /*
         * If formal parameter is of an incompatible type, the caller might in theory still intend
         * to pass a dervied expression. For example, "Foo(String file)" might intend to call
         * "Foo(File file)" by passing "new File(file)." If this comes up in practice, we could
         * provide the dummy suggested fix "someExpression(formalParamName)." However, my research
         * suggests that this will rarely if ever be what the user wants.
         */
      }
    }

    // All matches are reported through reportMatch calls instead of return values.
    return NO_MATCH;
  }

  private static Map<String, Type> indexTypeByName(List<? extends VariableTree> parameters) {
    Map<String, Type> result = newHashMap();
    for (VariableTree parameter : parameters) {
      result.put(parameter.getName().toString(), getType(parameter.getType()));
    }
    return result;
  }

  private void reportMatch(
      Tree diagnosticPosition, VisitorState state, Tree toReplace, String replaceWith) {
    state.reportMatch(describeMatch(diagnosticPosition, replace(toReplace, replaceWith)));
  }

  private static boolean referencesIdentifierWithName(
      final String name, ExpressionTree tree, VisitorState state) {
    Matcher<IdentifierTree> identifierMatcher =
        new Matcher<IdentifierTree>() {
          @Override
          public boolean matches(IdentifierTree tree, VisitorState state) {
            return isIdentifierWithName(tree, name);
          }
        };
    return hasIdentifier(identifierMatcher).matches(tree, state);
  }

  private static boolean isIdentifierWithName(ExpressionTree tree, String name) {
    return tree.getKind() == IDENTIFIER && ((IdentifierTree) tree).getName().contentEquals(name);
  }

  private static final class Caller {
    final MethodInvocationTree tree;
    final VisitorState state;

    Caller(MethodInvocationTree tree, VisitorState state) {
      this.tree = tree;
      this.state = state;
    }
  }
}
