package org.checkerframework.checker.formatter;

import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TypeCastTree;
import com.sun.source.util.SimpleTreeVisitor;
import java.util.IllegalFormatException;
import java.util.List;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.NullType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.SimpleTypeVisitor8;
import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey;
import org.checkerframework.checker.formatter.qual.ConversionCategory;
import org.checkerframework.checker.formatter.qual.Format;
import org.checkerframework.checker.formatter.qual.FormatMethod;
import org.checkerframework.checker.formatter.qual.InvalidFormat;
import org.checkerframework.checker.formatter.qual.ReturnsFormat;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.dataflow.cfg.node.ArrayCreationNode;
import org.checkerframework.dataflow.cfg.node.FieldAccessNode;
import org.checkerframework.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.dataflow.cfg.node.Node;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.TreeUtils;
import org.checkerframework.javacutil.TypesUtils;

/**
 * This class provides a collection of utilities to ease working with syntax trees that have
 * something to do with Formatters.
 */
public class FormatterTreeUtil {
  /** The checker. */
  public final BaseTypeChecker checker;

  /** The processing environment. */
  public final ProcessingEnvironment processingEnv;

  /** The value() element/field of an @Format annotation. */
  protected final ExecutableElement formatValueElement;

  /** The value() element/field of an @InvalidFormat annotation. */
  protected final ExecutableElement invalidFormatValueElement;

  // private final ExecutableElement formatArgTypesElement;

  public FormatterTreeUtil(BaseTypeChecker checker) {
    this.checker = checker;
    this.processingEnv = checker.getProcessingEnvironment();
    formatValueElement = TreeUtils.getMethod(Format.class, "value", 0, processingEnv);
    invalidFormatValueElement = TreeUtils.getMethod(InvalidFormat.class, "value", 0, processingEnv);
    /*
    this.formatArgTypesElement =
            TreeUtils.getMethod(
                    Format.class,
                    "value",
                    0,
                    processingEnv);
     */
  }

  /** Describes the ways a format method may be invoked. */
  public static enum InvocationType {
    /**
     * The parameters are passed as varargs. For example:
     *
     * <blockquote>
     *
     * <pre>
     * String.format("%s %d", "Example", 7);
     * </pre>
     *
     * </blockquote>
     */
    VARARG,

    /**
     * The parameters are passed as array. For example:
     *
     * <blockquote>
     *
     * <pre>
     * Object[] a = new Object[]{"Example",7};
     * String.format("%s %d", a);
     * </pre>
     *
     * </blockquote>
     */
    ARRAY,

    /**
     * A null array is passed to the format method. This happens seldomly.
     *
     * <blockquote>
     *
     * <pre>
     * String.format("%s %d", (Object[])null);
     * </pre>
     *
     * </blockquote>
     */
    NULLARRAY;
  }

  /** A wrapper around a value of type E, plus an ExpressionTree location. */
  public static class Result<E> {
    private final E value;
    public final ExpressionTree location;

    public Result(E value, ExpressionTree location) {
      this.value = value;
      this.location = location;
    }

    public E value() {
      return value;
    }
  }

  /**
   * Returns true if the call is to a method with the @ReturnsFormat annotation. An example of such
   * a method is FormatUtil.asFormat.
   */
  public boolean isAsFormatCall(MethodInvocationNode node, AnnotatedTypeFactory atypeFactory) {
    ExecutableElement method = node.getTarget().getMethod();
    AnnotationMirror anno = atypeFactory.getDeclAnnotation(method, ReturnsFormat.class);
    return anno != null;
  }

  private ConversionCategory @Nullable [] asFormatCallCategoriesLowLevel(
      MethodInvocationNode node) {
    Node vararg = node.getArgument(1);
    if (!(vararg instanceof ArrayCreationNode)) {
      return null;
    }
    List<Node> convs = ((ArrayCreationNode) vararg).getInitializers();
    ConversionCategory[] res = new ConversionCategory[convs.size()];
    for (int i = 0; i < convs.size(); ++i) {
      Node conv = convs.get(i);
      if (conv instanceof FieldAccessNode) {
        Class<? extends Object> clazz =
            TypesUtils.getClassFromType(((FieldAccessNode) conv).getType());
        if (clazz == ConversionCategory.class) {
          res[i] = ConversionCategory.valueOf(((FieldAccessNode) conv).getFieldName());
          continue; /* avoid returning null */
        }
      }
      return null;
    }
    return res;
  }

  public Result<ConversionCategory[]> asFormatCallCategories(MethodInvocationNode node) {
    // TODO make sure the method signature looks good
    return new Result<>(asFormatCallCategoriesLowLevel(node), node.getTree());
  }

  /**
   * Returns true if {@code tree} is a call to a method annotated with {@code @FormatMethod}.
   *
   * @param tree a method call
   * @param atypeFactory a type factory
   * @return true if {@code tree} is a call to a method annotated with {@code @FormatMethod}
   */
  public boolean isFormatMethodCall(MethodInvocationTree tree, AnnotatedTypeFactory atypeFactory) {
    ExecutableElement method = TreeUtils.elementFromUse(tree);
    AnnotationMirror anno = atypeFactory.getDeclAnnotation(method, FormatMethod.class);
    return anno != null;
  }

  /**
   * Creates a new FormatCall, or returns null.
   *
   * @param invocationTree a method invocation, where the method is annotated @FormatMethod
   * @param atypeFactory the type factory
   * @return a new FormatCall, or null if the invocation is of a method that is improperly
   *     annotated @FormatMethod
   */
  public @Nullable FormatCall create(
      MethodInvocationTree invocationTree, AnnotatedTypeFactory atypeFactory) {
    FormatterTreeUtil ftu = ((FormatterAnnotatedTypeFactory) atypeFactory).treeUtil;
    if (!ftu.isFormatMethodCall(invocationTree, atypeFactory)) {
      return null;
    }

    ExecutableElement methodElement = TreeUtils.elementFromUse(invocationTree);
    int formatStringIndex = FormatterVisitor.formatStringIndex(methodElement);
    if (formatStringIndex == -1) {
      // Reporting the error is redundant if the method was declared in source code, because
      // the visitor will have reported it; but it is necessary if the method was declared in
      // byte code.
      atypeFactory
          .getChecker()
          .reportError(invocationTree, "format.method", methodElement.getSimpleName());
      return null;
    }
    ExpressionTree formatStringTree = invocationTree.getArguments().get(formatStringIndex);
    AnnotatedTypeMirror formatStringType = atypeFactory.getAnnotatedType(formatStringTree);
    List<? extends ExpressionTree> allArgs = invocationTree.getArguments();
    List<? extends ExpressionTree> args = allArgs.subList(formatStringIndex + 1, allArgs.size());

    return new FormatCall(invocationTree, formatStringTree, formatStringType, args, atypeFactory);
  }

  /** Represents a format method invocation in the syntax tree. */
  public class FormatCall {
    /** The call itself. */
    /*package-private*/ final MethodInvocationTree invocationTree;

    /** The format string argument. */
    private final ExpressionTree formatStringTree;

    /** The type of the format string argument. */
    private final AnnotatedTypeMirror formatStringType;

    /** The arguments that follow the format string argument. */
    private final List<? extends ExpressionTree> args;

    /** The type factory. */
    private final AnnotatedTypeFactory atypeFactory;

    /**
     * Create a new FormatCall object.
     *
     * @param invocationTree the call itself
     * @param formatStringTree the format string argument
     * @param formatStringType the type of the format string argument
     * @param args the arguments that follow the format string argument
     * @param atypeFactory the type factory
     */
    private FormatCall(
        MethodInvocationTree invocationTree,
        ExpressionTree formatStringTree,
        AnnotatedTypeMirror formatStringType,
        List<? extends ExpressionTree> args,
        AnnotatedTypeFactory atypeFactory) {
      this.invocationTree = invocationTree;
      this.formatStringTree = formatStringTree;
      this.formatStringType = formatStringType;
      this.args = args;
      this.atypeFactory = atypeFactory;
    }

    /**
     * Returns an error description if the format-string argument's type is <em>not</em> annotated
     * as {@code @Format}. Returns null if it is annotated.
     *
     * @return an error description if the format string is not annotated as {@code @Format}, or
     *     null if it is
     */
    public final @Nullable Result<String> errMissingFormatAnnotation() {
      if (!formatStringType.hasPrimaryAnnotation(Format.class)) {
        String msg = "(is a @Format annotation missing?)";
        AnnotationMirror inv = formatStringType.getPrimaryAnnotation(InvalidFormat.class);
        if (inv != null) {
          msg = invalidFormatAnnotationToErrorMessage(inv);
        }
        return new Result<>(msg, formatStringTree);
      }
      return null;
    }

    /**
     * Returns the type of method invocation.
     *
     * @see InvocationType
     */
    public final Result<InvocationType> getInvocationType() {
      InvocationType type = InvocationType.VARARG;

      if (args.size() == 1) {
        ExpressionTree first = args.get(0);
        TypeMirror argType = atypeFactory.getAnnotatedType(first).getUnderlyingType();
        // figure out if argType is an array
        type =
            argType.accept(
                new SimpleTypeVisitor8<InvocationType, Class<Void>>() {
                  @Override
                  protected InvocationType defaultAction(TypeMirror e, Class<Void> p) {
                    // not an array
                    return InvocationType.VARARG;
                  }

                  @Override
                  public InvocationType visitArray(ArrayType t, Class<Void> p) {
                    // it's an array, now figure out if it's a (Object[])null
                    // array
                    return first.accept(
                        new SimpleTreeVisitor<InvocationType, Class<Void>>() {
                          @Override
                          protected InvocationType defaultAction(Tree tree, Class<Void> p) {
                            // just a normal array
                            return InvocationType.ARRAY;
                          }

                          @Override
                          public InvocationType visitTypeCast(TypeCastTree tree, Class<Void> p) {
                            // it's a (Object[])null
                            return atypeFactory
                                        .getAnnotatedType(tree.getExpression())
                                        .getUnderlyingType()
                                        .getKind()
                                    == TypeKind.NULL
                                ? InvocationType.NULLARRAY
                                : InvocationType.ARRAY;
                          }
                        },
                        p);
                  }

                  @Override
                  public InvocationType visitNull(NullType t, Class<Void> p) {
                    return InvocationType.NULLARRAY;
                  }
                },
                Void.TYPE);
      }

      ExpressionTree loc = invocationTree.getMethodSelect();
      if (type != InvocationType.VARARG && !args.isEmpty()) {
        loc = args.get(0);
      }
      return new Result<>(type, loc);
    }

    /**
     * Returns the conversion category for every parameter.
     *
     * @return the conversion categories of all the parameters
     * @see ConversionCategory
     */
    public final ConversionCategory[] getFormatCategories() {
      AnnotationMirror anno = formatStringType.getPrimaryAnnotation(Format.class);
      return formatAnnotationToCategories(anno);
    }

    /**
     * Returns the types of the arguments to the call. Use {@link #isValidArgument} and {@link
     * #isArgumentNull} to work with the result.
     *
     * @return the types of the arguments to the call
     */
    public final Result<TypeMirror>[] getArgTypes() {
      // One to suppress warning in javac, the other to suppress warning in Eclipse...
      @SuppressWarnings({"rawtypes", "unchecked"})
      Result<TypeMirror>[] res = new Result[args.size()];
      for (int i = 0; i < res.length; ++i) {
        ExpressionTree arg = args.get(i);
        TypeMirror argType;
        if (TreeUtils.isNullExpression(arg)) {
          argType = atypeFactory.getProcessingEnv().getTypeUtils().getNullType();
        } else {
          argType = atypeFactory.getAnnotatedType(arg).getUnderlyingType();
        }
        res[i] = new Result<>(argType, arg);
      }
      return res;
    }

    /**
     * Checks if the type of an argument returned from {@link #getArgTypes()} is valid for the
     * passed ConversionCategory.
     *
     * @param formatCat a format specifier
     * @param argType an argument type
     * @return true if the argument can be passed to the format specifier
     */
    public final boolean isValidArgument(ConversionCategory formatCat, TypeMirror argType) {
      if (argType.getKind() == TypeKind.NULL || isArgumentNull(argType)) {
        return true;
      }
      Class<? extends Object> type = TypesUtils.getClassFromType(argType);
      return formatCat.isAssignableFrom(type);
    }

    /**
     * Checks if the argument returned from {@link #getArgTypes()} is a {@code null} expression.
     *
     * @param type a type
     * @return true if the argument is a {@code null} expression
     */
    public final boolean isArgumentNull(TypeMirror type) {
      // TODO: Just check whether it is the VOID TypeMirror.

      // is it the null literal
      return type.accept(
          new SimpleTypeVisitor8<Boolean, Class<Void>>() {
            @Override
            protected Boolean defaultAction(TypeMirror e, Class<Void> p) {
              // it's not the null literal
              return false;
            }

            @Override
            public Boolean visitNull(NullType t, Class<Void> p) {
              // it's the null literal
              return true;
            }
          },
          Void.TYPE);
    }
  }

  // The failure() method is required so that FormatterTransfer, which has no access to the
  // FormatterChecker, can report errors.
  /**
   * Reports an error.
   *
   * @param res used for source location information
   * @param msgKey the diagnostic message key
   * @param args arguments to the diagnostic message
   */
  public final void failure(Result<?> res, @CompilerMessageKey String msgKey, Object... args) {
    checker.reportError(res.location, msgKey, args);
  }

  /**
   * Reports a warning.
   *
   * @param res used for source location information
   * @param msgKey the diagnostic message key
   * @param args arguments to the diagnostic message
   */
  public final void warning(Result<?> res, @CompilerMessageKey String msgKey, Object... args) {
    checker.reportWarning(res.location, msgKey, args);
  }

  /**
   * Takes an exception that describes an invalid formatter string and, returns a syntax trees
   * element that represents a {@link InvalidFormat} annotation with the exception's error message
   * as value.
   */
  public AnnotationMirror exceptionToInvalidFormatAnnotation(IllegalFormatException ex) {
    return stringToInvalidFormatAnnotation(ex.getMessage());
  }

  /**
   * Creates an {@link InvalidFormat} annotation with the given string as its value.
   *
   * @param invalidFormatString an invalid formatter string
   * @return an {@link InvalidFormat} annotation with the given string as its value
   */
  /*package-private*/ AnnotationMirror stringToInvalidFormatAnnotation(String invalidFormatString) {
    AnnotationBuilder builder = new AnnotationBuilder(processingEnv, InvalidFormat.class);
    builder.setValue("value", invalidFormatString);
    return builder.build();
  }

  /**
   * Gets the value() element/field out of an InvalidFormat annotation.
   *
   * @param anno an InvalidFormat annotation
   * @return its value() element/field
   */
  private String getInvalidFormatValue(AnnotationMirror anno) {
    return (String) anno.getElementValues().get(invalidFormatValueElement).getValue();
  }

  /**
   * Takes a syntax tree element that represents a {@link InvalidFormat} annotation, and returns its
   * value.
   *
   * @param anno an InvalidFormat annotation
   * @return its value() element/field
   */
  public String invalidFormatAnnotationToErrorMessage(AnnotationMirror anno) {
    return "\"" + getInvalidFormatValue(anno) + "\"";
  }

  /**
   * Creates a {@code @}{@link Format} annotation with the given list as its value.
   *
   * @param args conversion categories for the {@code @Format} annotation
   * @return a {@code @}{@link Format} annotation with the given list as its value
   */
  public AnnotationMirror categoriesToFormatAnnotation(ConversionCategory[] args) {
    AnnotationBuilder builder = new AnnotationBuilder(processingEnv, Format.class);
    builder.setValue("value", args);
    return builder.build();
  }

  /**
   * Returns the value of a {@code @}{@link Format} annotation.
   *
   * @param anno a {@code @}{@link Format} annotation
   * @return the annotation's {@code value} element
   */
  @SuppressWarnings("GetClassOnEnum")
  public ConversionCategory[] formatAnnotationToCategories(AnnotationMirror anno) {
    return AnnotationUtils.getElementValueEnumArray(
        anno, formatValueElement, ConversionCategory.class);
  }
}
