/*
 * 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 "WXRecyclerUpdateController.h"
#import "WXCellComponent.h"
#import "WXAssert.h"
#import "WXLog.h"
#import "WXDiffUtil.h"
#import "NSArray+Weex.h"

@interface WXRecyclerDiffResult : NSObject

@property (nonatomic, strong, readonly) NSIndexSet *insertSections;
@property (nonatomic, strong, readonly) NSIndexSet *deleteSections;
@property (nonatomic, strong, readonly) NSIndexSet *reloadSections;

@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 WXRecyclerDiffResult

- (instancetype)initWithInsertSections:(NSIndexSet *)insertSections
                        deleteSections:(NSIndexSet *)deletesSections
                        reloadSections:(NSIndexSet *)reloadSections
                      insertIndexPaths:(NSMutableSet<NSIndexPath *> *)insertIndexPaths
                      deleteIndexPaths:(NSMutableSet<NSIndexPath *> *)deleteIndexPaths
                      reloadIndexPaths:(NSMutableSet<NSIndexPath *> *)reloadIndexPaths
{
    if (self = [super init]) {
        _insertSections = [insertSections copy];
        _deleteSections = [deletesSections copy];
        _reloadSections = [reloadSections copy];
        _insertIndexPaths = [insertIndexPaths copy];
        _deleteIndexPaths = [deleteIndexPaths copy];
        _reloadIndexPaths = [reloadIndexPaths copy];
    }
    
    return self;
}

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

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

@end

@interface WXRecyclerUpdateController ()

@property (nonatomic, copy) NSArray<WXSectionDataController *> *theNewData;
@property (nonatomic, copy) NSArray<WXSectionDataController *> *theOldData;
@property (nonatomic, weak) UICollectionView *collectionView;
@property (nonatomic, strong) NSMutableSet<NSIndexPath *> *reloadIndexPaths;
@property (nonatomic, assign) BOOL isUpdating;

@end

@implementation WXRecyclerUpdateController

- (void)performUpdatesWithNewData:(NSArray<WXSectionDataController *> *)newData oldData:(NSArray<WXSectionDataController *> *)oldData view:(UICollectionView *)collectionView
{
    if (!collectionView) {
        return;
    }
    
    self.theNewData = newData;
    self.theOldData = oldData;
    self.collectionView = collectionView;
    
    [self checkUpdates];
}

- (void)reloadItemsAtIndexPath:(NSIndexPath *)indexPath
{
    if (!indexPath) {
        return;
    }
    
    if (!_reloadIndexPaths) {
        _reloadIndexPaths = [NSMutableSet set];
    }
    
    [_reloadIndexPaths addObject:indexPath];
    
    [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<WXSectionDataController *> *newData = [self.theNewData copy];
    NSArray<WXSectionDataController *> *oldData = [self.theOldData copy];

    [self cleanup];
    
    WXRecyclerDiffResult *diffResult = [self diffWithNewData:newData oldData:oldData];
    if (![diffResult hasChanges] && self.reloadIndexPaths.count == 0) {
        return;
    }
    
    void (^updates)(void) = [^{
        [self.delegate updateController:self willPerformUpdateWithNewData:newData];
        [UIView setAnimationsEnabled:NO];
        WXLogDebug(@"UICollectionView update:%@", diffResult);
        if(!diffResult.hasChanges) { return ; }
        [self applyUpdate:diffResult toCollectionView:self.collectionView];
    } copy];
    
    void (^completion)(BOOL) = [^(BOOL finished) {
        [UIView setAnimationsEnabled:YES];
        self.isUpdating = NO;
        [self.delegate updateController:self didPerformUpdateWithFinished:finished];
        [self.reloadIndexPaths removeAllObjects];
        [self checkUpdates];
    } copy];
    
    self.isUpdating = YES;
    
    if (!self.delegate || !collectionView.dataSource) {
        return;
    }
    WXLogDebug(@"Diff result:%@", diffResult);
    [collectionView performBatchUpdates:updates completion:completion];
}

- (void)cleanup
{
    self.theNewData = nil;
    self.theOldData = nil;
}

- (WXRecyclerDiffResult *)diffWithNewData:(NSArray<WXSectionDataController *> *)newData
                              oldData:(NSArray<WXSectionDataController *> *)oldData
{
    NSMutableIndexSet *reloadSections = [NSMutableIndexSet indexSet];
    NSMutableSet<NSIndexPath *> *reloadIndexPaths = [NSMutableSet set];
    NSMutableSet<NSIndexPath *> *deleteIndexPaths = [NSMutableSet set];
    NSMutableSet<NSIndexPath *> *insertIndexPaths = [NSMutableSet set];
    
    WXDiffResult *sectionDiffResult = [WXDiffUtil diffWithMinimumDistance:newData oldArray:oldData];
    
    WXLogDebug(@"section diff result:%@", sectionDiffResult);
    
    [sectionDiffResult.inserts enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
        WXSectionDataController *newSection = [newData wx_safeObjectAtIndex:idx];
        [newSection.cellComponents enumerateObjectsUsingBlock:^(WXCellComponent * _Nonnull obj, NSUInteger idx2, BOOL * _Nonnull stop) {
            if (obj.isLayoutComplete) {
                NSIndexPath *insertIndexPath = [NSIndexPath indexPathForItem:idx2 inSection:idx];
                [insertIndexPaths addObject:insertIndexPath];
            }
        }];
        WXAssert(newSection, @"No section found in  new index:%ld");
    }];
    
    for (WXDiffUpdateIndex *sectionUpdate in sectionDiffResult.updates) {
        WXSectionDataController *oldSection = [oldData wx_safeObjectAtIndex:sectionUpdate.oldIndex];
        WXSectionDataController *newSection = [newData wx_safeObjectAtIndex:sectionUpdate.newIndex];
        WXAssert(newSection && oldSection, @"No section found in old index:%ld, new index:%ld", sectionUpdate.oldIndex, sectionUpdate.newIndex);
        
        WXDiffResult *itemDiffResult = [WXDiffUtil diffWithMinimumDistance:newSection.cellComponents oldArray:oldSection.cellComponents];
        if (![itemDiffResult hasChanges]) {
            // header or footer need to be updated
            [reloadSections addIndex:sectionUpdate.oldIndex];
        } else {
            for (WXDiffUpdateIndex *update in itemDiffResult.updates) {
                NSIndexPath *reloadIndexPath = [NSIndexPath indexPathForItem:update.oldIndex inSection:sectionUpdate.oldIndex];
                [reloadIndexPaths addObject:reloadIndexPath];
            }
            
            [itemDiffResult.inserts enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
                WXCellComponent *cell = [newSection.cellComponents wx_safeObjectAtIndex:idx];
                if (cell.isLayoutComplete) {
                    NSIndexPath *insertIndexPath = [NSIndexPath indexPathForItem:idx inSection:sectionUpdate.oldIndex];
                    [insertIndexPaths addObject:insertIndexPath];
                }
            }];
            
            [itemDiffResult.deletes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
                NSIndexPath *deleteIndexPath = [NSIndexPath indexPathForItem:idx inSection:sectionUpdate.oldIndex];
                [deleteIndexPaths addObject:deleteIndexPath];
            }];
        }
        
    }
    
    WXRecyclerDiffResult *result = [[WXRecyclerDiffResult alloc] initWithInsertSections:sectionDiffResult.inserts
                                                                 deleteSections:sectionDiffResult.deletes
                                                                 reloadSections:reloadSections
                                                               insertIndexPaths:insertIndexPaths
                                                               deleteIndexPaths:deleteIndexPaths
                                                               reloadIndexPaths:reloadIndexPaths];
    
    return result;
}

- (void)applyUpdate:(WXRecyclerDiffResult *)diffResult toCollectionView:(UICollectionView *)collectionView
{
    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];
    
    [collectionView deleteItemsAtIndexPaths:[diffResult.deleteIndexPaths allObjects]];
    [collectionView insertItemsAtIndexPaths:[diffResult.insertIndexPaths allObjects]];
    [collectionView reloadItemsAtIndexPaths:[reloadIndexPaths allObjects]];
    
    [collectionView deleteSections:diffResult.deleteSections];
    [collectionView insertSections:diffResult.insertSections];
    [collectionView reloadSections:diffResult.reloadSections];
}

@end
