package hirondelle.web4j.ui.tag;

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.util.TimeSource;
import hirondelle.web4j.request.DateConverter;
import hirondelle.web4j.request.TimeZoneSource;
import hirondelle.web4j.ui.translate.Translator;
import hirondelle.web4j.util.Util;
import static hirondelle.web4j.util.Consts.NOT_FOUND;
import static hirondelle.web4j.util.Consts.EMPTY_STRING;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Logger;

/**
 Display a {@link Date} in a particular format.
 
 <P>This class uses:
 <ul>
   <li>{@link hirondelle.web4j.request.LocaleSource} to determine the Locale associated with the current request
   <li>{@link TimeZoneSource} for the time zone associated with the current request
   <li>{@link DateConverter} to format the given date
   <li>{@link Translator} for localizing the argument passed to {@link #setPatternKey}.
 </ul>
 
 <h3>Examples</h3>
 <P>Display the current system date :
<PRE>{@code 
<w:showDate/>
}</PRE>

 <P>Display a specific <tt>Date</tt> object, present in any scope :
<PRE>&lt;w:showDate <a href="#setName(java.lang.String)">name</a>="dateOfBirth"/&gt;</PRE>

 <P>Display a date returned by some object in scope :
<PRE>{@code 
<c:set value="${visit.lunchDate}" var="lunchDate"/>
<w:showDate name="lunchDate"/>
}</PRE>
 
 <P>Display with a non-default date format : 
<PRE>&lt;w:showDate name="lunchDate" <a href="#setPattern(java.lang.String)">pattern</a>="E, MMM dd"/&gt;</PRE>
 
 <P>Display with a non-default date format sensitive to {@link Locale} :
<PRE>&lt;w:showDate name="lunchDate" <a href="#setPatternKey(java.lang.String)">patternKey</a>="next.visit.lunch.date"/&gt;</PRE>
 
 <P>Display in a specific time zone :
<PRE>&lt;w:showDate name="lunchDate" <a href="#setTimeZone(java.lang.String)">timeZone</a>="America/Montreal"/&gt;</PRE>
 
 <P>Suppress the display of midnight, using a pipe-separated list of 'midnights' :
<PRE>&lt;w:showDate name="lunchDate" <a href="#setSuppressMidnight(java.lang.String)">suppressMidnight</a>="12:00 AM|00 h 00"/&gt;</PRE>
*/
public final class ShowDate extends TagHelper {
  
  /**
   Optionally set the name of a {@link Date} object already present in some scope. 
   Searches from narrow to wide scope to find the corresponding <tt>Date</tt>.
    
   <P>If this method is called and no corresponding object can be found using the 
   given name, then this tag will emit an empty String. 
   
   <P>If this method is not called at all, then the current system date is used, as 
   defined by the configured {@link TimeSource}.
  
   @param aName must have content.
  */
  public void setName(String aName){
    checkForContent("Name", aName);
    Object object = getPageContext().findAttribute(aName);
    if ( object == null ) {
      fDateObjectMissing = true;
      fLogger.fine("Cannot find object named " + Util.quote(aName) + " in any scope. Page Name : " + getPageName());
    }
    else {
      if ( ! (object instanceof Date) ) {
        throw new IllegalArgumentException(
          "Object named " + Util.quote(aName) + " is not a java.util.Date. Page Name :" + getPageName() 
        );
      }
      fDate = (Date)object;
    }
  }
   
  /**
   Optionally set the format for rendering the date.
   
   <P>Setting this attribute will override the default format of 
   {@link DateConverter#formatEyeFriendly(Date, Locale, TimeZone)}.
   
   <P><span class="highlight">Calling this method is suitable only when 
   the date format does not depend on {@link Locale}.</span> Otherwise,  
   {@link #setPatternKey(String)} must be used instead. 
  
   <P>Only one of {@link #setPattern(String)} and {@link #setPatternKey(String)}
   can be called at a time.
   
   @param aDateFormat has content, and is in the form expected by 
   {@link java.text.SimpleDateFormat}.
  */
  public void setPattern(String aDateFormat){
    checkForContent("Pattern", aDateFormat);
    fDateFormat = new SimpleDateFormat(aDateFormat, getLocale());
  }

  /**
   Optionally set the format for rendering the date according to {@link Locale}.
   
   <P>Setting this attribute will override the default format of 
   {@link DateConverter#formatEyeFriendly(Date, Locale, TimeZone)}.
   
   <P>This method uses a {@link Translator} to look up the "real" 
   date pattern to be used, according to the {@link Locale} returned 
   by {@link hirondelle.web4j.request.LocaleSource}.
   
   <P>For example, if the value '<tt>format.next.lunch.date</tt>' is passed to 
   this method, then that value is passed to a {@link Translator}, which will return 
   a pattern specific to the {@link Locale} attached to this request, such as 
   '<tt>EEE, dd MMM</tt>'. 
   
   <P>Only one of {@link #setPattern(String)} and {@link #setPatternKey(String)}
   can be called at a time.
   
   @param aFormatKey has content, and, when passed to {@link Translator}, will 
   return a date pattern in the form expected by {@link java.text.SimpleDateFormat}.
  */
  public void setPatternKey(String aFormatKey){
    checkForContent("PatternKey", aFormatKey);
    fDateFormatKey = aFormatKey;
  }
   
  /**
   Optionally set the {@link TimeZone} for formatting the date.
  
   <P>If this attribute is not set, then {@link TimeZoneSource} is used.
   
   @param aCustomTimeZone in the style expected by {@link TimeZone#getTimeZone(java.lang.String)}. 
   If the format is not in the expected style, then UTC is used (same as Greenwich Mean Time). 
  */
  public void setTimeZone(String aCustomTimeZone){
    fCustomTimeZone = TimeZone.getTimeZone(aCustomTimeZone);
  }
   
  /**
   Optionally suppress the display of midnight. 
   
   <P>For example, set this attribute to '<tt>00:00:00</tt>' to force '<tt>1999-12-31 00:00:00</tt>' to display as 
   <tt>1999-12-31</tt>, without the time.
   
   <P>If this attribute is set, and if any of the <tt>aMidnightStyles</tt> is found <em>anywhere</em> in the formatted date, 
   then the formatted date is truncated, starting from the given midnight style. That is, all text appearing after 
   the midnight style is removed, including any time zone information. (Then the result is trimmed.)
   
   @param aMidnightStyles is pipe-separated list of Strings which denote the possible forms of 
   midnight. Example value : '00:00|00 h 00'.
  */
  public void setSuppressMidnight(String aMidnightStyles){
    StringTokenizer parser = new StringTokenizer(aMidnightStyles, "|");
    while ( parser.hasMoreElements() ){
      fMidnightStyles = new ArrayList<String>();
      String midnightStyle = (String)parser.nextElement();
      if( Util.textHasContent(midnightStyle)){
        fMidnightStyles.add(midnightStyle.trim());
      }
    }
    fLogger.fine("Midnight styles: " + fMidnightStyles);
  }
   
  protected void crossCheckAttributes() {
    if(fDateFormatKey != null && fDateFormat != null){
      String message = "Cannot specify both 'pattern' and 'patternKey' attributes at the same time. Page Name : " + getPageName();
      fLogger.severe(message);
      throw new IllegalArgumentException(message);
    }
  }
   
  @Override protected String getEmittedText(String aOriginalBody) {
    String result = EMPTY_STRING;
    if( fDateObjectMissing ) return result;
    
    Locale locale = getLocale();
    TimeZone timeZone = getTimeZone();
    if(fDateFormat == null && fDateFormatKey == null){
      DateConverter dateConverter = BuildImpl.forDateConverter();
      result = dateConverter.formatEyeFriendly(fDate, locale, timeZone);
    }
    else if(fDateFormat != null && fDateFormatKey == null){
      adjustForTimeZone(fDateFormat, timeZone);
      result = fDateFormat.format(fDate);
    }
    else if(fDateFormat == null && fDateFormatKey != null){
      Translator translator = BuildImpl.forTranslator();
      String localPattern = translator.get(fDateFormatKey, locale);
      DateFormat localDateFormat = new SimpleDateFormat(localPattern, locale);
      adjustForTimeZone(localDateFormat, timeZone);
      result = localDateFormat.format(fDate);
    }
    else {
      throw new IllegalArgumentException("Cannot specify both 'pattern' and 'patternKey' attributes at the same time. Page Name : " + getPageName());
    }
    if( hasMidnightStyles() ) {
      result = removeMidnightIfPresent(result);
    }
    return result;
  }
  
  // PRIVATE 
  private Date fDate = new Date(BuildImpl.forTimeSource().currentTimeMillis()); //defaults to 'now'
  
  /** Flags if a named object is not found in any scope. */
  private boolean fDateObjectMissing = false;
  
  private DateFormat fDateFormat;
  private String fDateFormatKey;
  private TimeZone fCustomTimeZone;
  private List<String> fMidnightStyles = new ArrayList<String>();
  private static final Logger fLogger = Util.getLogger(ShowDate.class);
  
  private void adjustForTimeZone(DateFormat aFormat, TimeZone aTimeZone){
    aFormat.setTimeZone(aTimeZone);
  }
   
  private Locale getLocale(){
    return BuildImpl.forLocaleSource().get(getRequest());
  }
  
  private TimeZone getTimeZone(){
    TimeZone result = null;
    if( fCustomTimeZone != null ){
      result = fCustomTimeZone;
    }
    else {
      TimeZoneSource timeZoneSource = BuildImpl.forTimeZoneSource();
      result = timeZoneSource.get(getRequest());
    }
    return result;
  }
   
  private boolean hasMidnightStyles(){
    return ! fMidnightStyles.isEmpty();
  }
   
  private String removeMidnightIfPresent(String aFormattedDate){
    String result = aFormattedDate;
    for(String midnightStyle : fMidnightStyles){
      if ( hasMidnight(aFormattedDate, midnightStyle) ){
        result = removeMidnight(aFormattedDate, midnightStyle);        
      }
    }
    return result.trim();
  } 
   
  private boolean hasMidnight(String aFormattedDate, String aMidnightStyle){
    return aFormattedDate.indexOf(aMidnightStyle) != NOT_FOUND; 
  }
   
  private String removeMidnight(String aFormattedDate, String aMidnightStyle){
    int midnight = aFormattedDate.indexOf(aMidnightStyle);
    return aFormattedDate.substring(0,midnight);
  }
}
