package hirondelle.web4j.model;

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.readconfig.Config;
import hirondelle.web4j.request.DateConverter;
import hirondelle.web4j.request.Formats;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.util.Util;

import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/** Default implementation of {@link ConvertParam}.*/
public class ConvertParamImpl implements ConvertParam {
  
  /**
   Return <tt>true</tt> only if <tt>aTargetClass</tt> is supported by this implementation. 
   <P>
   The following classes are supported by this implementation as building block classes: 
   <ul>
   <li><tt>{@link SafeText}</tt>
   <li><tt>String</tt> (conditionally, see below)
   <li><tt>Integer</tt>
   <li><tt>Long</tt>
   <li><tt>Boolean</tt>
   <li><tt>BigDecimal</tt>
   <li><tt>{@link Decimal}</tt>
   <li><tt>{@link Id}</tt>
   <li><tt>{@link DateTime}</tt>
   <li><tt>java.util.Date</tt>
   <li><tt>Locale</tt> 
   <li><tt>TimeZone</tt> 
   <li><tt>InputStream</tt>
   </ul>
   
   <P><i>You are not obliged to use this class to model Locale and TimeZone. 
   Many will choose to implement them as just another 
   <a href='http://www.web4j.com/UserGuide.jsp#StartupTasksAndCodeTables'>code table</a>
    instead.</i> In this case, your model object constructors would usually take an {@link Id} parameter for these 
    items, and translate them into a {@link Code}. See the example apps for a demonstration of this technique. 
   
   <P><b>String is supported only when explicitly allowed.</b> 
   The <tt>AllowStringAsBuildingBlock</tt> setting in <tt>web.xml</tt>
   controls whether or not this class allows <tt>String</tt> as a supported class.
   By default, its value is <tt>FALSE</tt>, since {@link SafeText} is the recommended 
   replacement for <tt>String</tt>.  
  */
  public final boolean isSupported(Class<?> aTargetClass){
    boolean result = false;
    if (String.class.equals(aTargetClass)){
      result = fConfig.getAllowStringAsBuildingBlock();
    }
    else {
      for (Class standardClass : STANDARD_CLASSES){
        if (standardClass.isAssignableFrom(aTargetClass)){
          result = true;
          break;
        }
      }
    }
    return result;
  }

  /**
   Coerce all parameters with no visible content to <tt>null</tt>.
   
   <P>In addition, any raw input value that matches <tt>IgnorableParamValue</tt> in <tt>web.xml</tt> is 
   also coerced to <tt>null</tt>. See <tt>web.xml</tt> for more information.
   
   <P>Any non-<tt>null</tt> result is trimmed. 
   This method can be overridden, if desired.
  */
  public String filter(String aRawInputValue){
    String result = aRawInputValue;
    if ( ! Util.textHasContent(aRawInputValue) || aRawInputValue.equals(getIgnorableParamValue()) ){
      result = null;
    }
    return Util.trimPossiblyNull(result); //some apps may elect to trim elsewhere 
  }

  /**
   Apply reasonable parsing policies, suitable for most applications.
   
   <P>Roughly, the policies are: 
   <ul>
   <li><tt>SafeText</tt> uses {@link SafeText#SafeText(String)}
   <li><tt>String</tt> just return the filtered value as is
   <li><tt>Integer</tt> uses {@link Integer#Integer(String)}
   <li><tt>BigDecimal</tt> uses {@link Formats#getDecimalInputFormat()}
   <li><tt>Decimal</tt> uses {@link Formats#getDecimalInputFormat()}
   <li><tt>Boolean</tt> uses {@link Util#parseBoolean(String)}
   <li><tt>DateTime</tt> uses {@link DateConverter#parseEyeFriendlyDateTime(String, Locale)} 
   and {@link DateConverter#parseHandFriendlyDateTime(String, Locale)}
   <li><tt>Date</tt> uses {@link DateConverter#parseEyeFriendly(String, Locale, TimeZone)} 
   and {@link DateConverter#parseHandFriendly(String, Locale, TimeZone)}
   <li><tt>Long</tt> uses {@link Long#Long(String)}
   <li><tt>Id</tt> uses {@link Id#Id(String)}
   <li><tt>Locale</tt> uses {@link Locale#getAvailableLocales()} and {@link Locale#toString()}, case sensitive.
   <li><tt>TimeZone</tt> uses {@link TimeZone#getAvailableIDs()}, case sensitive.
  </ul>
  <tt>InputStream</tt>s are not converted by this class, and need to be handled separately by the caller.
  */
  public final <T> T convert(String aFilteredInputValue, Class<T> aSupportedTargetClass, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
    // Defensive : this check should have already been performed by the calling framework class.
    if( ! isSupported(aSupportedTargetClass) ) {
      throw new AssertionError("Unsupported type cannot be translated to an object: " + aSupportedTargetClass + ". If you're trying to use String, consider using SafeText instead. Otherwise, change the AllowStringAsBuildingBlock setting in web.xml.");
    }
    
    Object result = null;
    if (aSupportedTargetClass == SafeText.class){
      //no translation needed; some impl's might trim here, or force CAPS
      result = parseSafeText(aFilteredInputValue);
    }
    else if (aSupportedTargetClass == String.class) {
      result = aFilteredInputValue; //no translation needed; some impl's might trim here, or force CAPS
    }
    else if (aSupportedTargetClass == Integer.class || aSupportedTargetClass == int.class){
      result = parseInteger(aFilteredInputValue);
    }
    else if (aSupportedTargetClass == Boolean.class || aSupportedTargetClass == boolean.class){
      result = Util.parseBoolean(aFilteredInputValue);
    }
    else if (aSupportedTargetClass == BigDecimal.class){
      result = parseBigDecimal(aFilteredInputValue, aLocale, aTimeZone);
    }
    else if (aSupportedTargetClass == Decimal.class){
      result = parseDecimal(aFilteredInputValue, aLocale, aTimeZone);
    }
    else if (aSupportedTargetClass == java.util.Date.class){
      result = parseDate(aFilteredInputValue, aLocale, aTimeZone);
    }
    else if (aSupportedTargetClass == DateTime.class){
      result = parseDateTime(aFilteredInputValue, aLocale);
    }
    else if (aSupportedTargetClass == Long.class || aSupportedTargetClass == long.class){
      result = parseLong(aFilteredInputValue);
    }
    else if (aSupportedTargetClass == Id.class){
      result = new Id(aFilteredInputValue.trim());
    }
    else if (aSupportedTargetClass == Locale.class){
      result = parseLocale(aFilteredInputValue);
    }
    else if (TimeZone.class.isAssignableFrom(aSupportedTargetClass)){
      //the above style is needed since TimeZone is abstract
      result = parseTimeZone(aFilteredInputValue);
    }
    else {
       throw new AssertionError("Failed to build object for ostensibly supported class: " + aSupportedTargetClass);
    }
    fLogger.finer("Converted request param into a " + aSupportedTargetClass.getName());
    return (T)result; //this cast is unavoidable, and safe.
  }
  
  /**
   Return the <tt>IgnorableParamValue</tt> configured in <tt>web.xml</tt>.
   See <tt>web.xml</tt> for more information.
  */
  public final String getIgnorableParamValue(){
    return fConfig.getIgnorableParamValue();
  }
  
  // PRIVATE 
  
  private Config fConfig = new Config();
  private static List<Class<?>> STANDARD_CLASSES; //always the same 
  private static final ModelCtorException PROBLEM_FOUND = new ModelCtorException();
  private static final Logger fLogger = Util.getLogger(ConvertParamImpl.class);
  static {  
    STANDARD_CLASSES = new ArrayList<Class<?>>();
    STANDARD_CLASSES.add(Integer.class);
    STANDARD_CLASSES.add(int.class);
    STANDARD_CLASSES.add(Boolean.class);
    STANDARD_CLASSES.add(boolean.class);
    STANDARD_CLASSES.add(BigDecimal.class);
    STANDARD_CLASSES.add(java.util.Date.class);
    STANDARD_CLASSES.add(Long.class);
    STANDARD_CLASSES.add(long.class);
    STANDARD_CLASSES.add(Id.class);
    STANDARD_CLASSES.add(SafeText.class);
    STANDARD_CLASSES.add(Locale.class);
    STANDARD_CLASSES.add(TimeZone.class); 
    STANDARD_CLASSES.add(Decimal.class);
    STANDARD_CLASSES.add(DateTime.class);
    STANDARD_CLASSES.add(InputStream.class);
  }
  
  private Integer parseInteger(String aUserInputValue) throws ModelCtorException {
    try {
      return new Integer(aUserInputValue);
    }
    catch (NumberFormatException ex){
      throw PROBLEM_FOUND;
    }
  }
  
  private BigDecimal parseBigDecimal(String aUserInputValue, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
    BigDecimal result = null;
    Formats formats = new Formats(aLocale, aTimeZone);
    Pattern pattern = formats.getDecimalInputFormat();
    if ( Util.matches(pattern, aUserInputValue)) {
      //BigDecimal ctor only takes '.' as decimal sign, never ','          
      result = new BigDecimal(aUserInputValue.replace(',', '.'));
    }
    else {
      throw PROBLEM_FOUND;
    }
    return result;
  }
  
  private Decimal parseDecimal(String aUserInputValue, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
    Decimal result = null;
    BigDecimal amount = null;
    Formats formats = new Formats(aLocale, aTimeZone);
    Pattern pattern = formats.getDecimalInputFormat();
    if ( Util.matches(pattern, aUserInputValue)) {
      //BigDecimal ctor only takes '.' as decimal sign, never ','          
      amount = new BigDecimal(aUserInputValue.replace(',', '.'));
      try {
         result = new Decimal(amount);
      }
      catch(IllegalArgumentException ex){
        throw PROBLEM_FOUND;
      }
    }
    else {
      throw PROBLEM_FOUND;
    }
    return result;
  }
  
  private Date parseDate(String aUserInputValue, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
    Date result = null;
    DateConverter dateConverter = BuildImpl.forDateConverter();
    result = dateConverter.parseHandFriendly(aUserInputValue, aLocale, aTimeZone);
    if ( result == null ){
      result = dateConverter.parseEyeFriendly(aUserInputValue, aLocale, aTimeZone);
    }
    if ( result == null ) {
      throw PROBLEM_FOUND;
    }
    return result;
  }
  
  private DateTime parseDateTime(String aUserInputValue, Locale aLocale) throws ModelCtorException {
    DateTime result = null;
    DateConverter dateConverter = BuildImpl.forDateConverter();
    result = dateConverter.parseHandFriendlyDateTime(aUserInputValue, aLocale);
    if ( result == null ){
      result = dateConverter.parseEyeFriendlyDateTime(aUserInputValue, aLocale);
    }
    if ( result == null ) {
      throw PROBLEM_FOUND;
    }
    return result;
  }
  
  private Long parseLong(String aUserInputValue) throws ModelCtorException {
    Long result = null;
    if ( Util.textHasContent(aUserInputValue) ){
      try {
        result = new Long(aUserInputValue);
      }
      catch (NumberFormatException ex){
        throw PROBLEM_FOUND;
      }
    }
    return result;
  }
  
  private SafeText parseSafeText(String aUserInputValue) throws ModelCtorException {
    SafeText result = null;
    if( Util.textHasContent(aUserInputValue) ) {
      try {
        result = new SafeText(aUserInputValue);
      }
      catch(IllegalArgumentException ex){
        throw PROBLEM_FOUND;
      }
    }
    return result;
  }

  /** Translate user input into a known time zone id. Case sensitive. */
  private TimeZone parseTimeZone(String aUserInputValue) throws ModelCtorException {
    TimeZone result = null;
    if ( Util.textHasContent(aUserInputValue) ){
      List<String> allTimeZoneIds = Arrays.asList(TimeZone.getAvailableIDs());
      for(String id : allTimeZoneIds){
        if (id.equals(aUserInputValue)){
          result = TimeZone.getTimeZone(id);
          break;
        }
      }
      if(result == null){ //has content, but no match found
        throw PROBLEM_FOUND;
      }
    }
    return result;
  }
  
  /** Translate user input into a known Locale id. Case sensitive. */
  private Locale parseLocale(String aUserInputValue) throws ModelCtorException {
    Locale result = null;
    if ( Util.textHasContent(aUserInputValue) ){
      List<Locale> allLocales = Arrays.asList(Locale.getAvailableLocales());
      for(Locale locale: allLocales){
        if (locale.toString().equals(aUserInputValue)){
          result = locale;
          break;
        }
      }
      if(result == null){ //has content, but no match found
        throw PROBLEM_FOUND;
      }
    }
    return result;
  }
}
