package com.google.inject.internal;

import com.google.common.collect.ImmutableSet;
import com.google.inject.Key;
import com.google.inject.Provider;
import com.google.inject.internal.ProvisionListenerStackCallback.ProvisionCallback;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.HasDependencies;
import com.google.inject.spi.InjectionPoint;
import com.google.inject.spi.ProviderWithExtensionVisitor;

/**
 * A {@link ProviderInstanceBindingImpl} for implementing 'native' guice extensions.
 *
 * <p>Beyond the normal binding contract that is mostly handled by our baseclass, this also
 * implements {@link DelayedInitialize} in order to initialize factory state.
 */
final class InternalProviderInstanceBindingImpl<T> extends ProviderInstanceBindingImpl<T>
    implements DelayedInitialize {
  enum InitializationTiming {
    /** This factory can be initialized eagerly. This should be the case for most things. */
    EAGER,

    /**
     * Initialization of this factory should be delayed until after all other static initialization
     * completes. This will be useful for factories that need to call {@link
     * InjectorImpl#getExistingBinding(Key)} to not create jit bindings, but also want to be able to
     * conditionally consume jit bindings created by other other bindings.
     */
    DELAYED;
  }

  private final Factory<T> originalFactory;

  InternalProviderInstanceBindingImpl(
      InjectorImpl injector,
      Key<T> key,
      Object source,
      Factory<T> originalFactory,
      InternalFactory<? extends T> scopedFactory,
      Scoping scoping) {
    super(
        injector,
        key,
        source,
        scopedFactory,
        scoping,
        originalFactory,
        ImmutableSet.<InjectionPoint>of());
    this.originalFactory = originalFactory;
  }

  InitializationTiming getInitializationTiming() {
    return originalFactory.initializationTiming;
  }

  @Override
  public void initialize(final InjectorImpl injector, final Errors errors) throws ErrorsException {
    originalFactory.source = getSource();
    originalFactory.provisionCallback = injector.provisionListenerStore.get(this);
    // For these kinds of providers, the 'user supplied provider' is really 'guice supplied'
    // So make our user supplied provider just delegate to the guice supplied one.
    originalFactory.delegateProvider = getProvider();
    originalFactory.initialize(injector, errors);
  }

  /**
   * A base factory implementation. Any Factories that delegate to other bindings should use the
   * {@code CyclicFactory} subclass, but trivial factories can use this one.
   */
  abstract static class Factory<T> implements InternalFactory<T>, Provider<T>, HasDependencies {
    private final InitializationTiming initializationTiming;
    private Object source;
    private Provider<T> delegateProvider;
    ProvisionListenerStackCallback<T> provisionCallback;

    Factory(InitializationTiming initializationTiming) {
      this.initializationTiming = initializationTiming;
    }
    /**
     * The binding source.
     *
     * <p>May be useful for augmenting runtime error messages.
     *
     * <p>Note: this will return {@code null} until {@link #initialize(InjectorImpl, Errors)} has
     * already been called.
     */
    final Object getSource() {
      return source;
    }

    /**
     * A callback that allows for implementations to fetch dependencies on other bindings.
     *
     * <p>Will be called exactly once, prior to any call to {@link #doProvision}.
     */
    abstract void initialize(InjectorImpl injector, Errors errors) throws ErrorsException;

    @Override
    public final T get() {
      Provider<T> local = delegateProvider;
      if (local == null) {
        throw new IllegalStateException(
            "This Provider cannot be used until the Injector has been created.");
      }
      return local.get();
    }

    @Override
    public T get(final InternalContext context, final Dependency<?> dependency, boolean linked)
        throws InternalProvisionException {
      if (provisionCallback == null) {
        return doProvision(context, dependency);
      } else {
        return provisionCallback.provision(
            context,
            new ProvisionCallback<T>() {
              @Override
              public T call() throws InternalProvisionException {
                return doProvision(context, dependency);
              }
            });
      }
    }
    /**
     * Creates an object to be injected.
     *
     * @throws com.google.inject.internal.InternalProvisionException if a value cannot be provided
     * @return instance to be injected
     */
    protected abstract T doProvision(InternalContext context, Dependency<?> dependency)
        throws InternalProvisionException;
  }

  /**
   * An base factory implementation that can be extended to provide a specialized implementation of
   * a {@link ProviderWithExtensionVisitor} and also implements {@link InternalFactory}
   */
  abstract static class CyclicFactory<T> extends Factory<T> {

    CyclicFactory(InitializationTiming initializationTiming) {
      super(initializationTiming);
    }

    @Override
    public final T get(
        final InternalContext context, final Dependency<?> dependency, boolean linked)
        throws InternalProvisionException {
      final ConstructionContext<T> constructionContext = context.getConstructionContext(this);
      // We have a circular reference between bindings. Return a proxy.
      if (constructionContext.isConstructing()) {
        Class<?> expectedType = dependency.getKey().getTypeLiteral().getRawType();
        @SuppressWarnings("unchecked")
        T proxyType =
            (T) constructionContext.createProxy(context.getInjectorOptions(), expectedType);
        return proxyType;
      }
      // Optimization: Don't go through the callback stack if no one's listening.
      constructionContext.startConstruction();
      try {
        if (provisionCallback == null) {
          return provision(dependency, context, constructionContext);
        } else {
          return provisionCallback.provision(
              context,
              new ProvisionCallback<T>() {
                @Override
                public T call() throws InternalProvisionException {
                  return provision(dependency, context, constructionContext);
                }
              });
        }
      } finally {
        constructionContext.removeCurrentReference();
        constructionContext.finishConstruction();
      }
    }

    private T provision(
        Dependency<?> dependency,
        InternalContext context,
        ConstructionContext<T> constructionContext)
        throws InternalProvisionException {
      try {
        T t = doProvision(context, dependency);
        constructionContext.setProxyDelegates(t);
        return t;
      } catch (InternalProvisionException ipe) {
        throw ipe.addSource(getSource());
      } catch (Throwable t) {
        throw InternalProvisionException.errorInProvider(t).addSource(getSource());
      }
    }
  }
}
