/*
 * 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.dialect;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.persistence.GenerationType;

import org.seasar.extension.jdbc.gen.internal.sqltype.BinaryType;
import org.seasar.extension.jdbc.gen.internal.sqltype.BlobType;
import org.seasar.extension.jdbc.gen.internal.sqltype.BooleanType;
import org.seasar.extension.jdbc.gen.internal.sqltype.ClobType;
import org.seasar.extension.jdbc.gen.internal.sqltype.DecimalType;
import org.seasar.extension.jdbc.gen.internal.sqltype.FloatType;
import org.seasar.extension.jdbc.util.ConnectionUtil;
import org.seasar.framework.util.ResultSetUtil;
import org.seasar.framework.util.StatementUtil;

/**
 * DB2の方言を扱うクラスです。
 * 
 * @author taedium
 */
public class Db2GenDialect extends StandardGenDialect {

    /** テーブルが見つからないことを示すSQLステート */
    protected static String TABLE_NOT_FOUND_SQL_STATE = "42704";

    /** カラムが見つからないことを示すSQLステート */
    protected static String COLUMN_NOT_FOUND_SQL_STATE = "42703";

    /** シーケンスが見つからないことを示すSQLステート */
    protected static String SEQUENCE_NOT_FOUND_SQL_STATE = "42704";

    /**
     * インスタンスを構築します。
     */
    public Db2GenDialect() {
        sqlTypeMap
                .put(Types.BINARY, new BinaryType("varchar($l) for bit data"));
        sqlTypeMap.put(Types.BLOB, new BlobType("blob($l)"));
        sqlTypeMap.put(Types.BOOLEAN, new BooleanType("smallint"));
        sqlTypeMap.put(Types.CLOB, new ClobType("clob($l)"));
        sqlTypeMap.put(Types.DECIMAL, new DecimalType("decimal($p,$s)"));
        sqlTypeMap.put(Types.FLOAT, new FloatType("real"));

        columnTypeMap.put("blob", Db2ColumnType.BLOB);
        columnTypeMap.put("char () for bit data", Db2ColumnType.CHAR_BIT);
        columnTypeMap.put("clob", Db2ColumnType.CLOB);
        columnTypeMap.put("decimal", Db2ColumnType.DECIMAL);
        columnTypeMap.put("long varchar for bit data",
                Db2ColumnType.LONGVARCHAR_BIT);
        columnTypeMap.put("long varchar", Db2ColumnType.LONGVARCHAR);
        columnTypeMap.put("varchar () for bit data", Db2ColumnType.VARCHAR_BIT);
    }

    @Override
    public String getName() {
        return "db2";
    }

    @Override
    public String getDefaultSchemaName(String userName) {
        return userName != null ? userName.toUpperCase() : null;
    }

    @Override
    public GenerationType getDefaultGenerationType() {
        return GenerationType.IDENTITY;
    }

    @Override
    public boolean supportsSequence() {
        return true;
    }

    @Override
    public String getSequenceDefinitionFragment(String dataType,
            long initialValue, int allocationSize) {
        return "as " + dataType + " start with " + initialValue
                + " increment by " + allocationSize;
    }

    @Override
    public String getIdentityColumnDefinition() {
        return "generated by default as identity";
    }

    @Override
    public String getSqlBlockDelimiter() {
        return "@";
    }

    @Override
    public boolean isTableNotFound(Throwable t) {
        for (SQLException e : getAllSQLExceptions(t)) {
            if (TABLE_NOT_FOUND_SQL_STATE.equals(e.getSQLState())) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean isColumnNotFound(Throwable t) {
        for (SQLException e : getAllSQLExceptions(t)) {
            if (COLUMN_NOT_FOUND_SQL_STATE.equals(e.getSQLState())) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean isSequenceNotFound(Throwable t) {
        for (SQLException e : getAllSQLExceptions(t)) {
            if (SEQUENCE_NOT_FOUND_SQL_STATE.equals(e.getSQLState())) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean supportsIdentityInsert() {
        return true;
    }

    @Override
    public boolean supportsIdentity() {
        return true;
    }

    /**
     * 原因となった、もしくは関連付けられたすべての{@link SQLException}を取得します。
     * 
     * @param t
     *            例外
     * @return 原因となった、もしくは関連付けられた{@link SQLException}のリスト
     */
    protected List<SQLException> getAllSQLExceptions(Throwable t) {
        List<SQLException> sqlExceptionList = new ArrayList<SQLException>();
        while (t != null) {
            if (t instanceof SQLException) {
                SQLException cause = SQLException.class.cast(t);
                sqlExceptionList.add(cause);
                if (cause.getNextException() != null) {
                    cause = cause.getNextException();
                    sqlExceptionList.add(cause);
                    t = cause;
                    continue;
                }
            }
            t = t.getCause();
        }
        return sqlExceptionList;
    }

    @Override
    public SqlBlockContext createSqlBlockContext() {
        return new Db2SqlBlockContext();
    }

    @Override
    public boolean supportsNullableUnique() {
        return false;
    }

    @Override
    public String getSequenceNextValString(String sequenceName,
            int allocationSize) {
        return "values nextval for " + sequenceName;
    }

    @Override
    public boolean supportsCommentInCreateTable() {
        return false;
    }

    @Override
    public boolean supportsCommentOn() {
        return true;
    }

    @Override
    public boolean isAutoIncrement(Connection connection, String catalogName,
            String schemaName, String tableName, String columnName)
            throws SQLException {
        String sql = "select generated from syscat.columns where tabschema = ? and tabname = ? and colname = ?";
        logger.debug(String.format(sql.replace("?", "'%s'"), schemaName,
                tableName, columnName));

        PreparedStatement ps = ConnectionUtil.prepareStatement(connection, sql);
        ps.setString(1, schemaName);
        ps.setString(2, tableName);
        ps.setString(3, columnName);
        try {
            ResultSet rs = ps.executeQuery();
            try {
                if (rs.next()) {
                    String generated = rs.getString(1);
                    return "A".equals(generated) || "D".equals(generated);
                }
                return false;
            } finally {
                ResultSetUtil.close(rs);
            }
        } finally {
            StatementUtil.close(ps);
        }
    }

    /**
     * DB2用の{@link StandardColumnType}の実装です。
     * 
     * @author taedium
     */
    public static class Db2ColumnType extends StandardColumnType {

        private static Db2ColumnType BLOB = new Db2ColumnType("blob($l)",
                byte[].class);

        private static Db2ColumnType CHAR_BIT = new Db2ColumnType(
                "char($l) for bit data", byte[].class);

        private static Db2ColumnType CLOB = new Db2ColumnType("clob($l)",
                String.class);

        private static Db2ColumnType DECIMAL = new Db2ColumnType(
                "decimal($p,$s)", BigDecimal.class);

        private static Db2ColumnType LONGVARCHAR_BIT = new Db2ColumnType(
                "long varchar for bit data", byte[].class);

        private static Db2ColumnType LONGVARCHAR = new Db2ColumnType(
                "long varchar", String.class);

        private static Db2ColumnType VARCHAR_BIT = new Db2ColumnType(
                "varchar($l) for bit data", byte[].class);

        /**
         * インスタンスを構築します。
         * 
         * @param dataType
         *            データ型
         * @param attributeClass
         *            属性のクラス
         */
        public Db2ColumnType(String dataType, Class<?> attributeClass) {
            super(dataType, attributeClass);
        }

        /**
         * インスタンスを構築します。
         * 
         * @param dataType
         *            データ型
         * @param attributeClass
         *            属性のクラス
         * @param lob
         *            LOBの場合{@code true}
         */
        public Db2ColumnType(String dataType, Class<?> attributeClass,
                boolean lob) {
            super(dataType, attributeClass, lob);
        }

    }

    /**
     * DB2用の{@link SqlBlockContext}の実装クラスです。
     * 
     * @author taedium
     */
    public static class Db2SqlBlockContext extends StandardSqlBlockContext {

        /**
         * インスタンスを構築します。
         */
        protected Db2SqlBlockContext() {
            sqlBlockStartKeywordsList.add(Arrays.asList("create", "procedure"));
            sqlBlockStartKeywordsList.add(Arrays.asList("create", "function"));
            sqlBlockStartKeywordsList.add(Arrays.asList("create", "trigger"));
            sqlBlockStartKeywordsList.add(Arrays.asList("alter", "procedure"));
            sqlBlockStartKeywordsList.add(Arrays.asList("alter", "function"));
            sqlBlockStartKeywordsList.add(Arrays.asList("alter", "trigger"));
        }
    }
}
