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

import java.io.Serializable;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Set;

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

import org.seasar.extension.jdbc.DbmsDialect;
import org.seasar.extension.jdbc.FromClause;
import org.seasar.extension.jdbc.JoinColumnMeta;
import org.seasar.extension.jdbc.JoinType;
import org.seasar.extension.jdbc.PropertyMeta;
import org.seasar.extension.jdbc.SelectForUpdateType;
import org.seasar.extension.jdbc.ValueType;
import org.seasar.extension.jdbc.WhereClause;
import org.seasar.extension.jdbc.exception.OrderByNotFoundRuntimeException;
import org.seasar.extension.jdbc.types.ValueTypes;
import org.seasar.extension.jdbc.util.QueryTokenizer;
import org.seasar.framework.util.StringUtil;
import org.seasar.framework.util.tiger.CollectionsUtil;
import org.seasar.framework.util.tiger.Pair;

/**
 * 標準的な方言をあつかうクラスです
 * 
 * @author higa
 * 
 */
public class StandardDialect implements DbmsDialect {

    /**
     * {@link EntityExistsException}に該当するSQLステートです。
     */
    protected static final Set<String> entityExistsExceptionStateCode = CollectionsUtil
            .newHashSet(Arrays.asList("23", "27", "44"));

    public String getName() {
        return null;
    }

    public boolean supportsLimit() {
        return false;
    }

    public boolean supportsOffset() {
        return supportsLimit();
    }

    public boolean supportsOffsetWithoutLimit() {
        return supportsOffset();
    }

    public boolean supportsCursor() {
        return true;
    }

    public boolean needsParameterForResultSet() {
        return false;
    }

    public String convertLimitSql(String sql, int offset, int limit) {
        return sql;
    }

    public String convertGetCountSql(String sql) {
        return "select count(*) from ( " + sql + " ) COUNT_";
    }

    public String getCountSqlSelectList(List<PropertyMeta> idPropertyMeta) {
        return "count(*)";
    }

    public ValueType getValueType(PropertyMeta propertyMeta) {
        return propertyMeta.getValueType();
    }

    public ValueType getValueType(Class<?> clazz, boolean lob,
            TemporalType temporalType) {
        if (lob) {
            if (clazz == String.class) {
                return ValueTypes.CLOB;
            } else if (clazz == byte[].class) {
                return ValueTypes.BLOB;
            } else if (Serializable.class.isAssignableFrom(clazz)) {
                return ValueTypes.SERIALIZABLE_BLOB;
            }
        } else if (clazz == byte[].class) {
            return ValueTypes.BYTE_ARRAY;
        }

        if (temporalType != null) {
            if (Date.class == clazz) {
                switch (temporalType) {
                case DATE:
                    return ValueTypes.DATE_SQLDATE;
                case TIME:
                    return ValueTypes.DATE_TIME;
                case TIMESTAMP:
                    return ValueTypes.DATE_TIMESTAMP;
                }
            }
            if (Calendar.class == clazz) {
                switch (temporalType) {
                case DATE:
                    return ValueTypes.CALENDAR_SQLDATE;
                case TIME:
                    return ValueTypes.CALENDAR_TIME;
                case TIMESTAMP:
                    return ValueTypes.CALENDAR_TIMESTAMP;
                }
            }
        }

        ValueType valueType = getValueTypeInternal(clazz);
        if (valueType == null) {
            valueType = ValueTypes.getValueType(clazz);
        }
        if (valueType == ValueTypes.OBJECT
                && Serializable.class.isAssignableFrom(clazz)) {
            return ValueTypes.SERIALIZABLE_BYTE_ARRAY;
        }
        return valueType;
    }

    /**
     * 値タイプを返します。
     * 
     * @param clazz
     *            クラス
     * @return 値タイプ
     */
    protected ValueType getValueTypeInternal(@SuppressWarnings("unused")
    Class<?> clazz) {
        return null;
    }

    public void setupJoin(FromClause fromClause, WhereClause whereClause,
            JoinType joinType, String tableName, String tableAlias,
            String fkTableAlias, String pkTableAlias,
            List<JoinColumnMeta> joinColumnMetaList, String lockHint,
            String condition) {
        fromClause.addSql(joinType, tableName, tableAlias, fkTableAlias,
                pkTableAlias, joinColumnMetaList, lockHint, condition);

    }

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

    public boolean supportsIdentity() {
        return false;
    }

    public boolean isInsertIdentityColumn() {
        return false;
    }

    public boolean supportsGetGeneratedKeys() {
        return false;
    }

    public String getIdentitySelectString(final String tableName,
            final String columnName) {
        return null;
    }

    public boolean supportsSequence() {
        return false;
    }

    public String getSequenceNextValString(final String sequenceName,
            final int allocationSize) {
        return null;
    }

    public int getDefaultBatchSize() {
        return 0;
    }

    public boolean supportsBatchUpdateResults() {
        return true;
    }

    /**
     * 行番号ファンクション名を返します。
     * 
     * @return 行番号ファンクション名
     */
    protected String getRowNumberFunctionName() {
        return "row_number()";
    }

    /**
     * 行番号を使ったSQLに変換します。
     * 
     * @param sql
     *            SQL
     * @param offset
     *            オフセット
     * @param limit
     *            リミット
     * @return offset、limitつきのSQL
     * @throws OrderByNotFoundRuntimeException
     *             <code>order by</code>が見つからない場合。
     */
    protected String convertLimitSqlByRowNumber(String sql, int offset,
            int limit) throws OrderByNotFoundRuntimeException {
        StringBuilder buf = new StringBuilder(sql.length() + 150);
        String lowerSql = sql.toLowerCase();
        int startOfSelect = lowerSql.indexOf("select");
        buf.append(sql.substring(0, startOfSelect));
        buf.append("select * from ( select temp_.*, ");
        buf.append(getRowNumberFunctionName()).append(" over(");
        int orderByIndex = lowerSql.lastIndexOf("order by");
        if (orderByIndex > 0) {
            String orderBy = sql.substring(orderByIndex);
            buf.append(convertOrderBy(orderBy));
            sql = StringUtil.rtrim(sql.substring(0, orderByIndex));
        } else {
            throw new OrderByNotFoundRuntimeException(sql);
        }
        buf.append(") as rownumber_ from ( ");
        buf.append(sql.substring(startOfSelect));
        buf.append(" ) as temp_");
        buf.append(" ) as temp2_ where rownumber_ >= ");
        buf.append(offset + 1);
        if (limit > 0) {
            buf.append(" and rownumber_ <= ");
            buf.append(offset + limit);
        }
        return buf.toString();
    }

    /**
     * order by句のテーブルのエイリアスを一時的なテーブル名に変換します。
     * 
     * @param orderBy
     *            order by句
     * @return 変換後のorder by句
     */
    protected String convertOrderBy(String orderBy) {
        StringBuilder sb = new StringBuilder(10 + orderBy.length());
        QueryTokenizer tokenizer = new QueryTokenizer(orderBy);
        for (int type = tokenizer.nextToken(); type != QueryTokenizer.TT_EOF; type = tokenizer
                .nextToken()) {
            String token = tokenizer.getToken();
            if (type == QueryTokenizer.TT_WORD) {
                String[] names = StringUtil.split(token, ".");
                if (names.length == 2) {
                    sb.append("temp_.").append(names[1]);
                } else {
                    sb.append(token);
                }
            } else {
                sb.append(token);
            }
        }
        return sb.toString();
    }

    public boolean isUniqueConstraintViolation(Throwable t) {
        final String state = getSQLState(t);
        if (state != null && state.length() >= 2) {
            return entityExistsExceptionStateCode.contains(state
                    .substring(0, 2));
        }
        return false;
    }

    /**
     * 例外チェーンをたどって原因となった{@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;
    }

    public boolean supportsForUpdate(final SelectForUpdateType type,
            boolean withTarget) {
        return type == SelectForUpdateType.NORMAL && !withTarget;
    }

    public String getForUpdateString(final SelectForUpdateType type,
            final int waitSeconds, final Pair<String, String>... aliases) {
        return " for update";
    }

    public boolean supportsLockHint() {
        return false;
    }

    public String getLockHintString(final SelectForUpdateType type,
            final int waitSeconds) {
        return "";
    }

    public boolean supportsInnerJoinForUpdate() {
        return true;
    }

    public boolean supportsOuterJoinForUpdate() {
        return true;
    }

    public String getHintComment(String hint) {
        return "";
    }

}
