package hirondelle.web4j.ui.tag;

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

import java.util.*;
import java.util.logging.Logger;

/**
 Custom tag to display a {@link DateTime} 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 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, with the default format defined by {@link DateConverter} :
<PRE>{@code 
<w:showDateTime/>
}</PRE>

 <P>Display a specific date object, present in any scope :
<PRE>&lt;w:showDateTime <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:showDateTime name="lunchDate"/>
}</PRE>
 
 <P>Display with a non-default date format : 
<PRE>&lt;w:showDateTime name="lunchDate" <a href="#setPattern(java.lang.String)">pattern</a>="YYYY-MM-DD"/&gt;</PRE>
 
 <P>Display with a non-default date format sensitive to {@link Locale} :
<PRE>&lt;w:showDateTime name="lunchDate" <a href="#setPatternKey(java.lang.String)">patternKey</a>="next.visit.lunch.date"/&gt;</PRE>
 
 <P>Suppress the display of midnight, using a pipe-separated list of 'midnights' :
<PRE>&lt;w:showDateTime name="lunchDate" <a href="#setSuppressMidnight(java.lang.String)">suppressMidnight</a>="12:00 AM|00 h 00"/&gt;</PRE>
*/
public final class ShowDateTime extends TagHelper {

  /**
   Optionally set the name of a {@link DateTime} object already present in some scope. 
   Searches from narrow to wide scope to find the corresponding object.
    
   <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 ) {
      handleErrorCondition("Cannot find object named " + Util.quote(aName) + " in any scope.");
    }
    else {
      if (object instanceof DateTime){
        fTarget = Target.OBJECT_DATE_TIME;
        fDateTime = (DateTime)object;
      }
      else {
        handleErrorCondition(
          "Object named " + Util.quote(aName) + " is not a hirondelle.web4j.model.DateTime. It is a "  +
          object.getClass().getName()
        ); 
      }
    }
  }
  
  /**
   Optionally set the format for rendering the date.
   
   <P>Setting this attribute will override the default format used by  
   {@link DateConverter}.
   
   <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 aFormat has content, and is a date format suitable for the <tt>format</tt> .
   methods of {@link DateTime}. 
  */
  public void setPattern(String aFormat){
    checkForContent("Pattern", aFormat);
    fFormat = aFormat;
  }

  /**
   Optionally set the format for rendering the date according to {@link Locale}.
   
   <P>Setting this attribute will override the default format used by  
   {@link DateConverter}.
   
   <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>' (for a <tt>Date</tt>) or <tt>YYYY-MM-DD</tt> (for a {@link DateTime}). 
   
   <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 format suitable for the <tt>format</tt> methods of {@link DateTime}. 
  */
  public void setPatternKey(String aFormatKey){
    checkForContent("PatternKey", aFormatKey);
    fFormatKey = aFormatKey;
  }
   
  /**
   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(fFormatKey != null && fFormat != null){
      handleErrorCondition("Cannot specify both 'pattern' and 'patternKey' attributes at the same time.");
    }
  }
   
  @Override protected String getEmittedText(String aOriginalBody) {
    String result = EMPTY_STRING;
    if( fFoundError ) return result;
    
    if(Target.CURRENT_DATE_TIME == fTarget){
      fDateTime = DateTime.now(BuildImpl.forTimeZoneSource().get(getRequest()));
    }
    result = formatDateTime();
    
    if( hasMidnightStyles() ) {
      result = removeMidnightIfPresent(result);
    }
    return result;
  }

  // PRIVATE 
  private DateTime fDateTime; //defaults to now; cannot init here, since request does not exist yet
  
  /** The item to be formatted. */
  private enum Target {
    /** If no object is specified at all, then the current date time is assumed. */
    CURRENT_DATE_TIME, 
    OBJECT_DATE_TIME, 
  }
  private Target fTarget = Target.CURRENT_DATE_TIME; //default
  
  private String fFormat;
  private String fFormatKey;
  private List<String> fMidnightStyles = new ArrayList<String>();
  
  /** Flags presence of error conditions. If true, then only an empty String is emitted. */
  private boolean fFoundError;
  
  private static final Logger fLogger = Util.getLogger(ShowDate.class);

  private String formatDateTime(){
    String result = "";
    Locale locale = getLocale();
    if(fFormat == null && fFormatKey == null){
      DateConverter dateConverter = BuildImpl.forDateConverter();
      result = dateConverter.formatEyeFriendlyDateTime(fDateTime, locale);
    }
    else if(fFormat != null && fFormatKey == null){
      result = fDateTime.format(fFormat, getLocale());
    }
    else if(fFormat == null && fFormatKey != null){
      Translator translator = BuildImpl.forTranslator();
      String localPattern = translator.get(fFormatKey, locale);
      result = fDateTime.format(localPattern, locale);
    }
    return result;
  }
  
  private Locale getLocale(){
    return BuildImpl.forLocaleSource().get(getRequest());
  }
  
  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);
  }
  
  private void handleErrorCondition(String aMessage){
    fFoundError = true;
    String message = aMessage + " Page Name : " + getPageName();
    fLogger.severe(message);
  }
}
