package org.checkerframework.checker.formatter;

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 java.util.List;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey;
import org.checkerframework.checker.formatter.FormatterTreeUtil.FormatCall;
import org.checkerframework.checker.formatter.FormatterTreeUtil.InvocationType;
import org.checkerframework.checker.formatter.FormatterTreeUtil.Result;
import org.checkerframework.checker.formatter.qual.ConversionCategory;
import org.checkerframework.checker.formatter.qual.FormatMethod;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.basetype.BaseTypeVisitor;
import org.checkerframework.common.wholeprograminference.WholeProgramInference;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreePathUtil;
import org.checkerframework.javacutil.TreeUtils;
import org.checkerframework.javacutil.TypesUtils;
import org.checkerframework.javacutil.UserError;

/**
 * Whenever a format method invocation is found in the syntax tree, checks are performed as
 * specified in the Format String Checker manual.
 *
 * @checker_framework.manual #formatter-guarantees Format String Checker
 */
public class FormatterVisitor extends BaseTypeVisitor<FormatterAnnotatedTypeFactory> {
  public FormatterVisitor(BaseTypeChecker checker) {
    super(checker);
  }

  @Override
  public void processMethodTree(String className, MethodTree tree) {
    ExecutableElement methodElement = TreeUtils.elementFromDeclaration(tree);
    if (atypeFactory.getDeclAnnotation(methodElement, FormatMethod.class) != null) {
      int formatStringIndex = FormatterVisitor.formatStringIndex(methodElement);
      if (formatStringIndex == -1) {
        checker.reportError(tree, "format.method", methodElement.getSimpleName());
      }
    }
    super.processMethodTree(className, tree);
  }

  @Override
  public Void visitMethodInvocation(MethodInvocationTree tree, Void p) {
    FormatterTreeUtil ftu = atypeFactory.treeUtil;
    FormatCall fc = ftu.create(tree, atypeFactory);
    if (fc != null) {
      MethodTree enclosingMethod =
          TreePathUtil.enclosingMethod(atypeFactory.getPath(fc.invocationTree));

      Result<String> errMissingFormat = fc.errMissingFormatAnnotation();
      if (errMissingFormat != null) {
        // The string's type has no @Format annotation.
        if (isWrappedFormatCall(fc, enclosingMethod)) {
          // Nothing to do, because call is legal.
        } else {
          // I.1
          ftu.failure(errMissingFormat, "format.string", errMissingFormat.value());
        }
      } else {
        // The string has a @Format annotation.
        Result<InvocationType> invc = fc.getInvocationType();
        ConversionCategory[] formatCats = fc.getFormatCategories();
        switch (invc.value()) {
          case VARARG:
            Result<TypeMirror>[] argTypes = fc.getArgTypes();
            int argl = argTypes.length;
            int formatl = formatCats.length;
            if (argl < formatl) {
              // For assignments, format.missing.arguments is issued from
              // commonAssignmentCheck().
              // II.1
              ftu.failure(invc, "format.missing.arguments", formatl, argl);
            } else {
              if (argl > formatl) {
                // II.2
                ftu.warning(invc, "format.excess.arguments", formatl, argl);
              }
              for (int i = 0; i < formatl; ++i) {
                ConversionCategory formatCat = formatCats[i];
                Result<TypeMirror> arg = argTypes[i];
                TypeMirror argType = arg.value();

                switch (formatCat) {
                  case UNUSED:
                    // I.2
                    ftu.warning(arg, "format.argument.unused", " " + (1 + i));
                    break;
                  case NULL:
                    // I.3
                    if (argType.getKind() == TypeKind.NULL) {
                      ftu.warning(arg, "format.specifier.null", " " + (1 + i));
                    } else {
                      ftu.failure(arg, "format.specifier.null", " " + (1 + i));
                    }
                    break;
                  case GENERAL:
                    break;
                  default:
                    if (!fc.isValidArgument(formatCat, argType)) {
                      // II.3
                      ExecutableElement method = TreeUtils.elementFromUse(tree);
                      CharSequence methodName = ElementUtils.getSimpleDescription(method);
                      ftu.failure(
                          arg, "argument", "in varargs position", methodName, argType, formatCat);
                    }
                    break;
                }
              }
            }
            break;
          case ARRAY:
            // III
            if (!isWrappedFormatCall(fc, enclosingMethod)) {
              ftu.warning(invc, "format.indirect.arguments");
            }
            // TODO:  If it is explict array construction, such as "new Object[] {
            // ... }", then we could treat it like the VARARGS case, analyzing each
            // argument.  "new array" is probably rare, in the varargs position.
            // fall through
          case NULLARRAY:
            for (ConversionCategory cat : formatCats) {
              if (cat == ConversionCategory.NULL) {
                // I.3
                if (invc.value() == FormatterTreeUtil.InvocationType.NULLARRAY) {
                  ftu.warning(invc, "format.specifier.null", "");
                } else {
                  ftu.failure(invc, "format.specifier.null", "");
                }
              }
              if (cat == ConversionCategory.UNUSED) {
                // I.2
                ftu.warning(invc, "format.argument.unused", "");
              }
            }
            break;
        }
      }

      // Support -Ainfer command-line argument.
      WholeProgramInference wpi = atypeFactory.getWholeProgramInference();
      if (wpi != null && forwardsArguments(tree, enclosingMethod)) {
        wpi.addMethodDeclarationAnnotation(
            TreeUtils.elementFromDeclaration(enclosingMethod), atypeFactory.FORMATMETHOD);
      }
    }
    return super.visitMethodInvocation(tree, p);
  }

  /**
   * Returns true if {@code fc} is within a method m annotated as {@code @FormatMethod}, and fc's
   * arguments are m's formal parameters. In other words, fc forwards m's arguments to another
   * format method.
   *
   * @param fc an invocation of a format method
   * @param enclosingMethod the method that contains the call
   * @return true if {@code fc} is a call to a format method that forwards its enclosing method's
   *     arguments
   */
  private boolean isWrappedFormatCall(FormatCall fc, @Nullable MethodTree enclosingMethod) {
    if (enclosingMethod == null) {
      return false;
    }
    ExecutableElement enclosingMethodElement = TreeUtils.elementFromDeclaration(enclosingMethod);
    boolean withinFormatMethod =
        (atypeFactory.getDeclAnnotation(enclosingMethodElement, FormatMethod.class) != null);
    return withinFormatMethod && forwardsArguments(fc.invocationTree, enclosingMethod);
  }

  /**
   * Returns true if {@code invocationTree}'s arguments are {@code enclosingMethod}'s formal
   * parameters. In other words, {@code invocationTree} forwards {@code enclosingMethod}'s
   * arguments.
   *
   * <p>Only arguments from the first String formal parameter onward count. Returns false if there
   * is no String formal parameter.
   *
   * @param invocationTree an invocation of a method
   * @param enclosingMethod the method that contains the call
   * @return true if {@code invocationTree} is a call to a method that forwards its containing
   *     method's arguments
   */
  private boolean forwardsArguments(
      MethodInvocationTree invocationTree, @Nullable MethodTree enclosingMethod) {

    if (enclosingMethod == null) {
      return false;
    }

    ExecutableElement enclosingMethodElement = TreeUtils.elementFromDeclaration(enclosingMethod);
    int paramIndex = formatStringIndex(enclosingMethodElement);
    if (paramIndex == -1) {
      return false;
    }

    ExecutableElement calledMethodElement = TreeUtils.elementFromUse(invocationTree);
    int callIndex = formatStringIndex(calledMethodElement);
    if (callIndex == -1) {
      throw new UserError(
          "Method "
              + calledMethodElement
              + " is annotated @FormatMethod but has no String formal parameter");
    }

    List<? extends ExpressionTree> args = invocationTree.getArguments();
    List<? extends VariableTree> params = enclosingMethod.getParameters();

    if (params.size() - paramIndex != args.size() - callIndex) {
      return false;
    }
    while (paramIndex < params.size()) {
      ExpressionTree argTree = args.get(callIndex);
      if (argTree.getKind() != Tree.Kind.IDENTIFIER) {
        return false;
      }
      VariableTree param = params.get(paramIndex);
      if (param.getName() != ((IdentifierTree) argTree).getName()) {
        return false;
      }
      paramIndex++;
      callIndex++;
    }

    return true;
  }

  // TODO: Should this be the last String argument?  That would require that every method
  // annotated with @FormatMethod uses varargs syntax.
  /**
   * Returns the index of the format string of a method: the first formal parameter with declared
   * type String.
   *
   * @param m a method
   * @return the index of the last String formal parameter, or -1 if none
   */
  public static int formatStringIndex(ExecutableElement m) {
    List<? extends VariableElement> params = m.getParameters();
    for (int i = 0; i < params.size(); i++) {
      if (TypesUtils.isString(params.get(i).asType())) {
        return i;
      }
    }
    return -1;
  }

  @Override
  protected boolean commonAssignmentCheck(
      AnnotatedTypeMirror varType,
      AnnotatedTypeMirror valueType,
      Tree valueTree,
      @CompilerMessageKey String errorKey,
      Object... extraArgs) {
    boolean result =
        super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs);

    AnnotationMirror rhs = valueType.getPrimaryAnnotationInHierarchy(atypeFactory.UNKNOWNFORMAT);
    AnnotationMirror lhs = varType.getPrimaryAnnotationInHierarchy(atypeFactory.UNKNOWNFORMAT);

    // From the manual: "It is legal to use a format string with fewer format specifiers
    // than required, but a warning is issued."
    // The format.missing.arguments warning is issued here for assignments.
    // For method calls, it is issued in visitMethodInvocation.
    if (rhs != null
        && lhs != null
        && AnnotationUtils.areSameByName(rhs, FormatterAnnotatedTypeFactory.FORMAT_NAME)
        && AnnotationUtils.areSameByName(lhs, FormatterAnnotatedTypeFactory.FORMAT_NAME)) {
      ConversionCategory[] rhsArgTypes = atypeFactory.treeUtil.formatAnnotationToCategories(rhs);
      ConversionCategory[] lhsArgTypes = atypeFactory.treeUtil.formatAnnotationToCategories(lhs);

      if (rhsArgTypes.length < lhsArgTypes.length) {
        checker.reportWarning(
            valueTree, "format.missing.arguments", varType.toString(), valueType.toString());
        result = false;
      }
    }
    return result;
  }
}
