/*
 * $Id: W3cdtfDateFormat.java,v 0.0 2009/07/23 00:00:00 t-imamura Exp $
 *
 * Copyright (c) 2009 T.Imamura, All rights reserved.
 */
package w3cdtf;

import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.Calendar;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <code>W3cdtfDateFormat</code> は、W3Cのノートで示されている日時の表記法での
 * 日付のフォーマットと解析を行うための具象クラスです。
 *
 * <h4><a name="synchronization">同期</a></h4>
 * <p>日付フォーマットは同期化されません。スレッドごとに別のフォーマットインスタンスを
 * 作成することをお勧めします。複数のスレッドがフォーマットに同時にアクセスする場合は、
 * 外部的に同期化する必要があります。</p>
 *
 * <dl>
 * <dt>W3CDTF</dt>
 * <dd>Misha Wolf and Charles Wicksteed, Date and Time Formats, 1997-12-15, W3C Note
 * <br><a href="http://www.w3.org/TR/NOTE-datetime">http://www.w3.org/TR/NOTE-datetime</a></dd>
 * </dl>
 *
 * @see DateFormat
 */
public class W3cdtfDateFormat extends DateFormat {

	/** シリアルバージョン */
	private static final long serialVersionUID = 1L;

	/** 年のみ (eg 1997) */
	public static final int YEAR = 1;

	/** 年月 (eg 1997-07) */
	public static final int YEAR_MONTH = 2;
	/** 年月 ({@link W3cdtfDateFormat#YEAR_MONTH YEAR_MONTH} の別名) */
	public static final int MONTH = YEAR_MONTH;

	/** 年月日 (eg 1997-07-16) */
	public static final int COMPLETE_DATE = 3;
	/** 年月日 ({@link W3cdtfDateFormat#COMPLETE_DATE COMPLETE_DATE} の別名) */
	public static final int DATE = COMPLETE_DATE;
	/** XML Schema date datatypes */
	public static final int XS_DATE = COMPLETE_DATE;

	/** 年月日および時分 (eg 1997-07-16T19:20+01:00) */
	public static final int COMPLETE_DATE_HOURS_MINUTE = 4;
	/** 年月日および時分 ({@link W3cdtfDateFormat#COMPLETE_DATE_HOURS_MINUTE COMPLETE_DATE_HOURS_MINUTE} の別名) */
	public static final int MINUTE = COMPLETE_DATE_HOURS_MINUTE;

	/** 年月日および時分秒 (eg 1997-07-16T19:20:30+01:00) */
	public static final int COMPLETE_DATE_SECOND = 5;
	/** 年月日および時分秒 ({@link W3cdtfDateFormat#COMPLETE_DATE_SECOND COMPLETE_DATE_SECOND} の別名) */
	public static final int SECOND = COMPLETE_DATE_SECOND;

	/** 年月日および時分秒および小数部分 (eg 1997-07-16T19:20:30.456+01:00) */
	public static final int COMPLETE_DATE_MILLISECOND = 6;
	/** 年月日および時分秒および小数部分 ({@link W3cdtfDateFormat#COMPLETE_DATE_MILLISECOND COMPLETE_DATE_MILLISECOND} の別名) */
	public static final int MILLISECOND = COMPLETE_DATE_MILLISECOND;
	/** XML Schema dateTime datatypes */
	public static final int XS_DATETIME = COMPLETE_DATE_MILLISECOND;

	/** フォーマットパターン */
	private int pattern = 0;
	/** タイムゾーンを省略する */
	private boolean omitTimeZone = false;

	/**
	 * <code>W3cdtfDateFormat</code> を構築します。
	 */
	public W3cdtfDateFormat() {
		this(COMPLETE_DATE_MILLISECOND, false);
	}

	/**
	 * <code>W3cdtfDateFormat</code> を構築します。
	 * @param pattern フォーマットパターン
	 */
	public W3cdtfDateFormat(int pattern) {
		this(pattern, false);
	}

	/**
	 * <code>W3cdtfDateFormat</code> を構築します。
	 * @param pattern フォーマットパターン
	 * @param omitTimeZone タイムゾーンを省略するか
	 */
	public W3cdtfDateFormat(int pattern, boolean omitTimeZone) {
		this.pattern = pattern;
		this.omitTimeZone = omitTimeZone;
		this.calendar = Calendar.getInstance();
	}

	@Override
	public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
		calendar.setTime(date);

		if (pattern >= YEAR) {
			toAppendTo.append(String.format("%04d", calendar.get(Calendar.YEAR)));
		}
		if (pattern >= YEAR_MONTH) {
			toAppendTo.append(String.format("-%02d", calendar.get(Calendar.MONTH) + 1));
		}
		if (pattern >= COMPLETE_DATE) {
			toAppendTo.append(String.format("-%02d", calendar.get(Calendar.DAY_OF_MONTH)));
		}
		if (pattern >= COMPLETE_DATE_HOURS_MINUTE) {
			toAppendTo.append(String.format("T%02d", calendar.get(Calendar.HOUR_OF_DAY)));
			toAppendTo.append(String.format(":%02d", calendar.get(Calendar.MINUTE)));
		}
		if (pattern >= COMPLETE_DATE_SECOND) {
			toAppendTo.append(String.format(":%02d", calendar.get(Calendar.SECOND)));
		}
		if (pattern >= COMPLETE_DATE_MILLISECOND) {
			toAppendTo.append(String.format(".%03d", calendar.get(Calendar.MILLISECOND)));
		}
		if (pattern >= COMPLETE_DATE_HOURS_MINUTE && !omitTimeZone) {
			int tz = (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / 60000;
			if (tz == 0) {
				toAppendTo.append("Z");
			} else {
				toAppendTo.append(String.format("%+03d:%02d", (tz / 60), (tz % 60)));
			}
		}

		return toAppendTo;
	}

	/** 日付文字列解析用正規表現 */
	private static final Pattern W3CDTF_PATTERN = Pattern.compile("^(\\d{4})(?:[-/.](\\d{2})(?:[-/.](\\d{2})"
			+ "(?:[T ](\\d{2}):(\\d{2})(?:(?::(\\d{2})(.\\d+)?)?(?:([-+]\\d{2}):(\\d{2})|(Z))?)?)?)?)?$");

	@Override
	public Date parse(String source, ParsePosition pos) {
		if (source == null) {
			pos.setErrorIndex(pos.getIndex());
			return null;
		}
		source = source.substring(pos.getIndex());
		if (source.isEmpty()) {
			pos.setErrorIndex(pos.getIndex());
			return null;
		}
		Matcher m = W3CDTF_PATTERN.matcher(source);
		if (!m.find()) {
			pos.setErrorIndex(pos.getIndex());
			return null;
		}

		calendar.clear();
		calendar.set(Calendar.YEAR, Integer.parseInt(m.group(1)));
		calendar.set(Calendar.MONTH, (m.group(2) == null) ? 0 : Integer.parseInt(m.group(2)) - 1);
		calendar.set(Calendar.DAY_OF_MONTH, (m.group(3) == null) ? 0 : Integer.parseInt(m.group(3)));
		calendar.set(Calendar.HOUR_OF_DAY, (m.group(4) == null) ? 0 : Integer.parseInt(m.group(4)));
		calendar.set(Calendar.MINUTE, (m.group(5) == null) ? 0 : Integer.parseInt(m.group(5)));
		calendar.set(Calendar.SECOND, (m.group(6) == null) ? 0 : Integer.parseInt(m.group(6)));
		calendar.set(Calendar.MILLISECOND, (m.group(7) == null) ? 0 : (int) (Double.parseDouble(m.group(7)) * 1000.0D));
		if (m.group(8) != null) {
			int tz = ((int) (Double.parseDouble(m.group(8))) * 60 + Integer.parseInt(m.group(9))) * 60000;
			calendar.set(Calendar.ZONE_OFFSET, tz);
		} else if (m.group(10) != null) {
			calendar.set(Calendar.ZONE_OFFSET, 0);
		}
		pos.setIndex(pos.getIndex() + source.length() - 1);
		return calendar.getTime();
	}

}
