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

import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

/**
 * 区切り文字形式出力ストリームを提供します。
 *
 * @author 杉澤 浩二
 */
public class CsvWriter implements Closeable, Flushable {

	/**
	 * 文字出力ストリームを保持します。
	 */
	private Writer out;

	/**
	 * 区切り文字形式情報を保持します。
	 */
	private CsvConfig cfg;

	private static int defaultCharBufferSize = 8192;

	// ------------------------------------------------------------------------
	// コンストラクタ

	/**
	 * 指定されたバッファーサイズと指定された区切り文字入出力設定を使用して、このクラスを構築するコンストラクタです。
	 *
	 * @param out Writer
	 * @param sz 出力バッファのサイズ
	 * @param cfg 区切り文字形式データの入出力設定情報
	 * @throws IllegalArgumentException <code>sz</code> が <code>0</code> 以下の場合。または、<code>cfg</code> が <code>null</code> の場合
	 */
	public CsvWriter(final Writer out, final int sz, final CsvConfig cfg) {
		if (cfg == null) {
			throw new IllegalArgumentException("CsvConfig must not be null");
		}
		this.out = new BufferedWriter(out, sz);
		this.cfg = cfg;
	}

	/**
	 * デフォルトのバッファーサイズと指定された区切り文字入出力設定を使用して、このクラスを構築するコンストラクタです。
	 *
	 * @param out Writer
	 * @param cfg 区切り文字形式データの入出力設定情報
	 * @throws IllegalArgumentException <code>cfg</code> が <code>null</code> の場合
	 */
	public CsvWriter(final Writer out, final CsvConfig cfg) {
		this(out, defaultCharBufferSize, cfg);
	}

	/**
	 * 指定されたバッファーサイズとデフォルトの区切り文字入出力設定を使用して、このクラスを構築するコンストラクタです。
	 *
	 * @param out Writer
	 * @param sz 出力バッファのサイズ
	 * @throws IllegalArgumentException <code>sz</code> が <code>0</code> 以下の場合
	 */
	public CsvWriter(final Writer out, final int sz) {
		this(out, sz, new CsvConfig());
	}

	/**
	 * デフォルトのバッファーサイズとデフォルトの区切り文字入出力設定を使用して、このクラスを構築するコンストラクタです。
	 *
	 * @param out Writer
	 */
	public CsvWriter(final Writer out) {
		this(out, defaultCharBufferSize, new CsvConfig());
	}

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

	/**
	 * Checks to make sure that the stream has not been closed
	 */
	private void ensureOpen() throws IOException {
		if (out == null) {
		    throw new IOException("Stream closed");
		}
	}

	/**
	 * 指定された CSV トークンの値リストを書き込みます。
	 *
	 * @param values 書き込む CSV トークンの値リスト
	 * @throws IOException 入出力エラーが発生した場合
	 */
	public void writeValues(final List<String> values) throws IOException {
		synchronized (out) {
			ensureOpen();

			final StringBuilder buf = new StringBuilder();
			if (values != null) {
				final int max = values.size();
				for (int i = 0; i < max; i++) {
					if (i > 0) {
						buf.append(cfg.getSeparator());
					}
	
					String value = values.get(i);
					boolean quoteDisabled = cfg.isQuoteDisabled();
					if (value == null) {
						if (cfg.getNullString() == null) { 
							continue;
						}
						value = cfg.getNullString();
						quoteDisabled = true;
					}
	
					if (quoteDisabled) {
						buf.append(value);
					} else {
						buf.append(cfg.getQuote());
						if (!cfg.isEscapeDisabled()) {
							buf.append(escape(value));
						} else {
							buf.append(value);
						}
						buf.append(cfg.getQuote());
					}
				}
			}
			if (values != null || !cfg.isIgnoreEmptyLines()) {
				buf.append(cfg.getLineSeparator());
				out.write(buf.toString());
			}
		}
	}

	/**
	 * 指定された CSV トークンのリストを書き込みます。
	 * 
	 * @param tokens 書き込む CSV トークンのリスト
	 * @throws IOException 入出力エラーが発生した場合
	 */
	public void writeTokens(final List<CsvToken> tokens) throws IOException {
		if (tokens != null) {
			final List<String> values = new ArrayList<String>(tokens.size());
			for (final CsvToken token : tokens) {
				if (token == null) {
					values.add(null);
				} else {
					values.add(token.getValue());
				}
			}
			writeValues(values);
		} else {
			writeValues(null);
		}
	}

	/**
	 * 指定された文字列をエスケープ化して返します。
	 *
	 * @param value 文字列
	 * @return エスケープされた文字列
	 */
	private String escape(final String value) {
		return value.replace(
				new StringBuilder(1).append(cfg.getQuote()),
				new StringBuilder(2).append(cfg.getEscape()).append(cfg.getQuote())
			);
	}

	@Override
	public void flush() throws IOException {
		synchronized (out) {
			ensureOpen();
			out.flush();
		}
	}

	@Override
	public void close() throws IOException {
		synchronized (out) {
			if (out != null) {
				out.close();
				out = null;
				cfg = null;
			}
		}
	}

}
