/*
 * 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.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.GenerationType;
import javax.persistence.TemporalType;

import org.seasar.extension.jdbc.PropertyMeta;
import org.seasar.extension.jdbc.ValueType;
import org.seasar.extension.jdbc.gen.dialect.GenDialect;
import org.seasar.extension.jdbc.gen.exception.UnsupportedSqlTypeRuntimeException;
import org.seasar.extension.jdbc.gen.internal.sqltype.BigIntType;
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.CharType;
import org.seasar.extension.jdbc.gen.internal.sqltype.ClobType;
import org.seasar.extension.jdbc.gen.internal.sqltype.DateType;
import org.seasar.extension.jdbc.gen.internal.sqltype.DecimalType;
import org.seasar.extension.jdbc.gen.internal.sqltype.DoubleType;
import org.seasar.extension.jdbc.gen.internal.sqltype.FloatType;
import org.seasar.extension.jdbc.gen.internal.sqltype.IntegerType;
import org.seasar.extension.jdbc.gen.internal.sqltype.SmallIntType;
import org.seasar.extension.jdbc.gen.internal.sqltype.TimeType;
import org.seasar.extension.jdbc.gen.internal.sqltype.TimestampType;
import org.seasar.extension.jdbc.gen.internal.sqltype.VarcharType;
import org.seasar.extension.jdbc.gen.internal.util.ColumnUtil;
import org.seasar.extension.jdbc.gen.internal.util.TableUtil;
import org.seasar.extension.jdbc.gen.provider.ValueTypeProvider;
import org.seasar.extension.jdbc.gen.sqltype.SqlType;
import org.seasar.extension.jdbc.util.ConnectionUtil;
import org.seasar.framework.log.Logger;
import org.seasar.framework.util.CaseInsensitiveMap;
import org.seasar.framework.util.ResultSetUtil;
import org.seasar.framework.util.StatementUtil;
import org.seasar.framework.util.StringUtil;

/**
 * 標準的な方言をあつかうクラスです。
 * 
 * @author taedium
 */
public class StandardGenDialect implements GenDialect {

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

    /** SQL型をキー、{@link SqlType}を値とするマップ */
    protected Map<Integer, SqlType> sqlTypeMap = new HashMap<Integer, SqlType>();

    /** カラムの型名をキー、{@link ColumnType}を値とするマップ */
    @SuppressWarnings("unchecked")
    protected Map<Object, ColumnType> columnTypeMap = new CaseInsensitiveMap();

    /** カラムのSQL型をキー、{@link ColumnType}を値とするマップ。 */
    protected Map<Integer, ColumnType> fallbackColumnTypeMap = new HashMap<Integer, ColumnType>();

    /**
     * インスタンスを構築します。
     */
    public StandardGenDialect() {
        sqlTypeMap.put(Types.BIGINT, new BigIntType());
        sqlTypeMap.put(Types.BINARY, new BinaryType());
        sqlTypeMap.put(Types.BLOB, new BlobType());
        sqlTypeMap.put(Types.BOOLEAN, new BooleanType());
        sqlTypeMap.put(Types.CHAR, new CharType());
        sqlTypeMap.put(Types.CLOB, new ClobType());
        sqlTypeMap.put(Types.DATE, new DateType());
        sqlTypeMap.put(Types.DECIMAL, new DecimalType());
        sqlTypeMap.put(Types.DOUBLE, new DoubleType());
        sqlTypeMap.put(Types.FLOAT, new FloatType());
        sqlTypeMap.put(Types.INTEGER, new IntegerType());
        sqlTypeMap.put(Types.SMALLINT, new SmallIntType());
        sqlTypeMap.put(Types.TIME, new TimeType());
        sqlTypeMap.put(Types.TIMESTAMP, new TimestampType());
        sqlTypeMap.put(Types.VARCHAR, new VarcharType());

        columnTypeMap.put("bigint", StandardColumnType.BIGINT);
        columnTypeMap.put("binary", StandardColumnType.BINARY);
        columnTypeMap.put("bit", StandardColumnType.BIT);
        columnTypeMap.put("blob", StandardColumnType.BLOB);
        columnTypeMap.put("boolean", StandardColumnType.BOOLEAN);
        columnTypeMap.put("char", StandardColumnType.CHAR);
        columnTypeMap.put("clob", StandardColumnType.CLOB);
        columnTypeMap.put("date", StandardColumnType.DATE);
        columnTypeMap.put("decimal", StandardColumnType.DECIMAL);
        columnTypeMap.put("double", StandardColumnType.DOUBLE);
        columnTypeMap.put("float", StandardColumnType.FLOAT);
        columnTypeMap.put("integer", StandardColumnType.INTEGER);
        columnTypeMap.put("longvarbinary", StandardColumnType.LONGVARBINARY);
        columnTypeMap.put("longvarchar", StandardColumnType.LONGVARCHAR);
        columnTypeMap.put("numeric", StandardColumnType.NUMERIC);
        columnTypeMap.put("real", StandardColumnType.REAL);
        columnTypeMap.put("smallint", StandardColumnType.SMALLINT);
        columnTypeMap.put("time", StandardColumnType.TIME);
        columnTypeMap.put("timestamp", StandardColumnType.TIMESTAMP);
        columnTypeMap.put("tinyint", StandardColumnType.TINYINT);
        columnTypeMap.put("varbinary", StandardColumnType.VARBINARY);
        columnTypeMap.put("varchar", StandardColumnType.VARCHAR);

        fallbackColumnTypeMap.put(Types.BIGINT, StandardColumnType.BIGINT);
        fallbackColumnTypeMap.put(Types.BINARY, StandardColumnType.BINARY);
        fallbackColumnTypeMap.put(Types.BIT, StandardColumnType.BIT);
        fallbackColumnTypeMap.put(Types.BLOB, StandardColumnType.BLOB);
        fallbackColumnTypeMap.put(Types.BOOLEAN, StandardColumnType.BOOLEAN);
        fallbackColumnTypeMap.put(Types.CHAR, StandardColumnType.CHAR);
        fallbackColumnTypeMap.put(Types.CLOB, StandardColumnType.CLOB);
        fallbackColumnTypeMap.put(Types.DATE, StandardColumnType.DATE);
        fallbackColumnTypeMap.put(Types.DECIMAL, StandardColumnType.DECIMAL);
        fallbackColumnTypeMap.put(Types.DOUBLE, StandardColumnType.DOUBLE);
        fallbackColumnTypeMap.put(Types.FLOAT, StandardColumnType.FLOAT);
        fallbackColumnTypeMap.put(Types.INTEGER, StandardColumnType.INTEGER);
        fallbackColumnTypeMap.put(Types.LONGVARBINARY,
                StandardColumnType.LONGVARBINARY);
        fallbackColumnTypeMap.put(Types.LONGVARCHAR,
                StandardColumnType.LONGVARCHAR);
        fallbackColumnTypeMap.put(Types.NUMERIC, StandardColumnType.NUMERIC);
        fallbackColumnTypeMap.put(Types.REAL, StandardColumnType.REAL);
        fallbackColumnTypeMap.put(Types.SMALLINT, StandardColumnType.SMALLINT);
        fallbackColumnTypeMap.put(Types.TIME, StandardColumnType.TIME);
        fallbackColumnTypeMap
                .put(Types.TIMESTAMP, StandardColumnType.TIMESTAMP);
        fallbackColumnTypeMap.put(Types.TINYINT, StandardColumnType.TINYINT);
        fallbackColumnTypeMap
                .put(Types.VARBINARY, StandardColumnType.VARBINARY);
        fallbackColumnTypeMap.put(Types.VARCHAR, StandardColumnType.VARCHAR);
    }

    public String getName() {
        return null;
    }

    public String getDefaultSchemaName(String userName) {
        return userName;
    }

    public SqlType getSqlType(int sqlType) {
        return getSqlTypeInternal(sqlType);
    }

    public SqlType getSqlType(ValueTypeProvider valueTypeProvider,
            PropertyMeta propertyMeta) {
        ValueType valueType = valueTypeProvider.provide(propertyMeta);
        return getSqlTypeInternal(valueType.getSqlType());
    }

    /**
     * 内部的にSQL型を返します。
     * 
     * @param sqlType
     *            JDBCのSQL型
     * @return SQL型
     */
    protected SqlType getSqlTypeInternal(int sqlType) {
        SqlType type = sqlTypeMap.get(sqlType);
        if (type != null) {
            return type;
        }
        throw new UnsupportedSqlTypeRuntimeException(sqlType);
    }

    public ColumnType getColumnType(String typeName, int sqlType) {
        ColumnType columnType = columnTypeMap.get(typeName);
        return columnType != null ? columnType : fallbackColumnTypeMap
                .get(sqlType);
    }

    public GenerationType getDefaultGenerationType() {
        return GenerationType.TABLE;
    }

    public String getOpenQuote() {
        return "\"";
    }

    public String getCloseQuote() {
        return "\"";
    }

    public String quote(String value) {
        if (value == null) {
            return null;
        }
        return getOpenQuote() + value + getCloseQuote();
    }

    public String unquote(String value) {
        String s = StringUtil.ltrim(value, getOpenQuote());
        return StringUtil.rtrim(s, getCloseQuote());
    }

    public boolean supportsSequence() {
        return false;
    }

    public boolean supportsGetIndexInfo(String catalogName, String schemaName,
            String tableName) {
        return true;
    }

    public String getSequenceDefinitionFragment(String dataType,
            long initialValue, int allocationSize) {
        throw new UnsupportedOperationException("getSequenceDefinitionFragment");
    }

    public String getSqlBlockDelimiter() {
        return null;
    }

    public String getIdentityColumnDefinition() {
        throw new UnsupportedOperationException("getIdentityDefinition");
    }

    public String getDropForeignKeySyntax() {
        return "drop constraint";
    }

    public String getDropUniqueKeySyntax() {
        return "drop constraint";
    }

    public boolean isTableNotFound(Throwable throwable) {
        return false;
    }

    public boolean isColumnNotFound(Throwable throwable) {
        return false;
    }

    public boolean isSequenceNotFound(Throwable throwable) {
        return false;
    }

    public SqlBlockContext createSqlBlockContext() {
        return new StandardSqlBlockContext();
    }

    public boolean supportsIdentityInsert() {
        return false;
    }

    public boolean supportsIdentityInsertControlStatement() {
        return false;
    }

    public String getIdentityInsertEnableStatement(String tableName) {
        throw new UnsupportedOperationException("getIdentityInsertOnStatement");
    }

    public String getIdentityInsertDisableStatement(String tableName) {
        throw new UnsupportedOperationException("getIdentityInsertOffStatement");
    }

    public boolean supportsNullableUnique() {
        return true;
    }

    public boolean supportsIdentity() {
        return false;
    }

    public String getSequenceNextValString(final String sequenceName,
            final int allocationSize) {
        throw new UnsupportedOperationException("getSequenceNextValString");
    }

    public boolean supportsCommentInCreateTable() {
        return false;
    }

    public boolean supportsCommentOn() {
        return false;
    }

    public boolean isJdbcCommentAvailable() {
        return true;
    }

    public String getTableComment(Connection connection, String catalogName,
            String schemaName, String tableName) throws SQLException {
        throw new UnsupportedOperationException("getTableComment");
    }

    public Map<String, String> getColumnCommentMap(Connection connection,
            String catalogName, String schemaName, String tableName)
            throws SQLException {
        throw new UnsupportedOperationException("getColumnCommentMap");
    }

    public boolean supportsReferentialDeleteRule() {
        return true;
    }

    public boolean supportsReferentialUpdateRule() {
        return true;
    }

    public boolean isAutoIncrement(Connection connection, String catalogName,
            String schemaName, String tableName, String columnName)
            throws SQLException {
        String fullTableName = TableUtil.buildFullTableName(catalogName,
                schemaName, tableName);
        String sql = "select " + columnName + " from " + fullTableName
                + " where 1 = 0";
        logger.debug(sql);

        PreparedStatement ps = ConnectionUtil.prepareStatement(connection, sql);
        try {
            ResultSet rs = ps.executeQuery();
            try {
                ResultSetMetaData rsMetaData = rs.getMetaData();
                return rsMetaData.isAutoIncrement(1);
            } finally {
                ResultSetUtil.close(rs);
            }
        } finally {
            StatementUtil.close(ps);
        }
    }

    /**
     * 例外チェーンをたどって原因となった{@link SQLException#getSQLState() SQLステート}を返します。
     * <p>
     * 例外チェーンに{@link SQLException SQL例外}が存在しない場合や、SQLステートが設定されていない場合は
     * <code>null</code>を返します。
     * </p>
     * 
     * @param t
     *            例外
     * @return 原因となった{@link SQLException#getSQLState() SQLステート}
     */
    protected String getSQLState(Throwable t) {
        SQLException cause = getCauseSQLException(t);
        if (cause != null && !StringUtil.isEmpty(cause.getSQLState())) {
            return cause.getSQLState();
        }
        return null;
    }

    /**
     * 例外チェーンをたどって原因となった{@link SQLException#getErrorCode() ベンダー固有の例外コード}を返します。
     * <p>
     * 例外チェーンに{@link SQLException SQL例外}が存在しない場合や、例外コードが設定されていない場合は
     * <code>null</code>を返します。
     * </p>
     * 
     * @param t
     *            例外
     * @return 原因となった{@link SQLException#getErrorCode() ベンダー固有の例外コード}
     */
    protected Integer getErrorCode(Throwable t) {
        SQLException cause = getCauseSQLException(t);
        if (cause != null) {
            return cause.getErrorCode();
        }
        return null;
    }

    /**
     * 例外チェーンをたどって原因となった{@link SQLException SQL例外}を返します。
     * <p>
     * 例外チェーンにSQL例外が存在しない場合は<code>null</code>を返します。
     * </p>
     * 
     * @param t
     *            例外
     * @return 原因となった{@link SQLException SQL例外}
     */
    protected SQLException getCauseSQLException(Throwable t) {
        SQLException cause = null;
        while (t != null) {
            if (t instanceof SQLException) {
                cause = SQLException.class.cast(t);
                if (cause.getNextException() != null) {
                    cause = cause.getNextException();
                    t = cause;
                    continue;
                }
            }
            t = t.getCause();
        }
        return cause;
    }

    /**
     * 標準の{@link ColumnType}の実装クラスです。
     * 
     * @author taedium
     */
    public static class StandardColumnType implements ColumnType {

        private static StandardColumnType BIGINT = new StandardColumnType(
                "bigint", Long.class);

        private static StandardColumnType BINARY = new StandardColumnType(
                "binary", byte[].class);

        private static StandardColumnType BIT = new StandardColumnType("bit",
                Boolean.class);

        private static StandardColumnType BLOB = new StandardColumnType("blob",
                byte[].class, true);

        private static StandardColumnType BOOLEAN = new StandardColumnType(
                "boolean", Boolean.class);

        private static StandardColumnType CHAR = new StandardColumnType(
                "char($l)", String.class);

        private static StandardColumnType CLOB = new StandardColumnType("clob",
                String.class, true);

        private static StandardColumnType DATE = new StandardColumnType("date",
                java.sql.Date.class);

        private static StandardColumnType DECIMAL = new StandardColumnType(
                "decimal", BigDecimal.class);

        private static StandardColumnType DOUBLE = new StandardColumnType(
                "double", Double.class);

        private static StandardColumnType FLOAT = new StandardColumnType(
                "float", Float.class);

        private static StandardColumnType INTEGER = new StandardColumnType(
                "integer", Integer.class);

        private static StandardColumnType LONGVARBINARY = new StandardColumnType(
                "longvarbinary", byte[].class);

        private static StandardColumnType LONGVARCHAR = new StandardColumnType(
                "longvarchar", String.class);

        private static StandardColumnType NUMERIC = new StandardColumnType(
                "numeric", BigDecimal.class);

        private static StandardColumnType REAL = new StandardColumnType("real",
                Float.class);

        private static StandardColumnType SMALLINT = new StandardColumnType(
                "smallint", Short.class);

        private static StandardColumnType TIME = new StandardColumnType("time",
                java.sql.Time.class);

        private static StandardColumnType TIMESTAMP = new StandardColumnType(
                "timestamp", Timestamp.class);

        private static StandardColumnType TINYINT = new StandardColumnType(
                "tinyint", Short.class);

        private static StandardColumnType VARBINARY = new StandardColumnType(
                "varbinary($l)", byte[].class);

        private static StandardColumnType VARCHAR = new StandardColumnType(
                "varchar($l)", String.class);

        /** カラム定義 */
        protected String dataType;

        /** 属性のクラス */
        protected Class<?> attributeClass;

        /** LOBの場合{@code true} */
        protected boolean lob;

        /** 時制の種別 */
        protected TemporalType temporalType;

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

        /**
         * インスタンスを構築します。
         * 
         * @param dataType
         *            カラム定義
         * @param attributeClass
         *            属性のクラス
         * @param lob
         *            LOBの場合{@code true}
         */
        protected StandardColumnType(String dataType, Class<?> attributeClass,
                boolean lob) {
            this(dataType, attributeClass, lob, null);
        }

        /**
         * インスタンスを構築します。
         * 
         * @param dataType
         *            カラム定義
         * @param attributeClass
         *            属性のクラス
         * @param lob
         *            LOBの場合{@code true}
         * @param temporalType
         *            時制の種別
         */
        protected StandardColumnType(String dataType, Class<?> attributeClass,
                boolean lob, TemporalType temporalType) {
            this.dataType = dataType;
            this.attributeClass = attributeClass;
            this.lob = lob;
            this.temporalType = temporalType;
        }

        public String getColumnDefinition(int length, int precision, int scale,
                String defaultValue) {
            String completeDataType = ColumnUtil.formatDataType(dataType,
                    length, precision, scale);
            return getColumnDefinitionInternal(completeDataType, defaultValue);
        }

        /**
         * カラム定義を返します。
         * 
         * @param completeDataType
         *            完全なデータ型
         * @param defaultValue
         *            デフォルト値、存在しない場合は{@code null}
         * @return カラム定義
         */
        protected String getColumnDefinitionInternal(String completeDataType,
                String defaultValue) {
            if (defaultValue == null) {
                return completeDataType;
            }
            return completeDataType + " default " + defaultValue;
        }

        public Class<?> getAttributeClass(int length, int precision, int scale) {
            return attributeClass;
        }

        public boolean isLob() {
            return lob;
        }

        public TemporalType getTemporalType() {
            return temporalType;
        }

    }

    /**
     * 標準の{@link StandardColumnType}の実装クラスです。
     * 
     * @author taedium
     */
    public static class StandardSqlBlockContext implements SqlBlockContext {

        /** SQLブロックの開始を表すキーワードの連なりのリスト */
        protected List<List<String>> sqlBlockStartKeywordsList = new ArrayList<List<String>>();

        /** 追加されたキーワードの連なり */
        protected List<String> keywords = new ArrayList<String>();

        /** SQLブロックの内側の場合{@code true} */
        protected boolean inSqlBlock;

        public void addKeyword(String keyword) {
            if (!inSqlBlock) {
                keywords.add(keyword);
                check();
            }
        }

        /**
         * ブロックの内側かどうかチェックします。
         */
        protected void check() {
            for (List<String> startKeywords : sqlBlockStartKeywordsList) {
                if (startKeywords.size() > keywords.size()) {
                    continue;
                }
                for (int i = 0; i < startKeywords.size(); i++) {
                    String word1 = startKeywords.get(i);
                    String word2 = keywords.get(i);
                    inSqlBlock = word1.equalsIgnoreCase(word2);
                    if (!inSqlBlock) {
                        break;
                    }
                }
                if (inSqlBlock) {
                    break;
                }
            }
        }

        public boolean isInSqlBlock() {
            return inSqlBlock;
        }
    }

}
