//
//  QKQuerySet.m
//  QueryKit
//
//  Created by Kyle Fuller on 30/04/2013.
//
//

#import "QKQuerySet.h"

NSString * const QKQuerySetErrorDomain = @"QKQuerySetErrorDomain";

@interface QKQuerySet ()

@property (nonatomic, strong) NSArray *resultsCache;

@end

@implementation QKQuerySet

#pragma mark - Creation

- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext entityDescription:(NSEntityDescription *)entityDescription {
    return [self initWithManagedObjectContext:managedObjectContext entityDescription:entityDescription predicate:nil sortDescriptors:nil range:NSMakeRange(NSNotFound, NSNotFound)];
}

- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext fetchRequest:(NSFetchRequest *)fetchRequest {
    NSParameterAssert(fetchRequest != nil);

    NSEntityDescription *entityDescription = [fetchRequest entity];
    NSPredicate *predicate = [fetchRequest predicate];
    NSArray *sortDescriptors = [fetchRequest sortDescriptors];

    return [self initWithManagedObjectContext:managedObjectContext entityDescription:entityDescription predicate:predicate sortDescriptors:sortDescriptors range:NSMakeRange(NSNotFound, NSNotFound)];
}

- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext entityDescription:(NSEntityDescription *)entityDescription predicate:(NSPredicate *)predicate sortDescriptors:(NSArray *)sortDescriptors range:(NSRange)range {
    NSParameterAssert(managedObjectContext != nil);
    NSParameterAssert(entityDescription != nil);

    if (self = [super init]) {
        _managedObjectContext = managedObjectContext;
        _entityDescription = entityDescription;
        _predicate = [predicate copy];
        _sortDescriptors = sortDescriptors? [sortDescriptors copy] : @[];
        _range = range;
    }

    return self;
}

- (instancetype)init {
    NSString *reason = [NSString stringWithFormat:@"%@ Failed to call designated initializer.", NSStringFromClass([self class])];
    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil];
}

#pragma mark - Equality

- (NSUInteger)hash {
    return [self.managedObjectContext hash];
}

- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }

    if ([object isKindOfClass:[QKQuerySet class]] == NO) {
        return NO;
    }

    return [self isEqualToQuerySet:object];
}

- (BOOL)isEqualToQuerySet:(QKQuerySet *)queryset {
    return (
        [self.managedObjectContext isEqual:[queryset managedObjectContext]] &&
        [self.entityDescription isEqual:[queryset entityDescription]] &&
        [self.predicate isEqual:[queryset predicate]] &&
        [self.sortDescriptors isEqual:[queryset sortDescriptors]] &&
        NSEqualRanges(self.range, queryset.range)
    );
}

#pragma mark - NSCopying

- (instancetype)copyWithZone:(NSZone *)zone {
    return [[[self class] allocWithZone:zone] initWithManagedObjectContext:self.managedObjectContext entityDescription:self.entityDescription predicate:self.predicate sortDescriptors:self.sortDescriptors range:self.range];
}

#pragma mark - NSFastEnumeration

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len {
    if (_resultsCache == nil) {
        [self array:nil];
    }

    return [_resultsCache countByEnumeratingWithState:state objects:buffer count:len];
}

#pragma mark - Fetching

- (NSFetchRequest *)fetchRequest {
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    [fetchRequest setEntity:_entityDescription];
    [fetchRequest setPredicate:self.predicate];
    [fetchRequest setSortDescriptors:self.sortDescriptors];

    if (self.range.location != NSNotFound) {
        fetchRequest.fetchOffset = self.range.location;
        fetchRequest.fetchLimit = self.range.length;
    }

    return fetchRequest;
}

- (NSUInteger)count:(NSError **)error {
    NSUInteger count = 0;

    if (_resultsCache) {
        count = [_resultsCache count];
    } else {
        NSFetchRequest *fetchRequest = [self fetchRequest];
        count = [self.managedObjectContext countForFetchRequest:fetchRequest error:error];
    }

    return count;
}

- (NSArray *)array:(NSError **)error {
    if (_resultsCache == nil) {
        _resultsCache = [self.managedObjectContext executeFetchRequest:[self fetchRequest] error:error];
    }

    return _resultsCache;
}

- (NSSet *)set:(NSError **)error {
    NSArray *array = [self array:error];
    NSSet *set;

    if (array != nil) {
        set = [NSSet setWithArray:array];
    }

    return set;
}

- (NSOrderedSet *)orderedSet:(NSError **)error {
    NSArray *array = [self array:error];
    NSOrderedSet *orderedSet;

    if (array != nil) {
        orderedSet = [NSOrderedSet orderedSetWithArray:array];
    }

    return orderedSet;
}

- (BOOL)enumerateObjects:(void (^)(NSManagedObject *object, NSUInteger index, BOOL *stop))block error:(NSError **)error {
    NSArray *array = [self array:error];

    if (array != nil) {
        [array enumerateObjectsUsingBlock:block];
    }

    return array != nil;
}

- (BOOL)each:(void (^)(NSManagedObject *managedObject))block error:(NSError **)error {
    NSArray *array = [self array:error];

    if (array != nil) {
        for (NSManagedObject *managedObject in array) {
            block(managedObject);
        }
    }

    return array != nil;
}

#pragma mark - Deletion

- (NSUInteger)deleteObjects:(NSError **)error {
    NSArray *array = [self array:error];

    NSUInteger count = 0;

    if (array != nil) {
        NSManagedObjectContext *managedObjectContext = self.managedObjectContext;

        for (NSManagedObject *managedObject in array) {
            [managedObjectContext deleteObject:managedObject];
            ++count;
        }
    }

    return count;
}

@end

@implementation QKQuerySet (Sorting)

- (instancetype)orderBy:(NSArray *)sortDescriptors {
    return [[QKQuerySet alloc] initWithManagedObjectContext:_managedObjectContext entityDescription:_entityDescription predicate:_predicate sortDescriptors:sortDescriptors range:self.range];
}

- (instancetype)reverse {
    NSMutableArray *sortDescriptors = [[NSMutableArray alloc] initWithCapacity:[_sortDescriptors count]];

    for (NSSortDescriptor *sortDescriptor in _sortDescriptors) {
        [sortDescriptors addObject:[sortDescriptor reversedSortDescriptor]];
    }

    return [[QKQuerySet alloc] initWithManagedObjectContext:_managedObjectContext entityDescription:_entityDescription predicate:_predicate sortDescriptors:sortDescriptors range:self.range];
}

@end

@implementation QKQuerySet (Filtering)

- (instancetype)exclude:(NSPredicate *)predicate {
    predicate = [[NSCompoundPredicate alloc] initWithType:NSNotPredicateType subpredicates:@[predicate]];

    if (_predicate) {
        predicate = [[NSCompoundPredicate alloc] initWithType:NSAndPredicateType subpredicates:@[_predicate, predicate]];
    }

    return [[QKQuerySet alloc] initWithManagedObjectContext:_managedObjectContext entityDescription:_entityDescription predicate:predicate sortDescriptors:_sortDescriptors range:self.range];
}

- (instancetype)filter:(NSPredicate *)predicate {
    if (_predicate) {
        predicate = [[NSCompoundPredicate alloc] initWithType:NSAndPredicateType subpredicates:@[_predicate, predicate]];
    }

    return [[QKQuerySet alloc] initWithManagedObjectContext:_managedObjectContext entityDescription:_entityDescription predicate:predicate sortDescriptors:_sortDescriptors range:self.range];
}

@end

@implementation QKQuerySet (SingleObject)

- (NSManagedObject *)object:(NSError **)error {
    NSManagedObject *managedObject;
    NSArray *array;

    if (_resultsCache) {
        array = _resultsCache;
    } else {
        NSFetchRequest *fetchRequest = [self fetchRequest];
        fetchRequest.fetchBatchSize = 1; // Only request one

        array = [self.managedObjectContext executeFetchRequest:fetchRequest error:error];
    }

    NSUInteger count = [array count];

    if (count == 1) {
        managedObject = [array firstObject];
    } else if ((count > 1) && error != nil) {
        *error = [NSError errorWithDomain:QKQuerySetErrorDomain code:0 userInfo:@{
            NSLocalizedDescriptionKey: @"Find object in fetch request failed, should only result in a single result.",
        }];
    }

    return managedObject;
}

- (NSManagedObject *)firstObject:(NSError **)error {
    NSManagedObject *managedObject;

    if (_resultsCache) {
        managedObject = [_resultsCache firstObject];
    } else {
        NSFetchRequest *fetchRequest = [self fetchRequest];
        [fetchRequest setFetchLimit:1];

        NSArray *array = [self.managedObjectContext executeFetchRequest:fetchRequest error:error];
        managedObject = [array firstObject];
    }

    return managedObject;
}

- (NSManagedObject *)lastObject:(NSError **)error {
    NSManagedObject *managedObject;

    if (_resultsCache) {
        managedObject = [_resultsCache lastObject];
    } else {
        NSFetchRequest *fetchRequest = [self fetchRequest];
        NSArray *array = [self.managedObjectContext executeFetchRequest:fetchRequest error:error];
        managedObject = [array lastObject];
    }
    
    return managedObject;
}

@end

@implementation NSManagedObject (QKQuerySet)

+ (NSString *)entityName {
    return NSStringFromClass([self class]);
}

+ (QKQuerySet *)querySetWithManagedObjectContext:(NSManagedObjectContext *)context {
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:[self entityName] inManagedObjectContext:context];
    return [[QKQuerySet alloc] initWithManagedObjectContext:context entityDescription:entityDescription];
}

@end
