package hirondelle.web4j.model;                    
   
import java.util.*;
import java.util.logging.Logger;
import java.util.regex.*;
import java.math.BigDecimal;

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.security.SpamDetector;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.WebUtil;
import static hirondelle.web4j.util.Consts.PASSES;
import static hirondelle.web4j.util.Consts.FAILS;
import hirondelle.web4j.util.Args;

/**
 <span class="highlight">Returns commonly needed {@link Validator} objects.</span>
 
 <P>In general, the number of possible validations is <em>very</em> large. It is not appropriate 
 for a framework to attempt to implement <em>all</em> possible validations. Rather, a framework should 
 provide the most common validations, and allow the application programmer to extend 
 the validation mechanism as needed.
 
 <P>Validations are important parts of your program's logic. 
 Using tools such as JUnit to test your validation code is highly recommended. 
 Since your Model Objects have no dependencies on heavyweight objects, they're almost always easy to test.
  
 <P>If a specific validation is not provided here, other options include : 
<ul>
 <li>performing the validation directly in the Model Object, without using <tt>Check</tt> or 
 a {@link Validator}
 <li>create a new {@link Validator}, and pass it to either {@link #required(Object, Validator...)}
 or {@link #optional(Object, Validator...)}. This option is especially attractive when it will eliminate 
 code repetition.  
 <li>subclassing this class, and adding new <tt>static</tt> methods
 </ul>
 
 <P>The {@link #range(long, long)}, {@link #min(long)} and {@link #max(long)} methods return {@link Validator}s 
 that perform checks on a <tt>long</tt> value. <span class='highlight'>The <em>source</em> of the <tt>long</tt> value varies 
 according to the type of <tt>Object</tt> passed to the {@link Validator}</span>, and is taken as follows 
 (<tt>int</tt> is internally converted to <tt>long</tt> when necessary) : 
 <ul>
 <li>{@link Integer#intValue()}
 <li>{@link Long#longValue()} 
 <li>length of a trimmed {@link String} having content; the same is applied to {@link Id#toString()},
 {@link Code#getText()}, and {@link SafeText#getRawString()}.
 <li>{@link Collection#size()}
 <li>{@link Map#size()}
 <li>{@link Date#getTime()} - underlying millisecond value 
 <li>{@link Calendar#getTimeInMillis()} - underlying millisecond value 
 <li>any other class will cause an exception to be thrown by {@link #min(long)} and {@link #max(long)} 
 </ul>

 <P>The {@link #required(Object)}, {@link #required(Object, Validator...)} and {@link #optional(Object, Validator...)} 
 methods are important, and are separated out as distinct validations. <span class='highlight'>In addition, the 
 required/optional character of a field is always the <em>first</em> validation performed</span> (see examples below).

 <P><span class="highlight">In general, it is highly recommended that applications 
 aggressively perform all possible validations.</span>  
 
 <P> Note that when validation is performed in a Model Object, then it will apply both to objects created from 
 user input, and to objects created from a database <tt>ResultSet</tt>. 
 
 <P><b>Example 1</b><br>
 Example of a required field in a Model Object (that is, the field is of any type, and 
 must be non-<tt>null</tt>) :
 <PRE>
if ( ! Check.required(fStartDate) ) {
  ex.add("Start Date is Required.");
}
 </PRE>

  
 <P><b>Example 2</b><br>
 Example of a required <em>text</em> field, which must have visible content 
 (as in {@link Util#textHasContent(String)}) :
 <PRE>
if ( ! Check.required(fTitle) ) {
  ex.add("Title is required, and must have content.");
}
 </PRE>
  
 <P><b>Example 3</b><br>
 Example of a required text field, whose length must be in the range <tt>2..50</tt> :
 <PRE>
if ( ! Check.required(fTitle, Check.range(2,50)) ) {
  ex.add("Title is required, and must have between 2 and 50 characters.");
}
 </PRE>
 
 <P><b>Example 4</b><br>
 Example of an optional <tt>String</tt> field that matches the format '<tt>1234-5678</tt>' :
 <PRE>
//compile the pattern once, when the class is loaded
private static final Pattern fID_PATTERN = Pattern.compile("(\\d){4}-(\\d){4}");
...
if ( ! Check.optional(fSomeId, Check.pattern(fID_PATTERN)) ) {
  ex.add("Id is optional, and must have the form '1234-5678'.");
}
 </PRE>

 <P><b>Example 5</b><br>
 The initial <tt>!</tt> negation operator is easy to forget. Many will prefer a more explicit style, which seems 
 to be more legible :
 <PRE>
import static hirondelle.web4j.util.Consts.FAILS;
...
if ( FAILS == Check.required(fStartDate) ) {
  ex.add("Start Date is Required.");
}
 </PRE>
 
 <P>Here is one style for implementing custom validations for your application :
 <PRE>
//Checks that a person's age is in the range 0..150
public static Validator ageRange(){
  class CheckAge implements Validator {
    public boolean isValid(Object aObject) {
      Integer age = (Integer)aObject;
      return 0 <= age <= 150; 
    }
  }
  return new CheckAge();
}
 </PRE>
*/
public class Check {
  
  /**
   Return <tt>true</tt> only if <tt>aObject</tt> is non-<tt>null</tt>.
   
   <P><em><tt>String</tt> and {@link SafeText} objects are a special case</em> : instead of just 
   being non-<tt>null</tt>, they must have content according to {@link Util#textHasContent(String)}.
   
   @param aObject possibly-null field of a Model Object.
  */
  public static boolean required(Object aObject){
    boolean result = FAILS;
    if ( isText(aObject) ) {
       result = Util.textHasContent(getText(aObject));
    }
    else { 
      result = (aObject != null);
    }
    return result;
  }

  /**
   Return <tt>true</tt> only if <tt>aObject</tt> satisfies {@link #required(Object)}, 
   <em>and</em> it passes all given validations
    
   @param aObject possibly-null field of a Model Object.
  */ 
  public static boolean required(Object aObject, Validator... aValidators){
    boolean result = PASSES;
    if ( ! required(aObject) ) {
      result = FAILS;
    } 
    else { 
      result = passesValidations(aObject, aValidators);
    }
    return result;
  }
  
  /**
   Return <tt>true</tt> only if <tt>aObject</tt> is <tt>null</tt>, OR if <tt>aObject</tt> is non-<tt>null</tt> 
   and passes all validations.
   
   <P><em><tt>String</tt> and {@link SafeText} objects are a special case</em> : instead of just 
   being non-<tt>null</tt>, they must have content according to {@link Util#textHasContent(String)}.
   
   @param aObject possibly-null field of a Model Object.
  */
  public static boolean optional(Object aObject, Validator... aValidators){
    boolean result = PASSES;
    if ( aObject != null ){
      if( isText(aObject) ) {
        result = Util.textHasContent(getText(aObject)) && passesValidations(aObject, aValidators);
      }        
      else {
        result = passesValidations(aObject, aValidators);
      }
    }
    return result;
  }
  
  /**
   Return a {@link Validator} to check that all passed booleans are <tt>true</tt>. 
   Note that the single parameter is a sequence parameter, so you may pass in many booleans, not just one.
   <P>This is a bizarre method, but it's actually useful. It allows checks on an object's state to be treated as any other check.   
  */
  public static Validator isTrue(Boolean... aPredicates){
    class AllTrue implements Validator{
      AllTrue(Boolean... aPreds){
        fPredicates = aPreds;
      }
      public boolean isValid(Object aObject) {
        //aObject is ignored here
        boolean result = true;
        for (Boolean predicate: fPredicates){
          result = result && predicate;
        }
        return result;
      };
      private Boolean[] fPredicates;
    }
    return new AllTrue(aPredicates);
  }
  
  /**
   Return a {@link Validator} to check that all passed booleans are <tt>false</tt>. 
   Note that the single parameter is a sequence parameter, so you may pass in many booleans, not just one.
   <P>This is a bizarre method, but it's actually useful. It allows checks on an object's state to be treated as any other check.   
  */
  public static Validator isFalse(Boolean... aPredicates){
    class AllFalse implements Validator{
      AllFalse(Boolean... aPreds){
        fPredicates = aPreds;
      }
      public boolean isValid(Object aObject) {
        //aObject is ignored here
        boolean result = true;
        for (Boolean predicate: fPredicates){
          result = result && !predicate;
        }
        return result;
      };
      private Boolean[] fPredicates;
    }
    return new AllFalse(aPredicates);
  }
  
  /**
   Return a {@link Validator} to check that a field's value is greater than or equal to <tt>aMinimumValue</tt>. 
   See class comment for the kind of objects which may be checked by the returned {@link Validator}.
  */
  public static Validator min(final long aMinimumValue){
    class Minimum implements Validator {
      Minimum(long aMinValue){
        fMinValue = aMinValue;
      }
      public boolean isValid(Object aObject){
        long value = getValueAsLong(aObject);
        return value >= fMinValue;
      }
      private final long fMinValue;
    }
    return new Minimum(aMinimumValue);
  }
  
  /**
   Return a {@link Validator} to check that a {@link BigDecimal} field is greater than or equal 
   to <tt>aMinimumValue</tt>. 
  */
  public static Validator min(final BigDecimal aMinimumValue){
    class Minimum implements Validator {
      Minimum(BigDecimal aMinValue){
        fMinValue = aMinValue;
      }
      public boolean isValid(Object aObject){
        BigDecimal value = (BigDecimal)aObject;
        return value.compareTo(fMinValue) >= 0;
      }
      private final BigDecimal fMinValue;
    }
    return new Minimum(aMinimumValue);
  }

  /**
   Return a {@link Validator} to check that a {@link Decimal} amount is greater than or equal 
   to <tt>aMinimumValue</tt>. This methods allows comparisons between 
   <tt>Money</tt> objects of different currency.
  */
  public static Validator min(final Decimal aMinimumValue){
    class Minimum implements Validator {
      Minimum(Decimal aMinValue){
        fMinValue = aMinValue;
      }
      public boolean isValid(Object aObject){
        Decimal value = (Decimal)aObject;
        return value.gteq(fMinValue);
      }
      private final Decimal fMinValue;
    }
    return new Minimum(aMinimumValue);
  }
  
  /**
   Return a {@link Validator} to check that a {@link DateTime} is greater than or equal 
   to <tt>aMinimumValue</tt>. 
  */
  public static Validator min(final DateTime aMinimumValue){
    class Minimum implements Validator {
      Minimum(DateTime aMinValue){
        fMinValue = aMinValue;
      }
      public boolean isValid(Object aObject){
        DateTime value = (DateTime)aObject;
        return value.gteq(fMinValue);
      }
      private final DateTime fMinValue;
    }
    return new Minimum(aMinimumValue);
  }

  /**
   Return a {@link Validator} to check that a field's value is less than or equal to <tt>aMaximumValue</tt>. 
   See class comment for the kind of objects which may be checked by the returned {@link Validator}.
  */
  public static Validator max(final long aMaximumValue){
    class Maximum implements Validator {
      Maximum(long aMaxValue){
        fMaxValue = aMaxValue;
      }
      public boolean isValid(Object aObject){
        long value = getValueAsLong(aObject);
        return value <= fMaxValue;
      }
      private final long fMaxValue;
    }
    return new Maximum(aMaximumValue);
  }
  
  /**
   Return a {@link Validator} to check that a {@link BigDecimal} field is less than or equal 
   to <tt>aMaximumValue</tt>. 
  */
  public static Validator max(final BigDecimal aMaximumValue){
    class Maximum implements Validator {
      Maximum(BigDecimal aMaxValue){
        fMaxValue = aMaxValue;
      }
      public boolean isValid(Object aObject){
        BigDecimal value = (BigDecimal)aObject;
        return value.compareTo(fMaxValue) <= 0;
      }
      private final BigDecimal fMaxValue;
    }
    return new Maximum(aMaximumValue);
  }
  
  /**
   Return a {@link Validator} to check that a {@link Decimal} amount is less than or equal 
   to <tt>aMaximumValue</tt>. This methods allows comparisons between 
   <tt>Money</tt> objects of different currency.
  */
  public static Validator max(final Decimal aMaximumValue){
    class Maximum implements Validator {
      Maximum(Decimal aMaxValue){
        fMaxValue = aMaxValue;
      }
      public boolean isValid(Object aObject){
        Decimal value = (Decimal)aObject;
        return value.lteq(fMaxValue);
      }
      private final Decimal fMaxValue;
    }
    return new Maximum(aMaximumValue);
  }

  /**
  Return a {@link Validator} to check that a {@link DateTime} is less than or equal 
  to <tt>aMaximumValue</tt>. 
  */
  public static Validator max(final DateTime aMaximumValue){
    class Maximum implements Validator {
      Maximum(DateTime aMaxValue){
        fMaxValue = aMaxValue;
      }
      public boolean isValid(Object aObject){
        DateTime value = (DateTime)aObject;
        return value.lteq(fMaxValue);
      }
      private final DateTime fMaxValue;
    }
    return new Maximum(aMaximumValue);
  }
  
  /**
   Return a {@link Validator} to check that a field's value is in a certain range (inclusive). 
   See class comment for the kind of objects which may be checked by the returned {@link Validator}.
  */
  public static Validator range(final long aMinimumValue, final long aMaximumValue){
    class Range implements Validator {
      Range(long aMin, long aMax){
        fMinValue = aMin;
        fMaxValue = aMax;
      }
      public boolean isValid(Object aObject){
        long value = getValueAsLong(aObject);
        return fMinValue <= value && value <= fMaxValue;
      }
      private final long fMinValue;
      private final long fMaxValue;
    }
    return new Range(aMinimumValue, aMaximumValue);
  }

  /**
   Return a {@link Validator} to check that a {@link BigDecimal} value is in a certain range (inclusive). 
  */
  public static Validator range(final BigDecimal aMinimumValue, final BigDecimal aMaximumValue){
    class Range implements Validator {
      Range(BigDecimal aMin, BigDecimal aMax){
        fMinValue = aMin;
        fMaxValue = aMax;
      }
      public boolean isValid(Object aObject){
        BigDecimal value = (BigDecimal)aObject;
        return value.compareTo(fMinValue) >= 0 && value.compareTo(fMaxValue) <= 0;
      }
      private final BigDecimal fMinValue;
      private final BigDecimal fMaxValue;
    }
    return new Range(aMinimumValue, aMaximumValue);
  }
  
  /**
   Return a {@link Validator} to check that a {@link Decimal} amount is in a certain range (inclusive). 
   This methods allows comparisons between <tt>Money</tt> objects of different currency.
  */
  public static Validator range(final Decimal aMinimumValue, final Decimal aMaximumValue){
    class Range implements Validator {
      Range(Decimal aMin, Decimal aMax){
        fMinValue = aMin;
        fMaxValue = aMax;
      }
      public boolean isValid(Object aObject){
        Decimal value = (Decimal)aObject;
        return value.gteq(fMinValue) && value.lteq(fMaxValue);
      }
      private final Decimal fMinValue;
      private final Decimal fMaxValue;
    }
    return new Range(aMinimumValue, aMaximumValue);
  }

  /**
   Return a {@link Validator} to check that a {@link DateTime} is in a certain range (inclusive). 
  */
  public static Validator range(final DateTime aMinimumValue, final DateTime aMaximumValue){
    class Range implements Validator {
      Range(DateTime aMin, DateTime aMax){
        fMinValue = aMin;
        fMaxValue = aMax;
      }
      public boolean isValid(Object aObject){
        DateTime value = (DateTime)aObject;
        boolean isInRange = value.gt(fMinValue) && value.lt(fMaxValue);
        boolean isAtAnEndpoint = value.equals(fMinValue) || value.equals(fMaxValue);
        return isInRange || isAtAnEndpoint;
      }
      private final DateTime fMinValue;
      private final DateTime fMaxValue;
    }
    return new Range(aMinimumValue, aMaximumValue);
  }
  
  /**
   Return a {@link Validator} to check that the number of decimal places of a {@link Decimal} or 
   {@link BigDecimal} is <em>less than or equal to</em> <tt>aMaxNumberOfDecimalPlaces</tt>. 
   
   @param aMaxNumberOfDecimalPlaces is greater than or equal to <tt>1</tt>.
  */
  public static Validator numDecimalsMax(final int aMaxNumberOfDecimalPlaces){
    Args.checkForPositive(aMaxNumberOfDecimalPlaces);
    class MaxNumPlaces implements Validator {
      MaxNumPlaces(int aMaxNumberOfDecimals){
        Args.checkForPositive(aMaxNumberOfDecimals);
        fMaxNumPlaces = aMaxNumberOfDecimals;
      }
      public boolean isValid(Object aObject){
        return Util.hasMaxDecimals(extractNumber(aObject), fMaxNumPlaces);
      }
      private final int fMaxNumPlaces;
    }
    return new MaxNumPlaces(aMaxNumberOfDecimalPlaces);
  }
  
  /**
   Return a {@link Validator} to check that the number of decimal places of a {@link Decimal} 
   or {@link BigDecimal} is <em>exactly equal to</em> <tt>aNumDecimals</tt>.
    
   @param aNumDecimals is 0 or more.
  */
  public static Validator numDecimalsAlways(final int aNumDecimals){
    class NumPlaces implements Validator {
      NumPlaces(int aNumberOfDecimalPlaces){
        fNumPlaces = aNumberOfDecimalPlaces;
      }
      public boolean isValid(Object aObject){
        return Util.hasNumDecimals(extractNumber(aObject), fNumPlaces);
      }
      private final int fNumPlaces;
    }
    return new NumPlaces(aNumDecimals);
  }
  
  /**
   Return a {@link Validator} that checks a {@link String} or {@link SafeText} 
   field versus a regular expression {@link Pattern}.
   
   <P>This method might be used to validate a zip code, phone number, and so on - any text which has a 
   well defined format.
   
   <P>There must be a complete match versus the whole text, as in {@link Matcher#matches()}.
   In addition, the returned {@link Validator} will not trim the text before performing the validation.
   
   @param aRegex is a {@link Pattern}, which holds a regular expression. 
  */
  public static Validator pattern(final Pattern aRegex){
    class PatternValidator implements Validator {
      PatternValidator(Pattern aSomeRegex){
        fRegex = aSomeRegex;
      }
      public boolean isValid(Object aObject) {
          Matcher matcher = fRegex.matcher(getText(aObject));
          return matcher.matches();
      }
      private final Pattern fRegex;
    }
    return new PatternValidator(aRegex);
  }
  
  /**
   Return a {@link Validator} to verify a {@link String} or {@link SafeText} field is a syntactically 
   valid email address.
   
   <P>See {@link WebUtil#isValidEmailAddress(String)}. The text is not trimmed by the returned 
   {@link Validator}.
  */
  public static Validator email(){
    class EmailValidator implements Validator {
      public boolean isValid(Object aObject) {
        return WebUtil.isValidEmailAddress(getText(aObject));
      }
    }
    return new EmailValidator();
  }
  
  /** 
    Return a {@link Validator} to check that a {@link String} or {@link SafeText} field is not spam, 
   according to {@link SpamDetector}.   
  */
  public static Validator forSpam(){ 
    class SpamValidator implements Validator {
      public boolean isValid(Object aObject) {
        SpamDetector spamDetector = BuildImpl.forSpamDetector();
        return !spamDetector.isSpam(getText(aObject));
      }
    }
    return new SpamValidator();
  }
  
  /*
   Note : forURL() method was attempted, but abandoned. 
   The JDK implementation of the URL constructor seems very flaky.
  */

  /**
   This no-argument constructor is empty. 
   
   <P>This constructor exists only because of it has <tt>protected</tt> scope. 
   Having <tt>protected</tt> scope has two desirable effects:
   <ul>
   <li>typical callers cannot create <tt>Check</tt> objects. This is appropriate since this class contains 
   only static methods.
   <li>if needed, this class may be subclassed. This is useful when you need to add custom validations.
   </ul>
  */
  protected Check(){
    //empty - prevent construction by caller, but allow it for subclasses
  }
  
  // PRIVATE //

  private static final Logger fLogger = Util.getLogger(Check.class);
  
  private static boolean passesValidations(Object aObject, Validator... aValidators) {
    boolean result = PASSES;
    for(Validator validator: aValidators){
      if ( ! validator.isValid(aObject) ) {
        result = FAILS;
        fLogger.fine("Failed a validation.");
        break;
      }
    }
    return result;
  }

  private static long getValueAsLong(Object aObject){
    long result = 0;
    if ( aObject instanceof Integer){
      Integer value = (Integer)aObject;
      result = value.intValue();
    }
    else if (aObject instanceof Long) {
      Long value = (Long)aObject;
      result = value.longValue();
    }
    else if (aObject instanceof String){
      String text = (String)aObject;
      if ( Util.textHasContent(text) ) {
        result = text.trim().length();
      }
    }
    else if (aObject instanceof Id){
      Id id = (Id)aObject;
      if ( Util.textHasContent(id.toString()) ) {
        result = id.toString().trim().length();
      }
    }
    else if (aObject instanceof SafeText){
      SafeText text = (SafeText)aObject;
      if ( Util.textHasContent(text.getRawString()) ) {
        result = text.getRawString().trim().length();
      }
    }
    else if (aObject instanceof Code){
      Code code = (Code)aObject;
      if ( Util.textHasContent(code.getText()) ) {
        result = code.getText().getRawString().trim().length();
      }
    }
    else if (aObject instanceof Collection) {
      Collection collection = (Collection)aObject;
      result = collection.size();
    }
    else if (aObject instanceof Map) {
      Map map = (Map)aObject;
      result = map.size();
    }
    else if (aObject instanceof Date) {
      Date date = (Date)aObject;
      result = date.getTime(); 
    }
    else if (aObject instanceof Calendar){
      Calendar calendar = (Calendar)aObject;
      result = calendar.getTimeInMillis();
    }
    else {
      String message = "Unexpected type of Object: " + aObject.getClass().getName();
      fLogger.severe(message);
      throw new AssertionError(message);
    }
    return result;
  }
  
  private static boolean isText(Object aObject){
    return (aObject instanceof String) || (aObject instanceof SafeText);
  }
  
  private static String getText(Object aObject){
    String result = null;
    if ( aObject instanceof String ){
      String text = (String) aObject;
      result = text.toString();
    }
    else if (aObject instanceof SafeText ){
      SafeText text = (SafeText)aObject;
      result = text.getRawString();
    }
    return result;
  }
  
  /** aObject must be a BigDecimal or a Money object.  */
  private static BigDecimal extractNumber(Object aObject){
    BigDecimal result = null;
    if( aObject instanceof BigDecimal){
      result = (BigDecimal)aObject;
    }
    else if (aObject instanceof Decimal) {
      Decimal decimal = (Decimal)aObject;
      result = decimal.getAmount();
    }
    else {
      throw new IllegalArgumentException("Unexpected class: " + aObject.getClass());
    }
    return result;
  }
}
