package hirondelle.web4j.database;

import java.util.*;

import hirondelle.web4j.request.Formats;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.util.Util;

/**
 Utility for producing reports quickly from a <tt>ResultSet</tt>.
 
 <P>Most web applications need to produce reports of summary or transaction information.
 If a Model Object already exists for the given data, then it may be used to fetch and render the data, likely 
 with {@link Db}.
 
 <P>If a Model Object does <em>not</em> already exist for the given report data, then this class may 
 be used to quickly implement the report, without the need to create a full Model Object. 
 
 <P>This class translates each <tt>ResultSet</tt> row into a <tt>Map</tt> of some form. This <tt>Map</tt> is meant not as a 
 robust Model Object, but rather as a rather dumb data carrier, built only for the purpose of reporting.
 The <tt>Map</tt> key is always the column name, and the <tt>Map</tt> value takes various forms, according 
 to how the <tt>ResultSet</tt> is processed :
 <ul>
 <li><tt>formatted</tt> - column values are parsed using 
 {@link ConvertColumn} into <tt>Integer</tt>, <tt>Date</tt>, and so on, and then standard formatting is applied 
 using {@link Formats#objectToText(Object)}.  (This is the recommended style.)
 <li><tt>unformatted</tt> - column values are parsed using {@link ConvertColumn} into  
 <tt>Integer</tt>, <tt>Date</tt>, and so on, but formatting is deferred to the JSP. 
 <li><tt>raw</tt> - column values are all treated as simple text. To allow for various escaping styles in the view, 
 the text is actually returned as {@link SafeText}. By default, the text will be properly escaped for presentation 
 in HTML. If desired, some formatting can be applied directly in the underlying SQL statement itself, using database 
 formatting functions.
 </ul>
 
 <P>Example of using the <tt>Map</tt> in a JSP. Here, column values are assumed to be already formatted, using 
 either <tt>raw</tt> or <tt>formatted</tt>:
<PRE>
{@code
<c:forEach var="row" items="${reportMap}" >
 <tr>
  <td>${row['Name']}</td>  
  <td>${row['Visits']}</td>  
 </tr>
</c:forEach>
  }
</PRE>

 <P>If <tt>unformatted</tt> is used to build the <tt>Map</tt>, then formatting 
 of the resulting objects must be applied in the JSP. 
 
 <h3>Recommended Style</h3>
 The recommended style is to use <tt>formatted</tt>.
  
 <P>If <tt>raw</tt> or <tt>unformatted</tt> is used, then 
 the question usually arises of where to apply formatting: 
 <ul>
 <li>format with database formatting functions - then there will often be much repetition of formatting function calls 
 across different <tt>SELECT</tt>s. 
 <li>format in the JSP (usually with JSTL) - be aware that there is often  
 significant work involved. Not one but three operations can be necessary: a parse operation, 
 a format operation, and possibly a check-for-null.
 </ul>
 
<h3><a name="emptyContent"></a>Empty Values</h3>
 When the <tt>Map</tt> returned by this class has values as text, then any <tt>Strings</tt> which do not satisfy
 {@link Util#textHasContent(String)} are replaced with the 
 return value of {@link hirondelle.web4j.request.Formats#getEmptyOrNullText} (which is in 
 turn configured in <tt>web.xml</tt>). This is a workaround for the fact that most browsers do 
 not render empty <tt>TD</tt> tags very well when the cell has a border. An alternate
 (and likely superior) workaround is to set the 
 <tt><a href="http://www.w3.org/TR/REC-CSS2/tables.html#empty-cells">empty-cells</a></tt> 
 property of Cascading Style Sheets to <tt>'show'</tt>.
*/
public final class Report {
  
  /**
   Return column values without any processing. 
   
   <P>For the returned {@code Map<String, SafeText>} objects, 
   <ul>
   <li>key is the column name
   <li>value is the unprocessed column value, passed to a {@link SafeText}. <tt>SafeText</tt> is 
   used instead of <tt>String</tt> to allow easy escaping of special characters in the view.
   </ul> 
   
   @param aSqlId identifies the underlying <tt>SELECT</tt> statement
   @param aCriteria possible <em>dynamic</em> <tt>WHERE</tt> or <tt>ORDER BY</tt> clause. If no dynamic criteria, then 
   just pass {@link DynamicSql#NONE}. 
   @param aParams parameters for the <tt>SELECT</tt> statement, in the same order as in the underlying <tt>SELECT</tt> statement
  */
  public static List<Map<String, SafeText>> raw(SqlId aSqlId, DynamicSql aCriteria, Object... aParams) throws DAOException {
    List<Map<String,SafeText>> result = new ArrayList<Map<String, SafeText>>();
    ModelBuilder<Map<String, SafeText>> builder = new ReportBuilder();
    SqlFetcher fetcher = getFetcher(aSqlId, aCriteria, aParams);
    fetcher.fetchObjects(builder, result);
    return result;
  }

  /**
   Return column values after processing into formatted building block objects.
   
   <P>For the returned {@code Map<String, SafeText>} objects, 
   <ul>
   <li>key is the column name
   <li>value is the processed column value, as <tt>SafeText</tt>. Column values are first parsed into 
   building block objects using {@link ConvertColumn}. Then the objects are formatted in a 'standard' 
   way using the configured {@link Formats}.
   </ul> 
  
   @param aTargetClasses defines the target class for each column. 
   The order of the classes in the array corresponds one-to-one with the column order of the underlying <tt>ResultSet</tt>. 
   The size of the array matches the number of columns.
   Each class in the array must be supported by the configured {@link ConvertColumn}. 
   @param aLocale <tt>Locale</tt> returned by {@link hirondelle.web4j.request.LocaleSource}
   @param aTimeZone <tt>TimeZone</tt> returned by {@link hirondelle.web4j.request.TimeZoneSource}
   @param aSqlId identifies the underlying <tt>SELECT</tt> statement
   @param aCriteria possible <em>dynamic</em> <tt>WHERE</tt> or <tt>ORDER BY</tt> clause. If no dynamic criteria, then 
   just pass {@link DynamicSql#NONE}. 
   @param aParams parameters for the <tt>SELECT</tt> statement, in the same order as in the underlying <tt>SELECT</tt> statement
  */
  public static List<Map<String, SafeText>> formatted(Class<?>[] aTargetClasses, Locale aLocale, TimeZone aTimeZone, SqlId aSqlId, DynamicSql aCriteria, Object... aParams) throws DAOException {
    List<Map<String, SafeText>> result = new ArrayList<Map<String, SafeText>>();
    //any date columns must be formatted in a Locale-sensitive manner
    Formats formats = new Formats(aLocale, aTimeZone);
    ModelBuilder<Map<String, SafeText>> builder = new ReportBuilder(aTargetClasses, formats); 
    SqlFetcher fetcher = getFetcher(aSqlId, aCriteria, aParams);
    fetcher.fetchObjects(builder, result);
    return result;
  }
  
  /**
   Return column values as unformatted building block objects.
   
   <P>For the returned {@code Map<String, Object>} objects, 
   <ul>
   <li>key is the column name
   <li>value is the processed column value, parsed into a building block Object using {@link ConvertColumn}. 
   </ul> 
  
   @param aTargetClasses defines the target class for each column. 
   The order of the classes in the array corresponds one-to-one with the column order of the underlying <tt>ResultSet</tt>. 
   The size of the array matches the number of columns.
   Each class in the array must be supported by the configured {@link ConvertColumn}. 
   @param aSqlId identifies the underlying <tt>SELECT</tt> statement
   @param aCriteria possible <em>dynamic</em> <tt>WHERE</tt> or <tt>ORDER BY</tt> clause. If no dynamic criteria, then 
   just pass {@link DynamicSql#NONE}. 
   @param aParams parameters for the <tt>SELECT</tt> statement, in the same order as in the underlying <tt>SELECT</tt> statement
  */
  public static List<Map<String, Object>> unformatted(Class<?>[] aTargetClasses, SqlId aSqlId, DynamicSql aCriteria, Object... aParams) throws DAOException {
    List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
    ModelBuilder<Map<String, Object>> builder = new ReportBuilderUnformatted(aTargetClasses); 
    SqlFetcher fetcher = getFetcher(aSqlId, aCriteria, aParams);
    fetcher.fetchObjects(builder, result);
    return result;
  }
  
  // PRIVATE //
  
  private Report(){
    //empty - prevent construction by caller
  }
  
  private static SqlFetcher getFetcher(SqlId aSqlId, DynamicSql aCriteria, Object[] aParams) throws DAOException {
    SqlFetcher result = null;
    if( DynamicSql.NONE == aCriteria ) {
      result = SqlFetcher.forSingleOp(aSqlId, aParams);
    }
    else {
      result = SqlFetcher.forSearch(aSqlId, aCriteria, aParams);
    }
    return result;
  }
}
