//
//  RKPaginator.m
//  RestKit
//
//  Created by Blake Watters on 12/29/11.
//  Copyright (c) 2009-2012 RestKit. All rights reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

#import "RKPaginator.h"
#import "RKMappingOperation.h"
#import "RKObjectRequestOperation.h"
#import "RKManagedObjectRequestOperation.h"
#import "SOCKit.h"
#import "RKLog.h"
#import "RKPathUtilities.h"
#import "RKHTTPUtilities.h"

static NSUInteger RKPaginatorDefaultPerPage = 25;

// Private interface
@interface RKPaginator ()
@property (nonatomic, copy) NSURLRequest *request;
@property (nonatomic, strong) Class HTTPOperationClass;
@property (nonatomic, strong) RKObjectRequestOperation *objectRequestOperation;
@property (nonatomic, copy) NSArray *responseDescriptors;
@property (nonatomic, assign, readwrite) NSUInteger currentPage;
@property (nonatomic, assign, readwrite) NSUInteger offset;
@property (nonatomic, assign, readwrite) NSUInteger pageCount;
@property (nonatomic, assign, readwrite) NSUInteger objectCount;
@property (nonatomic, assign, readwrite) BOOL loaded;
@property (nonatomic, strong, readwrite) RKMappingResult *mappingResult;
@property (nonatomic, strong, readwrite) NSError *error;

// iOS 5.x compatible proxy attributes
@property (nonatomic, assign, readwrite) NSNumber *perPageNumber;
@property (nonatomic, assign, readwrite) NSNumber *currentPageNumber;
@property (nonatomic, assign, readwrite) NSNumber *pageCountNumber;
@property (nonatomic, assign, readwrite) NSNumber *objectCountNumber;

@property (nonatomic, copy) void (^successBlock)(RKPaginator *paginator, NSArray *objects, NSUInteger page);
@property (nonatomic, copy) void (^failureBlock)(RKPaginator *paginator, NSError *error);
@end

@implementation RKPaginator

- (id)initWithRequest:(NSURLRequest *)request
    paginationMapping:(RKObjectMapping *)paginationMapping
  responseDescriptors:(NSArray *)responseDescriptors;
{
    NSParameterAssert(request);
    NSParameterAssert(paginationMapping);
    NSParameterAssert(responseDescriptors);
    NSAssert([paginationMapping.objectClass isSubclassOfClass:[RKPaginator class]], @"The paginationMapping must have a target object class of `RKPaginator`");
    self = [super init];
    if (self) {
        self.HTTPOperationClass = [RKHTTPRequestOperation class];
        self.request = request;
        self.paginationMapping = paginationMapping;
        self.responseDescriptors = responseDescriptors;
        self.currentPage = NSNotFound;
        self.pageCount = NSNotFound;
        self.objectCount = NSNotFound;
        self.offset = NSNotFound;
        self.perPage = RKPaginatorDefaultPerPage;
        self.loaded = NO;
    }

    return self;
}

- (void)dealloc
{
    [self.objectRequestOperation cancel];
}

- (NSURL *)patternURL
{
    return self.request.URL;
}

- (NSURL *)URL
{
    NSString *pathAndQueryString = RKPathAndQueryStringFromURLRelativeToURL(self.patternURL, nil);
    NSString *interpolatedString = RKPathFromPatternWithObject(pathAndQueryString, self);
    return [NSURL URLWithString:interpolatedString relativeToURL:self.request.URL];
}

- (void)setHTTPOperationClass:(Class)operationClass
{
    NSAssert(operationClass == nil || [operationClass isSubclassOfClass:[RKHTTPRequestOperation class]], @"The HTTP operation class must be a subclass of `RKHTTPRequestOperation`");
    _HTTPOperationClass = operationClass;
}

- (void)setCompletionBlockWithSuccess:(void (^)(RKPaginator *paginator, NSArray *objects, NSUInteger page))success
                              failure:(void (^)(RKPaginator *paginator, NSError *error))failure
{
    self.successBlock = success;
    self.failureBlock = failure;
}

// Private. Public consumers can rely on isLoaded
- (BOOL)hasCurrentPage
{
    return _currentPage != NSNotFound;
}

- (BOOL)hasOffset
{
    return _offset != NSNotFound;
}

- (BOOL)hasPageCount
{
    return _pageCount != NSNotFound;
}

- (BOOL)hasObjectCount
{
    return _objectCount != NSNotFound;
}

- (NSUInteger)currentPage
{
    // Referenced during initial load, so we don't rely on isLoaded.
    NSAssert([self hasCurrentPage], @"Current page has not been initialized.");
    return _currentPage;
}

- (NSUInteger)offset
{
    if ([self hasOffset]) return _offset;
    return [self hasCurrentPage] ? ((_currentPage - 1) * _perPage) : 0;
}

- (BOOL)hasNextPage
{
    NSAssert(self.isLoaded, @"Cannot determine hasNextPage: paginator is not loaded.");
    NSAssert([self hasPageCount], @"Cannot determine hasNextPage: page count is not known.");

    return self.currentPage < self.pageCount;
}

- (BOOL)hasPreviousPage
{
    NSAssert(self.isLoaded, @"Cannot determine hasPreviousPage: paginator is not loaded.");
    return self.currentPage > 1;
}

#pragma mark - Action methods

- (void)loadNextPage
{
    [self loadPage:self.currentPage + 1];
}

- (void)loadPreviousPage
{
    [self loadPage:self.currentPage - 1];
}

- (void)loadPage:(NSUInteger)pageNumber
{
    NSAssert(self.responseDescriptors, @"Cannot perform a load with nil response descriptors.");
    NSAssert(! self.objectRequestOperation, @"Cannot perform a load while one is already in progress.");
    self.currentPage = pageNumber;
    
    NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
    mutableRequest.URL = self.URL;

    if (self.managedObjectContext) {
        RKHTTPRequestOperation *requestOperation = [[self.HTTPOperationClass alloc] initWithRequest:mutableRequest];
        RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithHTTPRequestOperation:requestOperation responseDescriptors:self.responseDescriptors];
        managedObjectRequestOperation.managedObjectContext = self.managedObjectContext;
        managedObjectRequestOperation.managedObjectCache = self.managedObjectCache;
        managedObjectRequestOperation.fetchRequestBlocks = self.fetchRequestBlocks;
        managedObjectRequestOperation.deletesOrphanedObjects = NO;
        
        self.objectRequestOperation = managedObjectRequestOperation;
    } else {
        self.objectRequestOperation = [[RKObjectRequestOperation alloc] initWithRequest:mutableRequest responseDescriptors:self.responseDescriptors];
    }
    
    // Add KVO to ensure notification of loaded state prior to execution of completion block
    [self.objectRequestOperation addObserver:self forKeyPath:@"isFinished" options:0 context:nil];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
    [self.objectRequestOperation setWillMapDeserializedResponseBlock:^id(id deserializedResponseBody) {
        NSError *error = nil;
        RKMappingOperation *mappingOperation = [[RKMappingOperation alloc] initWithSourceObject:deserializedResponseBody destinationObject:self mapping:self.paginationMapping];
        BOOL success = [mappingOperation performMapping:&error];
        if (!success) {
            self.pageCount = 0;
            self.currentPage = 0;
            RKLogError(@"Paginator didn't map info to compute page count. Assuming no pages.");
        } else if (self.perPage && [self hasObjectCount]) {
            float objectCountFloat = self.objectCount;
            self.pageCount = ceilf(objectCountFloat / self.perPage);
            RKLogInfo(@"Paginator objectCount: %ld pageCount: %ld", (long)self.objectCount, (long)self.pageCount);
        } else {
            RKLogError(@"Paginator perPage set is 0.");
        }
        
        return deserializedResponseBody;
    }];
    [self.objectRequestOperation setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
        if (self.successBlock) {
            self.successBlock(self, [mappingResult array], self.currentPage);
        }
    } failure:^(RKObjectRequestOperation *operation, NSError *error) {
        if (self.failureBlock) {
            self.failureBlock(self, error);
        }
    }];
#pragma clang diagnostic pop
    
    if (self.operationQueue) {
        [self.operationQueue addOperation:self.objectRequestOperation];
    } else {
        [self.objectRequestOperation start];
    }
}

- (void)waitUntilFinished
{
    [self.objectRequestOperation waitUntilFinished];
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"<%@: %p patternURL=%@ isLoaded=%@ perPage=%ld currentPage=%@ offset=%@ pageCount=%@ objectCount=%@>",
            NSStringFromClass([self class]), self, self.patternURL, self.isLoaded ? @"YES" : @"NO", (long) self.perPage,
            [self hasCurrentPage] ? @(self.currentPage) : @"???",
            [self hasOffset] ? @(self.offset) : @"???",
            [self hasPageCount] ? @(self.pageCount) : @"???",
            [self hasObjectCount] ? @(self.objectCount) : @"???"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"isFinished"] && [self.objectRequestOperation isFinished]) {
        self.loaded = (self.objectRequestOperation.mappingResult != nil);
        self.mappingResult = self.objectRequestOperation.mappingResult;
        self.error = self.objectRequestOperation.error;
        self.objectRequestOperation = nil;
        [object removeObserver:self forKeyPath:@"isFinished"];
    }
}

- (void)cancel
{
    [self.objectRequestOperation cancel];
}

#pragma mark - iOS 5 proxy attributes

- (NSNumber *)perPageNumber
{
    return [NSNumber numberWithUnsignedInteger:self.perPage];
}

- (void)setPerPageNumber:(NSNumber *)perPageNumber
{
    self.perPage = [perPageNumber unsignedIntegerValue];
}

- (NSNumber *)currentPageNumber
{
    return [NSNumber numberWithUnsignedInteger:self.currentPage];
}

- (void)setCurrentPageNumber:(NSNumber *)currentPageNumber
{
    self.currentPage = [currentPageNumber unsignedIntegerValue];
}

- (NSNumber *)pageCountNumber
{
    return [NSNumber numberWithUnsignedInteger:self.pageCount];
}

- (void)setPageCountNumber:(NSNumber *)pageCountNumber
{
    self.pageCount = [pageCountNumber unsignedIntegerValue];
}

- (NSNumber *)objectCountNumber
{
    return [NSNumber numberWithUnsignedInteger:self.objectCount];
}

- (void)setObjectCountNumber:(NSNumber *)objectCountNumber
{
    self.objectCount = [objectCountNumber unsignedIntegerValue];
}

- (NSNumber *)offsetNumber
{
    return [NSNumber numberWithUnsignedInteger:self.offset];
}

- (void)setOffsetNumber:(NSNumber *)offsetNumber
{
    self.offset = [offsetNumber unsignedIntegerValue];
}

@end
