/*
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * 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;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.BugPattern.Suppressibility;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.Tree;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckReturnValue;

/**
 * An accessor for information about a single bug checker, including the metadata in the check's
 * {@code @BugPattern} annotation and the class that implements the check.
 */
public class BugCheckerInfo implements Serializable {

  /**
   * The BugChecker class.
   */
  private final Class<? extends BugChecker> checker;

  /**
   * The canonical name of this check. Corresponds to the {@code name} attribute from its
   * {@code BugPattern} annotation.
   */
  private final String canonicalName;

  /**
   * Additional identifiers for this check, to be checked for in @SuppressWarnings annotations.
   * Corresponds to the canonical name plus all {@code altName}s from its {@code BugPattern}
   * annotation.
   */
  private final ImmutableSet<String> allNames;

  /**
   * The error message to print in compiler diagnostics when this check triggers. Corresponds to the
   * {@code summary} attribute from its {@code BugPattern}.
   */
  private final String message;

  /**
   * The default type of diagnostic (error or warning) to emit when this check triggers.
   */
  private final SeverityLevel defaultSeverity;

  /**
   * The link URL to display in the diagnostic message when this check triggers. Computed from the
   * {@code link} and {@code linkType} attributes from its {@code BugPattern}. May be null if no
   * link should be displayed.
   */
  private final String linkUrl;

  /**
   * Whether this check may be suppressed. Corresponds to the {@code suppressibility} attribute from
   * its {@code BugPattern}.
   */
  private final Suppressibility suppressibility;

  /**
   * A set of custom suppression annotations for this check. Computed from the
   * {@code suppressibility} and {@code customSuppressionAnnotations} attributes from its {@code
   * BugPattern}.  May be empty if there are no custom suppression annotations for this check.
   */
  private final Set<Class<? extends Annotation>> customSuppressionAnnotations;

  public static BugCheckerInfo create(Class<? extends BugChecker> checker) {
    BugPattern pattern =
        checkNotNull(
            checker.getAnnotation(BugPattern.class),
            "BugCheckers must be annotated with @BugPattern");
    try {
      BugPatternValidator.validate(pattern);
    } catch (ValidationException e) {
      throw new IllegalStateException(e);
    }
    return new BugCheckerInfo(checker, pattern);
  }

  private BugCheckerInfo(Class<? extends BugChecker> checker, BugPattern pattern) {
    this.checker = checker;
    canonicalName = pattern.name();
    allNames = ImmutableSet.<String>builder().add(canonicalName).add(pattern.altNames()).build();
    message = pattern.summary();
    defaultSeverity = pattern.severity();
    linkUrl = createLinkUrl(pattern);
    suppressibility = pattern.suppressibility();
    if (suppressibility == Suppressibility.CUSTOM_ANNOTATION) {
      customSuppressionAnnotations =
          new HashSet<>(Arrays.asList(pattern.customSuppressionAnnotations()));
    } else {
      customSuppressionAnnotations = Collections.<Class<? extends Annotation>>emptySet();
    }
  }

  private static final String URL_FORMAT = "http://errorprone.info/bugpattern/%s";

  private static String createLinkUrl(BugPattern pattern) {
    switch (pattern.linkType()) {
      case AUTOGENERATED:
        return String.format(URL_FORMAT, pattern.name());
      case CUSTOM:
        // annotation.link() must be provided.
        if (pattern.link().isEmpty()) {
          throw new IllegalStateException(
              "If linkType element of @BugPattern is CUSTOM, "
                  + "a link element must also be provided.");
        }
        return pattern.link();
      case NONE:
        return null;
      default:
        throw new IllegalStateException(
            "Unexpected value for linkType element of @BugPattern: " + pattern.linkType());
    }
  }

  /**
   * Returns a new builder for {@link Description}s.
   *
   * @param node the node where the error is
   * @param checker the {@code BugChecker} instance that is producing this {@code Description}
   */
  @CheckReturnValue
  public static Description.Builder buildDescriptionFromChecker(Tree node, BugChecker checker) {
    return Description.builder(
        Preconditions.checkNotNull(node),
        checker.canonicalName(),
        checker.linkUrl(),
        checker.defaultSeverity(),
        checker.message());
  }

  public String canonicalName() {
    return canonicalName;
  }

  public Set<String> allNames() {
    return allNames;
  }

  public String message() {
    return message;
  }

  public SeverityLevel defaultSeverity() {
    return defaultSeverity;
  }

  public SeverityLevel severity(Map<String, SeverityLevel> severities) {
    return firstNonNull(severities.get(canonicalName), defaultSeverity);
  }

  public String linkUrl() {
    return linkUrl;
  }

  public Suppressibility suppressibility() {
    return suppressibility;
  }

  public Set<Class<? extends Annotation>> customSuppressionAnnotations() {
    return customSuppressionAnnotations;
  }

  public Class<? extends BugChecker> checkerClass() {
    return checker;
  }

  @Override
  public int hashCode() {
    return checker.hashCode();
  }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof BugCheckerInfo)) {
      return false;
    }
    return checker.equals(((BugCheckerInfo) o).checker);
  }

  @Override
  public String toString() {
    return canonicalName;
  }
}

@@ -699,16 +699,82 @@
 onNull;%0A
+import static com.google.common.base.Preconditions.checkArgument;%0A
 import s
@@ -1237,24 +1237,59 @@
 Annotation;%0A
+import java.lang.reflect.Modifier;%0A
 import java.
@@ -3633,24 +3633,216 @@
 gPattern%22);%0A
+    checkArgument(%0A        !(Modifier.isAbstract(checker.getModifiers())%0A            %7C%7C Modifier.isInterface(checker.getModifiers())),%0A        %22%25s must be a concrete class%22,%0A        checker);%0A
     try %7B%0A  

Add some more validation to BugCheckerInfo