#import "TGDraggableCollectionView.h"

#import "TGDraggableCollectionViewFlowLayout.h"

#import "TGImageUtils.h"

NSString * const TGDraggableCollectionViewLayoutKey = @"collectionViewLayout";
const UIEdgeInsets TGDraggableDefaultScrollingTriggerEdgeInsets = { 60, 60, 60, 60 };
const CGFloat TGDraggableDefaultScrollingSpeed = 300.0f;

typedef enum
{
    TGDraggableCollectionScrollingDirectionUnknown,
    TGDraggableCollectionScrollingDirectionUp,
    TGDraggableCollectionScrollingDirectionLeft,
    TGDraggableCollectionScrollingDirectionRight,
    TGDraggableCollectionScrollingDirectionDown
} TGDraggableCollectionScrollingDirection;

@interface TGDraggableCollectionView () <UIGestureRecognizerDelegate>
{
    UIView *_draggedView;
    UIView *_insideSnapshotView;
    UIView *_outsideSnapshotView;
    
    NSIndexPath *_lastIndexPath;
    
    UIPanGestureRecognizer *_panGestureRecognizer;
    UILongPressGestureRecognizer *_pressGestureRecognizer;
    
    CADisplayLink *_scrollingDisplayLink;
    TGDraggableCollectionScrollingDirection _scrollingDirection;
}

@end

@implementation TGDraggableCollectionView

- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout
{
    NSAssert([layout isKindOfClass:[TGDraggableCollectionViewFlowLayout class]], @"collectionViewLayout must be an instance of TGDraggableCollectionViewFlowLayout");
    
    self = [super initWithFrame:frame collectionViewLayout:layout];
    if (self != nil)
    {
        _scrollingTriggerEdgeInsets = TGDraggableDefaultScrollingTriggerEdgeInsets;
        _scrollingSpeed = TGDraggableDefaultScrollingSpeed;
        
        _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleDragPan:)];
        _panGestureRecognizer.delegate = self;
        [self addGestureRecognizer:_panGestureRecognizer];
        
        _pressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleDragPress:)];
        _pressGestureRecognizer.minimumPressDuration = 0.25f;
        _pressGestureRecognizer.delegate = self;
        for (UIGestureRecognizer *gestureRecognizer in self.gestureRecognizers)
        {
            if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]])
                [gestureRecognizer requireGestureRecognizerToFail:_pressGestureRecognizer];
        }
        [self addGestureRecognizer:_pressGestureRecognizer];
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleApplicationWillResignActive:)
                                                     name:UIApplicationWillResignActiveNotification
                                                   object:nil];
    }
    return self;
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UIApplicationWillResignActiveNotification
                                                  object:nil];
}

- (void)handleApplicationWillResignActive:(NSNotification *)__unused notification
{
    _pressGestureRecognizer.enabled = false;
    _pressGestureRecognizer.enabled = true;
}

- (void)setDraggable:(bool)draggable
{
    _draggable = draggable;
    _pressGestureRecognizer.enabled = draggable;
    _panGestureRecognizer.enabled = draggable;
}

#pragma mark - Items

- (TGDraggableCollectionViewFlowLayout *)draggableLayout
{
    return (TGDraggableCollectionViewFlowLayout *)self.draggableLayout;
}

- (NSIndexPath *)indexPathForItemNearPoint:(CGPoint)point
{
    NSIndexPath *destinationIndexPath = self.draggableLayout.destinationIndexPath;
    self.draggableLayout.destinationIndexPath = nil;
    NSArray *layoutAttributes = [self.draggableLayout layoutAttributesForElementsInRect:self.bounds];
    self.draggableLayout.destinationIndexPath = destinationIndexPath;
 
    NSIndexPath *nearestItemIndexPath = nil;
    CGFloat minDistance = FLT_MAX;
    
    for (UICollectionViewLayoutAttributes *attributes in layoutAttributes)
    {
        CGFloat deltaX = attributes.center.x - point.x;
        CGFloat deltaY = attributes.center.y - point.y;
        CGFloat distance = (CGFloat)sqrt(deltaX * deltaX + deltaY * deltaY);
        
        if (distance < minDistance)
        {
            minDistance = distance;
            nearestItemIndexPath = attributes.indexPath;
        }
    }
    
    return nearestItemIndexPath;
}

#pragma mark - Dragged View

- (UIView *)_draggedViewSuperview
{
    return (self.draggedViewSuperview != nil) ? self.draggedViewSuperview : self;
}

- (UIView *)_prepareDraggedViewForCell:(UICollectionViewCell *)cell
{
    UIView *view = [[UIView alloc] initWithFrame:[self convertRect:cell.frame toView:[self _draggedViewSuperview]]];
    view.layer.rasterizationScale = TGScreenScaling();
    
    _outsideSnapshotView = [cell snapshotViewAfterScreenUpdates:false];
    [view addSubview:_outsideSnapshotView];
    
    _insideSnapshotView = [cell snapshotViewAfterScreenUpdates:false];
    
    _outsideSnapshotView.layer.shadowOffset = CGSizeZero;
    _outsideSnapshotView.layer.shadowColor = [UIColor blackColor].CGColor;
    _outsideSnapshotView.layer.shadowRadius = 3.0f;
    _outsideSnapshotView.layer.shadowOpacity = 0.87f;
    _outsideSnapshotView.layer.shadowPath = [UIBezierPath bezierPathWithRect:view.bounds].CGPath;
    
    return view;
}

- (void)_performDraggedViewHighlightTransitionWithDuration:(CGFloat)duration completion:(void (^)(void))completion
{
    [self addSubview:_insideSnapshotView];
    _insideSnapshotView.center = [self convertPoint:_draggedView.center fromView:[self _draggedViewSuperview]];
    
    _draggedView.layer.shouldRasterize = true;
    _draggedView.alpha = 0.0f;
    
    [UIView animateWithDuration:duration delay:0.0f options:UIViewAnimationOptionBeginFromCurrentState animations:^
     {
         _draggedView.alpha = 1.0f;
         _draggedView.transform = CGAffineTransformMakeScale(1.12f, 1.12f);
         _insideSnapshotView.transform = _draggedView.transform;
     } completion:^(BOOL finished)
     {
         if (finished)
         {
             _insideSnapshotView.hidden = true;
             if (completion != nil)
                 completion();
         }
     }];
}

- (void)_performDraggedViewUnhighlightTransitionWithDuration:(CGFloat)duration targetCellCenter:(CGPoint)targetCellCenter completion:(void (^)(void))completion
{
    _insideSnapshotView.center = [self convertPoint:_draggedView.center fromView:[self _draggedViewSuperview]];
    _insideSnapshotView.hidden = false;
    
    [UIView animateWithDuration:duration delay:0.0f options:UIViewAnimationOptionBeginFromCurrentState animations:^
     {
         _draggedView.alpha = 0.0f;
         _draggedView.transform = CGAffineTransformIdentity;
         _insideSnapshotView.transform = _draggedView.transform;
         _draggedView.center = [self convertPoint:targetCellCenter toView:[self _draggedViewSuperview]];
         _insideSnapshotView.center = targetCellCenter;
     } completion:^(__unused BOOL finished)
     {
         if (completion != nil)
             completion();
     }];
}

- (void)_performDraggedViewDropToIndexPath:(NSIndexPath *)destinationIndexPath completion:(void (^)(void))completion
{
    UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForItemAtIndexPath:destinationIndexPath];
    
    [self _performDraggedViewUnhighlightTransitionWithDuration:0.25f targetCellCenter:layoutAttributes.center completion:^
    {
        [_draggedView removeFromSuperview];
        _draggedView = nil;
        [_outsideSnapshotView removeFromSuperview];
        _outsideSnapshotView = nil;
        [_insideSnapshotView removeFromSuperview];
        _insideSnapshotView = nil;
         
        if (completion != nil)
            completion();
    }];
}

- (void)updateDestinationIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath == nil || [_lastIndexPath isEqual:indexPath])
        return;

    _lastIndexPath = indexPath;
    
    id<TGDraggableCollectionViewDataSource> dataSource = (id<TGDraggableCollectionViewDataSource>)self.dataSource;
    if ([dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:toIndexPath:)] &&
        ![dataSource collectionView:self canMoveItemAtIndexPath:self.draggableLayout.sourceIndexPath toIndexPath:indexPath])
    {
        return;
    }
    
    [self performBatchUpdates:^
    {
        self.draggableLayout.hiddenIndexPath = indexPath;
        self.draggableLayout.destinationIndexPath = indexPath;
    } completion:nil];
}

#pragma mark - Gesture Handling

- (void)handleDragPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
    switch (gestureRecognizer.state)
    {
        case UIGestureRecognizerStateBegan:
        {
            CGPoint location = [gestureRecognizer locationInView:self];
            NSIndexPath *indexPath = [self indexPathForItemAtPoint:location];
            if (indexPath == nil)
                return;
            
            id<TGDraggableCollectionViewDataSource> dataSource = (id<TGDraggableCollectionViewDataSource>)self.dataSource;
            if ([dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:)] &&
                ![dataSource collectionView:self canMoveItemAtIndexPath:indexPath])
            {
                return;
            }
            
            [_draggedView removeFromSuperview];
            _draggedView = [self _prepareDraggedViewForCell:[self cellForItemAtIndexPath:indexPath]];
            [[self _draggedViewSuperview] addSubview:_draggedView];
            
            [self _performDraggedViewHighlightTransitionWithDuration:0.25f completion:nil];
            
            _lastIndexPath = indexPath;
            self.draggableLayout.sourceIndexPath = indexPath;
            self.draggableLayout.destinationIndexPath = indexPath;
            self.draggableLayout.hiddenIndexPath = indexPath;
            [self.draggableLayout invalidateLayout];
        }
            break;
            
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled:
        {
            if (self.draggableLayout.sourceIndexPath == nil)
                return;
            
            NSIndexPath *sourceIndexPath = self.draggableLayout.sourceIndexPath;
            NSIndexPath *destinationIndexPath = self.draggableLayout.destinationIndexPath;
            
            id<TGDraggableCollectionViewDataSource> dataSource = (id<TGDraggableCollectionViewDataSource>)self.dataSource;
            [dataSource collectionView:self itemAtIndexPath:sourceIndexPath willMoveToIndexPath:destinationIndexPath];
            
            [self performBatchUpdates:^
            {
                [self moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
                self.draggableLayout.sourceIndexPath = nil;
                self.draggableLayout.destinationIndexPath = nil;
            } completion:nil];
            
            _pressGestureRecognizer.enabled = false;
            
            [self _performDraggedViewDropToIndexPath:destinationIndexPath completion:^
            {
                _pressGestureRecognizer.enabled = true;
                
                if ([dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:didMoveToIndexPath:)])
                    [dataSource collectionView:self itemAtIndexPath:sourceIndexPath didMoveToIndexPath:destinationIndexPath];
                
                self.draggableLayout.hiddenIndexPath = nil;
                [self.draggableLayout invalidateLayout];
            }];
            
            _lastIndexPath = nil;
            [self invalidateScrolling];
        }
            break;
            
        default:
            break;
    }
}

- (void)handleDragPan:(UIPanGestureRecognizer *)gestureRecognizer
{
    if (self.draggableLayout.sourceIndexPath == nil)
        return;
    
    switch (gestureRecognizer.state)
    {
        case UIGestureRecognizerStateBegan:
        case UIGestureRecognizerStateChanged:
        {
            CGPoint translation = [gestureRecognizer translationInView:self];
            [gestureRecognizer setTranslation:CGPointZero inView:self];
            
            _draggedView.center = CGPointMake(_draggedView.center.x + translation.x,
                                              _draggedView.center.y + translation.y);
            
            CGPoint center = [self convertPoint:_draggedView.center fromView:[self _draggedViewSuperview]];
            _insideSnapshotView.center = center;
            
            UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.draggableLayout;
            switch (layout.scrollDirection)
            {
                case UICollectionViewScrollDirectionHorizontal:
                {
                    if (center.x < (CGRectGetMinX(self.bounds) + self.scrollingTriggerEdgeInsets.left))
                    {
                        [self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionLeft];
                    }
                    else
                    {
                        if (center.x > (CGRectGetMaxX(self.bounds) - self.scrollingTriggerEdgeInsets.right))
                            [self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionRight];
                        else
                            [self invalidateScrolling];
                    }
                }
                    break;
                    
                case UICollectionViewScrollDirectionVertical:
                {
                    if (center.y < (CGRectGetMinY(self.bounds) + self.scrollingTriggerEdgeInsets.top))
                    {
                        [self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionUp];
                    }
                    else
                    {
                        if (center.y > (CGRectGetMaxY(self.bounds) - self.scrollingTriggerEdgeInsets.bottom))
                            [self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionDown];
                        else
                            [self invalidateScrolling];
                    }
                }
                    break;
                    
                default:
                    break;
            }
        }
            break;
            
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled:
        {
            [self invalidateScrolling];
        }
            break;
            
        default:
            break;
    }
    
    if (_scrollingDirection != TGDraggableCollectionScrollingDirectionUnknown)
        return;
    
    CGPoint location = [gestureRecognizer locationInView:self];
    NSIndexPath *indexPath = [self indexPathForItemNearPoint:location];
    [self updateDestinationIndexPath:indexPath];
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if (self.draggable && gestureRecognizer == _panGestureRecognizer)
        return (self.draggableLayout.sourceIndexPath != nil);
    
    return true;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if (gestureRecognizer == _pressGestureRecognizer && otherGestureRecognizer == _panGestureRecognizer)
        return true;
    else if (gestureRecognizer == _panGestureRecognizer && otherGestureRecognizer == _pressGestureRecognizer)
        return true;
    
    return false;
}

#pragma mark - Scrolling

- (void)setupScrollForDraggingInDirection:(TGDraggableCollectionScrollingDirection)direction
{
    if (_scrollingDirection == direction)
        return;
    
    [self invalidateScrolling];
    
    _scrollingDirection = direction;
    
    _scrollingDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDragScroll:)];
    [_scrollingDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)handleDragScroll:(CADisplayLink *)displayLink
{
    if (_scrollingDirection == TGDraggableCollectionScrollingDirectionUnknown)
        return;
    
    CGSize frameSize = self.bounds.size;
    CGSize contentSize = self.contentSize;
    CGPoint contentOffset = self.contentOffset;
    UIEdgeInsets contentInset = self.contentInset;
    
    CGFloat distance = (CGFloat)rint(self.scrollingSpeed * displayLink.duration);
    CGPoint translation = CGPointZero;
    
    switch (_scrollingDirection)
    {
        case TGDraggableCollectionScrollingDirectionUp:
        {
            distance = -distance;
            
            if (contentOffset.y + distance <= -contentInset.top)
                distance = -contentOffset.y - contentInset.top;
            
            translation = CGPointMake(0, distance);
        }
            break;
            
        case TGDraggableCollectionScrollingDirectionDown:
        {
            CGFloat maxY = contentSize.height - frameSize.height + contentInset.bottom;
            
            if ((contentOffset.y + distance) >= maxY)
                distance = maxY - contentOffset.y;
            
            translation = CGPointMake(0, distance);
        }
            break;
            
        case TGDraggableCollectionScrollingDirectionLeft:
        {
            distance = -distance;
            
            if (contentOffset.x + distance <= -contentInset.left)
                distance = -contentOffset.x - contentInset.left;
            
            translation = CGPointMake(distance, 0);
        }
            break;
            
        case TGDraggableCollectionScrollingDirectionRight:
        {
            CGFloat maxX = contentSize.width - frameSize.width + contentInset.right;
            
            if ((contentOffset.x + distance) >= maxX)
                distance = maxX - contentOffset.x;
            
            translation = CGPointMake(distance, 0);
        }
            break;
            
        default:
            break;
    }

    self.contentOffset = CGPointMake(contentOffset.x + translation.x,
                                     contentOffset.y + translation.y);
    
    NSIndexPath *indexPath = [self indexPathForItemNearPoint:_draggedView.center];
    [self updateDestinationIndexPath:indexPath];
}

- (void)invalidateScrolling
{
    [_scrollingDisplayLink invalidate];
    _scrollingDisplayLink = nil;
    
    _scrollingDirection = TGDraggableCollectionScrollingDirectionUnknown;
}

@end
