/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Middleware LLC.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 *
 */
package com.querydsl.jpa.support;

import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;

import org.hibernate.HibernateException;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.function.SQLFunctionTemplate;
import org.hibernate.dialect.function.VarArgsSQLFunction;
import org.hibernate.type.StandardBasicTypes;

/**
 * A dialect for the Teradata database created by MCR as part of the dialect
 * certification process.
 *
 * @author Jay Nance
 */
public class TeradataDialect extends Dialect {

    /**
     * Constructor
     */
    public TeradataDialect() {
        super();
        // registerColumnType data types
        registerColumnType(Types.NUMERIC, "NUMERIC($p,$s)");
        registerColumnType(Types.DOUBLE, "DOUBLE PRECISION");
        registerColumnType(Types.BIGINT, "NUMERIC(18,0)");
        registerColumnType(Types.BIT, "BYTEINT");
        registerColumnType(Types.TINYINT, "BYTEINT");
        registerColumnType(Types.VARBINARY, "VARBYTE($l)");
        registerColumnType(Types.BINARY, "BYTEINT");
        registerColumnType(Types.LONGVARCHAR, "LONG VARCHAR");
        registerColumnType(Types.CHAR, "CHAR(1)");
        registerColumnType(Types.DECIMAL, "DECIMAL");
        registerColumnType(Types.INTEGER, "INTEGER");
        registerColumnType(Types.SMALLINT, "SMALLINT");
        registerColumnType(Types.FLOAT, "FLOAT");
        registerColumnType(Types.VARCHAR, "VARCHAR($l)");
        registerColumnType(Types.DATE, "DATE");
        registerColumnType(Types.TIME, "TIME");
        registerColumnType(Types.TIMESTAMP, "TIMESTAMP");
        registerColumnType(Types.BOOLEAN, "BYTEINT"); // hibernate seems to
                                                      // ignore this type...
        registerColumnType(Types.BLOB, "BLOB");
        registerColumnType(Types.CLOB, "CLOB");

        registerFunction("year", new SQLFunctionTemplate(StandardBasicTypes.INTEGER,
                "extract(year from ?1)"));
        registerFunction("length", new SQLFunctionTemplate(StandardBasicTypes.INTEGER,
                "character_length(?1)"));
        registerFunction("concat",
                new VarArgsSQLFunction(StandardBasicTypes.STRING, "(", "||", ")"));
        registerFunction("substring", new SQLFunctionTemplate(StandardBasicTypes.STRING,
                "substring(?1 from ?2 for ?3)"));
        registerFunction("locate", new SQLFunctionTemplate(StandardBasicTypes.STRING,
                "position(?1 in ?2)"));
        registerFunction("mod", new SQLFunctionTemplate(StandardBasicTypes.STRING, "?1 mod ?2"));
        registerFunction("str", new SQLFunctionTemplate(StandardBasicTypes.STRING,
                "cast(?1 as varchar(255))"));

        // bit_length feels a bit broken to me. We have to cast to char in order
        // to
        // pass when a numeric value is supplied. But of course the answers
        // given will
        // be wildly different for these two datatypes. 1234.5678 will be 9
        // bytes as
        // a char string but will be 8 or 16 bytes as a true numeric.
        // Jay Nance 2006-09-22
        registerFunction("bit_length", new SQLFunctionTemplate(StandardBasicTypes.INTEGER,
                "octet_length(cast(?1 as char))*4"));

        // The preference here would be
        // SQLFunctionTemplate( Hibernate.TIMESTAMP, "current_timestamp(?1)",
        // false)
        // but this appears not to work.
        // Jay Nance 2006-09-22
        registerFunction("current_timestamp", new SQLFunctionTemplate(StandardBasicTypes.TIMESTAMP,
                "current_timestamp"));
        registerFunction("current_time", new SQLFunctionTemplate(StandardBasicTypes.TIME,
                "current_time"));
        registerFunction("current_date", new SQLFunctionTemplate(StandardBasicTypes.DATE,
                "current_date"));
        // IBID for current_time and current_date

        registerKeyword("account");
        registerKeyword("alias");
        registerKeyword("class");
        registerKeyword("column");
        registerKeyword("first");
        registerKeyword("map");
        registerKeyword("month");
        registerKeyword("password");
        registerKeyword("role");
        registerKeyword("summary");
        registerKeyword("title");
        registerKeyword("type");
        registerKeyword("value");
        registerKeyword("year");

        // Tell hibernate to use getBytes instead of getBinaryStream
        getDefaultProperties().setProperty(Environment.USE_STREAMS_FOR_BINARY, "false");
        // No batch statements
        getDefaultProperties().setProperty(Environment.STATEMENT_BATCH_SIZE, NO_BATCH);
    }

    /**
     * Does this dialect support the <tt>FOR UPDATE</tt> syntax?
     *
     * @return empty string ... Teradata does not support
     *         <tt>FOR UPDATE<tt> syntax
     */
    @Override
    public String getForUpdateString() {
        return "";
    }

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

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

    @Override
    public String getAddColumnString() {
        return "Add";
    }

    public boolean supportsTemporaryTables() {
        return true;
    }

    public String getCreateTemporaryTableString() {
        return "create global temporary table";
    }

    public String getCreateTemporaryTablePostfix() {
        return " on commit preserve rows";
    }

    public Boolean performTemporaryTableDDLInIsolation() {
        return Boolean.TRUE;
    }

    public boolean dropTemporaryTableAfterUse() {
        return false;
    }

    /**
     * Get the name of the database type associated with the given
     * <tt>java.sql.Types</tt> typecode.
     *
     * @param code
     *            <tt>java.sql.Types</tt> typecode
     * @param length
     *            the length or precision of the column
     * @param precision
     *            the precision of the column
     * @param scale
     *            the scale of the column
     *
     * @return the database type name
     *
     * @throws HibernateException
     */
    public String getTypeName(int code, int length, int precision, int scale)
            throws HibernateException {
        /*
         * We might want a special case for 19,2. This is very common for money
         * types and here it is converted to 18,1
         */
        float f = precision > 0 ? (float) scale / (float) precision : 0;
        int p = (precision > 18 ? 18 : precision);
        int s = (precision > 18 ? (int) (18.0 * f) : (scale > 18 ? 18 : scale));

        return super.getTypeName(code, length, p, s);
    }

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

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

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

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

    @Override
    public String getSelectClauseNullString(int sqlType) {
        String v = "null";

        switch (sqlType) {
        case Types.BIT:
        case Types.TINYINT:
        case Types.SMALLINT:
        case Types.INTEGER:
        case Types.BIGINT:
        case Types.FLOAT:
        case Types.REAL:
        case Types.DOUBLE:
        case Types.NUMERIC:
        case Types.DECIMAL:
            v = "cast(null as decimal)";
            break;
        case Types.CHAR:
        case Types.VARCHAR:
        case Types.LONGVARCHAR:
            v = "cast(null as varchar(255))";
            break;
        case Types.DATE:
        case Types.TIME:
        case Types.TIMESTAMP:
            v = "cast(null as timestamp)";
            break;
        case Types.BINARY:
        case Types.VARBINARY:
        case Types.LONGVARBINARY:
        case Types.NULL:
        case Types.OTHER:
        case Types.JAVA_OBJECT:
        case Types.DISTINCT:
        case Types.STRUCT:
        case Types.ARRAY:
        case Types.BLOB:
        case Types.CLOB:
        case Types.REF:
        case Types.DATALINK:
        case Types.BOOLEAN:
            break;
        }
        return v;
    }

    @Override
    public String getCreateMultisetTableString() {
        return "create multiset table ";
    }

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

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

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

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

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

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

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

    public boolean supportsDropPreProcess() {
        return true;
    }

    public String performDropPreProcess(Statement stmt, String dropSql) throws SQLException {

        String alterStr = "alter";
        String tableStr = "table";
        String dropStr = "drop";
        String constraintStr = "constraint";

        java.util.StringTokenizer st = new java.util.StringTokenizer(dropSql);
        if (alterStr.equalsIgnoreCase(st.nextToken()) && tableStr.equalsIgnoreCase(st.nextToken())) {
            String tableName = st.nextToken();

            if ((tableName.startsWith("\"")) && (!tableName.endsWith("\""))) {
                String next = null;
                while (true) {
                    next = st.nextToken();
                    tableName += " " + next;
                    if (next.endsWith("\\\"")) {
                        continue;
                    }
                    if (next.endsWith("\"")) {
                        break;
                    }
                }

            }
            if (dropStr.equalsIgnoreCase(st.nextToken())
                    && constraintStr.equalsIgnoreCase(st.nextToken())) {
                String constraintName = st.nextToken();

                // Table name might have whitespace characters within name so
                // just take whatever lies between
                // "alter table " and "drop constraint"

                int idxStart = dropSql.indexOf(tableStr, 0) + 5;
                int idxEnd = dropSql.lastIndexOf(dropStr);
                tableName = dropSql.substring(idxStart, idxEnd).trim();

                if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
                    tableName = tableName.substring(1, tableName.length() - 1);
                }

                String arrStr = null;
                String queryStr = "sel IndexId, ChildTable, IndexName from dbc.RI_Distinct_ChildrenV where IndexName = '"
                        + constraintName + "'";
                java.sql.ResultSet rs = stmt.executeQuery(queryStr);

                if (rs.next()) {
                    arrStr = "drop table \"" + tableName + "_" + rs.getString(1) + "\"";
                    rs.close();
                    return arrStr;
                }
            }
        }
        return null;
    }

    public void performDropPostProcess(Statement stmt, String dropSql) throws SQLException {
        if (dropSql == null) {
            return;
        }
        stmt.executeUpdate(dropSql);
    }
}