/*
 *  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 "CKDataSourceChangesetApplicator.h"

#import <atomic>
#import <vector>

#import <ComponentKit/CKDataSourceAppliedChanges.h>
#import <ComponentKit/CKDataSourceChange.h>
#import <ComponentKit/CKDataSourceChangesetModification.h>
#import <ComponentKit/CKDataSourceConfigurationInternal.h>
#import <ComponentKit/CKDataSourceInternal.h>
#import <ComponentKit/CKDataSourceListener.h>
#import <ComponentKit/CKDataSourceModificationHelper.h>
#import <ComponentKit/CKDataSourceQOSHelper.h>
#import <ComponentKit/CKDataSourceState.h>
#import <ComponentKit/CKDataSourceSplitChangesetModification.h>
#import <ComponentKit/CKNonNull.h>
#import <ComponentKit/CKSystraceScope.h>
#import <ComponentKit/CKTraitCollectionHelper.h>

static void *kQueueKey = &kQueueKey;
static NSString *const kChangesetApplicatorIdUserInfoKey = @"CKDataSourceChangesetApplicator.Id";

struct CKDataSourceChangesetApplicatorPipelineItem {
  CKDataSourceChangeset *changeset;
  NSDictionary *userInfo;
  CKDataSourceQOS qos;
  BOOL hasSplitChangeset;
};

@interface CKDataSourceChangesetApplicator () <CKDataSourceChangesetModificationItemGenerator, CKDataSourceListener>

@end

@implementation CKDataSourceChangesetApplicator
{
  __weak CKDataSource *_dataSource;
  CKDataSourceState *_dataSourceState;
  dispatch_queue_t _queue;
  NSNumber *_changesetApplicatorId;

  std::shared_ptr<CKTreeLayoutCache> _treeLayoutCache;

  std::vector<CKDataSourceChangesetApplicatorPipelineItem> _pipeline;
  NSUInteger _pipelineId;

  NSMapTable<CKDataSourceChangeset *, NSMapTable<id, CKDataSourceItem *> *> *_dataSourceItemCache;
  CKDataSourceChangeset *_currentChangeset;

  CKDataSourceViewport _viewport;
  UITraitCollection *_traitCollection;
}

- (instancetype)initWithDataSource:(CKDataSource *)dataSource
                             queue:(dispatch_queue_t)queue
{
  static std::atomic_int32_t globalChangesetApplicatorId;

  if (self = [super init]) {
    _dataSource = dataSource;
    _dataSourceState = dataSource.state;
    _queue = queue;
    _changesetApplicatorId = @(++globalChangesetApplicatorId);
    [_dataSource addListener:self];

    if (CKReadGlobalConfig().enableLayoutCaching) {
      _treeLayoutCache = std::make_shared<CKTreeLayoutCache>();
    }

    RCAssertNotNil(_queue, @"A dispatch queue must be specified for changeset applicator.");
    RCAssert(dispatch_queue_get_specific(_queue, kQueueKey) == NULL,
             @"Sharing queue between changeset applicators is not allowed.");
    dispatch_queue_set_specific(_queue, kQueueKey, kQueueKey, NULL);

    _dataSourceItemCache = _createMapTable();
  }
  return self;
}

- (void)dealloc
{
  dispatch_queue_set_specific(_queue, kQueueKey, NULL, NULL);
}

- (void)applyChangeset:(CKDataSourceChangeset *)changeset
              userInfo:(NSDictionary *)userInfo
                   qos:(CKDataSourceQOS)qos
{
  [self applyChangeset:changeset
              userInfo:userInfo
                   qos:qos
     hasSplitChangeset:NO];
}

- (void)applyChangeset:(CKDataSourceChangeset *)changeset
              userInfo:(NSDictionary *)userInfo
                   qos:(CKDataSourceQOS)qos
     hasSplitChangeset:(BOOL)hasSplitChangeset
{
  if (!_isRunningOnQueue()) {
    auto const asyncSwitchToApply = CK::Analytics::willStartAsyncBlock(CK::Analytics::BlockName::ChangeSetApplicatorWillSwitchToApply);
    dispatch_async(_queue, blockUsingDataSourceQOS(^{
      CKSystraceScope switchToApplyScope(asyncSwitchToApply);
      [self applyChangeset:changeset
                  userInfo:userInfo
                       qos:qos
         hasSplitChangeset:hasSplitChangeset];
    }, qos));
    return;
  }

  dispatch_async(dispatch_get_main_queue(), ^{
    self->_dataSource.shouldPauseStateUpdates = YES;
  });

  userInfo = _mergeUserInfoWithChangesetApplicatorId(userInfo, _changesetApplicatorId);
  BOOL shouldSplitChangeset = !hasSplitChangeset && _dataSourceState.configuration.options.splitChangesetOptions.enabled;

  // `_currentChangeset` is used in `buildDataSourceItemForPreviousRoot` for querying item cache for inserted items.
  _currentChangeset = changeset;
  __block CKDataSourceChange *change = nil;
  {
    id<CKDataSourceStateModifying> modification = nil;
    auto treeLayoutCacheCopy = _treeLayoutCache ? std::make_unique<CKTreeLayoutCache>(*_treeLayoutCache) : nullptr;
    if (!shouldSplitChangeset) {
      const auto m =
      [[CKDataSourceChangesetModification alloc]
       initWithChangeset:changeset
       stateListener:_dataSource
       userInfo:userInfo
       qos:qos
       treeLayoutCache:std::move(treeLayoutCacheCopy)];
      [m setItemGenerator:self];
      modification = m;
    } else {
      modification =
      [[CKDataSourceSplitChangesetModification alloc]
       initWithChangeset:changeset
       stateListener:_dataSource
       userInfo:userInfo
       viewport:_viewport
       qos:qos
       treeLayoutCache:std::move(treeLayoutCacheCopy)];
    }
    CKPerformWithCurrentTraitCollection(_traitCollection, ^{
      @autoreleasepool {
        change = [modification changeFromState:_dataSourceState];
      }
    });
  }
  _currentChangeset = nil;
  _dataSourceState = change.state;

  CKDataSourceChangeset *deferredChangeset = nil;
  if (!shouldSplitChangeset) {
    _pipeline.push_back({changeset, userInfo, qos, hasSplitChangeset});
  } else {
    deferredChangeset = change.deferredChangeset;
    // Changeset applicator will take over deferred changeset, so nil it out in `CKDataSourceChange`.
    change = [[CKDataSourceChange alloc]
              initWithState:change.state
              previousState:change.previousState
              appliedChanges:change.appliedChanges
              appliedChangeset:change.appliedChangeset
              deferredChangeset:nil
              addedComponentControllers:change.addedComponentControllers
              invalidComponentControllers:change.invalidComponentControllers];
    // Only push `appliedChangeset` to pipeline so that we can retry if
    // any  changeset application fails.
    _pipeline.push_back({change.appliedChangeset, userInfo, qos, YES});
  }
  NSUInteger pipelineId = _pipelineId;
  auto const willVerifyChange = CK::Analytics::willStartAsyncBlock(CK::Analytics::BlockName::ChangeSetApplicatorWillVerifyChange);
  dispatch_async(dispatch_get_main_queue(), ^{
    CKSystraceScope willVerifyChangeScope(willVerifyChange);
    const auto dataSource = self->_dataSource;
    if (!dataSource) {
      // We should stop processing changesets if `dataSource` is already deallocated.
      return;
    }
    const auto isValid = [dataSource verifyChange:change];
    const auto newState = dataSource.state;
    auto const willApplyChange = CK::Analytics::willStartAsyncBlock(CK::Analytics::BlockName::ChangeSetApplicatorWillApplyChange);

    if (CKReadGlobalConfig().enableLayoutCaching) {
      for (NSIndexPath *insertedIndex in [change.appliedChanges insertedIndexPaths]) {
        if ([newState numberOfSections] > insertedIndex.section && [newState numberOfObjectsInSection:insertedIndex.section] > insertedIndex.row) {
          CKDataSourceItem *insertedItem = [newState objectAtIndexPath:insertedIndex];
          _treeLayoutCache->update([[insertedItem scopeRoot] globalIdentifier], insertedItem.rootLayout.cache());
        }
      }
      for (NSIndexPath *updatedIndex in [change.appliedChanges finalUpdatedIndexPaths]) {
        if ([newState numberOfSections] > updatedIndex.section && [newState numberOfObjectsInSection:updatedIndex.section] > updatedIndex.row) {
          CKDataSourceItem *updatedItem = [newState objectAtIndexPath:updatedIndex];
          _treeLayoutCache->update([[updatedItem scopeRoot] globalIdentifier], updatedItem.rootLayout.cache());
        }
      }
    }

    dispatch_async(self->_queue, blockUsingDataSourceQOS(^{
      CKSystraceScope willApplyChangeScope(willApplyChange);
      if (self->_pipelineId != pipelineId) {
        // We don't need to handle the result since the current pipeline was discarded.
        return;
      }
      if (isValid) {
        self->_pipeline.erase(self->_pipeline.begin());
        [self->_dataSourceItemCache removeObjectForKey:changeset];
        if (self->_pipeline.size() == 0) {
          dispatch_async(dispatch_get_main_queue(), ^{
            self->_dataSource.shouldPauseStateUpdates = NO;
          });
        }
      } else {
        [self createNewPipelineWithNewDataSourceState:newState];
      }
    }, qos));
    __unused const auto isApplied = [dataSource applyChange:change];
    RCCAssert(isApplied == isValid, @"`CKDataSourceChange` is verified but not able to be applied.");
  });

  if (shouldSplitChangeset && deferredChangeset) {
    // In order to guarantee the order of applied changesets, we need to apply
    // deferred changeset in the same runloop.
    [self applyChangeset:deferredChangeset
                userInfo:userInfo
                     qos:qos
       hasSplitChangeset:YES];
  }
}

- (void)setViewPort:(CKDataSourceViewport)viewport
{
  dispatch_async(_queue, ^{
    _viewport = viewport;
  });
}

- (void)setTraitCollection:(UITraitCollection *)traitCollection
{
  dispatch_async(_queue, ^{
    _traitCollection = [traitCollection copy];
  });
}

#pragma mark - Internal

static BOOL _isRunningOnQueue()
{
  return dispatch_get_specific(kQueueKey) == kQueueKey;
}

static NSMapTable *_createMapTable()
{
  return [[NSMapTable alloc]
          initWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality
          valueOptions:NSPointerFunctionsStrongMemory
          capacity:10];
}

static NSDictionary *_mergeUserInfoWithChangesetApplicatorId(NSDictionary *userInfo,
                                                             NSNumber *changesetApplicatorId)
{
  NSMutableDictionary *mutableDictionary = [userInfo mutableCopy] ?: [NSMutableDictionary new];
  mutableDictionary[kChangesetApplicatorIdUserInfoKey] = changesetApplicatorId;
  return mutableDictionary;
}

- (void)createNewPipelineWithNewDataSourceState:(CKDataSourceState *)newState
{
  RCAssert(_isRunningOnQueue(), @"Pipeline must be created on process queue.");
  if (![_dataSourceState.configuration isEqual:newState.configuration]) {
    // Discard item cache if configuraiton is updated because `sizeRange` or `context` could affect layout.
    _dataSourceItemCache = _createMapTable();
  }
  _dataSourceState = newState;
  // A new pipeline is created and items in the existing pipeline are moved to the new one.
  _pipelineId++;
  const auto pipeline = _pipeline;
  _pipeline = {};
  for (const auto &item : pipeline) {
    [self applyChangeset:item.changeset
                userInfo:item.userInfo
                     qos:item.qos
       hasSplitChangeset:item.hasSplitChangeset];
  }
}

#pragma mark - CKDataSourceChangesetModificationItemGenerator

- (CKDataSourceItem *)buildDataSourceItemForPreviousRoot:(CK::NonNull<CKComponentScopeRoot *>)previousRoot
                                            stateUpdates:(const CKComponentStateUpdateMap &)stateUpdates
                                               sizeRange:(const CKSizeRange &)sizeRange
                                           configuration:(CKDataSourceConfiguration *)configuration
                                                   model:(id)model
                                                 context:(id)context
                                                itemType:(CKDataSourceChangesetModificationItemType)itemType
{
  RCAssert(_isRunningOnQueue(), @"`CKDataSourceItem` should be generated on process queue.");
  if (itemType != CKDataSourceChangesetModificationItemTypeInsert) {
    return CKBuildDataSourceItem(previousRoot, stateUpdates, sizeRange, configuration, model, context);
  }
  auto itemCache = [_dataSourceItemCache objectForKey:_currentChangeset];
  if (!itemCache) {
    itemCache = _createMapTable();
    [_dataSourceItemCache setObject:itemCache forKey:_currentChangeset];
  }
  auto dataSourceItem = [itemCache objectForKey:model];
  if (!dataSourceItem) {
    dataSourceItem = CKBuildDataSourceItem(previousRoot, stateUpdates, sizeRange, configuration, model, context);
    [itemCache setObject:dataSourceItem forKey:model];
  }
  return dataSourceItem;
}

#pragma mark - CKDataSourceListener

- (void)dataSource:(CKDataSource *)dataSource
didModifyPreviousState:(CKDataSourceState *)previousState
         withState:(CKDataSourceState *)state
 byApplyingChanges:(CKDataSourceAppliedChanges *)changes
{
  // We don't need to handle new dataSource state that are coming from changeset applicator itself.
  if ([_changesetApplicatorId isEqual:changes.userInfo[kChangesetApplicatorIdUserInfoKey]]) {
    return;
  }
  dispatch_async(_queue, ^{
    [self createNewPipelineWithNewDataSourceState:state];
  });
}

- (void)dataSource:(CKDataSource *)dataSource
willApplyDeferredChangeset:(CKDataSourceChangeset *)deferredChangeset
{

}

@end
