/*
 * Copyright 2020 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.formatstring;

import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.bugpatterns.formatstring.LenientFormatStringUtils.getLenientFormatStringPosition;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.util.ASTHelpers.constValue;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.isConsideredFinal;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.jspecify.annotations.Nullable;

/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
@BugPattern(
    summary =
        "Prefer to create format strings inline, instead of extracting them to a single-use"
            + " constant",
    severity = WARNING)
public class InlineFormatString extends BugChecker implements CompilationUnitTreeMatcher {

  private static @Nullable ExpressionTree formatString(
      MethodInvocationTree tree, VisitorState state) {
    ImmutableList<ExpressionTree> args = FormatStringUtils.formatMethodArguments(tree, state);
    if (!args.isEmpty()) {
      return args.get(0);
    }
    int index = getLenientFormatStringPosition(tree, state);
    return index != -1 ? tree.getArguments().get(index) : null;
  }

  @Override
  public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
    SetMultimap<Symbol, Tree> uses = MultimapBuilder.linkedHashKeys().linkedHashSetValues().build();
    Map<Symbol, VariableTree> declarations = new LinkedHashMap<>();
    // find calls to String.format and similar where the format string is a private compile-time
    // constant field
    tree.accept(
        new TreeScanner<Void, Void>() {
          @Override
          public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) {
            handle(tree);
            return super.visitMethodInvocation(tree, null);
          }

          private void handle(MethodInvocationTree tree) {
            ExpressionTree arg = formatString(tree, state);
            if (arg == null) {
              return;
            }
            Symbol variable = getSymbol(arg);
            if (variable == null || !isConstant(variable, arg)) {
              return;
            }
            uses.put(variable, arg);
          }

          private boolean isConstant(Symbol variable, ExpressionTree arg) {
            return switch (variable.getKind()) {
              case FIELD -> variable.isPrivate() && constValue(arg, String.class) != null;
              // locals don't have a computed constant value, this will be handled below when we
              // scan for constant initializers
              case LOCAL_VARIABLE -> isConsideredFinal(variable);
              default -> false;
            };
          }
        },
        null);
    // find all uses of the candidate fields, and reject them if they are used outside for calls to
    // format methods
    tree.accept(
        new TreeScanner<Void, Void>() {
          @Override
          public Void visitMemberSelect(MemberSelectTree tree, Void unused) {
            handle(tree);
            return super.visitMemberSelect(tree, null);
          }

          @Override
          public Void visitIdentifier(IdentifierTree tree, Void unused) {
            handle(tree);
            return super.visitIdentifier(tree, null);
          }

          private void handle(Tree tree) {
            Symbol sym = getSymbol(tree);
            if (sym == null) {
              return;
            }
            if (uses.containsKey(sym) && !uses.containsValue(tree)) {
              uses.removeAll(sym);
            }
          }
        },
        null);
    // find the field declarations
    new SuppressibleTreePathScanner<Void, Void>(state) {
      @Override
      public Void visitVariable(VariableTree tree, Void unused) {
        VarSymbol sym = getSymbol(tree);
        if (uses.containsKey(sym)) {
          declarations.put(sym, tree);
        }
        return super.visitVariable(tree, null);
      }
    }.scan(state.getPath(), null);
    for (Map.Entry<Symbol, Collection<Tree>> e : uses.asMap().entrySet()) {
      Symbol sym = e.getKey();
      Collection<Tree> use = e.getValue();
      VariableTree def = declarations.get(sym);
      if (def == null || !(def.getInitializer() instanceof LiteralTree)) {
        // only inline the constant if its initializer is a literal String
        continue;
      }
      String constValue = state.getSourceForNode(def.getInitializer());
      SuggestedFix.Builder fix = SuggestedFix.builder();
      if (use.size() > 1) {
        // if the format string is used multiple times don't inline at each use-site
        // TODO(cushon): consider suggesting a helper method to do the formatting
        continue;
      }
      fix.delete(def);
      for (Tree u : use) {
        fix.replace(u, constValue);
      }
      state.reportMatch(describeMatch(def, fix.build()));
    }
    return NO_MATCH;
  }
}
