/*
 * JBoss, Home of Professional Open Source
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.seam.contexts;

import static javax.faces.event.PhaseId.RESTORE_VIEW;

import java.util.Map;

import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.bpm.BusinessProcess;
import org.jboss.seam.core.ConversationPropagation;
import org.jboss.seam.core.Events;
import org.jboss.seam.core.Init;
import org.jboss.seam.core.Manager;
import org.jboss.seam.core.Mutable;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.transaction.Transaction;
import org.jboss.seam.web.Session;

/**
 * Provides access to the current contexts associated with the thread.
 * 
 * @author Gavin King
 * @author <a href="mailto:theute@jboss.org">Thomas Heute</a>
 */
public class Contexts 
{

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

   static final ThreadLocal<Context> applicationContext = new ThreadLocal<Context>();
   static final ThreadLocal<Context> methodContext = new ThreadLocal<Context>();
   static final ThreadLocal<Context> eventContext = new ThreadLocal<Context>();
   static final ThreadLocal<Context> pageContext = new ThreadLocal<Context>();
   static final ThreadLocal<Context> sessionContext = new ThreadLocal<Context>();
   static final ThreadLocal<Context> conversationContext = new ThreadLocal<Context>();
   static final ThreadLocal<Context> businessProcessContext = new ThreadLocal<Context>();

   public static Context getEventContext() 
   {
      return eventContext.get();
   }

   public static Context getMethodContext() 
   {
      return methodContext.get();
   }

   public static Context getPageContext() 
   {
      return pageContext.get();
   }

   public static Context getSessionContext() 
   {
      return sessionContext.get();
   }

   public static Context getApplicationContext() 
   {
      return applicationContext.get();
   }

   public static Context getConversationContext() 
   {
      return conversationContext.get();
   }

   public static Context getBusinessProcessContext() 
   {
      return businessProcessContext.get();
   }

   public static boolean isConversationContextActive() 
   {
      return getConversationContext() != null;
   }

   public static boolean isEventContextActive() 
   {
      return eventContext.get() != null;
   }

   public static boolean isMethodContextActive() 
   {
      return methodContext.get() != null;
   }

   /**
    * Is PageContext set?
    * 
    * If not, it is lazily created in case of RESTORE_VIEW is processing
    * This is a requirement for starting CONVERSATION - JSF2 change
    * @return
    */
   public static boolean isPageContextActive()
   {
      if (pageContext.get() == null)
      {
         log.debug("isPageContextActive is null");
         
         try
         {
            // lazy initialize the page context during restore view
            javax.faces.context.FacesContext facesContext = javax.faces.context.FacesContext.getCurrentInstance();
            if (facesContext != null)
            {
               if (FacesLifecycle.getPhaseId() == RESTORE_VIEW )
               {
                  log.debug("Page Context will be lazilly created");
                  FacesLifecycle.resumePage();
                  Map<String, String> parameters = facesContext.getExternalContext().getRequestParameterMap();
                  ConversationPropagation.instance().restoreConversationId(parameters);
                  boolean conversationFound = Manager.instance().restoreConversation();
                  pageContext.get().set("org.jboss.seam.jsf.SeamPhaseListener.conversationFound", conversationFound);
                  
                  FacesLifecycle.resumeConversation(facesContext.getExternalContext());
               }
            }
         }
         catch (NoClassDefFoundError e)
         {
            // do nothing as this context is supposed to be active only with JSF view layer
         }

      }
      return pageContext.get() != null;
   }

   public static boolean isSessionContextActive() 
   {
      return sessionContext.get() != null;
   }

   public static boolean isApplicationContextActive() 
   {
      return applicationContext.get() != null;
   }

    public static boolean isBusinessProcessContextActive() 
    {
        return businessProcessContext.get() != null;
    }
   
    /**
     * Remove the named component from all contexts.
     */
   public static void removeFromAllContexts(String name)
   {
      log.debug("removing from all contexts: " + name);
      if (isMethodContextActive())
      {
         getMethodContext().remove(name);
      }
      if (isEventContextActive())
      {
         getEventContext().remove(name);
      }
      if (isPageContextActive())
      {
         getPageContext().remove(name);
      }
      if (isConversationContextActive())
      {
         getConversationContext().remove(name);
      }
      if (isSessionContextActive())
      {
         getSessionContext().remove(name);
      }
      if (isBusinessProcessContextActive())
      {
         getBusinessProcessContext().remove(name);
      }
      if (isApplicationContextActive())
      {
         getApplicationContext().remove(name);
      }
   }

   /**
    * Search for a named attribute in all contexts, in the
    * following order: method, event, page, conversation,
    * session, business process, application.
    * 
    * @return the first component found, or null
    */
   public static Object lookupInStatefulContexts(String name)
   {
      if (isMethodContextActive())
      {
         Object result = getMethodContext().get(name);
         if (result!=null)
         {
             if ( log.isTraceEnabled() ) log.trace("found in method context: " + name);
            return result;
         }
      }
      
      if (isEventContextActive())
      {
         Object result = getEventContext().get(name);
         if (result!=null)
         {
             if ( log.isTraceEnabled() ) log.trace("found in event context: " + name);
            return result;
         }
      }
      
      if ( isPageContextActive() )
      {
         Object result = getPageContext().get(name);
         if (result!=null)
         {
             if ( log.isTraceEnabled() ) log.trace("found in page context: " + name);
            return result;
         }
      }
      
      if (isConversationContextActive())
      {
         Object result = getConversationContext().get(name);
         if (result!=null)
         {
             if ( log.isTraceEnabled() ) log.trace("found in conversation context: " + name);
            return result;
         }
      }
      
      if (isSessionContextActive())
      {
         Object result = getSessionContext().get(name);
         if (result!=null)
         {
             if ( log.isTraceEnabled() ) log.trace("found in session context: " + name);
            return result;
         }
      }
      
      if (isBusinessProcessContextActive())
      {
         Object result = getBusinessProcessContext().get(name);
         if (result!=null)
         {
             if ( log.isTraceEnabled() ) log.trace("found in business process context: " + name);
            return result;
         }
      }
      
      if (isApplicationContextActive())
      {
         Object result = getApplicationContext().get(name);
         if (result!=null)
         {
             if ( log.isTraceEnabled() ) log.trace("found in application context: " + name);
            return result;
         }
      }
      
      return null;
      
   }
   
   /**
    * Destroy all components in the given context
    */
   static void destroy(Context context)
   {
      if ( Events.exists() ) Events.instance().raiseEvent("org.jboss.seam.preDestroyContext." + context.getType().toString());
      
      Lifecycle.startDestroying();
      try
      {
         for ( String name: context.getNames() ) 
         {
            Component component = Component.forName(name);
            log.debug("destroying: " + name);
            if ( component!=null )
            {
               Object object = context.get(name);
               if (object!=null) //in a portal environment, this is possible
               {
                  if ( Events.exists() ) Events.instance().raiseEvent("org.jboss.seam.preDestroy." + name);
                  component.destroy(object);
               }
            }
         }
      }
      finally
      {
         Lifecycle.stopDestroying();
      }
      
      if ( Events.exists() ) Events.instance().raiseEvent("org.jboss.seam.postDestroyContext." + context.getType().toString());
   }
   
   /**
    * Startup all @Startup components in the given scope
    */
   static void startup(ScopeType scopeType)
   {
      Context context = Contexts.getApplicationContext();
      for ( String name: context.getNames() )
      {
         Object object = context.get(name);
         if ( object!=null && (object instanceof Component) )
         {
            Component component = (Component) object;
            if ( component.isStartup() && component.getScope()==scopeType )
            {
               startup(component);
            }
         }
      }
   }

   /**
    * Startup a component and all its dependencies
    */
   static void startup(Component component)
   {
      if ( component.isStartup() )
      {
         for ( String dependency: component.getDependencies() )
         {
            Component dependentComponent = Component.forName(dependency);
            if (dependentComponent!=null)
            {
               startup(dependentComponent);
            }
         }
      }

      if ( !component.getScope().getContext().isSet( component.getName() ) ) 
      {
         log.debug( "starting up: " + component.getName() );
         component.newInstance();
      }
   }

   /**
    * Does this context attribute need to be force-replicated?
    */
   static boolean isAttributeDirty(Object attribute)
   {
      return attribute instanceof Mutable && ( (Mutable) attribute ).clearDirty();
   }

   /**
    * At the end of a request, flush all contexts to their underlying
    * persistent stores, or destroy their attributes (one or the other!).
    */
   static void flushAndDestroyContexts()
   {
   
      if ( isConversationContextActive() )
      {
   
         if ( isBusinessProcessContextActive() )
         {
            boolean transactionActive = false;
            try
            {
               transactionActive = Transaction.instance().isActive();
            }
            catch (Exception e)
            {
               log.error("could not discover transaction status");
            }
            if (transactionActive)
            {
               //in calls to MDBs and remote calls to SBs, the 
               //transaction doesn't commit until after contexts
               //are destroyed, so pre-emptively flush here:
               getBusinessProcessContext().flush();
            }
            
            //TODO: it would be nice if BP context spanned redirects along with the conversation
            //      this would also require changes to BusinessProcessContext
            boolean destroyBusinessProcessContext = !Init.instance().isJbpmInstalled() ||
                  !BusinessProcess.instance().hasActiveProcess();
            if (destroyBusinessProcessContext)
            {
               //TODO: note that this occurs from Lifecycle.endRequest(), after
               //      the Seam-managed txn was committed, but Contexts.destroy()
               //      calls BusinessProcessContext.getNames(), which hits the
               //      database!
               log.debug("destroying business process context");
               destroy( getBusinessProcessContext() );
            }
         }
   
         if ( !Manager.instance().isLongRunningConversation() )
         {
            log.debug("destroying conversation context");
            destroy( getConversationContext() );
         }
         /*if ( !Init.instance().isClientSideConversations() )
         {*/
            //note that we need to flush even if the session is
            //about to be invalidated, since we still need
            //to destroy the conversation context in endSession()
            log.debug("flushing server-side conversation context");
            getConversationContext().flush();
        //}
   
         //uses the event and session contexts
         if ( Session.getInstance()!=null )
         {
            Manager.instance().unlockConversation();
         }
   
      }
      
      if ( isSessionContextActive() )
      {
         log.debug("flushing session context");
         getSessionContext().flush();
      }
      
      //destroy the event context after the
      //conversation context, since we need
      //the manager to flush() conversation
      if ( isEventContextActive() )
      {
         log.debug("destroying event context");
         destroy( getEventContext() );
      }
   
   }

   /**
    * Destroy a conversation context that is not currently bound to the request, called 
    * due to a timeout.
    * 
    * @param session the current session, to which both current and destroyed conversation belong
    * @param conversationId the conversation id of the conversation to be destroyed
    */
   static void destroyConversationContext(Map<String, Object> session, String conversationId)
   {
      Context current = getConversationContext();
      ServerConversationContext temp = new ServerConversationContext(session, conversationId);
      conversationContext.set(temp);
      try
      {
         destroy(temp);
         if ( !Session.instance().isInvalid() ) //its also unnecessary during a session timeout
         {
            temp.clear();
            temp.flush();
         }
      }
      finally
      {
         conversationContext.set(current);
      }
   }

}
