package hirondelle.web4j.database;

import static hirondelle.web4j.util.Consts.FAILURE;
import static hirondelle.web4j.util.Consts.NEW_LINE;
import static hirondelle.web4j.util.Consts.SUCCESS;
import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.util.Util;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.logging.Logger;

/**
 (UNPUBLISHED) Initialize the data layer and return related configuration information.

 <P>Acts as the single source of configuration information needed by the 
 data layer. The intent is to centralize the dependencies of the data 
 layer on its environment. For example, this is the only class in this package 
 which knows about <tt>ServletConfig</tt>.

 <P>This class carries simple static, immutable data, populated upon startup.
 It is safe to use this class in a multi-threaded environment.

 <P>In addition to reading in <tt>web.xml</tt> settings, this class :
<ul>
 <li>initializes connection sources
 <li>logs the name and version of both the database and the database driver
 <li>logs the support for transaction isolation levels (see {@link TxIsolationLevel})
 <li>reads in the <tt>*.sql</tt> file(s) (see package summary for more information)
</ul>

 <P>See <tt>web.xml</tt> for more information on the items encapsulated by this class.
 
 @un.published
*/
public final class DbConfig {
  
  /**
   Configure the data layer. Called upon startup. 
   
   <P> If <tt>aIndicator</tt> is <tt>YES</tt>, then 
   hard-coded test settings will be used. (This is intended solely as a developer 
   convenience for testing database connectivity. If called outside of a web 
   container, then <tt>aConfig</tt> must be <tt>null</tt>.)
   
   @return the names of all databases that were seen to be <em>up</em>.
  */
  public static Set<String> initDataLayer() throws DAOException {
    Set<String> result = new LinkedHashSet<String>();    
    //there are order dependencies here!!
    ConnectionSource connSrc = BuildImpl.forConnectionSource();
    for (String dbName : connSrc.getDatabaseNames()){
      boolean healthy = checkHealthOf(dbName);
      if(healthy) {
        result.add(dbName);
      }
    }
    
    int numDbs = connSrc.getDatabaseNames().size();    
    if((numDbs > 0) && (result.size() == numDbs)) {
      fLogger.config("*** SUCCESS : ALL DATABASES DETECTED OK! *** Num databases: " + numDbs);
    }
    
    SqlStatement.readSqlFile();
    return result;
  }
  
  /** 
   Check the health of the given database connection. For healthy connections, log 
   basic info as a side-effect. Return <tt>true</tt> only if healthy.
  */
  public static boolean checkHealthOf(String dbName) throws DAOException {
    boolean result = logDatabaseAndDriverNames(dbName);
    if(result) {
      fLogger.config("Success : Database named " + Util.quote(dbName) + " detected OK.");
      queryTxIsolationLevelSupport(dbName);
    }
    else {
      fLogger.severe("Cannot connect to database named " + Util.quote(dbName) + ". Is database running?");
    }
    return result;
  }
  
  // PRIVATE 
  
  private static final Logger fLogger = Util.getLogger(DbConfig.class);
  
  private DbConfig(){
    //empty - prevent construction by the caller
  }
  
  /** 
   Log name and version info, for both the database and driver.
   
   <P>This is the first place where the database is exercised. Returns false only if an error occurs. 
   This indicates to the caller that db init tasks have not completed normally.
   The caller must check the return value.
  */
  private static boolean logDatabaseAndDriverNames(String aDbName) throws DAOException {
    boolean result = SUCCESS;
    Connection connection = null;
    try { 
      connection = BuildImpl.forConnectionSource().getConnection(aDbName);
      DatabaseMetaData db = connection.getMetaData();
      String dbName = db.getDatabaseProductName() + "/" + db.getDatabaseProductVersion();
      String dbDriverName = db.getDriverName() + "/" + db.getDriverVersion();
      String message = NEW_LINE;
      message = message + "  Database Id passed to ConnectionSource: " + aDbName + NEW_LINE; 
      message = message + "  Database name: " + dbName + NEW_LINE;
      message = message + "  Database driver name: " + dbDriverName + NEW_LINE;
      message = message + "  Database URL: " + db.getURL() + NEW_LINE;
      boolean supportsScrollable = db.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
      message = message + "  Supports scrollable ResultSets (TYPE_SCROLL_INSENSITIVE, CONCUR_READ_ONLY): " + supportsScrollable;
      if ( ! supportsScrollable ){
        fLogger.severe("Database/driver " + aDbName + " does not support scrollable ResultSets. Parsing of ResultSets by ModelFromRow into 'Parent+List<Children>' kinds of objects will not work, since it depends on scrollable ResultSets. Parsing into 'ordinary' Model Objects will still work, however, since they do not depend on scrollable ResultSets.)");
      }
      fLogger.config(message);
    }
    catch (Throwable ex) {
      result = FAILURE;
    }
    finally {
      DbUtil.close(connection);
    }
    return result;
  }

  /** For each {@link TxIsolationLevel}, log if it is supported (true/false).   */  
  private static void queryTxIsolationLevelSupport(String aDbName) throws DAOException {
    Connection connection = BuildImpl.forConnectionSource().getConnection(aDbName);
    try { 
      int defaultTxLevel = connection.getMetaData().getDefaultTransactionIsolation();
      DatabaseMetaData db = connection.getMetaData();
      for(TxIsolationLevel level: TxIsolationLevel.values()){
        fLogger.config(getTxIsolationLevelMessage(db, level, defaultTxLevel)  );
      }
    }
    catch (SQLException ex) {
      throw new DAOException("Cannot query database for transaction level support", ex);
    }
    finally {
      DbUtil.close(connection);
    }
  }

  /** Create a message describing support for aTxIsolationLevel.  */
  private static String getTxIsolationLevelMessage (DatabaseMetaData aDb, TxIsolationLevel aLevel, int aDefaultTxLevel)  {
    StringBuilder result = new StringBuilder();
    result.append("Supports Tx Isolation Level " + aLevel.toString() + ": ");
    try { 
      boolean supportsLevel = aDb.supportsTransactionIsolationLevel(aLevel.getInt());
      result.append(supportsLevel);
    }
    catch(SQLException ex){
      fLogger.warning("Database driver doesn't support calla to supportsTransactionalIsolationLevel(int).");      
      result.append( "Unknown");
    }
    if ( aLevel.getInt() == aDefaultTxLevel ) {
      result.append(" (default)");
    }
    return result.toString();
  }
}
