/*
 * 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;
import java.util.Iterator;

import static org.opengion.fukurou.system.HybsConst.CR;				// 6.4.5.1 (2016/04/28)

/**
 * CSVTokenizer は、CSVファイルのデータを順次分割する StringTokenizer と非常に
 * 良く似たクラスです。
 *
 * StringTokenizer では、デリミタが連続する場合も、１つのデリミタとするため、
 * データが存在しない場合の表現が出来ませんでした。(例えば、AA,BB,,DD など)
 * 現在では、StringTokenizerの代わりに、String#split(String)が推奨されており、
 * これなら、区切り文字を正規表現で指定できるため、両方の形式に対応できます。
 * ただし、ダブルクオート等で囲われたデータを塊として処理することが出来ません。
 * ( AA,BB,"cc,dd",EE などを、「AA」「BB」「cc,dd」「EE」に分割したい、など)
 *
 * この、CSVTokenizer クラスでは、データが存在しない場合もトークンとして返します。
 *
 * また、ダブルコーテーション("")で囲まれた範囲のデリミタは無視します。
 * ("") は、２重にすることで、エスケープできます。これにより、改行コードで分割した
 * １行の中に、("")が奇数の場合は、次の行は、続いていると判定できます。
 * ただし、デリミタとしては、文字列は指定できず、cher 1文字 しか指定できません。
 *
 * @og.rev 6.4.5.1 (2016/04/28) Iteratorインタフェース化。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class CSVTokenizer implements Iterator<String> {
	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 のインスタンスを作成する。
	 *
	 * inQuote=true を初期値設定されます。
	 *
	 * @og.rev 6.4.5.1 (2016/04/28) Iteratorインタフェース化。
	 *
	 * @param str CSV形式の文字列  改行コードを含まない。
	 * @param delim  区切り文字(１文字のみ指定可)
	 */
	public CSVTokenizer( final String str, final char delim ) {
		this( str, delim, true );
	}

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

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

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

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

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

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