/*
 * JBoss, Home of Professional Open Source
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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.jboss.seam.resteasy;

import static org.jboss.seam.annotations.Install.BUILT_IN;

import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.ws.rs.ext.Providers;

import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.core.SynchronousDispatcher;
import org.jboss.resteasy.core.ThreadLocalResteasyProviderFactory;
import org.jboss.resteasy.plugins.providers.RegisterBuiltin;
import org.jboss.resteasy.plugins.server.resourcefactory.POJOResourceFactory;
import org.jboss.resteasy.spi.Registry;
import org.jboss.resteasy.spi.ResourceFactory;
import org.jboss.resteasy.spi.StringConverter;
import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.Seam;
import org.jboss.seam.annotations.AutoCreate;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Factory;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.JndiName;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Startup;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.core.Init;
import org.jboss.seam.deployment.AnnotationDeploymentHandler;
import org.jboss.seam.deployment.DeploymentStrategy;
import org.jboss.seam.log.Log;
import org.jboss.seam.util.EJB;
import org.jboss.seam.util.Reflections;

/**
 * Detects (through scanning and configuration) JAX-RS resources and providers, then
 * registers them with RESTEasy.
 * <p>
 * This class is a factory for <tt>org.jboss.seam.resteasy.dispatcher</tt> and it has been
 * designed for extension. Alternatively, you can ignore what this class is doing and provide a
 * different <tt>org.jboss.seam.resteasy.dispatcher</tt> yourself without extending this class.
 * </p>
 * <p>
 * The main methods of this class are <tt>registerProviders()</tt> and <tt>registerResources()</tt>.
 * These methods call out to the individual fine-grained registration procedures, which you can override
 * if a different registration strategy for a particular type/component is desired.
 * </p>
 *
 * @author Christian Bauer
 */
@Name("org.jboss.seam.resteasy.bootstrap")
@Scope(ScopeType.APPLICATION)
@Startup
@AutoCreate
@Install(precedence = BUILT_IN, classDependencies = "org.jboss.resteasy.spi.ResteasyProviderFactory")
public class ResteasyBootstrap
{

   @Logger
   Log log;

   @In
   protected Application application;

   // The job of this class is to initialize and configure the RESTEasy Dispatcher instance
   protected Dispatcher dispatcher;

   @Factory("org.jboss.seam.resteasy.dispatcher")
   public Dispatcher getDispatcher()
   {
      return dispatcher;
   }

   @Create
   public void init()
   {
      log.info("bootstrapping JAX-RS application");

      // Custom ResteasyProviderFactory that understands Seam component lookup at runtime
      SeamResteasyProviderFactory providerFactory = createProviderFactory();
      dispatcher = createDispatcher(providerFactory);
      initDispatcher();

      // Always use the "deployment sensitive" factory - that means it is handled through ThreadLocal, not static
      // TODO: How does that actually work? It's never used because the dispatcher is created with the original one
      SeamResteasyProviderFactory.setInstance(new ThreadLocalResteasyProviderFactory(providerFactory));

      // Put Providers, Registry and Dispatcher into RESTEasy context.
      dispatcher.getDefaultContextObjects().put(Providers.class, providerFactory);
      dispatcher.getDefaultContextObjects().put(Registry.class, dispatcher.getRegistry());
      dispatcher.getDefaultContextObjects().put(Dispatcher.class, dispatcher);
      Map contextDataMap = SeamResteasyProviderFactory.getContextDataMap();
      contextDataMap.putAll(dispatcher.getDefaultContextObjects());
      
      // Seam can scan the classes for us, we just have to list them in META-INF/seam-deployment.properties
      DeploymentStrategy deployment = (DeploymentStrategy) Component.getInstance("deploymentStrategy");
      AnnotationDeploymentHandler handler =
            (AnnotationDeploymentHandler) deployment.getDeploymentHandlers().get(AnnotationDeploymentHandler.NAME);

      Collection<Class<?>> providers = findProviders(handler);
      Collection<Class<?>> resources = findResources(handler);
      Collection<Component> seamComponents = findSeamComponents();

      registerProviders(seamComponents, providers);
      registerResources(seamComponents, resources);
   }

   protected SeamResteasyProviderFactory createProviderFactory()
   {
      return new SeamResteasyProviderFactory();
   }

   protected Dispatcher createDispatcher(SeamResteasyProviderFactory providerFactory)
   {
      return new SynchronousDispatcher(providerFactory);
   }

   protected void initDispatcher()
   {
      getDispatcher().setLanguageMappings(application.getLanguageMappings());
      getDispatcher().setMediaTypeMappings(application.getMediaTypeMappings());
   }

   protected Collection<Class<?>> findProviders(AnnotationDeploymentHandler handler)
   {
      return findTypes(
            handler,
            application.isScanProviders(),
            javax.ws.rs.ext.Provider.class.getName(),
            application.getProviderClassNames()
      );
   }

   protected Collection<Class<?>> findResources(AnnotationDeploymentHandler handler)
   {
      return findTypes(
            handler,
            application.isScanResources(),
            javax.ws.rs.Path.class.getName(),
            application.getResourceClassNames()
      );
   }

   protected Collection<Class<?>> findTypes(AnnotationDeploymentHandler handler, boolean scanClasspathForAnnotations,
                                            String annotationFQName, Collection<String> includeTypeNames)
   {

      Collection<Class<?>> types = new HashSet();

      if (scanClasspathForAnnotations)
      {
         Collection<Class<?>> annotatedTypes = handler.getClassMap().get(annotationFQName);
         if (annotatedTypes != null) types.addAll(annotatedTypes);
      }

      try
      {
         for (String s : new HashSet<String>(includeTypeNames))
         {
            types.add(Reflections.classForName(s));
         }
      }
      catch (ClassNotFoundException ex)
      {
         log.error("error loading JAX-RS type: " + ex.getMessage(), ex);
      }

      return types;
   }


   protected Collection<Component> findSeamComponents()
   {
      // Iterate through all variables in the application context that end with ".component"
      log.debug("discovering all Seam components");
      Collection<Component> seamComponents = new HashSet();
      String[] applicationContextNames = Contexts.getApplicationContext().getNames();
      for (String applicationContextName : applicationContextNames)
      {
         if (applicationContextName.endsWith(".component"))
         {
            Component seamComponent =
                  (Component) Component.getInstance(applicationContextName, ScopeType.APPLICATION);
            seamComponents.add(seamComponent);
         }
      }
      return seamComponents;
   }


   protected void registerProviders(Collection<Component> seamComponents, Collection<Class<?>> providerClasses)
   {

      // RESTEasy built-in providers first
      if (application.isUseBuiltinProviders())
      {
         log.info("registering built-in RESTEasy providers");
         RegisterBuiltin.register(getDispatcher().getProviderFactory());
      }

      Set<Class> handledProviders = new HashSet(); // Stuff we don't want to examine twice

      /* TODO: Retracted due to missing RESTEasy SPI for external provider metadata, see https://jira.jboss.org/jira/browse/JBSEAM-4247
      for (Component seamComponent : seamComponents)
      {
         // The component can have one (not many) @Provider annotated business interface
         Class providerInterface = getAnnotatedInterface(javax.ws.rs.ext.Provider.class, seamComponent);

         // How we register it depends on the component type
         switch (seamComponent.getType())
         {

            // TODO: We don't support EJB Seam components as providers

            case JAVA_BEAN:

               // We are only interested in components that have a @Provider annotation on iface or bean
               if (providerInterface == null
                     && !seamComponent.getBeanClass().isAnnotationPresent(javax.ws.rs.ext.Provider.class))
               {
                  break;
               }

               // They also have to be in the right scope, otherwise we can't handle their lifecylce (yet)
               switch (seamComponent.getScope())
               {
                  case APPLICATION:

                     // StringConverter is a special case
                     if (StringConverter.class.isAssignableFrom(seamComponent.getBeanClass()))
                     {
                        getDispatcher().getProviderFactory().addStringConverter(
                              (StringConverter) Component.getInstance(seamComponent.getName())
                        );
                     }
                     else
                     {
                        registerSeamComponentProvider(seamComponent);
                     }
                     break;

                  default:
                     throw new RuntimeException(
                           "Provider Seam component '" + seamComponent.getName() + "' must be scoped " +
                                 "APPLICATION"
                     );
               }
               break;
         }

         // We simply add everything we have seen so far... it's not really necessary but it doesn't hurt (does it?)
         handledProviders.add(seamComponent.getBeanClass());
         handledProviders.addAll(seamComponent.getBusinessInterfaces());
      }
      */

      for (Class<?> providerClass : providerClasses)
      {

         // An @Provider annotated type may:

         // - have been already handled as a Seam component in the previous routine
         if (handledProviders.contains(providerClass)) continue;

         // - be a RESTEasy built-in provider
         if (providerClass.getName().startsWith("org.jboss.resteasy.plugins.providers")) continue;

         // - be an interface, which we don't care about if we don't have an implementation
         if (providerClass.isInterface()) continue;

         // - be just plain RESTEasy, no Seam component lookup or lifecycle
         if (StringConverter.class.isAssignableFrom(providerClass))
         {
            log.debug("registering provider as RESTEasy StringConverter: {0}", providerClass);
            getDispatcher().getProviderFactory().addStringConverter((Class<? extends StringConverter>) providerClass);
         }
         else
         {
            log.debug("registering provider as plain JAX-RS type: {0}", providerClass);
            getDispatcher().getProviderFactory().registerProvider(providerClass);
         }
      }
   }

   protected void registerResources(Collection<Component> seamComponents, Collection<Class<?>> resourceClasses)
   {

      Set<Class> handledResources = new HashSet(); // Stuff we don't want to examine twice
      // These classes themselves should not be registered at all
      // Configured ResourceHome and ResourceQuery components will be registered later
      handledResources.add(ResourceHome.class);
      handledResources.add(ResourceQuery.class);

      for (Component seamComponent : seamComponents)
      {

         // A bean class of type (not subtypes) ResourceHome or ResourceQuery annotated with @Path, then
         // it's a Seam component resource we need to register with getPath() on the instance, it has been
         // configured in components.xml
         if (seamComponent.getBeanClass().equals(ResourceHome.class) ||
               seamComponent.getBeanClass().equals(ResourceQuery.class))
         {
            registerHomeQueryResources(seamComponent);
            continue;
         }

         // The component can have one (not many) @Path annotated business interface
         Class resourceInterface = getAnnotatedInterface(javax.ws.rs.Path.class, seamComponent);

         // How we register it depends on the component type
         switch (seamComponent.getType())
         {
            case STATELESS_SESSION_BEAN:
               // EJB seam component resources must be @Path annotated on one of their business interfaces
               if (resourceInterface != null)
               {
                  // TODO: Do we have to consider the scope? It should be stateless, right?
                  registerInterfaceSeamComponentResource(seamComponent, resourceInterface);
               }
               break;
            case STATEFUL_SESSION_BEAN:
               // EJB seam component resources must be @Path annotated on one of their business interfaces
               if (resourceInterface != null)
               {
                  log.error("Not implemented: Stateful EJB Seam component resource: " + seamComponent);
                  // TODO: registerStatefulEJBSeamComponentResource(seamComponent);
               }
               break;
            case JAVA_BEAN:

               // We are only interested in components that have a @Path annotation on iface or bean
               if (resourceInterface == null
                     && !seamComponent.getBeanClass().isAnnotationPresent(javax.ws.rs.Path.class))
               {
                  break;
               }

               // They also have to be in the right scope, otherwise we can't handle their lifecylce (yet)
               switch (seamComponent.getScope())
               {
                  case EVENT:
                  case APPLICATION:
                  case STATELESS:
                  case SESSION:
                     if (resourceInterface != null)
                     {
                        registerInterfaceSeamComponentResource(seamComponent, resourceInterface);
                     }
                     else if (seamComponent.getBeanClass().isAnnotationPresent(javax.ws.rs.Path.class))
                     {
                        registerSeamComponentResource(seamComponent);
                     }
                     break;
                  default:
                     throw new RuntimeException(
                           "Resource Seam component '" + seamComponent.getName() + "' must be scoped either " +
                                 "EVENT, APPLICATION, STATELESS, or SESSION"
                     );
               }
               break;
         }

         // We simply add everything we have seen so far... it's not really necessary but it doesn't hurt (does it?)
         handledResources.add(seamComponent.getBeanClass());
         handledResources.addAll(seamComponent.getBusinessInterfaces());

      }

      for (Class<?> resourceClass : resourceClasses)
      {
         // An @Path annotated type may:

         // - have been already handled as a Seam component in the previous routine
         if (handledResources.contains(resourceClass)) continue;

         // - be an interface, which we don't care about if we don't have an implementation
         if (resourceClass.isInterface()) continue;

         // - be a @Stateless EJB implementation class that was listed in components.xml
         if (resourceClass.isAnnotationPresent(EJB.STATELESS))
         {
            registerStatelessEJBResource(resourceClass);
         }
         // - be a @Stateful EJB implementation class that was listed in components.xml
         else if (resourceClass.isAnnotationPresent(EJB.STATEFUL))
         {
            throw new RuntimeException(
                  "Only stateless EJBs can be JAX-RS resources, remove from configuration: " + resourceClass.getName()
            );
         }
         else
         {
            // - just be a regular JAX-RS lifecycle instance that can created/destroyed by RESTEasy
            registerPlainResource(resourceClass);
         }
      }

   }

   protected void registerHomeQueryResources(Component seamComponent)
   {
      // We can always instantiate this safely here because it can't have dependencies!
      AbstractResource instance = (AbstractResource) seamComponent.newInstance();
      String path = instance.getPath();
      if (path != null)
      {
         if (!path.startsWith("/")) {
            path = "/" + path;
         }

         log.debug(
               "registering resource, configured ResourceHome/Query on path {1}, as Seam component: {0}",
               seamComponent.getName(),
               path
         );

         ResourceFactory factory = new SeamResteasyResourceFactory(
               seamComponent.getBeanClass(),
               seamComponent,
               getDispatcher().getProviderFactory()
         );

         getDispatcher().getRegistry().addResourceFactory(factory, path);
      }
      else
      {
         log.error("Unable to register {0} resource on null path, check components.xml", seamComponent.getName());
      }
   }

   protected void registerSeamComponentResource(Component seamComponent)
   {
      log.debug("registering resource as Seam component: {0}", seamComponent.getName());

      ResourceFactory factory = new SeamResteasyResourceFactory(
            seamComponent.getBeanClass(),
            seamComponent,
            getDispatcher().getProviderFactory()
      );

      getDispatcher().getRegistry().addResourceFactory(factory);
   }

   protected void registerInterfaceSeamComponentResource(Component seamComponent, Class resourceInterface)
   {
      log.debug(
            "registering resource, annotated interface {1}, as Seam component: {0}",
            seamComponent.getName(),
            resourceInterface.getName()
      );

      ResourceFactory factory = new SeamResteasyResourceFactory(
            resourceInterface,
            seamComponent,
            getDispatcher().getProviderFactory()
      );

      getDispatcher().getRegistry().addResourceFactory(factory);
   }

   protected void registerStatelessEJBResource(Class ejbImplementationClass)
   {
      String jndiName = getJndiName(ejbImplementationClass);

      log.debug(
            "registering resource, stateless EJB implementation {1}, as RESTEasy JNDI resource name: {0}",
            jndiName,
            ejbImplementationClass.getName()
      );
      getDispatcher().getRegistry().addJndiResource(jndiName);
   }

   protected void registerPlainResource(Class plainResourceClass)
   {
      log.debug("registering resource, event-scoped JAX-RS lifecycle: {0}", plainResourceClass.getName());
      getDispatcher().getRegistry().addResourceFactory(new POJOResourceFactory(plainResourceClass));
   }

   protected void registerSeamComponentProvider(Component seamComponent)
   {
      log.debug("registering provider as Seam component: {0}", seamComponent.getName());
      getDispatcher().getProviderFactory().registerProviderInstance(
            Component.getInstance(seamComponent.getName())
      );
   }

   protected Class getAnnotatedInterface(Class<? extends Annotation> annotation, Component seamComponent)
   {
      Class resourceInterface = null;
      for (Class anInterface : seamComponent.getBusinessInterfaces())
      {
         if (anInterface.isAnnotationPresent(annotation))
         {
            if (resourceInterface != null)
            {
               throw new IllegalStateException("Only one business interface can be annotated " + annotation + ": " + seamComponent);
            }
            resourceInterface = anInterface;
         }
      }
      return resourceInterface;
   }

   protected String getJndiName(Class<?> beanClass)
   {
      if (beanClass.isAnnotationPresent(JndiName.class))
      {
         return beanClass.getAnnotation(JndiName.class).value();
      }
      else
      {
         String jndiPattern = Init.instance().getJndiPattern();
         if (jndiPattern == null)
         {
            throw new IllegalArgumentException(
                  "You must specify org.jboss.seam.core.init.jndiPattern or use @JndiName: " + beanClass.getName()
            );
         }
         return jndiPattern.replace("#{ejbName}", Seam.getEjbName(beanClass));
      }
   }


}
