/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

#import "RCTImageLoader.h"

#import <UIKit/UIKit.h>

#import "RCTConvert.h"
#import "RCTDefines.h"
#import "RCTImageUtils.h"
#import "RCTLog.h"
#import "RCTNetworking.h"
#import "RCTUtils.h"

static void RCTDispatchCallbackOnMainQueue(void (^callback)(NSError *, id), NSError *error, UIImage *image)
{
  if ([NSThread isMainThread]) {
    callback(error, image);
  } else {
    dispatch_async(dispatch_get_main_queue(), ^{
      callback(error, image);
    });
  }
}

@implementation UIImage (React)

- (CAKeyframeAnimation *)reactKeyframeAnimation
{
  return objc_getAssociatedObject(self, _cmd);
}

- (void)setReactKeyframeAnimation:(CAKeyframeAnimation *)reactKeyframeAnimation
{
  objc_setAssociatedObject(self, @selector(reactKeyframeAnimation), reactKeyframeAnimation, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

@implementation RCTImageLoader
{
  NSArray *_loaders;
  NSArray *_decoders;
  NSURLCache *_cache;
}

@synthesize bridge = _bridge;

RCT_EXPORT_MODULE()

- (void)setBridge:(RCTBridge *)bridge
{
  // Get image loaders and decoders
  NSMutableArray *loaders = [NSMutableArray array];
  NSMutableArray *decoders = [NSMutableArray array];
  for (id<RCTBridgeModule> module in bridge.modules.allValues) {
    if ([module conformsToProtocol:@protocol(RCTImageURLLoader)]) {
      [loaders addObject:module];
    }
    if ([module conformsToProtocol:@protocol(RCTImageDataDecoder)]) {
      [decoders addObject:module];
    }
  }

  // Sort loaders in reverse priority order (highest priority first)
  [loaders sortUsingComparator:^NSComparisonResult(id<RCTImageURLLoader> a, id<RCTImageURLLoader> b) {
    float priorityA = [a respondsToSelector:@selector(loaderPriority)] ? [a loaderPriority] : 0;
    float priorityB = [b respondsToSelector:@selector(loaderPriority)] ? [b loaderPriority] : 0;
    if (priorityA > priorityB) {
      return NSOrderedAscending;
    } else if (priorityA < priorityB) {
      return NSOrderedDescending;
    } else {
      return NSOrderedSame;
    }
  }];

  // Sort decoders in reverse priority order (highest priority first)
  [decoders sortUsingComparator:^NSComparisonResult(id<RCTImageDataDecoder> a, id<RCTImageDataDecoder> b) {
    float priorityA = [a respondsToSelector:@selector(decoderPriority)] ? [a decoderPriority] : 0;
    float priorityB = [b respondsToSelector:@selector(decoderPriority)] ? [b decoderPriority] : 0;
    if (priorityA > priorityB) {
      return NSOrderedAscending;
    } else if (priorityA < priorityB) {
      return NSOrderedDescending;
    } else {
      return NSOrderedSame;
    }
  }];

  _bridge = bridge;
  _loaders = loaders;
  _decoders = decoders;
  _cache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024 // 5MB
                                         diskCapacity:200 * 1024 * 1024 // 200MB
                                             diskPath:@"React/RCTImageDownloader"];
}

- (id<RCTImageURLLoader>)imageURLLoaderForURL:(NSURL *)URL
{
  if (RCT_DEBUG) {
    // Check for handler conflicts
    float previousPriority = 0;
    id<RCTImageURLLoader> previousLoader = nil;
    for (id<RCTImageURLLoader> loader in _loaders) {
      if ([loader canLoadImageURL:URL]) {
        float priority = [loader respondsToSelector:@selector(loaderPriority)] ? [loader loaderPriority] : 0;
        if (previousLoader) {
          if (priority == previousPriority) {
            RCTLogError(@"The RCTImageURLLoaders %@ and %@ both reported that"
                        " they can load the URL %@, and have equal priority"
                        " (%g). This could result in non-deterministic behavior.",
                        loader, previousLoader, URL, priority);
          }
        } else {
          previousLoader = loader;
          previousPriority = priority;
        }
      }
    }
  }

  // Normal code path
  for (id<RCTImageURLLoader> loader in _loaders) {
    if ([loader canLoadImageURL:URL]) {
      return loader;
    }
  }
  return nil;
}

- (id<RCTImageDataDecoder>)imageDataDecoderForData:(NSData *)data
{
  if (RCT_DEBUG) {
    // Check for handler conflicts
    float previousPriority = 0;
    id<RCTImageDataDecoder> previousDecoder = nil;
    for (id<RCTImageDataDecoder> decoder in _decoders) {
      if ([decoder canDecodeImageData:data]) {
        float priority = [decoder respondsToSelector:@selector(decoderPriority)] ? [decoder decoderPriority] : 0;
        if (previousDecoder) {
          if (priority == previousPriority) {
            RCTLogError(@"The RCTImageDataDecoders %@ and %@ both reported that"
                        " they can decode the data <NSData %p; %tu bytes>, and"
                        " have equal priority (%g). This could result in"
                        " non-deterministic behavior.",
                        decoder, previousDecoder, data, data.length, priority);
          }
        } else {
          previousDecoder = decoder;
          previousPriority = priority;
        }
      }
    }
  }

  // Normal code path
  for (id<RCTImageDataDecoder> decoder in _decoders) {
    if ([decoder canDecodeImageData:data]) {
      return decoder;
    }
  }
  return nil;
}

- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
                                           callback:(RCTImageLoaderCompletionBlock)callback
{
  return [self loadImageWithTag:imageTag
                           size:CGSizeZero
                          scale:1
                     resizeMode:UIViewContentModeScaleToFill
                  progressBlock:nil
                completionBlock:callback];
}

- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
                                               size:(CGSize)size
                                              scale:(CGFloat)scale
                                         resizeMode:(UIViewContentMode)resizeMode
                                      progressBlock:(RCTImageLoaderProgressBlock)progressBlock
                                    completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
{
  if (imageTag.length == 0) {
    RCTLogWarn(@"source.uri should not be an empty string <Native>");
    return ^{};
  }

  // Ensure progress is dispatched on main thread
  RCTImageLoaderProgressBlock progressHandler = nil;
  if (progressBlock) {
    progressHandler = ^(int64_t progress, int64_t total) {
      if ([NSThread isMainThread]) {
        progressBlock(progress, total);
      } else {
        dispatch_async(dispatch_get_main_queue(), ^{
          progressBlock(progress, total);
        });
      }
    };
  }

  // Ensure completion is dispatched on main thread
  RCTImageLoaderCompletionBlock completionHandler = ^(NSError *error, UIImage *image) {
    RCTDispatchCallbackOnMainQueue(completionBlock, error, image);
  };

  // Find suitable image URL loader
  NSURLRequest *request = [RCTConvert NSURLRequest:imageTag];
  id<RCTImageURLLoader> loadHandler = [self imageURLLoaderForURL:request.URL];
  if (loadHandler) {
    return [loadHandler loadImageForURL:request.URL
                                   size:size
                                  scale:scale
                             resizeMode:resizeMode
                        progressHandler:progressHandler
                      completionHandler:completionHandler];
  }

  // Check if networking module is available
  if (![_bridge respondsToSelector:@selector(networking)]) {
    RCTLogError(@"No suitable image URL loader found for %@. You may need to "
                " import the RCTNetworking library in order to load images.",
                imageTag);
    return ^{};
  }

  // Use networking module to load image
  if ([_bridge.networking canHandleRequest:request]) {

    __weak RCTImageLoader *weakSelf = self;
    __block RCTImageLoaderCancellationBlock decodeCancel = nil;
    RCTURLRequestCompletionBlock processResponse =
    ^(NSURLResponse *response, NSData *data, __unused NSError *error) {

      // Check for http errors
      if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
        NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
        if (statusCode != 200) {
          completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain
                                                       code:statusCode
                                                   userInfo:nil], nil);
          return;
        }
      }

      // Decode image
      decodeCancel = [weakSelf decodeImageData:data
                                          size:size
                                         scale:scale
                                    resizeMode:resizeMode
                               completionBlock:completionHandler];
    };

    // Check for cached response before reloading
    // TODO: move URL cache out of RCTImageLoader into its own module
    NSCachedURLResponse *cachedResponse = [_cache cachedResponseForRequest:request];
    if (cachedResponse) {
      processResponse(cachedResponse.response, cachedResponse.data, nil);
      return ^{};
    }

    // Add missing png extension
    if (request.URL.fileURL && request.URL.pathExtension.length == 0) {
      NSMutableURLRequest *mutableRequest = [request mutableCopy];
      mutableRequest.URL = [NSURL fileURLWithPath:[request.URL.path stringByAppendingPathExtension:@"png"]];
      request = mutableRequest;
    }

    // Download image
    RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:
                            ^(NSURLResponse *response, NSData *data, NSError *error) {
      if (error) {
        completionHandler(error, nil);
        return;
      }

      // Cache the response
      // TODO: move URL cache out of RCTImageLoader into its own module
      RCTImageLoader *strongSelf = weakSelf;
      BOOL isHTTPRequest = [request.URL.scheme hasPrefix:@"http"];
      [strongSelf->_cache storeCachedResponse:
       [[NSCachedURLResponse alloc] initWithResponse:response
                                                data:data
                                            userInfo:nil
                                       storagePolicy:isHTTPRequest ? NSURLCacheStorageAllowed: NSURLCacheStorageAllowedInMemoryOnly]
                                   forRequest:request];

      // Process image data
      processResponse(response, data, nil);

    }];
    if (progressBlock) {
      task.downloadProgressBlock = progressBlock;
    }
    [task start];

    return ^{
      [task cancel];
      if (decodeCancel) {
        decodeCancel();
      }
    };
  }

  RCTLogError(@"No suitable image URL loader found for %@", imageTag);
  return ^{};
}

- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data
                                              size:(CGSize)size
                                             scale:(CGFloat)scale
                                        resizeMode:(UIViewContentMode)resizeMode
                                   completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
{
  id<RCTImageDataDecoder> imageDecoder = [self imageDataDecoderForData:data];
  if (imageDecoder) {
    return [imageDecoder decodeImageData:data
                                    size:size
                                   scale:scale
                              resizeMode:resizeMode
                       completionHandler:completionBlock];
  } else {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      UIImage *image = [UIImage imageWithData:data scale:scale];
      if (image) {
        completionBlock(nil, image);
      } else {
        NSString *errorMessage = [NSString stringWithFormat:@"Error decoding image data <NSData %p; %tu bytes>", data, data.length];
        NSError *finalError = RCTErrorWithMessage(errorMessage);
        completionBlock(finalError, nil);
      }
    });
    return ^{};
  }
}

#pragma mark - RCTURLRequestHandler

- (BOOL)canHandleRequest:(NSURLRequest *)request
{
  NSURL *requestURL = request.URL;
  for (id<RCTBridgeModule> module in _bridge.modules.allValues) {
    if ([module conformsToProtocol:@protocol(RCTImageURLLoader)] &&
        [(id<RCTImageURLLoader>)module canLoadImageURL:requestURL]) {
      return YES;
    }
  }
  return NO;
}

- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate
{
  __block RCTImageLoaderCancellationBlock requestToken;
  requestToken = [self loadImageWithTag:request.URL.absoluteString callback:^(NSError *error, UIImage *image) {
    if (error) {
      [delegate URLRequest:requestToken didCompleteWithError:error];
      return;
    }

    NSString *mimeType = nil;
    NSData *imageData = nil;
    if (RCTImageHasAlpha(image.CGImage)) {
      mimeType = @"image/png";
      imageData = UIImagePNGRepresentation(image);
    } else {
      mimeType = @"image/jpeg";
      imageData = UIImageJPEGRepresentation(image, 1.0);
    }

    NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
                                                        MIMEType:mimeType
                                           expectedContentLength:imageData.length
                                                textEncodingName:nil];

    [delegate URLRequest:requestToken didReceiveResponse:response];
    [delegate URLRequest:requestToken didReceiveData:imageData];
    [delegate URLRequest:requestToken didCompleteWithError:nil];
  }];

  return requestToken;
}

- (void)cancelRequest:(id)requestToken
{
  if (requestToken) {
    ((RCTImageLoaderCancellationBlock)requestToken)();
  }
}

@end

@implementation RCTBridge (RCTImageLoader)

- (RCTImageLoader *)imageLoader
{
  return self.modules[RCTBridgeModuleNameForClass([RCTImageLoader class])];
}

@end