/**
 * Copyright (C) 2006 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 com.google.inject;

import com.google.inject.internal.CircularDependencyProxy;
import com.google.inject.internal.LinkedBindingImpl;
import com.google.inject.spi.BindingScopingVisitor;
import com.google.inject.spi.ExposedBinding;

import java.lang.annotation.Annotation;

/**
 * Built-in scope implementations.
 *
 * @author crazybob@google.com (Bob Lee)
 */
public class Scopes {

  private Scopes() {}

  /** A sentinel value representing null. */
  private static final Object NULL = new Object();

  /**
   * One instance per {@link Injector}. Also see {@code @}{@link Singleton}.
   */
  public static final Scope SINGLETON = new Scope() {
    public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
      return new Provider<T>() {
        /*
         * The lazily initialized singleton instance. Once set, this will either have type T or will
         * be equal to NULL.
         */
        private volatile Object instance;

        // DCL on a volatile is safe as of Java 5, which we obviously require.
        @SuppressWarnings("DoubleCheckedLocking")
        public T get() {
          if (instance == null) {
            /*
             * This block is re-entrant for circular dependencies.
             */
            synchronized (this) {
              if (instance == null) {
                T provided = creator.get();

                // don't remember proxies; these exist only to serve circular dependencies
                if (isCircularProxy(provided)) {
                  return provided;
                }

                Object providedOrSentinel = (provided == null) ? NULL : provided;
                if (instance != null && instance != providedOrSentinel) {
                  throw new ProvisionException(
                      "Provider was reentrant while creating a singleton");
                }

                instance = providedOrSentinel;
              }
            }
          }

          Object localInstance = instance;
          // This is safe because instance has type T or is equal to NULL
          @SuppressWarnings("unchecked")
          T returnedInstance = (localInstance != NULL) ? (T) localInstance : null;
          return returnedInstance;
        }

        @Override
        public String toString() {
          return String.format("%s[%s]", creator, SINGLETON);
        }
      };
    }

    @Override public String toString() {
      return "Scopes.SINGLETON";
    }
  };

  /**
   * No scope; the same as not applying any scope at all.  Each time the
   * Injector obtains an instance of an object with "no scope", it injects this
   * instance then immediately forgets it.  When the next request for the same
   * binding arrives it will need to obtain the instance over again.
   *
   * <p>This exists only in case a class has been annotated with a scope
   * annotation such as {@link Singleton @Singleton}, and you need to override
   * this to "no scope" in your binding.
   *
   * @since 2.0
   */
  public static final Scope NO_SCOPE = new Scope() {
    public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped) {
      return unscoped;
    }
    @Override public String toString() {
      return "Scopes.NO_SCOPE";
    }
  };

  private static final BindingScopingVisitor<Boolean> IS_SINGLETON_VISITOR
      = new BindingScopingVisitor<Boolean>() {
        public Boolean visitNoScoping() {
          return false;
        }

        public Boolean visitScopeAnnotation(Class<? extends Annotation> scopeAnnotation) {
          return scopeAnnotation == Singleton.class
              || scopeAnnotation == javax.inject.Singleton.class;
        }

        public Boolean visitScope(Scope scope) {
          return scope == Scopes.SINGLETON;
        }

        public Boolean visitEagerSingleton() {
          return true;
        }
      };

  /**
   * Returns true if {@code binding} is singleton-scoped. If the binding is a {@link
   * com.google.inject.spi.LinkedKeyBinding linked key binding} and belongs to an injector (ie. it
   * was retrieved via {@link Injector#getBinding Injector.getBinding()}), then this method will
   * also true if the target binding is singleton-scoped.
   *
   * @since 3.0
   */
  public static boolean isSingleton(Binding<?> binding) {
    do {
      boolean singleton = binding.acceptScopingVisitor(IS_SINGLETON_VISITOR);
      if (singleton) {
        return true;
      }

      if (binding instanceof LinkedBindingImpl) {
        LinkedBindingImpl<?> linkedBinding = (LinkedBindingImpl) binding;
        Injector injector = linkedBinding.getInjector();
        if (injector != null) {
          binding = injector.getBinding(linkedBinding.getLinkedKey());
          continue;
        }
      } else if(binding instanceof ExposedBinding) {
        ExposedBinding<?> exposedBinding = (ExposedBinding)binding;
        Injector injector = exposedBinding.getPrivateElements().getInjector();
        if (injector != null) {
          binding = injector.getBinding(exposedBinding.getKey());
          continue;
        }
      }

      return false;
    } while (true);
  }

  /**

   * Returns true if {@code binding} has the given scope. If the binding is a {@link
   * com.google.inject.spi.LinkedKeyBinding linked key binding} and belongs to an injector (ie. it
   * was retrieved via {@link Injector#getBinding Injector.getBinding()}), then this method will
   * also true if the target binding has the given scope.
   *
   * @param binding binding to check
   * @param scope scope implementation instance
   * @param scopeAnnotation scope annotation class
   */
  public static boolean isScoped(Binding<?> binding, final Scope scope,
      final Class<? extends Annotation> scopeAnnotation) {
    do {
      boolean matches = binding.acceptScopingVisitor(new BindingScopingVisitor<Boolean>() {
        public Boolean visitNoScoping() {
          return false;
        }

        public Boolean visitScopeAnnotation(Class<? extends Annotation> visitedAnnotation) {
          return visitedAnnotation == scopeAnnotation;
        }

        public Boolean visitScope(Scope visitedScope) {
          return visitedScope == scope;
        }

        public Boolean visitEagerSingleton() {
          return false;
        }
      });

      if (matches) {
        return true;
      }

      if (binding instanceof LinkedBindingImpl) {
        LinkedBindingImpl<?> linkedBinding = (LinkedBindingImpl) binding;
        Injector injector = linkedBinding.getInjector();
        if (injector != null) {
          binding = injector.getBinding(linkedBinding.getLinkedKey());
          continue;
        }
      } else if(binding instanceof ExposedBinding) {
        ExposedBinding<?> exposedBinding = (ExposedBinding)binding;
        Injector injector = exposedBinding.getPrivateElements().getInjector();
        if (injector != null) {
          binding = injector.getBinding(exposedBinding.getKey());
          continue;
        }
      }

      return false;
    } while (true);
  }

  /**
   * Returns true if the object is a proxy for a circular dependency,
   * constructed by Guice because it encountered a circular dependency. Scope
   * implementations should be careful to <b>not cache circular proxies</b>,
   * because the proxies are not intended for general purpose use. (They are
   * designed just to fulfill the immediate injection, not all injections.
   * Caching them can lead to IllegalArgumentExceptions or ClassCastExceptions.)
   */
  public static boolean isCircularProxy(Object object) {
    return object instanceof CircularDependencyProxy;
  }
}
