package hirondelle.web4j.model;

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.request.Formats;
import hirondelle.web4j.request.RequestParameter;
import hirondelle.web4j.ui.translate.Translator;
import hirondelle.web4j.util.Args;
import hirondelle.web4j.util.EscapeChars;
import hirondelle.web4j.util.Util;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 Informative message presented to the end user.
   
 <P>This class exists in order to hide the difference between <em>simple</em> and 
 <em>compound</em> messages. 
 
 <P><a name="SimpleMessage"></a><b>Simple Messages</b><br>
 Simple messages are a single {@link String}, such as <tt>'Item deleted successfully.'</tt>. 
 They are created using {@link #forSimple(String)}.
 
 <P><a name="CompoundMessage"></a><b>Compound Messages</b><br>
 Compound messages are made up of several parts, and have parameters. They are created 
 using {@link #forCompound(String, Object...)}. A compound message
 is usually implemented in Java using {@link java.text.MessageFormat}. <span class="highlight">
 However, <tt>MessageFormat</tt> is not used by this class, to avoid the following issues </span>:
<ul>
 <li> the dreaded apostrophe problem. In <tt>MessageFormat</tt>, the apostrophe is a special 
 character, and must be escaped. This is highly unnatural for translators, and has been a 
 source of continual, bothersome errors. (This is the principal reason for not 
 using <tt>MessageFormat</tt>.)
 <li>the <tt>{0}</tt> placeholders start at <tt>0</tt>, not <tt>1</tt>. Again, this is 
 unnatural for translators.
 <li>the number of parameters cannot exceed <tt>10</tt>. (Granted, it is not often 
 that a large number of parameters are needed, but there is no reason why this 
 restriction should exist.)
 <li>in general, {@link MessageFormat} is rather complicated in its details.
</ul> 
 
 <P><a name="CustomFormat"></a><b>Format of Compound Messages</b><br>
 This class defines an alternative format to that defined by {@link java.text.MessageFormat}. 
 For example,  
 <PRE>
  "At this restaurant, the _1_ meal costs _2_."
  "On _2_, I am going to Manon's place to see _1_."
 </PRE>
 Here, 
<ul>
 <li>the placeholders appear as <tt>_1_</tt>, <tt>_2_</tt>, and so on. 
 They start at <tt>1</tt>, not <tt>0</tt>, and have no upper limit. There is no escaping 
 mechanism to allow the placeholder text to appear in the message 'as is'. The <tt>_i_</tt>
 placeholders stand for an <tt>Object</tt>, and carry no format information.
 <li>apostrophes can appear anywhere, and do not need to be escaped.
 <li>the formats applied to the various parameters are taken from {@link Formats}. 
 If the default formatting applied by {@link Formats} is not desired, then the caller 
 can always manually format the parameter as a {@link String}. (The {@link Translator} may be used when 
 a different pattern is needed for different Locales.)
 <li>the number of parameters passed at runtime must match exactly the number of <tt>_i_</tt> 
 placeholders
</ul>
 
 <P><b>Multilingual Applications</b><br>
 Multilingual applications will need to ensure that messages can be successfully translated when 
 presented in JSPs. In particular, some care must be exercised to <em>not</em> create 
 a <em>simple</em> message out of various pieces of data when a <em>compound</em> message  
 should be used instead. See {@link #getMessage(Locale, TimeZone)}.
 As well, see the <a href="../ui/translate/package-summary.html">hirondelle.web4j.ui.translate</a> 
 package for more information, in particular the 
 {@link hirondelle.web4j.ui.translate.Messages} tag used for rendering <tt>AppResponseMessage</tt>s, 
 even in single language applications.
 
  <P><b>Serialization</b><br>
  This class implements {@link Serializable} to allow messages stored in session scope to 
  be transferred over a network, and thus survive a failover operation. 
  <i>However, this class's implementation of Serializable interface has a minor defect.</i>  
  This class accepts <tt>Object</tt>s as parameters to messages. These objects almost always represent 
  data - String, Integer, Id, DateTime, and so on, and all such building block classes are Serializable. 
  If, however, the caller passes an unusual message parameter object which is not Serializable, then the 
  serialization of this object (if it occurs), will fail. 
  
  <P>The above defect will likely not be fixed since it has large ripple effects, and would seem to cause 
  more problems than it would solve. In retrospect, this the message parameters passed to 
  {@link #forCompound(String, Object[])} should likely have been typed as Serializable, not Object. 
*/
public final class AppResponseMessage implements Serializable {

  /**
   <a href="#SimpleMessage">Simple message</a> having no parameters.
   <tt>aSimpleText</tt> must have content.
  */
  public static AppResponseMessage forSimple(String aSimpleText){
    return new AppResponseMessage(aSimpleText, NO_PARAMS);
  }

  /**
   <a href="#CompoundMessage">Compound message</a> having parameters.
   
   <P><tt>aPattern</tt> follows the <a href="#CustomFormat">custom format</a> defined by this class.
   {@link Formats#objectToTextForReport} will be used to format all parameters.
    
   @param aPattern must be in the style of the <a href="#CustomFormat">custom format</a>, and 
   the number of placeholders must match the number of items in <tt>aParams</tt>. 
   @param aParams must have at least one member; all members must be non-null, but may be empty 
   {@link String}s.
  */
  public static AppResponseMessage forCompound(String aPattern, Object... aParams){
    if ( aParams.length < 1 ){
      throw new IllegalArgumentException("Compound messages must have at least one parameter.");
    }
    return new AppResponseMessage(aPattern, aParams);
  }
  
  /**
   Return either the 'simple text' or the <em>formatted</em> pattern with all parameter data rendered, 
   according to which factory method was called. 
   
   <P>The configured {@link Translator} is used to localize 
   <ul>
   <li>the text passed to {@link #forSimple(String)} 
   <li>the pattern passed to {@link #forCompound(String, Object...)}
   <li>any {@link hirondelle.web4j.request.RequestParameter} parameters passed to {@link #forCompound(String, Object...)}
   are localized by using {@link Translator} on the return value of {@link RequestParameter#getName()} 
   (This is intended for displaying localized versions of control names.) 
   </ul>
    
   <P>It is highly recommended that this method be called <em>late</em> in processing, in a JSP.
   
   <P>The <tt>Locale</tt> should almost always come from 
   {@link hirondelle.web4j.BuildImpl#forLocaleSource()}.
   The <tt>aLocale</tt> parameter is always required, even though there are cases when it 
   is not actually used to render the result. 
  */
  public String getMessage(Locale aLocale, TimeZone aTimeZone){
    String result = null;
    Translator translator = BuildImpl.forTranslator();
    Formats formats = new Formats(aLocale, aTimeZone);
    if( fParams.isEmpty() ){
      result = translator.get(fText, aLocale);
    }
    else {
      String localizedPattern = translator.get(fText, aLocale);
      List<String> formattedParams = new ArrayList<String>();
      for (Object param : fParams){
        if ( param instanceof RequestParameter ){
          RequestParameter reqParam = (RequestParameter)param;
          String translatedParamName = translator.get(reqParam.getName(), aLocale); 
          formattedParams.add( translatedParamName ); 
        }
        else {
          //this will escape any special HTML chars in params :
          formattedParams.add( formats.objectToTextForReport(param).toString() );
        }
      }
      result = populateParamsIntoCustomFormat(localizedPattern, formattedParams);
    }
    return result;
  }
  
  /**
   Return an unmodifiable <tt>List</tt> corresponding to the <tt>aParams</tt> passed to 
   the constructor.
   
   <P>If no parameters are being used, then return an empty list.
  */
  public List<Object> getParams(){
    return Collections.unmodifiableList(fParams);  
  }

  /**
   Return either the 'simple text' or the pattern, according to which factory method 
   was called. Typically, this method is <em>not</em> used to present text to the user (see {@link #getMessage}). 
  */
  @Override public String toString(){
    return fText;
  }
  
  @Override public boolean equals(Object aThat){
    Boolean result = ModelUtil.quickEquals(this, aThat);
    if ( result == null ){
      AppResponseMessage that = (AppResponseMessage) aThat;
      result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
    }
    return result;    
  }
  
  @Override public int hashCode(){
    return ModelUtil.hashCodeFor(getSignificantFields());
  }
  
  // PRIVATE 
  
  /** Holds either the simple text, or the custom pattern.  */
  private final String fText;
  
  /** List of Objects holds the parameters. Empty List if no parameters used.  */
  private final List<Object> fParams;
  
  private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("_(\\d)+_");
  private static final Object[] NO_PARAMS = new Object[0];
  private static final Logger fLogger = Util.getLogger(AppResponseMessage.class);
  
  private static final long serialVersionUID = 1000L;
  
  private AppResponseMessage(String aText, Object... aParams){
    fText = aText;
    fParams = Arrays.asList(aParams);
    validateState();
  }
  
  private void validateState(){
    Args.checkForContent(fText);
    if (fParams != null && fParams.size() > 0){
      for(Object item : fParams){
        if ( item == null ){
          throw new IllegalArgumentException("Parameters to compound messages must be non-null.");
        }
      }
    }
  }

  /**
   @param aFormattedParams contains Strings ready to be placed in to the pattern. The index <tt>i</tt> of the 
   List matches the <tt>_i_</tt> placeholder. The size of aFormattedParams must match the number of 
   placeholders.
  */
  private String populateParamsIntoCustomFormat(String aPattern, List<String> aFormattedParams){
    StringBuffer result = new StringBuffer();
    fLogger.finest("Populating " + Util.quote(aPattern) + " with params " + Util.logOnePerLine(aFormattedParams));
    Matcher matcher = PLACEHOLDER_PATTERN.matcher(aPattern);
    int numMatches = 0;
    while ( matcher.find() ) {
      ++numMatches;
      if(numMatches > aFormattedParams.size()){
        String message = "The number of placeholders exceeds the number of available parameters (" + aFormattedParams.size() + ")";
        fLogger.severe(message);
        throw new IllegalArgumentException(message);
      }
      matcher.appendReplacement(result, getReplacement(matcher, aFormattedParams));
    }
    if(numMatches < aFormattedParams.size()){
      String message = "The number of placeholders (" + numMatches + ") is less than the number of available parameters (" + aFormattedParams.size() + ")";
      fLogger.severe(message);
      throw new IllegalArgumentException(message);
    }
    matcher.appendTail(result);
    return result.toString();    
  }
  
  private String getReplacement(Matcher aMatcher, List<String> aFormattedParams){
    String result = null;
    String digit = aMatcher.group(1);
    int idx = Integer.parseInt(digit);
    if(idx <= 0){
      throw new IllegalArgumentException("Placeholder digit should be 1,2,3... but takes value " + idx);
    }
    if(idx > aFormattedParams.size()){
      throw new IllegalArgumentException("Placeholder index for _" + idx + "_ exceeds the number of available parameters (" + aFormattedParams.size() + ")");
    }
    result = aFormattedParams.get(idx - 1);
    return EscapeChars.forReplacementString(result);
  }
  
  private Object[] getSignificantFields(){
    return new Object[] {fText, fParams};
  }
  
   /**
    Always treat de-serialization as a full-blown constructor, by validating the final state of the deserialized object.
   */
   private void readObject(ObjectInputStream aInputStream) throws ClassNotFoundException, IOException {
     aInputStream.defaultReadObject();
     validateState();
  }
}
