/*
 * 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 "YSSDImageLoader.h"
#import "YSSDWebImageCacheKeyFilter.h"
#import "YSSDImageCodersManager.h"
#import "YSSDImageCoderHelper.h"
#import "YSSDAnimatedImage.h"
#import "UIImage+YSMetadata.h"
#import "YSSDInternalMacros.h"
#import "objc/runtime.h"

static void * YSSDImageLoaderProgressiveCoderKey = &YSSDImageLoaderProgressiveCoderKey;

UIImage * _Nullable YSSDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, YSSDWebImageOptions options, YSSDWebImageContext * _Nullable context) {
    NSCParameterAssert(imageData);
    NSCParameterAssert(imageURL);
    
    UIImage *image;
    id<YSSDWebImageCacheKeyFilter> cacheKeyFilter = context[YSSDWebImageContextCacheKeyFilter];
    NSString *cacheKey;
    if (cacheKeyFilter) {
        cacheKey = [cacheKeyFilter cacheKeyForURL:imageURL];
    } else {
        cacheKey = imageURL.absoluteString;
    }
    BOOL decodeFirstFrame = YSSD_OPTIONS_CONTAINS(options, YSSDWebImageDecodeFirstFrameOnly);
    NSNumber *scaleValue = context[YSSDWebImageContextImageScaleFactor];
    CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : YSSDImageScaleFactorForKey(cacheKey);
    NSNumber *preserveAspectRatioValue = context[YSSDWebImageContextImagePreserveAspectRatio];
    NSValue *thumbnailSizeValue;
    BOOL shouldScaleDown = YSSD_OPTIONS_CONTAINS(options, YSSDWebImageScaleDownLargeImages);
    if (shouldScaleDown) {
        CGFloat thumbnailPixels = YSSDImageCoderHelper.defaultScaleDownLimitBytes / 4;
        CGFloat dimension = ceil(sqrt(thumbnailPixels));
        thumbnailSizeValue = @(CGSizeMake(dimension, dimension));
    }
    if (context[YSSDWebImageContextImageThumbnailPixelSize]) {
        thumbnailSizeValue = context[YSSDWebImageContextImageThumbnailPixelSize];
    }
    
    YSSDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
    mutableCoderOptions[YSSDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
    mutableCoderOptions[YSSDImageCoderDecodeScaleFactor] = @(scale);
    mutableCoderOptions[YSSDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
    mutableCoderOptions[YSSDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
    mutableCoderOptions[YSSDImageCoderWebImageContext] = context;
    YSSDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
    
    // Grab the image coder
    id<YSSDImageCoder> imageCoder;
    if ([context[YSSDWebImageContextImageCoder] conformsToProtocol:@protocol(YSSDImageCoder)]) {
        imageCoder = context[YSSDWebImageContextImageCoder];
    } else {
        imageCoder = [YSSDImageCodersManager sharedManager];
    }
    
    if (!decodeFirstFrame) {
        // check whether we should use `YSSDAnimatedImage`
        Class animatedImageClass = context[YSSDWebImageContextAnimatedImageClass];
        if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(YSSDAnimatedImage)]) {
            image = [[animatedImageClass alloc] initWithData:imageData scale:scale options:coderOptions];
            if (image) {
                // Preload frames if supported
                if (options & YSSDWebImagePreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) {
                    [((id<YSSDAnimatedImage>)image) preloadAllFrames];
                }
            } else {
                // Check image class matching
                if (options & YSSDWebImageMatchAnimatedImageClass) {
                    return nil;
                }
            }
        }
    }
    if (!image) {
        image = [imageCoder decodedImageWithData:imageData options:coderOptions];
    }
    if (image) {
        BOOL shouldDecode = !YSSD_OPTIONS_CONTAINS(options, YSSDWebImageAvoidDecodeImage);
        if ([image.class conformsToProtocol:@protocol(YSSDAnimatedImage)]) {
            // `YSSDAnimatedImage` do not decode
            shouldDecode = NO;
        } else if (image.yssd_isAnimated) {
            // animated image do not decode
            shouldDecode = NO;
        }
        
        if (shouldDecode) {
            image = [YSSDImageCoderHelper decodedImageWithImage:image];
        }
    }
    
    return image;
}

UIImage * _Nullable YSSDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, BOOL finished,  id<YSSDWebImageOperation> _Nonnull operation, YSSDWebImageOptions options, YSSDWebImageContext * _Nullable context) {
    NSCParameterAssert(imageData);
    NSCParameterAssert(imageURL);
    NSCParameterAssert(operation);
    
    UIImage *image;
    id<YSSDWebImageCacheKeyFilter> cacheKeyFilter = context[YSSDWebImageContextCacheKeyFilter];
    NSString *cacheKey;
    if (cacheKeyFilter) {
        cacheKey = [cacheKeyFilter cacheKeyForURL:imageURL];
    } else {
        cacheKey = imageURL.absoluteString;
    }
    BOOL decodeFirstFrame = YSSD_OPTIONS_CONTAINS(options, YSSDWebImageDecodeFirstFrameOnly);
    NSNumber *scaleValue = context[YSSDWebImageContextImageScaleFactor];
    CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : YSSDImageScaleFactorForKey(cacheKey);
    NSNumber *preserveAspectRatioValue = context[YSSDWebImageContextImagePreserveAspectRatio];
    NSValue *thumbnailSizeValue;
    BOOL shouldScaleDown = YSSD_OPTIONS_CONTAINS(options, YSSDWebImageScaleDownLargeImages);
    if (shouldScaleDown) {
        CGFloat thumbnailPixels = YSSDImageCoderHelper.defaultScaleDownLimitBytes / 4;
        CGFloat dimension = ceil(sqrt(thumbnailPixels));
        thumbnailSizeValue = @(CGSizeMake(dimension, dimension));
    }
    if (context[YSSDWebImageContextImageThumbnailPixelSize]) {
        thumbnailSizeValue = context[YSSDWebImageContextImageThumbnailPixelSize];
    }
    
    YSSDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
    mutableCoderOptions[YSSDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
    mutableCoderOptions[YSSDImageCoderDecodeScaleFactor] = @(scale);
    mutableCoderOptions[YSSDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
    mutableCoderOptions[YSSDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
    mutableCoderOptions[YSSDImageCoderWebImageContext] = context;
    YSSDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
    
    // Grab the progressive image coder
    id<YSSDProgressiveImageCoder> progressiveCoder = objc_getAssociatedObject(operation, YSSDImageLoaderProgressiveCoderKey);
    if (!progressiveCoder) {
        id<YSSDProgressiveImageCoder> imageCoder = context[YSSDWebImageContextImageCoder];
        // Check the progressive coder if provided
        if ([imageCoder conformsToProtocol:@protocol(YSSDProgressiveImageCoder)]) {
            progressiveCoder = [[[imageCoder class] alloc] initIncrementalWithOptions:coderOptions];
        } else {
            // We need to create a new instance for progressive decoding to avoid conflicts
            for (id<YSSDImageCoder> coder in [YSSDImageCodersManager sharedManager].coders.reverseObjectEnumerator) {
                if ([coder conformsToProtocol:@protocol(YSSDProgressiveImageCoder)] &&
                    [((id<YSSDProgressiveImageCoder>)coder) canIncrementalDecodeFromData:imageData]) {
                    progressiveCoder = [[[coder class] alloc] initIncrementalWithOptions:coderOptions];
                    break;
                }
            }
        }
        objc_setAssociatedObject(operation, YSSDImageLoaderProgressiveCoderKey, progressiveCoder, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    // If we can't find any progressive coder, disable progressive download
    if (!progressiveCoder) {
        return nil;
    }
    
    [progressiveCoder updateIncrementalData:imageData finished:finished];
    if (!decodeFirstFrame) {
        // check whether we should use `YSSDAnimatedImage`
        Class animatedImageClass = context[YSSDWebImageContextAnimatedImageClass];
        if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(YSSDAnimatedImage)] && [progressiveCoder conformsToProtocol:@protocol(YSSDAnimatedImageCoder)]) {
            image = [[animatedImageClass alloc] initWithAnimatedCoder:(id<YSSDAnimatedImageCoder>)progressiveCoder scale:scale];
            if (image) {
                // Progressive decoding does not preload frames
            } else {
                // Check image class matching
                if (options & YSSDWebImageMatchAnimatedImageClass) {
                    return nil;
                }
            }
        }
    }
    if (!image) {
        image = [progressiveCoder incrementalDecodedImageWithOptions:coderOptions];
    }
    if (image) {
        BOOL shouldDecode = !YSSD_OPTIONS_CONTAINS(options, YSSDWebImageAvoidDecodeImage);
        if ([image.class conformsToProtocol:@protocol(YSSDAnimatedImage)]) {
            // `YSSDAnimatedImage` do not decode
            shouldDecode = NO;
        } else if (image.yssd_isAnimated) {
            // animated image do not decode
            shouldDecode = NO;
        }
        if (shouldDecode) {
            image = [YSSDImageCoderHelper decodedImageWithImage:image];
        }
        // mark the image as progressive (completionBlock one are not mark as progressive)
        image.yssd_isIncremental = YES;
    }
    
    return image;
}

YSSDWebImageContextOption const YSSDWebImageContextLoaderCachedImage = @"loaderCachedImage";
