/*
 * Copyright (C) 2013 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.container.jmx;

import org.exoplatform.commons.utils.ClassLoading;
import org.exoplatform.commons.utils.PropertyManager;
import org.exoplatform.container.ComponentAdapterDependenciesAware;
import org.exoplatform.container.ComponentTask;
import org.exoplatform.container.ComponentTaskContext;
import org.exoplatform.container.ComponentTaskType;
import org.exoplatform.container.ConcurrentContainer.CreationalContextComponentAdapter;
import org.exoplatform.container.ConcurrentContainerMT;
import org.exoplatform.container.CyclicDependencyException;
import org.exoplatform.container.Dependency;
import org.exoplatform.container.DependencyStackListener;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.LockManager;
import org.exoplatform.container.component.ComponentLifecycle;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.container.configuration.ConfigurationManager;
import org.exoplatform.container.util.ContainerUtil;
import org.exoplatform.container.xml.Component;
import org.exoplatform.container.xml.ExternalComponentPlugins;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.picocontainer.Startable;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicReference;

import javax.enterprise.context.spi.Context;
import javax.enterprise.context.spi.CreationalContext;
import javax.inject.Singleton;

/**
 * @author <a href="mailto:nfilotto@exoplatform.com">Nicolas Filotto</a>
 * @version $Id$
 *
 */
public class MX4JComponentAdapterMT<T> extends MX4JComponentAdapter<T> implements DependencyStackListener,
   ComponentAdapterDependenciesAware<T>
{

   /**
    * Serial Version ID
    */
   private static final long serialVersionUID = -9001193588034229411L;

   private transient final AtomicReference<Collection<Dependency>> createDependencies =
      new AtomicReference<Collection<Dependency>>();

   private transient final AtomicReference<Collection<Dependency>> initDependencies =
      new AtomicReference<Collection<Dependency>>();

   /**
    * The task to use to create the component
    */
   private transient final AtomicReference<ComponentTask<T>> createTask = new AtomicReference<ComponentTask<T>>();

   /**
    * The task to use to init the component
    */
   private transient final AtomicReference<Collection<ComponentTask<Void>>> initTasks =
      new AtomicReference<Collection<ComponentTask<Void>>>();

   /** . */
   private transient final ConcurrentContainerMT container;

   private static final Log LOG = ExoLogger.getLogger("exo.kernel.container.mt.MX4JComponentAdapterMT");

   public MX4JComponentAdapterMT(ExoContainer holder, ConcurrentContainerMT container, Object key,
      Class<T> implementation)
   {
      super(holder, container, key, implementation, LockManager.getInstance().createLock());
      this.container = container;
   }

   private void addComponentPlugin(List<ComponentTask<Void>> tasks, Set<Dependency> dependencies, boolean debug,
      List<org.exoplatform.container.xml.ComponentPlugin> plugins) throws Exception
   {
      if (plugins == null)
         return;
      for (org.exoplatform.container.xml.ComponentPlugin plugin : plugins)
      {
         try
         {
            Class<?> pluginClass = ClassLoading.forName(plugin.getType(), this);
            List<Dependency> lDependencies = new ArrayList<Dependency>();
            @SuppressWarnings("unchecked")
            Constructor<T> constructor = (Constructor<T>)container.getConstructor(pluginClass, lDependencies);
            dependencies.addAll(lDependencies);
            tasks.add(createPlugin(this, container, pluginClass, debug, plugin, constructor, plugin.getInitParams(),
               lDependencies));
         }
         catch (CyclicDependencyException e)
         {
            throw e;
         }
         catch (Exception ex)
         {
            LOG.error("Failed to instanciate plugin " + plugin.getName() + " for component "
               + getComponentImplementation() + ": " + ex.getMessage(), ex);
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public Collection<Dependency> getCreateDependencies()
   {
      return createDependencies.get();
   }

   /**
    * {@inheritDoc}
    */
   public Collection<Dependency> getInitDependencies()
   {
      return initDependencies.get();
   }

   /**
    * {@inheritDoc}
    */
   public void callDependency(ComponentTask<?> task, Dependency dep)
   {
      if (PropertyManager.isDevelopping())
      {
         if (dep.getKey() instanceof String
            || (dep.getKey() instanceof Class && ((Class<?>)dep.getKey()).isAnnotation()))
         {
            LOG.warn("An unexpected call of getComponentInstance(" + dep.getKey() + "," + dep.getBindType().getName()
               + ") has been detected please add the component in your constructor instead", new Exception(
               "This is the stack trace allowing you to identify where the unexpected "
                  + "call of getComponentInstanceOfType has been done"));
         }
         else if (dep.getKey() instanceof Class)
         {
            LOG.warn("An unexpected call of getComponentInstanceOfType(" + ((Class<?>)dep.getKey()).getName()
               + ") has been detected please add the component in your constructor instead", new Exception(
               "This is the stack trace allowing you to identify where the unexpected "
                  + "call of getComponentInstanceOfType has been done"));
         }
      }
      if (dep.getKey().equals(getComponentKey()))
      {
         return;
      }
      if (task.getType() == ComponentTaskType.CREATE)
      {
         getCreateDependencies().add(dep);
      }
      else if (task.getType() == ComponentTaskType.INIT)
      {
         getInitDependencies().add(dep);
      }
      container.getComponentTaskContext().checkDependency(dep.getKey(), task.getType());
   }

   /**
    * {@inheritDoc}
    */
   @SuppressWarnings("unchecked")
   protected ComponentTask<T> getCreateTask()
   {
      Component component = null;
      String componentKey;
      InitParams params = null;
      boolean debug = false;

      // Get the component
      Object key = getComponentKey();
      if (key instanceof String)
         componentKey = (String)key;
      else
         componentKey = ((Class<?>)key).getName();
      try
      {
         ConfigurationManager manager =
            (ConfigurationManager)exocontainer.getComponentInstanceOfType(ConfigurationManager.class);
         component = manager == null ? null : manager.getComponent(componentKey);
         if (component != null)
         {
            params = component.getInitParams();
            debug = component.getShowDeployInfo();
         }
         if (debug)
            LOG.debug("==> get constructor of the component : " + getComponentImplementation());
         List<Dependency> lDependencies = new ArrayList<Dependency>();
         Constructor<?> constructor = container.getConstructor(getComponentImplementation(), lDependencies);
         setCreateDependencies(lDependencies);
         if (debug)
            LOG.debug("==> create component : " + getComponentImplementation());
         return (ComponentTask<T>)container.createComponentTask(constructor, params, lDependencies, this);
      }
      catch (Exception e)
      {
         String msg = "Cannot instantiate component " + getComponentImplementation();
         if (component != null)
         {
            msg =
               "Cannot instantiate component key=" + component.getKey() + " type=" + component.getType() + " found at "
                  + component.getDocumentURL();
         }
         throw new RuntimeException(msg, e);
      }
   }

   protected void setCreateDependencies(List<Dependency> lDependencies)
   {
      if (createDependencies.get() == null)
      {
         createDependencies.compareAndSet(null, new CopyOnWriteArraySet<Dependency>(lDependencies));
      }
   }

   /**
    * {@inheritDoc}
    */
   protected Collection<ComponentTask<Void>> getInitTasks()
   {
      Component component = null;
      String componentKey;
      boolean debug = false;

      // Get the component
      Object key = getComponentKey();
      if (key instanceof String)
         componentKey = (String)key;
      else
         componentKey = ((Class<?>)key).getName();
      try
      {
         ConfigurationManager manager =
            (ConfigurationManager)exocontainer.getComponentInstanceOfType(ConfigurationManager.class);
         component = manager == null ? null : manager.getComponent(componentKey);
         if (component != null)
         {
            debug = component.getShowDeployInfo();
         }
         List<ComponentTask<Void>> tasks = new ArrayList<ComponentTask<Void>>();
         Set<Dependency> dependencies = new HashSet<Dependency>();

         final Class<T> implementationClass = getComponentImplementation();
         boolean isSingleton = this.isSingleton;
         boolean isInitialized = this.isInitialized;
         if (debug)
            LOG.debug("==> create  component : " + implementationClass.getName());
         boolean hasInjectableConstructor = !isSingleton || ContainerUtil.hasInjectableConstructor(implementationClass);
         boolean hasOnlyEmptyPublicConstructor =
            !isSingleton || ContainerUtil.hasOnlyEmptyPublicConstructor(implementationClass);
         if (hasInjectableConstructor || hasOnlyEmptyPublicConstructor)
         {
            // There is at least one constructor JSR 330 compliant or we already know 
            // that it is not a singleton such that the new behavior is expected
            List<Dependency> lDependencies = new ArrayList<Dependency>();
            boolean isInjectPresent = container.initializeComponent(implementationClass, lDependencies, tasks, this);
            dependencies.addAll(lDependencies);
            isSingleton = manageScope(isSingleton, isInitialized, hasInjectableConstructor, isInjectPresent);
         }
         else if (!isInitialized)
         {
            // The adapter has not been initialized yet
            // The old behavior is expected as there is no constructor JSR 330 compliant 
            isSingleton = this.isSingleton = true;
            scope.set(Singleton.class);
         }
         if (component != null && component.getComponentPlugins() != null)
         {
            addComponentPlugin(tasks, dependencies, debug, component.getComponentPlugins());
         }
         ExternalComponentPlugins ecplugins =
            manager == null ? null : manager.getConfiguration().getExternalComponentPlugins(componentKey);
         if (ecplugins != null)
         {
            addComponentPlugin(tasks, dependencies, debug, ecplugins.getComponentPlugins());
         }
         initDependencies.compareAndSet(null, new CopyOnWriteArraySet<Dependency>(dependencies));
         tasks.add(new ComponentTask<Void>("initialize component " + getComponentImplementation().getName(), container,
            this, ComponentTaskType.INIT)
         {
            public Void execute(CreationalContextComponentAdapter<?> cCtx) throws Exception
            {
               // check if component implement the ComponentLifecycle
               if (cCtx.get() instanceof ComponentLifecycle && exocontainer instanceof ExoContainer)
               {
                  ComponentLifecycle lc = (ComponentLifecycle)cCtx.get();
                  lc.initComponent((ExoContainer)exocontainer);
               }
               return null;
            }
         });
         if (!isInitialized)
         {
            this.isInitialized = true;
         }
         return tasks;
      }
      catch (Exception e)
      {
         String msg = "Cannot initialize component " + getComponentImplementation();
         if (component != null)
         {
            msg =
               "Cannot initialize component key=" + component.getKey() + " type=" + component.getType() + " found at "
                  + component.getDocumentURL();
         }
         throw new RuntimeException(msg, e);
      }
   }

   private ComponentTask<Void> createPlugin(final MX4JComponentAdapterMT<T> caller,
      final ConcurrentContainerMT exocontainer, final Class<?> pluginClass, final boolean debug,
      final org.exoplatform.container.xml.ComponentPlugin plugin, final Constructor<T> constructor, InitParams params,
      List<Dependency> lDependencies) throws Exception
   {
      final Object[] args = exocontainer.getArguments(constructor, params, lDependencies);
      return new ComponentTask<Void>("create/add plugin " + plugin.getName() + " for component "
         + getComponentImplementation().getName(), exocontainer, caller, ComponentTaskType.INIT)
      {
         public Void execute(final CreationalContextComponentAdapter<?> cCtx) throws Exception
         {
            try
            {
               getContainer().loadArguments(args);
               ComponentPlugin cplugin = (ComponentPlugin)constructor.newInstance(args);
               cplugin.setName(plugin.getName());
               cplugin.setDescription(plugin.getDescription());
               Class<?> clazz = getComponentImplementation();

               final Method m = getSetMethod(clazz, plugin.getSetMethod(), pluginClass);
               if (m == null)
               {
                  LOG.error("Cannot find the method '" + plugin.getSetMethod()
                     + "' that has only one parameter of type '" + pluginClass.getName() + "' in the class '"
                     + clazz.getName() + "'.");
                  return null;
               }
               final Object[] params = {cplugin};

               m.invoke(cCtx.get(), params);

               if (debug)
                  LOG.debug("==> add component plugin: " + cplugin);

               cplugin.setName(plugin.getName());
               cplugin.setDescription(plugin.getDescription());
               return null;
            }
            catch (InvocationTargetException e)
            {
               if (e.getCause() instanceof Exception)
               {
                  throw (Exception)e.getCause();
               }
               throw e;
            }
         }
      };
   }

   protected T createInstance(final Context ctx)
   {
      T result = ctx.get(this);
      if (result != null)
      {
         return result;
      }
      return create(new Callable<T>()
      {
         public T call() throws Exception
         {
            try
            {
               return ctx.get(MX4JComponentAdapterMT.this, container.<T> addComponentToCtx(getComponentKey()));
            }
            finally
            {
               container.removeComponentFromCtx(getComponentKey());
            }
         }
      });
   }

   /**
    * Must be used to create Singleton or Prototype only
    */
   protected T create()
   {
      return create(new Callable<T>()
      {
         public T call() throws Exception
         {
            return doCreate();
         }
      });
   }

   /**
    * Must be used to create Singleton or Prototype only
    */
   protected T doCreate()
   {
      return doCreate(false);
   }

   /**
    * Must be used to create Singleton or Prototype only
    */
   protected T doCreate(boolean useSharedMemory)
   {
      if (instance_ != null)
      {
         return instance_;
      }
      boolean toBeLocked = isSingleton;
      boolean skipFinally = false;
      try
      {
         CreationalContextComponentAdapter<T> ctx;
         if (toBeLocked)
         {
            if (useSharedMemory)
            {
               T result = container.<T> getComponentFromSharedMemory(getComponentKey());
               if (result != null)
               {
                  LOG.debug("The value could be found from the shared memory");
                  skipFinally = true;
                  return result;
               }
               LOG.debug("The value could not be found from the shared memory");
            }
            if (!lock.tryLock())
            {
               // The lock has already been acquired, let's make sure that we
               // don't have any deadlocks
               lock.lockInterruptibly();
            }
            ctx = container.<T> addComponentToCtx(getComponentKey());
         }
         else
         {
            // Don't add to context non singleton
            skipFinally = true;
            ctx = new CreationalContextComponentAdapter<T>();
         }
         return create(ctx);
      }
      catch (InterruptedException e)
      {
         // We make sure that the state of the Thread is back to normal
         Thread.interrupted();
         skipFinally = true;
         LOG.debug("A deadlock has been detected, let's retry using the shared memory");
         return doCreate(true);
      }
      finally
      {
         if (!skipFinally)
         {
            lock.unlock();
            container.removeComponentFromCtx(getComponentKey());
         }
      }
   }

   private T create(Callable<T> mainCreateTask)
   {
      ComponentTaskContext ctx = container.getComponentTaskContext();
      try
      {
         loadTasks();
         loadDependencies(ctx);
         return mainCreateTask.call();
      }
      catch (CyclicDependencyException e)
      {
         throw e;
      }
      catch (Exception e)
      {
         throw new RuntimeException("Cannot create component " + getComponentImplementation(), e);
      }
      finally
      {
         if (ctx == null)
         {
            container.setComponentTaskContext(null);
         }
      }
   }

   private void loadDependencies(ComponentTaskContext ctx) throws Exception
   {
      ComponentTaskContext createCtx = ctx;
      if (createCtx == null)
      {
         createCtx = new ComponentTaskContext(getComponentKey(), ComponentTaskType.CREATE);
         container.setComponentTaskContext(createCtx);
      }
      else if (!createCtx.isLast(getComponentKey()))
      {
         createCtx = createCtx.addToContext(getComponentKey());
         container.setComponentTaskContext(createCtx);
      }
      container.loadDependencies(getComponentKey(), createCtx, getCreateDependencies(), ComponentTaskType.CREATE);
   }

   /**
    * {@inheritDoc}
    */
   public T create(CreationalContext<T> creationalContext)
   {
      CreationalContextComponentAdapter<T> ctx = (CreationalContextComponentAdapter<T>)creationalContext;
      // Avoid to create duplicate instances if it is called at the same time by several threads
      if (instance_ != null)
         return instance_;
      else if (ctx.get() != null)
         return ctx.get();
      ComponentTaskContext taskCtx = container.getComponentTaskContext();
      boolean isRoot = taskCtx.isRoot();
      if (!isRoot)
      {
         container.setComponentTaskContext(taskCtx = taskCtx.setLastTaskType(ComponentTaskType.CREATE));
      }
      try
      {
         ComponentTask<T> task = createTask.get();
         T result = task.call(ctx);
         if (instance_ != null)
         {
            // Avoid instantiating twice the same component in case of a cyclic reference due
            // to component plugins
            return instance_;
         }
         else if (ctx.get() != null)
            return ctx.get();

         ctx.push(result);
      }
      catch (CyclicDependencyException e)
      {
         throw e;
      }
      catch (Exception e)
      {
         throw new RuntimeException("Cannot create component " + getComponentImplementation(), e);
      }
      if (isRoot)
      {
         container.setComponentTaskContext(taskCtx =
            taskCtx.resetDependencies(getComponentKey(), ComponentTaskType.INIT));
      }
      else
      {
         container.setComponentTaskContext(taskCtx = taskCtx.setLastTaskType(ComponentTaskType.INIT));
      }

      Collection<ComponentTask<Void>> tasks = initTasks.get();
      ComponentTask<Void> task = null;
      try
      {
         if (tasks != null && !tasks.isEmpty())
         {
            container.loadDependencies(getComponentKey(), taskCtx, getInitDependencies(), ComponentTaskType.INIT);
            for (Iterator<ComponentTask<Void>> it = tasks.iterator(); it.hasNext();)
            {
               task = it.next();
               task.call(ctx);
               task = null;
            }
         }
         if (instance_ != null)
         {
            return instance_;
         }
         else if (instance_ == null && isSingleton)
         {
            // In case of cyclic dependency the component could be already initialized
            // so we need to recheck the state
            instance_ = ctx.get();
         }
      }
      catch (CyclicDependencyException e)
      {
         throw e;
      }
      catch (Exception e)
      {
         if (task != null)
         {
            throw new RuntimeException("Cannot " + task.getName() + " for the component "
               + getComponentImplementation(), e);
         }
         throw new RuntimeException("Cannot initialize component " + getComponentImplementation(), e);
      }
      if (ctx.get() instanceof Startable && exocontainer.canBeStopped())
      {
         try
         {
            // Start the component if the container is already started
            ((Startable)ctx.get()).start();
         }
         catch (Exception e)
         {
            throw new RuntimeException("Cannot auto-start component " + getComponentImplementation(), e);
         }
      }
      return ctx.get();
   }

   private void loadTasks()
   {

      if (createTask.get() == null)
      {
         try
         {
            createTask.compareAndSet(null, getCreateTask());
         }
         catch (RuntimeException e)
         {
            throw new RuntimeException("Cannot get the create task of the component " + getComponentImplementation(), e);
         }
      }
      if (initTasks.get() == null)
      {
         try
         {
            initTasks.compareAndSet(null, getInitTasks());
         }
         catch (RuntimeException e)
         {
            throw new RuntimeException("Cannot get the init tasks of the component " + getComponentImplementation(), e);
         }
      }
   }
}
