package hirondelle.web4j.request; 

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.action.Action;
import hirondelle.web4j.model.AppException;
import hirondelle.web4j.model.BadRequestException;
import hirondelle.web4j.model.ConvertParam;
import hirondelle.web4j.model.ConvertParamError;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.util.Util;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 Abstract Base Class (ABC) for mapping a request to an {@link Action}. 

 <P>See the {@link hirondelle.web4j.BuildImpl} for important information on how this item is configured.

 <P><span class="highlight">Almost all concrete implementations of this Abstract Base Class will need to 
 implement only a single method</span> - {@link #getWebAction()}. WEB4J provides a default implementation 
 {@link RequestParserImpl}. 
 
 <P>The role of this class is to view the request at a higher level than the underlying 
 Servlet API. In particular, its services include :
<ul>
 <li>mapping a request to an {@link Action}
 <li>parsing request parameters into common "building block" objects (and Collections thereof), 
 such as {@link Date}, {@link BigDecimal} and so on, using the configured implementation of 
 {@link hirondelle.web4j.model.ConvertParam}. (The application programmer will usually use 
 {@link hirondelle.web4j.model.ModelFromRequest} to build Model Objects.) 
</ul>
 
 <P><a href="RequestParameter.html#FileUpload">File upload</a> parameters are not returned 
 by this class. Such parameters must be examined in an {@link Action}. The Servlet API 
 before version 3.0 of the specification has poor support for file upload parameters, 
 and use of a third party tool is recommended. 
 
 <P>The various <tt>toXXX</tt> methods are offered as a convenience for accessing <tt>String</tt> 
 and <tt>String</tt>-like data. All such <tt>toXXX</tt> methods apply the filtering (and possible 
 preprocessing) performed by {@link hirondelle.web4j.model.ConvertParam}.   
*/
public abstract class RequestParser {
  
  /**
   Return the configured concrete instance of this Abstract Base Class.
   <P>See the {@link hirondelle.web4j.BuildImpl} for important information on how 
   this item is configured.
  */
  public static RequestParser getInstance(HttpServletRequest aRequest, HttpServletResponse aResponse){
    List<Object> args = new ArrayList<Object>();
    args.add(aRequest);
    args.add(aResponse);
    RequestParser result = (RequestParser)BuildImpl.forAbstractionPassCtorArgs(
      RequestParser.class.getName(), 
      args
    );
    return result;
  }
  
  /** Constructor called by subclasses.  */
  public RequestParser(HttpServletRequest aRequest, HttpServletResponse aResponse){
    fRequest = aRequest;
    fResponse = aResponse;
    fLocale = BuildImpl.forLocaleSource().get(aRequest);
    fTimeZone = BuildImpl.forTimeZoneSource().get(aRequest);
    fConvertUserInput = BuildImpl.forConvertParam();
    fConversionError = BuildImpl.forConvertParamError();
  }

  /**
   Map a given request to a corresponding {@link Action}.
  
   <P>The mapping is determined entirely by concrete subclasses, and must  
   be implemented by the application programmer. {@link RequestParserImpl} is 
   provided as a default implementation, and is very likely adequate for most 
   applications.
   
   <P>If the incoming request does not map to a known {@link Action}, then throw 
   a {@link BadRequestException}. <span class="highlight">Such requests 
   are expected only for bugs and for malicious attacks, and never as part of the normal operation 
   of the program.</span>
  */
  abstract public Action getWebAction() throws BadRequestException;
  
  /**
   Return the parameter value exactly as it appears in the request.
    
   <P>Can return <tt>null</tt> values, empty values, values containing 
   only whitespace, and values equal to the <tt>IgnorableParamValue</tt> configured in <tt>web.xml</tt>.
  */
  public final String getRawParamValue(RequestParameter aReqParam){
    String result = fRequest.getParameter(aReqParam.getName());
    return result;
  }
  
  /**
   Return a multi-valued parameter's values exactly as they appear in the request.
   
   <P>Can return <tt>null</tt> values, empty values, values containing 
   only whitespace, and values equal to the <tt>IgnorableParamValue</tt> configured in <tt>web.xml</tt>.
  */
  public final String[] getRawParamValues(RequestParameter aReqParam){
    String[] result = fRequest.getParameterValues(aReqParam.getName());
    return result;
  }

  /**
   Return a building block object.
   
   <P>Uses all methods of the configured implementation of {@link ConvertParam}. 
   @param aReqParam underlying request parameter
   @param aSupportedTargetClass must be supported - see {@link ConvertParam#isSupported(Class)}
  */
  public <T> T toSupportedObject(RequestParameter aReqParam, Class<T> aSupportedTargetClass) throws ModelCtorException {
    T result = null;
    if( ! fConvertUserInput.isSupported(aSupportedTargetClass) ){
      throw new AssertionError("This class is not supported by ConvertParam: " + Util.quote(aSupportedTargetClass));
    }
    String filteredValue = fConvertUserInput.filter(getRawParamValue(aReqParam));
    if( Util.textHasContent(filteredValue) ){
      try {
        result  = fConvertUserInput.convert(filteredValue, aSupportedTargetClass, fLocale, fTimeZone);
      }
      catch (ModelCtorException ex){
        ModelCtorException conversionEx = fConversionError.get(aSupportedTargetClass, filteredValue, aReqParam);
        throw conversionEx;
      }
    }
    return result; 
  }

  /**
   Return an ummodifiable <tt>List</tt> of building block objects.
   
   <P>Uses all methods of the configured implementation of {@link ConvertParam}.
   <P>
   <em>Design Note</em><br>
   <tt>List</tt> is returned here since HTML specs state that browsers submit param values 
   in the order of appearance of the corresponding controls in the web page. 
   @param aReqParam underlying request parameter
   @param aSupportedTargetClass must be supported - see {@link ConvertParam#isSupported(Class)}
  */
  public <T> List<T> toSupportedObjects(RequestParameter aReqParam, Class<T> aSupportedTargetClass) throws ModelCtorException {
    List<T> result = new ArrayList<T>();
    ModelCtorException conversionExceptions = new ModelCtorException();
    if( ! fConvertUserInput.isSupported(aSupportedTargetClass) ){
      throw new AssertionError("This class is not supported by ConvertParam: " + Util.quote(aSupportedTargetClass));
    }
    String[] rawValues = getRawParamValues(aReqParam);
    if(rawValues != null){
      for(String rawValue: rawValues){
        String filteredValue = fConvertUserInput.filter(rawValue); //possibly null
        //is it possible to have a multi-valued boolean param???        
        if ( Util.textHasContent(filteredValue) || Boolean.class == aSupportedTargetClass){ 
          try {
            T convertedItem  = fConvertUserInput.convert(filteredValue, aSupportedTargetClass, fLocale, fTimeZone);
            result.add(convertedItem);
          }
          catch (ModelCtorException ex){
            AppException conversionEx = fConversionError.get(aSupportedTargetClass, filteredValue, aReqParam);
            conversionExceptions.add(conversionEx);
          }
        }
        else {
          result.add(null);
        }
      }
      if (conversionExceptions.isNotEmpty()) throw conversionExceptions;
    }
    return Collections.unmodifiableList(result);
  }

  /** Return a single-valued request parameter as {@link SafeText}.  */
  public final SafeText toSafeText(RequestParameter aReqParam) {
    SafeText result = null;
    try {
      result = toSupportedObject(aReqParam, SafeText.class); 
    }
    catch (ModelCtorException ex){
      changeToRuntimeException(ex);
    }
    return result;
  }
  
  /** Return a multi-valued request parameter as a {@code Collection<SafeText>}.  */
  public final Collection<SafeText> toSafeTexts(RequestParameter aReqParam) {
    Collection<SafeText> result = null;
    try {
      result = toSupportedObjects(aReqParam, SafeText.class);
    }
    catch (ModelCtorException ex){
      changeToRuntimeException(ex);
    }
    return result;
  }
    
  /** Return a single-valued request parameter as an {@link Id}.  */
  public final Id toId(RequestParameter aReqParam) {
    Id result = null;
    try {
      result = toSupportedObject(aReqParam, Id.class);
    }
    catch(ModelCtorException ex){
      changeToRuntimeException(ex);
    }
    return result;
  }
   
  /** Return a multi-valued request parameter as a {@code Collection<Id>}.  */
  public final Collection<Id> toIds(RequestParameter aReqParam) {
    Collection<Id> result = null;
    try {
      result = toSupportedObjects(aReqParam, Id.class);
    }
    catch (ModelCtorException ex){
      changeToRuntimeException(ex);
    }
    return result;
  }
  
  /** Return the underlying request.   */
  public final HttpServletRequest getRequest(){
    return fRequest;
  }
  
  /** Return the response associated with the underlying request.  */
  public final HttpServletResponse getResponse(){
    return fResponse;
  }
  
  /**
   Return <tt>true</tt> only if the request is a <tt>POST</tt>, and has  
   content type starting with <tt>multipart/form-data</tt>.
  */
  public final boolean isFileUploadRequest(){
    return 
      fRequest.getMethod().equalsIgnoreCase("POST") && 
      fRequest.getContentType().startsWith("multipart/form-data")
    ;
  }
  
  // PRIVATE //
  private final HttpServletRequest fRequest;
  private final HttpServletResponse fResponse;
  private final Locale fLocale;
  private final TimeZone fTimeZone;
  private final ConvertParam fConvertUserInput;
  private final ConvertParamError fConversionError;
  
  /**
   Change from a checked to an unchecked ex.
   
   <P>This is unusual, and a bit ugly. For stringy data, there isn't any possibility of a 
   parse error. Requiring Action constructors to catch or throw a ModelCtorEx is distasteful
   (this would happen for items that have an Operation built in the constructor.)
  */
  private void changeToRuntimeException(ModelCtorException ex){
    throw new IllegalArgumentException(ex);
  }
}
