/*
 * Copyright 2004-2013 the Seasar Foundation and the Others.
 *
 * 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.seasar.extension.jdbc.gen.internal.data;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import org.seasar.extension.jdbc.gen.internal.data.DumpFileTokenizer.TokenType;
import org.seasar.extension.jdbc.gen.internal.exception.IllegalDumpColumnSizeRuntimeException;
import org.seasar.extension.jdbc.gen.internal.util.CloseableUtil;
import org.seasar.extension.jdbc.gen.internal.util.DumpUtil;
import org.seasar.framework.exception.IORuntimeException;
import org.seasar.framework.util.StringUtil;

/**
 * ダンプファイルのリーダです。
 * 
 * @author taedium
 */
public class DumpFileReader {

    /** バッファのサイズ */
    protected static final int BUF_SIZE = 8192;

    /** ダンプファイル */
    protected File dumpFile;

    /** ダンプファイルのエンコーディング */
    protected String dumpFileEncoding;

    /** トークナイザ */
    protected DumpFileTokenizer tokenizer;

    /** リーダ */
    protected BufferedReader reader;

    /** バッファ */
    protected char[] buf = new char[BUF_SIZE];

    /** トークンタイプ */
    protected TokenType tokenType = TokenType.END_OF_BUFFER;

    /** 行番号 */
    protected int lineNumber;

    /** ヘッダーの列数 */
    protected int headerColumnSize;

    /** ファイルの終端の場合{@code true} */
    protected boolean endOfFile;

    /**
     * インスタンスを構築します。
     * 
     * @param dumpFile
     *            ダンプファイル
     * @param dumpFileEncoding
     *            ダンプファイルのエンコーディング
     * @param tokenizer
     *            トークナイザ
     */
    public DumpFileReader(File dumpFile, String dumpFileEncoding,
            DumpFileTokenizer tokenizer) {
        if (dumpFile == null) {
            throw new NullPointerException("dumpFile");
        }
        if (dumpFileEncoding == null) {
            throw new NullPointerException("dumpFileEncoding");
        }
        if (tokenizer == null) {
            throw new NullPointerException("tokenizer");
        }
        this.dumpFile = dumpFile;
        this.dumpFileEncoding = dumpFileEncoding;
        this.tokenizer = tokenizer;
    }

    /**
     * 一行を読みます。
     * 
     * @return ファイルの終端に達していなければ一行を表すリスト、ファイルの終端に達していれば{@code null}
     */
    public List<String> readLine() {
        if (endOfFile) {
            return null;
        }
        try {
            lineNumber++;
            List<String> valueList = readLineInternal();
            if (valueList == null) {
                lineNumber--;
                return null;
            }
            if (lineNumber == 1) {
                headerColumnSize = valueList.size();
            } else {
                if (headerColumnSize != valueList.size()) {
                    throw new IllegalDumpColumnSizeRuntimeException(dumpFile
                            .getPath(), lineNumber, valueList.size(),
                            headerColumnSize);
                }
            }
            return valueList;
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * 内部的に一行を読みます。
     * 
     * @return ファイルの終端に達していなければ一行を表すリスト、ファイルの終端に達していれば{@code null}
     * @throws IOException
     *             何らかのIO例外が発生した場合
     */
    protected List<String> readLineInternal() throws IOException {
        if (reader == null) {
            reader = createBufferedReader();
        }
        Line line = new Line();
        readLoop: for (;;) {
            if (tokenType == TokenType.END_OF_BUFFER) {
                int length = reader.read(buf);
                tokenizer.addChars(buf, length);
            }
            for (;;) {
                tokenType = tokenizer.nextToken();
                String token = tokenizer.getToken();
                switch (tokenType) {
                case VALUE:
                case NULL:
                    line.add(token);
                    break;
                case END_OF_BUFFER:
                    continue readLoop;
                case END_OF_LINE:
                    return line.toList();
                case END_OF_FILE:
                    endOfFile = true;
                    if (!StringUtil.isEmpty(token)) {
                        line.add(token);
                    }
                    return line.toList();
                default:
                    break;
                }
            }
        }
    }

    /**
     * {@link BufferedReader}を作成します。
     * 
     * @return {@link BufferedReader}
     * @throws IOException
     *             何らかのIO例外が発生した場合
     */
    protected BufferedReader createBufferedReader() throws IOException {
        InputStream is = new FileInputStream(dumpFile);
        return new BufferedReader(new InputStreamReader(is, dumpFileEncoding));
    }

    /**
     * 読み込んだ行番号を返します。
     * <p>
     * 行番号は1から始まります。
     * </p>
     * 
     * @return 行番号
     */
    public int getLineNumber() {
        return lineNumber;
    }

    /**
     * クローズします。
     */
    public void close() {
        CloseableUtil.close(reader);
    }

    /**
     * 行を表すクラスです。
     * 
     * @author taedium
     */
    protected class Line {

        /** 値のリスト */
        protected List<String> valueList = new ArrayList<String>(30);

        /**
         * 値のリストを返します。
         * 
         * @return 値のリスト
         */
        public List<String> toList() {
            return !valueList.isEmpty() ? valueList : null;
        }

        /**
         * 値を追加します。
         * 
         * @param value
         *            値
         */
        public void add(String value) {
            valueList.add(DumpUtil.decode(value));
        }
    }

}
