// clang-format off

//
//  Copyright (c) 2016 Google Inc.
//
//  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.
//

// clang-format on

#import "FirebaseFirestoreUI/Sources/Public/FirebaseFirestoreUI/FUIFirestoreCollectionViewDataSource.h"

@interface FUIFirestoreCollectionViewDataSource () <FUIBatchedArrayDelegate>

@property (nonatomic, readonly, nonnull) FUIBatchedArray *collection;

// We need to maintain our own count property because the self.collection.count is sometimes
// inconsistent with how the -[UICollectionView performBatchUpdates:] method expects it to work.
// -[UICollectionView performBatchUpdates:] calls -[UICollectionViewDataSource count] at the
// beginning and at the end of the method, and if the value of -[UICollectionViewDataSource count]
// after -[UICollectionView performBatchUpdates:] is not equal to the value of
// -[UICollectionViewDataSource count] before + (rows added - rows deleted) it throws an
// NSInternalInconsistencyException.
@property (nonatomic) NSUInteger count;

/**
 * The callback to populate a subclass of UICollectionViewCell with an object
 * provided by the datasource.
 */
@property (copy, nonatomic, readonly) UICollectionViewCell *(^populateCellAtIndexPath)
  (UICollectionView *collectionView, NSIndexPath *indexPath, FIRDocumentSnapshot *object);

@end

@implementation FUIFirestoreCollectionViewDataSource

#pragma mark - FUIDataSource initializer methods

- (instancetype)initWithCollection:(FUIBatchedArray *)collection
                      populateCell:(UICollectionViewCell * (^)(UICollectionView *,
                                                               NSIndexPath *,
                                                               FIRDocumentSnapshot *))populateCell {
  self = [super init];
  if (self) {
    _collection = collection;
    _collection.delegate = self;
    _populateCellAtIndexPath = populateCell;
    _count = collection.count;
  }
  return self;
}

- (instancetype)initWithQuery:(FIRQuery *)query
                 populateCell:(UICollectionViewCell *(^)(UICollectionView *,
                                                         NSIndexPath *,
                                                         FIRDocumentSnapshot *))populateCell {
  FUIBatchedArray *array = [[FUIBatchedArray alloc] initWithQuery:query delegate:self];
  return [self initWithCollection:array populateCell:populateCell];
}

- (NSArray<FIRDocumentSnapshot *> *)items {
  return self.collection.items;
}

- (FIRDocumentSnapshot *)snapshotAtIndex:(NSInteger)index {
  return self.collection[index];
}

- (void)bindToView:(UICollectionView *)view {
  self.collectionView = view;
  view.dataSource = self;
  [self.collection observeQuery];
}

- (void)unbind {
  self.collectionView.dataSource = nil;
  self.collectionView = nil;
  [self.collection stopObserving];
}

- (FIRQuery *)query {
  return self.collection.query;
}

- (void)setQuery:(FIRQuery *)query {
  self.collection.query = query;
}

#pragma mark - FUIBatchedArrayDelegate methods

- (void)batchedArray:(FUIBatchedArray *)array
  willUpdateWithDiff:(FUISnapshotArrayDiff<FIRDocumentSnapshot *> *)diff {
  // This fixes an issue where UICollectionView crashes due to an invalid number of items
  // https://fangpenlin.com/posts/2016/04/29/uicollectionview-invalid-number-of-items-crash-issue/
  [self.collectionView numberOfItemsInSection:0];
}

- (void)batchedArray:(FUIBatchedArray *)array
   didUpdateWithDiff:(FUISnapshotArrayDiff<FIRDocumentSnapshot *> *)diff {
  [self.collectionView performBatchUpdates:^{

    NSMutableArray *deletedIndexPaths =
        [NSMutableArray arrayWithCapacity:diff.deletedIndexes.count];
    for (NSNumber *deletedIndex in diff.deletedIndexes) {
      NSIndexPath *deleted = [NSIndexPath indexPathForItem:deletedIndex.integerValue inSection:0];
      [deletedIndexPaths addObject:deleted];
    }
    [self.collectionView deleteItemsAtIndexPaths:deletedIndexPaths];

    NSMutableArray *changedIndexPaths =
        [NSMutableArray arrayWithCapacity:diff.changedIndexes.count];
    for (NSNumber *changedIndex in diff.changedIndexes) {
      NSIndexPath *changed = [NSIndexPath indexPathForItem:changedIndex.integerValue inSection:0];
      [changedIndexPaths addObject:changed];
    }
    // Use a delete and insert instead of a reload. See
    // https://stackoverflow.com/questions/42147822/uicollectionview-batchupdate-edge-case-fails
    [self.collectionView deleteItemsAtIndexPaths:changedIndexPaths];
    [self.collectionView insertItemsAtIndexPaths:changedIndexPaths];

    for (NSInteger i = 0; i < diff.movedInitialIndexes.count; i++) {
      NSInteger initialIndex = diff.movedInitialIndexes[i].integerValue;
      NSInteger finalIndex   = diff.movedResultIndexes[i].integerValue;
      NSIndexPath *initialPath = [NSIndexPath indexPathForItem:initialIndex inSection:0];
      NSIndexPath *finalPath   = [NSIndexPath indexPathForItem:finalIndex inSection:0];

      [self.collectionView moveItemAtIndexPath:initialPath toIndexPath:finalPath];
    }

    NSMutableArray *insertedIndexPaths =
        [NSMutableArray arrayWithCapacity:diff.insertedIndexes.count];
    for (NSNumber *insertedIndex in diff.insertedIndexes) {
      NSIndexPath *inserted = [NSIndexPath indexPathForItem:insertedIndex.integerValue inSection:0];
      [insertedIndexPaths addObject:inserted];
    }
    [self.collectionView insertItemsAtIndexPaths:insertedIndexPaths];
    
    self.count = self.collection.count;
  } completion:^(BOOL finished) {
    // Reload paths that have been moved.
    NSMutableArray *movedIndexPaths =
    [NSMutableArray arrayWithCapacity:diff.movedResultIndexes.count];
    for (NSNumber *movedResultIndex in diff.movedResultIndexes) {
      NSIndexPath *moved = [NSIndexPath indexPathForItem:movedResultIndex.integerValue inSection:0];
      [movedIndexPaths addObject:moved];
    }
    [self.collectionView reloadItemsAtIndexPaths:movedIndexPaths];
  }];
}

- (void)batchedArray:(FUIBatchedArray *)array queryDidFailWithError:(NSError *)error {
  if (self.queryErrorHandler != nil) {
    self.queryErrorHandler(error);
  } else {
    NSLog(@"%@ Unhandled Firestore error: %@. Set the queryErrorHandler property to debug.",
          self, error);
  }
}

#pragma mark - UICollectionViewDataSource methods

- (nonnull UICollectionViewCell *)collectionView:(nonnull UICollectionView *)collectionView
                          cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
  FIRDocumentSnapshot *snap = [self.collection.items objectAtIndex:indexPath.item];

  UICollectionViewCell *cell = self.populateCellAtIndexPath(collectionView, indexPath, snap);

  return cell;
}

- (NSInteger)numberOfSectionsInCollectionView:(nonnull UICollectionView *)collectionView {
  return 1;
}

- (NSInteger)collectionView:(nonnull UICollectionView *)collectionView
     numberOfItemsInSection:(NSInteger)section {
  return self.count;
}

@end

@implementation UICollectionView (FUIFirestoreCollectionViewDataSource)

- (FUIFirestoreCollectionViewDataSource *)bindToFirestoreQuery:(FIRQuery *)query
    populateCell:(UICollectionViewCell *(^)(UICollectionView *,
                                            NSIndexPath *,
                                            FIRDocumentSnapshot *))populateCell {
  FUIFirestoreCollectionViewDataSource *dataSource =
      [[FUIFirestoreCollectionViewDataSource alloc] initWithQuery:query populateCell:populateCell];
  [dataSource bindToView:self];
  return dataSource;
}

@end
