package org.jboss.resteasy.integration.deployers;

import org.jboss.deployers.spi.DeploymentException;
import org.jboss.deployers.spi.deployer.DeploymentStages;
import org.jboss.deployers.spi.deployer.helpers.AbstractRealDeployer;
import org.jboss.deployers.structure.spi.DeploymentUnit;
import org.jboss.deployers.vfs.spi.structure.VFSDeploymentUnit;
import org.jboss.logging.Logger;
import org.jboss.metadata.javaee.spec.ParamValueMetaData;
import org.jboss.metadata.web.jboss.JBossWebMetaData;
import org.jboss.metadata.web.spec.FilterMetaData;
import org.jboss.metadata.web.spec.ServletMetaData;
import org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher;
import org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrapClasses;
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import org.jboss.scanning.annotations.spi.AnnotationIndex;
import org.jboss.scanning.annotations.spi.AnnotationRepository;
import org.jboss.scanning.annotations.spi.Element;
import org.jboss.scanning.hierarchy.spi.HierarchyIndex;

import javax.ws.rs.Path;
import javax.ws.rs.core.Application;
import javax.ws.rs.ext.Provider;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Use cases:
 *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class ResteasyScannerDeployer extends AbstractRealDeployer
{
   private static final Logger LOGGER = Logger.getLogger(ResteasyScannerDeployer.class);
   public static final Set<String> BOOT_CLASSES = new HashSet<String>();

   static
   {
      for (String clazz : ResteasyBootstrapClasses.BOOTSTRAP_CLASSES)
      {
         BOOT_CLASSES.add(clazz);
      }
   }

   public ResteasyScannerDeployer()
   {
      super();

      addRequiredInput(JBossWebMetaData.class);
      addInput(AnnotationRepository.class);
      addOutput(JBossWebMetaData.class);
      addOutput(ResteasyDeploymentScanningData.class);
      setStage(DeploymentStages.PRE_REAL); // TODO -- right stage?
   }

   protected void internalDeploy(DeploymentUnit du) throws DeploymentException
   {
      JBossWebMetaData webdata = du.getAttachment(JBossWebMetaData.class);
      boolean applicationClassDeployed = deployApplicationClass(du, webdata);

      // If an Application class is present, we assume the user wants to totally control deployment
      if (applicationClassDeployed) return;

      scan(du, webdata);

   }

   /**
    * If any servlet/filter classes are declared, then we probably don't want to scan.
    *
    * @param du
    * @param webdata
    * @return
    * @throws DeploymentException
    */
   protected boolean hasBootClasses(DeploymentUnit du, JBossWebMetaData webdata) throws DeploymentException
   {
      ClassLoader loader = du.getClassLoader();
      if (webdata.getServlets() != null)
      {
         for (ServletMetaData servlet : webdata.getServlets())
         {
            String servletClass = servlet.getServletClass();
            if (BOOT_CLASSES.contains(servletClass)) return true;
         }
      }
      if (webdata.getFilters() != null)
      {
         for (FilterMetaData filter : webdata.getFilters())
         {
            if (BOOT_CLASSES.contains(filter.getFilterClass())) return true;
         }
      }
      return false;

   }

   protected boolean deployApplicationClass(DeploymentUnit du, JBossWebMetaData webdata) throws DeploymentException
   {
      ClassLoader loader = du.getClassLoader();
      if (webdata.getServlets() == null) return false;

      for (ServletMetaData servlet : webdata.getServlets())
      {
         String servletClass = servlet.getServletClass();
         if (servletClass == null) continue;
         Class clazz = null;
         try
         {
            clazz = loader.loadClass(servletClass);
         }
         catch (ClassNotFoundException e)
         {
            throw new DeploymentException(e);
         }
         if (Application.class.isAssignableFrom(clazz))
         {
            servlet.setServletClass(HttpServlet30Dispatcher.class.getName());
            servlet.setAsyncSupported(true);
            ParamValueMetaData param = new ParamValueMetaData();
            param.setParamName("javax.ws.rs.Application");
            param.setParamValue(servletClass);
            List<ParamValueMetaData> params = servlet.getInitParam();
            if (params == null)
            {
               params = new ArrayList<ParamValueMetaData>();
               servlet.setInitParam(params);
            }
            params.add(param);
            ResteasyIntegrationDeployer.setContextParameter(webdata, ResteasyContextParameters.RESTEASY_UNWRAPPED_EXCEPTIONS, "javax.ejb.EJBException");
            try
            {
               Thread.currentThread().getContextClassLoader().loadClass(ResteasyIntegrationDeployer.CDI_INJECTOR_FACTORY_CLASS);
               // don't set this param if it is not in classpath
               if (((VFSDeploymentUnit) du).getMetaDataFile("beans.xml") != null)
               {
                  ResteasyIntegrationDeployer.setContextParameter(webdata, "resteasy.injector.factory", ResteasyIntegrationDeployer.CDI_INJECTOR_FACTORY_CLASS);
               }
            }
            catch (ClassNotFoundException ignored)
            {
            }
            return true;
         }
      }
      return false;

   }

   protected void addApplicationClass(Class<? extends Application> applicationClass, DeploymentUnit du, JBossWebMetaData webdata)
   {

   }

   protected void scan(DeploymentUnit du, JBossWebMetaData webdata)
           throws DeploymentException
   {
      ResteasyDeploymentScanningData scanningData = new ResteasyDeploymentScanningData();

      // If there is a resteasy boot class in web.xml, then the default should be to not scan
      boolean hasBoot = hasBootClasses(du, webdata);
      scanningData.setBootClasses(hasBoot);

      if (hasBoot == false && !webdata.isMetadataComplete())
      {
         scanningData.setScanAll(true);
         scanningData.setScanProviders(true);
         scanningData.setScanResources(true);
      }

      // check resteasy configuration flags


      List<ParamValueMetaData> contextParams = webdata.getContextParams();
      boolean hasResourceParam = false;
      boolean hasProviderParam = false;
      boolean hasJndiParam = false;

      if (contextParams != null)
      {
         for (ParamValueMetaData param : contextParams)
         {
            if (param.getParamName().equals(ResteasyContextParameters.RESTEASY_SCAN))
            {
               scanningData.setScanAll(Boolean.valueOf(param.getParamValue()));
            }
            else if (param.getParamName().equals(ResteasyContextParameters.RESTEASY_SCAN_PROVIDERS))
            {
               scanningData.setScanProviders(Boolean.valueOf(param.getParamValue()));
            }
            else if (param.getParamName().equals(ResteasyContextParameters.RESTEASY_SCAN_RESOURCES))
            {
               scanningData.setScanResources(Boolean.valueOf(param.getParamValue()));
            }
            else if (param.getParamName().equals(ResteasyContextParameters.RESTEASY_RESOURCES))
            {
               hasResourceParam = true;
            }
            else if (param.getParamName().equals(ResteasyContextParameters.RESTEASY_PROVIDERS))
            {
               hasProviderParam = true;
            }
            else if (param.getParamName().equals(ResteasyContextParameters.RESTEASY_JNDI_RESOURCES))
            {
               hasJndiParam = true;
            }
         }
      }

      if (hasResourceParam)
      {
         scanningData.setScanAll(false);
         scanningData.setScanResources(false);
      }

      if (hasProviderParam)
      {
         scanningData.setScanAll(false);
         scanningData.setScanProviders(false);
      }

      if (!scanningData.shouldScan())
      {
         if (hasResourceParam || hasProviderParam || hasJndiParam)
         {
            if (hasBoot == false)
            {
               scanningData.createDispatcher();
               du.addAttachment(ResteasyDeploymentScanningData.class, scanningData);
            }
         }
         return;
      }
      else
      {
      }

      du.addAttachment(ResteasyDeploymentScanningData.class, scanningData);

      // look for Application class, return if there is one

      HierarchyIndex hier = du.getAttachment(HierarchyIndex.class);
      if (hier != null)
      {
         Set<Class<? extends Application>> applicationClass = hier.getSubClassesOf(null, Application.class);
         if (applicationClass != null)
         {
            if (applicationClass.size() > 1)
            {
               StringBuilder builder = new StringBuilder("Only one JAX-RS Application Class allowed.");
               for (Class c : applicationClass)
               {
                  builder.append(" ").append(c.getName());
               }
               throw new DeploymentException(builder.toString());
            }
            else if (applicationClass.size() == 1)
            {
               Class<? extends Application> aClass = applicationClass.iterator().next();
               scanningData.setApplicationClass(aClass);

               // don't scan for anything else
               return;
            }
         }
      }
      else
      {
         LOGGER.debug("Expecting HierarchyIndex class for scanning WAR for JAX-RS Application class");
      }

      // Looked for annotated resources and providers
      AnnotationIndex env = du.getAttachment(AnnotationIndex.class);
      if (env == null)
      {
         LOGGER.debug("Expecting AnnotationRepository class for scanning WAR for JAX-RS classes");
         return;
      }


      Set<Element<Path, Class<?>>> resources = null;
      Set<Element<Provider, Class<?>>> providers = null;
      if (scanningData.isScanResources())
      {
         resources = env.classIsAnnotatedWith(Path.class);
      }
      if (scanningData.isScanProviders())
      {
         providers = env.classIsAnnotatedWith(Provider.class);
      }

      if ((resources == null || resources.isEmpty()) && (providers == null || providers.isEmpty())) return;

      if (resources != null)
      {
         for (Element e : resources)
         {
            if (e.getOwner().isInterface())
            {
               continue;
            }
            scanningData.getResources().add(e.getOwnerClassName());
         }
      }
      if (providers != null)
      {
         for (Element e : providers)
         {
            if (e.getOwner().isInterface()) continue;
            scanningData.getProviders().add(e.getOwnerClassName());
         }
      }

      Set<String> strings = env.classesImplementingInterfacesAnnotatedWith(Path.class.getName());
      scanningData.getResources().addAll(strings);
   }

}
