package at.gv.egovernment.moa.util;
import java.io.StringWriter;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
/**
 * Utility for parsing and building XML type dateTime,
 * according to ISO 8601.
 * 
 * @author Patrick Peck
 * @version $Id$
 * @see http://www.w3.org/2001/XMLSchema-datatypes"
 */
public class DateTimeUtils {
  /** Error messages. */
  private static MessageProvider msg = MessageProvider.getInstance();
  /**
   * Builds a dateTime value from a Calendar value.
   * @param cal the Calendar value
   * @return the dateTime value
   */
  public static String buildDateTime(Calendar cal) {
    StringWriter out = new StringWriter();
    out.write("" + cal.get(Calendar.YEAR));
    out.write("-");
    out.write(to2DigitString(cal.get(Calendar.MONTH) + 1));
    out.write("-");
    out.write(to2DigitString(cal.get(Calendar.DAY_OF_MONTH)));
    out.write("T");
    out.write(to2DigitString(cal.get(Calendar.HOUR_OF_DAY)));
    out.write(":");
    out.write(to2DigitString(cal.get(Calendar.MINUTE)));
    out.write(":");
    out.write(to2DigitString(cal.get(Calendar.SECOND)));
    int tzOffsetMilliseconds =
      cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET);
    if (tzOffsetMilliseconds != 0) {
      int tzOffsetMinutes = tzOffsetMilliseconds / (1000 * 60);
      int tzOffsetHours = tzOffsetMinutes / 60;
      tzOffsetMinutes -= tzOffsetHours * 60;
      if (tzOffsetMilliseconds > 0) {
        out.write("+");
        out.write(to2DigitString(tzOffsetHours));
        out.write(":");
        out.write(to2DigitString(tzOffsetMinutes));
      } else {
        out.write("-");
        out.write(to2DigitString(-tzOffsetHours));
        out.write(":");
        out.write(to2DigitString(-tzOffsetMinutes));
      }
    }
    return out.toString();
  }
  
  /**
   * Converts month, day, hour, minute, or second value
   * to a 2 digit String.
   * @param number the month, day, hour, minute, or second value
   * @return 2 digit String
   */
  private static String to2DigitString(int number) {
    if (number < 10)
      return "0" + number;
    else
      return "" + number;
  }
  /**
   * Parse a String containing a date and time instant, given in
   * ISO 8601 format.
   * 
   * @param dateTime The String to parse.
   * @return The Date representation of the contents of
   * dateTime.
   * @throws ParseException Parsing the dateTime failed.
   */
  public static Date parseDateTime(String dateTime) throws ParseException {
    GregorianCalendar calendar;
    long time;
    int yearSign = 1, year, month, day;
    int hour, minute, second;
    double fraction = 0.0;
    int tzSign = 1, tzHour = 0, tzMinute = 0;
    int curPos = 0;
    String fractStr;
    boolean localTime = false;
    char c;
    // parse year sign
    ensureChars(dateTime, curPos, 1);
    c = dateTime.charAt(curPos);
    if (c == '+' || c == '-') {
      yearSign = c == '+' ? 1 : -1;
      curPos++;
    }
    // parse year
    year = parseInt(dateTime, curPos, 4);
    curPos += 4;
    // parse '-'
    ensureChar(dateTime, curPos, '-');
    curPos++;
    // parse month
    month = parseInt(dateTime, curPos, 2);
    ensureValue(month, 1, 12, curPos);
    curPos += 2;
    // parse '-'
    ensureChar(dateTime, curPos, '-');
    curPos++;
    // parse day
    day = parseInt(dateTime, curPos, 2);
    ensureValue(day, 1, 31, curPos);
    curPos += 2;
    // parse 'T'
    ensureChar(dateTime, curPos, 'T');
    curPos++;
    // parse hour
    hour = parseInt(dateTime, curPos, 2);
    ensureValue(hour, 0, 23, curPos);
    curPos += 2;
    // parse ':'
    ensureChar(dateTime, curPos, ':');
    curPos++;
    // parse minute
    minute = parseInt(dateTime, curPos, 2);
    ensureValue(minute, 0, 59, curPos);
    curPos += 2;
    // parse ':'
    ensureChar(dateTime, curPos, ':');
    curPos++;
    // parse second
    second = parseInt(dateTime, curPos, 2);
    ensureValue(second, 0, 59, curPos);
    curPos += 2;
    // parse a fraction
    if (dateTime.length() > curPos && dateTime.charAt(curPos) == '.') {
      curPos++;
      ensureDigits(dateTime, curPos, 1);
      fractStr = "0.";
      fractStr
        += dateTime.substring(curPos, curPos + countDigits(dateTime, curPos));
      fraction = Double.parseDouble(fractStr);
      curPos += countDigits(dateTime, curPos);
    }
    // parse a time zone
    if (dateTime.length() > curPos) {
      c = dateTime.charAt(curPos);
      if (c == 'Z') {
        curPos++;
      } else if (c == '+' || c == '-') {
        // parse time zone sign
        tzSign = c == '+' ? 1 : -1;
        curPos++;
        // parse time zone hour
        tzHour = parseInt(dateTime, curPos, 2);
        ensureValue(tzHour, 0, 14, curPos);
        curPos += 2;
        // parse ':'
        ensureChar(dateTime, curPos, ':');
        curPos++;
        // parse time zone minute
        tzMinute = parseInt(dateTime, curPos, 2);
        ensureValue(tzMinute, 0, 59, curPos);
        curPos += 2;
      }
    } else {
      localTime = true;
    }
    // if we have characters left, it's an error
    if (dateTime.length() != curPos) {
      throw new ParseException(msg.getMessage("datetime.00", null), curPos);
    }
    // build the Date object
    year = year * yearSign;
    try {
      calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
      calendar.set(year, month - 1, day, hour, minute, second);
      calendar.set(Calendar.MILLISECOND, 0);
      time = calendar.getTime().getTime();
      time += (long) (fraction * 1000.0);
      time -= tzSign * ((tzHour * 60) + tzMinute) * 60 * 1000;
      if (localTime) {
        time -= TimeZone.getDefault().getRawOffset();        
      }
      return new Date(time);
    } catch (IllegalArgumentException e) {
      throw new ParseException(msg.getMessage("datetime.00", null), curPos);
    }
  }
  /**
   * Parse an integer value.
   * 
   * @param str The String containing the digits.
   * @param curPos The starting position.
   * @param digits The number of digist making up the integer value.
   * @return int The integer representation of the digits contained in
   * str.
   * @throws ParseException Parsing the integer value failed.
   */
  private static int parseInt(String str, int curPos, int digits)
    throws ParseException {
    ensureDigits(str, curPos, digits);
    return Integer.parseInt(str.substring(curPos, curPos + digits));
  }
  /**
   * Count the number of digits following curPos.
   * 
   * @param str The String in which to count digits.
   * @param curPos The starting position.
   * @return int The number of digits.
   */
  private static int countDigits(String str, int curPos) {
    int i;
    for (i = curPos; i < str.length() && Character.isDigit(str.charAt(i)); i++);
    return i - curPos;
  }
  /**
   * Ensure that a value falls in a given min/max range.
   * 
   * @param value The value to check.
   * @param min The minimum allowed value.
   * @param max The maximum allowed value.
   * @param curPos To indicate the parsing position in the
   * ParseException.
   * @throws ParseException Thrown, if value < min || value >
   * max
   */
  private static void ensureValue(int value, int min, int max, int curPos)
    throws ParseException {
    if (value < min || value > max) {
      throw new ParseException(msg.getMessage("datetime.00", null), curPos);
    }
  }
  /**
   * Ensure that the given String has a number of characters left.
   * 
   * @param str The String to check for its length.
   * @param curPos The starting position.
   * @param count The minimum number of characters that str must
   * contain, starting at from curPos.
   * @throws ParseException Thrown, if 
   * curPos + count > str.length().
   */
  private static void ensureChars(String str, int curPos, int count)
    throws ParseException {
    if (curPos + count > str.length()) {
      throw new ParseException(msg.getMessage("datetime.00", null), curPos);
    }
  }
  /**
   * Ensure that a given String contains a certain character at a
   * certain position.
   * 
   * @param str The String in which to look up the character. 
   * @param curPos The position in str that must contain the
   * character.
   * @param c The character value that must be contained at position
   * curPos.
   * @throws ParseException Thrown, if the characters do not match or
   * curPos is out of range.
   */
  private static void ensureChar(String str, int curPos, char c)
    throws ParseException {
    ensureChars(str, curPos, 1);
    if (str.charAt(curPos) != c) {
      throw new ParseException(msg.getMessage("datetime.00", null), curPos);
    }
  }
  /**
   * Ensure that a given String contains a number of digits,
   * starting at a given position.
   * 
   * @param str The String to scan for digits. 
   * @param curPos The starting postion.
   * @param count The number of digits that must be contained in
   * str, starting at curPos.
   * @throws ParseException Thrown, if str is not long enough, or
   * one of the characters following curPos in str is
   * not a digit.
   */
  private static void ensureDigits(String str, int curPos, int count)
    throws ParseException {
    ensureChars(str, curPos, count);
    for (int i = curPos; i < curPos + count; i++) {
      if (!Character.isDigit(str.charAt(i))) {
        throw new ParseException(msg.getMessage("datetime.00", null), curPos);
      }
    }
  }
}