/*
 * Copyright (c) 2009 OrangeSignal.com All rights reserved.
 * 
 * これは Apache ライセンス Version 2.0 (以下、このライセンスと記述) に
 * 従っています。このライセンスに準拠する場合以外、このファイルを使用
 * してはなりません。このライセンスのコピーは以下から入手できます。
 * 
 * http://www.apache.org/licenses/LICENSE-2.0.txt
 * 
 * 適用可能な法律がある、あるいは文書によって明記されている場合を除き、
 * このライセンスの下で配布されているソフトウェアは、明示的であるか暗黙の
 * うちであるかを問わず、「保証やあらゆる種類の条件を含んでおらず」、
 * 「あるがまま」の状態で提供されるものとします。
 * このライセンスが適用される特定の許諾と制限については、このライセンス
 * を参照してください。
 */

package jp.sf.orangesignal.ta.data.annotation;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import jp.sf.orangesignal.ta.data.CompressType;
import jp.sf.orangesignal.ta.data.DataConvertUtils;
import jp.sf.orangesignal.ta.data.DatasetItems;
import jp.sf.orangesignal.ta.data.DatasetSource;
import jp.sf.orangesignal.ta.data.DateFormatConfig;
import jp.sf.orangesignal.ta.data.NumberFormatConfig;
import jp.sf.orangesignal.ta.util.Assert;
import jp.sf.orangesignal.ta.util.StringUtils;

/**
 * 注釈を解析してデータ項目群情報を構築する機能を提供します。
 * 
 * @author 杉澤 浩二
 * @since 2.1
 */
public abstract class AnnotationParser {

	/**
	 * 指定されたデータセットの注釈を解析してデータ項目群情報を構築して返します。
	 * 
	 * @param obj データセット
	 * @return データ項目群情報
	 * @throws IllegalArgumentException データセットに <code>null</code> が指定された場合
	 * @throws AnnotationException 注釈の解析に失敗した場合
	 */
	public static DatasetSource parse(final Object obj) {
		Assert.notNull(obj, "Dataset must not be null");

		final Class<?> cls = obj.getClass();
		final DateFormatConfig defaultDateFormatConfig = createDateFormatConfig(cls.getAnnotation(DatePattern.class));
		final NumberFormatConfig defaultNumberFormatConfig = createNumberFormatConfig(cls.getAnnotation(NumberPattern.class));
		final CompressType defaultCompressType = (CompressType) AnnotationUtils.getValue(cls.getAnnotation(NumberCompressorType.class));

		final DatasetItems items = new DatasetItems();
		for (final Field field : cls.getDeclaredFields()) {
			try {
				// 日時データを解析します。
				final DateItem datetime = field.getAnnotation(DateItem.class);
				if (datetime != null) {
					final String name = StringUtils.defaultIfEmpty(datetime.value(), field.getName());
					final DateFormatConfig config = createDateFormatConfig(field, defaultDateFormatConfig);
					final Object value = getFieldValue(obj, field);

					final Date[] date;
					if (value instanceof Collection) {
						date = DataConvertUtils.toDateArray((Collection<?>) value, config);
					} else if (value instanceof Object[]) {
						date = DataConvertUtils.toDateArray((Object[]) value, config);
					} else {
						throw new AnnotationException("unknown type");
					}
					items.setDate(name, date);
					items.setDateFormatConfig(name, config);
				}

				// 数値データを解析します。
				final NumberItem data = field.getAnnotation(NumberItem.class);
				if (data != null) {
					final String name = StringUtils.defaultIfEmpty(data.value(), field.getName());
					final NumberFormatConfig config = createNumberFormatConfig(field, defaultNumberFormatConfig);
					final Object value = getFieldValue(obj, field);

					final Number[] number;
					if (value instanceof Collection) {
						number = DataConvertUtils.toNumberArray((Collection<?>) value, config);
					} else if (value instanceof Object[]) {
						number = DataConvertUtils.toNumberArray((Object[]) value, config);
					} else {
						throw new AnnotationException("unknown type");
					}
					items.setNumber(name, number);
					items.setNumberFormatConfig(name, config);
					items.setCompressType(name, getCompressorType(field, defaultCompressType));
				}
			} catch (IllegalAccessException e) {
				throw new AnnotationException(e.getMessage(), e);
			}
		}
		return items;
	}

	/**
	 * 指定されたデータコレクションの注釈を解析してデータ項目群情報を構築して返します。
	 * 
	 * @param collection データコレクション
	 * @return データ項目群情報
	 * @throws IllegalArgumentException データセットに <code>null</code> が指定された場合
	 * @throws AnnotationException 注釈の解析に失敗した場合
	 */
	public static DatasetSource parse(final Collection<?> collection) {
		Assert.notNull(collection, "Collection must not be null");
		return parse(collection.toArray());
	}

	/**
	 * 指定されたデータ配列の注釈を解析してデータ項目群情報を構築して返します。
	 * 
	 * @param array データ配列
	 * @return データ項目群情報
	 * @throws IllegalArgumentException データセットに <code>null</code> が指定された場合
	 * @throws AnnotationException 注釈の解析に失敗した場合
	 */
	public static DatasetSource parse(final Object[] array) {
		Assert.notNull(array, "Array must not be null");

		/**
		 * この内部クラスはデータ配列を解析時の日時データ及び数値データの形式変換で使用されます。
		 */
		final class InnerItems {

			private int initialCapacity;

			public InnerItems(final int initialCapacity) { this.initialCapacity = initialCapacity; }

			Map<String, List<Date>> dateMap = new HashMap<String, List<Date>>();
			Map<String, List<Number>> dataMap = new HashMap<String, List<Number>>();

			public void add(final String name, final Date date) {
				List<Date> list = this.dateMap.get(name);
				if (list != null) {
					list.add(date);
				} else {
					list = new ArrayList<Date>(this.initialCapacity);
					list.add(date);
					this.dateMap.put(name, list);
				}
			}

			public void add(final String name, final Number number) {
				List<Number> list = this.dataMap.get(name);
				if (list != null) {
					list.add(number);
				} else {
					list = new ArrayList<Number>(this.initialCapacity);
					list.add(number);
					this.dataMap.put(name, list);
				}
			}

		}

		final int length = array.length;
		final InnerItems inner = new InnerItems(length);
		final DatasetItems items = new DatasetItems();

		for (int i = 0; i < length; i++) {
			final Object obj = array[i];
			if (obj == null)
				continue;

			final Class<?> cls = obj.getClass();
			final DateFormatConfig defaultDateFormatConfig = createDateFormatConfig(cls.getAnnotation(DatePattern.class));
			final NumberFormatConfig defaultNumberFormatConfig = createNumberFormatConfig(cls.getAnnotation(NumberPattern.class));
			final CompressType defaultCompressType = (CompressType) AnnotationUtils.getValue(cls.getAnnotation(NumberCompressorType.class));

			for (final Field field : cls.getDeclaredFields()) {
				try {
					// 日時データを解析します。
					final DateItem datetime = field.getAnnotation(DateItem.class);
					if (datetime != null) {
						final Object value = getFieldValue(obj, field);
						if (value == null) {
							// FIXME: エラーとすべきでは？
						}
						final String name = StringUtils.defaultIfEmpty(datetime.value(), field.getName());
						final DateFormatConfig config = createDateFormatConfig(field, defaultDateFormatConfig);
						final Date date = DataConvertUtils.toDate(value, config);
						inner.add(name, date);
						items.setDateFormatConfig(name, config);
					}

					// 数値データを解析します。
					final NumberItem data = field.getAnnotation(NumberItem.class);
					if (data != null) {
						final Object value = getFieldValue(obj, field);
						final String name = StringUtils.defaultIfEmpty(data.value(), field.getName());
						final NumberFormatConfig config = createNumberFormatConfig(field, defaultNumberFormatConfig);
						final Number number = DataConvertUtils.toNumber(value, config);
						inner.add(name, number);
						items.setNumberFormatConfig(name, config);
						items.setCompressType(name, getCompressorType(field, defaultCompressType));
					}
				} catch (IllegalAccessException e) {
					throw new AnnotationException(e.getMessage(), e);
				}
			}
		}

		for (final Map.Entry<String, List<Date>> entry : inner.dateMap.entrySet()) {
			items.setDate(entry.getKey(), entry.getValue().toArray(new Date[0]));
		}
		for (final Map.Entry<String, List<Number>> entry : inner.dataMap.entrySet()) {
			items.setNumber(entry.getKey(), entry.getValue().toArray(new Number[0]));
		}

		return items;
	}

	// ------------------------------------------------------------------------

	private static DateFormatConfig createDateFormatConfig(final DatePattern annotation) {
		if (annotation == null)
			return null;

		return new DateFormatConfig(
				annotation.value(),
				getLocale(annotation.language(), annotation.country(), null),
				StringUtils.isNotEmpty(annotation.timezone()) ? TimeZone.getTimeZone(annotation.timezone()) : null
			);
	}

	private static NumberFormatConfig createNumberFormatConfig(final NumberPattern annotation) {
		if (annotation == null)
			return null;

		return new NumberFormatConfig(
				annotation.value(),
				getLocale(annotation.language(), annotation.country(), null),
				annotation.currency()
			);
	}

	private static DateFormatConfig createDateFormatConfig(final Field field, final DateFormatConfig defaultConfig) {
		final DateFormatConfig config = createDateFormatConfig(field.getAnnotation(DatePattern.class));
		if (config == null) {
			return defaultConfig;
		}
		return config;
	}

	private static NumberFormatConfig createNumberFormatConfig(final Field field, final NumberFormatConfig defaultConfig) {
		final NumberFormatConfig config = createNumberFormatConfig(field.getAnnotation(NumberPattern.class));
		if (config == null) {
			return defaultConfig;
		}
		return config;
	}

	private static Locale getLocale(final String language, final String country, final Locale defaultLocale) {
		if (language != null && !language.isEmpty())
			return new Locale(language, country != null ? country : "", "");
		return defaultLocale;
	}

	private static CompressType getCompressorType(final Field field, final CompressType defaultCompressType) {
		final CompressType compressType = (CompressType) AnnotationUtils.getValue(field.getAnnotation(NumberCompressorType.class));
		if (compressType != null) {
			return compressType;
		}
		return defaultCompressType;
	}

	private static Object getFieldValue(final Object obj, final Field field) throws IllegalAccessException {
		if (!field.isAccessible()) {
			field.setAccessible(true);
		}
		return field.get(obj);
	}

}
