/*
 * Copyright (C) 2008 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.internal;

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

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.inject.Binding;
import com.google.inject.Key;
import com.google.inject.Stage;
import com.google.inject.TypeLiteral;
import com.google.inject.internal.CycleDetectingLock.CycleDetectingLockFactory;
import com.google.inject.spi.InjectionPoint;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;

/**
 * Manages and injects instances at injector-creation time. This is made more complicated by
 * instances that request other instances while they're being injected. We overcome this by using
 * {@link Initializable}, which attempts to perform injection before use.
 *
 * @author jessewilson@google.com (Jesse Wilson)
 */
final class Initializer {

  /** Is set to true once {@link #validateOustandingInjections} is called. */
  private volatile boolean validationStarted = false;

  /**
   * Allows us to detect circular dependencies. It's only used during injectable reference
   * initialization. After initialization direct access through volatile field is used.
   */
  private final CycleDetectingLockFactory<Class<?>> cycleDetectingLockFactory =
      new CycleDetectingLockFactory<Class<?>>();

  /**
   * Instances that need injection during injector creation to a source that registered them. New
   * references added before {@link #validateOustandingInjections}. Cleared up in {@link
   * #injectAll}.
   */
  private final List<InjectableReference<?>> pendingInjections = Lists.newArrayList();

  /**
   * Map that guarantees that no instance would get two references. New references added before
   * {@link #validateOustandingInjections}. Cleared up in {@link #validateOustandingInjections}.
   */
  private final IdentityHashMap<Object, InjectableReference<?>> initializablesCache =
      Maps.newIdentityHashMap();

  /**
   * Registers an instance for member injection when that step is performed.
   *
   * @param instance an instance that optionally has members to be injected (each annotated
   *     with @Inject).
   * @param binding the binding that caused this initializable to be created, if it exists.
   * @param source the source location that this injection was requested
   */
  <T> Initializable<T> requestInjection(
      InjectorImpl injector,
      T instance,
      Binding<T> binding,
      Object source,
      Set<InjectionPoint> injectionPoints) {
    checkNotNull(source);
    Preconditions.checkState(
        !validationStarted, "Member injection could not be requested after validation is started");
    ProvisionListenerStackCallback<T> provisionCallback =
        binding == null ? null : injector.provisionListenerStore.get(binding);

    // short circuit if the object has no injections or listeners.
    if (instance == null
        || (injectionPoints.isEmpty()
            && !injector.membersInjectorStore.hasTypeListeners()
            && provisionCallback == null)) {
      return Initializables.of(instance);
    }

    if (initializablesCache.containsKey(instance)) {
      @SuppressWarnings("unchecked") // Map from T to InjectableReference<T>
      Initializable<T> cached = (Initializable<T>) initializablesCache.get(instance);
      return cached;
    }

    InjectableReference<T> injectableReference =
        new InjectableReference<T>(
            injector,
            instance,
            binding == null ? null : binding.getKey(),
            provisionCallback,
            source,
            cycleDetectingLockFactory.create(instance.getClass()));
    initializablesCache.put(instance, injectableReference);
    pendingInjections.add(injectableReference);
    return injectableReference;
  }

  /**
   * Prepares member injectors for all injected instances. This prompts Guice to do static analysis
   * on the injected instances.
   */
  void validateOustandingInjections(Errors errors) {
    validationStarted = true;
    initializablesCache.clear();
    for (InjectableReference<?> reference : pendingInjections) {
      try {
        reference.validate(errors);
      } catch (ErrorsException e) {
        errors.merge(e.getErrors());
      }
    }
  }

  /**
   * Performs creation-time injections on all objects that require it. Whenever fulfilling an
   * injection depends on another object that requires injection, we inject it first. If the two
   * instances are codependent (directly or transitively), ordering of injection is arbitrary.
   */
  void injectAll(final Errors errors) {
    Preconditions.checkState(validationStarted, "Validation should be done before injection");
    for (InjectableReference<?> reference : pendingInjections) {
      try {
        reference.get();
      } catch (InternalProvisionException ipe) {
        errors.merge(ipe);
      }
    }
    pendingInjections.clear();
  }

  private enum InjectableReferenceState {
    NEW,
    VALIDATED,
    INJECTING,
    READY
  }

  private static class InjectableReference<T> implements Initializable<T> {
    private volatile InjectableReferenceState state = InjectableReferenceState.NEW;
    private volatile MembersInjectorImpl<T> membersInjector = null;

    private final InjectorImpl injector;
    private final T instance;
    private final Object source;
    private final Key<T> key;
    private final ProvisionListenerStackCallback<T> provisionCallback;
    private final CycleDetectingLock<?> lock;

    public InjectableReference(
        InjectorImpl injector,
        T instance,
        Key<T> key,
        ProvisionListenerStackCallback<T> provisionCallback,
        Object source,
        CycleDetectingLock<?> lock) {
      this.injector = injector;
      this.key = key; // possibly null!
      this.provisionCallback = provisionCallback; // possibly null!
      this.instance = checkNotNull(instance, "instance");
      this.source = checkNotNull(source, "source");
      this.lock = checkNotNull(lock, "lock");
    }

    public void validate(Errors errors) throws ErrorsException {
      @SuppressWarnings("unchecked") // the type of 'T' is a TypeLiteral<T>
      TypeLiteral<T> type = TypeLiteral.get((Class<T>) instance.getClass());
      membersInjector = injector.membersInjectorStore.get(type, errors.withSource(source));
      Preconditions.checkNotNull(
          membersInjector,
          "No membersInjector available for instance: %s, from key: %s",
          instance,
          key);
      state = InjectableReferenceState.VALIDATED;
    }

    /**
     * Reentrant. If {@code instance} was registered for injection at injector-creation time, this
     * method will ensure that all its members have been injected before returning.
     */
    @Override
    public T get() throws InternalProvisionException {
      // skipping acquiring lock if initialization is already finished
      if (state == InjectableReferenceState.READY) {
        return instance;
      }

      // acquire lock for current binding to initialize an instance
      Multimap<?, ?> lockCycle = lock.lockOrDetectPotentialLocksCycle();
      if (!lockCycle.isEmpty()) {
        // Potential deadlock detected and creation lock is not taken.
        // According to injectAll()'s contract return non-initialized instance.

        // This condition should not be possible under the current Guice implementation.
        // This clause exists for defensive programming purposes.

        // Reasoning:
        // get() is called either directly from injectAll(), holds no locks and can not create
        // a cycle, or it is called through a singleton scope, which resolves deadlocks by itself.
        // Before calling get() object has to be requested for injection.
        // Initializer.requestInjection() is called either for constant object bindings, which wrap
        // creation into a Singleton scope, or from Binder.requestInjection(), which
        // has to use Singleton scope to reuse the same InjectableReference to potentially
        // create a lock cycle.
        return instance;
      }
      try {
        // lock acquired, current thread owns this instance initialization
        switch (state) {
          case READY:
            return instance;
            // When instance depends on itself in the same thread potential dead lock
            // is not detected. We have to prevent a stack overflow and we use
            // an "injecting" stage to short-circuit a call.
          case INJECTING:
            return instance;
          case VALIDATED:
            state = InjectableReferenceState.INJECTING;
            break;
          case NEW:
            throw new IllegalStateException("InjectableReference is not validated yet");
          default:
            throw new IllegalStateException("Unknown state: " + state);
        }

        // if in Stage.TOOL, we only want to inject & notify toolable injection points.
        // (otherwise we'll inject all of them)
        try {
          membersInjector.injectAndNotify(
              instance, key, provisionCallback, source, injector.options.stage == Stage.TOOL);
        } catch (InternalProvisionException ipe) {
          throw ipe.addSource(source);
        }
        // mark instance as ready to skip a lock on subsequent calls
        state = InjectableReferenceState.READY;
        return instance;
      } finally {
        // always release our creation lock, even on failures
        lock.unlock();
      }
    }

    @Override
    public String toString() {
      return instance.toString();
    }
  }
}
