/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.util;

import java.util.NoSuchElementException;

/**
 * CSVTokenizer は、CSVファイルのデータを順次分割する StringTokenizer と非常に
 * 良く似たクラスです。
 *
 * StringTokenizer では、デリミタが連続する場合も、１つのデリミタとするため、
 * データが存在しない場合の表現が出来ませんでした。(例えば、AA,BB,,DD など)
 * また、デリミタをデータ中に含む場合の処理が出来ません。( AA,BB,"cc,dd",EE など)
 * この、CSVTokenizer クラスでは、データが存在しない場合もトークンとして返します。
 * また、ダブルコーテーション("")で囲まれた範囲のデリミタは無視します。
 * ただし、デリミタとしては、常に１種類の cher 文字 しか指定できません。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class CSVTokenizer {
	private int		currentPosition;
	private int		maxPosition;
	private String	str;
	private char	delimChar ;
	private boolean inQuoteFlag ;		// "" クオート処理を行うかどうか

	/**
	 * CSV 形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
	 *
	 * @param str CSV形式の文字列  改行コードを含まない。
	 * @param delim  区切り文字(１文字のみ指定可)
	 * @param inQuote  クオート処理を行うかどうか [true:行う/false:行わない]
	 */
	public CSVTokenizer( final String str, final char delim, final boolean inQuote ) {
		currentPosition = 0;
		this.str        = str;
		maxPosition     = str.length();
		delimChar       = delim;
		inQuoteFlag     = inQuote;
	}

	/**
	 * CSV 形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
	 *
	 * @param str CSV形式の文字列  改行コードを含まない。
	 * @param delim  区切り文字(１文字のみ指定可)
	 */
	public CSVTokenizer( final String str, final char delim ) {
		this(str, delim, true);
	}

	/**
	 * CSV 形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
	 *
	 * @param str CSV形式の文字列  改行コードを含まない。
	 */
	public CSVTokenizer( final String str ) {
		this(str, ',', true);
	}

	/**
	 * 次のカンマがある位置を返す。
	 * カンマが残っていない場合は skipDelimiters() == maxPosition となる。
	 * また最後の項目が空の場合も skipDelimiters() == maxPosition となる。
	 *
	 * @param startPos 検索を開始する位置
	 *
	 * @return 次のカンマがある位置。カンマがない場合は、文字列の
	 *         長さの値となる。
	 */
	private int skipDelimiters( final int startPos ) {
		boolean inquote = false;
		int position = startPos;
		while (position < maxPosition) {
			char ch = str.charAt(position);
			if(!inquote && ch == delimChar) {
				break;
			} else if('"' == ch && inQuoteFlag) {
				inquote = !inquote;       // "" クオート処理を行う
			}
			position ++;
		}
		return position;
	}

	/**
	 * トークナイザの文字列で利用できるトークンがまだあるかどうかを判定します。
	 * このメソッドが true を返す場合、それ以降の引数のない nextToken への
	 * 呼び出しは適切にトークンを返します。
	 *
	 * @return  文字列内の現在の位置の後ろに 1 つ以上の
	 *          トークンがある場合だけ true、そうでない場合は false
	 */
	public boolean hasMoreTokens() {
		int newPosition = skipDelimiters(currentPosition);
		return newPosition <= maxPosition ;		// "<" だと末尾の項目を正しく処理できない
	}

	/**
	 * 文字列トークナイザから次のトークンを返します。
	 *
	 * @og.rev 5.2.0.0 (2010/09/01) トークンの前後が '"'である場合、"で囲われた文字列中の""は"に変換します。
	 *
	 * @return     文字列トークナイザからの次のトークン
	 * @throws  NoSuchElementException トークナイザの文字列に
	 *             トークンが残っていない場合
	*/
	public String nextToken() {
		// ">=" では末尾の項目を正しく処理できない。
		// 末尾の項目が空(カンマで1行が終わる)場合、例外が発生して
		// しまうので。
		if(currentPosition > maxPosition) {
			throw new NoSuchElementException(toString()+"#nextToken");
		}

		// 3.5.4.7 (2004/02/06)
		int from = currentPosition;
		int to   = skipDelimiters(currentPosition);
		currentPosition = to + 1;
		// 5.2.0.0 (2010/09/01) トークンの前後が '"'である場合、"で囲われた文字列中の""は"に変換します。
		String rtn = null;
		// 3.5.5.8 (2004/05/20) トークンの前後が '"' なら、削除します。
		if( inQuoteFlag && from < maxPosition && from < to &&
			str.charAt(from) == '"' && str.charAt(to-1) == '"' ) {
			from++;
			to--;
			rtn = str.substring( from,to ).replace( "\"\"", "\"" );
		}
		else {
			rtn = str.substring( from,to );
		}

		return rtn;
	}

	/**
	 * 例外を生成せずにトークナイザの <code>nextToken</code> メソッドを呼び出せる
	 * 回数を計算します。現在の位置は進みません。
	 *
	 * @return  現在の区切り文字を適用したときに文字列に残っているトークンの数
	 * @see     CSVTokenizer#nextToken()
	 */
	public int countTokens() {
		int count = 1;
		int currpos = 0;
		while ((currpos = skipDelimiters(currpos)) < maxPosition) {
			currpos++;
			count++;
		}
		return count;
	}

	/**
	 * インスタンスの文字列表現を返す。
	 *
	 * @return インスタンスの文字列表現。
	 */
	@Override
	public String toString() {
		return "CSVTokenizer(" + str + ")";
	}
}
