package org.jboss.seam.flex;

import java.io.*;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Collection;

import javax.servlet.*;
import javax.servlet.http.*;

import flex.messaging.*;
import flex.messaging.config.*;
import flex.messaging.endpoints.Endpoint;
import flex.messaging.log.ServletLogTarget;
import flex.messaging.services.RemotingService;
import flex.messaging.services.remoting.*;

import org.jboss.seam.annotations.Name;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.util.Reflections;
import org.jboss.seam.util.Resources;


public class MessageBrokerManager
{
   private static final String SEAM_ENDPOINT = "seam-amf";

   private static final LogProvider log = Logging.getLogProvider(MessageBrokerManager.class);

   private static String WAR_CONFIG_PREFIX = "/WEB-INF/flex/";
   private static String EAR_CONFIG_PREFIX = "/META-INF/flex/seam-default-";

   private flex.messaging.MessageBroker broker;
   
   private ServletConfig servletConfig;
   
   
   public void init(ServletConfig servletConfig)
      throws ServletException
   {
      this.servletConfig = servletConfig;
      
      // allocate thread local variables
      createThreadLocals();
      
      try
      {
         FlexContext.setThreadLocalObjects(null, null, null, null, null, servletConfig);         
         ServletLogTarget.setServletContext(servletConfig.getServletContext());
                 
         FlexConfigurationManager configManager = new SeamFlexConfigurationManager();
         MessagingConfiguration config = configManager.getMessagingConfiguration(servletConfig);
         
         config.createLogAndTargets();         
         broker = config.createBroker(servletConfig.getInitParameter("messageBrokerId"), 
                                      Thread.currentThread().getContextClassLoader());
         
         // Set the servlet config as thread local
         FlexContext.setThreadLocalObjects(null, null, broker, null, null, servletConfig);
         
         setInitServletContext(broker, servletConfig.getServletContext());
         
         // Create endpoints, services, security, and logger on the broker based on configuration
         config.configureBroker(broker);                 
         
         
         if (broker.getChannelIds()== null || !broker.getChannelIds().contains(SEAM_ENDPOINT)) {
            log.info("seam-amf endpoint not found. creating...");

            broker.createEndpoint(SEAM_ENDPOINT, 
                  "http://{server.name}:{server.port}/{context.root}/messagebroker/seam-amf", 
                  "flex.messaging.endpoints.AMFEndpoint");
         }
         
         
         //initialize the httpSessionToFlexSessionMap
         synchronized(HttpFlexSession.mapLock)
         {
            if (servletConfig.getServletContext().getAttribute(HttpFlexSession.SESSION_MAP) == null) {
               servletConfig.getServletContext().setAttribute(HttpFlexSession.SESSION_MAP, new ConcurrentHashMap());
            }
         }
         
         broker.start();
         
         configManager.reportTokens();
         config.reportUnusedProperties();
         
         
         
         // clear the broker and servlet config as this thread is done
         FlexContext.clearThreadLocalObjects();
      
      } catch (Throwable t){
         log.error("MessageBrokerServlet failed to initialize due to runtime exception");
         destroy();
         throw new ServletException(t);
      }
   }
   
   
   private void setInitServletContext(flex.messaging.MessageBroker broker, ServletContext ctx)    
      throws Exception
   {
      Method setMethod = flex.messaging.MessageBroker.class.
      getDeclaredMethod("setInitServletContext", ServletContext.class);
      setMethod.setAccessible(true);
      Reflections.invoke(setMethod, broker, ctx);
   }
   
   
   
   public void destroy()
   {
      if (broker != null) {
         broker.stop();
         // release static thread locals
         destroyThreadLocals();
      }
   }
   
   public void service(HttpServletRequest req, HttpServletResponse res)
   {
      log.info("=========== START FLEX REQUEST");
      try {
         broker.initThreadLocals();
         
         FlexContext.setThreadLocalObjects(null, null, broker, req, res, servletConfig);
         
         // necessary to create for later
         HttpFlexSession fs = HttpFlexSession.getFlexSession(req);
         log.info("flex session is " + fs);
         
         Endpoint endpoint = findEndpoint(req, res);            
         log.info("Endpoint: " + endpoint.describeEndpoint());
         
         endpoint.service(req, res);         
      } catch (UnsupportedOperationException ue) {     
         ue.printStackTrace();
         sendError(res);
      } catch (RuntimeException e) {
         e.printStackTrace();
      } finally {
         FlexContext.clearThreadLocalObjects();         
      }
      
      log.info("=========== END FLEX REQUEST");
   }
   
   
   private void sendError(HttpServletResponse res)
   {
      if (!res.isCommitted()) {
         try {                        
            res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
         } catch (IOException ignored) {
            
         }
      }
   }
   
   
   private Endpoint findEndpoint(HttpServletRequest req, HttpServletResponse res)
   {
      String contextPath = req.getContextPath();
      String pathInfo = req.getPathInfo();
      String endpointPath = req.getServletPath();
      if (pathInfo != null) {
         endpointPath = endpointPath + pathInfo;
      }
      
      log.info("flex request for cp=" + contextPath + " ep=" + endpointPath);
      try {
         return broker.getEndpoint(endpointPath, contextPath);
      } catch (MessageException me) {
         if (!res.isCommitted()) {
            try {                    
               res.sendError(HttpServletResponse.SC_NOT_FOUND);
            } catch (IOException ignore) {
               // ignore
            }
         }                
         return null;
      }
   }
   
   
   // Call ONLY on servlet startup
   public void createThreadLocals()
   {
      // allocate static thread local objects
      flex.messaging.MessageBroker.createThreadLocalObjects();
      FlexContext.createThreadLocalObjects();
      flex.messaging.io.SerializationContext.createThreadLocalObjects();
      flex.messaging.io.TypeMarshallingContext.createThreadLocalObjects();
   }
   
   protected void destroyThreadLocals()
   {
      // Destroy static thread local objects
      flex.messaging.MessageBroker.releaseThreadLocalObjects();
      FlexContext.releaseThreadLocalObjects();
      flex.messaging.io.SerializationContext.releaseThreadLocalObjects();
      flex.messaging.io.TypeMarshallingContext.releaseThreadLocalObjects();
   }
   
   
   private RemotingService createRemotingService() {
            RemotingService remotingService = null;
           
      remotingService = new RemotingService();
      remotingService.setId("remoting-service");

      broker.addService(remotingService);
      log.info("Flex remotingservice not found- creating " + remotingService);
      return remotingService;
   }
   
   
   private RemotingService findRemotingService() {
      return (RemotingService) broker.getServiceByType(RemotingService.class.getName());
   }
   
   private void registerSeamAdapter(RemotingService remotingService) {
      if (remotingService.getRegisteredAdapters().get(SeamAdapter.SEAM_ADAPTER_ID) == null) {
         remotingService.registerAdapter(SeamAdapter.SEAM_ADAPTER_ID,SeamAdapter.class.getName());
      }
   }
   
   private Destination createDestination(String destinationName, String componentName) {           
      RemotingService remotingService = findRemotingService();
      if (remotingService==null) {
         remotingService = createRemotingService();
      }
      
      RemotingDestination destination = 
         (RemotingDestination) remotingService.createDestination(destinationName);
      
      destination.setFactory(new FlexSeamFactory(destinationName, componentName));
            
      // configure adapter       
      registerSeamAdapter(remotingService);
      destination.createAdapter(SeamAdapter.SEAM_ADAPTER_ID);

      destination.addChannel(SEAM_ENDPOINT); 
      
      return destination;
   }
   
   public void addDestinations(Collection<Class<?>> destinations) {                            
      for (Class<?> annotatedClass: destinations) {
         log.info("Adding scanned flex desitionation for class " + annotatedClass);
         FlexRemote fr = annotatedClass.getAnnotation(FlexRemote.class);
         
         Name name = annotatedClass.getAnnotation(Name.class);
         
         String destinationName = fr.name();
         String componentName = name.value();
         Destination destination = createDestination(destinationName, componentName);
         
         destination.start();   
      }
   }
   
}
