#import "RNSScreenStackAnimator.h"
#import "RNSScreenStack.h"

#import "RNSScreen.h"

#pragma mark - Constants

// Default duration for transitions in seconds. Note, that this enforces the default
// only on Paper. On Fabric the transition duration coming from JS layer
// is never null, thus it defaults to the value set in component codegen spec.
static constexpr NSTimeInterval RNSDefaultTransitionDuration = 0.5;

// Proportions for diffrent phases of more complex animations.
// The reference duration differs from default transition duration,
// because we've changed the default duration & we want to keep proportions
// in tact. Unit = seconds.
static constexpr NSTimeInterval RNSTransitionDurationForProportion = 0.35;

static constexpr float RNSSlideOpenTransitionDurationProportion = 1;
static constexpr float RNSFadeOpenTransitionDurationProportion = 0.2 / RNSTransitionDurationForProportion;
static constexpr float RNSSlideCloseTransitionDurationProportion = 0.25 / RNSTransitionDurationForProportion;
static constexpr float RNSFadeCloseTransitionDurationProportion = 0.15 / RNSTransitionDurationForProportion;
static constexpr float RNSFadeCloseDelayTransitionDurationProportion = 0.1 / RNSTransitionDurationForProportion;

// Value used for dimming view attached for tranistion time.
// Same value is used in other projects using similar approach for transistions
// and it looks the most similar to the value used by Apple
static constexpr float RNSShadowViewMaxAlpha = 0.1;

@implementation RNSScreenStackAnimator {
UINavigationControllerOperation _operation;
NSTimeInterval _transitionDuration;
UIViewPropertyAnimator *_Nullable _inFlightAnimator;
}

- (instancetype)initWithOperation:(UINavigationControllerOperation)operation
{
if (self = [super init]) {
_operation = operation;
_transitionDuration = RNSDefaultTransitionDuration; // default duration in seconds
_inFlightAnimator = nil;
}
return self;
}

#pragma mark - UIViewControllerAnimatedTransitioning

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
RNSScreenView *screen;
if (_operation == UINavigationControllerOperationPush) {
UIViewController *toViewController =
[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
screen = ((RNSScreen *)toViewController).screenView;
} else if (_operation == UINavigationControllerOperationPop) {
UIViewController *fromViewController =
[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
screen = ((RNSScreen *)fromViewController).screenView;
}

if (screen != nil && screen.stackAnimation == RNSScreenStackAnimationNone) {
return 0;
}

if (screen != nil && screen.transitionDuration != nil && [screen.transitionDuration floatValue] >= 0) {
float durationInSeconds = [screen.transitionDuration floatValue] / 1000.0;
return durationInSeconds;
}

return _transitionDuration;
}

- (id<UIViewImplicitlyAnimating>)interruptibleAnimatorForTransition:
(id<UIViewControllerContextTransitioning>)transitionContext
{
return _inFlightAnimator;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromViewController =
[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];

RNSScreenView *screen;
if (_operation == UINavigationControllerOperationPush) {
screen = ((RNSScreen *)toViewController).screenView;
} else if (_operation == UINavigationControllerOperationPop) {
screen = ((RNSScreen *)fromViewController).screenView;
}

if (screen != nil) {
if ([screen.reactSuperview isKindOfClass:[RNSScreenStackView class]] &&
((RNSScreenStackView *)(screen.reactSuperview)).customAnimation) {
[self animateWithNoAnimation:transitionContext toVC:toViewController fromVC:fromViewController];
} else if (screen.fullScreenSwipeEnabled && transitionContext.isInteractive) {
// we are swiping with full width gesture
if (screen.customAnimationOnSwipe) {
[self animateTransitionWithStackAnimation:screen.stackAnimation
shadowEnabled:screen.fullScreenSwipeShadowEnabled
transitionContext:transitionContext
toVC:toViewController
fromVC:fromViewController];
} else {
// we have to provide an animation when swiping, otherwise the screen will be popped immediately,
// so in case of no custom animation on swipe set, we provide the one closest to the default
[self animateSimplePushWithShadowEnabled:screen.fullScreenSwipeShadowEnabled
transitionContext:transitionContext
toVC:toViewController
fromVC:fromViewController];
}
} else {
// we are going forward or provided custom animation on swipe or clicked native header back button
[self animateTransitionWithStackAnimation:screen.stackAnimation
shadowEnabled:screen.fullScreenSwipeShadowEnabled
transitionContext:transitionContext
toVC:toViewController
fromVC:fromViewController];
}
}
}

- (void)animationEnded:(BOOL)transitionCompleted
{
_inFlightAnimator = nil;
}

#pragma mark - Animation implementations

- (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled
transitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
float containerWidth = transitionContext.containerView.bounds.size.width;
float belowViewWidth = containerWidth * 0.3;

CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0);
CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0);

if (toViewController.navigationController.view.semanticContentAttribute ==
UISemanticContentAttributeForceRightToLeft) {
rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0);
leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0);
}

UIView *shadowView;
if (shadowEnabled) {
shadowView = [[UIView alloc] initWithFrame:fromViewController.view.frame];
shadowView.backgroundColor = [UIColor blackColor];
}

if (_operation == UINavigationControllerOperationPush) {
toViewController.view.transform = rightTransform;
[[transitionContext containerView] addSubview:toViewController.view];
if (shadowView) {
[[transitionContext containerView] insertSubview:shadowView belowSubview:toViewController.view];
shadowView.alpha = 0.0;
}

UIViewPropertyAnimator *animator =
[[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
timingParameters:[RNSScreenStackAnimator defaultSpringTimingParametersApprox]];

[animator addAnimations:^{
fromViewController.view.transform = leftTransform;
toViewController.view.transform = CGAffineTransformIdentity;
if (shadowView) {
shadowView.alpha = RNSShadowViewMaxAlpha;
}
}];

[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
if (shadowView) {
[shadowView removeFromSuperview];
}
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
toViewController.view.transform = leftTransform;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
if (shadowView) {
[[transitionContext containerView] insertSubview:shadowView belowSubview:fromViewController.view];
shadowView.alpha = RNSShadowViewMaxAlpha;
}

void (^animationBlock)(void) = ^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = rightTransform;
if (shadowView) {
shadowView.alpha = 0.0;
}
};

void (^completionBlock)(UIViewAnimatingPosition) = ^(UIViewAnimatingPosition finalPosition) {
if (shadowView) {
[shadowView removeFromSuperview];
}
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
};

if (!transitionContext.isInteractive) {
UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc]
initWithDuration:[self transitionDuration:transitionContext]
timingParameters:[RNSScreenStackAnimator defaultSpringTimingParametersApprox]];

[animator addAnimations:animationBlock];
[animator addCompletion:completionBlock];
_inFlightAnimator = animator;
[animator startAnimation];
} else {
// we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option
UIViewPropertyAnimator *animator =
[[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveLinear
animations:animationBlock];

[animator addCompletion:completionBlock];
[animator setUserInteractionEnabled:YES];
_inFlightAnimator = animator;
}
}
}

- (void)animateSlideFromLeftWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
float containerWidth = transitionContext.containerView.bounds.size.width;
float belowViewWidth = containerWidth * 0.3;

CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0);
CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0);

if (toViewController.navigationController.view.semanticContentAttribute ==
UISemanticContentAttributeForceRightToLeft) {
rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0);
leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0);
}

if (_operation == UINavigationControllerOperationPush) {
toViewController.view.transform = rightTransform;
[[transitionContext containerView] addSubview:toViewController.view];

UIViewPropertyAnimator *animator =
[[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
timingParameters:[RNSScreenStackAnimator defaultSpringTimingParametersApprox]];

[animator addAnimations:^{
fromViewController.view.transform = leftTransform;
toViewController.view.transform = CGAffineTransformIdentity;
}];
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
toViewController.view.transform = leftTransform;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];

void (^animationBlock)(void) = ^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = rightTransform;
};
void (^completionBlock)(UIViewAnimatingPosition) = ^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
};

if (!transitionContext.isInteractive) {
UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc]
initWithDuration:[self transitionDuration:transitionContext]
timingParameters:[RNSScreenStackAnimator defaultSpringTimingParametersApprox]];

[animator addAnimations:animationBlock];
[animator addCompletion:completionBlock];
_inFlightAnimator = animator;
[animator startAnimation];
} else {
// we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option
UIViewPropertyAnimator *animator =
[[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveLinear
animations:animationBlock];
[animator addCompletion:completionBlock];
[animator setUserInteractionEnabled:YES];
_inFlightAnimator = animator;
}
}
}

- (void)animateFadeWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];

if (_operation == UINavigationControllerOperationPush) {
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.alpha = 0.0;
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveEaseInOut
animations:^{
toViewController.view.alpha = 1.0;
}];
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
toViewController.view.alpha = 1.0;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveEaseInOut
animations:^{
fromViewController.view.alpha = 0.0;
}];
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.alpha = 1.0;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
}
}

- (void)animateSlideFromBottomWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
CGAffineTransform topBottomTransform =
CGAffineTransformMakeTranslation(0, transitionContext.containerView.bounds.size.height);

if (_operation == UINavigationControllerOperationPush) {
toViewController.view.transform = topBottomTransform;
[[transitionContext containerView] addSubview:toViewController.view];

auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveEaseInOut
animations:^{
fromViewController.view.transform =
CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
}];
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
toViewController.view.transform = CGAffineTransformIdentity;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];

void (^animationBlock)(void) = ^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = topBottomTransform;
};
void (^completionBlock)(UIViewAnimatingPosition) = ^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
};

if (!transitionContext.isInteractive) {
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveEaseInOut
animations:animationBlock];
[animator addCompletion:completionBlock];
_inFlightAnimator = animator;
[animator startAnimation];
} else {
// we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveLinear
animations:animationBlock];
[animator addCompletion:completionBlock];
_inFlightAnimator = animator;
}
}
}

- (void)animateFadeFromBottomWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
CGAffineTransform topBottomTransform =
CGAffineTransformMakeTranslation(0, 0.08 * transitionContext.containerView.bounds.size.height);

const float baseTransitionDuration = [self transitionDuration:transitionContext];

if (_operation == UINavigationControllerOperationPush) {
toViewController.view.transform = topBottomTransform;
toViewController.view.alpha = 0.0;
[[transitionContext containerView] addSubview:toViewController.view];

// Android Nougat open animation
// http://aosp.opersys.com/xref/android-7.1.2_r37/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml
auto slideAnimator = [[UIViewPropertyAnimator alloc]
initWithDuration:baseTransitionDuration * RNSSlideOpenTransitionDurationProportion
curve:UIViewAnimationCurveEaseOut
animations:^{
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
}];
[slideAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];

auto fadeAnimator = [[UIViewPropertyAnimator alloc]
initWithDuration:baseTransitionDuration * RNSFadeOpenTransitionDurationProportion
curve:UIViewAnimationCurveEaseOut
animations:^{
toViewController.view.alpha = 1.0;
}];

_inFlightAnimator = slideAnimator;
[slideAnimator startAnimation];
[fadeAnimator startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
toViewController.view.transform = CGAffineTransformIdentity;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];

// Android Nougat exit animation
// http://aosp.opersys.com/xref/android-7.1.2_r37/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml
auto slideAnimator = [[UIViewPropertyAnimator alloc]
initWithDuration:baseTransitionDuration * RNSSlideCloseTransitionDurationProportion
curve:UIViewAnimationCurveEaseIn
animations:^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = topBottomTransform;
}];
[slideAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.alpha = 1.0;
toViewController.view.alpha = 1.0;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];

auto fadeAnimator = [[UIViewPropertyAnimator alloc]
initWithDuration:baseTransitionDuration * RNSFadeCloseTransitionDurationProportion
curve:UIViewAnimationCurveLinear
animations:^{
fromViewController.view.alpha = 0.0;
}];

_inFlightAnimator = slideAnimator;
[slideAnimator startAnimation];
[fadeAnimator startAnimationAfterDelay:baseTransitionDuration * RNSFadeCloseDelayTransitionDurationProportion];
}
}

- (void)animateWithNoAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
if (_operation == UINavigationControllerOperationPush) {
[[transitionContext containerView] addSubview:toViewController.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
}
completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
} else if (_operation == UINavigationControllerOperationPop) {
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];

[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
}
completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
}

#pragma mark - Public API

- (nullable id<UITimingCurveProvider>)timingParamsForAnimationCompletion
{
return [RNSScreenStackAnimator defaultSpringTimingParametersApprox];
}

+ (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation
{
return (animation != RNSScreenStackAnimationFlip && animation != RNSScreenStackAnimationDefault);
}

#pragma mark - Helpers

- (void)animateTransitionWithStackAnimation:(RNSScreenStackAnimation)animation
shadowEnabled:(BOOL)shadowEnabled
transitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toVC
fromVC:(UIViewController *)fromVC
{
if (animation == RNSScreenStackAnimationSimplePush) {
[self animateSimplePushWithShadowEnabled:shadowEnabled transitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
} else if (animation == RNSScreenStackAnimationSlideFromLeft) {
[self animateSlideFromLeftWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
} else if (animation == RNSScreenStackAnimationFade || animation == RNSScreenStackAnimationNone) {
[self animateFadeWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
} else if (animation == RNSScreenStackAnimationSlideFromBottom) {
[self animateSlideFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
} else if (animation == RNSScreenStackAnimationFadeFromBottom) {
[self animateFadeFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
}
// simple_push is the default custom animation
[self animateSimplePushWithShadowEnabled:shadowEnabled transitionContext:transitionContext toVC:toVC fromVC:fromVC];
}

+ (UISpringTimingParameters *)defaultSpringTimingParametersApprox
{
// Default curve provider is as defined below, however spring timing defined this way
// ignores the requested duration of the animation, effectively impairing our `animationDuration` prop.
// We want to keep `animationDuration` functional.
// id<UITimingCurveProvider> timingCurveProvider = [[UISpringTimingParameters alloc] init];

// According to "Programming iOS 14" by Matt Neuburg, the params for the default spring are as follows:
// mass = 3, stiffness = 1000, damping = 500. Damping ratio is computed using formula
// ratio = damping / (2 * sqrt(stiffness * mass)) ==> default damping ratio should be ~= 4,56.
// I've found afterwards that this is even indicated here:
// https://developer.apple.com/documentation/uikit/uispringtimingparameters/1649802-init?language=objc

return [[UISpringTimingParameters alloc] initWithDampingRatio:4.56];
}

@end