/*
 * Copyright (c) 2006-2009 OrangeSignal.com All rights reserved.
 */

package jp.sourceforge.orangesignal.ta.dataset.loader;

import java.io.IOException;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jp.sourceforge.orangesignal.ta.dataset.PriceData;

/**
 * ファイルを読込んで価格データをロードするクラスを提供します。
 * 
 * @author 杉澤 浩二
 * @since 1.1
 */
public final class PriceDataLoader {

	/**
	 * デフォルトの区切り文字です。
	 */
	protected static final String DEFAULT_SEPARATOR = ",";

	/**
	 * デフォルトのエンコーディングです。
	 */
	protected static final String DEFAULT_ENCODING = "Windows-31J";

	/**
	 * デフォルトの列の種類と列位置の関係です。
	 */
	protected static final Map<PriceDataColumnType, Integer> DEFAULT_COLUMN_MAP = new EnumMap<PriceDataColumnType, Integer>(PriceDataColumnType.class);

	static {
		// デフォルトの列の種類と列位置の関係を構築します。
		DEFAULT_COLUMN_MAP.put(PriceDataColumnType.DATE, 0);
		DEFAULT_COLUMN_MAP.put(PriceDataColumnType.OPEN, 1);
		DEFAULT_COLUMN_MAP.put(PriceDataColumnType.HIGH, 2);
		DEFAULT_COLUMN_MAP.put(PriceDataColumnType.LOW, 3);
		DEFAULT_COLUMN_MAP.put(PriceDataColumnType.CLOSE, 4);
		DEFAULT_COLUMN_MAP.put(PriceDataColumnType.VOLUME, 5);
	}

	/**
	 * デフォルトの見出しマップです。
	 */
	protected static final Map<String, PriceDataColumnType> DEFAULT_TITLE_MAP = new HashMap<String, PriceDataColumnType>();

	static {
		// デフォルトの見出しマップを構築します。
		DEFAULT_TITLE_MAP.put("日時", PriceDataColumnType.DATE);
		DEFAULT_TITLE_MAP.put("始値", PriceDataColumnType.OPEN);
		DEFAULT_TITLE_MAP.put("高値", PriceDataColumnType.HIGH);
		DEFAULT_TITLE_MAP.put("安値", PriceDataColumnType.LOW);
		DEFAULT_TITLE_MAP.put("終値", PriceDataColumnType.CLOSE);
		DEFAULT_TITLE_MAP.put("出来高", PriceDataColumnType.VOLUME);

		DEFAULT_TITLE_MAP.put("日", PriceDataColumnType.DATE);
//		DEFAULT_TITLE_MAP.put("時", DataColumnType.TIME);
		DEFAULT_TITLE_MAP.put("始", PriceDataColumnType.OPEN);
		DEFAULT_TITLE_MAP.put("高", PriceDataColumnType.HIGH);
		DEFAULT_TITLE_MAP.put("安", PriceDataColumnType.LOW);
		DEFAULT_TITLE_MAP.put("終", PriceDataColumnType.CLOSE);
		DEFAULT_TITLE_MAP.put("出", PriceDataColumnType.VOLUME);

		DEFAULT_TITLE_MAP.put("D", PriceDataColumnType.DATE);
//		DEFAULT_TITLE_MAP.put("T", DataColumnType.TIME);
		DEFAULT_TITLE_MAP.put("O", PriceDataColumnType.OPEN);
		DEFAULT_TITLE_MAP.put("H", PriceDataColumnType.HIGH);
		DEFAULT_TITLE_MAP.put("L", PriceDataColumnType.LOW);
		DEFAULT_TITLE_MAP.put("C", PriceDataColumnType.CLOSE);
		DEFAULT_TITLE_MAP.put("V", PriceDataColumnType.VOLUME);

		DEFAULT_TITLE_MAP.put("d", PriceDataColumnType.DATE);
//		DEFAULT_TITLE_MAP.put("t", DataColumnType.TIME);
		DEFAULT_TITLE_MAP.put("o", PriceDataColumnType.OPEN);
		DEFAULT_TITLE_MAP.put("h", PriceDataColumnType.HIGH);
		DEFAULT_TITLE_MAP.put("l", PriceDataColumnType.LOW);
		DEFAULT_TITLE_MAP.put("c", PriceDataColumnType.CLOSE);
		DEFAULT_TITLE_MAP.put("v", PriceDataColumnType.VOLUME);
	}

	/**
	 * デフォルトの日付書式文字列のリストです。
	 */
	protected static final String[] DEFAULT_DATE_PATTERNS = new String[]{ "yyyy/MM/dd", "yyyy/M/d" };

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

	/**
	 * 列の種類と列位置の関係を指定してこのクラスを構築するコンストラクタです。
	 * 
	 * @param columnMap 列の種類と列位置の関係
	 */
	public PriceDataLoader(final Map<PriceDataColumnType, Integer> columnMap) {
		setColumnMap(columnMap);
	}

	/**
	 * 区切り文字を保持します。
	 */
	private String separator = DEFAULT_SEPARATOR;

	/**
	 * <p>区切り文字を返します。</p>
	 * <p>デフォルト値は {@link #DEFAULT_SEPARATOR} の値です。</p>
	 * 
	 * @return 区切り文字
	 */
	public String getSeparator() { return separator; }

	/**
	 * 区切り文字を設定します。
	 * 
	 * @param separator 区切り文字
	 */
	public void setSeparator(final String separator) { this.separator = separator; }

	/**
	 * エンコーディングを保持します。
	 */
	private String encoding = DEFAULT_ENCODING;

	/**
	 * <p>エンコーディングを返します。</p>
	 * <p>デフォルト値は {@link #DEFAULT_ENCODING} の値です。</p>
	 * 
	 * @return エンコーディング
	 */
	public String getEncoding() { return encoding; }

	/**
	 * エンコーディングを設定します。
	 * 
	 * @param encoding エンコーディング
	 */
	public void setEncoding(final String encoding) { this.encoding = encoding; }

	/**
	 * 列の種類と列位置の関係を保持します。
	 */
	private Map<PriceDataColumnType, Integer> columnMap = DEFAULT_COLUMN_MAP;

	/**
	 * 列の種類と列位置の関係を返します。
	 * 
	 * @return 列の種類と列位置の関係
	 */
	public Map<PriceDataColumnType, Integer> getColumnMap() { return columnMap; }

	/**
	 * 列の種類と列位置の関係を設定します。
	 * 
	 * @param columnMap 列の種類と列位置の関係
	 */
	public void setColumnMap(final Map<PriceDataColumnType, Integer> columnMap) { this.columnMap = columnMap; }

	/**
	 * 列見出しの行位置を保持します。
	 */
	private int titleRow = 0;

	/**
	 * 列見出しの行位置を返します。
	 * 
	 * @return 列見出しの行位置
	 */
	public int getTitleRow() { return titleRow; }

	/**
	 * 列見出しの行位置を設定します。
	 * 
	 * @param titleRow 列見出しの行位置
	 */
	public void setTitleRow(final int titleRow) { this.titleRow = titleRow; }

	/**
	 * 列見出しと列の種類の関係を保持します。
	 */
	private Map<String, PriceDataColumnType> titleMap = DEFAULT_TITLE_MAP;

	/**
	 * 列見出しと列の種類の関係を返します。
	 * 
	 * @return 列見出しと列の種類の関係
	 */
	public Map<String, PriceDataColumnType> getTitleMap() { return titleMap; }

	/**
	 * 列見出しと列の種類の関係を設定します。
	 * 
	 * @param titleMap 列見出しと列の種類の関係
	 */
	public void setTitleMap(final Map<String, PriceDataColumnType> titleMap) { this.titleMap = titleMap; }

	/**
	 * データの開始行位置を保持します。
	 */
	private int startRow = 1;	// 絶対位置

	/**
	 * データの開始行位置を返します。
	 * 
	 * @return データの開始行位置
	 */
	public int getStartRow() { return startRow; }

	/**
	 * データの開始行位置を設定します。
	 * 
	 * @param startRow データの開始行位置
	 */
	public void setStartRow(final int startRow) { this.startRow = startRow; }

	/**
	 * 日付書式文字列のリストを保持します。
	 */
	private String[] datePatterns = DEFAULT_DATE_PATTERNS;

	/**
	 * 日付書式文字列のリストを返します。
	 * 
	 * @return 日付書式文字列のリスト
	 */
	public String[] getDatePatterns() { return datePatterns; }

	/**
	 * 日付書式文字列のリストを設定します。
	 * 
	 * @param datePatterns 日付書式文字列のリスト
	 */
	public void setDatePatterns(final String[] datePatterns) { this.datePatterns = datePatterns; }

	/**
	 * 数値書式文字列のリストを保持します。
	 */
	private String[] numberPatterns = null;

	/**
	 * 数値書式文字列のリストを返します。
	 * 
	 * @return 数値書式文字列のリスト
	 */
	public String[] getNumberPatterns() { return numberPatterns; }

	/**
	 * 数値書式文字列のリストを設定します。
	 * 
	 * @param numberPatterns 数値書式文字列のリスト
	 */
	public void setNumberPatterns(final String[] numberPatterns) { this.numberPatterns = numberPatterns; }

	/**
	 * エラーを無視するかどうかを保持します。
	 */
	private boolean ignore = true;

	/**
	 * エラーを無視するかどうかを返します。
	 * 
	 * @return エラーを無視するかどうか
	 */
	public boolean isIgnore() { return ignore; }

	/**
	 * エラーを無視するかどうかを設定します。
	 * 
	 * @param ignore エラーを無視するかどうか
	 */
	public void setIgnore(final boolean ignore) { this.ignore = ignore; }

	/**
	 * 指定されたファイルから価格データを読込んで返します。
	 * 
	 * @param filename ファイル名
	 * @return 価格データのリスト
	 * @throws IOException ファイルの入力操作で例外が発生した場合
	 * @throws IllegalStateException 列見出しと列の種類の関係及び列見出しの行位置のいずれも指定されていない場合
	 * @throws LoadException ファイルデータから価格データへの変換に失敗した場合
	 */
	public List<PriceData> load(final String filename) throws IOException, IllegalStateException, LoadException {
		// データファイルを読込みます。
		final List<String[]> data = TextDataUtils.read(filename, encoding, separator);

		final Map<PriceDataColumnType, Integer> map;
		if (columnMap != null)
			map = columnMap;
		else if (titleRow >= 0)
			map = parseTitle(data.get(titleRow));
		else
			throw new IllegalStateException();

		return toPrices(data, map, ignore);
	}

	/**
	 * 指定された列見出し行データを解析して列の種類と列位置の関係を返します。
	 * 
	 * @param title 列見出し行データ
	 * @return 列の種類と列位置の関係
	 */
	private Map<PriceDataColumnType, Integer> parseTitle(final String[] title) {
		Map<PriceDataColumnType, Integer> results = new EnumMap<PriceDataColumnType, Integer>(PriceDataColumnType.class);
		if (title != null) {
			final int length = title.length;
			for (int i = 0; i < length; i++) {
				final String item = title[i];
				if (item == null || item.isEmpty())
					continue;
				final PriceDataColumnType key = titleMap.get(item);
				if (key != null)
					results.put(key, i);
			}
		}
		return results;
	}

	private List<PriceData> toPrices(
			final List<String[]> data,
			final Map<PriceDataColumnType, Integer> columnMap,
			final boolean ignore)
	throws LoadException {
		// 各列位置を取得します。
		final Integer date = columnMap.get(PriceDataColumnType.DATE);
		final Integer open = columnMap.get(PriceDataColumnType.OPEN);
		final Integer high = columnMap.get(PriceDataColumnType.HIGH);
		final Integer low = columnMap.get(PriceDataColumnType.LOW);
		final Integer close = columnMap.get(PriceDataColumnType.CLOSE);
		final Integer volume = columnMap.get(PriceDataColumnType.VOLUME);
		// データを変換して返します。
		return toPrices(data, startRow, date, open, high, low, close, volume != null ? volume : -1, ignore);
	}
	
	private List<PriceData> toPrices(
			final List<String[]> data,
			final int start,
			final int date,
			final int open,
			final int high,
			final int low,
			final int close,
			final int volume,
			final boolean ignore)
	throws LoadException {
		final List<PriceData> results = new ArrayList<PriceData>(data.size());
		final int length = data.size();
		for (int i = start; i < length; i++) {
			try {
				final PriceData price = toPrice(data.get(i), date, open, high, low, close, volume);
				if (price != null)
					results.add(price);
			} catch (Exception e) {
				if (!ignore) throw new LoadException(e.getMessage(), e);
			}
		}
		return results;
	}

	/**
	 * 
	 * @param items データ項目群
	 * @param date 日付の列位置
	 * @param open 始値の列位置
	 * @param high 高値の列位置
	 * @param low 安値の列位置
	 * @param close 終値の列位置
	 * @param volume 出来高の列位置(出来高がない場合は-1)
	 * @return 価格情報
	 * @throws IllegalArgumentException
	 * @throws ParseException 
	 */
	private PriceData toPrice(
			final String[] items,
			final int date,
			final int open,
			final int high,
			final int low,
			final int close,
			final int volume)
	throws IllegalArgumentException, ParseException {
		final OHLCV result = new OHLCV();

		if (items[date] == null || items[date].isEmpty())
			return null;
		if (datePatterns != null && datePatterns.length > 0)
			result.date = parseDate(items[date], datePatterns);
		else
			result.date = new SimpleDateFormat().parse(items[date]);

		if (numberPatterns != null && numberPatterns.length > 0) {
			result.open = parseNumber(items[open], numberPatterns);
			result.high = parseNumber(items[high], numberPatterns);
			result.low = parseNumber(items[low], numberPatterns);
			result.close = parseNumber(items[close], numberPatterns);
			if (volume >= 0)
				result.volume = parseNumber(items[volume], numberPatterns);
		} else {
			result.open = Double.parseDouble(items[open]);
			result.high = Double.parseDouble(items[high]);
			result.low = Double.parseDouble(items[low]);
			result.close = Double.parseDouble(items[close]);
			if (volume >= 0)
				result.volume = Double.parseDouble(items[volume]);
		}
		return result;
	}

	private static Date parseDate(
			final String str,
			final String[] parsePatterns)
	throws IllegalArgumentException, ParseException {
		if (str == null || parsePatterns == null)
			throw new IllegalArgumentException("Date and Patterns must not be null");

		SimpleDateFormat parser = null;
		ParsePosition pos = new ParsePosition(0);
		for (int i = 0; i < parsePatterns.length; i++) {
			if (i == 0) {
				parser = new SimpleDateFormat(parsePatterns[0]);
			} else {
				parser.applyPattern(parsePatterns[i]);
			}
			pos.setIndex(0);
			Date date = parser.parse(str, pos);
			if (date != null && pos.getIndex() == str.length()) {
				return date;
			}
		}
		throw new ParseException("Unable to parse the date: " + str, -1);
	}

	private static Number parseNumber(
			final String str,
			final String[] parsePatterns)
	throws IllegalArgumentException, ParseException {
		if (str == null || parsePatterns == null)
			throw new IllegalArgumentException("Number and Patterns must not be null");

		DecimalFormat parser = null;
		ParsePosition pos = new ParsePosition(0);
		for (int i = 0; i < parsePatterns.length; i++) {
			if (i == 0) {
				parser = new DecimalFormat(parsePatterns[0]);
			} else {
				parser.applyPattern(parsePatterns[i]);
			}
			pos.setIndex(0);
			Number number = parser.parse(str, pos);
			if (number != null && pos.getIndex() == str.length()) {
				return number;
			}
		}
		throw new ParseException("Unable to parse the number: " + str, -1);
	}

}
