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

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.Seam;
import org.jboss.seam.core.Events;
import org.jboss.seam.core.Manager;
import org.jboss.seam.persistence.PersistenceContexts;

/**
 * A conversation context is a logical context that lasts longer than 
 * a request but shorter than a login session. Conversation state
 * may be passivated or replicated.
 * 
 * @author Gavin King
 * @author <a href="mailto:theute@jboss.org">Thomas Heute</a>
 */
public class ServerConversationContext implements Context 
{

   private final Map<String, Object> session;
   private final Map<String, Object> additions = new HashMap<String, Object>();
   private final Set<String> removals = new HashSet<String>();
   private final String id;
   private final List<String> idStack;
   
   private List<String> getIdStack()
   {
      return idStack==null ? Manager.instance().getCurrentConversationIdStack() : idStack;
   }
   
   private String getId()
   {
      return id==null ? Manager.instance().getCurrentConversationId() : id;
   }
   
   public ScopeType getType()
   {
      return ScopeType.CONVERSATION;
   }

   private String getKey(String name)
   {
      return getPrefix( getId() ) + name;
   }

   private String getKey(String name, String id)
   {
      return getPrefix(id) + name;
   }

   private String getPrefix(String id)
   {
      return ScopeType.CONVERSATION.getPrefix() + '#' + id + '$';
   }

   public ServerConversationContext(Map<String, Object> session)
   {
      this.session = session;
      id = null;
      idStack = null;
   }
      
   public ServerConversationContext(Map<String, Object> session, String id)
   {
      this.session = session;
      this.id = id;
      this.idStack = new LinkedList<String>();
      idStack.add(id);
   }
      
    public Object get(String name) 
    {
      Object result = additions.get(name);
      if (result!=null)
      {
         return unwrapEntityBean(result);
      }
      else
      {
         if ( removals.contains(name) ) 
         {
            return null;
         }
         else
         {
            List<String> stack = getIdStack();
            if (stack==null)
            {
               return unwrapEntityBean( session.get( getKey(name) ) );
            }
            else
            {
               for ( int i=0; i<stack.size(); i++ )
               {
                  String id = stack.get(i);
                  result = session.get( getKey(name, id) );

                  if (result != null) 
                  {
                      return unwrapEntityBean(result);
                  }

                  // only continue checking if it is not pernestedconversation
                  if ( i==0 && isPerNestedConversation(name) ) 
                  {
                      return null;
                  }
               }
               return null;
            }
         }
      }
    }

    private boolean isPerNestedConversation(String name) 
    {
        Component component = Component.forName(name);
        return (component != null) && component.isPerNestedConversation();
    }

   private Object unwrapEntityBean(Object result)
   {
      if (result==null) return null;
      if ( result instanceof Wrapper )
      {
         return ( (Wrapper) result ).getInstance();
      }
      else
      {
         return result;
      }
   }

   public void set(String name, Object value) 
   {
      if ( Events.exists() ) Events.instance().raiseEvent("org.jboss.seam.preSetVariable." + name);
      if (value==null)
      {
         //yes, we need this
         remove(name);
      }
      else
      {
         removals.remove(name);
         if ( Seam.isEntityClass( value.getClass() ) )
         {
            value = new EntityBean(value);
         }
         else if ( value instanceof List )
         {
            value = new EntityBeanList( (List) value );
         }
         else if ( value instanceof Map )
         {
            value = new EntityBeanMap( (Map) value );
         }
         else if ( value instanceof Set )
         {
            value = new EntityBeanSet( (Set) value );
         }
         additions.put(name, value);
      }
      if ( Events.exists() ) Events.instance().raiseEvent("org.jboss.seam.postSetVariable." + name);
	}

	public boolean isSet(String name) 
   {
		return get(name)!=null;
	}
   
	public void remove(String name) 
   {
      if ( Events.exists() ) Events.instance().raiseEvent("org.jboss.seam.preRemoveVariable." + name);
      additions.remove(name);
      removals.add(name);
      if ( Events.exists() ) Events.instance().raiseEvent("org.jboss.seam.postRemoveVariable." + name);
	}

   public String[] getNames() 
   {
      Set<String> results = getNamesFromSession();
      results.addAll( additions.keySet() ); //after, to override
      return results.toArray(new String[]{});
   }

   private Set<String> getNamesFromSession() 
   {       
       HashSet<String> results = new HashSet<String>();
       
       String prefix = getPrefix(getId());
       for (String name: session.keySet()) {
           if (name.startsWith(prefix)) {
               name = name.substring(prefix.length());
               if (!removals.contains(name)) {
                   results.add(name);
               }
           }
       }

       return results;
   }
   
   private Set<String> getNamesForAllConversationsFromSession() 
   {       
       Set<String> results = new HashSet<String>();
       
       List<String> ids = Manager.instance().getCurrentConversationIdStack();
       
       if (ids != null) {
           for (String conversationId: ids) {
               String prefix = getPrefix(conversationId);
               for (String name: session.keySet()) {
                   if (name.startsWith(prefix)) {
                       String shortName = name.substring(prefix.length());
                       if (!removals.contains(shortName)) {
                           results.add(name);
                       }
                   }
               }
           }
       }

       return results;
   }
   

   public Object get(Class clazz)
   {
      return get( Component.getComponentName(clazz) );
   }
   
   public void clear()
   {
      additions.clear();
      removals.addAll( getNamesFromSession() );
   }
   
   public void unflush()
   {
      for ( String key: getNamesForAllConversationsFromSession() )
      {
         Object attribute = session.get(key);
         if ( attribute!=null && attribute instanceof Wrapper ) 
         {
            ( (Wrapper) attribute ).activate();
         }
      }
   }
   
   /**
    * Propagate additions and removals to the HttpSession if 
    * the current conversation is long-running, or remove all 
    * attributes if it is a temporary conversation. This work
    * may only be done at the end of the request, since we
    * don't know for sure the conversation id until then.
    */
   public void flush()
   {      
      boolean longRunning = !isCurrent() || Manager.instance().isLongRunningConversation();  
          
      if ( longRunning )
      {
          //force update for dirty mutable objects
          for (String key: getNamesForAllConversationsFromSession())  {
              Object attribute = session.get(key);
              
              if (attribute!=null) {
                  if (passivate(attribute) || isAttributeDirty(attribute)) {
                      session.put(key, attribute);
                  }
              }
          }
    
          //remove removed objects
          for (String name: removals) {
              session.remove(getKey(name));
          }
          removals.clear();

          // Nothing to do if there is no addition
          if (!additions.isEmpty())
          {
             // TODO this is a hack! We should find a more elegant way of handling
             // new objects being added to additions during the following for-loop
             PersistenceContexts.instance();
             
             //add new objects
             for (Map.Entry<String, Object> entry: additions.entrySet())  {
                 Object attribute = entry.getValue();
                 
                 passivate(attribute); 
                 session.put(getKey(entry.getKey()), attribute);
             }
             additions.clear();
          }
      }
      else
      {
         //TODO: for a pure temporary conversation, this is unnecessary, optimize it
         for (String name: getNamesFromSession()) {
            session.remove( getKey(name) );
         }
         
         // remove removed objects
         for (String name: removals) {
             session.remove(getKey(name));
         }
         removals.clear();
      }
   }

    private boolean passivate(Object attribute) {
        if (attribute instanceof Wrapper) {
            return ((Wrapper) attribute).passivate();
        } else {
            return false;
        }
    }
    
    private boolean isAttributeDirty(Object attribute) {
        return Contexts.isAttributeDirty(attribute);
    }
    
   private boolean isCurrent()
   {
      return id==null || id.equals( Manager.instance().getCurrentConversationId() );
   }

   @Override
   public String toString()
   {
      return "ConversationContext(" + getId() + ")";
   }

}
