package org.checkerframework.javacutil;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import org.checkerframework.dataflow.qual.SideEffectFree;
import org.plumelib.util.ArrayMap;

/** A utility for converting AnnotationMirrors to Strings. It omits full package names. */
public class DefaultAnnotationFormatter implements AnnotationFormatter {

  /**
   * Returns true if, by default, anno should not be printed.
   *
   * @param anno an annotation to check for printability/invisibility
   * @return true if anno's declaration was qualified by InvisibleQualifier
   * @see org.checkerframework.framework.qual.InvisibleQualifier
   */
  public static boolean isInvisibleQualified(AnnotationMirror anno) {
    TypeElement annoElement = (TypeElement) anno.getAnnotationType().asElement();
    for (AnnotationMirror metaAnno : annoElement.getAnnotationMirrors()) {
      if (AnnotationUtils.areSameByName(
          metaAnno, "org.checkerframework.framework.qual.InvisibleQualifier")) {
        return true;
      }
    }
    return false;
  }

  /**
   * Creates a String of each annotation in annos separated by a single space character and
   * terminated by a space character, obeying the printInvisible parameter.
   *
   * @param annos a collection of annotations to print
   * @param printInvisible whether or not to print "invisible" annotation mirrors
   * @return the list of annotations converted to a String
   */
  @Override
  @SideEffectFree
  public String formatAnnotationString(
      Collection<? extends AnnotationMirror> annos, boolean printInvisible) {
    StringBuilder sb = new StringBuilder();
    for (AnnotationMirror obj : annos) {
      if (obj == null) {
        throw new BugInCF(
            "AnnotatedTypeMirror.formatAnnotationString: found null AnnotationMirror");
      }
      if (isInvisibleQualified(obj) && !printInvisible) {
        continue;
      }
      formatAnnotationMirror(obj, sb);
      sb.append(" ");
    }
    return sb.toString();
  }

  /**
   * Returns the string representation of a single AnnotationMirror, without showing full package
   * names.
   *
   * @param anno the annotation mirror to convert
   * @return the string representation of a single AnnotationMirror, without showing full package
   *     names
   */
  @Override
  @SideEffectFree
  public String formatAnnotationMirror(AnnotationMirror anno) {
    StringBuilder sb = new StringBuilder();
    formatAnnotationMirror(anno, sb);
    return sb.toString();
  }

  /** A helper method to output a single AnnotationMirror, without showing full package names. */
  protected void formatAnnotationMirror(AnnotationMirror am, StringBuilder sb) {
    sb.append("@");
    sb.append(am.getAnnotationType().asElement().getSimpleName());
    Map<ExecutableElement, AnnotationValue> args = removeDefaultValues(am.getElementValues());
    if (!args.isEmpty()) {
      sb.append("(");
      boolean oneValue = false;
      if (args.size() == 1) {
        Map.Entry<ExecutableElement, AnnotationValue> first = args.entrySet().iterator().next();
        if (first.getKey().getSimpleName().contentEquals("value")) {
          formatAnnotationMirrorArg(first.getValue(), sb);
          oneValue = true;
        }
      }
      if (!oneValue) {
        boolean notfirst = false;
        for (Map.Entry<ExecutableElement, AnnotationValue> arg : args.entrySet()) {
          if (!"{}".equals(arg.getValue().toString())) {
            if (notfirst) {
              sb.append(", ");
            }
            notfirst = true;
            sb.append(arg.getKey().getSimpleName() + "=");
            formatAnnotationMirrorArg(arg.getValue(), sb);
          }
        }
      }
      sb.append(")");
    }
  }

  /**
   * Returns a new map that only has the values in {@code elementValues} that are not the same as
   * the default value.
   *
   * @param elementValues a mapping of annotation element to annotation value
   * @return a new map with only the non-default values of {@code elementValues}
   */
  private Map<ExecutableElement, AnnotationValue> removeDefaultValues(
      Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues) {
    // Most annotations have no elements.
    Map<ExecutableElement, AnnotationValue> nonDefaults = new ArrayMap<>(0);
    elementValues.forEach(
        (element, value) -> {
          if (element.getDefaultValue() == null
              || !Objects.equals(value.getValue(), element.getDefaultValue().getValue())) {
            nonDefaults.put(element, value);
          }
        });
    return nonDefaults;
  }

  // A helper method to print AnnotationValues (annotation arguments), without showing full
  // package names.
  @SuppressWarnings("unchecked")
  protected void formatAnnotationMirrorArg(AnnotationValue av, StringBuilder sb) {
    Object val = av.getValue();
    if (List.class.isAssignableFrom(val.getClass())) {
      List<AnnotationValue> vallist = (List<AnnotationValue>) val;
      if (vallist.size() == 1) {
        formatAnnotationMirrorArg(vallist.get(0), sb);
      } else {
        sb.append('{');
        boolean notfirst = false;
        for (AnnotationValue nav : vallist) {
          if (notfirst) {
            sb.append(", ");
          }
          notfirst = true;
          formatAnnotationMirrorArg(nav, sb);
        }
        sb.append('}');
      }
    } else if (VariableElement.class.isAssignableFrom(val.getClass())) {
      VariableElement ve = (VariableElement) val;
      sb.append(ve.getEnclosingElement().getSimpleName() + "." + ve.getSimpleName());
    } else {
      sb.append(av.toString());
    }
  }
}
