/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.core.db;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.util.Hashtable;
import java.util.Map;

import javax.naming.Context;
import javax.sql.DataSource;

import mazz.i18n.Logger;

/**
 * A factory class that creates {@link DatabaseType} objects based on the database being connected to. This should be
 * the only class (or at least one of the very few) that actually has database-specific knowledge coded into it.
 */
public class DatabaseTypeFactory {
    private static final Logger LOG = DbUtilsI18NFactory.getLogger(DatabaseTypeFactory.class);

    /**
     * Maps the database type that is associated with a particular connection type.
     */
    private static final Map<String, Class<? extends DatabaseType>> DATABASE_TYPES = new Hashtable<String, Class<? extends DatabaseType>>();

    /**
     * This provides a map that keys JDBC URL protocols with their corresponding <code>java.sql.Driver</code> classes.
     */
    private static final Map<String, String> DB_URL_DRIVER_MAP;

    /**
     * This can be set to store the defaultDatabaseType. Typically set it application initialization to
     * provide a quick way to get the (typically) unchanging db vendor setting.
     */
    private static DatabaseType defaultDatabaseType = null;

    static {
        DB_URL_DRIVER_MAP = new Hashtable<String, String>();
        DB_URL_DRIVER_MAP.put("jdbc:postgresql:", "org.postgresql.Driver");
        DB_URL_DRIVER_MAP.put("jdbc:oracle:thin:@", "oracle.jdbc.driver.OracleDriver");
        DB_URL_DRIVER_MAP.put("jdbc:oracle:oci8:", "oracle.jdbc.driver.OracleDriver");
        DB_URL_DRIVER_MAP.put("jdbc:h2:", "org.h2.Driver");
        DB_URL_DRIVER_MAP.put("jdbc:jtds:sqlserver", "net.sourceforge.jtds.jdbc.Driver");
    }

    /**
     * Prevents instantiation.
     */
    private DatabaseTypeFactory() {
        // don't instantiate us from external
    }

    /**
     * By default, this factory will remember what {@link DatabaseType} is associated with
     * a specific connection class type. However, if you change the JDBC URL, its possible
     * that this will also change the database type the connection class type should be
     * associated with (i.e. the URL may point to a different version of the same vendor's
     * database).  If you change the JDBC URL, you should call this method to clear the
     * cache, thus enabling the factory to re-check the connection metadata to associate
     * a new database type with the connection class type.
     */
    public static void clearDatabaseTypeCache() {
        DATABASE_TYPES.clear();
    }

    /**
     * Convenience method that gives you the {@link DatabaseType} given a context and datasource. This is a combination
     * of {@link #getDatabaseType(Context, String)} and {@link #getDatabaseType(Connection)}.
     *
     * @param  context    the context where the datasource can be found
     * @param  datasource the name of the datasource within the given context
     *
     * @return the {@link DatabaseType} of the type of database behind the data source
     *
     * @throws Exception if cannot find the datasource in the given context or the connection cannot be created or if
     *                   cannot determine what database the connection is connected to
     */
    public static DatabaseType getDatabaseType(Context context, String datasource) throws Exception {
        Connection c = getConnection(context, datasource);

        try {
            return getDatabaseType(c);
        } finally {
            try {
                c.close();
            } catch (Exception e) {
            }
        }
    }

    /**
     * Given a context and a JNDI datasource name, this will lookup within that context to find the named datasource. A
     * connection to that datasource will be created and returned - with the intent that the connection will be passed
     * to {@link #getDatabaseType(Connection)} to create a {@link DatabaseType}.
     *
     * @param  context    the context where the datasource can be found
     * @param  datasource the name of the datasource within the given context
     *
     * @return a connection to the given datasource
     *
     * @throws Exception if cannot find the datasource in the given context or the connection cannot be created
     */
    public static Connection getConnection(Context context, String datasource) throws Exception {
        DataSource ds = (DataSource) context.lookup(datasource);

        return ds.getConnection();
    }

    /**
     * Given a <code>Connection</code> object, this method returns its associated {@link DatabaseType}.
     *
     * @param  conn
     *
     * @return the {@link DatabaseType} that the connection is connected to
     *
     * @throws Exception if cannot determine what database the connection is connected to
     */
    public static DatabaseType getDatabaseType(Connection conn) throws Exception {
        String conn_class = conn.getClass().getName();
        Class<? extends DatabaseType> database_type_class = DATABASE_TYPES.get(conn_class);

        if (database_type_class == null) {
            DatabaseMetaData db_metadata = conn.getMetaData();
            String db_name = db_metadata.getDatabaseProductName().toLowerCase();
            String db_version = db_metadata.getDatabaseProductVersion().toLowerCase();

            LOG.debug(DbUtilsI18NResourceKeys.DB_CONNECTION_METADATA, db_name, db_version);

            if (db_name.indexOf("postgresql") != -1) {
                if (db_version.startsWith("7.")) {
                    database_type_class = Postgresql7DatabaseType.class;
                } else if (db_version.startsWith("9.0")) {
                    database_type_class = Postgresql90DatabaseType.class;
                } else if (db_version.startsWith("9.")) {  // 9.1, 9.2, 9.x...
                    database_type_class = Postgresql91DatabaseType.class;
                } else if (db_version.startsWith("8.4")) {
                    database_type_class = Postgresql84DatabaseType.class;
                } else if (db_version.startsWith("8.3")) {
                    database_type_class = Postgresql83DatabaseType.class;
                } else if (db_version.startsWith("8.2")) {
                    database_type_class = Postgresql82DatabaseType.class;
                } else if (db_version.startsWith("8.1")) {
                    database_type_class = Postgresql81DatabaseType.class;
                } else if (db_version.startsWith("8.")) {
                    database_type_class = Postgresql8DatabaseType.class;
                }
            } else if (db_name.indexOf("oracle") != -1) {
                if (db_version.startsWith("oracle8")) {
                    database_type_class = Oracle8DatabaseType.class;
                } else if (db_version.startsWith("oracle9")) {
                    database_type_class = Oracle9DatabaseType.class;
                } else if (db_version.startsWith("oracle database 10g")) {
                    database_type_class = Oracle10DatabaseType.class;
                } else if (db_version.startsWith("oracle database 11g")) {
                    database_type_class = Oracle11DatabaseType.class;
                } else if (db_version.startsWith("oracle database 12c")) {
                    database_type_class = Oracle12DatabaseType.class;
                }
            } else if (db_name.indexOf("h2") != -1) {
                if (db_version.startsWith("1.1")) {
                    database_type_class = H2v11DatabaseType.class;
                } else if (db_version.startsWith("1.2")) {
                    database_type_class = H2v12DatabaseType.class;
                }
            } else if (db_name.indexOf("sql server") != -1) {
                if (db_version.startsWith("09.00") || db_version.startsWith("9.00")) { // SQL Server 2005
                    database_type_class = SQLServer2005DatabaseType.class;
                } else if (db_version.startsWith("10.00")) { // SQL Server 2008
                    database_type_class = SQLServer2008DatabaseType.class;
                }
            }

            if (database_type_class == null) {
                throw new Exception(DbUtilsI18NFactory.getMsg().getMsg(DbUtilsI18NResourceKeys.UNKNOWN_DATABASE,
                    db_name, db_version));
            }

            DATABASE_TYPES.put(conn_class, database_type_class);
        }

        return database_type_class.newInstance();
    }

    /**
     * This is the getter of a get/set convenience mechanism for storing and retrieving the active database type.
     * Not a true singleton but typically the value will not change as the underlying db is typically
     * not going to change at runtime.
     *
     * @return The current DatabaseType or null if not yet set.
     */
    public static DatabaseType getDefaultDatabaseType() {
        return DatabaseTypeFactory.defaultDatabaseType;
    }

    /**
     * This is the setter of a get/set convenience mechanism for storing and retrieving the active database type.
     * Typically called one time when the dbType is established.  Not a true singleton but typically the value will
     * not change as the underlying db is typically not going to change.
     */
    public static void setDefaultDatabaseType(DatabaseType databaseType) {
        DatabaseTypeFactory.defaultDatabaseType = databaseType;
    }

    /**
     * Given a JDBC URL, this will attempt to load in the JDBC driver class that will be needed to connect to the
     * database via that URL and will return that driver class name. If the driver class could not be loaded, <code>
     * null</code> will be returned - in which case attempting to connect to the database with the given URL will
     * probably fail.
     *
     * @param  jdbc_url a connection URL to the database
     *
     * @return the driver class that was attempted to have been loaded
     */
    public static String loadJdbcDriver(String jdbc_url) {
        String ret_driver_class = null;

        for (Map.Entry<String, String> url_driver_entry : DB_URL_DRIVER_MAP.entrySet()) {
            String url_prefix = url_driver_entry.getKey();
            if (jdbc_url.startsWith(url_prefix)) {
                String driver_class_to_load = url_driver_entry.getValue();
                try {
                    Class.forName(driver_class_to_load);
                    ret_driver_class = driver_class_to_load;
                } catch (ClassNotFoundException e) {
                    LOG.warn(DbUtilsI18NResourceKeys.CANNOT_LOAD_JDBC_DRIVER, driver_class_to_load, jdbc_url);
                }

                break;
            }
        }

        return ret_driver_class;
    }

    /**
     * Is the database PostgreSQL?
     *
     * @param  c the connection to the database
     *
     * @return <code>true</code> if the connection is talking to a Postgres database
     *
     * @throws Exception
     */
    public static boolean isPostgres(Connection c) throws Exception {
        DatabaseType type = getDatabaseType(c);
        return isPostgres(type);
    }

    /**
     * Determines if the given type refers to a Postgres database.
     *
     * @param  type
     *
     * @return <code>true</code> if the type is a Postgres database
     */
    public static boolean isPostgres(DatabaseType type) {
        return (type instanceof PostgresqlDatabaseType);
    }

    /**
     * Is the database Oracle?
     *
     * @param  c the connection to the database
     *
     * @return <code>true</code> if the connection is talking to an Oracle database
     *
     * @throws Exception
     */
    public static boolean isOracle(Connection c) throws Exception {
        DatabaseType type = getDatabaseType(c);
        return isOracle(type);
    }

    /**
     * Determines if the given type refers to an Oracle database.
     *
     * @param  type
     *
     * @return <code>true</code> if the type is an Oracle database
     */
    public static boolean isOracle(DatabaseType type) {
        return (type instanceof OracleDatabaseType);
    }

    /**
     * Is the database H2?
     *
     * @param  c the connection to the database
     *
     * @return <code>true</code> if the connection is talking to an H2 database
     *
     * @throws Exception
     */
    public static boolean isH2(Connection c) throws Exception {
        DatabaseType type = getDatabaseType(c);
        return isH2(type);
    }

    /**
     * Determines if the given type refers to an H2 database.
     *
     * @param  type
     *
     * @return <code>true</code> if the type is an H2 database
     */
    public static boolean isH2(DatabaseType type) {
        return (type instanceof H2DatabaseType);
    }

    /**
     * Is the database SQL Server?
     *
     * @param  c the connection to the database
     *
     * @return <code>true</code> if the connection is talking to an SQL Server database
     *
     * @throws Exception
     */
    public static boolean isSQLServer(Connection c) throws Exception {
        DatabaseType type = getDatabaseType(c);
        return isSQLServer(type);
    }

    /**
     * Determines if the given type refers to an SQL Server database.
     *
     * @param  type
     *
     * @return <code>true</code> if the type is an SQL Server database
     */
    public static boolean isSQLServer(DatabaseType type) {
        return (type instanceof SQLServerDatabaseType);
    }
}