/*
 * 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.math.BigInteger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Arrays;
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.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.ClobType;
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.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.CharUtil;
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.util.CaseInsensitiveMap;
import org.seasar.framework.util.PreparedStatementUtil;
import org.seasar.framework.util.ResultSetUtil;
import org.seasar.framework.util.StatementUtil;
import org.seasar.framework.util.StringUtil;

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

    /** テーブルが見つからないことを示すエラーコード */
    protected static int TABLE_NOT_FOUND_ERROR_CODE = 942;

    /** カラムが見つからないことを示すエラーコード */
    protected static int COLUMN_NOT_FOUND_ERROR_CODE = 904;

    /** シーケンスが見つからないことを示すエラーコード */
    protected static int SEQUENCE_NOT_FOUND_ERROR_CODE = 2289;

    /** Oracle固有の{@literal DATE}型を使用する場合は{@literal true} */
    protected boolean useOracleDate = true;

    /**
     * インスタンスを構築します。
     */
    public OracleGenDialect() {
        sqlTypeMap.put(Types.BINARY, new BinaryType("raw($l)"));
        sqlTypeMap.put(Types.BIGINT, new BigIntType("number($p,0)"));
        sqlTypeMap.put(Types.BLOB, new BlobType("blob"));
        sqlTypeMap.put(Types.BOOLEAN, new BooleanType("number(1,0)"));
        sqlTypeMap.put(Types.CLOB, new ClobType("clob"));
        sqlTypeMap.put(Types.DECIMAL, new DecimalType("number($p,$s)"));
        sqlTypeMap.put(Types.DOUBLE, new DoubleType("double precision"));
        sqlTypeMap.put(Types.INTEGER, new IntegerType("number($p,0)"));
        sqlTypeMap.put(Types.SMALLINT, new SmallIntType("number($p,0)"));
        sqlTypeMap.put(Types.TIME, new TimeType("date"));
        sqlTypeMap.put(Types.VARCHAR, new VarcharType("varchar2($l)"));

        columnTypeMap.put("binary_double", OracleColumnType.BINARY_DOUBLE);
        columnTypeMap.put("binary_float", OracleColumnType.BINARY_FLOAT);
        columnTypeMap.put("blob", OracleColumnType.BLOB);
        columnTypeMap.put("clob", OracleColumnType.CLOB);
        columnTypeMap.put("long", OracleColumnType.LONG);
        columnTypeMap.put("long raw", OracleColumnType.LONG_RAW);
        columnTypeMap.put("nchar", OracleColumnType.NCHAR);
        columnTypeMap.put("nclob", OracleColumnType.NCLOB);
        columnTypeMap.put("number", OracleColumnType.NUMBER);
        columnTypeMap.put("nvarchar2", OracleColumnType.NVARCHAR2);
        columnTypeMap.put("raw", OracleColumnType.RAW);
        columnTypeMap.put("timestamp", OracleColumnType.TIMESTAMP);
        columnTypeMap.put("varchar2", OracleColumnType.VARCHAR2);
    }

    /**
     * Oracle固有の{@literal DATE}型を使用する場合は{@literal true}を返します。
     * 
     * @return Oracle固有の{@literal DATE}型を使用する場合は{@literal true}
     */
    public boolean isUseOracleDate() {
        return useOracleDate;
    }

    /**
     * Oracle固有の{@literal DATE}型を使用する場合は{@literal true}を設定します。
     * 
     * @param useOracleDate
     *            Oracle固有の{@literal DATE}型を使用する場合は{@literal true}
     */
    public void setUseOracleDate(boolean useOracleDate) {
        this.useOracleDate = useOracleDate;
    }

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

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

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

    @Override
    public boolean supportsGetIndexInfo(String catalogName, String schemaName,
            String tableName) {
        if (tableName == null) {
            throw new NullPointerException("tableName");
        }
        for (int i = 0; i < tableName.length(); i++) {
            if (!CharUtil.isAscii(tableName.charAt(i))) {
                return false;
            }
        }
        return true;
    }

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

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

    @Override
    public boolean isTableNotFound(Throwable throwable) {
        Integer errorCode = getErrorCode(throwable);
        return errorCode != null
                && errorCode.intValue() == TABLE_NOT_FOUND_ERROR_CODE;
    }

    @Override
    public boolean isColumnNotFound(Throwable throwable) {
        Integer errorCode = getErrorCode(throwable);
        return errorCode != null
                && errorCode.intValue() == COLUMN_NOT_FOUND_ERROR_CODE;
    }

    @Override
    public boolean isSequenceNotFound(Throwable throwable) {
        Integer errorCode = getErrorCode(throwable);
        return errorCode != null
                && errorCode.intValue() == SEQUENCE_NOT_FOUND_ERROR_CODE;
    }

    @Override
    public ColumnType getColumnType(String typeName, int sqlType) {
        if (useOracleDate && StringUtil.equalsIgnoreCase(typeName, "date")) {
            return OracleColumnType.DATE;
        }
        ColumnType columnType = columnTypeMap.get(typeName);
        if (columnType != null) {
            return columnType;
        }

        if (StringUtil.startsWithIgnoreCase(typeName, "timestamp")) {
            typeName = "timestamp";
        }
        return super.getColumnType(typeName, sqlType);
    }

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

    @Override
    public String getSequenceNextValString(String sequenceName,
            int allocationSize) {
        return "select " + sequenceName + ".nextval from dual";
    }

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

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

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

    @Override
    public String getTableComment(Connection connection, String catalogName,
            String schemaName, String tableName) throws SQLException {
        String sql = "select comments from all_tab_comments where owner = ? and table_name = ? and table_type = 'TABLE'";
        logger.debug(String.format(sql.replace("?", "'%s'"), schemaName,
                tableName));
        PreparedStatement ps = ConnectionUtil.prepareStatement(connection, sql);
        try {
            ps.setString(1, schemaName);
            ps.setString(2, tableName);
            ResultSet rs = PreparedStatementUtil.executeQuery(ps);
            try {
                if (rs.next()) {
                    return rs.getString(1);
                }
                return null;
            } finally {
                ResultSetUtil.close(rs);
            }
        } finally {
            StatementUtil.close(ps);
        }
    }

    @Override
    public Map<String, String> getColumnCommentMap(Connection connection,
            String catalogName, String schemaName, String tableName)
            throws SQLException {
        String sql = "select column_name, comments from all_col_comments where owner = ? and table_name = ?";
        logger.debug(String.format(sql.replace("?", "'%s'"), schemaName,
                tableName));
        PreparedStatement ps = ConnectionUtil.prepareStatement(connection, sql);
        try {
            ps.setString(1, schemaName);
            ps.setString(2, tableName);
            ResultSet rs = PreparedStatementUtil.executeQuery(ps);
            try {
                @SuppressWarnings("unchecked")
                Map<String, String> commentMap = new CaseInsensitiveMap();
                while (rs.next()) {
                    commentMap.put(rs.getString(1), rs.getString(2));
                }
                return commentMap;
            } finally {
                ResultSetUtil.close(rs);
            }
        } finally {
            StatementUtil.close(ps);
        }
    }

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

    @Override
    public SqlType getSqlType(ValueTypeProvider valueTypeProvider,
            PropertyMeta propertyMeta) {
        if (useOracleDate) {
            ValueType valueType = valueTypeProvider.provide(propertyMeta);
            if (valueType instanceof org.seasar.extension.jdbc.types.OracleDateType
                    || valueType instanceof org.seasar.extension.jdbc.types.OracleDateCalendarType) {
                return new TimestampType("date");
            }
        }
        return super.getSqlType(valueTypeProvider, propertyMeta);
    }

    /**
     * Oracle用の{@link ColumnType}の実装クラスです。
     * 
     * @author taedium
     */
    public static class OracleColumnType extends StandardColumnType {

        private static OracleColumnType BINARY_DOUBLE = new OracleColumnType(
                "binary_double", Double.class);

        private static OracleColumnType BINARY_FLOAT = new OracleColumnType(
                "binary_float", Float.class);

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

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

        private static OracleColumnType DATE = new OracleColumnType("date",
                Timestamp.class, false, TemporalType.TIMESTAMP);

        private static OracleColumnType LONG_RAW = new OracleColumnType(
                "long raw", byte[].class);

        private static OracleColumnType LONG = new OracleColumnType("long",
                String.class);

        private static OracleColumnType NCHAR = new OracleColumnType(
                "nchar($l)", String.class) {

            @Override
            public String getColumnDefinition(int length, int precision,
                    int scale, String defaultValue) {
                return super.getColumnDefinition(length / 2, precision, scale,
                        defaultValue);
            }
        };

        private static OracleColumnType NCLOB = new OracleColumnType("nclob",
                String.class, true);

        private static OracleColumnType NUMBER = new OracleColumnType(
                "number($p,$s)", BigDecimal.class) {

            @Override
            public Class<?> getAttributeClass(int length, int precision,
                    int scale) {
                if (scale != 0) {
                    return BigDecimal.class;
                }
                if (precision < 5) {
                    return Short.class;
                }
                if (precision < 10) {
                    return Integer.class;
                }
                if (precision < 19) {
                    return Long.class;
                }
                return BigInteger.class;
            }
        };

        private static OracleColumnType NVARCHAR2 = new OracleColumnType(
                "nvarchar2($l)", String.class) {

            @Override
            public String getColumnDefinition(int length, int precision,
                    int scale, String defaultValue) {
                return super.getColumnDefinition(length / 2, precision, scale,
                        defaultValue);
            }
        };

        private static OracleColumnType RAW = new OracleColumnType("raw($l)",
                byte[].class);

        private static OracleColumnType TIMESTAMP = new OracleColumnType(
                "timestamp($s)", Timestamp.class);

        private static OracleColumnType VARCHAR2 = new OracleColumnType(
                "varchar2($l)", String.class);

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

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

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

    /**
     * Oracle用の{@link StandardColumnType}の実装クラスです。
     * 
     * @author taedium
     */
    public static class OracleSqlBlockContext extends StandardSqlBlockContext {

        /**
         * インスタンスを構築します。
         */
        protected OracleSqlBlockContext() {
            sqlBlockStartKeywordsList.add(Arrays.asList("create", "or",
                    "replace", "procedure"));
            sqlBlockStartKeywordsList.add(Arrays.asList("create", "or",
                    "replace", "function"));
            sqlBlockStartKeywordsList.add(Arrays.asList("create", "or",
                    "replace", "triger"));
            sqlBlockStartKeywordsList.add(Arrays.asList("create", "procedure"));
            sqlBlockStartKeywordsList.add(Arrays.asList("create", "function"));
            sqlBlockStartKeywordsList.add(Arrays.asList("create", "trigger"));
            sqlBlockStartKeywordsList.add(Arrays.asList("declare"));
            sqlBlockStartKeywordsList.add(Arrays.asList("begin"));
        }
    }

}
