/*
 * 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 "YSSDWebImageManager.h"
#import "YSSDImageCache.h"
#import "YSSDWebImageDownloader.h"
#import "UIImage+YSMetadata.h"
#import "YSSDAssociatedObject.h"
#import "YSSDWebImageError.h"
#import "YSSDInternalMacros.h"

static id<YSSDImageCache> _defaultImageCache;
static id<YSSDImageLoader> _defaultImageLoader;

@interface YSSDWebImageCombinedOperation ()

@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (strong, nonatomic, readwrite, nullable) id<YSSDWebImageOperation> loaderOperation;
@property (strong, nonatomic, readwrite, nullable) id<YSSDWebImageOperation> cacheOperation;
@property (weak, nonatomic, nullable) YSSDWebImageManager *manager;

@end

@interface YSSDWebImageManager ()

@property (strong, nonatomic, readwrite, nonnull) YSSDImageCache *imageCache;
@property (strong, nonatomic, readwrite, nonnull) id<YSSDImageLoader> imageLoader;
@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
@property (strong, nonatomic, nonnull) dispatch_semaphore_t failedURLsLock; // a lock to keep the access to `failedURLs` thread-safe
@property (strong, nonatomic, nonnull) NSMutableSet<YSSDWebImageCombinedOperation *> *runningOperations;
@property (strong, nonatomic, nonnull) dispatch_semaphore_t runningOperationsLock; // a lock to keep the access to `runningOperations` thread-safe

@end

@implementation YSSDWebImageManager

+ (id<YSSDImageCache>)defaultImageCache {
    return _defaultImageCache;
}

+ (void)setDefaultImageCache:(id<YSSDImageCache>)defaultImageCache {
    if (defaultImageCache && ![defaultImageCache conformsToProtocol:@protocol(YSSDImageCache)]) {
        return;
    }
    _defaultImageCache = defaultImageCache;
}

+ (id<YSSDImageLoader>)defaultImageLoader {
    return _defaultImageLoader;
}

+ (void)setDefaultImageLoader:(id<YSSDImageLoader>)defaultImageLoader {
    if (defaultImageLoader && ![defaultImageLoader conformsToProtocol:@protocol(YSSDImageLoader)]) {
        return;
    }
    _defaultImageLoader = defaultImageLoader;
}

+ (nonnull instancetype)sharedManager {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

- (nonnull instancetype)init {
    id<YSSDImageCache> cache = [[self class] defaultImageCache];
    if (!cache) {
        cache = [YSSDImageCache sharedImageCache];
    }
    id<YSSDImageLoader> loader = [[self class] defaultImageLoader];
    if (!loader) {
        loader = [YSSDWebImageDownloader sharedDownloader];
    }
    return [self initWithCache:cache loader:loader];
}

- (nonnull instancetype)initWithCache:(nonnull id<YSSDImageCache>)cache loader:(nonnull id<YSSDImageLoader>)loader {
    if ((self = [super init])) {
        _imageCache = cache;
        _imageLoader = loader;
        _failedURLs = [NSMutableSet new];
        _failedURLsLock = dispatch_semaphore_create(1);
        _runningOperations = [NSMutableSet new];
        _runningOperationsLock = dispatch_semaphore_create(1);
    }
    return self;
}

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
    if (!url) {
        return @"";
    }
    
    NSString *key;
    // Cache Key Filter
    id<YSSDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
    if (cacheKeyFilter) {
        key = [cacheKeyFilter cacheKeyForURL:url];
    } else {
        key = url.absoluteString;
    }
    
    return key;
}

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url context:(nullable YSSDWebImageContext *)context {
    if (!url) {
        return @"";
    }
    
    NSString *key;
    // Cache Key Filter
    id<YSSDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
    if (context[YSSDWebImageContextCacheKeyFilter]) {
        cacheKeyFilter = context[YSSDWebImageContextCacheKeyFilter];
    }
    if (cacheKeyFilter) {
        key = [cacheKeyFilter cacheKeyForURL:url];
    } else {
        key = url.absoluteString;
    }
    
    // Thumbnail Key Appending
    NSValue *thumbnailSizeValue = context[YSSDWebImageContextImageThumbnailPixelSize];
    if (thumbnailSizeValue != nil) {
        CGSize thumbnailSize = CGSizeZero;
#if YSSD_MAC
        thumbnailSize = thumbnailSizeValue.sizeValue;
#else
        thumbnailSize = thumbnailSizeValue.CGSizeValue;
#endif
        BOOL preserveAspectRatio = YES;
        NSNumber *preserveAspectRatioValue = context[YSSDWebImageContextImagePreserveAspectRatio];
        if (preserveAspectRatioValue != nil) {
            preserveAspectRatio = preserveAspectRatioValue.boolValue;
        }
        key = YSSDThumbnailedKeyForKey(key, thumbnailSize, preserveAspectRatio);
    }
    
    // Transformer Key Appending
    id<YSSDImageTransformer> transformer = self.transformer;
    if (context[YSSDWebImageContextImageTransformer]) {
        transformer = context[YSSDWebImageContextImageTransformer];
        if (![transformer conformsToProtocol:@protocol(YSSDImageTransformer)]) {
            transformer = nil;
        }
    }
    if (transformer) {
        key = YSSDTransformedKeyForKey(key, transformer.transformerKey);
    }
    
    return key;
}

- (YSSDWebImageCombinedOperation *)loadImageWithURL:(NSURL *)url options:(YSSDWebImageOptions)options progress:(YSSDImageLoaderProgressBlock)progressBlock completed:(YSSDInternalCompletionBlock)completedBlock {
    return [self loadImageWithURL:url options:options context:nil progress:progressBlock completed:completedBlock];
}

- (YSSDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(YSSDWebImageOptions)options
                                          context:(nullable YSSDWebImageContext *)context
                                         progress:(nullable YSSDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull YSSDInternalCompletionBlock)completedBlock {
    // Invoking this method without a completedBlock is pointless
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[YSSDWebImagePrefetcher prefetchURLs] instead");

    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
    // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    YSSDWebImageCombinedOperation *operation = [YSSDWebImageCombinedOperation new];
    operation.manager = self;

    BOOL isFailedUrl = NO;
    if (url) {
        YSSD_LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        YSSD_UNLOCK(self.failedURLsLock);
    }

    if (url.absoluteString.length == 0 || (!(options & YSSDWebImageRetryFailed) && isFailedUrl)) {
        NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
        NSInteger code = isFailedUrl ? YSSDWebImageErrorBlackListed : YSSDWebImageErrorInvalidURL;
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:YSSDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
        return operation;
    }

    YSSD_LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    YSSD_UNLOCK(self.runningOperationsLock);
    
    // Preprocess the options and context arg to decide the final the result for manager
    YSSDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
    
    // Start the entry to load image from cache
    [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];

    return operation;
}

- (void)cancelAll {
    YSSD_LOCK(self.runningOperationsLock);
    NSSet<YSSDWebImageCombinedOperation *> *copiedOperations = [self.runningOperations copy];
    YSSD_UNLOCK(self.runningOperationsLock);
    [copiedOperations makeObjectsPerformSelector:@selector(cancel)]; // This will call `safelyRemoveOperationFromRunning:` and remove from the array
}

- (BOOL)isRunning {
    BOOL isRunning = NO;
    YSSD_LOCK(self.runningOperationsLock);
    isRunning = (self.runningOperations.count > 0);
    YSSD_UNLOCK(self.runningOperationsLock);
    return isRunning;
}

- (void)removeFailedURL:(NSURL *)url {
    if (!url) {
        return;
    }
    YSSD_LOCK(self.failedURLsLock);
    [self.failedURLs removeObject:url];
    YSSD_UNLOCK(self.failedURLsLock);
}

- (void)removeAllFailedURLs {
    YSSD_LOCK(self.failedURLsLock);
    [self.failedURLs removeAllObjects];
    YSSD_UNLOCK(self.failedURLsLock);
}

#pragma mark - Private

// Query normal cache process
- (void)callCacheProcessForOperation:(nonnull YSSDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(YSSDWebImageOptions)options
                             context:(nullable YSSDWebImageContext *)context
                            progress:(nullable YSSDImageLoaderProgressBlock)progressBlock
                           completed:(nullable YSSDInternalCompletionBlock)completedBlock {
    // Grab the image cache to use
    id<YSSDImageCache> imageCache;
    if ([context[YSSDWebImageContextImageCache] conformsToProtocol:@protocol(YSSDImageCache)]) {
        imageCache = context[YSSDWebImageContextImageCache];
    } else {
        imageCache = self.imageCache;
    }
    
    // Get the query cache type
    YSSDImageCacheType queryCacheType = YSSDImageCacheTypeAll;
    if (context[YSSDWebImageContextQueryCacheType]) {
        queryCacheType = [context[YSSDWebImageContextQueryCacheType] integerValue];
    }
    
    // Check whether we should query cache
    BOOL shouldQueryCache = !YSSD_OPTIONS_CONTAINS(options, YSSDWebImageFromLoaderOnly);
    if (shouldQueryCache) {
        NSString *key = [self cacheKeyForURL:url context:context];
        @WeakObj(operation);
        operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, YSSDImageCacheType cacheType) {
            @StrongObj(operation);
            if (!operation || operation.isCancelled) {
                // Image combined operation cancelled by user
                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:YSSDWebImageErrorDomain code:YSSDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
                [self safelyRemoveOperationFromRunning:operation];
                return;
            } else if (context[YSSDWebImageContextImageTransformer] && !cachedImage) {
                // Have a chance to query original cache instead of downloading
                [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
                return;
            }
            
            // Continue download process
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {
        // Continue download process
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:YSSDImageCacheTypeNone progress:progressBlock completed:completedBlock];
    }
}

// Query original cache process
- (void)callOriginalCacheProcessForOperation:(nonnull YSSDWebImageCombinedOperation *)operation
                                         url:(nonnull NSURL *)url
                                     options:(YSSDWebImageOptions)options
                                     context:(nullable YSSDWebImageContext *)context
                                    progress:(nullable YSSDImageLoaderProgressBlock)progressBlock
                                   completed:(nullable YSSDInternalCompletionBlock)completedBlock {
    // Grab the image cache to use
    id<YSSDImageCache> imageCache;
    if ([context[YSSDWebImageContextImageCache] conformsToProtocol:@protocol(YSSDImageCache)]) {
        imageCache = context[YSSDWebImageContextImageCache];
    } else {
        imageCache = self.imageCache;
    }
    
    // Get the original query cache type
    YSSDImageCacheType originalQueryCacheType = YSSDImageCacheTypeNone;
    if (context[YSSDWebImageContextOriginalQueryCacheType]) {
        originalQueryCacheType = [context[YSSDWebImageContextOriginalQueryCacheType] integerValue];
    }
    
    // Check whether we should query original cache
    BOOL shouldQueryOriginalCache = (originalQueryCacheType != YSSDImageCacheTypeNone);
    if (shouldQueryOriginalCache) {
        // Change originContext to mutable
        YSSDWebImageMutableContext * __block originContext;
        if (context) {
            originContext = [context mutableCopy];
        } else {
            originContext = [NSMutableDictionary dictionary];
        }
        
        // Disable transformer for cache key generation
        id<YSSDImageTransformer> transformer = originContext[YSSDWebImageContextImageTransformer];
        originContext[YSSDWebImageContextImageTransformer] = [NSNull null];
        
        NSString *key = [self cacheKeyForURL:url context:originContext];
        @WeakObj(operation);
        operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:originalQueryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, YSSDImageCacheType cacheType) {
            @StrongObj(operation);
            if (!operation || operation.isCancelled) {
                // Image combined operation cancelled by user
                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:YSSDWebImageErrorDomain code:YSSDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
                [self safelyRemoveOperationFromRunning:operation];
                return;
            }
            
            // Add original transformer
            if (transformer) {
                originContext[YSSDWebImageContextImageTransformer] = transformer;
            }
            
            // Use the store cache process instead of downloading, and ignore .refreshCached option for now
            [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:cachedImage downloadedData:cachedData finished:YES progress:progressBlock completed:completedBlock];
            
            [self safelyRemoveOperationFromRunning:operation];
        }];
    } else {
        // Continue download process
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:originalQueryCacheType progress:progressBlock completed:completedBlock];
    }
}

// Download process
- (void)callDownloadProcessForOperation:(nonnull YSSDWebImageCombinedOperation *)operation
                                    url:(nonnull NSURL *)url
                                options:(YSSDWebImageOptions)options
                                context:(YSSDWebImageContext *)context
                            cachedImage:(nullable UIImage *)cachedImage
                             cachedData:(nullable NSData *)cachedData
                              cacheType:(YSSDImageCacheType)cacheType
                               progress:(nullable YSSDImageLoaderProgressBlock)progressBlock
                              completed:(nullable YSSDInternalCompletionBlock)completedBlock {
    // Grab the image loader to use
    id<YSSDImageLoader> imageLoader;
    if ([context[YSSDWebImageContextImageLoader] conformsToProtocol:@protocol(YSSDImageLoader)]) {
        imageLoader = context[YSSDWebImageContextImageLoader];
    } else {
        imageLoader = self.imageLoader;
    }
    
    // Check whether we should download image from network
    BOOL shouldDownload = !YSSD_OPTIONS_CONTAINS(options, YSSDWebImageFromCacheOnly);
    shouldDownload &= (!cachedImage || options & YSSDWebImageRefreshCached);
    shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
    shouldDownload &= [imageLoader canRequestImageForURL:url];
    if (shouldDownload) {
        if (cachedImage && options & YSSDWebImageRefreshCached) {
            // If image was found in the cache but YSSDWebImageRefreshCached is provided, notify about the cached image
            // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
            YSSDWebImageMutableContext *mutableContext;
            if (context) {
                mutableContext = [context mutableCopy];
            } else {
                mutableContext = [NSMutableDictionary dictionary];
            }
            mutableContext[YSSDWebImageContextLoaderCachedImage] = cachedImage;
            context = [mutableContext copy];
        }
        
        @WeakObj(operation);
        operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
            @StrongObj(operation);
            if (!operation || operation.isCancelled) {
                // Image combined operation cancelled by user
                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:YSSDWebImageErrorDomain code:YSSDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] url:url];
            } else if (cachedImage && options & YSSDWebImageRefreshCached && [error.domain isEqualToString:YSSDWebImageErrorDomain] && error.code == YSSDWebImageErrorCacheNotModified) {
                // Image refresh hit the NSURLCache cache, do not call the completion block
            } else if ([error.domain isEqualToString:YSSDWebImageErrorDomain] && error.code == YSSDWebImageErrorCancelled) {
                // Download operation cancelled by user before sending the request, don't block failed URL
                [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
            } else if (error) {
                [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
                BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error options:options context:context];
                
                if (shouldBlockFailedURL) {
                    YSSD_LOCK(self.failedURLsLock);
                    [self.failedURLs addObject:url];
                    YSSD_UNLOCK(self.failedURLsLock);
                }
            } else {
                if ((options & YSSDWebImageRetryFailed)) {
                    YSSD_LOCK(self.failedURLsLock);
                    [self.failedURLs removeObject:url];
                    YSSD_UNLOCK(self.failedURLsLock);
                }
                // Continue store cache process
                [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
            }
            
            if (finished) {
                [self safelyRemoveOperationFromRunning:operation];
            }
        }];
    } else if (cachedImage) {
        [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
        [self safelyRemoveOperationFromRunning:operation];
    } else {
        // Image not in cache and download disallowed by delegate
        [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:YSSDImageCacheTypeNone finished:YES url:url];
        [self safelyRemoveOperationFromRunning:operation];
    }
}

// Store cache process
- (void)callStoreCacheProcessForOperation:(nonnull YSSDWebImageCombinedOperation *)operation
                                      url:(nonnull NSURL *)url
                                  options:(YSSDWebImageOptions)options
                                  context:(YSSDWebImageContext *)context
                          downloadedImage:(nullable UIImage *)downloadedImage
                           downloadedData:(nullable NSData *)downloadedData
                                 finished:(BOOL)finished
                                 progress:(nullable YSSDImageLoaderProgressBlock)progressBlock
                                completed:(nullable YSSDInternalCompletionBlock)completedBlock {
    // the target image store cache type
    YSSDImageCacheType storeCacheType = YSSDImageCacheTypeAll;
    if (context[YSSDWebImageContextStoreCacheType]) {
        storeCacheType = [context[YSSDWebImageContextStoreCacheType] integerValue];
    }
    // the original store image cache type
    YSSDImageCacheType originalStoreCacheType = YSSDImageCacheTypeNone;
    if (context[YSSDWebImageContextOriginalStoreCacheType]) {
        originalStoreCacheType = [context[YSSDWebImageContextOriginalStoreCacheType] integerValue];
    }
    // origin cache key
    YSSDWebImageMutableContext *originContext = [context mutableCopy];
    // disable transformer for cache key generation
    originContext[YSSDWebImageContextImageTransformer] = [NSNull null];
    NSString *key = [self cacheKeyForURL:url context:originContext];
    id<YSSDImageTransformer> transformer = context[YSSDWebImageContextImageTransformer];
    if (![transformer conformsToProtocol:@protocol(YSSDImageTransformer)]) {
        transformer = nil;
    }
    id<YSSDWebImageCacheSerializer> cacheSerializer = context[YSSDWebImageContextCacheSerializer];
    
    BOOL shouldTransformImage = downloadedImage && transformer;
    shouldTransformImage = shouldTransformImage && (!downloadedImage.yssd_isAnimated || (options & YSSDWebImageTransformAnimatedImage));
    shouldTransformImage = shouldTransformImage && (!downloadedImage.yssd_isVector || (options & YSSDWebImageTransformVectorImage));
    BOOL shouldCacheOriginal = downloadedImage && finished;
    
    // if available, store original image to cache
    if (shouldCacheOriginal) {
        // normally use the store cache type, but if target image is transformed, use original store cache type instead
        YSSDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
        if (cacheSerializer && (targetStoreCacheType == YSSDImageCacheTypeDisk || targetStoreCacheType == YSSDImageCacheTypeAll)) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                @autoreleasepool {
                    NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
                    [self storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
                        // Continue transform process
                        [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
                    }];
                }
            });
        } else {
            [self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
                // Continue transform process
                [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
            }];
        }
    } else {
        // Continue transform process
        [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
    }
}

// Transform process
- (void)callTransformProcessForOperation:(nonnull YSSDWebImageCombinedOperation *)operation
                                     url:(nonnull NSURL *)url
                                 options:(YSSDWebImageOptions)options
                                 context:(YSSDWebImageContext *)context
                           originalImage:(nullable UIImage *)originalImage
                            originalData:(nullable NSData *)originalData
                                finished:(BOOL)finished
                                progress:(nullable YSSDImageLoaderProgressBlock)progressBlock
                               completed:(nullable YSSDInternalCompletionBlock)completedBlock {
    // the target image store cache type
    YSSDImageCacheType storeCacheType = YSSDImageCacheTypeAll;
    if (context[YSSDWebImageContextStoreCacheType]) {
        storeCacheType = [context[YSSDWebImageContextStoreCacheType] integerValue];
    }
    // transformed cache key
    NSString *key = [self cacheKeyForURL:url context:context];
    id<YSSDImageTransformer> transformer = context[YSSDWebImageContextImageTransformer];
    if (![transformer conformsToProtocol:@protocol(YSSDImageTransformer)]) {
        transformer = nil;
    }
    id<YSSDWebImageCacheSerializer> cacheSerializer = context[YSSDWebImageContextCacheSerializer];
    
    BOOL shouldTransformImage = originalImage && transformer;
    shouldTransformImage = shouldTransformImage && (!originalImage.yssd_isAnimated || (options & YSSDWebImageTransformAnimatedImage));
    shouldTransformImage = shouldTransformImage && (!originalImage.yssd_isVector || (options & YSSDWebImageTransformVectorImage));
    // if available, store transformed image to cache
    if (shouldTransformImage) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            @autoreleasepool {
                UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
                if (transformedImage && finished) {
                    BOOL imageWasTransformed = ![transformedImage isEqual:originalImage];
                    NSData *cacheData;
                    // pass nil if the image was transformed, so we can recalculate the data from the image
                    if (cacheSerializer && (storeCacheType == YSSDImageCacheTypeDisk || storeCacheType == YSSDImageCacheTypeAll)) {
                        cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : originalData) imageURL:url];
                    } else {
                        cacheData = (imageWasTransformed ? nil : originalData);
                    }
                    [self storeImage:transformedImage imageData:cacheData forKey:key cacheType:storeCacheType options:options context:context completion:^{
                        [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:YSSDImageCacheTypeNone finished:finished url:url];
                    }];
                } else {
                    [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:YSSDImageCacheTypeNone finished:finished url:url];
                }
            }
        });
    } else {
        [self callCompletionBlockForOperation:operation completion:completedBlock image:originalImage data:originalData error:nil cacheType:YSSDImageCacheTypeNone finished:finished url:url];
    }
}

#pragma mark - Helper

- (void)safelyRemoveOperationFromRunning:(nullable YSSDWebImageCombinedOperation*)operation {
    if (!operation) {
        return;
    }
    YSSD_LOCK(self.runningOperationsLock);
    [self.runningOperations removeObject:operation];
    YSSD_UNLOCK(self.runningOperationsLock);
}

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)data
            forKey:(nullable NSString *)key
         cacheType:(YSSDImageCacheType)cacheType
           options:(YSSDWebImageOptions)options
           context:(nullable YSSDWebImageContext *)context
        completion:(nullable YSSDWebImageNoParamsBlock)completion {
    id<YSSDImageCache> imageCache;
    if ([context[YSSDWebImageContextImageCache] conformsToProtocol:@protocol(YSSDImageCache)]) {
        imageCache = context[YSSDWebImageContextImageCache];
    } else {
        imageCache = self.imageCache;
    }
    BOOL waitStoreCache = YSSD_OPTIONS_CONTAINS(options, YSSDWebImageWaitStoreCache);
    // Check whether we should wait the store cache finished. If not, callback immediately
    [imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{
        if (waitStoreCache) {
            if (completion) {
                completion();
            }
        }
    }];
    if (!waitStoreCache) {
        if (completion) {
            completion();
        }
    }
}

- (void)callCompletionBlockForOperation:(nullable YSSDWebImageCombinedOperation*)operation
                             completion:(nullable YSSDInternalCompletionBlock)completionBlock
                                  error:(nullable NSError *)error
                                    url:(nullable NSURL *)url {
    [self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:YSSDImageCacheTypeNone finished:YES url:url];
}

- (void)callCompletionBlockForOperation:(nullable YSSDWebImageCombinedOperation*)operation
                             completion:(nullable YSSDInternalCompletionBlock)completionBlock
                                  image:(nullable UIImage *)image
                                   data:(nullable NSData *)data
                                  error:(nullable NSError *)error
                              cacheType:(YSSDImageCacheType)cacheType
                               finished:(BOOL)finished
                                    url:(nullable NSURL *)url {
    dispatch_main_async_safe(^{
        if (completionBlock) {
            completionBlock(image, data, error, cacheType, finished, url);
        }
    });
}

- (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url
                              error:(nonnull NSError *)error
                            options:(YSSDWebImageOptions)options
                            context:(nullable YSSDWebImageContext *)context {
    id<YSSDImageLoader> imageLoader;
    if ([context[YSSDWebImageContextImageLoader] conformsToProtocol:@protocol(YSSDImageLoader)]) {
        imageLoader = context[YSSDWebImageContextImageLoader];
    } else {
        imageLoader = self.imageLoader;
    }
    // Check whether we should block failed url
    BOOL shouldBlockFailedURL;
    if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
        shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
    } else {
        shouldBlockFailedURL = [imageLoader shouldBlockFailedURLWithURL:url error:error];
    }
    
    return shouldBlockFailedURL;
}

- (YSSDWebImageOptionsResult *)processedResultForURL:(NSURL *)url options:(YSSDWebImageOptions)options context:(YSSDWebImageContext *)context {
    YSSDWebImageOptionsResult *result;
    YSSDWebImageMutableContext *mutableContext = [YSSDWebImageMutableContext dictionary];
    
    // Image Transformer from manager
    if (!context[YSSDWebImageContextImageTransformer]) {
        id<YSSDImageTransformer> transformer = self.transformer;
        [mutableContext setValue:transformer forKey:YSSDWebImageContextImageTransformer];
    }
    // Cache key filter from manager
    if (!context[YSSDWebImageContextCacheKeyFilter]) {
        id<YSSDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
        [mutableContext setValue:cacheKeyFilter forKey:YSSDWebImageContextCacheKeyFilter];
    }
    // Cache serializer from manager
    if (!context[YSSDWebImageContextCacheSerializer]) {
        id<YSSDWebImageCacheSerializer> cacheSerializer = self.cacheSerializer;
        [mutableContext setValue:cacheSerializer forKey:YSSDWebImageContextCacheSerializer];
    }
    
    if (mutableContext.count > 0) {
        if (context) {
            [mutableContext addEntriesFromDictionary:context];
        }
        context = [mutableContext copy];
    }
    
    // Apply options processor
    if (self.optionsProcessor) {
        result = [self.optionsProcessor processedResultForURL:url options:options context:context];
    }
    if (!result) {
        // Use default options result
        result = [[YSSDWebImageOptionsResult alloc] initWithOptions:options context:context];
    }
    
    return result;
}

@end


@implementation YSSDWebImageCombinedOperation

- (void)cancel {
    @synchronized(self) {
        if (self.isCancelled) {
            return;
        }
        self.cancelled = YES;
        if (self.cacheOperation) {
            [self.cacheOperation cancel];
            self.cacheOperation = nil;
        }
        if (self.loaderOperation) {
            [self.loaderOperation cancel];
            self.loaderOperation = nil;
        }
        [self.manager safelyRemoveOperationFromRunning:self];
    }
}

@end
