/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 "WXRecycleListUpdateManager.h"
#import "WXLog.h"
#import "WXAssert.h"
#import "WXDiffUtil.h"

@interface WXRecycleListDiffResult : NSObject

@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *deleteIndexPaths;
@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *insertIndexPaths;
@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *reloadIndexPaths;

- (BOOL)hasChanges;

@end

@implementation WXRecycleListDiffResult

- (instancetype)initWithInsertIndexPaths:(NSMutableSet<NSIndexPath *> *)insertIndexPaths
                        deleteIndexPaths:(NSMutableSet<NSIndexPath *> *)deleteIndexPaths
                        reloadIndexPaths:(NSMutableSet<NSIndexPath *> *)reloadIndexPaths
{
    if (self = [super init]) {
        _insertIndexPaths = [insertIndexPaths copy];
        _deleteIndexPaths = [deleteIndexPaths copy];
        _reloadIndexPaths = [reloadIndexPaths copy];
    }
    
    return self;
}

- (BOOL)hasChanges
{
    return _insertIndexPaths.count > 0 || _deleteIndexPaths.count > 0 || _reloadIndexPaths.count > 0;
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"<%@: %p; insert index paths: %@; delete index paths: %@; reload index paths: %@", NSStringFromClass([self class]), self, _insertIndexPaths, _deleteIndexPaths, _reloadIndexPaths];
}

@end

@interface WXRecycleListUpdateManager ()

@property (nonatomic, copy) NSArray *newerData;
@property (nonatomic, copy) NSArray *appendingData;
@property (nonatomic, copy) NSArray *olderData;
@property (nonatomic, assign) BOOL isUpdating;
@property (nonatomic, strong) NSMutableArray *completions;

@property (nonatomic, strong) NSMutableSet<NSIndexPath *> *reloadIndexPaths;

@end

@implementation WXRecycleListUpdateManager

- (instancetype)init
{
    if (self = [super init]) {
        _completions = [NSMutableArray array];
    }
    
    return self;
}

- (void)reload
{
    [_collectionView reloadData];
}

- (void)updateWithNewData:(NSArray *)newData
                  oldData:(NSArray *)oldData
               completion:(WXRecycleListUpdateCompletion)completion
                animation:(BOOL)isAnimated
{
    WXAssertMainThread();
    
    if (!_collectionView) {
        WXLogError(@"Update list with no collection view");
        if (completion) {
            completion(NO);
        }
        return;
    }
    
    self.newerData = newData;
    self.appendingData = nil;
    self.olderData = oldData;
    
    if (completion) {
        [_completions addObject:completion];
    }
    
    [self checkUpdates];
}

- (void)updateWithAppendingData:(NSArray *)appendingData
                        oldData:(NSArray *)oldData
                     completion:(WXRecycleListUpdateCompletion)completion
                      animation:(BOOL)isAnimated
{
    if (!_collectionView) {
        WXLogError(@"Update list with no collection view");
        completion(NO);
        return;
    }
    
    self.appendingData = appendingData;
    self.olderData = oldData;
    
    if (completion) {
        [_completions addObject:completion];
    }
    
    [self checkUpdates];
}


- (void)checkUpdates
{
    dispatch_async(dispatch_get_main_queue(), ^{
        if (self.isUpdating) {
            return ;
        }
        
        [self performBatchUpdates];
    });
}

- (void)performBatchUpdates
{
    WXAssertMainThread();
    WXAssert(!self.isUpdating, @"Can not perform updates while an updating is being performed");
    
    UICollectionView *collectionView = self.collectionView;
    if (!collectionView) {
        return;
    }
    
    NSArray *newData = [self.newerData copy];
    NSArray *oldData = [self.olderData copy];
    NSArray *appendingData = [self.appendingData copy];
    //TODO use completionBlocks
//    NSArray *completionBlocks = [self.completions copy];
    
    [self cleanup];
    
    WXDiffResult *diffResult;
    if (appendingData) {
        newData = [oldData arrayByAddingObjectsFromArray:appendingData];
        NSIndexSet *inserts = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(oldData.count, appendingData.count)];
        diffResult = [[WXDiffResult alloc] initWithInserts:inserts deletes:nil updates:nil];
    } else if (newData){
        diffResult = [WXDiffUtil diffWithMinimumDistance:newData oldArray:oldData];
    }
    
    WXRecycleListDiffResult *recycleListDiffResult = [self recycleListUpdatesByDiffResult:diffResult];
    
    if (![diffResult hasChanges] && self.reloadIndexPaths.count == 0) {
        return;
    }
    
    void (^updates)(void) = [^{
        [self.delegate updateManager:self willUpdateData:newData];
        [UIView setAnimationsEnabled:NO];
        [self applyUpdateWithDiffResult:recycleListDiffResult];
    } copy];
    
    void (^completion)(BOOL) = [^(BOOL finished) {
        [UIView setAnimationsEnabled:YES];
        self.isUpdating = NO;
        [self.delegate updateManager:self didUpdateData:newData withSuccess:finished];
        [self.reloadIndexPaths removeAllObjects];
        [self checkUpdates];
    } copy];
    
    self.isUpdating = YES;
    
    if (!self.delegate || !collectionView.dataSource) {
        return;
    }
    
    [collectionView performBatchUpdates:updates completion:completion];
}

- (WXRecycleListDiffResult *)recycleListUpdatesByDiffResult:(WXDiffResult *)diffResult
{
    NSMutableSet<NSIndexPath *> *reloadIndexPaths = [NSMutableSet set];
    NSMutableSet<NSIndexPath *> *deleteIndexPaths = [NSMutableSet set];
    NSMutableSet<NSIndexPath *> *insertIndexPaths = [NSMutableSet set];
    
    [diffResult.updates enumerateObjectsUsingBlock:^(WXDiffUpdateIndex * _Nonnull update, NSUInteger idx, BOOL * _Nonnull stop) {
        NSIndexPath *reloadIndexPath = [NSIndexPath indexPathForItem:update.oldIndex inSection:0];
        [reloadIndexPaths addObject:reloadIndexPath];
    }];
    
    [diffResult.inserts enumerateIndexesUsingBlock:^(NSUInteger insertIndex, BOOL * _Nonnull stop) {
        NSIndexPath *insertIndexPath = [NSIndexPath indexPathForItem:insertIndex inSection:0];
        [insertIndexPaths addObject:insertIndexPath];
    }];
    
    [diffResult.deletes enumerateIndexesUsingBlock:^(NSUInteger deleteIndex, BOOL * _Nonnull stop) {
        NSIndexPath *deleteIndexPath = [NSIndexPath indexPathForItem:deleteIndex inSection:0];
        [deleteIndexPaths addObject:deleteIndexPath];
    }];
    
    WXRecycleListDiffResult *result = [[WXRecycleListDiffResult alloc] initWithInsertIndexPaths:insertIndexPaths deleteIndexPaths:deleteIndexPaths reloadIndexPaths:reloadIndexPaths];
    
    return result;
}


- (void)applyUpdateWithDiffResult:(WXRecycleListDiffResult *)diffResult
{
    if (!_collectionView) {
        return;
    }
    
    // reload index paths should not inculde delete index paths, otherwise it will cause crash:
    // Assertion failure in
    // -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:]
    NSMutableSet *reloadIndexPaths = self.reloadIndexPaths ? [[diffResult.reloadIndexPaths setByAddingObjectsFromSet:self.reloadIndexPaths] mutableCopy]: [diffResult.reloadIndexPaths mutableCopy];
    [reloadIndexPaths minusSet:diffResult.deleteIndexPaths];
    
    if (diffResult.deleteIndexPaths.count > 0) {
        [_collectionView deleteItemsAtIndexPaths:[diffResult.deleteIndexPaths allObjects]];
    }
    if (diffResult.insertIndexPaths.count > 0) {
        [_collectionView insertItemsAtIndexPaths:[diffResult.insertIndexPaths allObjects]];
    }
    if (reloadIndexPaths.count > 0) {
        [_collectionView reloadItemsAtIndexPaths:[reloadIndexPaths allObjects]];
    }
}

- (void)cleanup
{
    self.newerData = nil;
    self.appendingData = nil;
    self.olderData = nil;
    [self.completions removeAllObjects];
}

@end
