/*
 Oaxaca: Useful Cocoa Library
 Copyright (C) 2005, Niko Matsakis (niko@alum.mit.edu)
 
 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#import "OACategories.h"
#import "OAMacros.h"
#import "OAReference.h"

void appendMapped(id result, id iterable, SEL selector) {
    FOREACH(id, item, iterable) {
        [result addObject:[item performSelector:selector]];
    } ENDFOREACH;
}

void appendMappedWithObject(id result, id iterable, SEL selector, id object) {
    FOREACH(id, item, iterable) {
        [result addObject:[item performSelector:selector withObject:object]];
    } ENDFOREACH;
}

NSMutableArray *mapToArray(id iterable, SEL selector) {
    NSMutableArray *res = [NSMutableArray array];
    appendMapped(res, iterable, selector);
    return res;
}

NSMutableArray *mapToArrayWithObject(id iterable, SEL selector, id object) {
    NSMutableArray *res = [NSMutableArray array];
    appendMappedWithObject(res, iterable, selector, object);
    return res;
}

NSMutableSet *mapToSet(id iterable, SEL selector) {
    NSMutableSet *result = [NSMutableSet setWithCapacity:[iterable count]];
    appendMapped(result, iterable, selector);
    return result;
}    

void filterInPlace(id iterable, SEL message) {
    id temp = [iterable copy];
    FOREACH(id, item, temp) {
        if (![[item performSelector:message] boolValue]) {
            [iterable removeObject:item];
        }
    } ENDFOREACH;
}

@implementation NSObject (Oaxaca)

- (BOOL) booleanValueForKeyPath:(NSString*)path
{
    return [[self valueForKeyPath:path] intValue] != 0;
}

@end


@implementation NSDecimalNumber (Oaxaca)

+ (NSDecimalNumber*) negativeOne
{
    return [NSDecimalNumber decimalNumberWithString:@"-1"];
}

- (NSDecimalNumber*) decimalNumberByNegation
{
    return [self decimalNumberByMultiplyingBy:[NSDecimalNumber negativeOne]];
}


@end

@implementation NSNumber (Oaxaca)

// using this is a hack (but a useful one)
+ (NSNumber*) numberWithPointer:(void*)ptr
{
    return [NSNumber numberWithLong:(long)ptr];
}

@end

@implementation NSArray (FlashExtensions)

+ (NSMutableArray*) arrayWithSet:(NSSet*)set
{
    NSMutableArray *array = [NSMutableArray arrayWithCapacity:[set count]];
    [array addAllObjectsFrom:set];
    return array;
}

+ (NSMutableArray*) arrayFromArray:(NSArray*)array withoutObjects:(id)obj, ...
{
    va_list ap;
    NSMutableArray *res = [NSMutableArray arrayWithCapacity:[array count]];
    [res addAllObjectsFrom:array];
    
    va_start(ap, obj);
    id value = obj;
    while (value != nil) {
        [res removeObject:value];
        value = va_arg(ap, id);
    }
    va_end(ap);
    
    return res;
}

void performFilter0 (id list, id result, SEL sel)
{
    NSEnumerator *items = [list objectEnumerator];
    id obj;
    while ( (obj = [items nextObject]) ) {
        if ([[obj performSelector:sel] boolValue]) {
            [result addObject:obj];
        }
    }
}   

void performFilter1 (id list, id result, SEL sel, id arg)
{
    NSEnumerator *items = [list objectEnumerator];
    id obj;
    while ( (obj = [items nextObject]) ) {
        if ([[obj performSelector:sel withObject:arg] boolValue]) {
            [result addObject:obj];
        }
    }
}   

// Invokes setValue:forKeyPath: on all objects with the given values
- (void) setValueOnAllObjects:(id)value forKeyPath:(NSString*)keypath
{
    FOREACH(id, obj, self) {
        [obj setValue:value forKeyPath:keypath];
    } ENDFOREACH;
}

// Returns a new array built by removing all the objects from this array that
// are contained in 'x'.
- (NSMutableArray*) subtractArray:(NSArray *)x
{
    NSMutableArray *result = [NSMutableArray arrayWithCapacity:[self count]];
    NSEnumerator *items = [self objectEnumerator];
    id obj;
    while ( (obj = [items nextObject]) ) {
        if (![x containsObject:obj]) {
            [result addObject:obj];
        }
    }
    return result;
}

- (NSMutableArray*) mapArrayWith:(SEL)sel
{
    return mapToArray(self, sel);
}

- (NSMutableSet*) mapSetWith:(SEL)sel
{
    return mapToSet(self, sel);
}

- (NSMutableArray*) mapArrayWith:(SEL)sel withObject:object
{
    return mapToArrayWithObject(self, sel, object);
}

- (NSMutableArray*) filterArrayWith:(SEL)sel
{
    NSMutableArray *result = [NSMutableArray arrayWithCapacity:[self count]];
    performFilter0 (self, result, sel);
    return result;
}

- (NSMutableArray*) filterArrayWith:(SEL)sel withObject:object
{
    NSMutableArray *result = [NSMutableArray arrayWithCapacity:[self count]];
    performFilter1 (self, result, sel, object);
    return result;
}

- (NSArray*)filteredArrayUsingPredicateFormat:(NSString*)format, ...
{
    va_list ap;
    va_start(ap, format);
    NSPredicate *pred = [NSPredicate predicateWithFormat:format arguments:ap];
    va_end(ap);
    return [self filteredArrayUsingPredicate:pred];
}

- (id) randomObject
{
    int count = [self count];
    if (!count) 
        return nil;
    long index = random () % count;
    return [self objectAtIndex:index];
}

- (NSMutableArray*) zipWithArray:(NSArray*)array
{
    int max = [self count];
    if ([array count] < max)
        max = [array count];
        
    NSMutableArray *res = [NSMutableArray arrayWithCapacity:max];
    for (int i = 0; i < max; i++) {
        NSArray *entry = [NSArray arrayWithObjects:[self objectAtIndex:i], [array objectAtIndex:i], nil];
        [res addObject:entry];
    }
    
    return res;
}

- (id) _0 { return [self objectAtIndex:0]; }
- (id) _1 { return [self objectAtIndex:1]; }
- (id) _2 { return [self objectAtIndex:2]; }

@end

@implementation NSMutableArray (Oaxaca)


// Allows use of mutable arrays like stack
- (void) pushObject:value
{
    [self addObject:value];
}

- (id) popObject 
{
    if ([self count] >= 1) {
        id result = [self lastObject];
        [self removeLastObject];
        return result;
    }
    return nil;
}

- (id) peekObject
{
    return [self lastObject];
}

- (void) addAllObjectsFrom:(id)iterable
{
    FOREACH(id, obj, iterable) {
        [self addObject:obj];
    } ENDFOREACH;
}

- (void) filterInPlace:(SEL)message
{
    filterInPlace(self, message);
}

@end

@implementation NSSet (FlashExtensions)

+ (NSMutableSet*) setWithIntersectionOfSet:(NSSet*)set1 andSet:(NSSet*)set2
{
    NSMutableSet *res = [NSMutableSet setWithCapacity:[set1 count]];
    [res setSet:set1];
    [res intersectSet:set2];
    return res;
}

+ (NSMutableSet*) setBySubtractingArray:(NSArray*)array fromSet:(NSSet*)set
{
    NSMutableSet *res = [NSMutableSet setWithCapacity:[set count]];
    [res setSet:set];
    [res minusArray:array];
    return res;
}

+ (NSMutableSet*) setBySubtractingSet:(NSSet*)set1 fromSet:(NSSet*)set2
{
    NSMutableSet *res = [NSMutableSet setWithCapacity:[set1 count]];
    [res setSet:set2];
    [res minusSet:set1];
    return res;
}

- (NSMutableSet *) mapSetWith:(SEL)sel
{
    NSMutableSet *result = [NSMutableSet setWithCapacity:[self count]];
    appendMapped(result, self, sel);
    return result;
}

- (NSMutableSet *) mapSetWith:(SEL)sel withObject:object
{
    NSMutableSet *result = [NSMutableSet setWithCapacity:[self count]];
    appendMappedWithObject(result, self, sel, object);
    return result;
}

- (NSMutableSet *) filterSetWith:(SEL)sel
{
    NSMutableSet *result = [NSMutableSet setWithCapacity:[self count]];
    performFilter0 (self, result, sel);
    return result;
}

- (NSMutableSet *) filterSetWith:(SEL)sel withObject:object
{
    NSMutableSet *result = [NSMutableSet setWithCapacity:[self count]];
    performFilter1 (self, result, sel, object);
    return result;
}

- (NSSet*)filteredSetUsingPredicate:(NSPredicate*)pred
{
    NSMutableSet *result = [NSMutableSet setWithCapacity:[self count]];
    FOREACH(id, obj, self) {
        if ([pred evaluateWithObject:obj])
            [result addObject:obj];
    } ENDFOREACH;
    return result;
}

- (NSSet*)filteredSetUsingPredicateFormat:(NSString*)format, ...
{
    va_list ap;
    va_start(ap, format);
    NSPredicate *pred = [NSPredicate predicateWithFormat:format arguments:ap];
    va_end(ap);
    return [self filteredSetUsingPredicate:pred];
}

- (id) randomObject
{
    int count = [self count];
    if (!count) 
        return nil;
    long index = random () % count;
    NSEnumerator *enumer = [self objectEnumerator];
    id result = [enumer nextObject];
    while (index--)
        result = [enumer nextObject];
    return result;
}

- (BOOL) containsObjectMatchingPredicate:(NSPredicate*)predicate
{
    FOREACH(id, obj, self) {
        if ([predicate evaluateWithObject:obj])
            return YES;
    } ENDFOREACH;
    return NO;
}

@end

@implementation NSMutableSet (Oaxaca)

- (void) minusArray:(NSArray*)array
{
    FOREACH (id, obj, array) {
        [self removeObject:obj];
    } ENDFOREACH;
}

- (void) addAllObjectsFrom:(id)iterable
{
    FOREACH(id, obj, iterable) {
        [self addObject:obj];
    } ENDFOREACH;
}

- (void) filterInPlace:(SEL)message
{
    filterInPlace(self, message);
}

@end

@implementation NSArrayController (FlashExtensions)

- (id) selectedObject
{
    unsigned int selidx = [self selectionIndex];
    if (selidx != NSNotFound) 
        return [[self arrangedObjects] objectAtIndex:selidx];
    return nil;
}

@end

@implementation NSMutableString (Oaxaca)

- (void) appendSpaces:(int)count
{
    for (int i = 0; i < count; i++)
        [self appendString:@" "];
}

@end

@implementation NSString (FlashExtensions)

- (int) sharedCaseInsensitivePrefixLength:(NSString*)other
{
    return [[self commonPrefixWithString:other options:NSCaseInsensitiveSearch] length];
}

- (int) sharedCaseInsensitiveSuffixLength:(NSString*)other
{
    NSRange range[2];
    int lengths[2];
    
    lengths[0] = [self length];
    lengths[1] = [other length];
    
    int sufflength = 1;
    
    while (sufflength <= lengths[0] && sufflength <= lengths[1]) {
        for (int i = 0; i < 2; i++) {
            range[i].location = lengths[i] - sufflength;
            range[i].length = sufflength;
        }
        
        /* horribly inefficient, but who cares? */
        NSString *othersubset = [other substringWithRange:range[1]];
        NSComparisonResult comp = [self compare:othersubset options:NSCaseInsensitiveSearch range:range[0]];
        if (comp != NSOrderedSame) 
            break;
        
        sufflength += 1;
    }
    
    return sufflength - 1;
}

- (NSMutableString*) stringByPurgingParentheticalText
{
    NSMutableString *result = [[[NSMutableString alloc] initWithCapacity:[self length]] autorelease];
    
    BOOL inparen = NO;
    int len = [self length];
    for (int i = 0; i < len; i++) {
        unichar ch = [self characterAtIndex:i];
        if (inparen) {
            if (ch == ')') {
                inparen = NO;
            }
        }
        else if (ch == '(') {
            inparen = YES;
        }
        else {
            [result appendFormat:@"%C", ch];
        }
    }
    
    return result;
}

- (NSRange) rangeAfterRange:(NSRange)range
{
    return [self rangeAfter:range.location + range.length];
}

- (BOOL) containsString:(NSString*)string
{
    NSRange range = [self rangeOfString:string];
    if (range.location == NSNotFound) return NO;
    return YES;
}

- (NSRange) rangeAfter:(int)index
{
    NSRange result;
    int length = [self length];
    
    if (index < -length)
        result.location = 0;               // i.e., [-100000:] yields entire string
    else if (index < 0)
        result.location = length + index;  // i.e., [-5:] yields last five characters
    else if (index > length)
        result.location = length;          // i.e., [100000:] yields empty string
    else 
        result.location = index;           // i.e., [5:] yields everything after first 5 chars
        
    NSAssert(result.location >= 0 && result.location <= length, @"Invalid range resulted");
    result.length = length - result.location;
    return result;
}

- (NSString*) strip
{
    return [self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}

- (BOOL) startsWith:(NSString*)prefix
{
    if ([prefix length] <= [self length]) {
        NSString *substr = [self substringToIndex:[prefix length]];
        return [substr isEqualToString:prefix];
    }
    return NO;
}

- (NSMutableArray*) componentsSeparatedByWhitespace 
{
    NSCharacterSet *whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet];    
    int length = [self length];
    NSMutableArray *result = [NSMutableArray array];
  
    BOOL inWord=NO;
    NSRange nonWS;
    for (int i = 0; i < length; i++) {
        unichar c = [self characterAtIndex:i];        
        if ([whitespace characterIsMember:c]) {
            if (inWord) {
                nonWS.length = i - nonWS.location;                
                [result addObject:[self substringWithRange:nonWS]];
                inWord = NO;
            }
        }
        else {
            if (!inWord) {
                nonWS.location = i;
                inWord = YES;
            }
        }
    }
    
    if (inWord)
        [result addObject:[self substringFromIndex:nonWS.location]];
    
    return result;
    
}

- (NSMutableString*) stripHTML
{
    // Maybe this would be better done with some regular expressions?

    // given some text, purges any HTML tags we find in it.  
    // Also tries to replace HTML entities a bit.
    NSMutableString *result = [NSMutableString string];    
    NSRange remainingString = [self rangeAfter:0];
    
    // Build up a string without any tags.  Assume no "<" within attribute values... is that wise?
    for (;;) {
        NSRange nextTag = [self rangeOfString:@"<"
                                      options:NSLiteralSearch 
                                        range:remainingString];
        if (nextTag.location == NSNotFound) {
            [result appendString:[self substringFromIndex:remainingString.location]];
            break;
        }
        
        // append all characters before the "<"
        NSRange inBetween;
        inBetween.location = remainingString.location;
        inBetween.length = nextTag.location - remainingString.location;
        [result appendString:[self substringWithRange:inBetween]];
        
        // find the matching ">"
        NSRange afterTag = [self rangeAfter:nextTag.location];
        NSRange endTag = [self rangeOfString:@">" options:NSLiteralSearch range:afterTag];
        if (endTag.location == NSNotFound) {
            // No matching ">" found... hmm... I will include the damn thing then
            [result appendString:[self substringWithRange:[self rangeAfterRange:inBetween]]];
            break;
        }
        
        remainingString = [self rangeAfterRange:endTag];
    }
    
    // Replace some well known entities
    NSRange entireString = [result rangeAfter:0];
    [result replaceOccurrencesOfString:@"&lt;" withString:@"<" options:NSCaseInsensitiveSearch range:entireString];
    [result replaceOccurrencesOfString:@"&gt;" withString:@">" options:NSCaseInsensitiveSearch range:entireString];
    [result replaceOccurrencesOfString:@"&quot;" withString:@"\"" options:NSCaseInsensitiveSearch range:entireString];
    [result replaceOccurrencesOfString:@"&apos;" withString:@"'" options:NSCaseInsensitiveSearch range:entireString];
    [result replaceOccurrencesOfString:@"&amp;" withString:@"&" options:NSCaseInsensitiveSearch range:entireString];
    
    return result;
}

- (NSMutableString*) normalizeWhitespace
{
    // there must be an easier way to do this
    
    NSCharacterSet *whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet];
    int length = [self length];
    NSMutableString *result = [NSMutableString stringWithCapacity:[self length]];
    
    BOOL pendingWS=NO, pendingNonWS = NO;
    NSRange nonWS;
    for (int i = 0; i < length; i++) {
        unichar c = [self characterAtIndex:i];
        if ([whitespace characterIsMember:c]) {
            if (pendingNonWS) {
                NSAssert (!pendingWS, @"Can't be pending WS and Non-WS at same time");
                nonWS.length = i - nonWS.location;
                [result appendString:[self substringWithRange:nonWS]];
                pendingNonWS = NO;
                pendingWS = YES;
            }
        }
        else {
            if (pendingWS) {
                NSAssert (!pendingNonWS, @"Can't be pending WS and Non-WS at same time");
                [result appendString:@" "];
                pendingWS = NO;
            }
            if (!pendingNonWS) {
                pendingNonWS = YES;
                nonWS.location = i;
            }
        }
    }
    
    if (pendingNonWS) {
        [result appendString:[self substringFromIndex:nonWS.location]];
        NSAssert (!pendingWS, @"Can't be pending WS and Non-WS at same time");
    }
    
    return result;
}

- (BOOL) isEqualToStringIgnoringCase:(NSString*)other
{
    return [self caseInsensitiveCompare:other] == NSOrderedSame;
}

- (NSComparisonResult) compareIgnoringCase:(NSString*)otherString
{
    return [self compare:otherString options:NSCaseInsensitiveSearch];
}

- (NSNumber*) hasPrefixReturningNSNumber:(NSString*)other
{
    return [NSNumber numberWithBool:[self hasPrefix:other]];
}

+ (NSMutableString*) stringByJoining:(id)toJoin withString:(NSString*)string
{
    NSMutableString *result = [NSMutableString string];
    BOOL first = YES;
    FOREACH(id, obj, toJoin) {
        if (!first) 
            [result appendString:string];
        else
            first = NO;
        [result appendString:[obj description]];
    } ENDFOREACH;
    return result;
}
    
@end

@implementation NSForm (Oaxaca)

- (NSMutableDictionary*) valuesAsDictionary
{
    int i, numentries = [self numberOfRows];
    NSMutableDictionary *result = [[[NSMutableDictionary alloc] initWithCapacity:numentries] autorelease];
    for (i = 0; i < numentries; i++) {
        id cell = [self cellAtIndex:i];
        NSString *title = [cell title];
        
        // Check if the last character in the title is ':'
        assert ([title length] > 0);
        int lastchar = [title length] - 1;
        if ([title characterAtIndex:lastchar] == ':') {
            title = [title substringToIndex:lastchar];
        }
        
        [result setObject:[cell stringValue]
                   forKey:title];
    }
    
    return result;
}

- (void) setValuesFromDictionary:(NSDictionary*)dict
{
    int i, numentries = [self numberOfRows];
    for (i = 0; i < numentries; i++) {
        id cell = [self cellAtIndex:i];
        NSString *title = [cell title];
        
        // Check if the last character in the title is ':'
        assert ([title length] > 0);
        int lastchar = [title length] - 1;
        if ([title characterAtIndex:lastchar] == ':') {
            title = [title substringToIndex:lastchar];
        }
        
        id value = [dict objectForKey:title];
        if (value) {
            [cell setStringValue:[value description]];
        }
    }    
}

@end

@implementation NSDictionary (Oaxaca)

+ (NSDictionary*) dictionaryWithValues:(NSArray*)values keysFromSelector:(SEL)selector;
{
    NSMutableDictionary *res = [NSMutableDictionary dictionaryWithCapacity:[values count]];
    FOREACH(id, value, values) {
        id key = [values performSelector:selector];
        [res setObject:value forKey:key];
    } ENDFOREACH;
    return res;
}

@end
