/*
 * このソースコードは blanco Frameworkによって自動生成されています。
 */
package blanco.nlpack.generator.csv.io;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;

import blanco.nlpack.generator.csv.record.BlancoNLpackGeneratorResultDetailCsvRecord;
import blanco.nlpack.generator.csv.util.BlancoCsvRuntimeUtil;

/**
 * ファイル定義[BlancoNLpackGeneratorResultDetail/blancoNLpackGenerator が内部的に利用するCSVファイル。翻訳フラグメントの入力ファイルとしても利用します。なお、翻訳フラグメント入力ファイルとして利用する場合には、BundleURI, Input, Output 以外の列は無視されます。]のリーダクラス
 *
 * このクラスはblancoCsvファイル定義書から自動生成されたリーダクラスです。<br>
 * 　1.文字列長チェックには、デフォルトエンコーディングを利用します。<br>
 * 　2.クラスの利用した後は必ず close()を呼び出してください。<br>
 */
public class BlancoNLpackGeneratorResultDetailCsvReader {
    /**
     * リーダオブジェクト
     *
     * 内部的に実際に入力を行うのリーダを記憶します。
     */
    private BufferedReader fReader;

    /**
     * 処理中の行カウンタ
     *
     * 現在処理を行っている行の位置を記憶します。
     */
    private long fLineCounter = 0;

    /**
     * 現在処理中の行データ
     */
    private String fLine;

    /**
     * 現在処理中の行のためのリーダ
     */
    private StringReader fLineReader;

    /**
     * CSVリーダクラスのコンストラクタ
     *
     * 与えられたリーダと連結します。
     *
     * @param arg 連結するリーダ
     */
    public BlancoNLpackGeneratorResultDetailCsvReader(final BufferedReader arg) {
        fReader = arg;
    }

    /**
     * 次の一行を読み込みます
     *
     * @return 行オブジェクトを返します。もはや行が無い場合には nullを返します。
     * @throws BlancoCsvIOException 入力データが不正な場合など。
     * @throws IOException 連結先リーダで異常が発生した場合。
     */
    public BlancoNLpackGeneratorResultDetailCsvRecord readRecord() throws BlancoCsvIOException, IOException {
        fLine = fReader.readLine();
        if (fLine == null) {
            // ファイルの終端に到達しました。
            return null;
        }
        fLineCounter++;
        final BlancoNLpackGeneratorResultDetailCsvRecord record = new BlancoNLpackGeneratorResultDetailCsvRecord();
        fLineReader = new StringReader(fLine);
        String tokenString = null;

        // 項目番号[1]項目名[BundleUri/URI]
        // クオートを考慮した読み込みを行います。
        tokenString = readTokenWithQuote(',', false);
        if (tokenString == null) {
            throw new BlancoCsvIOException("入力" + fLineCounter + "行目 1項目目。項目番号[1] 項目名[BundleUri/URI]の処理中において不正な終端を検知しました。");
        }
        // 必須項目。
        if (tokenString.length() == 0) {
            throw new BlancoCsvIOException("入力" + fLineCounter + "行目 1項目目。項目番号[1] 項目名[BundleUri/URI]の処理中において必須項目に値が入っていないことを検知しました。");
        }
        record.setBundleUri(tokenString);

        // 項目番号[2]項目名[Action/Action]
        // クオートを考慮した読み込みを行います。
        tokenString = readTokenWithQuote(',', false);
        if (tokenString == null) {
            throw new BlancoCsvIOException("入力" + fLineCounter + "行目 2項目目。項目番号[2] 項目名[Action/Action]の処理中において不正な終端を検知しました。");
        }
        // 任意項目。
        if (tokenString.length() == 0) {
            // 必須項目ではない文字列項目に長さ0の値が読み込まれた場合には、読み込み後の値としても長さ0の文字列とします。
            record.setAction(tokenString);
        } else {
            if (tokenString.getBytes().length < 3) {
                throw new BlancoCsvIOException("入力" + fLineCounter + "行目 2項目目。項目番号[2] 項目名[Action/Action]の処理中においてMIN長さ(3)よりも短い値[" + tokenString + "]を検知しました。");
            }
            record.setAction(tokenString);
        }

        // 項目番号[3]項目名[Key/Key]
        // クオートを考慮した読み込みを行います。
        tokenString = readTokenWithQuote(',', false);
        if (tokenString == null) {
            throw new BlancoCsvIOException("入力" + fLineCounter + "行目 3項目目。項目番号[3] 項目名[Key/Key]の処理中において不正な終端を検知しました。");
        }
        // 任意項目。
        if (tokenString.length() == 0) {
            // 必須項目ではない文字列項目に長さ0の値が読み込まれた場合には、読み込み後の値としても長さ0の文字列とします。
            record.setKey(tokenString);
        } else {
            if (tokenString.getBytes().length < 1) {
                throw new BlancoCsvIOException("入力" + fLineCounter + "行目 3項目目。項目番号[3] 項目名[Key/Key]の処理中においてMIN長さ(1)よりも短い値[" + tokenString + "]を検知しました。");
            }
            record.setKey(tokenString);
        }

        // 項目番号[4]項目名[Input/Input]
        // クオートを考慮した読み込みを行います。
        tokenString = readTokenWithQuote(',', false);
        if (tokenString == null) {
            throw new BlancoCsvIOException("入力" + fLineCounter + "行目 4項目目。項目番号[4] 項目名[Input/Input]の処理中において不正な終端を検知しました。");
        }
        // 任意項目。
        if (tokenString.length() == 0) {
            // 必須項目ではない文字列項目に長さ0の値が読み込まれた場合には、読み込み後の値としても長さ0の文字列とします。
            record.setInput(tokenString);
        } else {
            record.setInput(tokenString);
        }

        // 項目番号[5]項目名[Output/Output]
        // クオートを考慮した読み込みを行います。
        tokenString = readTokenWithQuote(',', false);
        if (tokenString == null) {
            throw new BlancoCsvIOException("入力" + fLineCounter + "行目 5項目目。項目番号[5] 項目名[Output/Output]の処理中において不正な終端を検知しました。");
        }
        // 任意項目。
        if (tokenString.length() == 0) {
            // 必須項目ではない文字列項目に長さ0の値が読み込まれた場合には、読み込み後の値としても長さ0の文字列とします。
            record.setOutput(tokenString);
        } else {
            record.setOutput(tokenString);
        }

        // 項目番号[6]項目名[Note/Note]
        // クオートを考慮した読み込みを行います。
        tokenString = readTokenWithQuote(',', true);
        if (tokenString == null) {
            throw new BlancoCsvIOException("入力" + fLineCounter + "行目 6項目目。項目番号[6] 項目名[Note/Note]の処理中において不正な終端を検知しました。");
        }
        // 任意項目。
        if (tokenString.length() == 0) {
            // 必須項目ではない文字列項目に長さ0の値が読み込まれた場合には、読み込み後の値としても長さ0の文字列とします。
            record.setNote(tokenString);
        } else {
            record.setNote(tokenString);
        }
        return record;
    }

    /**
     * 内部的に保持しているリーダを返します。
     *
     * @return 内部的に保持しているリーダ。
     */
    public BufferedReader getReader() {
        return fReader;
    }

    /**
     * 現在処理中の行カウンタを返します。
     *
     * @return 現在処理中の行カウンタ。
     */
    public long getLineCounter() {
        return fLineCounter;
    }

    /**
     * このリーダを閉じます。
     *
     * 関連づけられていたリーダに対してもclose()を呼び出します。
     *
     * @throws IOException 関連づけられていたリーダのclose()に失敗した場合。
     */
    public void close() throws IOException {
        fReader.close();
    }

    /**
     * 与えられたReaderから 与えられたデリミタを使ってトークンを取り出します
     *
     * @param delimiter デリミタ
     * @param isEndOfLine 行の終端であるのかどうかフラグ
     * @return 取り出されたトークン。もはやリーダが空の場合にはnullを返します。
     * @throws BlancoCsvIOException 項目数が足りないなど与えられた文字列に関する例外が発生した場合。
     * @throws IOException 入出力例外が発生した場合。
     */
    protected String readToken(final char delimiter, final boolean isEndOfLine) throws BlancoCsvIOException, IOException {
        final StringBuffer buffer = new StringBuffer();
        for (;;) {
            final int iRead = fLineReader.read();
            if (iRead < 0) {
                // 終端に到達。
                if (isEndOfLine == false) {
                    throw new BlancoCsvIOException("行の終端でないのにも関わらずデリミタが現れずに行が終了してしまいました。");
                }
                break;
            }
            if (iRead == delimiter) {
                if (isEndOfLine) {
                    throw new BlancoCsvIOException("行の終端であるべきにも関わらずデリミタが現れました。");
                }
                break;
            } else {
                buffer.append((char) iRead);
            }
        }
        return buffer.toString();
    }

    /**
     * 与えられたReaderから ダブルクオート処理付きで 与えられたデリミタを使ってトークンを取り出します
     *
     * @param delimiter デリミタ
     * @param isEndOfLine 行の終端であるのかどうかフラグ
     * @return 取り出されたトークン。もはやリーダが空の場合にはnullを返します。
     * @throws BlancoCsvIOException ダブルクオートの不一致など与えられた文字列に関する例外が発生した場合。
     * @throws IOException 入出力例外が発生した場合。
     */
    protected String readTokenWithQuote(final char delimiter, final boolean isEndOfLine) throws BlancoCsvIOException, IOException {
        final StringBuffer buffer = new StringBuffer();
        boolean isStringStarted = false;
        boolean isStringEnded = false;
        for (;;) {
            int iRead = fLineReader.read();
            if (iRead < 0) {
                // 終端に到達。
                if (isStringStarted && isStringEnded == false) {
                    // 改行を出力。
                    buffer.append('\n');
                    // ダブルクオート内。
                    fLine = fReader.readLine();
                    if (fLine == null) {
                        // ダブルクオート内であるのにもかかわらずファイルの終端に到達しました。
                        break;
                    } else {
                        fLineReader = new StringReader(fLine);
                        continue;
                    }
                } else if (isEndOfLine == false) {
                    throw new BlancoCsvIOException("行の終端でないのにも関わらずデリミタが現れずに行が終了してしまいました。");
                }
                break;
            }
            if (isStringStarted == false) {
                if (iRead != '"') {
                    throw new BlancoCsvIOException("ダブルクオート無しで文字列が開始されました。");
                }
                // ダブルクオーテーションは読み飛ばします。
                isStringStarted = true;
            } else if (isStringEnded) {
                if (iRead == delimiter) {
                    // 文字列終端に到達しました。
                    if (isEndOfLine) {
                        throw new BlancoCsvIOException("行の終端であるべきにも関わらずデリミタが現れました。");
                    }
                    break;
                }
                throw new BlancoCsvIOException("ダブルクオートによる文字列終了より後に文字列が与えられました。");
            } else {
                // 通常の文字列エリア
                if (iRead == '"') {
                    // ダブルクオートが重ねられたものでないかどうかチェックします。
                    fLineReader.mark(1);
                    if (fLineReader.read() == '"') {
                        // エスケープされたダブルクオート
                        // 2つでひとつなので、ひとつは読み捨てます。
                        buffer.append((char) iRead);
                    } else {
                        // ダブルクオートによるエスケープではありませんでした。
                        // これは終端を意味しています。
                        fLineReader.reset();
                        isStringEnded = true;
                    }
                } else {
                    buffer.append((char) iRead);
                }
            }
        }
        if (isStringStarted == false) {
            throw new BlancoCsvIOException("ダブルクオートが必要な文字列であるのにダブルクオートによる開始が現れませんでした。");
        }
        if (isStringEnded == false) {
            throw new BlancoCsvIOException("ダブルクオートが必要な文字列であるのにダブルクオートによる終了が現れませんでした。");
        }
        return buffer.toString();
    }
}
