package org.jboss.seam.deployment;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.ServletContext;

import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;

/**
 * A {@link DeploymentStrategy} coordinates the deployment of resources for a Seam
 * application.
 * 
 * @author Pete Muir
 *
 */
public abstract class DeploymentStrategy
{
   
   private static final LogProvider log = Logging.getLogProvider(DeploymentStrategy.class);

   private Scanner scanner;
   
   private List<File> files = new ArrayList<File>();
   
   private Map<String, DeploymentHandler> deploymentHandlers;
   
   /**
    * The key under which to list possible scanners. System properties take
    * precedence over /META-INF/seam-scanner.properties. Entries will be tried
    * in sequential order until a Scanner can be loaded. 
    * 
    * This can be specified as a System property or in 
    * /META-INF/seam-deployment.properties
    */
   public static final String SCANNERS_KEY = "org.jboss.seam.deployment.scanners";
   
   /**
    * Do the scan for resources
    * 
    * Should only be called by Seam
    * 
    */
   public abstract void scan();
   
   /**
    * Get the scanner being used
    * 
    */
   protected Scanner getScanner()
   {
      if (scanner == null)
      {
         initScanner();
      }
      return scanner;
   }
   
   /**
    * Get the classloader to use
    */
   public abstract ClassLoader getClassLoader();
   
   /**
    * Get the ServletContext to use
    */
   public abstract ServletContext getServletContext();

   /**
    * Get (or modify) any registered {@link DeploymentHandler}s
    * 
    * Implementations of {@link DeploymentStrategy} may add default 
    * {@link DeploymentHandler}s 
    */
   public Map<String, DeploymentHandler> getDeploymentHandlers()
   {
      if (deploymentHandlers == null)
      {
         initDeploymentHandlers();
      }
      return this.deploymentHandlers;
   }
   
   private void initDeploymentHandlers()
   {
      this.deploymentHandlers = new HashMap<String, DeploymentHandler>();
      List<String> deploymentHandlersClassNames = new SeamDeploymentProperties(getClassLoader()).getPropertyValues(getDeploymentHandlersKey());
      addHandlers(deploymentHandlersClassNames);
   }

   protected abstract String getDeploymentHandlersKey();
      
   protected void initScanner()
   {
      List<String> scanners = new SeamDeploymentProperties(getClassLoader()).getPropertyValues(SCANNERS_KEY);
      for ( String className : scanners )
      {
         Scanner scanner = instantiateScanner(className);
         if (scanner != null)
         {
            log.trace("Using " + scanner.toString());
            this.scanner = scanner;
            return;
         }        
      }
      log.trace("Using default URLScanner");
      this.scanner = new URLScanner(this);
   }
   
   private Scanner instantiateScanner(String className)
   {
      try
      {
         Class<Scanner> scannerClass =  (Class<Scanner>) getClassLoader().loadClass(className);
         Constructor<Scanner> constructor = scannerClass.getConstructor(new Class[] {DeploymentStrategy.class});
         return constructor.newInstance( new Object[] {this} );
      }
      catch (ClassNotFoundException e)
      {
         log.trace("Unable to use " + className + " as scanner (class not found)", e);
      }
      catch (LinkageError e) 
      {
         log.trace("Unable to use " + className + " as scanner (dependency not found)", e);
      }
      catch (ClassCastException e) 
      {
         log.trace("Unable to use " + className + " as scanner (class does not implement org.jboss.seam.deployment.Scanner)");
      }
      catch (InstantiationException e)
      {
         log.trace("Unable to instantiate scanner " + className, e);
      }
      catch (IllegalAccessException e)
      {
         log.trace("Unable to instantiate scanner " + className, e);
      }
      catch (SecurityException e)
      {
         log.trace(className + " must declare public " + className + "( ClassLoader classLoader, String ... resourceNames )", e);
      }
      catch (NoSuchMethodException e)
      {
         log.trace(className + " must declare public " + className + "( ClassLoader classLoader, String ... resourceNames )", e);
      }
      catch (IllegalArgumentException e)
      {
         log.trace(className + " must declare public " + className + "( ClassLoader classLoader, String ... resourceNames )", e);
      }
      catch (InvocationTargetException e)
      {
         log.trace(className + " must declare public " + className + "( ClassLoader classLoader, String ... resourceNames )", e);
      }
      return null;
   }
   
   private void addHandlers(List<String> handlers)
   {
      for (String handler : handlers)
      {
         addHandler(handler);
      }
   }
   
   private void addHandler(String className)
   {
      DeploymentHandler deploymentHandler = instantiateDeploymentHandler(className);
      if (deploymentHandler != null)
      {
         log.trace("Adding " + deploymentHandler + " as a deployment handler");
         deploymentHandlers.put(deploymentHandler.getName(), deploymentHandler);
      }
   }
   
   private DeploymentHandler instantiateDeploymentHandler(String className)
   {
      try
      {
         Class<DeploymentHandler> clazz = (Class<DeploymentHandler>) getClassLoader().loadClass(className);
         return clazz.newInstance();
      }
      catch (ClassNotFoundException e)
      {
         log.trace("Unable to use " + className + " as a deployment handler (class not found)", e);
      }
      catch (LinkageError e) 
      {
         log.trace("Unable to use " + className + " as a deployment handler (dependency not found)", e);
      }
      catch (InstantiationException e)
      {
         log.trace("Unable to instantiate deployment handler " + className, e);
      }
      catch (IllegalAccessException e)
      {
         log.trace("Unable to instantiate deployment handler " + className, e);
      }
      return null;
   }

   public List<File> getFiles()
   {
      return files;
   }
   
   public void setFiles(List<File> files)
   {
      this.files = files;
   }
   
   public long getTimestamp()
   {
      return getScanner().getTimestamp();
   }
   
   protected void postScan()
   {
      for (Entry<String, DeploymentHandler> entry : getDeploymentHandlers().entrySet())
      {
         entry.getValue().postProcess(getClassLoader());
      }
   }
   
}
