Pods/PINRemoteImage/Source/Classes/PINProgressiveImage.m
//
//  PINProgressiveImage.m
//  Pods
//
//  Created by Garrett Moon on 2/9/15.
//
//

#import "PINProgressiveImage.h"

#import <ImageIO/ImageIO.h>
#import <Accelerate/Accelerate.h>

#import "PINRemoteImage.h"
#import "PINImage+DecodedImage.h"
#import "PINRemoteImageDownloadTask.h"
#import "PINSpeedRecorder.h"

@interface PINProgressiveImage ()

@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
@property (nonatomic, strong) NSMutableData *mutableData;
@property (nonatomic, assign) int64_t expectedNumberOfBytes;
@property (nonatomic, assign) CGImageSourceRef imageSource;
@property (nonatomic, assign) CGSize size;
@property (nonatomic, assign) BOOL isProgressiveJPEG;
@property (nonatomic, assign) NSUInteger currentThreshold;
@property (nonatomic, assign) NSUInteger startingBytes;
@property (nonatomic, assign) NSUInteger scannedByte;
@property (nonatomic, assign) NSInteger sosCount;
@property (nonatomic, strong) PINRemoteLock *lock;
#if DEBUG
@property (nonatomic, assign) CFTimeInterval scanTime;
#endif

@end

@implementation PINProgressiveImage

@synthesize progressThresholds = _progressThresholds;
@synthesize estimatedRemainingTimeThreshold = _estimatedRemainingTimeThreshold;

- (nonnull instancetype)initWithDataTask:(nonnull NSURLSessionDataTask *)dataTask
{
    if (self = [super init]) {
        self.lock = [[PINRemoteLock alloc] initWithName:@"PINProgressiveImage"];
        
        _dataTask = dataTask;
        _imageSource = CGImageSourceCreateIncremental(NULL);;
        self.size = CGSizeZero;
        self.isProgressiveJPEG = NO;
        self.currentThreshold = 0;
        self.progressThresholds = @[@0.00, @0.35, @0.65];
        self.estimatedRemainingTimeThreshold = -1;
        self.sosCount = 0;
        self.scannedByte = 0;
#if DEBUG
        self.scanTime = 0;
#endif
    }
    return self;
}

- (void)dealloc
{
    [self.lock lock];
        if (self.imageSource) {
            CFRelease(_imageSource);
        }
    [self.lock unlock];
}

#pragma mark - public

- (void)setProgressThresholds:(NSArray *)progressThresholds
{
    // There's no reason to set an empty progress thresholds array, instead don't use the progressive feature
    if (progressThresholds.count > 0) {
        [self.lock lock];
            _progressThresholds = [progressThresholds copy];
        [self.lock unlock];
    }
}

- (NSArray *)progressThresholds
{
    [self.lock lock];
        NSArray *progressThresholds = _progressThresholds;
    [self.lock unlock];
    return progressThresholds;
}

- (void)setEstimatedRemainingTimeThreshold:(CFTimeInterval)estimatedRemainingTimeThreshold
{
    [self.lock lock];
        _estimatedRemainingTimeThreshold = estimatedRemainingTimeThreshold;
    [self.lock unlock];
}

- (CFTimeInterval)estimatedRemainingTimeThreshold
{
    [self.lock lock];
        CFTimeInterval estimatedRemainingTimeThreshold = _estimatedRemainingTimeThreshold;
    [self.lock unlock];
    return estimatedRemainingTimeThreshold;
}

- (CFTimeInterval)estimatedRemainingTime
{
    __block CFTimeInterval estimatedRemainingTime;
    [self.lock lockWithBlock:^{
        estimatedRemainingTime = [self l_estimatedRemainingTime];
    }];
    return estimatedRemainingTime;
}

- (CFTimeInterval)l_estimatedRemainingTime
{
    if (_dataTask.countOfBytesExpectedToReceive == NSURLSessionTransferSizeUnknown) {
        return MAXFLOAT;
    }
    NSUInteger remainingBytes = (NSUInteger)_dataTask.countOfBytesExpectedToReceive - (NSUInteger)_dataTask.countOfBytesReceived;
    if (remainingBytes == 0) {
        return 0;
    }
    
    float bytesPerSecond = [[PINSpeedRecorder sharedRecorder] weightedAdjustedBytesPerSecondForHost:[_dataTask.currentRequest.URL host]];
    if (bytesPerSecond == -1) {
        return MAXFLOAT;
    }
    return remainingBytes / bytesPerSecond;
}

- (void)updateProgressiveImageWithData:(nonnull NSData *)data expectedNumberOfBytes:(int64_t)expectedNumberOfBytes isResume:(BOOL)isResume
{
    [self.lock lock];
        if (isResume) {
            NSAssert(self.mutableData == nil, @"If we're resuming, data shouldn't be setup yet.");
            self.startingBytes = data.length;
        }
    
        if (self.mutableData == nil) {
            NSUInteger bytesToAlloc = 0;
            if (expectedNumberOfBytes > 0) {
                bytesToAlloc = (NSUInteger)expectedNumberOfBytes;
            }
            self.mutableData = [[NSMutableData alloc] initWithCapacity:bytesToAlloc];
            self.expectedNumberOfBytes = expectedNumberOfBytes;
        }
        [self.mutableData appendData:data];
        
        while ([self l_hasCompletedFirstScan] == NO && self.scannedByte < self.mutableData.length) {
    #if DEBUG
            CFTimeInterval start = CACurrentMediaTime();
    #endif
            NSUInteger startByte = self.scannedByte;
            if (startByte > 0) {
                startByte--;
            }
            if ([self l_scanForSOSinData:self.mutableData startByte:startByte scannedByte:&_scannedByte]) {
                self.sosCount++;
            }
    #if DEBUG
            CFTimeInterval total = CACurrentMediaTime() - start;
            self.scanTime += total;
    #endif
        }
        
        if (self.imageSource) {
            CGImageSourceUpdateData(self.imageSource, (CFDataRef)self.mutableData, NO);
        }
    [self.lock unlock];
}

- (PINImage *)currentImageBlurred:(BOOL)blurred maxProgressiveRenderSize:(CGSize)maxProgressiveRenderSize renderedImageQuality:(out CGFloat *)renderedImageQuality
{
    [self.lock lock];
        if (self.imageSource == nil) {
            [self.lock unlock];
            return nil;
        }
        
        if (self.currentThreshold == _progressThresholds.count) {
            [self.lock unlock];
            return nil;
        }
        
        if (_estimatedRemainingTimeThreshold > 0 && [self l_estimatedRemainingTime] < _estimatedRemainingTimeThreshold) {
            [self.lock unlock];
            return nil;
        }
        
        if ([self l_hasCompletedFirstScan] == NO) {
            [self.lock unlock];
            return nil;
        }
        
    #if DEBUG
        if (self.scanTime > 0) {
            PINLog(@"scan time: %f", self.scanTime);
            self.scanTime = 0;
        }
    #endif
        
        PINImage *currentImage = nil;
        
        //Size information comes after JFIF so jpeg properties should be available at or before size?
        if (self.size.width <= 0 || self.size.height <= 0) {
            //attempt to get size info
            NSDictionary *imageProperties = (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(self.imageSource, 0, NULL));
            CGSize size = self.size;
            if (size.width <= 0 && imageProperties[(NSString *)kCGImagePropertyPixelWidth]) {
                size.width = [imageProperties[(NSString *)kCGImagePropertyPixelWidth] floatValue];
            }
            
            if (size.height <= 0 && imageProperties[(NSString *)kCGImagePropertyPixelHeight]) {
                size.height = [imageProperties[(NSString *)kCGImagePropertyPixelHeight] floatValue];
            }
            
            self.size = size;
            
            NSDictionary *jpegProperties = imageProperties[(NSString *)kCGImagePropertyJFIFDictionary];
            NSNumber *isProgressive = jpegProperties[(NSString *)kCGImagePropertyJFIFIsProgressive];
            self.isProgressiveJPEG = jpegProperties && [isProgressive boolValue];
        }
    
        if (self.size.width > maxProgressiveRenderSize.width || self.size.height > maxProgressiveRenderSize.height) {
            [self.lock unlock];
            return nil;
        }
        
        float progress = 0;
        if (self.expectedNumberOfBytes > 0) {
            progress = (float)self.mutableData.length / (float)self.expectedNumberOfBytes;
        }
        
        //Don't bother if we're basically done
        if (progress >= 0.99) {
            [self.lock unlock];
            return nil;
        }

        if (self.isProgressiveJPEG && self.size.width > 0 && self.size.height > 0 && progress > [_progressThresholds[self.currentThreshold] floatValue]) {
            while (self.currentThreshold < _progressThresholds.count && progress > [_progressThresholds[self.currentThreshold] floatValue]) {
                self.currentThreshold++;
            }
            PINLog(@"Generating preview image");
            CGImageRef image = CGImageSourceCreateImageAtIndex(self.imageSource, 0, NULL);
            if (image) {
                if (blurred) {
                    currentImage = [self l_postProcessImage:[PINImage imageWithCGImage:image] withProgress:progress];
                } else {
                    currentImage = [PINImage imageWithCGImage:image];
                }
                CGImageRelease(image);
                if (renderedImageQuality) {
                    *renderedImageQuality = progress;
                }
            }
        }
    
    [self.lock unlock];
    return currentImage;
}

- (NSData *)data
{
    [self.lock lock];
        NSData *data = [self.mutableData copy];
    [self.lock unlock];
    return data;
}

#pragma mark - private

- (BOOL)l_scanForSOSinData:(NSData *)data startByte:(NSUInteger)startByte scannedByte:(NSUInteger *)scannedByte
{
    //check if we have a complete scan
    Byte scanMarker[2];
    //SOS marker
    scanMarker[0] = 0xFF;
    scanMarker[1] = 0xDA;
    
    //scan one byte back in case we only got half the SOS on the last data append
    NSRange scanRange;
    scanRange.location = startByte;
    scanRange.length = data.length - scanRange.location;
    NSRange sosRange = [data rangeOfData:[NSData dataWithBytes:scanMarker length:2] options:0 range:scanRange];
    if (sosRange.location != NSNotFound) {
        if (scannedByte) {
            *scannedByte = NSMaxRange(sosRange);
        }
        return YES;
    }
    if (scannedByte) {
        *scannedByte = NSMaxRange(scanRange);
    }
    return NO;
}

- (BOOL)l_hasCompletedFirstScan
{
    return self.sosCount >= 2;
}

//Heavily cribbed from https://developer.apple.com/library/ios/samplecode/UIImageEffects/Listings/UIImageEffects_UIImageEffects_m.html#//apple_ref/doc/uid/DTS40013396-UIImageEffects_UIImageEffects_m-DontLinkElementID_9
- (PINImage *)l_postProcessImage:(PINImage *)inputImage withProgress:(float)progress
{
    PINImage *outputImage = nil;
    CGImageRef inputImageRef = CGImageRetain(inputImage.CGImage);
    if (inputImageRef == nil) {
        return nil;
    }
    
    CGSize inputSize = inputImage.size;
    if (inputSize.width < 1 ||
        inputSize.height < 1) {
        CGImageRelease(inputImageRef);
        return nil;
    }

#if PIN_TARGET_IOS
    CGFloat imageScale = inputImage.scale;
#elif PIN_TARGET_MAC
    // TODO: What scale factor should be used here?
    CGFloat imageScale = [[NSScreen mainScreen] backingScaleFactor];
#endif
    
    CGFloat radius = (inputImage.size.width / 25.0) * MAX(0, 1.0 - progress);
    radius *= imageScale;
    
    //we'll round the radius to a whole number below anyway,
    if (radius < FLT_EPSILON) {
        CGImageRelease(inputImageRef);
        return inputImage;
    }
    
    CGContextRef ctx;
#if PIN_TARGET_IOS
    UIGraphicsBeginImageContextWithOptions(inputSize, YES, imageScale);
    ctx = UIGraphicsGetCurrentContext();
#elif PIN_TARGET_MAC
    ctx = CGBitmapContextCreate(0, inputSize.width, inputSize.height, 8, 0, [NSColorSpace genericRGBColorSpace].CGColorSpace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
#endif
    
    if (ctx) {
#if PIN_TARGET_IOS
        CGContextScaleCTM(ctx, 1.0, -1.0);
        CGContextTranslateCTM(ctx, 0, -inputSize.height);
#endif
        
        vImage_Buffer effectInBuffer;
        vImage_Buffer scratchBuffer;
        
        vImage_Buffer *inputBuffer;
        vImage_Buffer *outputBuffer;
        
        vImage_CGImageFormat format = {
            .bitsPerComponent = 8,
            .bitsPerPixel = 32,
            .colorSpace = NULL,
            // (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little)
            // requests a BGRA buffer.
            .bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little,
            .version = 0,
            .decode = NULL,
            .renderingIntent = kCGRenderingIntentDefault
        };
        
        vImage_Error e = vImageBuffer_InitWithCGImage(&effectInBuffer, &format, NULL, inputImage.CGImage, kvImagePrintDiagnosticsToConsole);
        if (e == kvImageNoError)
        {
            e = vImageBuffer_Init(&scratchBuffer, effectInBuffer.height, effectInBuffer.width, format.bitsPerPixel, kvImageNoFlags);
            if (e == kvImageNoError) {
                inputBuffer = &effectInBuffer;
                outputBuffer = &scratchBuffer;
                
                // A description of how to compute the box kernel width from the Gaussian
                // radius (aka standard deviation) appears in the SVG spec:
                // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
                //
                // For larger values of 's' (s >= 2.0), an approximation can be used: Three
                // successive box-blurs build a piece-wise quadratic convolution kernel, which
                // approximates the Gaussian kernel to within roughly 3%.
                //
                // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
                //
                // ... if d is odd, use three box-blurs of size 'd', centered on the output pixel.
                //
                if (radius - 2. < __FLT_EPSILON__)
                    radius = 2.;
                uint32_t wholeRadius = floor((radius * 3. * sqrt(2 * M_PI) / 4 + 0.5) / 2);
                
                wholeRadius |= 1; // force wholeRadius to be odd so that the three box-blur methodology works.
                
                //calculate the size necessary for vImageBoxConvolve_ARGB8888, this does not actually do any operations.
                NSInteger tempBufferSize = vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, NULL, 0, 0, wholeRadius, wholeRadius, NULL, kvImageGetTempBufferSize | kvImageEdgeExtend);
                void *tempBuffer = malloc(tempBufferSize);
                
                if (tempBuffer) {
                    //errors can be ignored because we've passed in allocated memory
                    vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, tempBuffer, 0, 0, wholeRadius, wholeRadius, NULL, kvImageEdgeExtend);
                    vImageBoxConvolve_ARGB8888(outputBuffer, inputBuffer, tempBuffer, 0, 0, wholeRadius, wholeRadius, NULL, kvImageEdgeExtend);
                    vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, tempBuffer, 0, 0, wholeRadius, wholeRadius, NULL, kvImageEdgeExtend);
                    
                    free(tempBuffer);
                    
                    //switch input and output
                    vImage_Buffer *temp = inputBuffer;
                    inputBuffer = outputBuffer;
                    outputBuffer = temp;
                    
                    CGImageRef effectCGImage = vImageCreateCGImageFromBuffer(inputBuffer, &format, &cleanupBuffer, NULL, kvImageNoAllocate, NULL);
                    if (effectCGImage == NULL) {
                        //if creating the cgimage failed, the cleanup buffer on input buffer will not be called, we must dealloc ourselves
                        free(inputBuffer->data);
                    } else {
                        // draw effect image
                        CGContextSaveGState(ctx);
                        CGContextDrawImage(ctx, CGRectMake(0, 0, inputSize.width, inputSize.height), effectCGImage);
                        CGContextRestoreGState(ctx);
                        CGImageRelease(effectCGImage);
                    }
                    
                    // Cleanup
                    free(outputBuffer->data);
#if PIN_TARGET_IOS
                    outputImage = UIGraphicsGetImageFromCurrentImageContext();
#elif PIN_TARGET_MAC
                    CGImageRef outputImageRef = CGBitmapContextCreateImage(ctx);
                    outputImage = [[NSImage alloc] initWithCGImage:outputImageRef size:inputSize];
                    CFRelease(outputImageRef);
#endif
                    
                }
            } else {
                if (scratchBuffer.data) {
                    free(scratchBuffer.data);
                }
                free(effectInBuffer.data);
            }
        } else {
            if (effectInBuffer.data) {
                free(effectInBuffer.data);
            }
        }
    }
    
#if PIN_TARGET_IOS
    UIGraphicsEndImageContext();
#endif

    CGImageRelease(inputImageRef);
    
    return outputImage;
}

//  Helper function to handle deferred cleanup of a buffer.
static void cleanupBuffer(void *userData, void *buf_data)
{
    free(buf_data);
}

@end
