/**
 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 "POPAnimator.h"
#import "POPAnimatorPrivate.h"

#import <list>
#import <objc/objc-auto.h>
#import <vector>

#import <QuartzCore/QuartzCore.h>

#import "POPAnimation.h"
#import "POPAnimationExtras.h"
#import "POPBasicAnimationInternal.h"
#import "POPDecayAnimation.h"

using namespace std;
using namespace POP;

#define ENABLE_LOGGING_DEBUG 0
#define ENABLE_LOGGING_INFO 0

#if ENABLE_LOGGING_DEBUG
#define FBLogAnimDebug NSLog
#else
#define FBLogAnimDebug(...)
#endif

#if ENABLE_LOGGING_INFO
#define FBLogAnimInfo NSLog
#else
#define FBLogAnimInfo(...)
#endif

class POPAnimatorItem
{
public:
  id __weak object;
  NSString *key;
  POPAnimation *animation;
  NSInteger refCount;
  id __unsafe_unretained unretainedObject;

  POPAnimatorItem(id o, NSString *k, POPAnimation *a) POP_NOTHROW
  {
    object = o;
    key = [k copy];
    animation = a;
    refCount = 1;
    unretainedObject = o;
  }

  ~POPAnimatorItem()
  {
  }

  bool operator==(const POPAnimatorItem& o) const {
    return unretainedObject == o.unretainedObject && animation == o.animation && [key isEqualToString:o.key];
  }

};

typedef std::shared_ptr<POPAnimatorItem> POPAnimatorItemRef;
typedef std::shared_ptr<const POPAnimatorItem> POPAnimatorItemConstRef;

typedef std::list<POPAnimatorItemRef> POPAnimatorItemList;
typedef POPAnimatorItemList::iterator POPAnimatorItemListIterator;
typedef POPAnimatorItemList::const_iterator POPAnimatorItemListConstIterator;

static BOOL _disableBackgroundThread = YES;

@interface POPAnimator ()
{
#if TARGET_OS_IPHONE
  CADisplayLink *_displayLink;
#else
  CVDisplayLinkRef _displayLink;
#endif
  POPAnimatorItemList _list;
  CFMutableDictionaryRef _dict;
  NSMutableArray *_observers;
  POPAnimatorItemList _pendingList;
  CFRunLoopObserverRef _pendingListObserver;
  CFTimeInterval _slowMotionStartTime;
  CFTimeInterval _slowMotionLastTime;
  CFTimeInterval _slowMotionAccumulator;
  CFTimeInterval _beginTime;
  OSSpinLock _lock;
  BOOL _disableDisplayLink;
}
@end

@implementation POPAnimator
@synthesize delegate = _delegate;
@synthesize disableDisplayLink = _disableDisplayLink;
@synthesize beginTime = _beginTime;

#if !TARGET_OS_IPHONE
static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *context)
{
  if (_disableBackgroundThread) {
      dispatch_async(dispatch_get_main_queue(), ^{
        [(__bridge POPAnimator*)context render];
      });
  } else {
    [(__bridge POPAnimator*)context render];
  }
  return kCVReturnSuccess;
}
#endif

// call while holding lock
static void updateDisplayLink(POPAnimator *self)
{
  BOOL paused = (0 == self->_observers.count && self->_list.empty()) || self->_disableDisplayLink;

#if TARGET_OS_IPHONE
  if (paused != self->_displayLink.paused) {
    FBLogAnimInfo(paused ? @"pausing display link" : @"unpausing display link");
    self->_displayLink.paused = paused;
  }
#else
  if (paused == CVDisplayLinkIsRunning(self->_displayLink)) {
    FBLogAnimInfo(paused ? @"pausing display link" : @"unpausing display link");
    if (paused) {
      CVDisplayLinkStop(self->_displayLink);
    } else {
      CVDisplayLinkStart(self->_displayLink);
    }
  }
#endif
}

static void updateAnimatable(id obj, POPPropertyAnimationState *anim, bool shouldAvoidExtraneousWrite = false)
{
  // handle user-initiated stop or pause; hault animation
  if (!anim->active || anim->paused)
    return;

  if (anim->hasValue()) {
    pop_animatable_write_block write = anim->property.writeBlock;
    if (NULL == write)
      return;

    // current animation value
    VectorRef currentVec = anim->currentValue();

    if (!anim->additive) {

      // if avoiding extraneous writes and we have a read block defined
      if (shouldAvoidExtraneousWrite) {

        pop_animatable_read_block read = anim->property.readBlock;
        if (read) {
          // compare current animation value with object value
          Vector4r currentValue = currentVec->vector4r();
          Vector4r objectValue = read_values(read, obj, anim->valueCount);
          if (objectValue == currentValue) {
            return;
          }
        }
      }
      
      // update previous values; support animation convergence
      anim->previous2Vec = anim->previousVec;
      anim->previousVec = currentVec;

      // write value
      write(obj, currentVec->data());
      if (anim->tracing) {
        [anim->tracer writePropertyValue:POPBox(currentVec, anim->valueType, true)];
      }
    } else {
      pop_animatable_read_block read = anim->property.readBlock;
      NSCAssert(read, @"additive requires an animatable property readBlock");
      if (NULL == read) {
        return;
      }

      // object value
      Vector4r objectValue = read_values(read, obj, anim->valueCount);

      // current value
      Vector4r currentValue = currentVec->vector4r();
      
      // determine animation change
      if (anim->previousVec) {
        Vector4r previousValue = anim->previousVec->vector4r();
        currentValue -= previousValue;
      }

      // avoid writing no change
      if (shouldAvoidExtraneousWrite && currentValue == Vector4r::Zero()) {
        return;
      }
      
      // add to object value
      currentValue += objectValue;
      
      // update previous values; support animation convergence
      anim->previous2Vec = anim->previousVec;
      anim->previousVec = currentVec;
      
      // write value
      write(obj, currentValue.data());
      if (anim->tracing) {
        [anim->tracer writePropertyValue:POPBox(currentVec, anim->valueType, true)];
      }
    }
  }
}

static void applyAnimationTime(id obj, POPAnimationState *state, CFTimeInterval time)
{
  if (!state->advanceTime(time, obj)) {
    return;
  }
  
  POPPropertyAnimationState *ps = dynamic_cast<POPPropertyAnimationState*>(state);
  if (NULL != ps) {
    updateAnimatable(obj, ps);
  }
  
  state->delegateApply();
}

static void applyAnimationToValue(id obj, POPAnimationState *state)
{
  POPPropertyAnimationState *ps = dynamic_cast<POPPropertyAnimationState*>(state);

  if (NULL != ps) {
    
    // finalize progress
    ps->finalizeProgress();
    
    // write to value, updating only if needed
    updateAnimatable(obj, ps, true);
  }
  
  state->delegateApply();
}

static POPAnimation *deleteDictEntry(POPAnimator *self, id __unsafe_unretained obj, NSString *key, BOOL cleanup = YES)
{
  POPAnimation *anim = nil;

  // lock
  OSSpinLockLock(&self->_lock);

  NSMutableDictionary *keyAnimationsDict = (__bridge id)CFDictionaryGetValue(self->_dict, (__bridge void *)obj);
  if (keyAnimationsDict) {

    anim = keyAnimationsDict[key];
    if (anim) {

      // remove key
      [keyAnimationsDict removeObjectForKey:key];

      // cleanup empty dictionaries
      if (cleanup && 0 == keyAnimationsDict.count) {
        CFDictionaryRemoveValue(self->_dict, (__bridge void *)obj);
      }
    }
  }

  // unlock
  OSSpinLockUnlock(&self->_lock);
  return anim;
}

static void stopAndCleanup(POPAnimator *self, POPAnimatorItemRef item, bool shouldRemove, bool finished)
{
  // remove
  if (shouldRemove) {
    deleteDictEntry(self, item->unretainedObject, item->key);
  }

  // stop
  POPAnimationState *state = POPAnimationGetState(item->animation);
  state->stop(shouldRemove, finished);

  if (shouldRemove) {
    // lock
    OSSpinLockLock(&self->_lock);

    // find item in list
    // may have already been removed on animationDidStop:
    POPAnimatorItemListIterator find_iter = find(self->_list.begin(), self->_list.end(), item);
    BOOL found = find_iter != self->_list.end();

    if (found) {
      self->_list.erase(find_iter);
    }

    // unlock
    OSSpinLockUnlock(&self->_lock);
  }
}

+ (id)sharedAnimator
{
  static POPAnimator* _animator = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _animator = [[POPAnimator alloc] init];
  });
  return _animator;
}

+ (BOOL)disableBackgroundThread
{
  return _disableBackgroundThread;
}

+ (void)setDisableBackgroundThread:(BOOL)flag
{
  _disableBackgroundThread = flag;
}

#pragma mark - Lifecycle

- (id)init
{
  self = [super init];
  if (nil == self) return nil;

#if TARGET_OS_IPHONE
  _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render)];
  _displayLink.paused = YES;
  [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
#else
  CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
  CVDisplayLinkSetOutputCallback(_displayLink, displayLinkCallback, (__bridge void *)self);
#endif

  _dict = POPDictionaryCreateMutableWeakPointerToStrongObject(5);
  _lock = OS_SPINLOCK_INIT;

  return self;
}

- (void)dealloc
{
#if TARGET_OS_IPHONE
  [_displayLink invalidate];
#else
  CVDisplayLinkStop(_displayLink);
  CVDisplayLinkRelease(_displayLink);
#endif
  [self _clearPendingListObserver];
}

#pragma mark - Utility

- (void)_processPendingList
{
  // rendering pending animations
  CFTimeInterval time = [self _currentRenderTime];
  [self _renderTime:(0 != _beginTime) ? _beginTime : time items:_pendingList];

  // lock
  OSSpinLockLock(&_lock);

  // clear list and observer
  _pendingList.clear();
  [self _clearPendingListObserver];

  // unlock
  OSSpinLockUnlock(&_lock);
}

- (void)_clearPendingListObserver
{
  if (_pendingListObserver) {
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _pendingListObserver, kCFRunLoopCommonModes);
    CFRelease(_pendingListObserver);
    _pendingListObserver = NULL;
  }
}

- (void)_scheduleProcessPendingList
{
  // see WebKit for magic numbers, eg http://trac.webkit.org/changeset/166540
  static const CFIndex CATransactionCommitRunLoopOrder = 2000000;
  static const CFIndex POPAnimationApplyRunLoopOrder = CATransactionCommitRunLoopOrder - 1;

  // lock
  OSSpinLockLock(&_lock);

  if (!_pendingListObserver) {
    __weak POPAnimator *weakSelf = self;

    _pendingListObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting | kCFRunLoopExit, false, POPAnimationApplyRunLoopOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
      [weakSelf _processPendingList];
    });

    if (_pendingListObserver) {
      CFRunLoopAddObserver(CFRunLoopGetMain(), _pendingListObserver,  kCFRunLoopCommonModes);
    }
  }

  // unlock
  OSSpinLockUnlock(&_lock);
}

- (void)_renderTime:(CFTimeInterval)time items:(std::list<POPAnimatorItemRef>)items
{
  // begin transaction with actions disabled
  [CATransaction begin];
  [CATransaction setDisableActions:YES];

  // notify delegate
  __strong __typeof__(_delegate) delegate = _delegate;
  [delegate animatorWillAnimate:self];

  // lock
  OSSpinLockLock(&_lock);

  // count active animations
  const NSUInteger count = items.size();
  if (0 == count) {
    // unlock
    OSSpinLockUnlock(&_lock);
  } else {
    // copy list into vectory
    std::vector<POPAnimatorItemRef> vector{ std::begin(items), std::end(items) };

    // unlock
    OSSpinLockUnlock(&_lock);

    for (auto item : vector) {
      [self _renderTime:time item:item];
    }
  }

  // notify observers
  for (id observer in self.observers) {
    [observer animatorDidAnimate:(id)self];
  }

  // lock
  OSSpinLockLock(&_lock);

  // update display link
  updateDisplayLink(self);

  // unlock
  OSSpinLockUnlock(&_lock);

  // notify delegate and commit
  [delegate animatorDidAnimate:self];
  [CATransaction commit];
}

- (void)_renderTime:(CFTimeInterval)time item:(POPAnimatorItemRef)item
{
  id obj = item->object;
  POPAnimation *anim = item->animation;
  POPAnimationState *state = POPAnimationGetState(anim);

  if (nil == obj) {
    // object exists not; stop animating
    NSAssert(item->unretainedObject, @"object should exist");
    stopAndCleanup(self, item, true, false);
  } else {

    // start if needed
    state->startIfNeeded(obj, time, _slowMotionAccumulator);

    // only run active, not paused animations
    if (state->active && !state->paused) {
      // object exists; animate
      applyAnimationTime(obj, state, time);

      FBLogAnimDebug(@"time:%f running:%@", time, item->animation);
      if (state->isDone()) {
        // set end value
        applyAnimationToValue(obj, state);

        state->repeatCount--;
        if (state->repeatForever || state->repeatCount > 0) {
          if ([anim isKindOfClass:[POPPropertyAnimation class]]) {
            POPPropertyAnimation *propAnim = (POPPropertyAnimation *)anim;
            id oldFromValue = propAnim.fromValue;
            propAnim.fromValue = propAnim.toValue;

            if (state->autoreverses) {
              if (state->tracing) {
                [state->tracer autoreversed];
              }

              if (state->type == kPOPAnimationDecay) {
                POPDecayAnimation *decayAnimation = (POPDecayAnimation *)propAnim;
                decayAnimation.velocity = [decayAnimation reversedVelocity];
              } else {
                propAnim.toValue = oldFromValue;
              }
            } else {
              if (state->type == kPOPAnimationDecay) {
                POPDecayAnimation *decayAnimation = (POPDecayAnimation *)propAnim;
                id originalVelocity = decayAnimation.originalVelocity;
                decayAnimation.velocity = originalVelocity;
              } else {
                propAnim.fromValue = oldFromValue;
              }
            }
          }

          state->stop(NO, NO);
          state->reset(true);

          state->startIfNeeded(obj, time, _slowMotionAccumulator);
        } else {
          stopAndCleanup(self, item, state->removedOnCompletion, YES);
        }
      }
    }
  }
}

#pragma mark - API

- (NSArray *)observers
{
  // lock
  OSSpinLockLock(&_lock);

  // get observers
  NSArray *observers = 0 != _observers.count ? [_observers copy] : nil;

  // unlock
  OSSpinLockUnlock(&_lock);
  return observers;
}

- (void)addAnimation:(POPAnimation *)anim forObject:(id)obj key:(NSString *)key
{
  if (!anim || !obj) {
    return;
  }

  // support arbitrarily many nil keys
  if (!key) {
    key = [[NSUUID UUID] UUIDString];
  }

  // lock
  OSSpinLockLock(&_lock);

  // get key, animation dict associated with object
  NSMutableDictionary *keyAnimationDict = (__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj);

  // update associated animation state
  if (nil == keyAnimationDict) {
    keyAnimationDict = [NSMutableDictionary dictionary];
    CFDictionarySetValue(_dict, (__bridge void *)obj, (__bridge void *)keyAnimationDict);
  } else {
    // if the animation instance already exists, avoid cancelling only to restart
    POPAnimation *existingAnim = keyAnimationDict[key];
    if (existingAnim) {
      // unlock
      OSSpinLockUnlock(&_lock);

      if (existingAnim == anim) {
        return;
      }
      [self removeAnimationForObject:obj key:key cleanupDict:NO];
        
      // lock
      OSSpinLockLock(&_lock);
    }
  }
  keyAnimationDict[key] = anim;

  // create entry after potential removal
  POPAnimatorItemRef item(new POPAnimatorItem(obj, key, anim));

  // add to list and pending list
  _list.push_back(item);
  _pendingList.push_back(item);

  // support animation re-use, reset all animation state
  POPAnimationGetState(anim)->reset(true);

  // update display link
  updateDisplayLink(self);

  // unlock
  OSSpinLockUnlock(&_lock);

  // schedule runloop processing of pending animations
  [self _scheduleProcessPendingList];
}

- (void)removeAllAnimationsForObject:(id)obj
{
  // lock
  OSSpinLockLock(&_lock);

  NSArray *animations = [(__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj) allValues];
  CFDictionaryRemoveValue(_dict, (__bridge void *)obj);

  // unlock
  OSSpinLockUnlock(&_lock);

  if (0 == animations.count) {
    return;
  }

  NSHashTable *animationSet = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality capacity:animations.count];
  for (id animation in animations) {
    [animationSet addObject:animation];
  }

  // lock
  OSSpinLockLock(&_lock);

  POPAnimatorItemRef item;
  for (auto iter = _list.begin(); iter != _list.end();) {
    item = *iter;
    if(![animationSet containsObject:item->animation]) {
      iter++;
    } else {
      iter = _list.erase(iter);
    }
  }

  // unlock
  OSSpinLockUnlock(&_lock);

  for (POPAnimation *anim in animations) {
    POPAnimationState *state = POPAnimationGetState(anim);
    state->stop(true, !state->active);
  }
}

- (void)removeAnimationForObject:(id)obj key:(NSString *)key cleanupDict:(BOOL)cleanupDict
{
  POPAnimation *anim = deleteDictEntry(self, obj, key, cleanupDict);
  if (nil == anim) {
    return;
  }

  // lock
  OSSpinLockLock(&_lock);

  // remove from list
  POPAnimatorItemRef item;
  for (auto iter = _list.begin(); iter != _list.end();) {
    item = *iter;
    if(anim == item->animation) {
      _list.erase(iter);
      break;
    } else {
      iter++;
    }
  }

  // remove from pending list
  for (auto iter = _pendingList.begin(); iter != _pendingList.end();) {
    item = *iter;
    if(anim == item->animation) {
      _pendingList.erase(iter);
      break;
    } else {
      iter++;
    }
  }

  // unlock
  OSSpinLockUnlock(&_lock);

  // stop animation and callout
  POPAnimationState *state = POPAnimationGetState(anim);
  state->stop(true, (!state->active && !state->paused));
}

- (void)removeAnimationForObject:(id)obj key:(NSString *)key
{
  [self removeAnimationForObject:obj key:key cleanupDict:YES];
}

- (NSArray *)animationKeysForObject:(id)obj
{
  // lock
  OSSpinLockLock(&_lock);

  // get keys
  NSArray *keys = [(__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj) allKeys];

  // unlock
  OSSpinLockUnlock(&_lock);
  return keys;
}

- (id)animationForObject:(id)obj key:(NSString *)key
{
  // lock
  OSSpinLockLock(&_lock);

  // lookup animation
  NSDictionary *keyAnimationsDict = (__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj);
  POPAnimation *animation = keyAnimationsDict[key];

  // unlock
  OSSpinLockUnlock(&_lock);
  return animation;
}

- (CFTimeInterval)_currentRenderTime
{
  CFTimeInterval time = CACurrentMediaTime();

#if TARGET_IPHONE_SIMULATOR
  // support slow-motion animations
  time += _slowMotionAccumulator;
  float f = POPAnimationDragCoefficient();

  if (f > 1.0) {
    if (!_slowMotionStartTime) {
      _slowMotionStartTime = time;
    } else {
      time = (time - _slowMotionStartTime) / f + _slowMotionStartTime;
      _slowMotionLastTime = time;
    }
  } else if (_slowMotionStartTime) {
    CFTimeInterval dt = (_slowMotionLastTime - time);
    time += dt;
    _slowMotionAccumulator += dt;
    _slowMotionStartTime = 0;
  }
#endif

  return time;
}

- (void)render
{
  CFTimeInterval time = [self _currentRenderTime];
  [self renderTime:time];
}

- (void)renderTime:(CFTimeInterval)time
{
  [self _renderTime:time items:_list];
}

- (void)addObserver:(id<POPAnimatorObserving>)observer
{
  NSAssert(nil != observer, @"attempting to add nil %@ observer", self);
  if (nil == observer) {
    return;
  }

  // lock
  OSSpinLockLock(&_lock);

  if (!_observers) {
    // use ordered collection for deterministic callout
    _observers = [[NSMutableArray alloc] initWithCapacity:1];
  }

  [_observers addObject:observer];
  updateDisplayLink(self);

  // unlock
  OSSpinLockUnlock(&_lock);
}

- (void)removeObserver:(id<POPAnimatorObserving>)observer
{
  NSAssert(nil != observer, @"attempting to remove nil %@ observer", self);
  if (nil == observer) {
    return;
  }

  // lock
  OSSpinLockLock(&_lock);

  [_observers removeObject:observer];
  updateDisplayLink(self);

  // unlock
  OSSpinLockUnlock(&_lock);
}

@end
