package hirondelle.web4j.ui.tag;

import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.logging.Logger;

import hirondelle.web4j.database.SqlId;
import hirondelle.web4j.Controller;
import hirondelle.web4j.util.Consts;
import hirondelle.web4j.util.Regex;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.EscapeChars;

/**
 Generate links for paging through a long list of records.
 
 <P>With this class, paging is implemented in two steps :
 <ul>
   <li>emitting the proper links (with the proper request parameters) to identify each page 
   <li>extracting the data from the database, using the given request parameters, and perhaps subsetting the <tt>ResultSet</tt>
  </ul>
  
 The first step is performed by this tag, but the second step is not (see below). 
 For an example of paging, please see the 'Discussion' feature of the <i>Fish &amp; Chips Club</i> application.
 
 <h3>Emitting Links For Paging</h3>
 <P>This tag works by emitting various links which <i>are modifications of the current URI</i>.
 
 <P><b>Example</b>
 <PRE>
 &lt;w:pager pageFrom="PageIndex"&gt; 
  &lt;a href="placeholder_first_link"&gt;First&lt;/a&gt; | 
  &lt;a href="placeholder_next_link"&gt;Next&lt;/a&gt; | 
  &lt;a href="placeholder_previous_link"&gt;Previous&lt;/a&gt; | 
 &lt;/w:pager&gt;
 </PRE>
 
 <P>The <tt>placeholder_xxx</tt> items act as placeholders. 
 <em>They are replaced by this tag with modified values of the current URI.</em> 
 The &lt;w:pager&gt; tag has a single <tt>pageFrom</tt> attribute.
 It tells this tag which request parameter it will need to modify, in order to generate the various links. 
 This modified request parameter represents the page index, in the range <tt>1..9999</tt> : for example, <tt>PageIndex=1</tt>, 
 <tt>PageIndex=2</tt>, and so on.
 
 <P>For example, if the following URI displays "page 5" :
 <PRE>http://www.blah.com/main/SomeAction.do?PageIndex=5&Criteria=Blah</PRE>
 then this tag will let you emit the following links, derived from the above URI simply by replacing the value of the <tt>PageIndex</tt> request parameter :
 <PRE>http://www.blah.com/main/SomeAction.do?PageIndex=1&Criteria=Blah
http://www.blah.com/main/SomeAction.do?PageIndex=6&Criteria=Blah
http://www.blah.com/main/SomeAction.do?PageIndex=4&Criteria=Blah</PRE>
 
  <P>Of course, these generated links don't do the actual subsetting of the data. 
  Rather, these links simply <i>alter the current URI to use the desired value for the page index request parameter.</i> 
  The resulting request parameters are then used elsewhere to extract the desired data (as described below).
  
 <P><b>Link Suppression</b>
 <P>If the emission of a link would create a 'link to self', then this tag will not emit the link. 
 For example, if you are already on the first page, then the First and Previous links will not be emitted as links - only the bare text, without the link, is emitted.  
   
 
<h3>Extracting and Subsetting the Data</h3>
 There are three alternatives to implementing the subsetting of the <tt>ResultSet</tt> : 
 <ul>
   <li>in the JSP itself
   <li>in the Data Access Object (DAO)
   <li>directly in the underlying <tt>SELECT</tt> statement
 </ul>
 
 <P><b>Subsetting in the JSP</b><br>
<ul>
 <li>this is the simplest style
 <li>it can be applied quickly, to add paging to any existing listing
 <li>the DAO performs a single, unchanging <tt>SELECT</tt> that always returns the <i>same</i> set of records for all pages
 <li>it requires only a single change to the {@link hirondelle.web4j.action.Action} - the addition of a 
 <tt>public static final</tt> {@link hirondelle.web4j.request.RequestParameter} field for the page index parameter
 <li>the JSP performs the subsetting to display a single page at a time, simply using <tt>&lt;c:out&gt;</tt>
</ul>

 The JSP performs the subsetting with simple JSTL.
 In the following example, the page size is hard-coded to 25 items per page.
 The <tt>&lt;c:out&gt;</tt> tag has <tt>begin</tt> and <tt>end</tt> attributes which can control the range of rendered items : 
<PRE>
&lt;c:forEach 
  var="item" 
  items="${items}" 
  begin="${25 * (param.PageIndex - 1)}" 
  end="${25 * param.PageIndex - 1}"
&gt;
  ...display each item...
&lt;/c:forEach&gt;
</PRE>

Note the different expressions for begin and end :
<PRE>begin = 25 * (PageIndex - 1)
end = (25 * PageIndex) - 1
</PRE>

which give the following values :
<P>
<table border='1' cellspacing='0' cellpadding='3'>
 <tr>
  <th>Page</th>
  <th>Begin</th>
  <th>End</th>
 </tr>
 <tr>
  <td>1</td>
  <td>0</td>
  <td>24</td>
 </tr>
 <tr>
  <td>2</td>
  <td>25</td>
  <td>49</td>
 </tr>
 <tr>
  <td>...</td>
  <td>...</td>
  <td>...</td>
 </tr>
</table>

<P>An alternate variation would be to allow the page <i>size</i> to also come from a request parameter :
<PRE>
&lt;c:forEach 
  var="item" 
  items="${items}" 
  begin="${param.PageSize * (param.PageIndex - 1)}" 
  end="${param.PageSize * param.PageIndex - 1}"
&gt;
  ...display each line...
&lt;/c:forEach&gt;
</PRE>


  <P><b>Subsetting in the DAO</b><br>
<ul>
 <li>the full <tt>ResultSet</tt> is still returned by the <tt>SELECT</tt>, as in the JSP case above 
 <li>however, this time the DAO performs the subsetting, using 
 {@link hirondelle.web4j.database.Db#listRange(Class, SqlId, Integer, Integer, Object[])}. This avoids  
 the cost of unnecessarily parsing records into Model Objects.
 <li>the <tt>&lt;c:out&gt;</tt> tag does not use any <tt>begin</tt> and <tt>end</tt> attributes, since the subsetting 
 has already been done.
</ul>
 
 <P><b>Subsetting in the SELECT</b><br>
<ul>
 <li>the underlying <tt>SELECT</tt> is constructed to return only the desired records. (Such a <tt>SELECT</tt>
 may be difficult to construct, according to the capabilities of the target database.)
 <li>the <tt>SELECT</tt> will likely need two request parameters to form the proper query: a page index and
 a page size, from which <tt>start</tt> and <tt>end</tt> indices may be calculated 
 <li>the DAO and JSP are implemented as in any regular listing 
</ul>
 
<P>Here are the kinds of calculations you may need when constructing such a SELECT statement   
<P>For items enumerated with a 0-based index :
 <PRE>
 start_index = page_size * (page_index - 1)
 end_index = page_size * page_index - 1
 </PRE>
 
 <P>For items enumerated with a 1-based index : 
 <PRE>
 start_index = page_size * (page_index - 1) + 1
 end_index = page_size * page_index
 </PRE>
 
 <h3>Setting In <tt>web.xml</tt></h3>
 Note that there is a <tt>MaxRows</tt> setting in <tt>web.xml</tt> which controls the maximum number of records returned by 
 a <tt>SELECT</tt>. 
*/
public final class Pager extends TagHelper {

  /**
   Name of request parameter that holds the index of the current page. 
   
   <P>This request parameter takes values in the range <tt>1..9999</tt>. 
   
    <P>(The name of this method is confusing. It should rather be named <tt>setPageIndex</tt>.)
  */
  public void setPageFrom(String aPageIndexParamName) {
    checkForContent("PageFrom", aPageIndexParamName);
    fPageIndexParamName = aPageIndexParamName.trim();
    fPageIdxPattern = Pattern.compile(fPageIndexParamName + "=(?:\\d){1,4}");
  }

  /** See class comment. */
  @Override protected String getEmittedText(String aOriginalBody) {
    fCurrentPageIdx = getNumericParamValue(fPageIndexParamName);
    fCurrentURI = getCurrentURI();
    fLogger.fine("Current URI: " + fCurrentURI);
    fLogger.fine("Current Page : " + fCurrentPageIdx);
    String firstURI = getFirstURI();
    String nextURI = getNextURI();
    String previousURI = getPreviousURI();
    String result = getBodyWithUpdatedPlaceholders(aOriginalBody, firstURI, nextURI, previousURI);
    return result;
  }

  // PRIVATE //

  private String fPageIndexParamName;
  private Pattern fPageIdxPattern;
  private String fCurrentURI;
  private int fCurrentPageIdx;
  
  private static final String NO_LINK = Consts.EMPTY_STRING;
  private static final int FIRST_PAGE = 1;
  private static final String PLACEHOLDER_FIRST_LINK = "placeholder_first_link";
  private static final String PLACEHOLDER_NEXT_LINK = "placeholder_next_link";
  private static final String PLACEHOLDER_PREVIOUS_LINK = "placeholder_previous_link";
  /** Regex for an anchor tag 'A'. */
  private static final Pattern LINK_PATTERN = Pattern.compile(Regex.LINK, Pattern.CASE_INSENSITIVE);
  private static final Logger fLogger = Util.getLogger(Pager.class);

  private String getCurrentURI() {
    return (String)getRequest().getAttribute(Controller.CURRENT_URI);
  }

  private boolean isFirstPage() {
    return fCurrentPageIdx == FIRST_PAGE;
  }
  
  private int getNumericParamValue(String aParamName) {
    String value = getRequest().getParameter(aParamName);
    Integer result = Integer.valueOf(value);
    if (result < 1) { 
      throw new IllegalArgumentException("Param named " + Util.quote(aParamName) + " must be >= 1. Value: " + Util.quote(result) + ". Page Name: " + getPageName()); 
    }
    return result.intValue();
  }

  private String getFirstURI() {
    return isFirstPage() ? NO_LINK : forPage(FIRST_PAGE); 
  }

  private String getNextURI() {
    return  forPage(fCurrentPageIdx + 1); 
  }
  
  private String getPreviousURI() {
    return isFirstPage() ? NO_LINK : forPage(fCurrentPageIdx - 1); 
  }
  
  private String forPage(int aNewPageIndex){
    String result = null;
    StringBuffer uri = new StringBuffer();
    Matcher matcher = fPageIdxPattern.matcher(fCurrentURI);
    while ( matcher.find() ){
      matcher.appendReplacement(uri, getReplacement(aNewPageIndex));
    }
    matcher.appendTail(uri);
    result = getResponse().encodeURL(uri.toString());
    return EscapeChars.forHTML(result);
  }
  
  private String getReplacement(int aNewPageIdx){
    return EscapeChars.forReplacementString(fPageIndexParamName + "=" + aNewPageIdx);
  }
  
  private String getBodyWithUpdatedPlaceholders(String aOriginalBody, String aFirstURI, String aNextURI, String aPreviousURI){
    fLogger.fine("First URI: " + aFirstURI);
    fLogger.fine("Next URI: " + aNextURI);
    fLogger.fine("Previous URI: " + aPreviousURI);
    
    StringBuffer result = new StringBuffer();
    Matcher matcher = LINK_PATTERN.matcher(aOriginalBody);
    while ( matcher.find() ){
      fLogger.finest("Getting href as first group.");
      String href = matcher.group(Regex.FIRST_GROUP);
      String replacement = null;
      if ( PLACEHOLDER_FIRST_LINK.equals(href) ){
        replacement = aFirstURI;
      }
      else if ( PLACEHOLDER_NEXT_LINK.equals(href) ){
        replacement = aNextURI;
      }
      else if ( PLACEHOLDER_PREVIOUS_LINK.equals(href) ){
        replacement = aPreviousURI;
      }
      else {
        String message = "Body of pager tag can only contain links to special placeholder text. Page Name "+ getPageName();
        fLogger.severe(message);
        throw new IllegalArgumentException(message);
      }
      matcher.appendReplacement(result, getReplacementHref(matcher, replacement));
    }
    matcher.appendTail(result);
    return result.toString();
  }
  
  private String getReplacementHref(Matcher aMatcher, String aReplacementHref){
    String result = null;
    int LINK_BODY = 3;
    int ATTRS_AFTER_HREF = 2;
    if ( Util.textHasContent(aReplacementHref) ){
      result = "<A HREF=\"" + aReplacementHref + "\" " + aMatcher.group(ATTRS_AFTER_HREF)+" >" + aMatcher.group(LINK_BODY) + "</A>";
    }
    else {
      result = aMatcher.group(LINK_BODY);
    }
    return EscapeChars.forReplacementString(result);
  }
}
