/*
 * This file is part of the YSSDWebImage package.
 * (c) Olivier Poitrey <rs@dailymotion.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

#import "UIView+YSWebCache.h"
#import "objc/runtime.h"
#import "UIView+YSWebCacheOperation.h"
#import "YSSDWebImageError.h"
#import "YSSDInternalMacros.h"
#import "YSSDWebImageTransitionInternal.h"

const int64_t YSSDWebImageProgressUnitCountUnknown = 1LL;

@implementation UIView (YSWebCache)

- (nullable NSURL *)yssd_imageURL {
    return objc_getAssociatedObject(self, @selector(yssd_imageURL));
}

- (void)setYssd_imageURL:(NSURL * _Nullable)yssd_imageURL {
    objc_setAssociatedObject(self, @selector(yssd_imageURL), yssd_imageURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (nullable NSString *)yssd_latestOperationKey {
    return objc_getAssociatedObject(self, @selector(yssd_latestOperationKey));
}

- (void)setYssd_latestOperationKey:(NSString * _Nullable)yssd_latestOperationKey {
    objc_setAssociatedObject(self, @selector(yssd_latestOperationKey), yssd_latestOperationKey, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSProgress *)yssd_imageProgress {
    NSProgress *progress = objc_getAssociatedObject(self, @selector(yssd_imageProgress));
    if (!progress) {
        progress = [[NSProgress alloc] initWithParent:nil userInfo:nil];
        self.yssd_imageProgress = progress;
    }
    return progress;
}

- (void)setYssd_imageProgress:(NSProgress *)yssd_imageProgress {
    objc_setAssociatedObject(self, @selector(yssd_imageProgress), yssd_imageProgress, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)yssd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(YSSDWebImageOptions)options
                           context:(nullable YSSDWebImageContext *)context
                     setImageBlock:(nullable YSSDSetImageBlock)setImageBlock
                          progress:(nullable YSSDImageLoaderProgressBlock)progressBlock
                         completed:(nullable YSSDInternalCompletionBlock)completedBlock {
    if (context) {
        // copy to avoid mutable object
        context = [context copy];
    } else {
        context = [NSDictionary dictionary];
    }
    NSString *validOperationKey = context[YSSDWebImageContextSetImageOperationKey];
    if (!validOperationKey) {
        // pass through the operation key to downstream, which can used for tracing operation or image view class
        validOperationKey = NSStringFromClass([self class]);
        YSSDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[YSSDWebImageContextSetImageOperationKey] = validOperationKey;
        context = [mutableContext copy];
    }
    self.yssd_latestOperationKey = validOperationKey;
    [self yssd_cancelImageLoadOperationWithKey:validOperationKey];
    self.yssd_imageURL = url;
    
    if (!(options & YSSDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self yssd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:YSSDImageCacheTypeNone imageURL:url];
        });
    }
    
    if (url) {
        // reset the progress
        NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(yssd_imageProgress));
        if (imageProgress) {
            imageProgress.totalUnitCount = 0;
            imageProgress.completedUnitCount = 0;
        }
        
#if YSSD_UIKIT || YSSD_MAC
        // check and start image indicator
        [self yssd_startImageIndicator];
        id<YSSDWebImageIndicator> imageIndicator = self.yssd_imageIndicator;
#endif
        YSSDWebImageManager *manager = context[YSSDWebImageContextCustomManager];
        if (!manager) {
            manager = [YSSDWebImageManager sharedManager];
        } else {
            // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
            YSSDWebImageMutableContext *mutableContext = [context mutableCopy];
            mutableContext[YSSDWebImageContextCustomManager] = nil;
            context = [mutableContext copy];
        }
        
        YSSDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            if (imageProgress) {
                imageProgress.totalUnitCount = expectedSize;
                imageProgress.completedUnitCount = receivedSize;
            }
#if YSSD_UIKIT || YSSD_MAC
            if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
                double progress = 0;
                if (expectedSize != 0) {
                    progress = (double)receivedSize / expectedSize;
                }
                progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imageIndicator updateIndicatorProgress:progress];
                });
            }
#endif
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        @WeakObj(self);
        id <YSSDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, YSSDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            @StrongObj(self);
            if (!self) { return; }
            // if the progress not been updated, mark it to complete state
            if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
                imageProgress.totalUnitCount = YSSDWebImageProgressUnitCountUnknown;
                imageProgress.completedUnitCount = YSSDWebImageProgressUnitCountUnknown;
            }
            
#if YSSD_UIKIT || YSSD_MAC
            // check and stop image indicator
            if (finished) {
                [self yssd_stopImageIndicator];
            }
#endif
            
            BOOL shouldCallCompletedBlock = finished || (options & YSSDWebImageAvoidAutoSetImage);
            BOOL shouldNotSetImage = ((image && (options & YSSDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & YSSDWebImageDelayPlaceholder)));
            YSSDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (!self) { return; }
                if (!shouldNotSetImage) {
                    [self yssd_setNeedsLayout];
                }
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, data, error, cacheType, finished, url);
                }
            };
            
            // case 1a: we got an image, but the YSSDWebImageAvoidAutoSetImage flag is set
            // OR
            // case 1b: we got no image and the YSSDWebImageDelayPlaceholder is not set
            if (shouldNotSetImage) {
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;
            }
            
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            if (image) {
                // case 2a: we got an image and the YSSDWebImageAvoidAutoSetImage is not set
                targetImage = image;
                targetData = data;
            } else if (options & YSSDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the YSSDWebImageDelayPlaceholder flag is set
                targetImage = placeholder;
                targetData = nil;
            }
            
#if YSSD_UIKIT || YSSD_MAC
            // check whether we should use the image transition
            YSSDWebImageTransition *transition = nil;
            if (finished && (options & YSSDWebImageForceTransition || cacheType == YSSDImageCacheTypeNone)) {
                transition = self.yssd_imageTransition;
            }
#endif
            dispatch_main_async_safe(^{
#if YSSD_UIKIT || YSSD_MAC
                [self yssd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                [self yssd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
                callCompletedBlockClojure();
            });
        }];
        [self yssd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
#if YSSD_UIKIT || YSSD_MAC
        [self yssd_stopImageIndicator];
#endif
        dispatch_main_async_safe(^{
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:YSSDWebImageErrorDomain code:YSSDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                completedBlock(nil, nil, error, YSSDImageCacheTypeNone, YES, url);
            }
        });
    }
}

- (void)yssd_cancelCurrentImageLoad {
    [self yssd_cancelImageLoadOperationWithKey:self.yssd_latestOperationKey];
}

- (void)yssd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(YSSDSetImageBlock)setImageBlock cacheType:(YSSDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
#if YSSD_UIKIT || YSSD_MAC
    [self yssd_setImage:image imageData:imageData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:nil cacheType:cacheType imageURL:imageURL];
#else
    // watchOS does not support view transition. Simplify the logic
    if (setImageBlock) {
        setImageBlock(image, imageData, cacheType, imageURL);
    } else if ([self isKindOfClass:[UIImageView class]]) {
        UIImageView *imageView = (UIImageView *)self;
        [imageView setImage:image];
    }
#endif
}

#if YSSD_UIKIT || YSSD_MAC
- (void)yssd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(YSSDSetImageBlock)setImageBlock transition:(YSSDWebImageTransition *)transition cacheType:(YSSDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
    UIView *view = self;
    YSSDSetImageBlock finalSetImageBlock;
    if (setImageBlock) {
        finalSetImageBlock = setImageBlock;
    } else if ([view isKindOfClass:[UIImageView class]]) {
        UIImageView *imageView = (UIImageView *)view;
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, YSSDImageCacheType setCacheType, NSURL *setImageURL) {
            imageView.image = setImage;
        };
    }
#if YSSD_UIKIT
    else if ([view isKindOfClass:[UIButton class]]) {
        UIButton *button = (UIButton *)view;
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, YSSDImageCacheType setCacheType, NSURL *setImageURL) {
            [button setImage:setImage forState:UIControlStateNormal];
        };
    }
#endif
#if YSSD_MAC
    else if ([view isKindOfClass:[NSButton class]]) {
        NSButton *button = (NSButton *)view;
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, YSSDImageCacheType setCacheType, NSURL *setImageURL) {
            button.image = setImage;
        };
    }
#endif
    
    if (transition) {
#if YSSD_UIKIT
        [UIView transitionWithView:view duration:0 options:0 animations:^{
            // 0 duration to let UIKit render placeholder and prepares block
            if (transition.prepares) {
                transition.prepares(view, image, imageData, cacheType, imageURL);
            }
        } completion:^(BOOL finished) {
            [UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{
                if (finalSetImageBlock && !transition.avoidAutoSetImage) {
                    finalSetImageBlock(image, imageData, cacheType, imageURL);
                }
                if (transition.animations) {
                    transition.animations(view, image);
                }
            } completion:transition.completion];
        }];
#elif YSSD_MAC
        [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull prepareContext) {
            // 0 duration to let AppKit render placeholder and prepares block
            prepareContext.duration = 0;
            if (transition.prepares) {
                transition.prepares(view, image, imageData, cacheType, imageURL);
            }
        } completionHandler:^{
            [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
                context.duration = transition.duration;
                #pragma clang diagnostic push
                #pragma clang diagnostic ignored "-Wdeprecated-declarations"
                CAMediaTimingFunction *timingFunction = transition.timingFunction;
                #pragma clang diagnostic pop
                if (!timingFunction) {
                    timingFunction = YSSDTimingFunctionFromAnimationOptions(transition.animationOptions);
                }
                context.timingFunction = timingFunction;
                context.allowsImplicitAnimation = YSSD_OPTIONS_CONTAINS(transition.animationOptions, YSSDWebImageAnimationOptionAllowsImplicitAnimation);
                if (finalSetImageBlock && !transition.avoidAutoSetImage) {
                    finalSetImageBlock(image, imageData, cacheType, imageURL);
                }
                CATransition *trans = YSSDTransitionFromAnimationOptions(transition.animationOptions);
                if (trans) {
                    [view.layer addAnimation:trans forKey:kCATransition];
                }
                if (transition.animations) {
                    transition.animations(view, image);
                }
            } completionHandler:^{
                if (transition.completion) {
                    transition.completion(YES);
                }
            }];
        }];
#endif
    } else {
        if (finalSetImageBlock) {
            finalSetImageBlock(image, imageData, cacheType, imageURL);
        }
    }
}
#endif

- (void)yssd_setNeedsLayout {
#if YSSD_UIKIT
    [self setNeedsLayout];
#elif YSSD_MAC
    [self setNeedsLayout:YES];
#elif YSSD_WATCH
    // Do nothing because WatchKit automatically layout the view after property change
#endif
}

#if YSSD_UIKIT || YSSD_MAC

#pragma mark - Image Transition
- (YSSDWebImageTransition *)yssd_imageTransition {
    return objc_getAssociatedObject(self, @selector(yssd_imageTransition));
}

- (void)setYssd_imageTransition:(YSSDWebImageTransition *)yssd_imageTransition {
    objc_setAssociatedObject(self, @selector(yssd_imageTransition), yssd_imageTransition, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

#pragma mark - Indicator
- (id<YSSDWebImageIndicator>)yssd_imageIndicator {
    return objc_getAssociatedObject(self, @selector(yssd_imageIndicator));
}

- (void)setYssd_imageIndicator:(id<YSSDWebImageIndicator>)yssd_imageIndicator {
    // Remove the old indicator view
    id<YSSDWebImageIndicator> previousIndicator = self.yssd_imageIndicator;
    [previousIndicator.indicatorView removeFromSuperview];
    
    objc_setAssociatedObject(self, @selector(yssd_imageIndicator), yssd_imageIndicator, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    // Add the new indicator view
    UIView *view = yssd_imageIndicator.indicatorView;
    if (CGRectEqualToRect(view.frame, CGRectZero)) {
        view.frame = self.bounds;
    }
    // Center the indicator view
#if YSSD_MAC
    [view setFrameOrigin:CGPointMake(round((NSWidth(self.bounds) - NSWidth(view.frame)) / 2), round((NSHeight(self.bounds) - NSHeight(view.frame)) / 2))];
#else
    view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
#endif
    view.hidden = NO;
    [self addSubview:view];
}

- (void)yssd_startImageIndicator {
    id<YSSDWebImageIndicator> imageIndicator = self.yssd_imageIndicator;
    if (!imageIndicator) {
        return;
    }
    dispatch_main_async_safe(^{
        [imageIndicator startAnimatingIndicator];
    });
}

- (void)yssd_stopImageIndicator {
    id<YSSDWebImageIndicator> imageIndicator = self.yssd_imageIndicator;
    if (!imageIndicator) {
        return;
    }
    dispatch_main_async_safe(^{
        [imageIndicator stopAnimatingIndicator];
    });
}

#endif

@end
