package hirondelle.web4j.request;

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.model.Code;
import hirondelle.web4j.model.DateTime;
import hirondelle.web4j.model.Decimal;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.readconfig.Config;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.util.Consts;
import hirondelle.web4j.util.Util;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Pattern;

/**
 Standard display formats for the application.
 
 <P>The formats used by this class are <em>mostly</em> configured in 
 <tt>web.xml</tt>, and are read by this class upon startup. 
 <span class="highlight">See the <a href='http://www.web4j.com/UserGuide.jsp#ConfiguringWebXml'>User Guide</tt> 
 for more information.</span>
 
 <P>Most formats are localized using the {@link java.util.Locale} passed to this object. 
 See {@link LocaleSource} for more information.  

 <P>These formats are intended for implementing standard formats for display of 
 data both in forms ({@link hirondelle.web4j.ui.tag.Populate}) and in 
 listings ({@link hirondelle.web4j.database.Report}).
 
 <P>See also {@link DateConverter}, which is also used by this class.
*/
public final class Formats {
  
  /**
   Construct with a {@link Locale} and {@link TimeZone} to be applied to non-localized patterns. 
   
   @param aLocale almost always comes from {@link LocaleSource}.
   @param aTimeZone almost always comes from {@link TimeZoneSource}. A defensive copy is made of 
   this mutable object.
  */
  public Formats(Locale aLocale, TimeZone aTimeZone){
    fLocale = aLocale;
    fTimeZone = TimeZone.getTimeZone(aTimeZone.getID()); //defensive copy
    fDateConverter = BuildImpl.forDateConverter();
  }
  
  /** Return the {@link Locale} passed to the constructor.  */
  public Locale getLocale(){
    return fLocale;
  }
  
  /** Return a TimeZone of the same id as the one passed to the constructor.  */
  public TimeZone getTimeZone(){
    return TimeZone.getTimeZone(fTimeZone.getID());
  }
  
  /** Return the format in which {@link BigDecimal}s and {@link Decimal}s are displayed in a form.  */
  public DecimalFormat getBigDecimalDisplayFormat(){
    return getDecimalFormat(fConfig.getBigDecimalDisplayFormat());
  }
  
  /**
   Return the regular expression for validating the format of numeric amounts input by the user, having a 
   possible decimal portion, with any number of decimals.
    
   <P>The returned {@link Pattern} is controlled by a setting in <tt>web.xml</tt>, 
   for decimal separator(s). It is suitable for both {@link Decimal} and {@link BigDecimal} values.
   This item is not affected by a {@link Locale}. 
   
   <P>See <tt>web.xml</tt> for more information. 
  */
  public Pattern getDecimalInputFormat(){
    return fConfig.getDecimalInputPattern();
  }
  
  /** Return the format in which integer amounts are displayed in a report.  */
  public DecimalFormat getIntegerReportDisplayFormat(){
    return getDecimalFormat(fConfig.getIntegerDisplayFormat());
  }
  
  /**
   Return the text used to render boolean values in a report.
   
   <P>The return value does not depend on {@link Locale}.
  */
  public static String getBooleanDisplayText(Boolean aBoolean){
    Config config = new Config();
    return aBoolean ? config.getBooleanTrueDisplayFormat() : config.getBooleanFalseDisplayFormat();
  }

  /**
   Return the text used to render empty or <tt>null</tt> values in a report.
   
   <P>The return value does not depend on {@link Locale}. See <tt>web.xml</tt> for more information.
  */
  public static String getEmptyOrNullText() {
    return new Config().getEmptyOrNullDisplayFormat();
  }
  
  /**
   Translate an object into text, suitable for presentation <em>in an HTML form</em>.
   
   <P>The intent of this method is to return values matching those POSTed during form submission, 
   not the visible text presented to the user. 
   
   <P>The returned text is not escaped in any way.
   That is, <em>if special characters need to be escaped, the caller must perform the escaping</em>.
   
   <P>Apply these policies in the following order :
  <ul>
   <li>if <tt>null</tt>, return an empty <tt>String</tt>
   <li>if a {@link DateTime}, apply {@link DateConverter#formatEyeFriendlyDateTime(DateTime, Locale)}
   <li>if a {@link Date}, apply {@link DateConverter#formatEyeFriendly(Date, Locale, TimeZone)}
   <li>if a {@link BigDecimal}, display in the form of {@link BigDecimal#toString}, with 
   one exception : the decimal separator will be as configured in <tt>web.xml</tt>. 
   (If the setting for the decimal separator allows for <em>both</em> a period and a comma, 
   then a period is used.)
   <li>if a {@link Decimal}, display the amount only, using the same rendering as for <tt>BigDecimal</tt> 
   <li>if a {@link TimeZone}, return {@link TimeZone#getID()}
   <li>if a {@link Code}, return {@link Code#getId()}.toString()
   <li>if a {@link Id}, return {@link Id#getRawString()}
   <li>if a {@link SafeText}, return {@link SafeText#getRawString()}
   <li>otherwise, return <tt>aObject.toString()</tt>
  </ul>
   
   <P>If <tt>aObject</tt> is a <tt>Collection</tt>, then the caller must call 
   this method for every element in the <tt>Collection</tt>.
  
   @param aObject must not be a <tt>Collection</tt>.
  */
  public String objectToText(Object aObject) {
    String result = null;
    if ( aObject == null ){
      result = Consts.EMPTY_STRING;
    }
    else if ( aObject instanceof DateTime ){
      DateTime dateTime = (DateTime)aObject;
      result = fDateConverter.formatEyeFriendlyDateTime(dateTime, fLocale);
    }
    else if ( aObject instanceof Date ){
      Date date = (Date)aObject;
      result = fDateConverter.formatEyeFriendly(date, fLocale, fTimeZone);
    }
    else if ( aObject instanceof BigDecimal ){
      BigDecimal amount = (BigDecimal)aObject;
      result = renderBigDecimal(amount);
    }
    else if ( aObject instanceof Decimal ){
      Decimal money = (Decimal)aObject;
      result = renderBigDecimal(money.getAmount());
    }
    else if ( aObject instanceof TimeZone ) {
      TimeZone timeZone = (TimeZone)aObject;
      result = timeZone.getID();
    }
    else if ( aObject instanceof Code ) {
      Code code = (Code)aObject;
      result = code.getId().getRawString();
    }
    else if ( aObject instanceof Id ) {
      Id id = (Id)aObject;
      result = id.getRawString();
    }
    else if ( aObject instanceof SafeText ) {
      //The Populate tag will safely escape all such text data.
      //To avoid double escaping, the raw form is returned.
      SafeText safeText = (SafeText)aObject;
      result = safeText.getRawString();
    }
    else {
      result = aObject.toString();
    }
    return result;
  }
  
  /**
   Translate an object into text suitable for direct presentation in a JSP.
   
   <P>In general, a report can be rendered in various ways: HTML, XML, plain text. 
   Each of these styles has different needs for escaping special characters. 
   This method returns a {@link SafeText}, which can escape characters in 
   various ways. 
   
   <P>This method applies the following policies to get the <em>unescaped</em> text :
   <P>
   <table border=1 cellpadding=3 cellspacing=0>
    <tr><th>Type</th> <th>Action</th></tr>
    <tr>   
     <td><tt>SafeText</tt></td> 
     <td>use {@link SafeText#getRawString()}</td>
    </tr>
    <tr>   
     <td><tt>Id</tt></td> 
     <td>use {@link Id#getRawString()}</td>
    </tr>
    <tr>   
     <td><tt>Code</tt></td> 
     <td>use {@link Code#getText()}.getRawString()</td>
    </tr>
    <tr>
     <td><tt>hirondelle.web4.model.DateTime</tt></td> 
     <td>apply {@link DateConverter#formatEyeFriendlyDateTime(DateTime, Locale)} </td>
    </tr>
    <tr>
     <td><tt>java.util.Date</tt></td> 
     <td>apply {@link DateConverter#formatEyeFriendly(Date, Locale, TimeZone)} </td>
    </tr>
    <tr>
     <td><tt>BigDecimal</tt></td> 
     <td>use {@link #getBigDecimalDisplayFormat} </td>
    </tr>
    <tr>
     <td><tt>Decimal</tt></td> 
     <td>use {@link #getBigDecimalDisplayFormat} on <tt>decimal.getAmount()</tt></td>
    </tr>
    <tr>
     <td><tt>Boolean</tt></td> 
     <td>use {@link #getBooleanDisplayText} </td>
    </tr>
    <tr>
     <td><tt>Integer</tt></td> 
     <td>use {@link #getIntegerReportDisplayFormat} </td>
    </tr>
    <tr>
     <td><tt>Long</tt></td> 
     <td>use {@link #getIntegerReportDisplayFormat} </td>
    </tr>
    <tr>   
     <td><tt>Locale</tt></td> 
     <td>use {@link Locale#getDisplayName(java.util.Locale)} </td>
    </tr>
    <tr>   
     <td><tt>TimeZone</tt></td> 
     <td>use {@link TimeZone#getDisplayName(boolean, int, java.util.Locale)} (with no daylight savings hour, and in the <tt>SHORT</tt> style </td>
    </tr>
    <tr>
     <td>..other...</td> 
     <td>
       use <tt>toString</tt>, and pass result to constructor of {@link SafeText}. 
     </td>
    </tr>
   </table>
  
   <P>In addition, the value returned by {@link #getEmptyOrNullText} is used if :
   <ul>
   <li><tt>aObject</tt> is itself <tt>null</tt>
   <li>the result of the above policies returns text which has no content
  </ul>
  */
  public SafeText objectToTextForReport(Object aObject) {
    String result = null;
    if ( aObject == null ){
      result = null;
    }
    else if (aObject instanceof SafeText){
      //it is odd to extract an identical object like this, 
      //but it safely avoids double escaping at the end of this method
      SafeText text = (SafeText) aObject;
      result = text.getRawString();
    }
    else if (aObject instanceof Id){
      Id id = (Id) aObject;
      result = id.getRawString();
    }
    else if (aObject instanceof Code){
      Code code = (Code) aObject;
      result = code.getText().getRawString();
    }
    else if (aObject instanceof String) {
      result = aObject.toString();
    }
    else if ( aObject instanceof DateTime ){
      DateTime dateTime = (DateTime)aObject;
      result = fDateConverter.formatEyeFriendlyDateTime(dateTime, fLocale);
    }
    else if ( aObject instanceof Date ){
      Date date = (Date)aObject;
      result = fDateConverter.formatEyeFriendly(date, fLocale, fTimeZone);
    }
    else if ( aObject instanceof BigDecimal ){
      BigDecimal amount = (BigDecimal)aObject;
      result = getBigDecimalDisplayFormat().format(amount.doubleValue());
    }
    else if ( aObject instanceof Decimal ){
      Decimal money = (Decimal)aObject;
      result = getBigDecimalDisplayFormat().format(money.getAmount().doubleValue());
    }
    else if ( aObject instanceof Boolean ){
      Boolean value = (Boolean)aObject;
      result = getBooleanDisplayText(value);
    }
    else if ( aObject instanceof Integer ) {
      Integer value = (Integer)aObject;
      result = getIntegerReportDisplayFormat().format(value);
    }
    else if ( aObject instanceof Long ) {
      Long value = (Long)aObject;
      result = getIntegerReportDisplayFormat().format(value.longValue());
    }
    else if ( aObject instanceof Locale ) {
      Locale locale = (Locale)aObject;
      result = locale.getDisplayName(fLocale);
    }
    else if ( aObject instanceof TimeZone ) {
      TimeZone timeZone = (TimeZone)aObject;
      result = timeZone.getDisplayName(false, TimeZone.SHORT, fLocale);
    }
    else {
      result = aObject.toString();
    }
    //ensure that all empty results have configured content
    if ( ! Util.textHasContent(result) ) {
      result = fConfig.getEmptyOrNullDisplayFormat();
    }
    return new SafeText(result);
  }
  
  // PRIVATE
  private final Locale fLocale;
  private final TimeZone fTimeZone;
  private final DateConverter fDateConverter;
  private Config fConfig = new Config();
 
  private DecimalFormat getDecimalFormat(String aFormat){
    DecimalFormat result = null;
    NumberFormat format = NumberFormat.getNumberInstance(fLocale);
    if (format instanceof DecimalFormat){
      result = (DecimalFormat)format;
    }
    else {
      throw new AssertionError();
    }
    result.applyPattern(aFormat);
    return result;
  }
  
  /**
   Return the pattern applicable to numeric input of a number with a possible decimal portion. 
  */
  private String replacePeriodWithComma(String aValue){
    return aValue.replace(".", ",");
  }
  
  private String renderBigDecimal(BigDecimal aBigDecimal){
    String result = aBigDecimal.toPlainString();
    if( "COMMA".equalsIgnoreCase(fConfig.getDecimalSeparator()) ){
      result = replacePeriodWithComma(result);
    }
    return result;
  }
  
  /** Informal test harness.   */
  private static void main(String... args){
    Formats formats = new Formats(Locale.CANADA, TimeZone.getTimeZone("Canada/Atlantic"));
    System.out.println("en_fr: " + formats.objectToTextForReport(new Locale("en_fr")));
    System.out.println("Canada/Pacific: " + formats.objectToTextForReport(TimeZone.getTimeZone("Canada/Pacific")));
  }
}
