/*
 * 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.guice;

import com.google.inject.AbstractModule;
import com.google.inject.Binder;
import com.google.inject.Binding;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Provider;
import com.google.inject.name.Names;

import org.exoplatform.container.AbstractComponentAdapter;
import org.exoplatform.container.AbstractInterceptor;
import org.exoplatform.container.configuration.ConfigurationManager;
import org.exoplatform.container.spi.ComponentAdapter;
import org.exoplatform.container.spi.ContainerException;
import org.exoplatform.container.spi.Interceptor;
import org.exoplatform.container.xml.Component;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * The implementation of an {@link Interceptor} allowing eXo Kernel to interact with a Google Guice container
 * 
 * @author <a href="mailto:nfilotto@exoplatform.com">Nicolas Filotto</a>
 * @version $Id$
 *
 */
public class GuiceContainer extends AbstractInterceptor
{

   /**
    * The serial version UID
    */
   private static final long serialVersionUID = -6662420267945445921L;

   /**
    * The logger
    */
   private static final Log LOG = ExoLogger.getLogger("exo.kernel.container.ext.provider.impl.guice.v3.GuiceContainer");

   /**
    * The Google Guice injector
    */
   private Injector injector;

   /**
    * {@inheritDoc}
    */
   @SuppressWarnings("unchecked")
   @Override
   public <T> T getComponentInstance(Object componentKey, Class<T> bindType, boolean autoRegistration)
   {
      T result = super.getComponentInstance(componentKey, bindType, autoRegistration);
      if (result == null && injector != null)
      {
         final Binding<?> binding;
         if (componentKey instanceof Class<?> && !((Class<?>)componentKey).isAnnotation())
         {
            binding = injector.getExistingBinding(Key.get((Class<?>)componentKey));
         }
         else
         {
            if (componentKey instanceof String)
            {
               binding = injector.getExistingBinding(Key.get(bindType, Names.named((String)componentKey)));
            }
            else if (componentKey instanceof Class<?>)
            {
               binding = injector.getExistingBinding(Key.get(bindType, (Class<? extends Annotation>)componentKey));
            }
            else
            {
               return null;
            }
         }
         if (binding == null || binding.getProvider().toString().startsWith(ComponentAdapterProvider.class.getName()))
         {
            return null;
         }
         result = bindType.cast(binding.getProvider().get());
      }
      return result;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public <T> T getComponentInstanceOfType(Class<T> componentType, boolean autoRegistration)
   {
      T result = super.getComponentInstanceOfType(componentType, autoRegistration);
      if (result == null && injector != null)
      {
         Binding<?> binding = injector.getExistingBinding(Key.get(componentType));
         if (binding == null || binding.getProvider().toString().startsWith(ComponentAdapterProvider.class.getName()))
         {
            return null;
         }
         result = componentType.cast(binding.getProvider().get());
      }
      return result;
   }

   /**
    * {@inheritDoc}
    */
   @SuppressWarnings("unchecked")
   @Override
   public <T> ComponentAdapter<T> getComponentAdapter(Object componentKey, Class<T> bindType, boolean autoRegistration)
   {
      ComponentAdapter<T> result = super.getComponentAdapter(componentKey, bindType, autoRegistration);
      if (result == null && injector != null)
      {
         final Binding<?> binding;
         if (componentKey instanceof Class<?> && !((Class<?>)componentKey).isAnnotation())
         {
            binding = injector.getExistingBinding(Key.get((Class<?>)componentKey));
         }
         else
         {
            if (componentKey instanceof String)
            {
               binding = injector.getExistingBinding(Key.get(bindType, Names.named((String)componentKey)));
            }
            else if (componentKey instanceof Class<?>)
            {
               binding = injector.getExistingBinding(Key.get(bindType, (Class<? extends Annotation>)componentKey));
            }
            else
            {
               return null;
            }
         }
         if (binding == null || binding.getProvider().toString().startsWith(ComponentAdapterProvider.class.getName()))
         {
            return null;
         }
         result = createComponentAdapter(bindType, binding);
      }
      return result;
   }

   private <T> ComponentAdapter<T> createComponentAdapter(final Class<T> type, final Binding<?> binding)
   {
      return new AbstractComponentAdapter<T>(type, type)
      {
         /**
          * The serial UID
          */
         private static final long serialVersionUID = 4241559622835718141L;

         public T getComponentInstance() throws ContainerException
         {
            return type.cast(binding.getProvider().get());
         }

         public boolean isSingleton()
         {
            return false;
         }
      };
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public <T> ComponentAdapter<T> getComponentAdapterOfType(Class<T> componentType, boolean autoRegistration)
   {
      ComponentAdapter<T> result = super.getComponentAdapterOfType(componentType, autoRegistration);
      if (result == null && injector != null)
      {
         final Binding<?> binding = injector.getExistingBinding(Key.get(componentType));
         if (binding == null || binding.getProvider().toString().startsWith(ComponentAdapterProvider.class.getName()))
         {
            return null;
         }
         result = createComponentAdapter(componentType, binding);
      }
      return result;
   }

   /**
    * {@inheritDoc}
    */
   @SuppressWarnings("unchecked")
   @Override
   public <T> List<ComponentAdapter<T>> getComponentAdaptersOfType(Class<T> componentType)
   {
      List<ComponentAdapter<T>> result = super.getComponentAdaptersOfType(componentType);
      if (injector != null)
      {
         result = new ArrayList<ComponentAdapter<T>>(result);
         for (Binding<?> b : injector.getAllBindings().values())
         {
            if (b.getProvider().toString().startsWith(ComponentAdapterProvider.class.getName()))
            {
               continue;
            }
            else if (componentType.isAssignableFrom(b.getKey().getTypeLiteral().getRawType()))
            {
               result.add((ComponentAdapter<T>)createComponentAdapter(b.getKey().getTypeLiteral().getRawType(), b));
            }
         }
      }
      return result;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public <T> List<T> getComponentInstancesOfType(Class<T> componentType) throws ContainerException
   {
      List<T> result = super.getComponentInstancesOfType(componentType);
      if (injector != null)
      {
         result = new ArrayList<T>(result);
         for (Binding<?> b : injector.getAllBindings().values())
         {
            if (b.getProvider().toString().startsWith(ComponentAdapterProvider.class.getName()))
            {
               continue;
            }
            else if (componentType.isAssignableFrom(b.getKey().getTypeLiteral().getRawType()))
            {
               result.add(componentType.cast(b.getProvider().get()));
            }
         }
      }
      return result;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void start()
   {
      ConfigurationManager cm = super.getComponentInstanceOfType(ConfigurationManager.class, false);
      // We check if the component has been defined in the configuration of the current container
      // The goal is to enable the GuicegContainer only if it is needed
      Component component = cm.getComponent(ModuleProvider.class);
      if (component == null)
      {
         if (LOG.isDebugEnabled())
         {
            LOG.debug("No ModuleProvider has been defined, thus the GuiceContainer will be disabled."
               + " To enable the Guice Integration please define a ModuleProvider");
         }
      }
      else
      {
         ModuleProvider provider = super.getComponentInstanceOfType(ModuleProvider.class, false);
         injector = Guice.createInjector(provider.getModule(), new AbstractModule()
         {
            @SuppressWarnings({"unchecked", "rawtypes"})
            @Override
            protected void configure()
            {
               Collection<ComponentAdapter<?>> adapters = delegate.getComponentAdapters();
               Binder binder = binder();
               for (ComponentAdapter<?> adapter : adapters)
               {
                  Object key = adapter.getComponentKey();
                  Class<?> type;
                  Annotation annotation = null;
                  Class<? extends Annotation> annotationType = null;
                  if (key instanceof Class<?> && !((Class<?>)key).isAnnotation())
                  {
                     type = (Class<?>)key;
                  }
                  else
                  {
                     if (key instanceof String)
                     {
                        annotation = Names.named((String)key);
                     }
                     else if (key instanceof Class<?>)
                     {
                        annotationType = (Class<? extends Annotation>)key;
                     }
                     type = adapter.getComponentImplementation();
                  }
                  if (annotation == null && annotationType == null)
                  {
                     binder.bind(type).toProvider(new ComponentAdapterProvider(type, adapter));
                  }
                  else
                  {
                     // As we don't know the type, we will bind it for each super classes and interfaces too
                     ComponentAdapterProvider provider = new ComponentAdapterProvider(type, adapter);
                     bindAll(binder, type, provider, annotation, annotationType);
                  }
               }
            }
         });
         LOG.info("A GuiceContainer has been enabled using the ModuleProvider " + provider.getClass());
      }
      super.start();
   }

   @SuppressWarnings({"rawtypes", "unchecked"})
   private static void bindAll(Binder binder, Class<?> clazz, ComponentAdapterProvider provider, Annotation annotation,
      Class<? extends Annotation> annotationType)
   {
      if (clazz == null || clazz.equals(Object.class))
         return;
      if (annotation == null)
      {
         binder.bind(clazz).annotatedWith(annotationType).toProvider(provider);
      }
      else
      {
         binder.bind(clazz).annotatedWith(annotation).toProvider(provider);
      }
      for (Class<?> c : clazz.getInterfaces())
      {
         bindAll(binder, c, provider, annotation, annotationType);
      }
      bindAll(binder, clazz.getSuperclass(), provider, annotation, annotationType);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void stop()
   {
      super.stop();
      injector = null;
   }

   /**
    * {@inheritDoc}
    */
   public String getId()
   {
      return "GuiceIntegration";
   }

   private static class ComponentAdapterProvider<T> implements Provider<T>
   {

      private final Class<T> type;

      private final ComponentAdapter<T> adapter;

      private ComponentAdapterProvider(Class<T> type, ComponentAdapter<T> adapter)
      {
         this.type = type;
         this.adapter = adapter;
      }

      /**
       * {@inheritDoc}
       */
      public T get()
      {
         return type.cast(adapter.getComponentInstance());
      }
   }
}
