/* 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 "ASFlowLayoutController.h"
#import "ASAssert.h"
#import "ASDisplayNode.h"
#import "ASIndexPath.h"

#include <map>
#include <vector>
#include <cassert>

static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3;

@interface ASFlowLayoutController()
{
  ASIndexPathRange _visibleRange;
  std::vector<ASIndexPathRange> _rangesByType;  // All ASLayoutRangeTypes besides visible.
}

@end

@implementation ASFlowLayoutController

- (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection
{
  if (!(self = [super init])) {
    return nil;
  }
  _layoutDirection = layoutDirection;
  _rangesByType = std::vector<ASIndexPathRange>(ASLayoutRangeTypeCount);
  return self;
}

#pragma mark - Visible Indices

// FIXME: This method can be removed once ASRangeControllerBeta becomes the main version.
- (BOOL)shouldUpdateForVisibleIndexPaths:(NSArray *)indexPaths rangeType:(ASLayoutRangeType)rangeType
{
  if (!indexPaths.count || rangeType >= _rangesByType.size()) {
    return NO;
  }

  ASIndexPathRange existingRange = _rangesByType[rangeType];
  ASIndexPathRange newRange = [self indexPathRangeForIndexPaths:indexPaths];
  
  ASIndexPath maximumStart = ASIndexPathMaximum(existingRange.start, newRange.start);
  ASIndexPath minimumEnd = ASIndexPathMinimum(existingRange.end, newRange.end);
  
  if (ASIndexPathEqualToIndexPath(maximumStart, existingRange.start) || ASIndexPathEqualToIndexPath(minimumEnd, existingRange.end)) {
    return YES;
  }

  NSInteger newStartDelta       = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.start, newRange.start)];
  NSInteger existingStartDelta  = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.start, existingRange.start)] * kASFlowLayoutControllerRefreshingThreshold;
  
  NSInteger newEndDelta         = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.end, newRange.end)];
  NSInteger existingEndDelta    = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.end, existingRange.end)] * kASFlowLayoutControllerRefreshingThreshold;
  
  return (newStartDelta > existingStartDelta) || (newEndDelta > existingEndDelta);
}

- (void)setVisibleNodeIndexPaths:(NSArray *)indexPaths
{
  _visibleRange = [self indexPathRangeForIndexPaths:indexPaths];
}

/**
 * IndexPath array for the element in the working range.
 */

- (NSSet *)indexPathsForScrolling:(ASScrollDirection)scrollDirection rangeType:(ASLayoutRangeType)rangeType
{
  CGFloat viewportScreenMetric;
  ASScrollDirection leadingDirection;
  CGSize viewportSize = [self viewportSize];

  if (_layoutDirection == ASFlowLayoutDirectionHorizontal) {
    ASDisplayNodeAssert(scrollDirection == ASScrollDirectionNone || scrollDirection == ASScrollDirectionLeft || scrollDirection == ASScrollDirectionRight, @"Invalid scroll direction");

    viewportScreenMetric = viewportSize.width;
    leadingDirection = ASScrollDirectionLeft;
  } else {
    ASDisplayNodeAssert(scrollDirection == ASScrollDirectionNone || scrollDirection == ASScrollDirectionUp || scrollDirection == ASScrollDirectionDown, @"Invalid scroll direction");

    viewportScreenMetric = viewportSize.height;
    leadingDirection = ASScrollDirectionUp;
  }

  ASRangeTuningParameters tuningParameters = [self tuningParametersForRangeType:rangeType];
  CGFloat backScreens = scrollDirection == leadingDirection ? tuningParameters.leadingBufferScreenfuls : tuningParameters.trailingBufferScreenfuls;
  CGFloat frontScreens = scrollDirection == leadingDirection ? tuningParameters.trailingBufferScreenfuls : tuningParameters.leadingBufferScreenfuls;

  
  ASIndexPath startPath = [self findIndexPathAtDistance:(-backScreens * viewportScreenMetric) fromIndexPath:_visibleRange.start];
  ASIndexPath endPath = [self findIndexPathAtDistance:(frontScreens * viewportScreenMetric) fromIndexPath:_visibleRange.end];

  ASDisplayNodeAssert(startPath.section <= endPath.section, @"startPath should never begin at a further position than endPath");
  
  NSMutableSet *indexPathSet = [[NSMutableSet alloc] init];

  NSArray *completedNodes = [_dataSource completedNodes];

  ASIndexPath currPath = startPath;
  
  while (!ASIndexPathEqualToIndexPath(currPath, endPath)) {
    [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:currPath]];
    currPath.row++;

    // Once we reach the end of the section, advance to the next one.  Keep advancing if the next section is zero-sized.
    while (currPath.row >= [(NSArray *)completedNodes[currPath.section] count] && currPath.section < completedNodes.count - 1) {
      currPath.row = 0;
      currPath.section++;
      ASDisplayNodeAssert(currPath.section <= endPath.section, @"currPath should never reach a further section than endPath");
    }
  }

  [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:endPath]];
  
  return indexPathSet;
}

#pragma mark - Utility

- (ASIndexPathRange)indexPathRangeForIndexPaths:(NSArray *)indexPaths
{
  // Set up an initial value so the MIN and MAX can work in the enumeration.
  __block ASIndexPath currentIndexPath = [[indexPaths firstObject] ASIndexPathValue];
  __block ASIndexPathRange range;
  range.start = currentIndexPath;
  range.end = currentIndexPath;
  
  [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
    currentIndexPath = [indexPath ASIndexPathValue];
    range.start = ASIndexPathMinimum(range.start, currentIndexPath);
    range.end = ASIndexPathMaximum(range.end, currentIndexPath);
  }];
  return range;
}

- (ASIndexPath)findIndexPathAtDistance:(CGFloat)distance fromIndexPath:(ASIndexPath)start
{
  // "end" is the index path we'll advance until we have gone far enough from "start" to reach "distance"
  ASIndexPath end = start;
  // "previous" will store one iteration before "end", in case we go too far and need to reset "end" to be "previous"
  ASIndexPath previous = start;

  NSArray *completedNodes = [_dataSource completedNodes];
  NSUInteger numberOfSections = [completedNodes count];
  NSUInteger numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count];
  
  // If "distance" is negative, advance "end" backwards across rows and sections.
  // Otherwise, advance forward.  In either case, bring "distance" closer to zero by the dimension of each row passed.
  if (distance < 0.0 && end.section >= 0 && end.section < numberOfSections && end.row >= 0 && end.row < numberOfRowsInSection) {
    while (distance < 0.0 && end.section >= 0 && end.row >= 0) {
      previous = end;
      ASDisplayNode *node = completedNodes[end.section][end.row];
      CGSize size = node.calculatedSize;
      distance += (_layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height);
      end.row--;
      // If we've gone to a negative row, set to the last row of the previous section.  While loop is required to handle empty sections.
      while (end.row < 0 && end.section > 0) {
        end.section--;
        numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count];
        end.row = numberOfRowsInSection - 1;
      }
    }

    if (end.row < 0) {
      end = previous;
    }
  } else {
    while (distance > 0.0 && end.section >= 0 && end.section < numberOfSections && end.row >= 0 && end.row < numberOfRowsInSection) {
      previous = end;
      ASDisplayNode *node = completedNodes[end.section][end.row];
      CGSize size = node.calculatedSize;
      distance -= _layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height;

      end.row++;
      // If we've gone beyond the section, reset to the beginning of the next section.  While loop is required to handle empty sections.
      while (end.row >= numberOfRowsInSection && end.section < numberOfSections - 1) {
        end.row = 0;
        end.section++;
        numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count];
      }
    }

    if (end.row >= numberOfRowsInSection) {
      end = previous;
    }
  }

  return end;
}

- (NSInteger)flowLayoutDistanceForRange:(ASIndexPathRange)range
{
  // This method should only be called with the range in proper order (start comes before end).
  ASDisplayNodeAssert(ASIndexPathEqualToIndexPath(ASIndexPathMinimum(range.start, range.end), range.start), @"flowLayoutDistanceForRange: called with invalid range");
  
  if (ASIndexPathEqualToIndexPath(range.start, range.end)) {
    return 0;
  }
  
  NSInteger totalRowCount = 0;
  NSUInteger numberOfRowsInSection = 0;
  NSArray *completedNodes = [_dataSource completedNodes];

  for (NSInteger section = range.start.section; section <= range.end.section; section++) {
    numberOfRowsInSection = [(NSArray *)completedNodes[section] count];
    totalRowCount += numberOfRowsInSection;
    
    if (section == range.start.section) {
      // For the start section, make sure we don't count the rows before the start row.
      totalRowCount -= range.start.row;
    } else if (section == range.end.section) {
      // For the start section, make sure we don't count the rows after the end row.
      totalRowCount -= (numberOfRowsInSection - (range.end.row + 1));
    }
  }
  
  ASDisplayNodeAssert(totalRowCount >= 0, @"totalRowCount in flowLayoutDistanceForRange: should not be negative");
  return totalRowCount;
}

@end
