/*
 * Copyright (C) 2015 Google, Inc.
 *
 * 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 dagger.internal.codegen;

import com.google.auto.common.AnnotationMirrors;
import com.google.auto.common.MoreTypes;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import javax.annotation.Nullable;
import javax.inject.Singleton;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import static com.google.auto.common.MoreTypes.isTypeOf;
import static dagger.internal.codegen.ErrorMessages.stripCommonTypePrefixes;
import static dagger.internal.codegen.InjectionAnnotations.getScopeAnnotation;

/**
 * A representation of the scope (or lack of it) associated with a component, providing method
 * or injection location.
 */
final class Scope {

  /**
   * An internal representation for an unscoped binding.
   */
  private static final Scope UNSCOPED = new Scope();

  /**
   * The underlying {@link AnnotationMirror} that represents the scope annotation.
   */
  @Nullable
  private final AnnotationMirror annotationMirror;

  private Scope(@Nullable AnnotationMirror annotationMirror) {
    this.annotationMirror = annotationMirror;
  }

  private Scope() {
    this(null);
  }

  /**
   * Returns representation for an unscoped binding.
   */
  static Scope unscoped() {
    return UNSCOPED;
  }

  /**
   * If the source code element has an associated scoped annotation then returns a representation
   * of that scope, otherwise returns a representation for an unscoped binding.
   */
  static Scope scopeOf(Element element) {
    Optional<AnnotationMirror> scopeAnnotation = getScopeAnnotation(element);
    return scopeAnnotation.isPresent() ? new Scope(scopeAnnotation.get()) : UNSCOPED;
  }

  /**
   * Returns true if the scope is present, i.e. it's not unscoped binding.
   */
  public boolean isPresent() {
    return annotationMirror != null;
  }

  /**
   * Returns true if the scope represents the {@link Singleton @Singleton} annotation.
   */
  public boolean isSingleton() {
    return annotationMirror != null
        && isTypeOf(Singleton.class, annotationMirror.getAnnotationType());
  }

  /**
   * Returns the readable source representation (name with @ prefix) of the annotation type.
   *
   * <p>It's readable source because it has had common package prefixes removed, e.g.
   * {@code @javax.inject.Singleton} is returned as {@code @Singleton}.
   *
   * <p>Make sure that the scope is actually {@link #isPresent() present} before calling as it will
   * throw an {@link IllegalStateException} otherwise. This does not return any annotation values
   * as according to {@link javax.inject.Scope} scope annotations are not supposed to use them.
   */
  public String getReadableSource() {
    return stripCommonTypePrefixes("@" + getQualifiedName());
  }

  /**
   * Returns the fully qualified name of the annotation type.
   *
   * <p>Make sure that the scope is actually {@link #isPresent() present} before calling as it will
   * throw an {@link IllegalStateException} otherwise. This does not return any annotation values
   * as according to {@link javax.inject.Scope} scope annotations are not supposed to use them.
   */
  public String getQualifiedName() {
    Preconditions.checkState(annotationMirror != null,
        "Cannot create a stripped source representation of no annotation");
    TypeElement typeElement = MoreTypes.asTypeElement(annotationMirror.getAnnotationType());
    return typeElement.getQualifiedName().toString();
  }

  /**
   * Scopes are equal if the underlying {@link AnnotationMirror} are equivalent according to
   * {@link AnnotationMirrors#equivalence()}.
   */
  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    } else if (obj instanceof Scope) {
      Scope that = (Scope) obj;
      return AnnotationMirrors.equivalence()
        .equivalent(this.annotationMirror, that.annotationMirror);
    } else {
      return false;
    }
  }

  @Override
  public int hashCode() {
    return AnnotationMirrors.equivalence().hash(annotationMirror);
  }

  /**
   * Returns a debug representation of the scope.
   */
  @Override
  public String toString() {
    return annotationMirror == null ? "UNSCOPED" : annotationMirror.toString();
  }
}