package org.infinispan.factories;

import static org.infinispan.commons.util.ReflectionUtil.invokeAccessibly;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

import org.infinispan.IllegalLifecycleStateException;
import org.infinispan.commons.CacheConfigurationException;
import org.infinispan.commons.CacheException;
import org.infinispan.commons.api.Lifecycle;
import org.infinispan.commons.util.ReflectionUtil;
import org.infinispan.commons.util.Util;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.factories.annotations.DefaultFactoryFor;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.factories.annotations.SurvivesRestarts;
import org.infinispan.factories.components.ComponentMetadata;
import org.infinispan.factories.components.ComponentMetadataRepo;
import org.infinispan.factories.scopes.Scope;
import org.infinispan.factories.scopes.Scopes;
import org.infinispan.lifecycle.ComponentStatus;
import org.infinispan.util.TimeService;
import org.infinispan.util.logging.Log;

/**
 * A registry where components which have been created are stored.  Components are stored as singletons, registered
 * under a specific name.
 * <p/>
 * Components can be retrieved from the registry using {@link #getComponent(Class)}.
 * <p/>
 * Components can be registered using {@link #registerComponent(Object, Class)}, which will cause any dependencies to be
 * wired in as well.  Components that need to be created as a result of wiring will be done using {@link
 * #getOrCreateComponent(Class)}, which will look up the default factory for the component type (factories annotated
 * with the appropriate {@link DefaultFactoryFor} annotation.
 * <p/>
 * Default factories are treated as components too and will need to be wired before being used.
 * <p/>
 * The registry can exist in one of several states, as defined by the {@link org.infinispan.lifecycle.ComponentStatus}
 * enumeration. In terms of the cache, state changes in the following manner: <ul> <li>INSTANTIATED - when first
 * constructed</li> <li>CONSTRUCTED - when created using the DefaultCacheFactory</li> <li>STARTED - when {@link
 * org.infinispan.Cache#start()} is called</li> <li>STOPPED - when {@link org.infinispan.Cache#stop()} is called</li>
 * </ul>
 * <p/>
 * Cache configuration can only be changed and will only be re-injected if the cache is not in the {@link
 * org.infinispan.lifecycle.ComponentStatus#RUNNING} state.
 *
 * Thread Safety: instances of {@link GlobalComponentRegistry} can be concurrently updated so all
 * the write operations are serialized through class intrinsic lock.
 *
 * @author Manik Surtani
 * @author Galder Zamarreño
 * @since 4.0
 */
@SurvivesRestarts
@Scope(Scopes.NAMED_CACHE)
public abstract class AbstractComponentRegistry implements Lifecycle, Cloneable {
   private final boolean trace = getLog().isTraceEnabled();

   private static final String DEPENDENCIES_ENABLE_JVMOPTION = "infinispan.debugDependencies";

   /**
    * Set the system property <li>infinispan.debugDependencies</li> to <li>true</li> to enable some extra information to
    * errors generated by the component factory.
    */
   public static final boolean DEBUG_DEPENDENCIES = Boolean.getBoolean(DEPENDENCIES_ENABLE_JVMOPTION);
   private Stack<String> debugStack = DEBUG_DEPENDENCIES ? new Stack<>() : null;

   /**
    * Contains class definitions of component factories that can be used to construct certain components
    */
//   private Map<String, Class<? extends AbstractComponentFactory>> defaultFactories = null;

   private static final Object NULL_COMPONENT = new Object();

   // component and method containers
   private final ConcurrentMap<String, Component> componentLookup = new ConcurrentHashMap<>(1);

   protected volatile ComponentStatus state = ComponentStatus.INSTANTIATED;

   private static final PrioritizedMethod[] EMPTY_PRIO_METHODS = {};

   /**
    * Retrieves the state of the registry
    *
    * @return state of the registry
    */
   public ComponentStatus getStatus() {
      return state;
   }

   protected abstract ClassLoader getClassLoader();

   protected abstract Log getLog();

   public abstract ComponentMetadataRepo getComponentMetadataRepo();

   /**
    * Wires an object instance with dependencies annotated with the {@link Inject} annotation, creating more components
    * as needed based on the Configuration passed in if these additional components don't exist in the {@link
    * ComponentRegistry}.  Strictly for components that don't otherwise live in the registry and have a lifecycle, such
    * as Commands.
    *
    * @param target object to wire
    * @throws CacheConfigurationException if there is a problem wiring the instance
    */
   public void wireDependencies(Object target) throws CacheConfigurationException {
      try {
         Class<?> targetClass = target.getClass();
         ComponentMetadata metadata = getComponentMetadataRepo().findComponentMetadata(targetClass);
         if (metadata != null && metadata.getInjectMethods() != null && metadata.getInjectMethods().length != 0) {
            // search for anything we need to inject
            for (ComponentMetadata.InjectMetadata injectMetadata : metadata.getInjectMethods()) {
               Class<?>[] methodParameters = injectMetadata.getParameterClasses();
               if (methodParameters == null) {
                  if (System.getSecurityManager() == null) {
                     methodParameters = ReflectionUtil.toClassArray(injectMetadata.getParameters(), getClassLoader());
                  } else {
                     methodParameters = AccessController.doPrivileged((PrivilegedExceptionAction<Class<?>[]>) () -> ReflectionUtil.toClassArray(injectMetadata.getParameters(), getClassLoader()));
                  }
                  injectMetadata.setParameterClasses(methodParameters);
               }

               Method method = injectMetadata.getMethod();
               if (method == null) {
                  if (System.getSecurityManager() == null) {
                     method = ReflectionUtil.findMethod(targetClass, injectMetadata.getMethodName(), injectMetadata.getParameterClasses());
                  } else {
                     method = AccessController.doPrivileged((PrivilegedExceptionAction<Method>) () -> ReflectionUtil.findMethod(targetClass, injectMetadata.getMethodName(), injectMetadata.getParameterClasses()));
                  }
                  injectMetadata.setMethod(method);
               }
               invokeInjectionMethod(target, injectMetadata);
            }
         }
      } catch (Exception e) {
         throw new CacheConfigurationException("Unable to configure component (type: " + target.getClass() + ", instance " + target + ")", e);
      }
   }

   /**
    * Registers a component in the registry under the given type, and injects any dependencies needed.  If a component
    * of this type already exists, it is overwritten.
    *
    * @param component component to register
    * @param type      type of component
    */
   public synchronized final void registerComponent(Object component, Class<?> type) {
      registerComponent(component, type.getName(), type.equals(component.getClass()));
   }

   public synchronized final void registerComponent(Object component, String name) {
      registerComponent(component, name, name.equals(component.getClass().getName()));
   }

   public synchronized final void registerComponent(Object component, String name, boolean nameIsFQCN) {
      registerComponentInternal(component, name, nameIsFQCN);
   }

   protected synchronized final void registerNonVolatileComponent(Object component, String name) {
      registerComponentInternal(component, name, false);
   }

   protected synchronized final void registerNonVolatileComponent(Object component, Class<?> type) {
      registerComponentInternal(component, type.getName(), true);
   }

   protected synchronized void registerComponentInternal(Object component, String name, boolean nameIsFQCN) {
      if (state.isStopping() || state.isTerminated()) {
         throw new IllegalLifecycleStateException("Trying to register a component after stopping: " + name);
      }
      if (component == null)
         throw new NullPointerException("Cannot register a null component under name [" + name + "]");
      Component old = componentLookup.get(name);

      if (old != null) {
         // if they are equal don't bother
         if (old.instance.equals(component)) {
            getLog().tracef("Attempting to register a component equal to one that already exists under the same name (%s).  Not doing anything.", name);
            return;
         }
      }

      Component c;
      if (old != null) {
         getLog().tracef("Replacing old component %s with new instance %s", old, component);
         old.instance = component;
         old.methodsScanned = false;
         c = old;
      } else {
         c = new Component();
         c.name = name;
         c.instance = component;
         componentLookup.put(name, c);
      }

      c.metadata = getComponentMetadataRepo().findComponentMetadata(component.getClass());
      try {
         c.buildInjectionMethodsList();
      } catch (ClassNotFoundException cnfe) {
         throw new CacheException("Error injecting dependencies for component " + name, cnfe);
      }
      // inject dependencies for this component
      // we inject dependencies only after the component is already in the map to support cyclical dependencies
      c.injectDependencies();

      if (old == null) getLog().tracef("Registering component %s under name %s", c, name);
      if (state == ComponentStatus.RUNNING || state == ComponentStatus.INITIALIZING) {
         populateLifeCycleMethods(c);
         try {
            invokePrioritizedMethods(Arrays.asList(c.startMethods));
         } catch (Throwable t) {
            // the component hasn't started properly, remove its registration
            componentLookup.remove(name);
            // the caller will log the exception
            handleLifecycleTransitionFailure(t);
         }
      }
   }

   @SuppressWarnings("unchecked")
   private void invokeInjectionMethod(Object o, ComponentMetadata.InjectMetadata injectMetadata) {
      Class<?>[] dependencies = injectMetadata.getParameterClasses();
      if (dependencies.length > 0) {
         Object[] params = new Object[dependencies.length];
         if (trace)
            getLog().tracef("Injecting dependencies for method [%s] on an instance of [%s].", injectMetadata,
                  o.getClass().getName());
         for (int i = 0; i < dependencies.length; i++) {
            String name = injectMetadata.getParameterName(i);
            boolean nameIsFQCN = !injectMetadata.isParameterNameSet(i);
            params[i] = getOrCreateComponent(dependencies[i], name, nameIsFQCN);
         }
         if (System.getSecurityManager() == null) {
            invokeAccessibly(o, injectMetadata.getMethod(), params);
         } else {
            AccessController.doPrivileged((PrivilegedAction<Object>) () -> invokeAccessibly(o, injectMetadata.getMethod(), params));
         }
      }
   }

   /**
    * Retrieves a component if one exists, and if not, attempts to find a factory capable of constructing the component
    * (factories annotated with the {@link DefaultFactoryFor} annotation that is capable of creating the component
    * class).
    * <p/>
    * If an instance needs to be constructed, dependencies are then automatically wired into the instance, based on
    * methods on the component type annotated with {@link Inject}.
    * <p/>
    * Summing it up, component retrieval happens in the following order:<br /> 1.  Look for a component that has already
    * been created and registered. 2.  Look for an appropriate component that exists in the {@link Configuration} that
    * may be injected from an external system. 3.  Look for a class definition passed in to the {@link Configuration} -
    * such as an EvictionPolicy implementation 4.  Attempt to create it by looking for an appropriate factory (annotated
    * with {@link DefaultFactoryFor})
    * <p/>
    *
    * @param componentClass type of component to be retrieved.  Should not be null.
    * @return a fully wired component instance, or null if one cannot be found or constructed.
    * @throws CacheConfigurationException if there is a problem with constructing or wiring the instance.
    */
   protected synchronized <T> T getOrCreateComponent(Class<T> componentClass) {
      return getOrCreateComponent(componentClass, componentClass.getName(), true);
   }

   protected <T> T getOrCreateComponent(Class<T> componentClass, String name) {
      return getOrCreateComponent(componentClass, name, false);
   }

   @SuppressWarnings("unchecked")
   protected synchronized <T> T getOrCreateComponent(Class<T> componentClass, String name, boolean nameIsFQCN) {
      if (DEBUG_DEPENDENCIES) debugStack.push(name);

      Object component;
      Component oldWrapper = lookupComponent(componentClass.getName(), name, nameIsFQCN);
      if (oldWrapper != null) {
         component = unwrapComponent(oldWrapper);
      } else {
         // create this component and add it to the registry
         AbstractComponentFactory factory = getFactory(componentClass);
         getLog().tracef("Creating component %s with factory %s", name, factory.getClass());
         component = factory instanceof NamedComponentFactory ?
               ((NamedComponentFactory) factory).construct(componentClass, name)
               : factory.construct(componentClass);

         if (component != null) {
            registerComponent(component, name, nameIsFQCN);
         } else {
            getLog().tracef("Registering a null for component %s", name);
            registerNullComponent(name);
         }
      }

      if (DEBUG_DEPENDENCIES) debugStack.pop();
      return (T) component;
   }

   /**
    * Retrieves a component factory instance capable of constructing components of a specified type.  If the factory
    * doesn't exist in the registry, one is created.
    *
    * @param componentClass type of component to construct
    * @return component factory capable of constructing such components
    */
   protected synchronized AbstractComponentFactory getFactory(Class<?> componentClass) {
      getLog().tracef("Looking up factory for class %s", componentClass);
      String cfClass = getComponentMetadataRepo().findFactoryForComponent(componentClass);
      if (cfClass == null) {
         throwStackAwareConfigurationException("No registered default factory for component '" + componentClass + "' found!");
      }
      // a component factory is a component too!  See if one has been created and exists in the registry
      AbstractComponentFactory cf = getComponent(cfClass);
      if (cf == null) {
         cf = createComponentFactoryInternal(componentClass, cfClass);
      }

      // ensure the component factory is in the STARTED state!
      Component c = lookupComponent(cfClass, cfClass, true);
      if (c.instance != cf)
         throwStackAwareConfigurationException("Component factory " + cfClass + " incorrectly registered!");
      return cf;
   }

   protected synchronized AbstractComponentFactory createComponentFactoryInternal(Class<?> componentClass, String cfClass) {
      //first check as it might have been created in between by another thread
      AbstractComponentFactory component = getComponent(cfClass);
      if (component != null) return component;

      //hasn't yet been created.  Create and put in registry
      getLog().tracef("Creating factory %s for component %s", cfClass, componentClass);
      AbstractComponentFactory cf = instantiateFactory(cfClass);
      if (cf == null)
         throwStackAwareConfigurationException("Unable to locate component factory for component " + componentClass);
      // we simply register this factory.  Registration will take care of constructing any dependencies.
      registerComponent(cf, cfClass);
      return cf;
   }

   protected Component lookupComponent(String componentClassName, String componentName, boolean nameIsFQCN) {
      return componentLookup.get(componentName);
   }

   /**
    * No such thing as a meta factory yet.  Factories are created using this method which attempts to use an empty
    * public constructor.
    *
    * @param factoryName classname of factory to be created
    * @return factory instance
    */
   AbstractComponentFactory instantiateFactory(String factoryName) {
      Class<?> factory = Util.loadClass(factoryName, getClass().getClassLoader());
      if (AutoInstantiableFactory.class.isAssignableFrom(factory)) {
         try {
            return (AbstractComponentFactory) factory.newInstance();
         } catch (Exception e) {
            // unable to get a hold of an instance!!
            throw new CacheConfigurationException("Unable to instantiate factory " + factory + "  Debug stack: " + debugStack, e);
         }
      } else {
         throw new CacheConfigurationException("Cannot auto-instantiate factory " + factory + " as it doesn't implement " + AutoInstantiableFactory.class.getSimpleName() + "!  Debug stack: " + debugStack);
      }
   }

   /**
    * registers a special "null" component that has no dependencies.
    *
    * @param name name of component to register as a null
    */
   protected synchronized final void registerNullComponent(String name) {
      registerComponent(NULL_COMPONENT, name, false);
   }

   /**
    * Retrieves the configuration component.
    *
    * @return a Configuration object
    */
   protected Configuration getConfiguration() {
      // this is assumed to always be present as a part of the bootstrap/construction of a ComponentRegistry.
      return getComponent(Configuration.class);
   }

   /**
    * Retrieves a component of a specified type from the registry, or null if it cannot be found.
    *
    * @param type type to find
    * @return component, or null
    */
   @SuppressWarnings("unchecked")
   public <T> T getComponent(Class<T> type) {
      String className = type.getName();
      return (T) getComponent(className, className, true);
   }

   @SuppressWarnings("unchecked")
   public <T> T getComponent(String componentClassName) {
      return (T) getComponent(componentClassName, componentClassName, true);
   }

   @SuppressWarnings("unchecked")
   public <T> T getComponent(String componentClassName, String name) {
      return (T) getComponent(componentClassName, name, false);
   }

   @SuppressWarnings("unchecked")
   public <T> T getComponent(Class<T> componentClass, String name) {
      return (T) getComponent(componentClass.getName(), name, false);
   }

   @SuppressWarnings("unchecked")
   public <T> T getComponent(String componentClassName, String name, boolean nameIsFQCN) {
      Component wrapper = lookupComponent(componentClassName, name, nameIsFQCN);
      if (wrapper == null) return null;

      return (T) unwrapComponent(wrapper);
   }

   public <T> Optional<T> getOptionalComponent(Class<T> type) {
      return Optional.ofNullable(getComponent(type));
   }

   /**
    * Get the component from a wrapper, properly handling <code>null</code> components.
    */
   private Object unwrapComponent(Component wrapper) {
      return wrapper.instance == NULL_COMPONENT ? null : wrapper.instance;
   }

   /**
    * Registers the default class loader.  This method *must* be called before any other components are registered,
    * typically called by bootstrap code.  Defensively, it is called in the constructor of ComponentRegistry with a null
    * parameter.
    *
    * @param loader a class loader to use by default.  If this is null, the class loader used to load this instance of
    *               ComponentRegistry is used.
    */
   protected ClassLoader registerDefaultClassLoader(ClassLoader loader) {
      ClassLoader loaderToUse = loader == null ? getClass().getClassLoader() : loader;
      registerComponent(loaderToUse, ClassLoader.class);
      // make sure the class loader is non-volatile, so it survives restarts.
//      componentLookup.get(ClassLoader.class.getName()).survivesRestarts = true;
      return loaderToUse;
   }

   /**
    * Rewires components.  Used to rewire components in the CR if a cache has been stopped (moved to state TERMINATED),
    * which would (almost) empty the registry of components.  Rewiring will re-inject all dependencies so that the cache
    * can be started again.
    * <p/>
    */
   public void rewire() {
      // need to re-inject everything again.
      for (Component c : new HashSet<>(componentLookup.values())) {
         // inject dependencies for this component
         c.injectDependencies();
      }
   }

   /**
    * Scans each registered component for lifecycle methods, and adds them to the appropriate lists, and then sorts them
    * by priority.
    */
   private void populateLifecycleMethods() {
      for (Component c : componentLookup.values()) populateLifeCycleMethods(c);
   }

   private PrioritizedMethod[] processPrioritizedMethods(ComponentMetadata.PrioritizedMethodMetadata[] methodMetadata,
                                                         Class<?> componentClass, Component c) {
      PrioritizedMethod[] retval;
      int numStartMethods = methodMetadata.length;
      if (numStartMethods == 0) {
         retval = EMPTY_PRIO_METHODS;
      } else {
         retval = new PrioritizedMethod[numStartMethods];
         for (int i = 0; i < numStartMethods; i++) {
            retval[i] = new PrioritizedMethod();
            retval[i].component = c;
            retval[i].metadata = methodMetadata[i];

            if (methodMetadata[i].getMethod() == null) {
               Method method = ReflectionUtil.findMethod(componentClass, methodMetadata[i].getMethodName());
               methodMetadata[i].setMethod(method);
            }
         }
         if (retval.length > 1) Arrays.sort(retval);
      }
      return retval;
   }


   private void populateLifeCycleMethods(Component c) {
      if (!c.methodsScanned) {
         c.methodsScanned = true;
         Class<?> componentClass = c.instance.getClass();

         // START methods first
         c.startMethods = processPrioritizedMethods(c.metadata.getStartMethods(), componentClass, c);

         // POSTSTART methods next
         c.postStartMethods = processPrioritizedMethods(c.metadata.getPostStartMethods(), componentClass, c);

         // And now the STOP methods
         c.stopMethods = processPrioritizedMethods(c.metadata.getStopMethods(), componentClass, c);
      }
   }

   /**
    * Removes any components not annotated as @SurvivesRestarts.
    */
   public synchronized void resetVolatileComponents() {
      // destroy all components to clean up resources
      getLog().tracef("Resetting volatile components");
      for (Component c : new HashSet<>(componentLookup.values())) {
         // the component is volatile!!
         if (!c.metadata.isSurvivesRestarts()) {
            getLog().tracef("Removing volatile component %s", c.name);
            componentLookup.remove(c.name);
         }
      }

      if (trace)
         getLog().tracef("Reset volatile components. Registry now contains %s", componentLookup.keySet());
   }

   // ------------------------------ START: Publicly available lifecycle methods -----------------------------
   //   These methods perform a check for appropriate transition and then delegate to similarly named internal methods.

   /**
    * This starts the components in the registry, connecting to channels, starting service threads, etc.  If the component is
    * not in the {@link org.infinispan.lifecycle.ComponentStatus#INITIALIZING} state, it will be initialized first.
    */
   @Override
   public void start() {
      state = ComponentStatus.INITIALIZING;
      try {
         internalStart();
      } catch (Throwable t) {
         handleLifecycleTransitionFailure(t);
      }
   }

   protected void postStart() {
      List<PrioritizedMethod> methods = new ArrayList<>(componentLookup.size());
      for (Component c : componentLookup.values()) {
         Collections.addAll(methods, c.postStartMethods);
      }

      // fire all POSTSTART methods according to priority
      invokePrioritizedMethods(methods);
   }

   /**
    * Stops the component and sets its status to {@link org.infinispan.lifecycle.ComponentStatus#TERMINATED} once it
    * is done.  If the component is not in the {@link org.infinispan.lifecycle.ComponentStatus#RUNNING} state, this is a
    * no-op.
    */
   @Override
   public synchronized void stop() {
      if (!state.stopAllowed()) {
         getLog().debugf("Ignoring call to stop() as current state is %s", state);
         return;
      }

      // Trying to stop() from FAILED is valid, but may not work
      boolean failed = state == ComponentStatus.FAILED;

      try {
         internalStop();
      } catch (Throwable t) {
         if (failed) {
            getLog().failedToCallStopAfterFailure(t);
         }
         failed = true;
         handleLifecycleTransitionFailure(t);
      } finally {
         if (!failed) state = ComponentStatus.TERMINATED;
      }
   }

   // ------------------------------ END: Publicly available lifecycle methods -----------------------------

   // ------------------------------ START: Actual internal lifecycle methods --------------------------------

   /**
    * Sets the cacheStatus to FAILED and re-throws the problem as one of the declared types. Converts any
    * non-RuntimeException Exception to CacheException.
    *
    * @param t throwable thrown during failure
    */
   private void handleLifecycleTransitionFailure(Throwable t) {
      state = ComponentStatus.FAILED;
      if (t.getCause() != null && t.getCause() instanceof CacheConfigurationException)
         throw (CacheConfigurationException) t.getCause();
      else if (t.getCause() != null && t.getCause() instanceof InvocationTargetException && t.getCause().getCause() != null && t.getCause().getCause() instanceof CacheConfigurationException)
         throw (CacheConfigurationException) t.getCause().getCause();
      else if (t instanceof CacheException)
         throw (CacheException) t;
      else if (t instanceof RuntimeException)
         throw (RuntimeException) t;
      else if (t instanceof Error)
         throw (Error) t;
      else
         throw new CacheException(t);
   }

   private void internalStart() throws CacheException, IllegalArgumentException {
      // start all internal components
      // first cache all start, stop and destroy methods.
      populateLifecycleMethods();

      List<PrioritizedMethod> methods = new ArrayList<>(componentLookup.size());
      for (Component c : componentLookup.values()) {
         Collections.addAll(methods, c.startMethods);
      }

      // fire all START methods according to priority
      invokePrioritizedMethods(methods);
      addShutdownHook();

      state = ComponentStatus.RUNNING;
   }

   private void invokePrioritizedMethods(List<PrioritizedMethod> methods) {
      boolean traceEnabled = getLog().isTraceEnabled();
      // sort the methods by priority
      Collections.sort(methods);
      for (PrioritizedMethod em : methods) {
         if (traceEnabled)
            getLog().tracef("Invoking method %s on component %s", em.metadata, em.component.getName());
         em.invoke();
      }
   }

   protected void addShutdownHook() {
      // no op.  Override if needed.
   }

   protected void removeShutdownHook() {
      // no op.  Override if needed.
   }

   /**
    * Actual stop
    */
   private void internalStop() {
      state = ComponentStatus.STOPPING;
      removeShutdownHook();

      List<PrioritizedMethod> stopMethods = new ArrayList<>(componentLookup.size());
      for (Component c : componentLookup.values()) {
         // if one of the components threw an exception during startup
         // the stop methods list may not have been initialized
         if (c.stopMethods != null) {
            Collections.addAll(stopMethods, c.stopMethods);
         }
      }

      Collections.sort(stopMethods);

      // fire all STOP methods according to priority
      boolean traceEnabled = getLog().isTraceEnabled();
      for (PrioritizedMethod em : stopMethods) {
         if (traceEnabled)
            getLog().tracef("Invoking stop method %s on component %s", em.metadata, em.component.getName());
         try {
            em.invoke();
         } catch (Throwable t) {
            getLog().componentFailedToStop(t);
         }
      }

      resetVolatileComponents();
   }

   // ------------------------------ END: Actual internal lifecycle methods --------------------------------

   /**
    * Asserts whether invocations are allowed on the cache or not.  Returns <tt>true</tt> if invocations are to be
    * allowed, <tt>false</tt> otherwise.  If the origin of the call is remote and the cache status is {@link
    * org.infinispan.lifecycle.ComponentStatus#INITIALIZING}, this method will block for up to {@link
    * org.infinispan.configuration.cache.StateTransferConfiguration#timeout()} millis, checking for a valid state.
    *
    * @param originLocal true if the call originates locally (i.e., from the {@link org.infinispan.cache.impl.CacheImpl} or false
    *                    if it originates remotely, i.e., from the {@link org.infinispan.remoting.inboundhandler.InboundInvocationHandler}.
    * @return true if invocations are allowed, false otherwise.
    */
   public boolean invocationsAllowed(boolean originLocal) {
      getLog().trace("Testing if invocations are allowed.");
      if (state.allowInvocations()) return true;

      // if this is a locally originating call and the cache is not in a valid state, return false.
      if (originLocal) return false;

      getLog().trace("Is remotely originating.");

      // else if this is a remote call and the status is STARTING, wait until the cache starts.
      if (state == ComponentStatus.INITIALIZING) {
         getLog().trace("Cache is initializing; block.");
         try {
            blockUntilCacheStarts();
            return true;
         } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
         }
      } else {
         getLog().cacheNotStarted();
      }
      return false;
   }

   /**
    * Blocks until the current cache instance is in its {@link org.infinispan.lifecycle.ComponentStatus#RUNNING started}
    * phase. Blocks for up to {@link org.infinispan.configuration.cache.StateTransferConfiguration#timeout()} milliseconds, throwing an
    * IllegalStateException if the cache doesn't reach this state even after this maximum wait time.
    *
    * @throws InterruptedException  if interrupted while waiting
    * @throws IllegalStateException if even after waiting the cache has not started.
    */
   private void blockUntilCacheStarts() throws InterruptedException, IllegalStateException {
      int pollFrequencyMS = 20;
      TimeService timeService = getTimeService();
      final long startupWaitTime = getConfiguration().clustering().stateTransfer().timeout();
      final long giveUpTime = timeService.expectedEndTime(startupWaitTime, TimeUnit.MILLISECONDS);

      while (!timeService.isTimeExpired(giveUpTime)) {
         if (state.allowInvocations()) break;
         Thread.sleep(pollFrequencyMS);
      }

      // check if we have started.
      if (!state.allowInvocations())
         throw new IllegalStateException("Cache not in STARTED state, even after waiting " + startupWaitTime + " millis.");
   }

   /**
    * Returns an immutable set containing all the components that exists in the repository at this moment.
    *
    * @return a set of components
    */
   public Set<Component> getRegisteredComponents() {
      HashSet<Component> defensiveCopy = new HashSet<>(componentLookup.values());
      return Collections.unmodifiableSet(defensiveCopy);
   }

   @Override
   public AbstractComponentRegistry clone() throws CloneNotSupportedException {
      AbstractComponentRegistry dolly = (AbstractComponentRegistry) super.clone();
      dolly.state = ComponentStatus.INSTANTIATED;
      return dolly;
   }

   public abstract TimeService getTimeService();

   /**
    * A wrapper representing a component in the registry
    */
   public class Component {

      /**
       * A reference to the object instance for this component.
       */
      Object instance;
      /**
       * The name of the component
       */
      String name;
      boolean methodsScanned;
      /**
       * List of injection methods used to inject dependencies into the component
       */
      ComponentMetadata.InjectMetadata[] injectionMethods;
      PrioritizedMethod[] startMethods;
      PrioritizedMethod[] postStartMethods;
      PrioritizedMethod[] stopMethods;
      ComponentMetadata metadata;

      @Override
      public String toString() {
         return "Component{" +
               "instance=" + instance +
               ", name=" + name +
               '}';
      }

      /**
       * Injects dependencies into this component.
       */
      public void injectDependencies() {
         if (injectionMethods != null && injectionMethods.length > 0) {
            for (ComponentMetadata.InjectMetadata injectMetadata : injectionMethods) invokeInjectionMethod(instance, injectMetadata);
         }
      }

      public Object getInstance() {
         return instance;
      }

      public String getName() {
         return name;
      }

      public ComponentMetadata getMetadata() {
         return metadata;
      }

      public void buildInjectionMethodsList() throws ClassNotFoundException {
         injectionMethods = metadata.getInjectMethods();
         if (injectionMethods != null && injectionMethods.length > 0) {
            Class<?> clazz = instance.getClass();
            for (ComponentMetadata.InjectMetadata meta: injectionMethods) {
               Class<?>[] parameterClasses = meta.getParameterClasses();
               if (parameterClasses == null) {
                  parameterClasses = ReflectionUtil.toClassArray(meta.getParameters(), getClassLoader());
                  meta.setParameterClasses(parameterClasses);
               }
               Method m = meta.getMethod();
               if (m == null) {
                  try {
                     m = ReflectionUtil.findMethod(clazz, meta.getMethodName(), parameterClasses);
                  } catch (CacheException e) {
                     throw new CacheException("Injection method not found in class " + clazz + ": " + meta.getMethodName() + Arrays.toString(parameterClasses), e);
                  }
                  meta.setMethod(m);
               }
            }
         }
      }
   }

   /**
    * Wrapper to encapsulate a method along with a priority
    */
   static class PrioritizedMethod implements Comparable<PrioritizedMethod> {
      ComponentMetadata.PrioritizedMethodMetadata metadata;
      Component component;

      @Override
      public int compareTo(PrioritizedMethod o) {
         int thisVal = metadata.getPriority();
         int anotherVal = o.metadata.getPriority();
         return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1));
      }


      @Override
      public boolean equals(Object o) {
         if (this == o) return true;
         if (!(o instanceof PrioritizedMethod)) return false;

         PrioritizedMethod that = (PrioritizedMethod) o;

         if (component != null ? !component.equals(that.component) : that.component != null) return false;
         return metadata != null ? metadata.equals(that.metadata) : that.metadata == null;

      }

      @Override
      public int hashCode() {
         int result = metadata != null ? metadata.hashCode() : 0;
         result = 31 * result + (component != null ? component.hashCode() : 0);
         return result;
      }

      void invoke() {
         invokeAccessibly(component.instance, metadata.getMethod(), null);
      }

      @Override
      public String toString() {
         return "PrioritizedMethod{" +
               "method=" + metadata +
               ", priority=" + metadata.getPriority() +
               '}';
      }

   }

   protected void throwStackAwareConfigurationException(String message) {
      if (debugStack == null) {
         throw new CacheConfigurationException(message + ". To get more detail set the system property " + DEPENDENCIES_ENABLE_JVMOPTION + " to true");
      } else {
         throw new CacheConfigurationException(message + " Debug stack: " + debugStack);
      }
   }

}
