/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.storage.rdbms.migration;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.commons.lang.StringUtils;

/**
 * This Utility class offers utility methods which may be of use to perform
 * common Java database migration task(s) (via Flyway).
 * <P>
 * NOTE: This class specifically CANNOT utilize Hibernate, because it is
 * used for database migrations which take place PRIOR to Hibernate loading.
 * However, as you'll see below, all methods are protected to ensure the rest
 * of the API cannot bypass Hibernate.
 * 
 * @author Tim Donohue
 */
public class MigrationUtils
{
    /**
     * Drop a given Database Constraint (based on the current database type).
     * Returns a "checksum" for this migration which can be used as part of
     * a Flyway Java migration
     *
     * @param connection the current Database connection
     * @param tableName the name of the table the constraint applies to
     * @param columnName the name of the column the constraint applies to
     * @param constraintSuffix Only used for PostgreSQL, whose constraint naming convention depends on a suffix (key, fkey, etc)
     * @return migration checksum as an Integer
     * @throws SQLException if a database error occurs
     */
    protected static Integer dropDBConstraint(Connection connection, String tableName, String columnName, String constraintSuffix)
            throws SQLException
    {
        Integer checksum = -1;

        // First, in order to drop the appropriate Database constraint, we
        // must determine the unique name of the constraint. As constraint
        // naming is DB specific, this is dependent on our DB Type
        String dbtype = connection.getMetaData().getDatabaseProductName();
        String constraintName = null;
        String constraintNameSQL = null;
        boolean cascade = false;
        switch(dbtype.toLowerCase())
        {
            case "postgres":
            case "postgresql":
                // In Postgres, constraints are always named:
                // {tablename}_{columnname(s)}_{suffix}
                // see: http://stackoverflow.com/a/4108266/3750035
                constraintName = StringUtils.lowerCase(tableName);
                if(!StringUtils.equals(constraintSuffix, "pkey"))
                {
                    constraintName += "_" + StringUtils.lowerCase(columnName);
                }

                constraintName += "_" + StringUtils.lowerCase(constraintSuffix);
                cascade = true;
                break;
            case "oracle":
                // In Oracle, constraints are listed in the USER_CONS_COLUMNS table
                constraintNameSQL = "SELECT CONSTRAINT_NAME " +
                                    "FROM USER_CONS_COLUMNS " +
                                    "WHERE TABLE_NAME = ? AND COLUMN_NAME = ?";
                cascade = true;
                break;
            case "h2":
                // In H2, constraints are listed in the "information_schema.constraints" table
                constraintNameSQL = "SELECT DISTINCT CONSTRAINT_NAME " +
                                    "FROM information_schema.constraints " +
                                    "WHERE table_name = ? AND column_list = ?";
                break;
            default:
                throw new SQLException("DBMS " + dbtype + " is unsupported in this migration.");
        }

        // If we have a SQL query to run for the constraint name, then run it
        if (constraintNameSQL!=null)
        {
            // Run the query to obtain the constraint name, passing it the parameters
            PreparedStatement statement = connection.prepareStatement(constraintNameSQL);
            statement.setString(1, StringUtils.upperCase(tableName));
            statement.setString(2, StringUtils.upperCase(columnName));
            try
            {
                ResultSet results = statement.executeQuery();
                if(results.next())
                {
                    constraintName = results.getString("CONSTRAINT_NAME");
                }
                results.close();
            }
            finally
            {
                statement.close();
            }
        }

        // As long as we have a constraint name, drop it
        if (constraintName!=null && !constraintName.isEmpty())
        {
            // This drop constaint SQL should be the same in all databases
            String dropConstraintSQL = "ALTER TABLE " + tableName + " DROP CONSTRAINT " + constraintName;
            if(cascade){
                dropConstraintSQL += " CASCADE";
            }

            try(PreparedStatement statement = connection.prepareStatement(dropConstraintSQL))
            {
                statement.execute();
            }
            // Return the size of the query we just ran
            // This will be our "checksum" for this Flyway migration (see getChecksum())
            checksum = dropConstraintSQL.length();
        }

        return checksum;
    }
    
    
    /**
     * Drop a given Database Table (based on the current database type).
     * Returns a "checksum" for this migration which can be used as part of
     * a Flyway Java migration
     * <P>
     * NOTE: Ideally, if you need to do a DROP TABLE, you should just create
     * a Flyway SQL migration. This method should ONLY be used if the table name
     * needs to be dynamically determined via Java.
     *
     * @param connection the current Database connection
     * @param tableName the name of the table to drop
     * @return migration checksum as an Integer
     * @throws SQLException if a database error occurs
     */
    protected static Integer dropDBTable(Connection connection, String tableName)
            throws SQLException
    {
        String dropTableSQL = null;
        Integer checksum = -1;

        // First, in order to drop the appropriate Database table, we must 
        // determine the query based on DB type
        String dbtype = connection.getMetaData().getDatabaseProductName();
        switch(dbtype.toLowerCase())
        {
            case "postgres":
            case "postgresql":
                dropTableSQL = "DROP TABLE IF EXISTS " + tableName + " CASCADE";
                break;
            case "oracle":
                dropTableSQL = "DROP TABLE " + tableName + " CASCADE CONSTRAINTS";
                break;
            case "h2":
                dropTableSQL = "DROP TABLE IF EXISTS " + tableName + " CASCADE";
                break;
            default:
                throw new SQLException("DBMS " + dbtype + " is unsupported in this migration.");
        }
        
        // If we have a SQL query to run, then run it
        if (dropTableSQL!=null)
        {
            try(PreparedStatement statement = connection.prepareStatement(dropTableSQL))
            {
                statement.execute();
            }
            // Return the size of the query we just ran
            // This will be our "checksum" for this Flyway migration (see getChecksum())
            checksum = dropTableSQL.length();
        }
        
        return checksum;
    }
    
    /**
     * Drop a given Database Sequence (based on the current database type).
     * Returns a "checksum" for this migration which can be used as part of
     * a Flyway Java migration
     * <P>
     * NOTE: Ideally, if you need to do a DROP SEQUENCE, you should just create
     * a Flyway SQL migration. This method should ONLY be used if the sequence name
     * needs to be dynamically determined via Java.
     *
     * @param connection the current Database connection
     * @param sequenceName the name of the sequence to drop
     * @return migration checksum as an Integer
     * @throws SQLException if a database error occurs
     */
    protected static Integer dropDBSequence(Connection connection, String sequenceName)
            throws SQLException
    {
        String dropSequenceSQL = null;
        Integer checksum = -1;

        String dbtype = connection.getMetaData().getDatabaseProductName();
        switch(dbtype.toLowerCase())
        {
            case "postgres":
            case "postgresql":
                dropSequenceSQL = "DROP SEQUENCE IF EXISTS " + sequenceName;
                break;
            case "oracle":
                dropSequenceSQL = "DROP SEQUENCE " + sequenceName ;
                break;
            case "h2":
                dropSequenceSQL = "DROP SEQUENCE IF EXISTS " + sequenceName;
                break;
            default:
                throw new SQLException("DBMS " + dbtype + " is unsupported in this migration.");
        }

        // If we have a SQL query to run, then run it
        if (dropSequenceSQL!=null)
        {
            try(PreparedStatement statement = connection.prepareStatement(dropSequenceSQL))
            {
                statement.execute();
            }
            // Return the size of the query we just ran
            // This will be our "checksum" for this Flyway migration (see getChecksum())
            checksum = dropSequenceSQL.length();
        }
        
        return checksum;
    }
    
    /**
     * Drop a given Database View (based on the current database type).
     * Returns a "checksum" for this migration which can be used as part of
     * a Flyway Java migration
     * <P>
     * NOTE: Ideally, if you need to do a DROP VIEW, you should just create
     * a Flyway SQL migration. This method should ONLY be used if the view name
     * needs to be dynamically determined via Java.
     *
     * @param connection the current Database connection
     * @param viewName the name of the view to drop
     * @return migration checksum as an Integer
     * @throws SQLException if a database error occurs
     */
    protected static Integer dropDBView(Connection connection, String viewName)
            throws SQLException
    {
        String dropViewSQL = null;
        Integer checksum = -1;

        String dbtype = connection.getMetaData().getDatabaseProductName();
        switch(dbtype.toLowerCase())
        {
            case "postgres":
            case "postgresql":
                dropViewSQL = "DROP VIEW IF EXISTS " + viewName + " CASCADE";
                break;
            case "oracle":
                dropViewSQL = "DROP VIEW " + viewName + " CASCADE CONSTRAINTS";
                break;
            case "h2":
                dropViewSQL = "DROP VIEW IF EXISTS " + viewName + " CASCADE";
                break;
            default:
                throw new SQLException("DBMS " + dbtype + " is unsupported in this migration.");
        }

        // If we have a SQL query to run, then run it
        if (dropViewSQL!=null)
        {
            try(PreparedStatement statement = connection.prepareStatement(dropViewSQL))
            {
                statement.execute();
            }
            // Return the size of the query we just ran
            // This will be our "checksum" for this Flyway migration (see getChecksum())
            checksum = dropViewSQL.length();
        }
        
        return checksum;
    }
}
