/*
 *  Copyright (c) 2014-present, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 *
 */

#import "CKDataSourceAppliedChanges.h"

#import <ComponentKit/RCEqualityHelpers.h>
#import <ComponentKit/CKMacros.h>

#import "CKIndexSetDescription.h"
#import "ComponentUtilities.h"

@implementation CKDataSourceAppliedChanges

- (instancetype)init
{
  return [self initWithUpdatedIndexPaths:nil
                       removedIndexPaths:nil
                         removedSections:nil
                         movedIndexPaths:nil
                        insertedSections:nil
                      insertedIndexPaths:nil
                                userInfo:nil];
}

- (instancetype)initWithUpdatedIndexPaths:(NSSet *)updatedIndexPaths
                        removedIndexPaths:(NSSet *)removedIndexPaths
                          removedSections:(NSIndexSet *)removedSections
                          movedIndexPaths:(NSDictionary *)movedIndexPaths
                         insertedSections:(NSIndexSet *)insertedSections
                       insertedIndexPaths:(NSSet *)insertedIndexPaths
                                 userInfo:(NSDictionary *)userInfo
{
  if (self = [super init]) {
    _updatedIndexPaths = [updatedIndexPaths copy] ?: [NSSet set];
    _removedIndexPaths = [removedIndexPaths copy] ?: [NSSet set];
    _removedSections = [removedSections copy] ?: [NSIndexSet indexSet];
    _movedIndexPaths = [movedIndexPaths copy] ?: @{};
    _insertedSections = [insertedSections copy] ?: [NSIndexSet indexSet];
    _insertedIndexPaths = [insertedIndexPaths copy] ?: [NSSet set];
    _userInfo = [userInfo copy];
    _finalUpdatedIndexPaths = finalUpdatedIndexPaths(_updatedIndexPaths,
                                                     _removedIndexPaths,
                                                     _removedSections,
                                                     _movedIndexPaths,
                                                     _insertedSections,
                                                     _insertedIndexPaths);
  }
  return self;
}

- (BOOL)isEmpty
{
  return [_updatedIndexPaths count] == 0 && [_removedIndexPaths count] == 0 &&
  [_removedSections count] == 0 && [_movedIndexPaths count] == 0 &&
  [_insertedSections count] == 0 && [_insertedIndexPaths count] == 0;
}

- (NSString *)description
{
  if ([self isEmpty]) {
    return @"";
  }

  auto const description = [NSMutableString new];
  [description appendString:@"{\n"];
  [description appendString:indexPathsDescriptionWithTitle(_updatedIndexPaths, @"Updated Items")];
  [description appendString:indexPathsDescriptionWithTitle(_removedIndexPaths, @"Removed Items")];
  [description appendString:withNewLineIfNotEmpty(CK::indexSetDescription(_removedSections, @"Removed Sections", 2))];
  [description appendString:indexPathToIndexPathMapDescriptionWithTitle(_movedIndexPaths, @"Moved Items")];
  [description appendString:withNewLineIfNotEmpty(CK::indexSetDescription(_insertedSections, @"Inserted Sections", 2))];
  [description appendString:indexPathsDescriptionWithTitle(_insertedIndexPaths, @"Inserted Items")];
  [description appendString:@"}"];
  return description;
}

- (BOOL)isEqual:(id)object
{
  return RCCompareObjectEquality(self, object, ^BOOL(CKDataSourceAppliedChanges *a, CKDataSourceAppliedChanges *b) {
    return RCObjectIsEqual(a.updatedIndexPaths, b.updatedIndexPaths)
    && RCObjectIsEqual(a.removedIndexPaths, b.removedIndexPaths)
    && RCObjectIsEqual(a.removedSections, b.removedSections)
    && RCObjectIsEqual(a.movedIndexPaths, b.movedIndexPaths)
    && RCObjectIsEqual(a.insertedSections, b.insertedSections)
    && RCObjectIsEqual(a.insertedIndexPaths, b.insertedIndexPaths)
    && RCObjectIsEqual(a.userInfo, b.userInfo);
  });
}

- (NSUInteger)hash
{
  NSUInteger subhashes[] = {
    [_updatedIndexPaths hash],
    [_removedIndexPaths hash],
    [_removedSections hash],
    [_movedIndexPaths hash],
    [_insertedSections hash],
    [_insertedIndexPaths hash],
    [_userInfo hash],
  };
  return RCIntegerArrayHash(subhashes, CK_ARRAY_COUNT(subhashes));
}

static auto withNewLineIfNotEmpty(NSString *s) -> NSString *
{
  return s.length > 0 ? [s stringByAppendingString:@"\n"] : @"";
}

static auto indexPathsDescriptionWithTitle(NSSet<NSIndexPath *> *indexPaths, NSString *title) -> NSString *
{
  if ([indexPaths count] == 0) {
    return @"";
  }

  auto description = [NSMutableString new];
  [description appendFormat:@"  %@: {\n", title];

  auto const sortedIndexPaths = [[indexPaths allObjects] sortedArrayUsingSelector:@selector(compare:)];
  auto const indexPathStrs = [NSMutableArray<NSString *> new];
  for (NSIndexPath *const indexPath : sortedIndexPaths) {
    auto const ipStr = [NSString stringWithFormat:@"    (%ld-%ld)", (long)indexPath.section, (long)indexPath.item];
    [indexPathStrs addObject:ipStr];
  }
  [description appendString:[indexPathStrs componentsJoinedByString:@",\n"]];

  [description appendString:@"\n  }\n"];
  return description;
}

static auto indexPathToIndexPathMapDescriptionWithTitle(NSDictionary<NSIndexPath *, NSIndexPath *> *map, NSString *title) -> NSString *
{
  if ([map count] == 0) {
    return @"";
  }

  auto description = [NSMutableString new];
  [description appendFormat:@"  %@: {\n", title];

  auto const sortedIndexPaths = [[map allKeys] sortedArrayUsingSelector:@selector(compare:)];
  auto const indexPathStrs = [NSMutableArray<NSString *> new];
  for (NSIndexPath *const indexPath : sortedIndexPaths) {
    auto const toIndexPath = map[indexPath];
    auto const ipStr = [NSString stringWithFormat:@"    (%ld-%ld) -> (%ld-%ld)",
                        (long)indexPath.section, (long)indexPath.item,
                        (long)toIndexPath.section, (long)toIndexPath.item];
    [indexPathStrs addObject:ipStr];
  }
  [description appendString:[indexPathStrs componentsJoinedByString:@",\n"]];

  [description appendString:@"\n  }\n"];
  return description;
}

static NSArray *sortedIndexPaths(NSArray<NSIndexPath *> *indexPaths, BOOL reverse)
{
  return [indexPaths sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath *_Nonnull obj1, NSIndexPath *_Nonnull obj2) {
    NSComparisonResult sectionComparison = [@(obj1.section) compare:@(obj2.section)];
    if (sectionComparison != NSOrderedSame) {
      return (NSComparisonResult)(sectionComparison * (reverse ? -1 : 1));
    } else {
      return (NSComparisonResult)([@(obj1.row) compare:@(obj2.row)] * (reverse ? -1 : 1));
    }
  }];
}

static NSIndexPath *indexPathWithDeltas(NSIndexPath *indexPath, NSUInteger sectionDelta, NSUInteger rowDelta)
{
  return [NSIndexPath indexPathForRow:(indexPath.row + rowDelta) inSection:(indexPath.section + sectionDelta)];
}

static NSDictionary<NSIndexPath *, NSIndexPath *> *finalUpdatedIndexPaths(NSSet *updatedIndexPaths,
                                                                          NSSet *removedIndexPaths,
                                                                          NSIndexSet *removedSections,
                                                                          NSDictionary *movedIndexPaths,
                                                                          NSIndexSet *insertedSections,
                                                                          NSSet *insertedIndexPaths)
{
  // The dictionary that will be returned will have the structure:
  // (old update index path) -> (new update index path)
  NSMutableDictionary<NSIndexPath *, NSIndexPath *> *finalUpdatedIndexPaths = [NSMutableDictionary new];

  // Initialize with updated index paths mapping to themselves
  for (NSIndexPath *indexPath in updatedIndexPaths) {
    finalUpdatedIndexPaths[indexPath] = indexPath;
  }

  // Translate moves into removals and insertions
  NSArray *moveRemovals = [movedIndexPaths allKeys];
  NSArray *moveInsertions = [movedIndexPaths allValues];

  // Removed rows
  // Reverse sort (hence the -1 coefficient) so we don't end up in a situation like the following
  // Updating (0, 5) and removing (0,2) (0,3) (0,4). Since the removed index paths are unordered (NSSet) we could end up in a situation where we remove (0,2) and (0,3), so (0,5) -> (0,3) and when we remove (0,4) the updated row is assumed unaffected
  NSArray *allRemovals = [[removedIndexPaths allObjects] arrayByAddingObjectsFromArray:moveRemovals];
  for (NSIndexPath *removedIndexPath in sortedIndexPaths(allRemovals, YES)) {
    for (NSIndexPath *sourceUpdatePath in updatedIndexPaths) {
      NSIndexPath *destinationUpdatePath = finalUpdatedIndexPaths[sourceUpdatePath];
      if (removedIndexPath.section == destinationUpdatePath.section &&
          removedIndexPath.row < destinationUpdatePath.row) {
        finalUpdatedIndexPaths[sourceUpdatePath] = indexPathWithDeltas(destinationUpdatePath, 0, -1);
      }
    }
  }

  // Removed sections
  [removedSections enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger removedSection, BOOL *_Nonnull removedSectionsStop) {
    for (NSIndexPath *sourceUpdatePath in updatedIndexPaths) {
      NSIndexPath *destinationUpdatePath = finalUpdatedIndexPaths[sourceUpdatePath];
      if (removedSection < destinationUpdatePath.section) {
        finalUpdatedIndexPaths[sourceUpdatePath] = indexPathWithDeltas(destinationUpdatePath, -1, 0);
      }
    }
  }];

  // Inserted sections
  [insertedSections enumerateIndexesUsingBlock:^(NSUInteger insertedSection, BOOL *_Nonnull insertedSectionsStop) {
    for (NSIndexPath *sourceUpdatePath in updatedIndexPaths) {
      NSIndexPath *destinationUpdatePath = finalUpdatedIndexPaths[sourceUpdatePath];
      if (insertedSection <= destinationUpdatePath.section) {
        finalUpdatedIndexPaths[sourceUpdatePath] = indexPathWithDeltas(destinationUpdatePath, 1, 0);
      }
    }
  }];

  // Inserted rows
  // Sort for the same reason as above when we sorted the removed index paths
  // First take care of the "normal" insertions, where we check the destination update path
  NSArray *allInsertions = [[insertedIndexPaths allObjects] arrayByAddingObjectsFromArray:moveInsertions];
  for (NSIndexPath *insertedIndexPath in sortedIndexPaths(allInsertions, NO)) {
    for (NSIndexPath *sourceUpdatePath in updatedIndexPaths) {
      NSIndexPath *destinationUpdatePath = finalUpdatedIndexPaths[sourceUpdatePath];
      if (insertedIndexPath.section == destinationUpdatePath.section &&
          insertedIndexPath.row <= destinationUpdatePath.row) {
        finalUpdatedIndexPaths[sourceUpdatePath] = indexPathWithDeltas(destinationUpdatePath, 0, 1);
      }
    }
  }

  // The destination index path of a move is *always* correct and we have it take the precedence over all above.
  // If one inserts a section at the front, even a move within an existing section
  // has to provide the correct final section index.
  [movedIndexPaths enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *sourceIndexPath, NSIndexPath *destinationIndexPath, BOOL *_Nonnull stop) {
    if (finalUpdatedIndexPaths[sourceIndexPath]) {
      finalUpdatedIndexPaths[sourceIndexPath] = destinationIndexPath;
    }
  }];

  return finalUpdatedIndexPaths;
}

@end
