/*
 * 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;

import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import jp.sf.orangesignal.ta.TechnicalAnalysis;
import jp.sf.orangesignal.ta.util.Assert;

/**
 * {@link DatasetSource} のデフォルトの実装クラスを提供します。
 * 
 * @author 杉澤 浩二
 * @since 2.1
 */
public class DatasetItems implements Serializable, DatasetSource {

	private static final long serialVersionUID = -8136157897132992167L;

	/**
	 * 名前と日時データのマップを保持します。
	 */
	private Map<String, Date[]> dateMap = new HashMap<String, Date[]>();

	/**
	 * 名前と数値データのマップを保持します。
	 */
	private Map<String, Number[]> numberMap = new HashMap<String, Number[]>();

	/**
	 * 名前と日時書式文字列情報のマップを保持します。
	 */
	private Map<String, DateFormatConfig> dateFormatConfigMap = new HashMap<String, DateFormatConfig>();

	/**
	 * 名前と数値文字列解析設定情報のマップを保持します。
	 */
	private Map<String, NumberFormatConfig> numberFormatConfigMap = new HashMap<String, NumberFormatConfig>();

	/**
	 * 名前と足単位変換の種類のマップを保持します。
	 */
	private Map<String, CompressType> compressTypeMap = new HashMap<String, CompressType>();

	/**
	 * デフォルトコンストラクタです。
	 */
	public DatasetItems() {}

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

	@Override
	public Map.Entry<String, Date[]> getUniqueDateEntry() throws IllegalStateException {
		if (dateMap.size() == 1) {
			for (final Entry<String, Date[]> entry : dateMap.entrySet()) {
				return entry;
			}
		}
		throw new IllegalStateException("Non-unique date");
	}

	@Override public Date[] getDate(final String name) { return this.dateMap.get(name); }
	@Override public Map<String, Date[]> getDateMap() { return dateMap; }

	/**
	 * 指定された名前をキーとして指定された日時データを関連付けます。
	 * 
	 * @param name 名前
	 * @param date 日時データ
	 */
	public void setDate(final String name, final Date[] date) { this.dateMap.put(name, date); }

	@Override
	public Map.Entry<String, Number[]> getUniqueNumberEntry() throws IllegalStateException {
		if (numberMap.size() == 1) {
			for (final Entry<String, Number[]> entry : numberMap.entrySet()) {
				return entry;
			}
		}
		throw new IllegalStateException("Non-unique data");
	}

	@Override public Number[] getNumber(final String name) { return this.numberMap.get(name); }
	@Override public Map<String, Number[]> getNumberMap() { return numberMap; }

	/**
	 * 指定された名前をキーとして指定された数値データを関連付けます。
	 * 
	 * @param key 名前
	 * @param data 数値データ
	 */
	public void setNumber(final String key, final Number[] data) { this.numberMap.put(key, data); }

	@Override public DateFormatConfig getDateFormatConfig(final String name) { return this.dateFormatConfigMap.get(name); }

	/**
	 * 指定された名前をキーとして指定された日時書式文字列情報を関連付けます。
	 * 
	 * @param name 名前
	 * @param config 日時書式文字列情報
	 */
	public void setDateFormatConfig(final String name, final DateFormatConfig config) { this.dateFormatConfigMap.put(name, config); }

	@Override public NumberFormatConfig getNumberFormatConfig(final String name) { return this.numberFormatConfigMap.get(name); }

	/**
	 * 指定された名前をキーとして指定された数値/通貨書式文字列情報を関連付けます。
	 * 
	 * @param name 名前
	 * @param config 数値/通貨書式文字列情報
	 */
	public void setNumberFormatConfig(final String name, final NumberFormatConfig config) { this.numberFormatConfigMap.put(name, config); }

	@Override public CompressType getCompressType(final String key) { return this.compressTypeMap.get(key); }
	@Override public Map<String, CompressType> getCompressTypeMap() { return compressTypeMap; }

	/**
	 * 指定された名前をキーとして指定された日時精度単位変換の種類を関連付けます。
	 * 
	 * @param name 名前
	 * @param compressType 日時精度単位変換の種類
	 */
	public void setCompressType(final String name, final CompressType compressType) { this.compressTypeMap.put(name, compressType); }

	@Override
	public boolean isCompressable() {
		for (final Map.Entry<String, CompressType> entry : compressTypeMap.entrySet()) {
			if (entry.getValue() != null) {
				return true;
			}
		}
		return false;
	}

	// ------------------------------------------------------------------------
	// merge

	@Override
	public DatasetItems merge(final DatasetSource items, final MergeMatchType matchType) {
		return merge(items, matchType, null, null);
	}

	@Override
	public DatasetItems merge(final DatasetSource items, final MergeMatchType matchType, final MergeGapFillType fillType, final Number fill) {
		Assert.notNull(items, "Items must not be null");
		Assert.isTrue(matchType != MergeMatchType.INSERT, "Unsupported merge match type: " + matchType);

		final Map.Entry<String, Date[]> baseEntry = getUniqueDateEntry();
		final Date[] base = baseEntry.getValue();
		final Date[] date = items.getUniqueDateEntry().getValue();

		// 指定されたデータをマージします。
		for (final Map.Entry<String, Number[]> entry : items.getNumberMap().entrySet()) {
			final Map<Date, Number> map = DataConvertUtils.merge(base, date, entry.getValue(), matchType, fillType, fill);
			numberMap.put(entry.getKey(), map.values().toArray(new Number[0]));
			// 日時データを設定します。
			dateMap.put(baseEntry.getKey(), map.keySet().toArray(new Date[0]));
		}
		return this;
	}

	// ------------------------------------------------------------------------
	// compress

	@Override
	public DatasetItems compress(final int unit) {
		return compress(unit, Calendar.getInstance(), new DateTruncater());
	}

	@Override
	public DatasetItems compress(final int unit, final Calendar calendar) {
		return compress(unit, calendar, new DateTruncater());
	}

	@Override
	public DatasetItems compress(final int unit, final Calendar calendar, final DateTruncater truncater) {
		if (!isCompressable())
			return this;

		final Map.Entry<String, Date[]> dateEntry = getUniqueDateEntry();
		final Date[] date = dateEntry.getValue();

		for (final Map.Entry<String, Number[]> data : numberMap.entrySet()) {
			final CompressType compressType = compressTypeMap.get(data.getKey());
			if (compressType != null) {
				final Map<Date, Number> map = DataConvertUtils.compress(date, data.getValue(), compressType, unit, calendar, truncater);
				dateMap.put(dateEntry.getKey(), map.keySet().toArray(new Date[0]));
				data.setValue(map.values().toArray(new Number[0]));
			} else {
				data.setValue(null);
			}
		}
		return this;
	}

	// ------------------------------------------------------------------------
	// split

	@Override
	public DatasetItems split(final String key, final String... names) {
		final Number[] split = numberMap.get(key);
		Assert.notNull(split, String.format("Invalid split key %s", key));
		final String[] _names = (names != null && names.length > 0) ? names : numberMap.keySet().toArray(new String[0]);
		for (final Map.Entry<String, Number[]> data : numberMap.entrySet()) {
			if (data.getKey().equals(key)) {
				continue;
			}
			for (final String _name : _names) {
				if (data.getKey().equals(_name)) {
					data.setValue(TechnicalAnalysis.split(data.getValue(), split));
				}
			}
		}
		return this;
	}

	// ------------------------------------------------------------------------
	// dataset

	@Override
	public <T> DatasetBuilder<T> bind(final T dataset) {
		return new DatasetBuilder<T>(this, dataset);
	}

	@Override
	public <T> DatasetBuilder<T> build(final Class<T> datasetClass) {
		Assert.notNull(datasetClass, "datasetClass must not be null");
		try {
			return new DatasetBuilder<T>(this, datasetClass.newInstance());
		} catch (IllegalAccessException e) {
			throw new DatasetPreparationException(e.getMessage(), e);
		} catch (InstantiationException e) {
			throw new DatasetPreparationException(e.getMessage(), e);
		}
	}

}
