/* Copyright (C) 1998, 1999  Cygnus Solutions

   This file is part of libgcj.

This software is copyrighted work licensed under the terms of the
Libgcj License.  Please consult the file "LIBGCJ_LICENSE" for
details.  */

package java.text;

import java.util.*;

/**
 * @author Per Bothner <bothner@cygnus.com>
 * @date October 25, 1998.
 */
/* Written using "Java Class Libraries", 2nd edition, plus online
 * API docs for JDK 1.2 beta from http://www.javasoft.com.
 * Status:  parse is not implemented.
 */

public class SimpleDateFormat extends DateFormat
{
  private Date defaultCenturyStart;
  private DateFormatSymbols formatData;
  private String pattern;

  public SimpleDateFormat ()
  {
    this("dd/MM/yy HH:mm", Locale.getDefault());
  }

  public SimpleDateFormat (String pattern)
  {
    this(pattern, Locale.getDefault());
  }

  public SimpleDateFormat (String pattern, Locale locale)
  {
    this.pattern = pattern;
    this.calendar = Calendar.getInstance(locale);
    this.numberFormat = NumberFormat.getInstance(locale);
    numberFormat.setGroupingUsed(false);
    this.formatData = new DateFormatSymbols (locale);
  }

  public SimpleDateFormat (String pattern, DateFormatSymbols formatData)
  {
    this.pattern = pattern;
    this.formatData = formatData;
    this.calendar = Calendar.getInstance();
    this.numberFormat = NumberFormat.getInstance();
    numberFormat.setGroupingUsed(false);
  }

  public Date get2DigitYearStart()
  {
    return defaultCenturyStart;
  }

  public void set2DigitYearStart(Date startDate)
  {
    defaultCenturyStart = startDate;
  }

  public DateFormatSymbols getDateFormatSymbols ()
  {
    return formatData;
  }

  public void setDateFormatSymbols (DateFormatSymbols value)
  {
    formatData = value;
  }

  public String toPattern ()
  {
    return pattern;
  }

  public void applyPattern (String pattern)
  {
    this.pattern = pattern;
  }

  private String applyLocalizedPattern (String pattern,
					String oldChars, String newChars)
  {
    int len = pattern.length();
    StringBuffer buf = new StringBuffer(len);
    boolean quoted = false;
    for (int i = 0;  i < len;  i++)
      {
	char ch = pattern.charAt(i);
	if (ch == '\'')
	  quoted = ! quoted;
	if (! quoted)
	  {
	    int j = oldChars.indexOf(ch);
	    if (j >= 0)
	      ch = newChars.charAt(j);
	  }
	buf.append(ch);
      }
    return buf.toString();
  }

  public void applyLocalizedPattern (String pattern)
  {
    String localChars = formatData.getLocalPatternChars();
    String standardChars = DateFormatSymbols.localPatternCharsDefault;
    pattern = applyLocalizedPattern (pattern, localChars, standardChars);
    applyPattern(pattern);
  }

  public String toLocalizedPattern ()
  {
    String localChars = formatData.getLocalPatternChars();
    String standardChars = DateFormatSymbols.localPatternCharsDefault;
    return applyLocalizedPattern (pattern, standardChars, localChars);
  }

  private final void append (StringBuffer buf, int value, int numDigits)
  {
    numberFormat.setMinimumIntegerDigits(numDigits);
    numberFormat.format(value, buf, null);
  }

  public StringBuffer format (Date date, StringBuffer buf, FieldPosition pos)
  {
    Calendar calendar = (Calendar) this.calendar.clone();
    calendar.setTime(date);
    int len = pattern.length();
    int quoteStart = -1;
    for (int i = 0;  i < len;  i++)
      {
	char ch = pattern.charAt(i);
	if (ch == '\'')
	  {
	    // We must do a little lookahead to see if we have two
	    // single quotes embedded in quoted text.
	    if (i < len - 1 && pattern.charAt(i + 1) == '\'')
	      {
		++i;
		buf.append(ch);
	      }
	    else
	      quoteStart = quoteStart < 0 ? i : -1;
	  }
	// From JCL: any characters in the pattern that are not in
	// the ranges of [a..z] and [A..Z] are treated as quoted
	// text.
	else if (quoteStart != -1
	    || ((ch < 'a' || ch > 'z')
		&& (ch < 'A' || ch > 'Z')))
	  buf.append(ch);
	else
	  {
	    int first = i;
	    int value;
	    while (++i < len && pattern.charAt(i) == ch) ;
	    int count = i - first; // Number of repetions of ch in pattern.
	    int beginIndex = buf.length();
	    int field;
	    i--;  // Skip all but last instance of ch in pattern.
	    switch (ch)
	      {
	      case 'd':
		append(buf, calendar.get(Calendar.DATE), count);
		field = DateFormat.DATE_FIELD;
		break;
	      case 'D':
		append(buf, calendar.get(Calendar.DAY_OF_YEAR), count);
		field = DateFormat.DAY_OF_YEAR_FIELD;
		break;
	      case 'F':
		append(buf, calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH),count);
		field = DateFormat.DAY_OF_WEEK_IN_MONTH_FIELD;
		break;
	      case 'E':
		value = calendar.get(calendar.DAY_OF_WEEK);
		buf.append(count <= 3 ? formatData.getShortWeekdays()[value]
			   : formatData.getWeekdays()[value]);
		field = DateFormat.DAY_OF_WEEK_FIELD;
		break;
	      case 'w':
		append(buf, calendar.get(Calendar.WEEK_OF_YEAR), count); 
		field = DateFormat.WEEK_OF_YEAR_FIELD;
                break;
	      case 'W':
		append(buf, calendar.get(Calendar.WEEK_OF_MONTH), count); 
		field = DateFormat.WEEK_OF_MONTH_FIELD;
                break;
	      case 'M':
		value = calendar.get(Calendar.MONTH);
		if (count <= 2)
		  append(buf, value + 1, count);
		else
		  buf.append(count <= 3 ? formatData.getShortMonths()[value]
			   : formatData.getMonths()[value]);
		field = DateFormat.MONTH_FIELD;
		break;
	      case 'y':
		value = calendar.get(Calendar.YEAR);
		append(buf, count <= 2 ? value % 100 : value, count);
		field = DateFormat.YEAR_FIELD;
		break;
	      case 'K':
		append(buf, calendar.get(Calendar.HOUR), count);
		field = DateFormat.HOUR0_FIELD;
		break;
	      case 'h':
		value = ((calendar.get(Calendar.HOUR) + 11) % 12) + 1;
		append(buf, value, count);
		field = DateFormat.HOUR1_FIELD;
		break;
	      case 'H':
		append(buf, calendar.get(Calendar.HOUR_OF_DAY), count);
		field = DateFormat.HOUR_OF_DAY0_FIELD;
		break;
	      case 'k':
		value = ((calendar.get(Calendar.HOUR_OF_DAY) + 23) % 24) + 1;
		append(buf, value, count);
		field = DateFormat.HOUR_OF_DAY1_FIELD;
		break;
	      case 'm':
		append(buf, calendar.get(Calendar.MINUTE), count);
		field = DateFormat.MINUTE_FIELD;
		break;
	      case 's':
		append(buf, calendar.get(Calendar.SECOND), count);
		field = DateFormat.SECOND_FIELD;
		break;
	      case 'S':
		append(buf, calendar.get(Calendar.MILLISECOND), count);
		field = DateFormat.MILLISECOND_FIELD;
		break;
	      case 'a':
		value = calendar.get(calendar.AM_PM);
		buf.append(formatData.getAmPmStrings()[value]);
		field = DateFormat.AM_PM_FIELD;
		break;
	      case 'z':
		String zoneID = calendar.getTimeZone().getID();
		String[][] zoneStrings = formatData.getZoneStrings();
		int zoneCount = zoneStrings.length;
		for (int j = 0;  j < zoneCount;  j++)
		  {
		    String[] strings = zoneStrings[j];
		    if (zoneID.equals(strings[0]))
		      {
			j = count > 3 ? 2 : 1;
			if (calendar.get(Calendar.DST_OFFSET) != 0)
			  j+=2;
			zoneID = strings[j];
			break;
		      }
		  }
		buf.append(zoneID);
		field = DateFormat.TIMEZONE_FIELD;
		break;
	      default:
		// Note that the JCL is actually somewhat
		// contradictory here.  It defines the pattern letters
		// to be a particular list, but also says that a
		// pattern containing an invalid pattern letter must
		// throw an exception.  It doesn't describe what an
		// invalid pattern letter might be, so we just assume
		// it is any letter in [a-zA-Z] not explicitly covered
		// above.
		throw new RuntimeException("bad format string");
	      }
	    if (pos != null && field == pos.getField())
	      {
		pos.setBeginIndex(beginIndex);
		pos.setEndIndex(buf.length());
	      }
	  }
      }
    return buf;
  }

  private final boolean expect (String source, ParsePosition pos,
				char ch)
  {
    int x = pos.getIndex();
    boolean r = x < source.length() && source.charAt(x) == ch;
    if (r)
      pos.setIndex(x + 1);
    else
      pos.setErrorIndex(x);
    return r;
  }

  public Date parse (String source, ParsePosition pos)
  {
    int fmt_index = 0;
    int fmt_max = pattern.length();

    calendar.clear();
    int quote_start = -1;
    for (; fmt_index < fmt_max; ++fmt_index)
      {
	char ch = pattern.charAt(fmt_index);
	if (ch == '\'')
	  {
	    int index = pos.getIndex();
	    if (fmt_index < fmt_max - 1
		&& pattern.charAt(fmt_index + 1) == '\'')
	      {
		if (! expect (source, pos, ch))
		  return null;
		++fmt_index;
	      }
	    else
	      quote_start = quote_start < 0 ? fmt_index : -1;
	    continue;
	  }

	if (quote_start != -1
	    || ((ch < 'a' || ch > 'z')
		&& (ch < 'A' || ch > 'Z')))
	  {
	    if (! expect (source, pos, ch))
	      return null;
	    continue;
	  }

	// We've arrived at a potential pattern character in the
	// pattern.
	int first = fmt_index;
	while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
	  ;
	int count = fmt_index - first;
	--fmt_index;

	// We can handle most fields automatically: most either are
	// numeric or are looked up in a string vector.  In some cases
	// we need an offset.  When numeric, `offset' is added to the
	// resulting value.  When doing a string lookup, offset is the
	// initial index into the string array.
	int calendar_field;
	boolean is_numeric = true;
	String[] match = null;
	int offset = 0;
	int zone_number = 0;
	switch (ch)
	  {
	  case 'd':
	    calendar_field = Calendar.DATE;
	    break;
	  case 'D':
	    calendar_field = Calendar.DAY_OF_YEAR;
	    break;
	  case 'F':
	    calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
	    break;
	  case 'E':
	    is_numeric = false;
	    offset = 1;
	    calendar_field = Calendar.DAY_OF_WEEK;
	    match = (count <= 3
		     ? formatData.getShortWeekdays()
		     : formatData.getWeekdays());
	    break;
	  case 'w':
	    calendar_field = Calendar.WEEK_OF_YEAR;
	    break;
	  case 'W':
	    calendar_field = Calendar.WEEK_OF_MONTH;
	    break;
	  case 'M':
	    calendar_field = Calendar.MONTH;
	    if (count <= 2)
	      ;
	    else
	      {
		is_numeric = false;
		match = (count <= 3
			 ? formatData.getShortMonths()
			 : formatData.getMonths());
	      }
	    break;
	  case 'y':
	    calendar_field = Calendar.YEAR;
	    if (count <= 2)
	      offset = 1900;
	    break;
	  case 'K':
	    calendar_field = Calendar.HOUR;
	    break;
	  case 'h':
	    calendar_field = Calendar.HOUR;
	    offset = -1;
	    break;
	  case 'H':
	    calendar_field = Calendar.HOUR_OF_DAY;
	    break;
	  case 'k':
	    calendar_field = Calendar.HOUR_OF_DAY;
	    offset = -1;
	    break;
	  case 'm':
	    calendar_field = Calendar.MINUTE;
	    break;
	  case 's':
	    calendar_field = Calendar.SECOND;
	    break;
	  case 'S':
	    calendar_field = Calendar.MILLISECOND;
	    break;
	  case 'a':
	    is_numeric = false;
	    calendar_field = Calendar.AM_PM;
	    match = formatData.getAmPmStrings();
	    break;
	  case 'z':
	    // We need a special case for the timezone, because it
	    // uses a different data structure than the other cases.
	    is_numeric = false;
	    calendar_field = Calendar.DST_OFFSET;
	    String[][] zoneStrings = formatData.getZoneStrings();
	    int zoneCount = zoneStrings.length;
	    int index = pos.getIndex();
	    boolean found_zone = false;
	    for (int j = 0;  j < zoneCount;  j++)
	      {
		String[] strings = zoneStrings[j];
		int k;
		for (k = 1; k < strings.length; ++k)
		  {
		    if (source.startsWith(strings[k], index))
		      break;
		  }
		if (k != strings.length)
		  {
		    if (k > 2)
		      ;		// FIXME: dst.
		    zone_number = 0; // FIXME: dst.
		    // FIXME: raw offset to SimpleTimeZone const.
		    calendar.setTimeZone(new SimpleTimeZone (1, strings[0]));
		    pos.setIndex(index + strings[k].length());
		    break;
		  }
	      }
	    if (! found_zone)
	      {
		pos.setErrorIndex(pos.getIndex());
		return null;
	      }
	    break;
	  default:
	    pos.setErrorIndex(pos.getIndex());
	    return null;
	  }

	// Compute the value we should assign to the field.
	int value;
	if (is_numeric)
	  {
	    numberFormat.setMinimumIntegerDigits(count);
	    Number n = numberFormat.parse(source, pos);
	    if (pos == null || ! (n instanceof Long))
	      return null;
	    value = n.intValue() + offset;
	  }
	else if (match != null)
	  {
	    int index = pos.getIndex();
	    int i;
	    for (i = offset; i < match.length; ++i)
	      {
		if (source.startsWith(match[i], index))
		  break;
	      }
	    if (i == match.length)
	      {
		pos.setErrorIndex(index);
		return null;
	      }
	    pos.setIndex(index + match[i].length());
	    value = i;
	  }
	else
	  value = zone_number;

	// Assign the value and move on.
	try
	  {
	    calendar.set(calendar_field, value);
	  }
	// FIXME: what exception is thrown on an invalid
	// non-lenient set?
	catch (IllegalArgumentException x)
	  {
	    pos.setErrorIndex(pos.getIndex());
	    return null;
	  }
      }

    return calendar.getTime();
  }

  public boolean equals (Object obj)
  {
    if (! (obj instanceof SimpleDateFormat) || ! super.equals(obj) )
      return false;
    SimpleDateFormat other = (SimpleDateFormat) obj;
    return (DateFormatSymbols.equals(pattern, other.pattern)
	    && DateFormatSymbols.equals(formatData, other.formatData)
	    && DateFormatSymbols.equals(defaultCenturyStart,
					other.defaultCenturyStart));
  }

  public int hashCode ()
  {
    int hash = super.hashCode();
    if (pattern != null)
      hash ^= pattern.hashCode();
    return hash;
  }
}
