//
//  MRProgressOverlayView.m
//  MRProgress
//
//  Created by Marius Rackwitz on 09.10.13.
//  Copyright (c) 2013 Marius Rackwitz. All rights reserved.
//

//#define MRProgress_EnableUIVisualEffectView
#if defined(MRProgress_EnableUIVisualEffectView)
    #define MR_UIEffectViewIsEnabled   1
#else
    #define MR_UIEffectViewIsEnabled   0
#endif
#define MR_UIEffectViewIsAllowed   (MR_UIEffectViewIsEnabled && __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000)
#define MR_UIEffectViewIsAvailable (MR_UIEffectViewIsAllowed && NSClassFromString(@"UIVisualEffectView") != nil)

#import <QuartzCore/QuartzCore.h>
#import <CoreGraphics/CoreGraphics.h>
#import "MRProgressOverlayView.h"
#import "MRActivityIndicatorView.h"
#import "MRBlurView.h"
#import "MRCircularProgressView.h"
#import "MRIconView.h"
#import "MRProgressHelper.h"


static const CGFloat MRProgressOverlayViewCornerRadius = 7;
static const CGFloat MRProgressOverlayViewMotionEffectExtent = 10;


@interface MRProgressOverlayView () {
    NSDictionary *_savedAttributes;
}

@property (nonatomic, weak, readwrite) UIView *dialogView;
@property (nonatomic, weak, readwrite) UIView *blurView;
@property (nonatomic, strong, readwrite) UIView *blurMaskView;
@property (nonatomic, weak, readwrite) UILabel *titleLabel;

- (UIView *)createModeView;
- (UIView *)createViewForMode:(MRProgressOverlayViewMode)mode;

- (MRActivityIndicatorView *)createActivityIndicatorView;
- (MRActivityIndicatorView *)createSmallActivityIndicatorView;
- (UIActivityIndicatorView *)createSmallDefaultActivityIndicatorView;
- (MRCircularProgressView *)createCircularProgressView;
- (UIProgressView *)createHorizontalBarProgressView;
- (MRIconView *)createCheckmarkIconView;
- (MRIconView *)createCrossIconView;
- (UIView *)createCustomView;

- (void)showModeView:(UIView *)modeView;
- (void)hideModeView:(UIView *)modeView;

- (BOOL)mayStop;

- (void)setSubviewTransform:(CGAffineTransform)transform alpha:(CGFloat)alpha;

- (void)registerForNotificationCenter;
- (void)unregisterFromNotificationCenter;
- (void)deviceOrientationDidChange:(NSNotification *)notification;

- (void)registerForKVO;
- (void)unregisterFromKVO;
- (NSArray *)observableKeypaths;

- (CGAffineTransform)transformForOrientation;

- (NSDictionary *)titleTextAttributesToCopy;

@end


@implementation MRProgressOverlayView

static void *MRProgressOverlayViewObservationContext = &MRProgressOverlayViewObservationContext;

#pragma mark - Static helper methods

+ (instancetype)showOverlayAddedTo:(UIView *)view animated:(BOOL)animated {
    MRProgressOverlayView *overlayView = [self new];
    [view addSubview:overlayView];
    [overlayView show:animated];
    return overlayView;
}

+ (instancetype)showOverlayAddedTo:(UIView *)view title:(NSString *)title mode:(MRProgressOverlayViewMode)mode animated:(BOOL)animated {
    MRProgressOverlayView *overlayView = [self new];
    overlayView.mode = mode;
    overlayView.titleLabelText = title;
    [view addSubview:overlayView];
    [overlayView show:animated];
    return overlayView;
}

+ (instancetype)showOverlayAddedTo:(UIView *)view title:(NSString *)title mode:(MRProgressOverlayViewMode)mode animated:(BOOL)animated stopBlock:(MRProgressOverlayViewStopBlock)stopBlock {
    MRProgressOverlayView *overlayView = [self new];
    overlayView.mode = mode;
    overlayView.titleLabelText = title;
    overlayView.stopBlock = stopBlock;
    [view addSubview:overlayView];
    [overlayView show:animated];
    return overlayView;
}

+ (BOOL)dismissOverlayForView:(UIView *)view animated:(BOOL)animated {
    return [self dismissOverlayForView:view animated:animated completion:nil];
}

+ (BOOL)dismissOverlayForView:(UIView *)view animated:(BOOL)animated completion:(void(^)())completionBlock {
    MRProgressOverlayView *overlayView = [self overlayForView:view];
    if (overlayView) {
        [overlayView dismiss:animated completion:completionBlock];
        return YES;
    }
    return NO;
}

+ (NSUInteger)dismissAllOverlaysForView:(UIView *)view animated:(BOOL)animated {
    return [self dismissAllOverlaysForView:view animated:animated completion:nil];
}

+ (NSUInteger)dismissAllOverlaysForView:(UIView *)view animated:(BOOL)animated completion:(void(^)())completionBlock {
    NSArray *views = [self allOverlaysForView:view];
    for (MRProgressOverlayView *overlayView in views) {
        [overlayView dismiss:animated completion:completionBlock];
    }
    return views.count;
}

+ (instancetype)overlayForView:(UIView *)view {
    NSEnumerator *subviewsEnum = view.subviews.reverseObjectEnumerator;
    for (UIView *subview in subviewsEnum) {
        if ([subview isKindOfClass:self]) {
            return (MRProgressOverlayView *)subview;
        }
    }
    return nil;
}

+ (NSArray *)allOverlaysForView:(UIView *)view {
    NSMutableArray *overlays = [NSMutableArray new];
    NSArray *subviews = view.subviews;
    for (UIView *view in subviews) {
        if ([view isKindOfClass:self]) {
            [overlays addObject:view];
        }
    }
    return overlays;
}


#pragma mark - Initialization

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (void)commonInit {
    self.accessibilityViewIsModal = YES;
    
    self.hidden = YES;
    self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    
    const CGFloat cornerRadius = MRProgressOverlayViewCornerRadius;
    
    // Create blurView
    self.blurView = [self createBlurView];
    
    // Create container with contents
    UIView *dialogView = [UIView new];
    [self addSubview:dialogView];
    self.dialogView = dialogView;
    [self applyMotionEffects];
    
    // Style the dialog to match the iOS7 UIAlertView
    dialogView.backgroundColor = UIColor.clearColor;
    dialogView.layer.cornerRadius = cornerRadius;
    dialogView.layer.shadowRadius = cornerRadius + 5;
    dialogView.layer.shadowOpacity = 0.1f;
    dialogView.layer.shadowOffset = CGSizeMake(-(cornerRadius+5)/2.0f, -(cornerRadius+5)/2.0f);
    
    // Create titleLabel
    UILabel *titleLabel = [UILabel new];
    self.titleLabel = titleLabel;
    self.titleLabel.attributedText = [[NSAttributedString alloc] initWithString:@"Loading ..." attributes:@{
        NSForegroundColorAttributeName: UIColor.blackColor,
        NSFontAttributeName:            [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline],
        NSKernAttributeName:            NSNull.null,  // turn on auto-kerning
    }];
    titleLabel.accessibilityTraits = UIAccessibilityTraitHeader;
    titleLabel.textAlignment = NSTextAlignmentCenter;
    titleLabel.numberOfLines = 0;
    titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
    [dialogView addSubview:titleLabel];
    
    // Create modeView
    [self createModeView];
    
    // Observe key paths and notification center
    [self registerForKVO];
    [self registerForNotificationCenter];
    
    [self tintColorDidChange];
}


#pragma mark - Clean up

- (void)dealloc {
    [self unregisterFromKVO];
    [self unregisterFromNotificationCenter];
}


#pragma mark - Notifications

- (void)registerForNotificationCenter {
    NSNotificationCenter *center = NSNotificationCenter.defaultCenter;
    [center addObserver:self selector:@selector(deviceOrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil];
}

- (void)unregisterFromNotificationCenter {
    NSNotificationCenter *center = NSNotificationCenter.defaultCenter;
    [center removeObserver:self];
}

- (void)deviceOrientationDidChange:(NSNotification *)notification {
    if ([self.superview isKindOfClass:UIWindow.class]) {
        [UIView animateWithDuration:0.3 animations:^{
            [self manualLayoutSubviews];
        }];
    } else {
        [self manualLayoutSubviews];
    }
}


#pragma mark - Key-Value-Observing

- (void)registerForKVO {
    for (NSString *keyPath in self.observableKeypaths) {
        [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionPrior context:MRProgressOverlayViewObservationContext];
    }
}

- (void)unregisterFromKVO {
    for (NSString *keyPath in self.observableKeypaths) {
        [self removeObserver:self forKeyPath:keyPath];
    }
}

- (NSArray *)observableKeypaths {
    return @[@"titleLabel.text"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == MRProgressOverlayViewObservationContext) {
        if ([keyPath isEqualToString:@"titleLabel.text"]) {
            if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
                _savedAttributes = self.titleTextAttributesToCopy;
                return;
            } else {
                if (!_savedAttributes) {
                    self.titleLabelText = self.titleLabel.text;
                    #if DEBUG
                        NSLog(@"** WARNING - Instance of %@ used automatically setTitleLabelText: internally, instead of titleLabel.text, but some text attributes may been lost.",
                              NSStringFromClass(self.class));
                    #endif
                } else {
                    NSString *plainText = self.titleLabel.text;
                    NSString *_Nonnull text = plainText ? (NSString *_Nonnull)plainText :  @"";
                    self.titleLabelText = (id)[[NSAttributedString alloc] initWithString:text attributes:_savedAttributes];
                    _savedAttributes = nil;
                }
            }
        }
        [self manualLayoutSubviews];
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}


#pragma mark - Create subviews

- (UIView *)createBlurView {
    const CGFloat cornerRadius = MRProgressOverlayViewCornerRadius;
    
    if (MR_UIEffectViewIsAvailable) {
        #if MR_UIEffectViewIsAllowed
            UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]];
            [self addSubview:effectView];
            
            // Setup mask view
            UIView *maskView = [UIView new];
            maskView.backgroundColor = UIColor.whiteColor;
            maskView.layer.cornerRadius = cornerRadius;
            self.blurMaskView = maskView; // Memorize for layout changes
            
            effectView.maskView = maskView;
            
            return effectView;
        #endif
    } else {
        UIView *blurView = [MRBlurView new];
        blurView.alpha = 0.98;
        blurView.layer.cornerRadius = cornerRadius;
        [self addSubview:blurView];
        return blurView;
    }
}

- (UIView *)createModeView {
    UIView *modeView = [self createViewForMode:self.mode];
    self.modeView = modeView;
    modeView.tintColor = self.tintColor;
    
    if ([modeView conformsToProtocol:@protocol(MRStopableView)]
        && [modeView respondsToSelector:@selector(stopButton)]) {
        UIButton *stopButton = [((id<MRStopableView>)modeView) stopButton];
        [stopButton addTarget:self action:@selector(modeViewStopButtonTouchUpInside) forControlEvents:UIControlEventTouchUpInside];
    }
    
    return modeView;
}

- (void)setModeView:(UIView *)modeView {
    _modeView = modeView;
    [self.dialogView addSubview:modeView];
}

- (UIView *)createViewForMode:(MRProgressOverlayViewMode)mode {
    switch (mode) {
        case MRProgressOverlayViewModeIndeterminate:
            return [self createActivityIndicatorView];
        
        case MRProgressOverlayViewModeIndeterminateSmall:
            return [self createSmallActivityIndicatorView];
        
        case MRProgressOverlayViewModeIndeterminateSmallDefault:
            return [self createSmallDefaultActivityIndicatorView];
        
        case MRProgressOverlayViewModeDeterminateCircular:
            return [self createCircularProgressView];
        
        case MRProgressOverlayViewModeDeterminateHorizontalBar:
            return [self createHorizontalBarProgressView];
        
        case MRProgressOverlayViewModeCheckmark:
            return [self createCheckmarkIconView];
        
        case MRProgressOverlayViewModeCross:
            return [self createCrossIconView];
            
        case MRProgressOverlayViewModeCustom:
            return [self createCustomView];
    }
    return nil;
}


#pragma mark - Mode view factory methods

- (MRActivityIndicatorView *)createActivityIndicatorView {
    // Create activity indicator for indeterminate mode
    MRActivityIndicatorView *activityIndicatorView = [MRActivityIndicatorView new];
    return activityIndicatorView;
}

- (MRActivityIndicatorView *)createSmallActivityIndicatorView {
    // Create small activity indicator for text mode
    MRActivityIndicatorView *smallActivityIndicatorView = [MRActivityIndicatorView new];
    smallActivityIndicatorView.hidesWhenStopped = YES;
    return smallActivityIndicatorView;
}

- (UIActivityIndicatorView *)createSmallDefaultActivityIndicatorView {
    // Create small default activity indicator for text mode
    UIActivityIndicatorView *smallDefaultActivityIndicatorView = [UIActivityIndicatorView new];
    smallDefaultActivityIndicatorView.hidesWhenStopped = YES;
    smallDefaultActivityIndicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
    return smallDefaultActivityIndicatorView;
}

- (MRCircularProgressView *)createCircularProgressView {
    // Create circular progress view for determinate circular mode
    MRCircularProgressView *circularProgressView = [MRCircularProgressView new];
    return circularProgressView;
}

- (UIProgressView *)createHorizontalBarProgressView {
    // Create horizontal progress bar for determinate horizontal bar mode
    UIProgressView *horizontalBarProgressView = [UIProgressView new];
    return horizontalBarProgressView;
}

- (MRIconView *)createCheckmarkIconView {
    // Create checkmark icon view for checkmark mode
    MRCheckmarkIconView *checkmarkIconView = [MRCheckmarkIconView new];
    return checkmarkIconView;
}

- (MRIconView *)createCrossIconView {
    // Create cross icon view for cross mode
    MRCrossIconView *crossIconView = [MRCrossIconView new];
    return crossIconView;
}

- (UIView *)createCustomView {
    // Create custom base view
    return [UIView new];
}


#pragma mark - Title label text

- (NSDictionary *)titleTextAttributesToCopy {
    if (self.titleLabel.text.length > 0) {
        return [self.titleLabel.attributedText attributesAtIndex:0 effectiveRange:NULL];
    } else {
        return @{};
    }
}

- (void)setTitleLabelText:(NSString *)titleLabelText {
    self.titleLabelAttributedText = [[NSAttributedString alloc] initWithString:titleLabelText attributes:self.titleTextAttributesToCopy];
    [self manualLayoutSubviews];
}

- (NSString *)titleLabelText {
    return self.titleLabel.text;
}

- (void)setTitleLabelAttributedText:(NSAttributedString *)titleLabelAttributedText {
    self.titleLabel.attributedText = titleLabelAttributedText;
    [self manualLayoutSubviews];
}

- (NSAttributedString *)titleLabelAttributedText {
    return self.titleLabel.attributedText;
}


#pragma mark - Tint color

- (void)setTintColor:(UIColor *)tintColor {
    // Implemented to silent warning
    super.tintColor = tintColor;
}

- (void)tintColorDidChange {
    [super tintColorDidChange];
    self.modeView.tintColor = self.tintColor;
}


#pragma mark - Mode

- (void)setMode:(MRProgressOverlayViewMode)mode {
    [self hideModeView:self.modeView];
    
    _mode = mode;
    
    [self showModeView:[self createModeView]];
    [self updateModeViewMayStop];
    
    if (!self.hidden) {
        [self manualLayoutSubviews];
    }
}

- (void)showModeView:(UIView *)modeView {
    modeView.hidden = NO;
    if ([modeView respondsToSelector:@selector(startAnimating)]) {
        [modeView performSelector:@selector(startAnimating)];
    }
}

- (void)hideModeView:(UIView *)modeView {
    modeView.hidden = YES;
    if ([modeView respondsToSelector:@selector(stopAnimating)]) {
        [modeView performSelector:@selector(stopAnimating)];
    }
}


#pragma mark - Stop button

- (void)setStopBlock:(MRProgressOverlayViewStopBlock)stopBlock {
    _stopBlock = stopBlock;
    
    if (![self updateModeViewMayStop]) {
        #if DEBUG
            NSLog(@"** WARNING - %@: %@ is only valid to call when the mode view supports %@ declared in %@!",
                  NSStringFromClass(self.class),
                  NSStringFromSelector(_cmd),
                  NSStringFromSelector(@selector(setMayStop:)),
                  NSStringFromProtocol(@protocol(MRStopableView)));
        #endif
    }
}

- (BOOL)mayStop {
    return _stopBlock != nil;
}

- (BOOL)updateModeViewMayStop {
    if ([self.modeView conformsToProtocol:@protocol(MRStopableView)]
        && [self.modeView respondsToSelector:@selector(setMayStop:)]) {
        [((id<MRStopableView>)self.modeView) setMayStop:self.mayStop];
        return YES;
    }
    return NO;
}

- (void)modeViewStopButtonTouchUpInside {
    if (self.stopBlock) {
        self.stopBlock(self);
    }
}


#pragma mark - A11y

- (BOOL)accessibilityPerformEscape {
    if (self.mayStop) {
        [self modeViewStopButtonTouchUpInside];
        return YES;
    } else {
        return NO;
    }
}


#pragma mark - Transitions

- (void)setSubviewTransform:(CGAffineTransform)transform alpha:(CGFloat)alpha {
    self.blurView.transform = transform;
    self.dialogView.transform = transform;
    self.alpha = alpha;
}

- (void)show:(BOOL)animated {
    [self showModeView:self.modeView];
    
    [self manualLayoutSubviews];
    
    if (animated) {
        [self setSubviewTransform:CGAffineTransformMakeScale(1.3f, 1.3f) alpha:0.5f];
        self.backgroundColor = UIColor.clearColor;
    }
    
    self.hidden = NO;
    
    void(^animBlock)() = ^{
        [self setSubviewTransform:CGAffineTransformIdentity alpha:1.0f];
        self.backgroundColor = [UIColor colorWithWhite:0 alpha:0.4f];
    };
    
    if (animated) {
        [UIView animateWithDuration:0.2f delay:0.0 options:UIViewAnimationOptionCurveEaseInOut
                         animations:animBlock
                         completion:nil];
    } else {
        animBlock();
    }
    
    UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
    UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.titleLabelText);
}

- (void)dismiss:(BOOL)animated {
    [self dismiss:animated completion:nil];
}

- (void)dismiss:(BOOL)animated completion:(void(^)())completionBlock {
    [self hide:animated completion:^{
        [self removeFromSuperview];
        if (completionBlock) {
            completionBlock();
        }
    }];
}

- (void)hide:(BOOL)animated {
    [self hide:animated completion:nil];
}

- (void)hide:(BOOL)animated completion:(void(^)())completionBlock {
    [self setSubviewTransform:CGAffineTransformIdentity alpha:1.0f];
    
    void(^animBlock)() = ^{
        [self setSubviewTransform:CGAffineTransformMakeScale(0.6f, 0.6f) alpha:0.0f];
        self.backgroundColor = UIColor.clearColor;
    };
    
    void(^animCompletionBlock)(BOOL) = ^(BOOL finished) {
        self.hidden = YES;
        [self hideModeView:self.modeView];
        
        UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
        
        if (completionBlock) {
            completionBlock();
        }
    };
    
    if (animated) {
        [UIView animateWithDuration:0.2f delay:0.0 options:UIViewAnimationOptionCurveEaseInOut
                         animations:animBlock
                         completion:animCompletionBlock];
    } else {
        animBlock();
        animCompletionBlock(YES);
    }
}


#pragma mark - Layout

- (CGAffineTransform)transformForOrientation {
    if ([self.superview isKindOfClass:UIWindow.class]) {
        return CGAffineTransformMakeRotation(MRRotationForStatusBarOrientation());
    }
    return CGAffineTransformIdentity;
}

// Don't overwrite layoutSubviews here. This would cause issues with animation.
- (void)manualLayoutSubviews {
    if (!MRSystemVersionGreaterThanOrEqualTo8()) {
        self.transform = self.transformForOrientation;
    }
    
    CGRect bounds = self.superview.bounds;
    UIEdgeInsets insets = UIEdgeInsetsZero;
    
    if ([self.superview isKindOfClass:[UIScrollView class]]) {
        UIScrollView *scrollView = (UIScrollView *)self.superview;
        insets = scrollView.contentInset;
    }
    
    self.center = CGPointMake((bounds.size.width - insets.left - insets.right) / 2.0f,
                              (bounds.size.height - insets.top - insets.bottom) / 2.0f);

    if (MRSystemVersionGreaterThanOrEqualTo8()) {
        self.bounds = (CGRect){CGPointZero, bounds.size};
    } else {
        if ([self.superview isKindOfClass:UIWindow.class] && UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication.statusBarOrientation)) {
            // Swap width and height
            self.bounds = (CGRect){CGPointZero, {bounds.size.height, bounds.size.width}};
        } else {
            self.bounds = (CGRect){CGPointZero, bounds.size};
        }
    }
    
    const CGFloat dialogPadding = 15;
    const CGFloat modePadding = 30;
    const CGFloat dialogMargin = 10;
    const CGFloat dialogMinWidth = 150;
    
    const BOOL hasSmallIndicator = self.mode == MRProgressOverlayViewModeIndeterminateSmall
        || self.mode == MRProgressOverlayViewModeIndeterminateSmallDefault;
    const BOOL isTextNonEmpty = self.titleLabel.text.length > 0;
    
    CGFloat dialogWidth = hasSmallIndicator ? CGRectGetWidth(bounds) - dialogMargin * 2 : dialogMinWidth;
    if (self.mode == MRProgressOverlayViewModeCustom) {
        dialogWidth = self.modeView.frame.size.width + 2*modePadding;
    }
    
    CGFloat y = (isTextNonEmpty || hasSmallIndicator) ? 7 : modePadding;
    
    CGSize modeViewSize;
    if (hasSmallIndicator) {
        modeViewSize = CGSizeMake(20, 20);
    }
    
    if (!self.titleLabel.hidden && isTextNonEmpty) {
        const CGFloat innerViewWidth = dialogWidth - 2*dialogPadding;
        
        CGFloat titleLabelMinX = dialogPadding;
        CGFloat titleLabelMaxWidth = innerViewWidth;
        CGFloat offset = 0;
        
        if (hasSmallIndicator) {
            offset = modeViewSize.width + 7;
        }
        
        titleLabelMinX += offset;
        titleLabelMaxWidth -= offset;
        
        y += 3;
        
        CGSize titleLabelMaxSize = CGSizeMake(titleLabelMaxWidth, self.bounds.size.height);
        CGRect boundingRect = [self.titleLabel.attributedText boundingRectWithSize:titleLabelMaxSize
                                                                           options:NSStringDrawingUsesLineFragmentOrigin
                                                                           context:nil];
        CGSize titleLabelSize = CGSizeMake(MRCGFloatCeil(boundingRect.size.width),
                                           MRCGFloatCeil(boundingRect.size.height));
        CGPoint titleLabelOrigin;
        if (hasSmallIndicator) {
            CGFloat titleLabelMinWidth = dialogMinWidth - 2*dialogPadding - offset;
            if (titleLabelSize.width > titleLabelMinWidth) {
                dialogWidth = titleLabelSize.width + offset + 2*dialogPadding;
                titleLabelOrigin = CGPointMake(titleLabelMinX, y);
            } else {
                dialogWidth = dialogMinWidth;
                titleLabelOrigin = CGPointMake(titleLabelMinX + (titleLabelMinWidth - titleLabelSize.width) / 2.0f, y);
            }
            
            CGPoint modeViewOrigin = CGPointMake(titleLabelOrigin.x - offset,
                                                 y + (titleLabelSize.height - modeViewSize.height) / 2.0f);
            CGRect modeViewFrame = {modeViewOrigin, modeViewSize};
            self.modeView.frame = modeViewFrame;
        } else {
            titleLabelOrigin = CGPointMake(titleLabelMinX + (titleLabelMaxWidth - titleLabelSize.width) / 2.0f, y);
        }
        
        CGRect titleLabelFrame = {titleLabelOrigin, titleLabelSize};
        self.titleLabel.frame = titleLabelFrame;
        
        y += CGRectGetMaxY(titleLabelFrame);
    } else if (hasSmallIndicator) {
        dialogWidth = modeViewSize.width + 2*y;
        
        CGPoint modeViewOrigin = CGPointMake(y, y);
        CGRect modeViewFrame = {modeViewOrigin, modeViewSize};
        self.modeView.frame = modeViewFrame;
        
        y += CGRectGetMaxY(modeViewFrame);
    }
    
    if (!hasSmallIndicator) {
        const CGFloat innerViewWidth = dialogWidth - 2*modePadding;
        
        CGRect modeViewFrame;
        CGFloat paddingBottom = 0;
        
        if (self.mode != MRProgressOverlayViewModeDeterminateHorizontalBar) {
            if (self.mode == MRProgressOverlayViewModeCustom) {
                modeViewFrame = CGRectMake(modePadding, y, innerViewWidth, self.modeView.frame.size.height);
            } else {
                modeViewFrame = CGRectMake(modePadding, y, innerViewWidth, innerViewWidth);
            }
            paddingBottom = isTextNonEmpty ? 20 : modePadding;
        } else {
            modeViewFrame = CGRectMake(10, y, dialogWidth-20, 5);
            paddingBottom = 15;
        }
        
        self.modeView.frame = modeViewFrame;
        y += modeViewFrame.size.height + paddingBottom;
    }
    
    {
        self.dialogView.frame = MRCenterCGSizeInCGRect(CGSizeMake(dialogWidth, y), self.bounds);
        
        if (!CGRectEqualToRect(self.blurView.frame, self.dialogView.frame)) {
            self.blurView.frame = self.dialogView.frame;
            
            if (MR_UIEffectViewIsAvailable) {
                #if MR_UIEffectViewIsAllowed
                    // As the blurMaskView will be copied internally by UIKit, we have to re-assign
                    // it to the blurView, after we change its layout
                    self.blurMaskView.frame = self.dialogView.bounds;
                    self.blurView.maskView = self.blurMaskView;
                #endif
            }
        }
    }
}


#pragma mark - Control progress

- (void)setProgress:(float)progress {
    [self setProgress:progress animated:NO];
}

- (void)setProgress:(float)progress animated:(BOOL)animated {
    NSParameterAssert(progress >= 0 && progress <= 1);
    _progress = progress;
    [self applyProgressAnimated:(BOOL)animated];
}
    
- (void)applyProgressAnimated:(BOOL)animated {
    if ([self.modeView respondsToSelector:@selector(setProgress:animated:)]) {
        [((id)self.modeView) setProgress:self.progress animated:animated];
    } else if ([self.modeView respondsToSelector:@selector(setProgress:)]) {
        if (animated) {
            #if DEBUG
                NSLog(@"** WARNING - %@: %@ is only valid to call when receiver is in a determinate mode or custom view supports %@!",
                      NSStringFromClass(self.class),
                      NSStringFromSelector(_cmd),
                      NSStringFromSelector(@selector(setProgress:animated:)));
            #endif
        }
        // [((id)self.modeView) setProgress:self.progress];
        // Fixes build issue in Xcode 15
        [self.modeView performSelector:@selector(setProgress:) withObject:[NSNumber numberWithFloat:self.progress]];
    } else {
        NSAssert(self.mode == MRProgressOverlayViewModeDeterminateCircular
                 || self.mode == MRProgressOverlayViewModeDeterminateHorizontalBar,
                 @"Mode must support %@, but doesnot!", NSStringFromSelector(@selector(setProgress:animated:)));
        #if DEBUG
            NSLog(@"** ERROR - %@: %@ or %@ are only valid to call when receiver is in a determinate mode!",
                  NSStringFromClass(self.class),
                  NSStringFromSelector(@selector(setProgress:)),
                  NSStringFromSelector(_cmd));
        #endif
    }
}


#pragma mark - Helper to create UIMotionEffects

- (UIInterpolatingMotionEffect *)motionEffectWithKeyPath:(NSString *)keyPath type:(UIInterpolatingMotionEffectType)type {
    UIInterpolatingMotionEffect *effect = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:keyPath type:type];
    effect.minimumRelativeValue = @(-MRProgressOverlayViewMotionEffectExtent);
    effect.maximumRelativeValue = @(MRProgressOverlayViewMotionEffectExtent);
    return effect;
}

- (void)applyMotionEffects {
    UIMotionEffectGroup *motionEffectGroup = [[UIMotionEffectGroup alloc] init];
    motionEffectGroup.motionEffects = @[[self motionEffectWithKeyPath:@"center.x" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis],
                                        [self motionEffectWithKeyPath:@"center.y" type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis]];
    [self.dialogView addMotionEffect:motionEffectGroup];
    [self.blurView addMotionEffect:motionEffectGroup];
}

@end
