/*
**  Criterion.m
**
**  Copyright (c) 2004
**
**  Author: Yen-Ju Chen <yjchenx@hotmail.com>
**
**  This program is free software; you can redistribute it and/or modify
**  it under the terms of the GNU General Public License as published by
**  the Free Software Foundation; either version 2 of the License, or
**  (at your option) any later version.
**
**  This program is distributed in the hope that it will be useful,
**  but WITHOUT ANY WARRANTY; without even the implied warranty of
**  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
**  GNU General Public License for more details.
**
**  You should have received a copy of the GNU General Public License
**  along with this program; if not, write to the Free Software
**  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include "Criterion.h"
#include "RegEx.h"
#include "GNUstep.h"
#include "Constants.h"
#include "ContentIndexer.h"

static NSString *CriterionMatchAllKey = @"CriterionMatchAllKey";
static NSString *CriterionAllCriterionsKey = @"CriterionaAllCriterionsKey";

@interface NSString (SubjectComparison)
- (NSComparisonResult) sortSubject: (NSString *) another;
@end

@implementation NSString (SubjectComparison)
- (NSComparisonResult) sortSubject: (NSString *) another
{
  BOOL selfHasPrefix = [self hasPrefix: @"-"];
  BOOL anotherHasPrefix = [another hasPrefix: @"-"];
  if (selfHasPrefix == anotherHasPrefix)
    {
      return [self compare: another];
    }
  else if (selfHasPrefix) /* and another don't have prefix */
    {
      return NSOrderedDescending;
    }
  else
    {
      return NSOrderedAscending;
    }
}
@end


@implementation Criterion

- (void) setCriterion: (Criterion *) another
{
  NSArray *allSubjects;
  unsigned int i, count;
  id subject;
  CriterionType type;

  [self loadPropertyList: [another propertyList]];

  /* set available subject */
  allSubjects = [another subjectsToMatch];
  count = [allSubjects count];
  for(i = 0; i < count; i++)
    {
      subject = [allSubjects objectAtIndex: i];
      type = [another typeOfSubject: subject];
      [self  addAvailableSubject: subject forType: type];
    }
}

/* add available subject */
- (void) removeAllAvailableSubjects
{
  [anyStringSet removeAllObjects];
  [stringSet removeAllObjects];
  [dateSet removeAllObjects];
  [numberSet removeAllObjects];
  [selectionSet removeAllObjects];
  [contentSet removeAllObjects];
}

- (void) addAvailableSubject: (NSString *) subject
                     forType: (CriterionType) type
{
  switch(type)
    {
      case CriterionAnyStringType:
	[anyStringSet addObject: AUTORELEASE([subject copy])];
	break;
      case CriterionStringType:
	[stringSet addObject: AUTORELEASE([subject copy])];
	break;
      case CriterionDateType:
	[dateSet addObject: AUTORELEASE([subject copy])];
	break;
      case CriterionNumberType:
	[numberSet addObject: AUTORELEASE([subject copy])];
	break;
      case CriterionSelectionType:
	[selectionSet addObject: AUTORELEASE([subject copy])];
	break;
      case CriterionContentType:
	[contentSet addObject: AUTORELEASE([subject copy])];
	break;
    }
}

- (void) setAvailableSelections: (NSArray *) array
                     forSubject: (NSString *) subject
{
  if (array == nil)
    [selections removeObjectForKey: subject];
  else
    [selections setObject: AUTORELEASE([array copy])
	          forKey: AUTORELEASE([subject copy])];
}

/* read criterion */
- (unsigned int) numberOfCriterionsInView: (CriterionView *) view
{
  return [criterions count];
}

- (NSArray *) availableSubjectsOfCriterionsInView: (CriterionView *) view
{
  NSArray *array1, *array2;
  NSMutableSet *allSet = [[NSMutableSet alloc] init];
  [allSet unionSet: anyStringSet];
  [allSet unionSet: stringSet];
  [allSet unionSet: dateSet];
  [allSet unionSet: numberSet];
  [allSet unionSet: selectionSet];
  [allSet unionSet: contentSet];
  array1 = [allSet allObjects];
  AUTORELEASE(allSet);
  array2 = [array1 sortedArrayUsingSelector:@selector(sortSubject:)];

  return array2;
}

- (NSString *) criterionView: (CriterionView *) view
               subjectAtIndex: (unsigned int) index
{
  if (index < [criterions count])
    {
      return AUTORELEASE([[[criterions objectAtIndex: index] objectAtIndex: 0] copy]);
    }
}

- (CriterionType) criterionView: (CriterionView *) view
                  typeOfSubject: (NSString *) subject
{
  return [self typeOfSubject: subject];
}

- (CriterionType) typeOfSubject: (NSString *) subject
{
  if ([anyStringSet containsObject: subject])
    return CriterionAnyStringType;
  else if ([stringSet containsObject: subject])
    return CriterionStringType;
  else if ([dateSet containsObject: subject])
    return CriterionDateType;
  else if ([numberSet containsObject: subject])
    return CriterionNumberType;
  else if ([selectionSet containsObject: subject])
    return CriterionSelectionType;
  else if ([contentSet containsObject: subject])
    return CriterionContentType;
  else
    {
      //NSLog(@"Internal Error: unknown criterion type");
      return CriterionAnyStringType;
    }
}

- (NSArray *) criterionView: (CriterionView *) view
              availableSelectionsForSubject: (NSString *) subject
{
  NSArray *array = [selections objectForKey: subject];
  return AUTORELEASE([array copy]);
}

- (CriterionVerb) criterionView: (CriterionView *) view
                    verbAtIndex: (unsigned int) index
{
  if (index < [criterions count])
    {
      return [[[criterions objectAtIndex: index] objectAtIndex: 1] intValue];
    }
}

- (id) criterionView: (CriterionView *) view
       objectAtIndex: (unsigned int) index
{
  if (index < [criterions count])
    {
      return AUTORELEASE([[[criterions objectAtIndex: index] objectAtIndex: 2] copy]);
    }
}


/* accessories */
- (void) addCriterionWithSubject: (NSString *) subject
                            verb: (CriterionVerb) verb
                          object: (id) object
{
  if ((subject == nil) || (object == nil) || 
      (verb < 1) || (verb >= CriterionVerbCount))
    {
      NSLog(@"Invalide criterion: Cannot add criterion");
      return;
    }
  else
    {
      //NSLog(@"Criterion add criterion");
      NSArray *c = [NSArray arrayWithObjects: AUTORELEASE([subject copy]),
					      [NSNumber numberWithInt: verb],
					      AUTORELEASE([object copy]),
					      nil];
      [criterions addObject: c];
    }
}

- (void) insertCriterionWithSubject: (NSString *) subject
                               verb: (CriterionVerb) verb
                             object: (id) object
                            atIndex: (unsigned int) index
{
  if ((subject == nil) || (object == nil) || 
      (verb < 1) || (verb >= CriterionVerbCount) ||
      (index == NSNotFound))
    {
      NSLog(@"Invalide criterion: Cannot add criterion");
      return;
    }
  else if (index >= [criterions count])
    {
      [self addCriterionWithSubject: subject
	                       verb: verb
			     object: object];
    }
  else 
    {
      //NSLog(@"Criterion insert in %d", index);
      NSArray *c = [NSArray arrayWithObjects: AUTORELEASE([subject copy]),
					      [NSNumber numberWithInt: verb],
					      AUTORELEASE([object copy]),
					      nil];
      [criterions insertObject: c atIndex: index];
    }
}


- (void) removeCriterionAtIndex: (unsigned int) index
{
  if ((index == NSNotFound) || (index >= [criterions count]))
    {
      NSLog(@"Invalide criterion index: Cannot remove criterion");
      return;
    }
  else
    {
      [criterions removeObjectAtIndex: index];
    }

}

- (void) setCriterionWithSubject: (NSString *) subject
                            verb: (CriterionVerb) verb
                          object: (id) object
                         atIndex: (unsigned int) index
{
  if ((index == NSNotFound) || (index >= [criterions count]) ||
      (subject == nil) || (object == nil) || 
      (verb < 1) || (verb >= CriterionVerbCount))
    {
      NSLog(@"Invalide criterion: Cannot set criterion");
      return;
    }
  else
    {
      NSArray *c = [NSArray arrayWithObjects: AUTORELEASE([subject copy]),
					      [NSNumber numberWithInt: verb],
					      AUTORELEASE([object copy]),
					      nil];

      [criterions replaceObjectAtIndex: index withObject: c];
      //NSLog(@"Set %@ %d %@ index %d", subject, verb, object, index);
    }
}

- (id) propertyList
{
  return [NSDictionary dictionaryWithObjectsAndKeys:
	            [NSNumber numberWithBool: matchAll], CriterionMatchAllKey,
                   AUTORELEASE([criterions copy]), CriterionAllCriterionsKey,
		   nil];
}

- (void) loadPropertyList: (id) propertyList
{
  if ([propertyList isKindOfClass: [NSDictionary class]] == NO)
    {
      NSLog(@"Internal Error: Property list for criterions is not valide");
    }
  NSDictionary *dict = propertyList;
  NSArray *array;
  matchAll = [[dict objectForKey: CriterionMatchAllKey] boolValue];
  array = [dict objectForKey: CriterionAllCriterionsKey];
  [criterions setArray: AUTORELEASE([array copy])];
}

- (unsigned int) count
{
  [criterions count];
}

/* match criterion */
- (void) setMatchAll: (BOOL) match
{
  matchAll = match;
}

- (BOOL) matchAll
{
  return matchAll;
}

- (NSArray *) subjectsToMatch
{
  NSMutableArray *array = [[NSMutableArray alloc] init];
  NSString *subject;
  unsigned int i, count = [criterions count];
  for(i = 0; i < count; i++)
    {
      subject = [[criterions objectAtIndex: i] objectAtIndex: 0];
      [array addObject: AUTORELEASE([subject copy])];
    }
  return AUTORELEASE(array);
}

/* ignore subject */
- (BOOL) _matchCriterionAtIndex: (unsigned int) index
                     withObject: (id) o
{
  CriterionVerb verb = [[[criterions objectAtIndex: index] objectAtIndex: 1] intValue];
  id first, second;
  id object = [[criterions objectAtIndex: index] objectAtIndex: 2];
  unsigned int length;
  NSRange range;
  NSComparisonResult comparisonResult;

  /* Cache */
  Class stringClass, dateClass, numberClass, arrayClass;
  stringClass = [NSString class];
  dateClass = [NSDate class];
  numberClass = [NSNumber class];
  arrayClass = [NSArray class];

  if (([object isKindOfClass: stringClass]) &&
      ([o isKindOfClass: stringClass]))
    {
      length = [(NSString *)o length];
      range = [o rangeOfString: object options: NSCaseInsensitiveSearch]; 
      switch (verb)
        {
  	  case CriterionContainsVerb:
	    if (range.location != NSNotFound)
	      {
	        //NSLog(@"If %@ contains %@", o, object);
                return YES;
	      }
	    break;
	  case CriterionDoesNotContainVerb:
	    if (range.location == NSNotFound)
	      {
	        //NSLog(@"If %@ does not contains %@", o, object);
                return YES;
	      }
	    break;
	  case CriterionIsVerb:
	    if ((range.location == 0) && (range.length == length))
	      {
	        //NSLog(@"If %@ is %@", o, object);
                return YES;
	      }
	    break;
	  case CriterionIsNotVerb:
	    if ((range.location == NSNotFound) ||
		(range.location != 0) ||
		(range.length != length))
	      {
	        //NSLog(@"If %@ is  not %@", o, object);
                return YES;
	      }
	    break;
	  case CriterionStartsWithVerb:
	    if (range.location == 0)
	      {
	        //NSLog(@"If %@ starts with %@", o, object);
                return YES;
	      }
	    break;
	  case CriterionEndsWithVerb:
	    if (NSMaxRange(range) == length)
	      {
	        //NSLog(@"If %@ ends with %@", o, object);
                return YES;
	      }
	    break;
	  case CriterionMatchRegularExpressionVerb:
	    {
	      range = [RegExParser rangeOfString: object
		                        inString: o];
	      if (range.location != NSNotFound)
	        {
	          //NSLog(@"If %@ match regex %@", o, object);
                  return YES;
	        }
	      break;
	    }
        }
    }
  else if (([object isKindOfClass: stringClass]) &&
           ([o isKindOfClass: arrayClass]))
    {
      switch (verb)
        {
  	  case CriterionIsVerb:
	    if ([o containsObject: object])
	      return YES;
	    break;
	  case CriterionIsNotVerb:
	    if ([o containsObject: object] == NO)
	      return YES;
	    break;
	}
    }
  else if (([object isKindOfClass: dateClass]) &&
           ([o isKindOfClass: dateClass]))
    {
      comparisonResult = [(NSDate *)o compare: object];
      switch (verb)
        {
  	  case CriterionIsVerb:
	    if (comparisonResult == NSOrderedSame)
	      {
	        //NSLog(@"If %@ is %@", o, object);
                return YES;
	      }
	    break;
	  case CriterionIsNotVerb:
	    if (comparisonResult != NSOrderedSame)
	      {
	        //NSLog(@"If %@ is not %@", o, object);
                return YES;
	      }
	    break;
	  case CriterionIsAfterVerb:
	    if (comparisonResult == NSOrderedDescending)
	      {
	        //NSLog(@"If %@ is after %@", o, object);
                return YES;
	      }
	    break;
	  case CriterionIsBeforeVerb:
	    if (comparisonResult == NSOrderedAscending)
	      {
	        //NSLog(@"If %@ is before %@", o, object);
                return YES;
	      }
	    break;
	}
    }
  else if (([object isKindOfClass: arrayClass]) &&
           ([o isKindOfClass: dateClass]))
    {
      switch (verb)
        {
  	  case CriterionIsInTheLastVerb:
	  case CriterionIsNotInTheLastVerb:
  	    {
              first = [object objectAtIndex: 0];
              second = [object lastObject];
              if (([first isKindOfClass: numberClass]) &&
                  ([second isKindOfClass: numberClass]))
              {
		NSCalendarDate *before, *current = [NSCalendarDate date];
	        CriterionDateUnit unit = [second intValue];
		int count = [first intValue];
	        NSTimeInterval interval;
		NSTimeInterval since = [current timeIntervalSinceDate: o];
	        switch (unit)
	          {
		    case CriterionSecondUnit:
		      interval = count*1;
		      break;
		    case CriterionMinuteUnit:
		      interval = count*60;
		      break;
		    case CriterionHourUnit:
		      interval = count*60*60;
		      break;
		    case CriterionDayUnit:
		      interval = count*60*60*24;
		      break;
		    case CriterionWeekUnit:
		      interval = count*60*60*24*7;
		      break;
		    case CriterionMonthUnit:
                      before = [current dateByAddingYears: 0
				                   months: -count
				                     days: 0
					            hours: 0
						  minutes: 0
					          seconds: 0];
		      interval = [current timeIntervalSinceDate: before];
		      break;
		    case CriterionYearUnit:
                      before = [current dateByAddingYears: -count
				                   months: 0
				                     days: 0
					            hours: 0
						  minutes: 0
					          seconds: 0];
		      interval = [current timeIntervalSinceDate: before];
		      break;
	          }

		if (since < interval) 
		  {
		    if (verb == CriterionIsInTheLastVerb)
		      {
			//NSLog(@"%@ is in the last %@", o, object);
		        return YES;
		      }
		  }
		else
		  {
		    if (verb == CriterionIsNotInTheLastVerb)
		      {
			//NSLog(@"%@ is not in the last %@", o, object);
		        return YES;
		      }
		  }
	      }
	      break;
	    }
	  case CriterionIsInTheRangeVerb:
            first = [object objectAtIndex: 0];
            second = [object lastObject];
            if (([first isKindOfClass: dateClass]) &&
                ([second isKindOfClass: dateClass]))
              {
                comparisonResult = [(NSDate *)first compare: second];
		if (([(NSDate *)o compare: first] == NSOrderedDescending) &&
		    ([(NSDate *)o compare: second] == NSOrderedAscending) &&
		    (comparisonResult == NSOrderedAscending))
		  {
	            //NSLog(@"%@ is in the rage from %@ to %@", o, first, second);
	            return YES;
		  }
		else if (([(NSDate *)o compare: first] == NSOrderedAscending) &&
		         ([(NSDate *)o compare: second] == NSOrderedDescending) &&
			 (comparisonResult == NSOrderedDescending))
		  {
	            //NSLog(@"%@ is in the rage from %@ to %@", o, first, second);
		    return YES;
		  }
	      }
	    break;
	}
    }
  else if (([object isKindOfClass: numberClass]) &&
           ([o isKindOfClass: numberClass]))
    {
      comparisonResult = [(NSNumber *)o compare: object];
      switch (verb)
        {
  	  case CriterionIsVerb:
	    if (comparisonResult == NSOrderedSame)
	      {
		//NSLog(@"%@ is %@", o, object);
	        return YES;
	      }
	    break;
	  case CriterionIsNotVerb:
	    if (comparisonResult != NSOrderedSame)
	      {
		//NSLog(@"%@ is not %@", o, object);
	        return YES;
	      }
	    break;
	  case CriterionIsGreaterThanVerb:
	    if (comparisonResult == NSOrderedDescending)
	      {
		//NSLog(@"%@ is greater than %@", o, object);
	        return YES;
	      }
	    break;
	  case CriterionIsLessThanVerb:
	    if (comparisonResult == NSOrderedAscending)
	      {
		//NSLog(@"%@ is less than %@", o, object);
	        return YES;
	      }
	    break;
	}
    }
  else if (([object isKindOfClass: arrayClass]) &&
           ([o isKindOfClass: numberClass]))
    {
      first = [object objectAtIndex: 0];
      second = [object lastObject];
      if (([first isKindOfClass: numberClass]) &&
          ([second isKindOfClass: numberClass]))
        {
          comparisonResult = [(NSNumber *)first compare: second];
          switch (verb)
            {
  	      case CriterionIsInTheRangeVerb:
		if (([(NSNumber *)o compare: first] == NSOrderedDescending) &&
		    ([(NSNumber *)o compare: second] == NSOrderedAscending) &&
		    (comparisonResult == NSOrderedAscending))
	  	  {
		    //NSLog(@"%@ is in the range %@ to %@", o, first, second);
	            return YES;
		  }
		else if (([(NSNumber *)o compare: first] == NSOrderedAscending) &&
		         ([(NSNumber *)o compare: second] == NSOrderedDescending) &&
			 (comparisonResult == NSOrderedDescending))
		  {
		    //NSLog(@"%@ is in the range %@ to %@", o, first, second);
		    return YES;
		  }
		break;
	    }
	}
    }
  return NO;
}

-(BOOL) _matchCriterionAtIndex: (unsigned int) index
             withContentAtPath: (id) o
{
  //NSLog(@"Criterion: _matchCriterionAtIndex %d withContentAtPath %@", index, o);
  id object = [[criterions objectAtIndex: index] objectAtIndex: 2];
  NSArray *files = [[ContentIndexer sharedIndexer] filesContain: object];
  
  if ([files containsObject: o])
    return YES;
  else
    return NO;
}

- (BOOL) match: (NSDictionary *) information
{
  unsigned int i, icount = [criterions count];
  unsigned int j, jcount;
  NSString *title;
  CriterionType criterionType;
  BOOL result, jresult;
  NSArray *allKeys;
  id key, object;

  if (matchAll == YES)
    {
      //NSLog(@"Match All %d", count);
      result = NO;
      for(i = 0; i < icount; i++)
        {
           title = [[criterions objectAtIndex: i] objectAtIndex: 0];
           criterionType = [self typeOfSubject: title];
	   //NSLog(@"Title %@, type %d", title, criterionType);
	   if (criterionType == CriterionAnyStringType)
	     {
	       /* Go throught all information */
	       allKeys = [information allKeys];
	       jcount = [allKeys count];
	       jresult = NO;
	       for(j = 0; j < jcount; j++)
	         {
		   key = [allKeys objectAtIndex: j];
	           object = [information objectForKey: key];
                   jresult = [self _matchCriterionAtIndex: i 
		                              withObject: object];
		   if (jresult == YES)
		     {
		       result = YES;
		       break;
		     }
	         }
	       if (jresult == NO)
		 return NO;
	     }
	   else
	     {
	       object = [information objectForKey: title];
	       //NSLog(@"title %@, object %@", title, object);
	       if (object)
                 {
	           //if ([title isEqualToString: sFileContent])
	           if (criterionType == CriterionContentType)
	             {
	               result = [self _matchCriterionAtIndex: i
			                   withContentAtPath: object];
	             }
	           else
	            {
                       result = [self _matchCriterionAtIndex: i 
			                          withObject: object];
	               //NSLog(@"Match <%@> (%@) %d", title, object, result);
		    }

                   if (result == NO) 
	             {
	               //NSLog(@"%@ match NO", title);
	               return NO;
	             }
	           result = YES;
	         }
	       else
	         {
	           return NO; /* attributes missing */
	         }
	     }
         }
      return result;
    }
  else
    {
      //NSLog(@"Match Any %d", count);
      result = NO;
      for(i = 0; i < icount; i++)
        {
           title = [[criterions objectAtIndex: i] objectAtIndex: 0];
           criterionType = [self typeOfSubject: title];
	   if (criterionType == CriterionAnyStringType)
	     {
	       /* Go throught all information */
	       allKeys = [information allKeys];
	       jcount = [allKeys count];
	       jresult = NO;
	       for(j = 0; j < jcount; j++)
	         {
		   key = [allKeys objectAtIndex: j];
	           object = [information objectForKey: key];
                   jresult = [self _matchCriterionAtIndex: i 
		                              withObject: object];
		   if (jresult == YES)
		     {
		       return YES;
		     }
	         }
	       if (jresult == NO)
		 result = NO;
	     }
	   else
	     {
	       object = [information objectForKey: title];
	       if (object)
                 {
	           if (criterionType == CriterionContentType)
	             {
	               result = [self _matchCriterionAtIndex: i
		  	                   withContentAtPath: object];
	             }
	           else
	             {
	               //NSLog(@"Match <%@> (%@)", title, object);
                       result = [self _matchCriterionAtIndex: i 
			                          withObject: object];
		     }
                   if (result == YES) 
	             {
	               //NSLog(@"%@ match YES", title);
	               return YES;
	             }
	           result = NO;
	         }
	       else
	         {
	           /* Attribute missing */
	           result = NO;
	         }
	     }
         }
      return result;
    }
  return NO;
}

- (id) init
{
  self = [super init];
  criterions = [[NSMutableArray alloc] init];
  selections = [[NSMutableDictionary alloc] init];
  anyStringSet = [[NSMutableSet alloc] init];
  stringSet = [[NSMutableSet alloc] init];
  dateSet = [[NSMutableSet alloc] init];
  numberSet = [[NSMutableSet alloc] init];
  selectionSet = [[NSMutableSet alloc] init];
  contentSet = [[NSMutableSet alloc] init];
  matchAll = NO;
  return self;
}

- (void) dealloc
{
  RELEASE(criterions);
  RELEASE(selections);
  RELEASE(anyStringSet);
  RELEASE(stringSet);
  RELEASE(dateSet);
  RELEASE(numberSet);
  RELEASE(selectionSet);
  RELEASE(contentSet);
  [super dealloc];
}

@end
