package hirondelle.web4j.ui.tag;

import java.util.*;
import java.util.logging.*;
import javax.servlet.jsp.JspException;
import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.action.Operation;
import hirondelle.web4j.request.Formats;
import hirondelle.web4j.util.Util;
import static hirondelle.web4j.util.Consts.EMPTY_STRING;

/**
 Custom tag which populates form controls in a simple, elegant way.
 
 <P>From the point of view of this tag, there are 3 sources of data for a form control: 
 <ul>
   <li>the HTML defined in your JSP can define an initial default value
   <li>Request parameter values
  <li>a Model Object
 </ul>
 
 <P>For reference, here is the logic that defines which data source is used, and related 
 naming conventions :
<PRE>
if a Model Object of the given name is in any scope {
  override the default HTML for each control
  use the Model Object 
  (match control names to getXXX methods of the Model Object)
}
else if the request is a POST  {
  override the default HTML for each control
  must populate <i>every</i> control using request parameter values
  (match control names to request param names) 
}
else if the request is a GET {
  if control name has a matching req param name {
    override the default HTML for each control
    populate control using request parameter values
    (match control names to request param names) 
  }
  else {
    use the default HTML for that control
  }
}
</PRE>
 
 <P><span class='highlight'>This tag simply wraps static HTML forms</span>. 
 This is very economical since it does not force the page author to completely
 replace well-known static HTML with a large set of custom tags. 

<h3>Example use case</h3>
 This use case corresponds to either an 'add' or a 'change' of a Model Object. The <tt>using</tt> 
 attribute signifies that a 'change' case is possible. (This example works with 
 an {@link hirondelle.web4j.action.ActionTemplateListAndEdit} action.)
    
<PRE>
&lt;c:url value="RestoAction.do" var="baseURL"/&gt;
&lt;form action='${baseURL}' method="post" class="user-input"&gt; 
<b>&lt;w:populate using="itemForEdit"&gt;</b>  
&lt;input name="Id" type="hidden"&gt;
&lt;table align="center"&gt;
&lt;tr&gt;
 &lt;td&gt;&lt;label&gt;Name&lt;/label&gt; *&lt;/td&gt;
 &lt;td&gt;&lt;input name="Name" type="text"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
 &lt;td&gt;&lt;label&gt;Location&lt;/label&gt;&lt;/td&gt;
 &lt;td&gt;&lt;input name="Location" type="text"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
 &lt;td&gt;&lt;label&gt;Price&lt;/label&gt;&lt;/td&gt;
 &lt;td&gt;&lt;input name="Price" type="text"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
 &lt;td&gt;&lt;label&gt;Comment&lt;/label&gt;&lt;/td&gt;
 &lt;td&gt;&lt;input name="Comment" type="text"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
 &lt;td align="center" colspan=2&gt;
  &lt;input type='submit' value="Edit"&gt;
 &lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
<b>&lt;/w:populate&gt;</b>
 &lt;tags:hiddenOperationParam/&gt;
&lt;/form&gt;
</PRE>
    
 Here, the <tt>itemForEdit</tt> Model Object has the following methods, corresponding to 
 the above populated controls :
 <PRE>
  public Id getId() {...}
  public SafeText getName() {...}
  public SafeText getLocation() {...}
  public BigDecimal getPrice() {...}
  public SafeText getComment() {...}
</PRE>
 
 <h3>Example without <tt>using</tt> attribute</h3>
 No <tt>using</tt> attribute is specified when :
 <ul>
 <li>only an 'add' operation is performed, and not a 'change' operation.
 <li>or, only a <tt>Search</tt> {@link Operation} is performed. In this case, a form with <tt>method="GET"</tt> is used 
 to specify parameters to a <tt>SELECT</tt> statement.
 </ul>
 
 <P>Here is an example of a form used only for 'add' operations :
<PRE>
<b>&lt;w:populate&gt;</b>
&lt;c:url value="AddMessageAction.do?Operation=Apply" var="baseURL"/&gt; 
&lt;form action='${baseURL}' method=post class="user-input"&gt;
&lt;table align="center"&gt;
&lt;tr&gt;
 &lt;td&gt;
  &lt;label&gt;Message&lt;/label&gt; *
 &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
 &lt;td&gt;
  &lt;textarea name="Message Body"&gt;
  &lt;/textarea&gt;
 &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
 &lt;td colspan=2&gt;
  &lt;label&gt;Preview First ?&lt;/label&gt; &lt;input type="radio" name="Preview" value="true"&gt; Yes
 &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
 &lt;td align="center" colspan=2&gt;
  &lt;input type="submit" value="Add Message"&gt; 
 &lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/form&gt;
<b>&lt;/w:populate&gt;</b>
 </PRE>

<h3>Supported Controls</h3>
 <P>The following form input items are called <em>supported controls</em> here, and 
 include all items which undergo population by this class :
<ul>
 <li><tt>INPUT</tt> tags with type=<tt>text</tt>, <tt>password</tt>, <tt>radio</tt>, 
 <tt>checkbox</tt>, <tt>hidden</tt>
 <li>HTML5 input tags with type=<tt>search</tt>, <tt>email</tt>, <tt>url</tt>, <tt>number</tt>, <tt>tel</tt>, <tt>color</tt>, <tt>range</tt>
 <li><tt>SELECT</tt> tags
 <li><tt>TEXTAREA</tt> tags
</ul>

 <P>Population is implemented by editing these supported control attributes :
<ul>
 <li>the <tt>checked</tt> attribute for INPUT tags of type <tt>radio</tt> 
 and <tt>checkbox</tt>
 <li>the <tt>value</tt> attribute for the remaining INPUT tags (of the different types listed above) 
 <li>the <tt>selected</tt> attribute for OPTION tags appearing in a SELECT
 <li>the body of a TEXTAREA tag
</ul>

<P>The body of this tag is HTML, with the following minor restrictions:
<ul>
 <li>all supported controls must include a <tt>name</tt> attribute
 <li>all supported INPUT controls must include a <tt>type</tt> attribute 
 <li>all attributes must be quoted, using either single or double quotes. For example, 
 <tt>&lt;input type='text' ... &gt;</tt> is allowed but 
 <tt>&lt;input type=text ... &gt;</tt> is not
 <li> for SELECT tags, the &lt;/option&gt; end tag is not optional, and must be included.
 <li>INPUT tags with <tt>type='email'</tt> are treated as always being single-valued
 <li>the repetitive form of <tt>selected='selected'</tt> can't be used
</ul>

HTML often allows alternate ways of expressing the exact same thing.
For example, the <tt>selected</tt> attribute can be expressed as <tt>selected='selected'</tt>, or simply as 
the single word <tt>selected</tt> - this tag only accepts the second form, not the first.
These sorts of restrictions are a nuisance, and result from the way the framework parses HTML internally.
Sometimes you will need to tweak your form hypertext in order to let this tag parse the form correctly. Sorry about that.

<P><b>Warning: unfortunately, INPUT controls of type color and range can't represent nullable items in a database.</b> 
This is because the (draft) HTML5 specification doesn't allow such controls to POST non-empty
values when forms are submitted.
The only workaround for this defect of the specification is to define magic values which map to null. 
Use such controls with caution. 

<h3>Prepopulating only portions of a form</h3>
 There is no requirement that the entire HTML form be wrapped by this tag. If 
 desired, only part of a form may be placed in the body of this tag. This is useful 
 when some form controls take a fixed, static value.

 <h3>Convention Regarding Control Names</h3>
 This tag depends on a specfic convention to allow automatic 'binding' between supported controls 
 and corresponding <tt>getXXX</tt> methods of the Model Object. This convention is explained in 
 {@link hirondelle.web4j.request.RequestParameter}.  

 <h3>Deriving values from <tt>getXXX()</tt> methods of the Model Object</h3>
 The return value is found. Any primitives are converted into corresponding wrapper 
 objects. The {@link hirondelle.web4j.request.Formats#objectToText} method is then used to
 translate the object into text. If the return value of the <tt>getXXX</tt> is a 
 <tt>Collection</tt>, then the above is applied to each element. 
 
 <h3>Escaping special characters</h3>
 When this tag assigns a text value to the content of an <tt>INPUT</tt> or <tt>TEXTAREA</tt> tag, then the 
 value is always escaped for special characters using {@link hirondelle.web4j.util.EscapeChars#forHTML(String)}.
 
 <h3><tt>GET</tt> versus <tt>POST</tt></h3>
 This tag depends on the proper <tt>GET/POST</tt> behavior of forms : a <tt>POST</tt>
 request must only be used when an edit to the database is being attempted. (This is the usual style, 
 and would not be regarded by most as being a restriction.)
*/
public final class Populate extends TagHelper {

  /**
   Key for the Model Object to be used for form population. 
  
   <P>This attribute is specified only if the form can be used to edit or change an 
   existing Model Object. If the Model Object is present, then it will be used by this tag to 
   populate supported controls.
  
   <P>This tag searches for the Model Object in the same way as <tt>JspContext.findAttribute(String)</tt>,
   by searching scopes in a specific order : page scope, request scope, session scope, and finally 
   application scope. 
  
   @param aModelObjectKey satisfies {@link Util#textHasContent(String)}.
  */
  public void setUsing(String aModelObjectKey){
    checkForContent("Using", aModelObjectKey);
    fModel = getPageContext().findAttribute(aModelObjectKey);
  }
  
  /**
   Emit the possibly-changed body of this tag, by possibly editing supported form controls 
   contained in the body of this tag.
  */
  @Override protected String getEmittedText(String aOriginalBody) throws JspException {
    String result = null;
    setUseCaseStyle();
    if ( Style.ECHO == fStyle ){
      result = aOriginalBody;
    }
    else {
      result = getEditedBody(aOriginalBody, fStyle);
    }
    return result;
  }
  
  // PRIVATE

  /**
   The ModelObject which is to be used to populate supported controls in the "edit" use case. 
   Is identified by the value of the 'using' attribute
  */
  private Object fModel;
  
  /** Use case style  */
  private Style fStyle;
  
  enum Style {ECHO, USE_MODEL_OBJECT, MUST_RECYCLE_PARAMS, RECYCLE_PARAM_IF_PRESENT}
  
  private static final String GET = "GET";
  private static final String POST = "POST";
  private static final Logger fLogger = Util.getLogger(Populate.class);
  
  private void setUseCaseStyle(){
    String PREAMBLE = "Form population use case: ";
    if( fModel != null ) {
      fLogger.fine(PREAMBLE +"'Using' object is specified and present. All controls will be populated using getXXX methods of the 'using' object.");
      fStyle = Style.USE_MODEL_OBJECT;
    }
    else if( isRequest(POST) ){
      /* Minor Problem: delete ids infecting ADD operations. */
      fLogger.fine(PREAMBLE + "POST. All controls will be populated using request parameter values.");
      fStyle = Style.MUST_RECYCLE_PARAMS;
    }
    else if( isRequest(GET) ) {
      if ( hasNoRequestParameters() ) {
        fLogger.fine(PREAMBLE + "GET, with no request parameters present. Echoing the HTML of entire form as is.");
        fStyle = Style.ECHO;        
      }
      else {
        fLogger.fine(PREAMBLE + "GET. Any request parameter whose name matches a form control will be used to populate that control.");
        fStyle = Style.RECYCLE_PARAM_IF_PRESENT;
      }
    }
    else {
      throw new AssertionError("Unexpected use case.");
    }
  }
  
  private boolean isRequest(String aRequestStyle){
    return getRequest().getMethod().equalsIgnoreCase(aRequestStyle);
  }

  private String getEditedBody(String aOriginalBody, Style aUseCaseStyle) throws JspException {
    PopulateHelper populator = new PopulateHelper(new Wrapper(), aOriginalBody, aUseCaseStyle);
    return populator.getEditedBody();
  }
  
  /** This exists solely to provide a particular 'view' of this object that does not leak into the public API. */
  private class Wrapper implements PopulateHelper.Context {
    public String getReqParamValue(String aParamName){
      String value = getRequest().getParameter(aParamName);
      return value == null ? EMPTY_STRING : value;
    }
    public Collection<String> getReqParamValues(String aParamName){
      Collection<String> result = Collections.emptyList(); //default return value
      String[] values = getRequest().getParameterValues(aParamName);
      if ( values != null ) {
        result = Collections.unmodifiableCollection( Arrays.asList(values) );
      }
      return result;
    }
    public boolean hasRequestParamNamed(String aParamName) {
      boolean result = false; 
      Enumeration allParamNames = getRequest().getParameterNames();
      while (allParamNames.hasMoreElements()){
        if (allParamNames.nextElement().equals(aParamName)) {
          result = true;
          break;
        }
      }
      return result;
    }
    public boolean isModelObjectPresent(){
      return fModel != null;
    }
    public Object getModelObject(){
      return fModel;
    }
    public Formats getFormats(){
      Locale locale = BuildImpl.forLocaleSource().get(getRequest());
      TimeZone timeZone = BuildImpl.forTimeZoneSource().get(getRequest());
      return new Formats(locale, timeZone);
    }
  }
  
  private boolean hasNoRequestParameters(){
    Enumeration namesEnum = getRequest().getParameterNames();
    int numParams = 0;
    while ( namesEnum.hasMoreElements() ){
      ++numParams;
      namesEnum.nextElement();
    }
    return numParams == 0;
  }
}