/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@ricochet.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2004 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.ui.dlg;

import java.io.IOException;
import java.text.*;
import java.util.*;
import org.apache.log4j.*;
import org.apache.regexp.*;
import org.w3c.dom.*;
import com.silverwrist.util.*;
import com.silverwrist.venice.except.*;
import com.silverwrist.venice.ui.*;
import com.silverwrist.venice.ui.helpers.HTMLRendering;
import com.silverwrist.venice.util.XMLLoader;

public class DateField implements DialogField
{
  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  public static final String TAGNAME = "date";

  private static Logger logger = Logger.getLogger(DateField.class);

  private static final int FLD_MONTH = 1;
  private static final int FLD_DAY = 2;
  private static final int FLD_YEAR = 3;

  private static REProgram YRANGE = null;
  private static REProgram YPLUS = null;
  private static REProgram YMINUS = null;
  private static REProgram YPLUSMINUS = null;
  private static REProgram YMINUSPLUS = null;
  private static REProgram YSOLO = null;

  private static Hashtable s_locale_ordering = new Hashtable();

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private String m_name;                  // field name (parameter name)
  private String m_caption;               // primary caption
  private String m_caption2;              // secondary caption
  private boolean m_required;             // is this field required?
  private boolean m_enabled;              // are we enabled?
  private int m_year_start;               // first year for dropdown
  private int m_year_stop;                // last year for dropdown
  private java.sql.Date m_value = null;   // selected date
  private int m_ndx_month = -1;           // index of selected month
  private int m_ndx_day = -1;             // index of selected day
  private int m_ndx_year = -1;            // index of selected year
  private boolean m_input_error = false;  // input error?

  /*--------------------------------------------------------------------------------
   * Constructors
   *--------------------------------------------------------------------------------
   */

  public DateField(String name, String caption, String caption2, boolean required, boolean enabled, int year_start,
		   int year_stop)
  {
    m_name = name;
    m_caption = caption;
    m_caption2 = caption2;
    m_required = required;
    m_enabled = enabled;
    m_year_start = year_start;
    m_year_stop = year_stop;

  } // end constructor

  public DateField(Element elt) throws ConfigException
  {
    XMLLoader loader = XMLLoader.get();
    m_name = loader.configGetAttribute(elt,"name");
    m_caption = loader.configGetAttribute(elt,"capt");
    m_caption2 = elt.getAttribute("capt2");
    DOMElementHelper h = new DOMElementHelper(elt);
    m_required = h.hasAttribute("required");
    m_enabled = !(h.hasAttribute("disabled"));
    String countdir = elt.getAttribute("direction");
    if (StringUtil.isStringEmpty(countdir))
      countdir = "down";
    else if (!(countdir.equalsIgnoreCase("up") || countdir.equalsIgnoreCase("down")))
      throw new ConfigException("<date/> direction= attribute must be 'up' or 'down'",elt);
    int y1, y2;
    String yearspec = elt.getAttribute("years");
    if (StringUtil.isStringEmpty(yearspec))
    { // use a 70 year span back from the current year
      Calendar cal = Calendar.getInstance();
      y1 = cal.get(Calendar.YEAR);
      y2 = y1 - 70;

    } // end if
    else
    { // now recognize what the field value is
      do
      { // attempt to recognize a range
	RE re = new RE(YRANGE);
	if (re.match(yearspec))
	{ // got a year range
	  y1 = Integer.parseInt(re.getParen(1));
	  y2 = Integer.parseInt(re.getParen(2));
	  break;

	} // end if

	Calendar cal = Calendar.getInstance();
	re = new RE(YPLUS);
	if (re.match(yearspec))
	{ // +number - range from current year forward some years
	  y1 = cal.get(Calendar.YEAR);
	  y2 = y1 + Integer.parseInt(re.getParen(1));
	  break;

	} // end if

	re = new RE(YMINUS);
	if (re.match(yearspec))
	{ // -number - range from current year backward some years
	  y1 = cal.get(Calendar.YEAR);
	  y2 = y1 - Integer.parseInt(re.getParen(1));
	  break;

	} // end if

	re = new RE(YPLUSMINUS);
	if (re.match(yearspec))
	{ // +number -number - brackets around the current year
	  int foo = cal.get(Calendar.YEAR);
	  y1 = foo + Integer.parseInt(re.getParen(1));
	  y2 = foo - Integer.parseInt(re.getParen(2));
	  break;

	} // end if

	re = new RE(YMINUSPLUS);
	if (re.match(yearspec))
	{ // -number +number - brackets around the current year
	  int foo = cal.get(Calendar.YEAR);
	  y1 = foo - Integer.parseInt(re.getParen(1));
	  y2 = foo + Integer.parseInt(re.getParen(2));
	  break;

	} // end if

	re = new RE(YSOLO);
	if (re.match(yearspec))
	{ // solo number - one end of year spec, current year being the other
	  y1 = cal.get(Calendar.YEAR);
	  y2 = Integer.parseInt(re.getParen(1));
	  break;

	} // end if

	throw new ConfigException("<date/> years= attribute: invalid syntax",elt);

      } while (false);  // end do

    } // end else

    if (countdir.equalsIgnoreCase("down"))
    { // organize for counting downward
      m_year_start = Math.max(y1,y2);
      m_year_stop = Math.min(y1,y2);

    } // end if
    else
    { // organize for counting upward
      m_year_start = Math.min(y1,y2);
      m_year_stop = Math.max(y1,y2);

    } // end else

  } // end constructor

  protected DateField(DateField other)
  {
    m_name = other.m_name;
    m_caption = other.m_caption;
    m_caption2 = other.m_caption2;
    m_required = other.m_required;
    m_enabled = other.m_enabled;
    m_year_start = other.m_year_start;
    m_year_stop = other.m_year_stop;

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal operations
   *--------------------------------------------------------------------------------
   */

  private static final int[] getDateFieldOrdering(Locale locale)
  {
    int[] rc = (int[])(s_locale_ordering.get(locale));
    if (rc!=null)
      return rc;

    // We determine the date format by encoding the date September 18, 2003, into "short"
    // date format, then measuring the relative positions of the digits 9, 8, and 3.
    // It's severely hackish, but there seems to be no Java API whereby we can directly
    // get this information.
    DateFormat fmt = DateFormat.getDateInstance(DateFormat.SHORT,locale);
    Calendar cal = fmt.getCalendar();
    cal.set(Calendar.YEAR,2003);
    cal.set(Calendar.MONTH,Calendar.SEPTEMBER);
    cal.set(Calendar.DAY_OF_MONTH,18);
    String s = fmt.format(cal.getTime());
    int pday = s.indexOf('8');
    int pmonth = s.indexOf('9');
    int pyear = s.indexOf('3');
    rc = new int[3];
    if (pday<pmonth)
    { // day before month
      if (pday<pyear)
      { // we're definitely DAY first
	rc[0] = FLD_DAY;
	if (pmonth<pyear)
	{ // D-M-Y (Euro standard)
	  rc[1] = FLD_MONTH;
	  rc[2] = FLD_YEAR;

	} // end if
	else
	{ // D-Y-M?  Well, OK...
	  rc[1] = FLD_YEAR;
	  rc[2] = FLD_MONTH;

	} // end else

      } // end if
      else
      { // day before month, but after year - Y-D-M? OK...
	rc[0] = FLD_YEAR;
	rc[1] = FLD_DAY;
	rc[2] = FLD_MONTH;

      } // end else

    } // end if
    else
    { // month before day
      if (pmonth<pyear)
      { // definitely MONTH first...
	rc[0] = FLD_MONTH;
	if (pday<pyear)
	{ // M-D-Y (American standard)
	  rc[1] = FLD_DAY;
	  rc[2] = FLD_YEAR;

	} // end if
	else
	{ // M-Y-D? OK...
	  rc[1] = FLD_YEAR;
	  rc[2] = FLD_DAY;

	} // end else

      } // end if
      else
      { // month before day, but after year - Y-M-D (Japanese standard)
	rc[0] = FLD_YEAR;
	rc[1] = FLD_MONTH;
	rc[2] = FLD_DAY;

      } // end else

    } // end else

    s_locale_ordering.put(locale,rc);
    return rc;

  } // end getDateFieldOrdering

  private final void renderDropDown(int which, RequestOutput out) throws IOException
  {
    HTMLRendering html = (HTMLRendering)(out.queryService(HTMLRendering.class));
    Calendar cal = Calendar.getInstance(TimeZone.getDefault(),html.getLocale());
    if (which==FLD_MONTH)
    { // render the "month" field
      out.write("<select class=\"content\" name=\"" + m_name + "_month\" size=\"1\"");
      if (!m_enabled)
	out.write(" disabled=\"disabled\"");
      out.write("><option value=\"-1\"");
      if (m_ndx_month==-1)
	out.write(" selected=\"selected\"");
      out.write(">---</option>");
      DateFormatSymbols syms = new DateFormatSymbols(html.getLocale());
      String[] months = syms.getMonths();
      for (int i=cal.getMinimum(Calendar.MONTH); i<=cal.getMaximum(Calendar.MONTH); i++)
      { // write the choices
	out.write("<option value=\"" + i + "\"");
	if (m_ndx_month==i)
	  out.write(" selected=\"selected\"");
	out.write(">" + StringUtil.encodeHTML(months[i]) + "</option>");
	
      } // end for

      out.write("</select>\n");

    } // end if
    else if (which==FLD_DAY)
    { // render the "day" field
      out.write("<select class=\"content\" name=\"" + m_name + "_day\" size=\"1\"");
      if (!m_enabled)
	out.write(" disabled=\"disabled\"");
      out.write("><option value=\"-1\"");
      if (m_ndx_day==-1)
	out.write(" selected=\"selected\"");
      out.write(">---</option>");
      for (int i=cal.getMinimum(Calendar.DAY_OF_MONTH); i<=cal.getMaximum(Calendar.DAY_OF_MONTH); i++)
      { // write the day values
	out.write("<option value=\"" + i + "\"");
	if (m_ndx_day==i)
	  out.write(" selected=\"selected\"");
	out.write(">" + i + "</option>");
	
      } // end for

      out.write("</select>\n");

    } // end if
    else if (which==FLD_YEAR)
    { // render the "year" drop down
      out.write("<select class=\"content\" name=\"" + m_name + "_year\" size=\"1\"");
      if (!m_enabled)
	out.write(" disabled=\"disabled\"");
      out.write("><option value=\"-1\"");
      if (m_ndx_year==-1)
	out.write(" selected=\"selected\"");
      out.write(">---</option>");
      int step = ((m_year_start>m_year_stop) ? -1 : 1);
      for (int i=m_year_start; i!=m_year_stop; i+=step)
      { // write out years
	out.write("<option value=\"" + i + "\"");
	if (m_ndx_year==i)
	  out.write(" selected=\"selected\"");
	out.write(">" + i + "</option>");

      } // end for

      // write the last year value
      out.write("<option value=\"" + m_year_stop + "\"");
      if (m_ndx_year==m_year_stop)
	out.write(" selected=\"selected\"");
      out.write(">" + m_year_stop + "</option></select>\n");

    } // end else if

  } // end renderDropDown

  /*--------------------------------------------------------------------------------
   * Implementations from interface RenderDirect
   *--------------------------------------------------------------------------------
   */

  public void render(RequestOutput out) throws IOException
  {
    HTMLRendering html = (HTMLRendering)(out.queryService(HTMLRendering.class));
    Calendar cal = Calendar.getInstance(TimeZone.getDefault(),html.getLocale());
    if ((m_value!=null) && !m_input_error)
    { // split date into month, day, and year
      cal.setTime(m_value);
      if (m_ndx_month==-1)
	m_ndx_month = cal.get(Calendar.MONTH);
      if (m_ndx_day==-1)
	m_ndx_day = cal.get(Calendar.DAY_OF_MONTH);
      if (m_ndx_year==-1)
	m_ndx_year = cal.get(Calendar.YEAR);

    } // end if
    else  // everything is not set
      m_ndx_month = m_ndx_day = m_ndx_year = -1;

    // write the caption and the basic table framework
    out.write("<tr valign=\"middle\">\n<td align=\"right\" class=\"content\">");
    out.write(html.getFontTag(m_enabled ? html.CONTENT_FOREGROUND : html.CONTENT_DISABLED,"content"));
    out.write(StringUtil.encodeHTML(m_caption));
    if (!(StringUtil.isStringEmpty(m_caption2)))
      out.write(" " + StringUtil.encodeHTML(m_caption2));
    out.write(":</font>");
    if (m_required)
      out.write(html.getFontTag("red","content") + "*</font>");
    out.write("</td>\n<td align=\"left\" class=\"content\">");

    // write the drop-down list boxes
    int[] order = getDateFieldOrdering(html.getLocale());
    renderDropDown(order[0],out);
    out.write("&nbsp;\n");
    renderDropDown(order[1],out);
    out.write("&nbsp;\n");
    renderDropDown(order[2],out);

    // finish up
    out.write("</td>\n</tr>\n");

  } // end render

  /*--------------------------------------------------------------------------------
   * Implementations from interface DialogField
   *--------------------------------------------------------------------------------
   */

  public String getName()
  {
    return m_name;

  } // end getName

  public Object getValue()
  {
    return m_value;

  } // end getValue

  public void setValue(Object o)
  {
    if (o==null)
      m_value = null;
    else if (o instanceof java.sql.Date)
      m_value = (java.sql.Date)(((java.sql.Date)o).clone());
    else if (o instanceof java.util.Date)
      m_value = new java.sql.Date(((java.util.Date)o).getTime());
    else if (o instanceof Calendar)
      m_value = new java.sql.Date(((Calendar)o).getTimeInMillis());
    else if (o instanceof Number)
      m_value = new java.sql.Date(((Number)o).longValue());
    else
      throw new IllegalArgumentException("cannot convert argument to java.sql.Date");
    if (logger.isDebugEnabled())
      logger.debug("Set date: " + m_value);
    m_ndx_month = m_ndx_day = m_ndx_year = -1;  // kill indexes
    m_input_error = false;

  } // end setValue

  public void setValueFrom(RequestInput ri)
  {
    m_ndx_month = ri.getParameterInt(m_name + "_month",-1);
    m_ndx_day = ri.getParameterInt(m_name + "_day",-1);
    m_ndx_year = ri.getParameterInt(m_name + "_year",-1);
    if (logger.isDebugEnabled())
      logger.debug("Raw input: M=" + m_ndx_month + ", D=" + m_ndx_day + ", Y=" + m_ndx_year);

    if ((m_ndx_month==-1) || (m_ndx_day==-1) || (m_ndx_year==-1))
    { // the date is to be treated as "unspecified"
      logger.debug("Killing the kittens!");
      m_value = null;
      m_ndx_month = m_ndx_day = m_ndx_year = -1;  // kill indexes
      m_input_error = false;
      return;

    } // end if

    HTMLRendering html = (HTMLRendering)(ri.queryService(HTMLRendering.class));
    Calendar cal = Calendar.getInstance(TimeZone.getDefault(),html.getLocale());
    cal.clear();
    cal.setLenient(false);
    m_input_error = false;
    try
    { // set the calendar and create the actual value
      cal.set(Calendar.YEAR,m_ndx_year);
      cal.set(Calendar.MONTH,m_ndx_month);
      cal.set(Calendar.DAY_OF_MONTH,m_ndx_day);
      cal.set(Calendar.HOUR,0);
      cal.set(Calendar.MINUTE,0);
      cal.set(Calendar.SECOND,0);
      cal.set(Calendar.MILLISECOND,0);
      m_value = new java.sql.Date(cal.getTimeInMillis());
      if (logger.isDebugEnabled())
	logger.debug("Verified date: " + m_value);

    } // end try
    catch (IllegalArgumentException e)
    { // set error flag and recompute value with relaxed rules
      m_input_error = true;
      cal.clear();
      cal.setLenient(true);
      cal.set(Calendar.YEAR,m_ndx_year);
      cal.set(Calendar.MONTH,m_ndx_month);
      cal.set(Calendar.DAY_OF_MONTH,m_ndx_day);
      cal.set(Calendar.HOUR,0);
      cal.set(Calendar.MINUTE,0);
      cal.set(Calendar.SECOND,0);
      cal.set(Calendar.MILLISECOND,0);
      m_value = new java.sql.Date(cal.getTimeInMillis());
      if (logger.isDebugEnabled())
	logger.debug("UN-Verified date: " + m_value);

    } // end catch

  } // end setValueFrom

  public boolean isRequired()
  {
    return m_required;

  } // end isRequired

  public boolean isFile()
  {
    return false;

  } // end isFile

  public boolean isHidden()
  {
    return false;

  } // end isHidden

  public void validate() throws ValidationException
  {
    if (m_required && (m_value==null))
      throw new ValidationException("The '" + m_caption + "' field is required.");
    if (m_input_error)
      throw new ValidationException("Invalid date entered in the '" + m_caption + "' field.");

  } // end validate

  public boolean isEnabled()
  {
    return m_enabled;

  } // end isEnabled

  public void setEnabled(boolean flag)
  {
    m_enabled = flag;

  } // end setEnabled

  public Object sendMessage(String msg, Object data)
  {
    return null;

  } // end sendMessage

  public DialogField duplicate()
  {
    return new DateField(this);

  } // end duplicate

  /*--------------------------------------------------------------------------------
   * Static initializer
   *--------------------------------------------------------------------------------
   */

  static
  {
    RECompiler compiler = new RECompiler();
    try
    { // compile all local regular expressions
      YRANGE = compiler.compile("^\\s*(\\d+)\\s*-\\s*(\\d+)\\s*$");
      YPLUS = compiler.compile("^\\s*\\+\\s*(\\d+)\\s*$");
      YMINUS = compiler.compile("^\\s*-\\s*(\\d+)\\s*$");
      YPLUSMINUS = compiler.compile("^\\s*\\+\\s*(\\d+)\\s*-\\s*(\\d+)\\s*$");
      YMINUSPLUS = compiler.compile("^\\s*-\\s*(\\d+)\\s*\\+\\s*(\\d+)\\s*$");
      YSOLO = compiler.compile("^\\s*(\\d+)\\s*$");

    } // end try
    catch (RESyntaxException e)
    { // whoops!
      logger.fatal("Regexp Syntax Error",e);

    } // end catch

  } // end static initializer

} // end class DateField
