package hirondelle.web4j.database;

import java.util.*;
import java.util.logging.*;
import java.sql.Connection;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.Args;

/**
 Perform the simplest kinds of transactions.
 
 <P>Executes a number of SQL statements, in the same order as passed to the constructor. 
 Each SQL statement is executed once and only once, without any iteration.
 Each SQL statement must perform an <tt>INSERT</tt>, <tt>DELETE</tt>, or <tt>UPDATE</tt> operation.
 No <tt>SELECT</tt>s are allowed.
 
 <P><span class="highlight">It is likely that a significant fraction (perhaps as high as 50%) of all 
 transactions can be implemented with this class. </span>
 
 <P>Example use case:
 <PRE>
 SqlId[] sqlIds = {ADD_THIS, DELETE_THAT};
 Object[] params = {blah.getX(), blah.getY(), blah.getZ(), blah.getQ()}; 
 Tx doStuff = new TxSimple(sqlIds, params);
 int numRecordsAffected = doStuff.executeTx();
 </PRE>
 
 <P>In this example, <tt>blah</tt> will usually represent a Model Object. The data in <tt>params</tt>
 is divided up among the various <tt>sqlIds</tt>. (This division is performed internally by this class.) 
 
 <P>This class uses strong ordering conventions. <span class="highlight">The order of 
 items in each array passed to the constructor is very important</span> (see {@link #TxSimple(SqlId[], Object[])}).
*/
public final class TxSimple implements Tx {
  
  /**
   Constructor.
  
   <P><tt>aAllParams</tt> includes the parameters for all operations, concatenated in a single array. Has at least 
   one member. 
   
   <P>In general, successive pieces of <tt>aAllParams</tt> are used to populate each corresponding 
   statement, in their order of execution. The order of items is here <em>doubly</em> important: the 
   first N parameters are used against the first SQL statement, the next M parameters are 
   used against the second SQL statement, and so on. (This breakdown is deduced internally.)
    
   <P>And as usual, within each subset of <tt>aAllParams</tt> 
   corresponding to an SQL statement, the order of parameters matches the order 
   of <tt>'?'</tt> placeholders appearing in the underlying SQL statement.  
   
   @param aSqlIds identifiers for the operations to be performed, <em>in the order of their intended execution</em>. Has 
   at least one member. All of the operations are against the same database.
   @param aAllParams the parameters for all operations, concatenated in a single array. Has at least 
   one member.  (See above for important requirements on their order.) 
  */
  public TxSimple(SqlId[] aSqlIds, Object[] aAllParams){
    Args.checkForPositive(aSqlIds.length);
    Args.checkForPositive(aAllParams.length);
    
    fSqlIds = aSqlIds;
    fParams = aAllParams;
    fParamsForStatement = new LinkedHashMap<SqlId, Object[]>();
    divideUpParams();
  }
  
  public int executeTx() throws DAOException {
    Tx transaction = new SimpleTransaction(getDbName());
    return transaction.executeTx();
  }

  // PRIVATE //
  private SqlId[] fSqlIds;
  private Object[] fParams;
  private Map<SqlId, Object[]> fParamsForStatement;
  private static final Object[] EMPTY = new Object[0];
  private static final Logger fLogger = Util.getLogger(TxSimple.class);
  
  private final class SimpleTransaction extends TxTemplate {
    SimpleTransaction(String aDbName){
      super(aDbName);
    }
    public int executeMultipleSqls(Connection aConnection) throws DAOException {
      int result = 0;
      for (SqlId sqlId : fSqlIds){
        Object[] params = fParamsForStatement.get(sqlId);
        SqlEditor editor = null;
        if( params.length == 0 ){
          editor = SqlEditor.forTx(sqlId, aConnection);
        }
        else {
          editor = SqlEditor.forTx(sqlId, aConnection, params);
        }
        result = result + editor.editDatabase();
      }
      return result;
    }
  }
  
  private String getDbName() {
    return fSqlIds[0].getDatabaseName();
  }
  
  private void divideUpParams(){
    int startWith = 0;
    int totalNumExpectedParams = 0;
    for (SqlId sqlId : fSqlIds){
      int numExpectedParams = SqlStatement.getNumParameters(sqlId);
      totalNumExpectedParams = totalNumExpectedParams + numExpectedParams;
      chunkParams(startWith, numExpectedParams, sqlId);
      startWith = startWith + numExpectedParams;
    }
    if ( totalNumExpectedParams != fParams.length ){
      throw new IllegalArgumentException(
        "Size mismatch. Number of parameters passed as data: " + fParams.length + 
        ".  Number of '?' items appearing in underlying SQL statements: " + totalNumExpectedParams
      );
    }
    fLogger.finest("Chunked params " + Util.logOnePerLine(fParamsForStatement));
  }
  
  private void chunkParams(int aStartWithIdx, int aNumExpectedParams, SqlId aSqlId){
    int endWithIdx = aStartWithIdx + aNumExpectedParams - 1;
    int maxIdx = fParams.length - 1;
    fLogger.fine("Chunking params for " + aSqlId +  ". Start with index " + aStartWithIdx + ", end with index " + endWithIdx);
    if( aStartWithIdx > maxIdx ){
      throw new IllegalArgumentException("Error chunking parameter data. Starting-index (" + aStartWithIdx + ") greater than expected maximum (" + maxIdx + "). Mismatch between number of params passed as data, and number of params expected by underlying SQL statements.");
    }
    if ( endWithIdx > maxIdx ){
      throw new IllegalArgumentException("Error chunking parameter data. Ending-index (" + endWithIdx + ") greater than expected maximum (" + maxIdx + "). Mismatch between number of params passed as data, and number of params expected by underlying SQL statements.");
    }
    
    if( aNumExpectedParams == 0 ){
      fParamsForStatement.put(aSqlId, EMPTY);
    }
    else {
      List list = Arrays.asList(fParams);
      List sublist = list.subList(aStartWithIdx, endWithIdx + 1);
      fParamsForStatement.put(aSqlId, sublist.toArray(EMPTY));
    }
  }
}
