package hirondelle.web4j.database;

import java.util.*;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.ResultSetMetaData;

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.request.Formats;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.util.Args;

/**
 Translates a <tt>ResultSet</tt> row into a <tt>Map</tt>.
  
 <P>The returned {@code Map<String, SafeText>} has : 
 <ul>
 <li>key - column name.
 <li>value - column value as {@link SafeText}. The column values may be formatted 
 using  {@link Formats#objectToTextForReport(Object)}, according to which constructor 
 is called.
 </ul>
 
 <P><tt>SafeText</tt> is used to protect the caller against unescaped special characters, 
 and Cross Site Scripting attacks.
*/
final class ReportBuilder extends ModelBuilder<Map<String, SafeText>> {
  
  /**
   Builds a <tt>Map</tt> whose values are simply the <em>raw, unprocessed text</em> of the
   underlying <tt>ResultSet</tt>. These values are placed into {@link SafeText}
   objects, to allow for various styles of escaping special characters when presenting 
   the data in the view.
  
   <P>Database formatting functions can be used in the underlying SELECT statement
   to format values to the desired form.
  */
  ReportBuilder(){
    /* 
     Implementation Note
     This is a relatively rare instance where is it makes sense to
     declare an empty constructor.
    */
  }

  /**
   Constructor for applying some processing to column values, instead of using the raw text.
  
   <P><tt>aColumnTypes</tt> is used to convert each column into an <tt>Object</tt>.
   The supported types are the same as for {@link ConvertColumn}.
   
   <P>After conversion into the desired class, 
   {@link hirondelle.web4j.request.Formats#objectToTextForReport(Object)} is used to apply 
   formats to the various objects.
    
   @param aColumnTypes defines the type of each column, in order from left to right, as 
   they appear in the <tt>ResultSet</tt>; contains <tt>N</tt> class literals, 
   where <tt>N</tt> is the number of columns in the <tt>ResultSet</tt>; contains only 
   the classes supported by {@link ConvertColumn}; this constructor will create a defensive 
   copy of this parameter. 
   @param aFormats used to apply the 
   formats specified in <tt>web.xml</tt> and 
   {@link hirondelle.web4j.request.Formats#objectToTextForReport(Object)}, 
   localized using {@link hirondelle.web4j.request.LocaleSource}.
  */
  ReportBuilder(Class<?>[] aColumnTypes, Formats aFormats){
    Args.checkForPositive(aColumnTypes.length);
    fColumnTypes = defensiveCopy(aColumnTypes);
    fFormats = aFormats;
    fColumnToObject = BuildImpl.forConvertColumn();
  }
  
  /**
   Returns an unmodifiable <tt>Map</tt>.
   
   <P>See class description.
  */
  Map<String, SafeText> build(ResultSet aRow) throws SQLException {
    LinkedHashMap<String, SafeText> result = new LinkedHashMap<String, SafeText>();
    SafeText value = null;
    ResultSetMetaData metaData = aRow.getMetaData();
    for (int columnIdx = 1; columnIdx <= metaData.getColumnCount(); ++columnIdx) {
      if ( doNotTranslateToObjects() ) {
        value = getSafeText(aRow, columnIdx);
        //fLogger.finest("Raw unformatted text value: " + value.getRawString());
      }
      else {
        if ( fColumnTypes.length != metaData.getColumnCount() ) {
          throw new IllegalArgumentException(
            "Class[] has different number of elements than ResultSet: " + fColumnTypes.length + 
            " versus " + metaData.getColumnCount () + ", respectively."
          );
        }
        value = getFormattedObject(aRow, columnIdx);
        //security risk to log data: fLogger.fine("Formatted object: " + value);
      }
      result.put(metaData.getColumnName(columnIdx), value);
    }
    return Collections.unmodifiableMap(result);
  }
  
  // PRIVATE //

  /** The types to which ResultSet columns will be converted.  */
  private Class<?>[] fColumnTypes; 
  
  /** Applies formatting to objects created from ResultSet columns.  */
  private Formats fFormats;
  
  /** Translates ResultSet columns into desired Objects. */
  private ConvertColumn fColumnToObject;
  
  private Class[] defensiveCopy(Class[] aClassArray){
    Class[] result = new Class[aClassArray.length];
    System.arraycopy(aClassArray, 0, result, 0, aClassArray.length);
    return result;
  }
  
  private boolean doNotTranslateToObjects() {
    return fColumnTypes == null;
  }

  private SafeText getSafeText(ResultSet aRow, int aColumn) throws SQLException {
    SafeText result = null;
    String text = aRow.getString(aColumn);
    if ( text == null ) {
      result = new SafeText(Formats.getEmptyOrNullText()); 
    }
    else {
      result = new SafeText(text);
    }
    return result;
  }

  private SafeText getFormattedObject(ResultSet aRow, int aColumnIdx) throws SQLException {
    Object translatedObject = getUnformattedObject(aRow, aColumnIdx);
    return fFormats.objectToTextForReport(translatedObject);
  }

  private Object getUnformattedObject(ResultSet aRow, int aColumnIdx) throws SQLException {
    return fColumnToObject.convert(aRow, aColumnIdx, fColumnTypes[aColumnIdx-1]);
  }
}
