package org.jboss.seam.faces;

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

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import javax.faces.application.FacesMessage;
import javax.faces.application.FacesMessage.Severity;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import org.hibernate.validator.InvalidValue;
import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
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.core.Interpolator;
import org.jboss.seam.core.SeamResourceBundle;
import org.jboss.seam.util.Strings;

/**
 * A Seam component that propagates FacesMessages across redirects
 * and interpolates EL expressions in the message string.
 * 
 * @author Gavin King
 */
@Scope(ScopeType.CONVERSATION)
@Name("org.jboss.seam.faces.facesMessages")
@Install(precedence=BUILT_IN, classDependencies="javax.faces.context.FacesContext")
@BypassInterceptors
public class FacesMessages implements Serializable
{
   private static final long serialVersionUID = -5395975397632138270L;
   private transient List<Runnable> tasks;
   
   private List<Message> facesMessages = new ArrayList<Message>();
   private Map<String, List<Message>> keyedFacesMessages = new HashMap<String, List<Message>>();
   
   /**
    * Workaround for non-serializability of
    * JSF FacesMessage.Severity class.
    * 
    * @author Gavin King
    *
    */
   class Message implements Serializable
   {
      private String summary;
      private String detail;
      private int severityOrdinal;
      
      Message(FacesMessage fm)
      {
         summary = fm.getSummary();
         detail = fm.getDetail();
         severityOrdinal = fm.getSeverity().getOrdinal();
      }
      
      FacesMessage toFacesMessage()
      {
         Severity severity = null;
         for (Object o : FacesMessage.VALUES) 
         {
            severity = (Severity) o;
            if (severity.getOrdinal() == severityOrdinal) 
            {
               break;
            }
         }
         return new FacesMessage(severity, summary, detail );
      }
   }

   public void beforeRenderResponse() 
   {
      for (Message message: facesMessages)
      {
         FacesContext.getCurrentInstance().addMessage( null,message.toFacesMessage() );
      }
      for ( Map.Entry<String, List<Message>> entry: keyedFacesMessages.entrySet() )
      {
         for ( Message msg: entry.getValue() )
         {
            FacesContext.getCurrentInstance().addMessage( entry.getKey(), msg.toFacesMessage() );
         }
      }
      clear();
   }
   
   /**
    * Get all faces messages that have already been added
    * to the context.
    * 
    * @return a list of messages
    */
   public List<FacesMessage> getCurrentMessages()
   {
      List<FacesMessage> result = new ArrayList<FacesMessage>();
      Iterator<FacesMessage> iter = FacesContext.getCurrentInstance().getMessages();
      while ( iter.hasNext() )
      {
         result.add( iter.next() );
      }
      return result;
   }
   
   /**
    * Get all faces global messages that have already been added
    * to the context.
    * 
    * @return a list of global messages
    */
   public List<FacesMessage> getCurrentGlobalMessages()
   {
      List<FacesMessage> result = new ArrayList<FacesMessage>();
      Iterator<FacesMessage> iter = FacesContext.getCurrentInstance().getMessages(null);
      while ( iter.hasNext() )
      {
         result.add( iter.next() );
      }
      return result;
   }
   
   /**
    * Get all faces messages that have already been added
    * to the control.
    * 
    * @return a list of messages
    */
   public List<FacesMessage> getCurrentMessagesForControl(String id)
   {
      String clientId = getClientId(id);
      List<FacesMessage> result = new ArrayList<FacesMessage>();
      Iterator<FacesMessage> iter = FacesContext.getCurrentInstance().getMessages(clientId);
      while ( iter.hasNext() )
      {
         result.add( iter.next() );
      }
      return result;
   }
   
   private void runTasks()
   {
      if (tasks!=null)
      {
         for (Runnable task: tasks) task.run();
         tasks.clear();
      }
   }
   
   public static void afterPhase()
   {
      if ( Contexts.isConversationContextActive() )
      {
         FacesMessages instance = (FacesMessages) Component.getInstance(FacesMessages.class, ScopeType.CONVERSATION, false);
         if (instance!=null) instance.runTasks();
      }
   }
   
   public void clear()
   {
      facesMessages.clear();
      keyedFacesMessages.clear();
   }
   
   /**
    * Add a FacesMessage that will be used
    * the next time a page is rendered.
    */
   public void add(FacesMessage facesMessage) 
   {
      if (facesMessage!=null)
      {
         facesMessages.add( new Message(facesMessage) );
      }
   }
   
   /**
    * Add a FacesMessage instance to a particular component id
    * @param id a JSF component id
    */
   public void addToControl(String id, FacesMessage facesMessage)
   {
      if (facesMessage!=null)
      {
         String clientId = getClientId(id);
         List<Message> list = keyedFacesMessages.get(clientId);
         if (list==null)
         {
            list = new ArrayList<Message>();
            keyedFacesMessages.put(clientId, list);
         }
         list.add( new Message(facesMessage) );
      }
   }
   
   /**
    * Add a templated FacesMessage that will be used
    * the next time a page is rendered.
    */
   public void add(String messageTemplate, Object... params)
   {
      addToTasks(FacesMessage.SEVERITY_INFO, null, messageTemplate, params);
   }
   
   /**
    * Add a templated FacesMessage that will be used
    * the next time a page is rendered.
    */
   public void add(Severity severity, String messageTemplate, Object... params)
   {
      addToTasks(severity, null, messageTemplate, params);
   }
   
   /**
    * Add a templated FacesMessage to a particular JSF control
    * @param id a JSF component id
    */
   public void addToControl(String id, String messageTemplate, Object... params)
   {
      addToControl(id, FacesMessage.SEVERITY_INFO, messageTemplate, params);
   }
   
   /**
    * Add a templated FacesMessage to a particular JSF control
    * @param id a JSF component id
    */
   public void addToControl(String id, Severity severity, String messageTemplate, Object... params)
   {
      addToTasks(id, severity, null, messageTemplate, params);
   }
   
   /**
    * Add a templated FacesMessage by looking for the message
    * template in the resource bundle. 
    */
   public void addFromResourceBundle(String key, Object... params)
   {
      addFromResourceBundle(FacesMessage.SEVERITY_INFO, key, params);
   }
   
   /**
    * Add a templated FacesMessage by looking for the message
    * template in the resource bundle. 
    */
   public void addFromResourceBundle(Severity severity, String key, Object... params)
   {
      addFromResourceBundleOrDefault(severity, key, key, params);
   }
   
   /**
    * Add a templated FacesMessage to a particular component id by looking 
    * for the message template in the resource bundle. If it is missing, use
    * the given message template.
    */
   public void addFromResourceBundleOrDefault(String key, String defaultMessageTemplate, Object... params)
   {
      addFromResourceBundleOrDefault(FacesMessage.SEVERITY_INFO, key, defaultMessageTemplate, params);
   }

   /**
    * Add a templated FacesMessage to a particular component id by looking 
    * for the message template in the resource bundle. If it is missing, use
    * the given message template.
    */
   public void addFromResourceBundleOrDefault(Severity severity, String key, String defaultMessageTemplate, Object... params)
   {
      addToTasks(severity, key, defaultMessageTemplate, params);
   }

   /**
    * Add a templated FacesMessage to a particular component id by looking 
    * for the message template in the resource bundle. 
    */
   public void addToControlFromResourceBundle(String id, String key, Object... params)
   {
      addToControlFromResourceBundle(id, FacesMessage.SEVERITY_INFO, key, params);
   }
   
   /**
    * Add a templated FacesMessage to a particular component id by looking 
    * for the message template in the resource bundle. 
    */
   public void addToControlFromResourceBundle(String id, Severity severity, String key, Object... params)
   {
      addToControlFromResourceBundleOrDefault(id, severity, key, key, params);
   }
   
   /**
    * Add a templated FacesMessage to a particular component id by looking 
    * for the message template in the resource bundle. If it is missing, use
    * the given message template.
    */
   public void addToControlFromResourceBundleOrDefault(String id, String key, String defaultMessageTemplate, Object... params)
   {
      addToControlFromResourceBundleOrDefault(id, FacesMessage.SEVERITY_INFO, key, defaultMessageTemplate, params);
   }

   /**
    * Add a templated FacesMessage to a particular component id by looking 
    * for the message template in the resource bundle. If it is missing, use
    * the given message template.
    */
   public void addToControlFromResourceBundleOrDefault(String id, Severity severity, String key, String defaultMessageTemplate, Object... params)
   {
      addToTasks(id, severity, key, defaultMessageTemplate, params);
   }

   private static String getBundleMessage(String key, String defaultMessageTemplate)
   {
      String messageTemplate = defaultMessageTemplate;
      if ( key!=null )
      {
         ResourceBundle resourceBundle = SeamResourceBundle.getBundle();
         if ( resourceBundle!=null ) 
         {
            try
            {
               String bundleMessage = resourceBundle.getString(key);
               if (bundleMessage!=null) messageTemplate = bundleMessage;
            }
            catch (MissingResourceException mre) {} //swallow
         }
      }
      return messageTemplate;
   }

   public void add(InvalidValue[] ivs)
   {
      for (InvalidValue iv: ivs)
      {
         add(iv);
      }
   }
   
   public void addToControls(InvalidValue[] ivs)
   {
      for (InvalidValue iv: ivs)
      {
         addToControl(iv);
      }
   }
   
   public void add(InvalidValue iv)
   {
      add( FacesMessage.SEVERITY_WARN, iv.getMessage() );
   }
   
   public void addToControl(InvalidValue iv)
   {
      addToControl( iv.getPropertyName(), iv );
   }
   
   public void addToControl(String id, InvalidValue iv)
   {
      addToControl( id, FacesMessage.SEVERITY_WARN, iv.getMessage() );
   }
   
   public static FacesMessage createFacesMessage(Severity severity, String messageTemplate, Object... params)
   {
      return new FacesMessage( severity, Interpolator.instance().interpolate(messageTemplate, params), null );
   }
   
   public static FacesMessage createFacesMessage(Severity severity, String key, String defaultMessageTemplate, Object... params)
   {
      String message = getBundleMessage(key, defaultMessageTemplate);
      if ( !Strings.isEmpty(message) )
      {
         return createFacesMessage( severity, message, params );
      }
      else
      {
         return null;
      }
   }
   
   private String getClientId(String id)
   {
      FacesContext facesContext = FacesContext.getCurrentInstance();
      return getClientId( facesContext.getViewRoot(), id, facesContext);
   }

   private static String getClientId(UIComponent component, String id, FacesContext facesContext)
   {
      String componentId = component.getId();
      if (componentId!=null && componentId.equals(id))
      {
         return component.getClientId(facesContext);
      }
      else
      {
         Iterator iter = component.getFacetsAndChildren();
         while ( iter.hasNext() )
         {
            UIComponent child = (UIComponent) iter.next();
            String clientId = getClientId(child, id, facesContext);
            if (clientId!=null) return clientId;
         }
         return null;
      }
   }
   
   private List<Runnable> getTasks()
   {
      if (tasks==null)
      {
         tasks = new ArrayList<Runnable>();
      }
      return tasks;
   }
  
   private void addToTasks(final Severity severity, final String key, final String messageTemplate, final Object... params)
   {
      getTasks().add( new Runnable() {
         public void run() { add( createFacesMessage(severity, key, messageTemplate, params) ); }
      } );
   }
      
   private void addToTasks(final String id, final Severity severity, final String key, final String messageTemplate, final Object... params)
   {
      getTasks().add( new Runnable() {
         public void run() { addToControl( id, createFacesMessage(severity, key, messageTemplate, params) ); }
      } );
   }

   public static FacesMessages instance()
   {
      if ( !Contexts.isConversationContextActive() )
      {
         throw new IllegalStateException("No active conversation context");
      }
      return (FacesMessages) Component.getInstance(FacesMessages.class, ScopeType.CONVERSATION);
   }
      
}
