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

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

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.jboss.seam.Component;
import org.jboss.seam.ConcurrentRequestTimeoutException;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.FlushModeType;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.contexts.Lifecycle;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.navigation.ConversationIdParameter;
import org.jboss.seam.navigation.Pages;
//import org.jboss.seam.pageflow.Pageflow;
import org.jboss.seam.util.Id;
import org.jboss.seam.web.Session;

/**
 * The Seam conversation manager.
 *
 * @author Gavin King
 * @author <a href="mailto:theute@jboss.org">Thomas Heute</a>
 */
@Scope(ScopeType.EVENT)
@Name("org.jboss.seam.core.manager")
@Install(precedence=BUILT_IN)
@BypassInterceptors
public class Manager
{
   public static final String EVENT_CONVERSATION_TIMEOUT = "org.jboss.seam.conversationTimeout";
   public static final String EVENT_CONVERSATION_DESTROYED = "org.jboss.seam.conversationDestroyed";
   public static final String EVENT_CONVERSATION_BEGIN = "org.jboss.seam.beginConversation";
   public static final String EVENT_CONVERSATION_END = "org.jboss.seam.endConversation";
   
   private static final LogProvider log = Logging.getLogProvider(Manager.class);
   
   public static final String REDIRECT_FROM_MANAGER = "org.jboss.seam.core.Manager";

   private static final String DEFAULT_ENCODING = "UTF-8";

   //The id of the current conversation
   private String currentConversationId;
   private List<String> currentConversationIdStack;

   //Is the current conversation "long-running"?
   private boolean isLongRunningConversation;
   
   //private boolean updateModelValuesCalled;

   private boolean destroyBeforeRedirect;
   
   private int conversationTimeout = 600000; //10 mins
   private int concurrentRequestTimeout = 1000; //one second
   
   private String conversationIdParameter = "conversationId";
   private String parentConversationIdParameter = "parentConversationId";

   private String URIEncoding = DEFAULT_ENCODING;
   
   private FlushModeType defaultFlushMode;

   /**
    * Kills all conversations except the current one 
    */
   public void killAllOtherConversations()
   {
      ConversationEntries conversationEntries = ConversationEntries.instance();
      Events events = Events.exists() ? Events.instance() : null;

      if (conversationEntries != null)
      {
         List<ConversationEntry> entries = new ArrayList<ConversationEntry>(
               conversationEntries.getConversationEntries());

         for (ConversationEntry conversationEntry : entries)
         {
            // kill all entries expect the current one
            // current conversation entry will be null if , kill-all is called
            // inside a new @Begin
            if (getCurrentConversationEntry() == null
                  || !getCurrentConversationIdStack().contains(
                        conversationEntry.getId()))
            {
               log.debug("Kill all other conversations, executed: kill conversation id = "
                           + conversationEntry.getId());

               boolean locked = conversationEntry.lockNoWait(); // we had better
               // not wait for it, or we would be waiting for ALL other requests
               try
               {
                  if (locked)
                  {
                     if (log.isDebugEnabled())
                     {
                        log.debug("conversation killed manually: " + conversationEntry.getId());
                     }
                  } 
                  else
                  {
                     // if we could not acquire the lock, someone has left a
                     // garbage lock lying around
                     // the reason garbage locks can exist is that we don't
                     // require a servlet filter to
                     // exist - but if we do use SeamExceptionFilter, it will
                     // clean up garbage and this
                     // case should never occur

                     // NOTE: this is slightly broken - in theory there is a
                     // window where a new request
                     // could have come in and got the lock just before us but
                     // called touch() just
                     // after we check the timeout - but in practice this would
                     // be extremely rare,
                     // and that request will get an
                     // IllegalMonitorStateException when it tries to
                     // unlock() the CE
                     log.debug("kill conversation with garbage lock: "
                           + conversationEntry.getId());
                  }
                  if (events != null)
                  {
                     events.raiseEvent(EVENT_CONVERSATION_DESTROYED, conversationEntry);
                  }
                  destroyConversation(conversationEntry.getId(), getSessionMap());
               } 
               finally
               {
                  if (locked)
                  {
                     conversationEntry.unlock();
                  }
               }
            }
         }
      }      
   }
   
   /**
    * @return Map session
    */
   private Map<String, Object> getSessionMap()
   {
      // this method could be moved to a utility class
      Map<String, Object> session = new HashMap<String, Object>();
      String[] sessionAttributeNames = Contexts.getSessionContext().getNames();
   
      for (String attributeName : sessionAttributeNames)
      {
         session.put(attributeName, Contexts.getSessionContext().get(attributeName));
      }
      return session;
   }   
   
   // DONT BREAK, icefaces uses this
   public String getCurrentConversationId()
   {
      return currentConversationId;
   }

   /**
    * Only public for the unit tests!
    * @param id
    */
   public void setCurrentConversationId(String id)
   {
      currentConversationId = id;
      currentConversationEntry = null;
   }
   
   /**
    * Change the id of the current conversation.
    * 
    * @param id the new conversation id
    */
   public void updateCurrentConversationId(String id)
   {
      if (id != null && id.equals(currentConversationId))
      {
         // the conversation id hasn't changed, do nothing       
         return;
      }
      
      if ( ConversationEntries.instance().getConversationIds().contains(id) )
      {
         throw new IllegalStateException("Conversation id is already in use: " + id);
      }
      
      String[] names = Contexts.getConversationContext().getNames();
      Object[] values = new Object[names.length];
      for (int i=0; i<names.length; i++)
      {
         values[i] = Contexts.getConversationContext().get(names[i]);
         Contexts.getConversationContext().remove(names[i]);
      }
      Contexts.getConversationContext().flush();
      
      ConversationEntry ce = ConversationEntries.instance().updateConversationId(currentConversationId, id);
      String priorId = currentConversationId;
      setCurrentConversationId(id);
      
      if (ce!=null)
      {
         setCurrentConversationIdStack( ce.getConversationIdStack() );
         //TODO: what about child conversations?!
      } 
      else 
      {
          // when ce is null, the id stack will be left with a reference to
          // the old conversation id, so we need patch that up
          int pos = currentConversationIdStack.indexOf(priorId);
          if (pos != -1) 
          {
              currentConversationIdStack.set(pos, id);
          }          
      }
      
      for (int i=0; i<names.length; i++)
      {
         Contexts.getConversationContext().set(names[i], values[i]);
      }
   }

   private void touchConversationStack(List<String> stack)
   {
      if ( stack!=null )
      {
         //iterate in reverse order, so that current conversation 
         //sits at top of conversation lists
         ListIterator<String> iter = stack.listIterator( stack.size() );
         while ( iter.hasPrevious() )
         {
            String conversationId = iter.previous();
            ConversationEntry conversationEntry = ConversationEntries.instance().getConversationEntry(conversationId);
            if (conversationEntry!=null)
            {
               conversationEntry.touch();
            }
         }
      }
   }
   
   private void endNestedConversations(String id)
   {
      for ( ConversationEntry ce: ConversationEntries.instance().getConversationEntries() )
      {
         if ( ce.getConversationIdStack().contains(id) )
         {
            ce.end();
         }
      }
   }

   public List<String> getCurrentConversationIdStack()
   {
      return currentConversationIdStack;
   }

   public void setCurrentConversationIdStack(List<String> stack)
   {
      currentConversationIdStack = stack;
   }

   private List<String> createCurrentConversationIdStack(String id)
   {
      currentConversationIdStack = new ArrayList<String>();
      currentConversationIdStack.add(id);
      return currentConversationIdStack;
   }

   public String getCurrentConversationDescription()
   {
      ConversationEntry ce = getCurrentConversationEntry();
      if ( ce==null ) return null;
      return ce.getDescription();
   }

   public Integer getCurrentConversationTimeout()
   {
      ConversationEntry ce = getCurrentConversationEntry();
      if ( ce==null ) return null;
      return ce.getTimeout();
   }
   
   public Integer getCurrentConversationConcurrentRequestTimeout()
   {
      ConversationEntry ce = getCurrentConversationEntry();
      if (ce == null) return null;
      return ce.getConcurrentRequestTimeout();
   }

   public String getCurrentConversationViewId()
   {
      ConversationEntry ce = getCurrentConversationEntry();
      if ( ce==null ) return null;
      return ce.getViewId();
   }
   
   public String getParentConversationViewId()
   {
      ConversationEntry conversationEntry = ConversationEntries.instance().getConversationEntry(getParentConversationId());
      return conversationEntry==null ? null : conversationEntry.getViewId();
   }
   
   public String getParentConversationId()
   {
      return currentConversationIdStack==null || currentConversationIdStack.size()<2 ?
            null : currentConversationIdStack.get(1);
   }

   public String getRootConversationId()
   {
      return currentConversationIdStack==null || currentConversationIdStack.size()<1 ?
            null : currentConversationIdStack.get( currentConversationIdStack.size()-1 );
   }

   // DONT BREAK, icefaces uses this
   public boolean isLongRunningConversation()
   {
      return isLongRunningConversation;
   }

   public boolean isLongRunningOrNestedConversation()
   {
      return isLongRunningConversation() || isNestedConversation();
   }

   public boolean isReallyLongRunningConversation()
   {
      return isLongRunningConversation() && 
            !getCurrentConversationEntry().isRemoveAfterRedirect() &&
            !Session.instance().isInvalid();
   }
   
   public boolean isNestedConversation()
   {
      return currentConversationIdStack!=null && 
            currentConversationIdStack.size()>1;
   }

   public void setLongRunningConversation(boolean isLongRunningConversation)
   {
      this.isLongRunningConversation = isLongRunningConversation;
   }

   public static Manager instance()
   {
      if ( !Contexts.isEventContextActive() )
      {
         throw new IllegalStateException("No active event context");
      }
      Manager instance = (Manager) Component.getInstance(Manager.class, ScopeType.EVENT);
      if (instance==null)
      {
         throw new IllegalStateException("No Manager could be created, make sure the Component exists in application scope");
      }
      return instance;
   }

   /**
    * Clean up timed-out conversations
    */
   public void conversationTimeout(Map<String, Object> session)
   {
      long currentTime = System.currentTimeMillis();
      ConversationEntries conversationEntries = ConversationEntries.getInstance();
      if (conversationEntries!=null)
      {
         List<ConversationEntry> entries = new ArrayList<ConversationEntry>( conversationEntries.getConversationEntries() );
         for (ConversationEntry conversationEntry: entries)
         {
            boolean locked = conversationEntry.lockNoWait(); //we had better not wait for it, or we would be waiting for ALL other requests
            try
            {
               long delta = currentTime - conversationEntry.getLastRequestTime();
               if ( delta > conversationEntry.getTimeout() )
               {
                  if ( locked )
                  { 
                     if ( log.isDebugEnabled() )
                     {
                        log.debug("conversation timeout for conversation: " + conversationEntry.getId());
                     }
                  }
                  else
                  {
                     //if we could not acquire the lock, someone has left a garbage lock lying around
                     //the reason garbage locks can exist is that we don't require a servlet filter to
                     //exist - but if we do use SeamExceptionFilter, it will clean up garbage and this
                     //case should never occur
                     
                     //NOTE: this is slightly broken - in theory there is a window where a new request 
                     //      could have come in and got the lock just before us but called touch() just 
                     //      after we check the timeout - but in practice this would be extremely rare, 
                     //      and that request will get an IllegalMonitorStateException when it tries to 
                     //      unlock() the CE
                     log.debug("destroying conversation with garbage lock: " + conversationEntry.getId());
                  }
                  if ( Events.exists() ) 
                  {
                     Events.instance().raiseEvent(EVENT_CONVERSATION_TIMEOUT, conversationEntry.getId());
                  }
                  destroyConversation( conversationEntry.getId(), session );
               }
            }
            finally
            {
               if (locked) conversationEntry.unlock();
            }
         }
      }
   }

   /**
    * Clean up all state associated with a conversation
    */
   private void destroyConversation(String conversationId, Map<String, Object> session)
   {
      Lifecycle.destroyConversationContext(session, conversationId);
      ConversationEntries.instance().removeConversationEntry(conversationId);
   }

   /**
    * Touch the conversation stack, destroy ended conversations, 
    * and timeout inactive conversations.
    */
   public void endRequest(Map<String, Object> session)
   {
      if ( isLongRunningConversation() )
      {
         if ( log.isDebugEnabled() )
         {
            log.debug("Storing conversation state: " + getCurrentConversationId());
         }
         touchConversationStack( getCurrentConversationIdStack() );
      }
      else
      {
         if ( log.isDebugEnabled() )
         {
            log.debug("Discarding conversation state: " + getCurrentConversationId());
         }
         //now safe to remove the entry
         removeCurrentConversationAndDestroyNestedContexts(session);
      }

      /*if ( !Init.instance().isClientSideConversations() ) 
      {*/
         // difficult question: is it really safe to do this here?
         // right now we do have to do it after committing the Seam
         // transaction because we can't close EMs inside a txn
         // (this might be a bug in HEM)
         Manager.instance().conversationTimeout(session);
      //}
   }
   
   public void unlockConversation()
   {
      ConversationEntry ce = getCurrentConversationEntry();
      if (ce!=null) 
      {
         if ( ce.isLockedByCurrentThread() )
         {
            ce.unlock();
         }
      }
      else if ( isNestedConversation() )
      {
         ConversationEntries.instance().getConversationEntry( getParentConversationId() ).unlock();
      }
   }

   private void removeCurrentConversationAndDestroyNestedContexts(Map<String, Object> session) 
   {
      ConversationEntries conversationEntries = ConversationEntries.getInstance();
      if (conversationEntries!=null)
      {
         conversationEntries.removeConversationEntry( getCurrentConversationId() );
         destroyNestedConversationContexts( session, getCurrentConversationId() );
      }
   }

   private void destroyNestedConversationContexts(Map<String, Object> session, String conversationId) 
   {
      List<ConversationEntry> entries = new ArrayList<ConversationEntry>( ConversationEntries.instance().getConversationEntries() );
      for  ( ConversationEntry ce: entries )
      {
         if ( ce.getConversationIdStack().contains(conversationId) )
         {
            String entryConversationId = ce.getId();
            log.debug("destroying nested conversation: " + entryConversationId);
            destroyConversation(entryConversationId, session);
         }
      }
   }

   /**
    * Look for a conversation propagation style in the request
    * parameters and begin, nested or join the conversation,
    * as necessary.
    * 
    * @param parameters the request parameters
    */
   public void handleConversationPropagation(Map parameters)
   {      
      ConversationPropagation propagation = ConversationPropagation.instance();
      
      if (propagation.getPropagationType() == null)
      {
         return;
      }

      switch (propagation.getPropagationType())
      {
         case BEGIN:
            if ( isLongRunningConversation )
            {
               throw new IllegalStateException("long-running conversation already active");
            }
            beginConversation();
            
//            if (propagation.getPageflow() != null)
//            {
//               Pageflow.instance().begin( propagation.getPageflow() );
//            }
            break;
         case JOIN:
            if ( !isLongRunningConversation )
            {
               beginConversation();
//               
//               if (propagation.getPageflow() != null)
//               {
//                  Pageflow.instance().begin( propagation.getPageflow() );
//               }
            }
            break;
         case NESTED:
            if ( isLongRunningOrNestedConversation() ) 
            {
                beginNestedConversation();
            }
            else 
            {
                beginConversation();
            }
            
            if (propagation.getPageflow() != null)
            {
//               Pageflow.instance().begin( propagation.getPageflow() );
            }
            break;
         case END:
            endConversation(false);
            break;
         case ENDROOT:
            endRootConversation(false);
            break;
      }
   }
   
   /**
    * Initialize the request conversation context, given the 
    * conversation id and optionally a parent conversation id.
    * If no conversation entry is found for the first id, try
    * the parent, and if that also fails, initialize a new 
    * temporary conversation context.
    * 
    * @return false if the conversation entry was not found
    *         and it was required
    */
   public boolean restoreConversation() 
   {
      ConversationPropagation cp = ConversationPropagation.instance();
      String conversationId = cp.getConversationId();
      String parentConversationId = cp.getParentConversationId();
      ConversationEntry ce = null;
      if (conversationId!=null)
      {
         ConversationEntries entries = ConversationEntries.instance();
         ce = entries.getConversationEntry(conversationId);
         if (ce==null)
         {
            ce = entries.getConversationEntry(parentConversationId);
         }
      }
      
      return restoreAndLockConversation(ce) || !cp.isValidateLongRunningConversation();
   }

   private boolean restoreAndLockConversation(ConversationEntry ce)
   {
      if (ce == null)
      {
         //there was no id in either place, so there is no
         //long-running conversation to restore
         log.debug("No stored conversation");
         initializeTemporaryConversation();
         return false;
      }
      else if ( ce.lock() )
      {
         // do this ASAP, since there is a window where conversationTimeout() might  
         // try to destroy the conversation, even if he cannot obtain the lock!
         touchConversationStack( ce.getConversationIdStack() );

         //we found an id and obtained the lock, so restore the long-running conversation
         log.debug("Restoring conversation with id: " + ce.getId());
         setLongRunningConversation(true);
         setCurrentConversationId( ce.getId() );
         setCurrentConversationIdStack( ce.getConversationIdStack() );

         boolean removeAfterRedirect = ce.isRemoveAfterRedirect() && !Pages.isDebugPage(); //TODO: hard dependency to JSF!!
         if (removeAfterRedirect)
         {
            setLongRunningConversation(false);
            ce.setRemoveAfterRedirect(false);
         }
         
         return true;

      } 
      else
      {
         log.debug("Concurrent call to conversation");
         throw new ConcurrentRequestTimeoutException("Concurrent call to conversation");
      }
   }

   /**
    * Initialize a new temporary conversation context,
    * and assign it a conversation id.
    */
   public void initializeTemporaryConversation()
   {
      String id = generateInitialConversationId();
      setCurrentConversationId(id);
      createCurrentConversationIdStack(id);
      setLongRunningConversation(false);
   }

   protected String generateInitialConversationId()
   {
      return Id.nextId();
   }

   private ConversationEntry createConversationEntry()
   {
      ConversationEntry entry = ConversationEntries.instance()
            .createConversationEntry( getCurrentConversationId(), getCurrentConversationIdStack() );
      if ( !entry.isNested() ) 
      {
         //if it is a newly created nested 
         //conversation, we already own the
         //lock
         entry.lock();
      }
      return entry;
   }

   /**
    * Promote a temporary conversation and make it long-running
    */
   public void beginConversation()
   {
      if ( !isLongRunningConversation() )
      {
         log.debug("Beginning long-running conversation");
         setLongRunningConversation(true);
         createConversationEntry();
         Conversation.instance(); //force instantiation of the Conversation in the outer (non-nested) conversation
         storeConversationToViewRootIfNecessary();
         if ( Events.exists() ) Events.instance().raiseEvent(EVENT_CONVERSATION_BEGIN);
      }
   }

   /**
    * Begin a new nested conversation.
    */
   public void beginNestedConversation()   
   {
      log.debug("Beginning nested conversation");
      List<String> oldStack = getCurrentConversationIdStack();
      if (oldStack==null)
      {
         throw new IllegalStateException("No long-running conversation active");
      }

      String id = Id.nextId();
      setCurrentConversationId(id);      
      createCurrentConversationIdStack(id).addAll(oldStack);
      createConversationEntry();
      storeConversationToViewRootIfNecessary();
      if ( Events.exists() ) Events.instance().raiseEvent(EVENT_CONVERSATION_BEGIN);
   }
   
   /**
    * Make a long-running conversation temporary.
    */
   public void endConversation(boolean beforeRedirect)
   {
      if ( isLongRunningConversation() )
      {
         log.debug("Ending long-running conversation");
         if ( Events.exists() ) Events.instance().raiseEvent(EVENT_CONVERSATION_END);
         setLongRunningConversation(false);
         destroyBeforeRedirect = beforeRedirect;
         endNestedConversations( getCurrentConversationId() );
         storeConversationToViewRootIfNecessary();
      }
      unlockConversation();
   }
   
   /**
    * Make the root conversation in the current conversation stack temporary.
    */
   public void endRootConversation(boolean beforeRedirect)
   {
      if(isNestedConversation())
      {
         switchConversation(getRootConversationId());
      }
      
      endConversation(beforeRedirect);
   }
   
   protected void storeConversationToViewRootIfNecessary() {}

   // two reasons for this: 
   // (1) a cache
   // (2) so we can unlock() it after destruction of the session context 
   private ConversationEntry currentConversationEntry; 
   
   public ConversationEntry getCurrentConversationEntry() 
   {
      if (currentConversationEntry==null)
      {
         currentConversationEntry = ConversationEntries.instance().getConversationEntry( getCurrentConversationId() );
      }
      return currentConversationEntry;
   }
   
   /**
    * Leave the scope of the current conversation, leaving
    * it completely intact.
    */
   public void leaveConversation()
   {
      unlockConversation();
      initializeTemporaryConversation();
   }

   /**
    * Switch to another long-running conversation and mark the conversation as long-running,
    * overriding a previous call in the same thread to demote a long-running conversation.
    * 
    * @param id the id of the conversation to switch to
    * @return true if the conversation exists
    */
   public boolean switchConversation(String id)
   {
      return switchConversation(id, true);
   }

   /**
    * Switch to another long-running conversation.
    * 
    * @param id the id of the conversation to switch to
    * @param promote promote the current conversation to long-running, overriding any previous demotion
    * @return true if the conversation exists
    */
   public boolean switchConversation(String id, boolean promote)
   {
      ConversationEntry ce = ConversationEntries.instance().getConversationEntry(id);
      if (ce!=null)
      {
         if ( ce.lock() )
         {
            unlockConversation();
            setCurrentConversationId(id);
            setCurrentConversationIdStack( ce.getConversationIdStack() );
            if (promote)
            {
               setLongRunningConversation(true);
            }
            return true;
         }
         else
         {
            return false;
         }
      }
      else
      {
         return false;
      }
   }

   public int getConversationTimeout() {
      return conversationTimeout;
   }

   public void setConversationTimeout(int conversationTimeout) {
      this.conversationTimeout = conversationTimeout;
   }
   
   /**
    * Temporarily promote a temporary conversation to
    * a long running conversation for the duration of
    * a browser redirect. After the redirect, the 
    * conversation will be demoted back to a temporary
    * conversation.
    */
   public void beforeRedirect()
   {
      //DONT BREAK, icefaces uses this
      if (!destroyBeforeRedirect)
      {
         ConversationEntry ce = getCurrentConversationEntry();
         if (ce==null)
         {
            ce = createConversationEntry();
         }
         //ups, we don't really want to destroy it on this request after all!
         ce.setRemoveAfterRedirect( !isLongRunningConversation() );
         setLongRunningConversation(true);
      }
   }

   protected static boolean isDifferentConversationId(ConversationIdParameter sp, ConversationIdParameter tp)
   {
      return sp.getName()!=tp.getName() && ( sp.getName()==null || !sp.getName().equals( tp.getName() ) );
   }
   
   /**
    * Add the conversation id to a URL, if necessary
    * 
    * @deprecated use encodeConversationId(String url, String viewId)
    */
   public String encodeConversationId(String url)
   {
      //DONT BREAK, icefaces uses this
      return encodeConversationIdParameter( url, getConversationIdParameter(), getCurrentConversationId() );
   }
         
   /**
    * Add the conversation id to a URL, if necessary
    */
   public String encodeConversationId(String url, String viewId) 
   {
      //DONT BREAK, icefaces uses this
      ConversationIdParameter cip = Pages.instance().getPage(viewId).getConversationIdParameter();
      return encodeConversationIdParameter( url, cip.getParameterName(), cip.getParameterValue() );
   }
 
   /**
    * Add the conversation id to a URL, if necessary
    */
   public String encodeConversationId(String url, String viewId, String conversationId) 
   {
      //DONT BREAK, icefaces uses this
      ConversationIdParameter cip = Pages.instance().getPage(viewId).getConversationIdParameter();
      return encodeConversationIdParameter( url, cip.getParameterName(), cip.getParameterValue(conversationId) );
   }
 
   protected String encodeConversationIdParameter(String url, String paramName, String paramValue)
   {         
      if ( Session.instance().isInvalid() || containsParameter(url, paramName) )
      {
         return url;
      }
      else if (destroyBeforeRedirect)
      {
         if ( isNestedConversation() )
         {
            return new StringBuilder( url.length() + paramName.length() + 5 )
                  .append(url)
                  .append( url.contains("?") ? '&' : '?' )
                  .append(paramName)
                  .append('=')
                  .append( encode( getParentConversationId() ) )
                  .toString();
         }
         else
         {
            return url;
         }
      }
      else
      {
         StringBuilder builder = new StringBuilder( url.length() + paramName.length() + 5 )
               .append(url)
               .append( url.contains("?") ? '&' : '?' )
               .append(paramName)
               .append('=')
               .append( encode(paramValue) );
         if ( isNestedConversation() && !isReallyLongRunningConversation() )
         {
            builder.append('&')
                  .append(parentConversationIdParameter)
                  .append('=')
                  .append( encode( getParentConversationId() ) );
         }
         return builder.toString();
      }
   }

   /**
    * Add the parameters to a URL
    */
   public String encodeParameters(String url, Map<String, Object> parameters)
   {
      if ( parameters.isEmpty() ) return url;
      
      StringBuilder builder = new StringBuilder(url);
      for ( Map.Entry<String, Object> param: parameters.entrySet() )
      {
         String parameterName = param.getKey();
         if ( !containsParameter(url, parameterName) )
         {
            Object parameterValue = param.getValue();
            if (parameterValue instanceof Iterable)
            {
               for ( Object value: (Iterable) parameterValue )
               {
                  builder.append('&')
                        .append(parameterName)
                        .append('=');
                  if (value!=null)
                  {
                     builder.append(encode(value));
                  }
               }
            }
            else
            {
               builder.append('&')
                     .append(parameterName)
                     .append('=');
               if (parameterValue!=null)
               {
                  builder.append(encode(parameterValue));
               }
            }
         }
      }
      if ( url.indexOf('?')<0 ) 
      {
         builder.setCharAt( url.length() ,'?' );
      }
      return builder.toString();
   }

   private boolean containsParameter(String url, String parameterName)
   {
      return url.indexOf('?' + parameterName + '=')>0 || 
            url.indexOf( '&' + parameterName + '=')>0;
   }

   private String encode(Object value)
   {
      try
      {
         return URLEncoder.encode(String.valueOf(value),getUriEncoding());
      }
      catch (UnsupportedEncodingException iee)
      {
         throw new RuntimeException(iee);
      }
   }
   
   public String getConversationIdParameter()
   {
      return conversationIdParameter;
   }

   public void setConversationIdParameter(String conversationIdParameter)
   {
      this.conversationIdParameter = conversationIdParameter;
   }

   public String getParentConversationIdParameter()
   {
      return parentConversationIdParameter;
   }

   public void setParentConversationIdParameter(String nestedConversationIdParameter)
   {
      this.parentConversationIdParameter = nestedConversationIdParameter;
   }

   public int getConcurrentRequestTimeout()
   {
      return concurrentRequestTimeout;
   }

   public void setConcurrentRequestTimeout(int requestWait)
   {
      this.concurrentRequestTimeout = requestWait;
   }
   
   public FlushModeType getDefaultFlushMode()
   {
      return defaultFlushMode;
   }
   
   public void setDefaultFlushMode(FlushModeType defaultFlushMode)
   {
      this.defaultFlushMode = defaultFlushMode;
   }

   @Override
   public String toString()
   {
      return "Manager(" + currentConversationIdStack + ")";
   }

   public void redirect(String viewId, String id)
   {
      //declare it here since ConversationEntry calls it!
      throw new UnsupportedOperationException();
   }

   public void redirect(String viewId)
   {
      //declare it here since Conversation calls it!
      throw new UnsupportedOperationException();
   }

   protected void flushConversationMetadata()
   {
      if ( isLongRunningConversation() )
      {
         //important: only do this stuff when a long-running
         //           conversation exists, otherwise we would
         //           force creation of a conversation entry
         Conversation.instance().flush();
      }
   }

   public String getUriEncoding() {
       return URIEncoding;
   }

   public void setUriEncoding(String encoding) {
       URIEncoding = encoding;
   }

}
