package org.checkerframework.framework.type.typeannotator;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultFor;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.framework.type.treeannotator.LiteralTreeAnnotator;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationMirrorSet;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.TypeSystemError;
import org.checkerframework.javacutil.TypesUtils;

/**
 * Adds annotations to a type based on the use of a type. This class applies annotations specified
 * by {@link DefaultFor}; it is designed to be used in a {@link ListTypeAnnotator} constructed in
 * {@link GenericAnnotatedTypeFactory#createTypeAnnotator()} ()}
 *
 * <p>{@link DefaultForTypeAnnotator} traverses types deeply.
 *
 * <p>This class takes care of two of the attributes of {@link DefaultFor}; the others are handled
 * in {@link org.checkerframework.framework.util.defaults.QualifierDefaults}.
 *
 * @see ListTypeAnnotator
 */
public class DefaultForTypeAnnotator extends TypeAnnotator {

  /** Map from {@link TypeKind} to annotations. */
  private final Map<TypeKind, AnnotationMirrorSet> typeKinds;

  /** Map from {@link AnnotatedTypeMirror} classes to annotations. */
  private final Map<Class<? extends AnnotatedTypeMirror>, AnnotationMirrorSet> atmClasses;

  /** Map from fully qualified class name strings to annotations. */
  private final Map<String, AnnotationMirrorSet> types;

  /**
   * A list where each element associates an annotation with name regexes and name exception
   * regexes.
   */
  private final ListOfNameRegexes listOfNameRegexes;

  /** {@link QualifierHierarchy} */
  private final QualifierHierarchy qualHierarchy;

  /**
   * Creates a {@link DefaultForTypeAnnotator} from the given checker, using that checker to
   * determine the annotations that are in the type hierarchy.
   */
  public DefaultForTypeAnnotator(AnnotatedTypeFactory typeFactory) {
    super(typeFactory);
    this.typeKinds = new EnumMap<>(TypeKind.class);
    this.atmClasses = new HashMap<>();
    this.types = new HashMap<>();
    this.listOfNameRegexes = new ListOfNameRegexes();

    this.qualHierarchy = typeFactory.getQualifierHierarchy();

    // Get type qualifiers from the checker.
    Set<Class<? extends Annotation>> quals = typeFactory.getSupportedTypeQualifiers();

    // For each qualifier, read the @DefaultFor annotation and put its types, kinds, and names
    // into maps.
    for (Class<? extends Annotation> qual : quals) {
      DefaultFor defaultFor = qual.getAnnotation(DefaultFor.class);
      if (defaultFor == null) {
        continue;
      }

      AnnotationMirror theQual = AnnotationBuilder.fromClass(typeFactory.getElementUtils(), qual);

      for (org.checkerframework.framework.qual.TypeKind typeKind : defaultFor.typeKinds()) {
        TypeKind mappedTk = mapTypeKinds(typeKind);
        addTypeKind(mappedTk, theQual);
      }

      for (Class<?> typeName : defaultFor.types()) {
        addTypes(typeName, theQual);
      }

      listOfNameRegexes.add(theQual, defaultFor);
    }
  }

  /**
   * Map between {@link org.checkerframework.framework.qual.TypeKind} and {@link
   * javax.lang.model.type.TypeKind}.
   *
   * @param typeKind the Checker Framework TypeKind
   * @return the javax TypeKind
   */
  private TypeKind mapTypeKinds(org.checkerframework.framework.qual.TypeKind typeKind) {
    return TypeKind.valueOf(typeKind.name());
  }

  /** Add default qualifier, {@code theQual}, for the given TypeKind. */
  public void addTypeKind(TypeKind typeKind, AnnotationMirror theQual) {
    boolean res = qualHierarchy.updateMappingToMutableSet(typeKinds, typeKind, theQual);
    if (!res) {
      throw new BugInCF(
          "TypeAnnotator: invalid update of typeKinds "
              + typeKinds
              + " at "
              + typeKind
              + " with "
              + theQual);
    }
  }

  /** Add default qualifier, {@code theQual}, for the given {@link AnnotatedTypeMirror} class. */
  public void addAtmClass(
      Class<? extends AnnotatedTypeMirror> typeClass, AnnotationMirror theQual) {
    boolean res = qualHierarchy.updateMappingToMutableSet(atmClasses, typeClass, theQual);
    if (!res) {
      throw new BugInCF(
          "TypeAnnotator: invalid update of atmClasses "
              + atmClasses
              + " at "
              + typeClass
              + " with "
              + theQual);
    }
  }

  /** Add default qualifier, {@code theQual}, for the given type. */
  public void addTypes(Class<?> clazz, AnnotationMirror theQual) {
    String typeNameString = clazz.getCanonicalName();
    boolean res = qualHierarchy.updateMappingToMutableSet(types, typeNameString, theQual);
    if (!res) {
      throw new BugInCF(
          "TypeAnnotator: invalid update of types " + types + " at " + clazz + " with " + theQual);
    }
  }

  @Override
  protected Void scan(AnnotatedTypeMirror type, Void p) {
    // If the type's fully-qualified name is in the appropriate map, annotate the type. Do this
    // before looking at kind or class, as this information is more specific.

    String qname;
    if (type.getKind() == TypeKind.DECLARED) {
      qname = TypesUtils.getQualifiedName((DeclaredType) type.getUnderlyingType());
    } else if (type.getKind().isPrimitive()) {
      qname = type.getUnderlyingType().toString();
    } else {
      qname = null;
    }

    if (qname != null) {
      AnnotationMirrorSet fromQname = types.get(qname);
      if (fromQname != null) {
        type.addMissingAnnotations(fromQname);
      }
    }

    // If the type's kind or class is in the appropriate map, annotate the type.
    AnnotationMirrorSet fromKind = typeKinds.get(type.getKind());
    if (fromKind != null) {
      type.addMissingAnnotations(fromKind);
    } else if (!atmClasses.isEmpty()) {
      Class<? extends AnnotatedTypeMirror> t = type.getClass();
      AnnotationMirrorSet fromClass = atmClasses.get(t);
      if (fromClass != null) {
        type.addMissingAnnotations(fromClass);
      }
    }

    return super.scan(type, p);
  }

  /**
   * Adds standard rules. Currently, sets Void to bottom if no other qualifier is set for Void.
   * Also, see {@link LiteralTreeAnnotator#addStandardLiteralQualifiers()}.
   *
   * @return this
   */
  public DefaultForTypeAnnotator addStandardDefaults() {
    if (!types.containsKey(Void.class.getCanonicalName())) {
      for (AnnotationMirror bottom : qualHierarchy.getBottomAnnotations()) {
        addTypes(Void.class, bottom);
      }
    } else {
      AnnotationMirrorSet annos = types.get(Void.class.getCanonicalName());
      for (AnnotationMirror top : qualHierarchy.getTopAnnotations()) {
        if (qualHierarchy.findAnnotationInHierarchy(annos, top) == null) {
          addTypes(Void.class, qualHierarchy.getBottomAnnotation(top));
        }
      }
    }

    return this;
  }

  /**
   * Apply defaults based on a variable name to a type.
   *
   * @param type a type to apply defaults to
   * @param name the name of the variable that has type {@code type}, or the name of the method
   *     whose return type is {@code type}
   */
  public void defaultTypeFromName(AnnotatedTypeMirror type, String name) {
    // TODO: Check whether the annotation is applicable to this Java type?
    AnnotationMirror defaultAnno = listOfNameRegexes.getDefaultAnno(name);
    if (defaultAnno != null) {
      if (atypeFactory
              .getQualifierHierarchy()
              .findAnnotationInHierarchy(type.getPrimaryAnnotations(), defaultAnno)
          == null) {
        type.addAnnotation(defaultAnno);
      }
    }
  }

  @Override
  public Void visitExecutable(AnnotatedExecutableType type, Void aVoid) {
    ExecutableElement element = type.getElement();

    Iterator<AnnotatedTypeMirror> paramTypes = type.getParameterTypes().iterator();
    for (VariableElement paramElt : element.getParameters()) {
      String paramName = paramElt.getSimpleName().toString();
      AnnotatedTypeMirror paramType = paramTypes.next();
      defaultTypeFromName(paramType, paramName);
    }

    String methodName = element.getSimpleName().toString();
    AnnotatedTypeMirror returnType = type.getReturnType();
    defaultTypeFromName(returnType, methodName);

    return super.visitExecutable(type, aVoid);
  }

  /**
   * A list where each element associates an annotation with name regexes and name exception
   * regexes.
   */
  private static class ListOfNameRegexes extends ArrayList<NameRegexes> {

    static final long serialVersionUID = 20200218L;

    /**
     * Update this list from the {@code names} and {@code namesExceptions} fields of a @DefaultFor
     * annotation.
     *
     * @param theQual the qualifier that a @DefaultFor annotation is written on
     * @param defaultFor the @DefaultFor annotation written on {@code theQual}
     */
    void add(AnnotationMirror theQual, DefaultFor defaultFor) {
      if (defaultFor.names().length != 0) {
        NameRegexes thisName = new NameRegexes(theQual);
        for (String nameRegex : defaultFor.names()) {
          try {
            thisName.names.add(Pattern.compile(nameRegex));
          } catch (PatternSyntaxException e) {
            throw new TypeSystemError(
                "In annotation %s, names() value \"%s\" is not a regular expression",
                theQual, nameRegex);
          }
        }
        for (String namesExceptionsRegex : defaultFor.namesExceptions()) {
          try {
            thisName.namesExceptions.add(Pattern.compile(namesExceptionsRegex));
          } catch (PatternSyntaxException e) {
            throw new TypeSystemError(
                "In annotation %s, namesExceptions() value \"%s\" is not a regular expression",
                theQual, namesExceptionsRegex);
          }
        }
        add(thisName);
      } else if (defaultFor.namesExceptions().length != 0) {
        throw new TypeSystemError(
            "On annotation %s, %s has empty names() but nonempty namesExceptions()",
            theQual, defaultFor);
      }
    }

    /**
     * Returns the annotation that should be the default for a variable of the given name, or for
     * the return type of a method of the given name.
     *
     * @param name a variable name
     * @return the annotation that should be the default for a variable named {@code name}, or null
     *     if none
     */
    @Nullable AnnotationMirror getDefaultAnno(String name) {
      if (this.isEmpty()) {
        return null;
      }
      AnnotationMirror result = null;
      for (NameRegexes nameRegexes : this) {
        if (nameRegexes.matches(name)) {
          if (result == null) {
            result = nameRegexes.anno;
          } else {
            // This could combine the annotations instead, but I think doing so
            // silently would confuse users.
            throw new TypeSystemError(
                "Multiple annotations are applicable to the name \"%s\"", name);
          }
        }
      }
      return result;
    }
  }

  /**
   * Associates an annotation with the variable names that cause the annotation to be chosen as a
   * default.
   */
  private static class NameRegexes {
    /** The annotation. */
    final AnnotationMirror anno;

    /** The name regexes. */
    final List<Pattern> names = new ArrayList<>(0);

    /** The name exception regexes. */
    final List<Pattern> namesExceptions = new ArrayList<>(0);

    /**
     * Constructs a NameRegexes from a @DefaultFor annotation.
     *
     * @param theQual the qualifier that {@code defaultFor} is written on
     */
    NameRegexes(AnnotationMirror theQual) {
      this.anno = theQual;
    }

    /**
     * Returns true if the regular expressions match the given name -- that is, if {@link #anno}
     * should be used as the default type for a variable named {@code name}, or for the return type
     * of a method named {@code name}.
     *
     * @param name a variable or method name
     * @return true if {@link #anno} should be used as the default for a variable named {@code name}
     */
    public boolean matches(String name) {
      return names.stream().anyMatch(p -> p.matcher(name).matches())
          && namesExceptions.stream().noneMatch(p -> p.matcher(name).matches());
    }
  }
}
