/*
 * Copyright 2004-2015 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.File;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import org.seasar.extension.jdbc.gen.data.Loader;
import org.seasar.extension.jdbc.gen.desc.ColumnDesc;
import org.seasar.extension.jdbc.gen.desc.DatabaseDesc;
import org.seasar.extension.jdbc.gen.desc.TableDesc;
import org.seasar.extension.jdbc.gen.dialect.GenDialect;
import org.seasar.extension.jdbc.gen.exception.LoadFailedRuntimeException;
import org.seasar.extension.jdbc.gen.internal.exception.DumpFileEmptyRuntimeException;
import org.seasar.extension.jdbc.gen.sql.SqlExecutionContext;
import org.seasar.extension.jdbc.gen.sqltype.SqlType;
import org.seasar.framework.log.Logger;
import org.seasar.framework.util.StringUtil;

/**
 * {@link Loader}の実装クラスです。
 * 
 * @author taedium
 */
public class LoaderImpl implements Loader {

    /** ロガー */
    protected static Logger logger = Logger.getLogger(LoaderImpl.class);

    /** 方言 */
    protected GenDialect dialect;

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

    /** 区切り文字 */
    protected char delimiter = ',';

    /** 拡張子 */
    protected String extension = ".csv";

    /** バッチサイズ */
    protected int batchSize;

    /** ロードの前に存在するデータを削除する場合{@code true}、削除しない場合{@code false}を設定します。 */
    protected boolean delete;

    /**
     * インスタンスを構築します。
     * 
     * @param dialect
     *            方言
     * @param dumpFileEncoding
     *            ダンプファイルのエンコーディング
     * @param batchSize
     *            バッチサイズ
     * @param delete
     *            ロードの前に存在するデータを削除する場合{@code true}、削除しない場合{@code false}を設定します。
     */
    public LoaderImpl(GenDialect dialect, String dumpFileEncoding,
            int batchSize, boolean delete) {
        if (dialect == null) {
            throw new NullPointerException("dialect");
        }
        if (dumpFileEncoding == null) {
            throw new NullPointerException("dumpFileEncoding");
        }
        if (batchSize < 0) {
            throw new IllegalArgumentException("batchSize");
        }
        this.dialect = dialect;
        this.dumpFileEncoding = dumpFileEncoding;
        this.batchSize = batchSize;
        this.delete = delete;
    }

    public void load(SqlExecutionContext sqlExecutionContext,
            DatabaseDesc databaseDesc, File dumpFile) {
        TableDesc tableDesc = getTableDesc(databaseDesc, dumpFile);
        if (tableDesc == null) {
            return;
        }
        logger.log("DS2JDBCGen0013", new Object[] { dumpFile.getPath(),
                tableDesc.getFullName() });
        DumpFileReader reader = createDumpFileReader(dumpFile);
        try {
            List<String> columnNameList = reader.readLine();
            if (columnNameList == null) {
                throw new DumpFileEmptyRuntimeException(dumpFile.getPath());
            }
            List<SqlType> sqlTypeList = getSqlTypeList(tableDesc,
                    columnNameList);
            String sql = buildSql(tableDesc, columnNameList);
            sqlExecutionContext.begin();
            try {
                if (delete) {
                    deleteData(sqlExecutionContext, tableDesc);
                }
                preLoadData(sqlExecutionContext, tableDesc);
                loadData(sqlExecutionContext, reader, sqlTypeList, sql);
                postLoadData(sqlExecutionContext, tableDesc);
            } catch (Exception e) {
                logger.log("DS2JDBCGen0021",
                        new Object[] { dumpFile.getPath() });
                if (dialect.isTableNotFound(e)) {
                    logger.log("DS2JDBCGen0012", new Object[] { tableDesc
                            .getFullName() });
                    sqlExecutionContext.notifyException();
                } else {
                    LoadFailedRuntimeException ex = new LoadFailedRuntimeException(
                            e, dumpFile.getPath(), reader.getLineNumber());
                    sqlExecutionContext.addException(ex);
                }
                return;
            } finally {
                sqlExecutionContext.end();
            }
            logger.log("DS2JDBCGen0014", new Object[] { dumpFile.getPath(),
                    tableDesc.getFullName(), reader.getLineNumber() - 1 });
        } finally {
            reader.close();
        }
    }

    public boolean isTarget(DatabaseDesc databaseDesc, File file) {
        if (databaseDesc == null || file == null) {
            return false;
        }
        if (!file.getName().endsWith(extension)) {
            return false;
        }
        if (getTableDesc(databaseDesc, file) == null) {
            return false;
        }
        return true;
    }

    /**
     * データを削除します。
     * 
     * @param sqlExecutionContext
     *            SQL実行コンテキスト
     * @param tableDesc
     *            テーブル記述
     * @throws SQLException
     *             SQL例外が発生した場合
     */
    protected void deleteData(SqlExecutionContext sqlExecutionContext,
            TableDesc tableDesc) throws SQLException {
        Statement statement = sqlExecutionContext.getStatement();
        String sql = "delete from " + tableDesc.getFullName();
        logger.debug(sql);
        statement.execute(sql);
    }

    /**
     * データのロード前に処理します。
     * 
     * @param sqlExecutionContext
     *            SQL実行コンテキスト
     * @param tableDesc
     *            テーブル記述
     * @throws SQLException
     *             SQL例外が発生した場合
     */
    protected void preLoadData(SqlExecutionContext sqlExecutionContext,
            TableDesc tableDesc) throws SQLException {
        if (tableDesc.hasIdentityColumn()
                && dialect.supportsIdentityInsertControlStatement()) {
            Statement statement = sqlExecutionContext.getStatement();
            String sql = dialect.getIdentityInsertEnableStatement(tableDesc
                    .getFullName());
            logger.debug(sql);
            statement.execute(sql);
        }
    }

    /**
     * データをロードします。
     * 
     * @param sqlExecutionContext
     *            SQL実行コンテキスト
     * @param reader
     *            リーダ
     * @param sqlTypeList
     *            {@link SqlType}のリスト
     * @param sql
     *            SQL
     * @throws SQLException
     *             SQL例外が発生した場合
     */
    protected void loadData(SqlExecutionContext sqlExecutionContext,
            DumpFileReader reader, List<SqlType> sqlTypeList, String sql)
            throws SQLException {
        PreparedStatement ps = sqlExecutionContext.getPreparedStatement(sql);
        List<String> valueList = null;
        boolean remaining = false;
        for (int i = 0; (valueList = reader.readLine()) != null; i++) {
            bindArgs(ps, sqlTypeList, valueList);
            ps.addBatch();
            if (batchSize > 0 && (i + 1) % batchSize == 0) {
                ps.executeBatch();
                sqlExecutionContext.commitLocalTx();
                remaining = false;
            } else {
                remaining = true;
            }
        }
        if (remaining) {
            ps.executeBatch();
        }
    }

    /**
     * データのロード後に処理します。
     * 
     * @param sqlExecutionContext
     *            SQL実行コンテキスト
     * @param tableDesc
     *            テーブル記述
     * @throws SQLException
     *             SQL例外が発生した場合
     */
    protected void postLoadData(SqlExecutionContext sqlExecutionContext,
            TableDesc tableDesc) throws SQLException {
        if (tableDesc.hasIdentityColumn()
                && dialect.supportsIdentityInsertControlStatement()) {
            Statement statement = sqlExecutionContext.getStatement();
            String sql = dialect.getIdentityInsertDisableStatement(tableDesc
                    .getFullName());
            logger.debug(sql);
            statement.execute(sql);
        }
    }

    /**
     * 引数をバインドします。
     * 
     * @param ps
     *            準備されたステートメント
     * @param sqlTypeList
     *            {@link SqlType}のリスト
     * @param valueList
     *            値のリスト
     * @throws SQLException
     *             SQL例外が発生した場合
     */
    protected void bindArgs(PreparedStatement ps, List<SqlType> sqlTypeList,
            List<String> valueList) throws SQLException {
        for (int i = 0; i < sqlTypeList.size(); i++) {
            SqlType sqlType = sqlTypeList.get(i);
            String value = valueList.get(i);
            sqlType.bindValue(ps, i + 1, value);
        }
    }

    /**
     * SQLを組み立てます。
     * 
     * @param tableDesc
     *            テーブル記述
     * @param columnNameList
     *            カラム名のリスト
     * @return SQL
     */
    protected String buildSql(TableDesc tableDesc, List<String> columnNameList) {
        StringBuilder buf = new StringBuilder();
        buf.append("insert into ");
        buf.append(tableDesc.getFullName());
        buf.append(" (");
        for (String columnName : columnNameList) {
            buf.append(columnName);
            buf.append(", ");
        }
        buf.setLength(buf.length() - 2);
        buf.append(") values (");
        for (int i = 0; i < columnNameList.size(); i++) {
            buf.append("?, ");
        }
        buf.setLength(buf.length() - 2);
        buf.append(")");
        return buf.toString();
    }

    /**
     * {@link SqlType}のリストを返します。
     * 
     * @param tableDesc
     *            テーブル記述
     * @param columnNameList
     *            カラム名のリスト
     * @return {@link SqlType}のリストを返します。
     */
    protected List<SqlType> getSqlTypeList(TableDesc tableDesc,
            List<String> columnNameList) {
        List<SqlType> sqlTypeList = new ArrayList<SqlType>();
        for (int i = 0; i < columnNameList.size(); i++) {
            String columnName = columnNameList.get(i);
            ColumnDesc columnDesc = tableDesc.getColumnDesc(columnName);
            sqlTypeList.add(columnDesc.getSqlType());
        }
        return sqlTypeList;
    }

    /**
     * ダンプファイルのトークナイザを作成します。
     * 
     * @return ダンプファイルのトークナイザ
     */
    protected DumpFileTokenizer createDumpFileTokenizer() {
        return new DumpFileTokenizer(delimiter);
    }

    /**
     * ダンプファイルのリーダを作成します。
     * 
     * @param dumpFile
     *            ダンプファイル
     * @return ダンプファイルのリーダ
     */
    protected DumpFileReader createDumpFileReader(File dumpFile) {
        return new DumpFileReader(dumpFile, dumpFileEncoding,
                createDumpFileTokenizer());
    }

    /**
     * テーブル記述を返します。
     * 
     * @param databaseDesc
     *            データベース記述
     * @param dumpFile
     *            ダンプファイル
     * @return テーブル記述、存在しない場合{@code null}
     */
    protected TableDesc getTableDesc(DatabaseDesc databaseDesc, File dumpFile) {
        String name = StringUtil.trimSuffix(dumpFile.getName(), extension);
        return databaseDesc.getTableDesc(name);
    }
}
