package hirondelle.web4j.util;

import static hirondelle.web4j.util.Consts.NOT_FOUND;

import java.util.regex.*;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.ServletContext;
import javax.servlet.ServletConfig;

/**
 Static convenience methods for common web-related tasks, which eliminate code duplication.

<P> Similar to {@link hirondelle.web4j.util.Util}, but for methods particular to the web.
*/
public final class WebUtil {
  
  /** Called only upon startup, by the framework.  */
  public static void init(ServletConfig aConfig){
    fContext = aConfig.getServletContext();
  }
  
  /**
   Validate the form of an email address.
  
   <P>Return <tt>true</tt> only if 
  <ul> 
   <li> <tt>aEmailAddress</tt> can successfully construct an 
   {@link javax.mail.internet.InternetAddress} 
   <li> when parsed with "@" as delimiter, <tt>aEmailAddress</tt> contains 
   two tokens which satisfy {@link hirondelle.web4j.util.Util#textHasContent}.
  </ul>
  
  <P> The second condition arises since local email addresses, simply of the form
   "<tt>albert</tt>", for example, are valid for {@link javax.mail.internet.InternetAddress}, 
   but almost always undesired.
  */
  public static boolean isValidEmailAddress(String aEmailAddress){
    if (aEmailAddress == null) return false;
    boolean result = true;
    try {
      InternetAddress emailAddr = new InternetAddress(aEmailAddress);
      if ( ! hasNameAndDomain(aEmailAddress) ) {
        result = false;
      }
    }
    catch (AddressException ex){
      result = false;
    }
    return result;
  }

  /**
   Ensure a particular name-value pair is present in a URL.
    
   <P>If the parameter does not currently exist in the URL, then the name-value
   pair is appended to the URL; if the parameter is already present in the URL,
   however, then its value is changed.
  
   <P>Any number of query parameters can be added to a URL, one after the other.
   Any special characters in <tt>aParamName</tt> and <tt>aParamValue</tt> will be 
   escaped by this method using {@link EscapeChars#forURL}.
  
   <P>This method is intended for cases in which an <tt>Action</tt> requires
   a redirect after processing, and the redirect in turn requires <em>dynamic</em> 
   query parameters. (With a redirect, this is the only way to 
   pass data to the destination page. Items placed in request scope for the 
   original request will no longer be available to the second request caused 
   by the redirect.)
  
   <P>Example 1, where a new parameter is added :<P>
   <tt>setQueryParam("blah.do", "artist", "Tom Thomson")</tt>
  <br>
   returns the value :<br> <tt>blah.do?artist=Tom+Thomson</tt>
  
   <P>Example 2, where an existing parameter is updated :<P>
   <tt>setQueryParam("blah.do?artist=Tom+Thomson", "artist", "A Y Jackson")</tt>
  <br>
   returns the value :<br> <tt>blah.do?artist=A+Y+Jackson</tt>
   
   <P>Example 3, with a parameter name of slightly different form :<P>
   <tt>setQueryParam("blah.do?Favourite+Artist=Tom+Thomson", "Favourite Artist", "A Y Jackson")</tt>
  <br>
   returns the value :<br> <tt>blah.do?Favourite+Artist=A+Y+Jackson</tt>
   
   @param aURL a base URL, with <em>escaped</em> parameter names and values
   @param aParamName <em>unescaped</em> parameter name
   @param aParamValue <em>unescaped</em> parameter value
  */
  public static String setQueryParam(String aURL, String aParamName, String aParamValue){
    String result = null;
    if ( aURL.indexOf(EscapeChars.forURL(aParamName) + "=") == -1) {
      result = appendParam(aURL, aParamName, aParamValue);
    }
    else {
      result = replaceParam(aURL, aParamName, aParamValue);
    }
    return result;
  }
  
  /**
   Return {@link HttpServletRequest#getRequestURL}, optionally concatenated with 
   <tt>?</tt> and {@link HttpServletRequest#getQueryString}.
   
   <P>Query parameters are added only if they are present.
   
   <P>If the underlying method is a <tt>GET</tt> which does NOT edit the database, 
   then presenting the return value of this method in a link is usually acceptable. 
   
   <P>If the underlying method is a <tt>POST</tt>, or if it is a <tt>GET</tt> which 
   (erroneously) edits the database, it is recommended that the return value of 
   this method NOT be placed in a link. 
   
   <P><em>Warning</em> : if this method is called in JSP or custom tag, then it 
   is likely that the original query string has been overwritten by the server, 
   as result of an internal <tt>forward</tt> operation.
  */
  public static String getURLWithQueryString(HttpServletRequest aRequest){
    StringBuilder result = new StringBuilder();
    result.append(aRequest.getRequestURL());
    String queryString = aRequest.getQueryString();
    if ( Util.textHasContent(queryString) ) {
      result.append("?");
      result.append(queryString);
    }
    return result.toString();
  }
  
  /**
   Return the original, complete URL submitted by the browser.
   
   <P>Session id is included in the return value.
   
   <P>Somewhat frustratingly, the original client request is not directly available from 
   the Servlet API. 
   <P>This implementation is based on an example in the 
   <a href="http://www.exampledepot.com/egs/javax.servlet/GetReqUrl.html">Java Almanac</a>.
  */
  public static String getOriginalRequestURL(HttpServletRequest aRequest, HttpServletResponse aResponse){
    String result = null;
    //http://hostname.com:80/mywebapp/servlet/MyServlet/a/b;c=123?d=789
    String scheme = aRequest.getScheme();             // http
    String serverName = aRequest.getServerName();     // hostname.com
    int serverPort = aRequest.getServerPort();        // 80
    String contextPath = aRequest.getContextPath();   // /mywebapp
    String servletPath = aRequest.getServletPath();   // /servlet/MyServlet
    String pathInfo = aRequest.getPathInfo();         // /a/b;c=123
    String queryString = aRequest.getQueryString();   // d=789

    // Reconstruct original requesting URL
    result = scheme + "://" + serverName + ":" + serverPort + contextPath + servletPath;
    if (Util.textHasContent(pathInfo)) {
        result = result + pathInfo;
    }
    if (Util.textHasContent(queryString)) {
        result = result + "?" + queryString;
    }
    return aResponse.encodeURL(result);    
  }
  
  /**
   Find an attribute by searching request scope, session scope (if it exists), and application scope (in that order).
   
   <P>If there is no session, then this method will not create one. 
   
   <P>If no <tt>Object</tt> corresponding to <tt>aKey</tt> is found, then <tt>null</tt> is returned. 
  */
  public static Object findAttribute(String aKey, HttpServletRequest aRequest){
    //This method is similar to {@link javax.servlet.jsp.JspContext#findAttribute(java.lang.String)}
    Object result = null;
    result = aRequest.getAttribute(aKey);
    if( result == null ) {
      HttpSession session = aRequest.getSession(DO_NOT_CREATE);
      if( session != null ) {
        result = session.getAttribute(aKey);
      }
    }
    if( result == null ) {
      result = fContext.getAttribute(aKey);
    }
    return result;
  }
  
  /**
   Returns the 'file extension' for a given URL. 
   
   <P>Some example return values for this method :
   <table  border='1' cellpadding='3' cellspacing='0'>
    <tr><th>URL</th><th>'File Extension'</th></tr>
    <tr>
      <td>.../VacationAction.do</td>
      <td>do</td>
    </tr>
    <tr>
      <td>.../VacationAction.fetchForChange?Id=103</td>
      <td>fetchForChange</td>
    </tr>
    <tr>
      <td>.../VacationAction.list?Start=Now&End=Never</td>
      <td>list</td>
    </tr>
    <tr>
      <td>.../SomethingAction.show;jsessionid=32131?SomeId=123456</td>
      <td>show</td>
    </tr>
   </table>
   
   @param aURL has content, and contains a '.' character (which defines the start of the 'file extension'.)
  */
  public static String getFileExtension(String aURL) {
    String result = null;
    int lastPeriod = aURL.lastIndexOf(".");
    if( lastPeriod == NOT_FOUND ) {
      throw new RuntimeException("Cannot find '.' character in URL: " + Util.quote(aURL));
    }
    int jsessionId = aURL.indexOf(";jsessionid");
    int firstQuestionMark = aURL.indexOf("?");
    if( jsessionId != NOT_FOUND){
      result = aURL.substring(lastPeriod + 1, jsessionId);
    }
    else if( firstQuestionMark != NOT_FOUND){
      result = aURL.substring(lastPeriod + 1, firstQuestionMark);
    }
    else {
      result = aURL.substring(lastPeriod + 1);
    }
    return result;
  }
  
  // PRIVATE 
  
  private WebUtil(){
    //empty - prevent construction
  }
  
  /** Needed for searching application scope for attributes. */
  private static ServletContext fContext;
  
  private static final boolean DO_NOT_CREATE = false;
  
  private static String appendParam(String aURL, String aParamName, String aParamValue){
    StringBuilder result = new StringBuilder(aURL);
    if (aURL.indexOf("?") == -1) {
      result.append("?");
    }
    else {
      result.append("&");
    }
    result.append( EscapeChars.forURL(aParamName) );
    result.append("=");
    result.append( EscapeChars.forURL(aParamValue) );
    return result.toString();
  }

  private static String replaceParam(String aURL, String aParamName, String aParamValue){
    String regex = "(\\?|\\&)(" + EscapeChars.forRegex(EscapeChars.forURL(aParamName)) + "=)([^\\&]*)";
    StringBuffer result = new StringBuffer();
    Pattern pattern = Pattern.compile( regex );
    Matcher matcher = pattern.matcher(aURL);
    while ( matcher.find() ) {
      matcher.appendReplacement(result, getReplacement(matcher, aParamValue));
    }
    matcher.appendTail(result);
    return result.toString();
  }
  
  private static String getReplacement(Matcher aMatcher, String aParamValue){
    return aMatcher.group(1) + aMatcher.group(2) + EscapeChars.forURL(aParamValue);
  }
  
  private static boolean hasNameAndDomain(String aEmailAddress){
    String[] tokens = aEmailAddress.split("@");
    return 
     tokens.length == 2 &&
     Util.textHasContent( tokens[0] ) && 
     Util.textHasContent( tokens[1] ) ;
  }
}
