/**
 * 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.
 */

#import "FBScreenshot.h"

@import UniformTypeIdentifiers;

#import "FBConfiguration.h"
#import "FBErrorBuilder.h"
#import "FBImageProcessor.h"
#import "FBLogger.h"
#import "FBMacros.h"
#import "FBXCodeCompatibility.h"
#import "FBXCTestDaemonsProxy.h"
#import "XCTestManager_ManagerInterface-Protocol.h"
#import "XCUIScreen.h"

static const NSTimeInterval SCREENSHOT_TIMEOUT = 20.;
static const CGFloat SCREENSHOT_SCALE = 1.0;  // Screenshot API should keep the original screen scale
static const CGFloat HIGH_QUALITY = 0.8;
static const CGFloat LOW_QUALITY = 0.25;

NSString *formatTimeInterval(NSTimeInterval interval) {
  NSUInteger milliseconds = (NSUInteger)(interval * 1000);
  return [NSString stringWithFormat:@"%lu ms", milliseconds];
}

@implementation FBScreenshot

+ (CGFloat)compressionQualityWithQuality:(NSUInteger)quality
{
  switch (quality) {
    case 1:
      return HIGH_QUALITY;
    case 2:
      return LOW_QUALITY;
    default:
      return 1.0;
  }
}

+ (UTType *)imageUtiWithQuality:(NSUInteger)quality
{
  switch (quality) {
    case 1:
    case 2:
      return UTTypeJPEG;
    case 3:
      return UTTypeHEIC;
    default:
      return UTTypePNG;
  }
}

+ (NSData *)takeInOriginalResolutionWithQuality:(NSUInteger)quality
                                          error:(NSError **)error
{
  XCUIScreen *mainScreen = XCUIScreen.mainScreen;
  return [self.class takeWithScreenID:mainScreen.displayID
                                scale:SCREENSHOT_SCALE
                   compressionQuality:[self.class compressionQualityWithQuality:quality]
                            sourceUTI:[self.class imageUtiWithQuality:quality]
                                error:error];
}

+ (NSData *)takeWithScreenID:(long long)screenID
                       scale:(CGFloat)scale
          compressionQuality:(CGFloat)compressionQuality
                   sourceUTI:(UTType *)uti
                       error:(NSError **)error
{
  NSData *screenshotData = [self.class takeInOriginalResolutionWithScreenID:screenID
                                                         compressionQuality:compressionQuality
                                                                        uti:uti
                                                                    timeout:SCREENSHOT_TIMEOUT
                                                                      error:error];
  if (nil == screenshotData) {
    return nil;
  }
  return [[[FBImageProcessor alloc] init] scaledImageWithData:screenshotData
                                                          uti:UTTypePNG
                                                scalingFactor:1.0 / scale
                                           compressionQuality:FBMaxCompressionQuality
                                                        error:error];
}

+ (NSData *)takeInOriginalResolutionWithScreenID:(long long)screenID
                              compressionQuality:(CGFloat)compressionQuality
                                             uti:(UTType *)uti
                                         timeout:(NSTimeInterval)timeout
                                           error:(NSError **)error
{
  id<XCTestManager_ManagerInterface> proxy = [FBXCTestDaemonsProxy testRunnerProxy];
  __block NSData *screenshotData = nil;
  __block NSError *innerError = nil;
  dispatch_semaphore_t sem = dispatch_semaphore_create(0);
  id screnshotRequest = [self.class screenshotRequestWithScreenID:screenID
                                                             rect:CGRectNull
                                                              uti:uti
                                               compressionQuality:compressionQuality
                                                            error:error];
  if (nil == screnshotRequest) {
    return nil;
  }
  [proxy _XCT_requestScreenshot:screnshotRequest
                      withReply:^(id image, NSError *err) {
    if (nil != err) {
      innerError = err;
    } else {
      screenshotData = [image data];
    }
    dispatch_semaphore_signal(sem);
  }];
  int64_t timeoutNs = (int64_t)(timeout * NSEC_PER_SEC);
  if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) {
    NSString *timeoutMsg = [NSString stringWithFormat:@"Cannot take a screenshot within %@ timeout", formatTimeInterval(SCREENSHOT_TIMEOUT)];
    if (nil == error) {
      [FBLogger log:timeoutMsg];
    } else if (nil == innerError) {
      [[[FBErrorBuilder builder]
        withDescription:timeoutMsg]
       buildError:error];
    }
  }
  if (nil != error && nil != innerError) {
    *error = innerError;
  }
  return screenshotData;
}

+ (nullable id)imageEncodingWithUniformTypeIdentifier:(UTType *)uti
                                   compressionQuality:(CGFloat)compressionQuality
                                                error:(NSError **)error
{
  Class imageEncodingClass = NSClassFromString(@"XCTImageEncoding");
  if (nil == imageEncodingClass) {
    [[[FBErrorBuilder builder]
      withDescription:@"Cannot find XCTImageEncoding class"]
     buildError:error];
    return nil;
  }

  if ([uti conformsToType:UTTypeHEIC]) {
    static BOOL isHeicSuppported = NO;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
      SEL selector = NSSelectorFromString(@"supportsHEICImageEncoding");
      NSMethodSignature *signature = [imageEncodingClass methodSignatureForSelector:selector];
      if (nil != signature) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        [invocation setSelector:selector];
        [invocation invokeWithTarget:imageEncodingClass];
        [invocation getReturnValue:&isHeicSuppported];
      }
    });
    if (!isHeicSuppported) {
      [FBLogger logFmt:@"The device under test does not support HEIC image encoding. Falling back to PNG"];
      uti = UTTypePNG;
    }
  }

  id imageEncodingAllocated = [imageEncodingClass alloc];
  SEL imageEncodingConstructorSelector = NSSelectorFromString(@"initWithUniformTypeIdentifier:compressionQuality:");
  if (![imageEncodingAllocated respondsToSelector:imageEncodingConstructorSelector]) {
    [[[FBErrorBuilder builder]
      withDescription:@"'initWithUniformTypeIdentifier:compressionQuality:' contructor is not found on XCTImageEncoding class"]
     buildError:error];
    return nil;
  }
  NSMethodSignature *imageEncodingContructorSignature = [imageEncodingAllocated methodSignatureForSelector:imageEncodingConstructorSelector];
  NSInvocation *imageEncodingInitInvocation = [NSInvocation invocationWithMethodSignature:imageEncodingContructorSignature];
  [imageEncodingInitInvocation setSelector:imageEncodingConstructorSelector];
  NSString *utiIdentifier = uti.identifier;
  [imageEncodingInitInvocation setArgument:&utiIdentifier atIndex:2];
  [imageEncodingInitInvocation setArgument:&compressionQuality atIndex:3];
  [imageEncodingInitInvocation invokeWithTarget:imageEncodingAllocated];
  id __unsafe_unretained imageEncoding;
  [imageEncodingInitInvocation getReturnValue:&imageEncoding];
  return imageEncoding;
}

+ (nullable id)screenshotRequestWithScreenID:(long long)screenID
                                        rect:(struct CGRect)rect
                                         uti:(UTType *)uti
                          compressionQuality:(CGFloat)compressionQuality
                                       error:(NSError **)error
{
  id imageEncoding = [self.class imageEncodingWithUniformTypeIdentifier:uti
                                                     compressionQuality:compressionQuality
                                                                  error:error];
  if (nil == imageEncoding) {
    return nil;
  }

  Class screenshotRequestClass = NSClassFromString(@"XCTScreenshotRequest");
  if (nil == screenshotRequestClass) {
    [[[FBErrorBuilder builder]
      withDescription:@"Cannot find XCTScreenshotRequest class"]
     buildError:error];
    return nil;
  }
  id screenshotRequestAllocated = [screenshotRequestClass alloc];
  SEL screenshotRequestConstructorSelector = NSSelectorFromString(@"initWithScreenID:rect:encoding:");
  if (![screenshotRequestAllocated respondsToSelector:screenshotRequestConstructorSelector]) {
    [[[FBErrorBuilder builder]
      withDescription:@"'initWithScreenID:rect:encoding:' contructor is not found on XCTScreenshotRequest class"]
     buildError:error];
    return nil;
  }
  NSMethodSignature *screenshotRequestContructorSignature = [screenshotRequestAllocated methodSignatureForSelector:screenshotRequestConstructorSelector];
  NSInvocation *screenshotRequestInitInvocation = [NSInvocation invocationWithMethodSignature:screenshotRequestContructorSignature];
  [screenshotRequestInitInvocation setSelector:screenshotRequestConstructorSelector];
  [screenshotRequestInitInvocation setArgument:&screenID atIndex:2];
  [screenshotRequestInitInvocation setArgument:&rect atIndex:3];
  [screenshotRequestInitInvocation setArgument:&imageEncoding atIndex:4];
  [screenshotRequestInitInvocation invokeWithTarget:screenshotRequestAllocated];
  id __unsafe_unretained screenshotRequest;
  [screenshotRequestInitInvocation getReturnValue:&screenshotRequest];
  return screenshotRequest;
}

@end
