#import "ReactNativeFs.h"

#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>

#import "NSArray+Map.h"
#import "Downloader.h"
#import "Uploader.h"

#import <React/RCTEventDispatcher.h>
#import <React/RCTUtils.h>

#if __has_include(<React/RCTImageLoader.h>)
#import <React/RCTImageLoader.h>
#else
#import <React/RCTImageLoaderProtocol.h>
#endif

#import <CommonCrypto/CommonDigest.h>
#import <Photos/Photos.h>

#import "RNFSBackgroundDownloads.h"
#import "RNFSException.h"

@implementation ReactNativeFs

// The prefix of "Bookmark URLs".
// See https://developer.apple.com/documentation/foundation/nsurl#1663783
// to learn about bookmark objects in iOS / macOS. To such object between
// native and JS layers, we convert it into binary (NSData) representation,
// then encode it into Base64 string, prefix it with this BOOKMARK prefix,
// and pass the resulting "Bookmark URLs" string around.
static NSString *BOOKMARK = @"bookmark://";

NSMutableDictionary<NSValue*,NSArray*> *pendingPickFilePromises;

RCT_EXPORT_MODULE()

- (instancetype) init
{
  pendingPickFilePromises = [NSMutableDictionary dictionaryWithCapacity:1];
  return [super init];
}

RCT_EXPORT_METHOD(readDir:(NSString *)dirPath
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSError *error = nil;
  NSURL *dirUrl = [ReactNativeFs pathToUrl:dirPath error:&error];
  if (error) return [[RNFSException fromError:error] reject:reject];

  BOOL allowed = [dirUrl startAccessingSecurityScopedResource];

  NSFileManager *fileManager = [NSFileManager defaultManager];

  @try {
    NSArray *contents = [fileManager contentsOfDirectoryAtURL:dirUrl
                                   includingPropertiesForKeys:@[
      NSURLContentModificationDateKey, NSURLCreationDateKey,
      NSURLFileSizeKey, NSURLIsDirectoryKey, NSURLIsRegularFileKey
    ] options:0 error:&error];
    NSMutableArray *tagetContents = [[NSMutableArray alloc] init];
    for (NSURL *url in contents) {
      NSDictionary *attrs = [url resourceValuesForKeys:@[
        NSURLContentModificationDateKey, NSURLCreationDateKey,
        NSURLFileSizeKey, NSURLIsDirectoryKey, NSURLIsRegularFileKey
      ] error:nil];

      if(attrs != nil) {
        NSNumber *size = [attrs objectForKey:NSURLFileSizeKey];
        if (size == nil) size = @(64);

        NSString *path = url.resourceSpecifier;

        NSString *type = @"N/A";
        if ([[attrs objectForKey:NSURLIsRegularFileKey] boolValue])
          type = NSFileTypeRegular;
        else if ([[attrs objectForKey:NSURLIsDirectoryKey] boolValue]) {
          type = NSFileTypeDirectory;

          // Trims closing dash from the end of folder paths.
          path = [path substringToIndex:[path length] - 1];
        }

        [tagetContents addObject:@{
          @"ctime": [self dateToTimeIntervalNumber:(NSDate *)[attrs objectForKey:NSURLCreationDateKey]],
          @"mtime": [self dateToTimeIntervalNumber:(NSDate *)[attrs objectForKey:NSURLContentModificationDateKey]],
          @"name": url.lastPathComponent,
          @"path": path,
          @"size": size,
          @"type": type
        }];
      }
    }

    if (error) return [[RNFSException fromError:error] reject:reject];

    resolve(tagetContents);
  }
  @catch (NSException *exception) {
    reject(@"exception", exception.reason, nil);
  }
  @finally {
    if (allowed) [dirUrl stopAccessingSecurityScopedResource];
  }
}

RCT_EXPORT_METHOD(exists:(NSString *)filepath
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(__unused RCTPromiseRejectBlock)reject)
{
  NSError *error = nil;
  NSURL *url = [ReactNativeFs pathToUrl:filepath error:&error];
  if (error) return [[RNFSException fromError:error] reject:reject];

  BOOL allowed = [url startAccessingSecurityScopedResource];

  @try {
    BOOL fileExists = [url checkResourceIsReachableAndReturnError:&error];
    resolve([NSNumber numberWithBool:fileExists]);
  }
  @finally {
    if (allowed) [url stopAccessingSecurityScopedResource];
  }
}

RCT_EXPORT_METHOD(stat:(NSString *)filepath
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSError *error = nil;
  NSURL *url = [ReactNativeFs pathToUrl:filepath error:&error];
  if (error) return [[RNFSException fromError:error] reject:reject];

  BOOL allowed = [url startAccessingSecurityScopedResource];

  @try {
    NSError *error = nil;
    NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filepath error:&error];

    if (error) return [[RNFSException fromError:error] reject:reject];

    attributes = @{
                   @"ctime": [self dateToTimeIntervalNumber:(NSDate *)[attributes objectForKey:NSFileCreationDate]],
                   @"mtime": [self dateToTimeIntervalNumber:(NSDate *)[attributes objectForKey:NSFileModificationDate]],
                   @"size": [attributes objectForKey:NSFileSize],
                   @"type": [attributes objectForKey:NSFileType],
                   @"mode": @([[NSString stringWithFormat:@"%ld", (long)[(NSNumber *)[attributes objectForKey:NSFilePosixPermissions] integerValue]] integerValue]),
                   @"originalFilepath": @"NOT_SUPPORTED_ON_IOS"
                   };

    resolve(attributes);
  }
  @finally {
    if (allowed) [url stopAccessingSecurityScopedResource];
  }
}

RCT_EXPORT_METHOD(writeFile:(NSString *)filepath
                  b64:(NSString *)base64Content
                  options:(JS::NativeReactNativeFs::FileOptionsT &)options
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSData *data = [[NSData alloc] initWithBase64EncodedString:base64Content options:NSDataBase64DecodingIgnoreUnknownCharacters];

  NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];

  if (options.NSFileProtectionKey() != nil) {
    [attributes setValue:options.NSFileProtectionKey() forKey:@"NSFileProtectionKey"];
  }

  BOOL success = [[NSFileManager defaultManager] createFileAtPath:filepath contents:data attributes:attributes];

  if (!success) {
    return reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no such file or directory, open '%@'", filepath], nil);
  }

  return resolve(nil);
}

RCT_EXPORT_METHOD(appendFile:(NSString *)filepath
                  b64:(NSString *)b64
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSError *error = nil;
  NSURL *url = [ReactNativeFs pathToUrl:filepath error:&error];
  if (error) return [[RNFSException fromError:error] reject:reject];

  BOOL allowed = [url startAccessingSecurityScopedResource];

  @try {
    NSData *data = [[NSData alloc] initWithBase64EncodedString:b64 options:NSDataBase64DecodingIgnoreUnknownCharacters];

    NSFileManager *fM = [NSFileManager defaultManager];

    if (![fM fileExistsAtPath:filepath])
    {
      BOOL success = [[NSFileManager defaultManager] createFileAtPath:filepath contents:data attributes:nil];

      if (!success) {
        return reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no such file or directory, open '%@'", filepath], nil);
      } else {
        return resolve(nil);
      }
    }

    @try {
      NSFileHandle *fH = [NSFileHandle fileHandleForUpdatingAtPath:filepath];

      [fH seekToEndOfFile];
      [fH writeData:data];

      return resolve(nil);
    } @catch (NSException *exception) {
      NSMutableDictionary * info = [NSMutableDictionary dictionary];
      [info setValue:exception.name forKey:@"ExceptionName"];
      [info setValue:exception.reason forKey:@"ExceptionReason"];
      [info setValue:exception.callStackReturnAddresses forKey:@"ExceptionCallStackReturnAddresses"];
      [info setValue:exception.callStackSymbols forKey:@"ExceptionCallStackSymbols"];
      [info setValue:exception.userInfo forKey:@"ExceptionUserInfo"];
      NSError *err = [NSError errorWithDomain:@"RNFS" code:0 userInfo:info];

      return [[RNFSException fromError:err] reject:reject];
    }
  }
  @finally {
    if (allowed) [url stopAccessingSecurityScopedResource];
  }
}

RCT_EXPORT_METHOD(write:(NSString *)filepath
                  b64:(NSString *)b64
                  position:(double)position
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSData *data = [[NSData alloc] initWithBase64EncodedString:b64 options:NSDataBase64DecodingIgnoreUnknownCharacters];

  NSFileManager *fM = [NSFileManager defaultManager];

  if (![fM fileExistsAtPath:filepath])
  {
    BOOL success = [[NSFileManager defaultManager] createFileAtPath:filepath contents:data attributes:nil];

    if (!success) {
      return reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no such file or directory, open '%@'", filepath], nil);
    } else {
      return resolve(nil);
    }
  }

  @try {
    NSFileHandle *fH = [NSFileHandle fileHandleForUpdatingAtPath:filepath];

    if (position >= 0) {
      [fH seekToFileOffset:position];
    } else {
      [fH seekToEndOfFile];
    }
    [fH writeData:data];

    return resolve(nil);
  } @catch (NSException *e) {
    return reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: error writing file: '%@'", filepath], nil);
  }
}

RCT_EXPORT_METHOD(unlink:(NSString*)filepath
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSError *error = nil;
  NSURL *url = [ReactNativeFs pathToUrl:filepath error:&error];
  if (error) return [[RNFSException fromError:error] reject:reject];

  BOOL allowed = [url startAccessingSecurityScopedResource];

  @try {
    NSFileManager *manager = [NSFileManager defaultManager];
    BOOL exists = [manager fileExistsAtPath:filepath isDirectory:NULL];

    if (!exists) {
      return reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no such file or directory, open '%@'", filepath], nil);
    }

    NSError *error = nil;
    BOOL success = [manager removeItemAtPath:filepath error:&error];

    if (!success) return [[RNFSException fromError:error] reject:reject];

    resolve(nil);
  }
  @finally {
    if (allowed) [url stopAccessingSecurityScopedResource];
  }
}

RCT_EXPORT_METHOD(mkdir:(NSString *)filepath
                  options:(JS::NativeReactNativeFs::MkdirOptionsT &)options
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSFileManager *manager = [NSFileManager defaultManager];

  NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];

  if (options.NSFileProtectionKey() != nil) {
      [attributes setValue:options.NSFileProtectionKey() forKey:@"NSFileProtectionKey"];
  }

  NSError *error = nil;
    BOOL success = [manager createDirectoryAtPath:filepath withIntermediateDirectories:YES attributes:attributes error:&error];

  if (!success) return [[RNFSException fromError:error] reject:reject];

  NSURL *url = [NSURL fileURLWithPath:filepath];

  if (options.NSURLIsExcludedFromBackupKey().has_value()) {
    NSNumber *value = [NSNumber numberWithBool:*options.NSURLIsExcludedFromBackupKey()];
    success = [url setResourceValue: value forKey: NSURLIsExcludedFromBackupKey error: &error];

    if (!success) return [[RNFSException fromError:error] reject:reject];
  }

  resolve(nil);
}

RCT_EXPORT_METHOD(readFile:(NSString *)filepath
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSError *error = nil;
  NSURL *url = [ReactNativeFs pathToUrl:filepath error:&error];
  if (error) return [[RNFSException fromError:error] reject:reject];

  BOOL allowed = [url startAccessingSecurityScopedResource];

  @try {
    BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:filepath];

    if (!fileExists) {
      return reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no such file or directory, open '%@'", filepath], nil);
    }

    NSError *error = nil;

    NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filepath error:&error];

    if (error) return [[RNFSException fromError:error] reject:reject];

    if ([attributes objectForKey:NSFileType] == NSFileTypeDirectory) {
      return reject(@"EISDIR", @"EISDIR: illegal operation on a directory, read", nil);
    }

    NSData *content = [[NSFileManager defaultManager] contentsAtPath:filepath];
    NSString *base64Content = [content base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];

    resolve(base64Content);
  }
  @finally {
    if (allowed) [url stopAccessingSecurityScopedResource];
  }
}

RCT_EXPORT_METHOD(read:(NSString *)path
                  length: (double)length
                  position: (double)position
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSError *error = nil;
  NSURL *url = [ReactNativeFs pathToUrl:path error:&error];
  if (error) return [[RNFSException fromError:error] reject:reject];

  BOOL allowed = [url startAccessingSecurityScopedResource];

  @try{
    BOOL fileExists = [url checkResourceIsReachableAndReturnError:&error];

    if (!fileExists) {
        return reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no such file or directory, open '%@'", path], nil);
    }

    NSError *error = nil;

    NSFileAttributeType type;
    BOOL success = [url getResourceValue:&type forKey:NSFileType error:&error];
    if (!success) return [[RNFSException fromError:error] reject:reject];

    if (type == NSFileTypeDirectory) {
        return reject(@"EISDIR", @"EISDIR: illegal operation on a directory, read", nil);
    }

    // Open the file handler.
    NSFileHandle *file = [NSFileHandle fileHandleForReadingFromURL:url error:&error];
    if (file == nil) {
        return reject(@"EISDIR", @"EISDIR: Could not open file for reading", error);
    }

    // Seek to the position if there is one.
    [file seekToFileOffset: (long)position];

    NSData *content;
    if ((long)length > 0) {
        content = [file readDataOfLength: (long)length];
    } else {
        content = [file readDataToEndOfFile];
    }

    NSString *base64Content = [content base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];

    resolve(base64Content);
  }
  @finally {
    if (allowed) [url stopAccessingSecurityScopedResource];
  }
}

RCT_EXPORT_METHOD(hash:(NSString *)filepath
                  algorithm:(NSString *)algorithm
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSError *error = nil;
  NSURL *url = [ReactNativeFs pathToUrl:filepath error:&error];
  if (error) return [[RNFSException fromError:error] reject:reject];

  BOOL allowed = [url startAccessingSecurityScopedResource];

  @try {
    BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:filepath];

    if (!fileExists) {
      return reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no such file or directory, open '%@'", filepath], nil);
    }

    NSError *error = nil;

    NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filepath error:&error];

    if (error) return [[RNFSException fromError:error] reject:reject];

    if ([attributes objectForKey:NSFileType] == NSFileTypeDirectory) {
      return reject(@"EISDIR", @"EISDIR: illegal operation on a directory, read", nil);
    }

    NSData *content = [[NSFileManager defaultManager] contentsAtPath:filepath];

    NSArray *keys = [NSArray arrayWithObjects:@"md5", @"sha1", @"sha224", @"sha256", @"sha384", @"sha512", nil];

    NSArray *digestLengths = [NSArray arrayWithObjects:
      @CC_MD5_DIGEST_LENGTH,
      @CC_SHA1_DIGEST_LENGTH,
      @CC_SHA224_DIGEST_LENGTH,
      @CC_SHA256_DIGEST_LENGTH,
      @CC_SHA384_DIGEST_LENGTH,
      @CC_SHA512_DIGEST_LENGTH,
      nil];

    NSDictionary *keysToDigestLengths = [NSDictionary dictionaryWithObjects:digestLengths forKeys:keys];

    int digestLength = [[keysToDigestLengths objectForKey:algorithm] intValue];

    if (!digestLength) {
      return reject(@"Error", [NSString stringWithFormat:@"Invalid hash algorithm '%@'", algorithm], nil);
    }

    unsigned char buffer[digestLength];

    if ([algorithm isEqualToString:@"md5"]) {
      CC_MD5(content.bytes, (CC_LONG)content.length, buffer);
    } else if ([algorithm isEqualToString:@"sha1"]) {
      CC_SHA1(content.bytes, (CC_LONG)content.length, buffer);
    } else if ([algorithm isEqualToString:@"sha224"]) {
      CC_SHA224(content.bytes, (CC_LONG)content.length, buffer);
    } else if ([algorithm isEqualToString:@"sha256"]) {
      CC_SHA256(content.bytes, (CC_LONG)content.length, buffer);
    } else if ([algorithm isEqualToString:@"sha384"]) {
      CC_SHA384(content.bytes, (CC_LONG)content.length, buffer);
    } else if ([algorithm isEqualToString:@"sha512"]) {
      CC_SHA512(content.bytes, (CC_LONG)content.length, buffer);
    } else {
      return reject(@"Error", [NSString stringWithFormat:@"Invalid hash algorithm '%@'", algorithm], nil);
    }

    NSMutableString *output = [NSMutableString stringWithCapacity:digestLength * 2];
    for(int i = 0; i < digestLength; i++)
      [output appendFormat:@"%02x",buffer[i]];

    resolve(output);
  }
  @finally {
    if (allowed) [url stopAccessingSecurityScopedResource];
  }
}

RCT_EXPORT_METHOD(moveFile:(NSString *)from
                  into:(NSString *)into
                  options:(JS::NativeReactNativeFs::FileOptionsT &)options
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSError *error = nil;
  NSURL *url = [ReactNativeFs pathToUrl:from error:&error];
  if (error) return [[RNFSException fromError:error] reject:reject];

  BOOL allowed = [url startAccessingSecurityScopedResource];

  @try {
    NSFileManager *manager = [NSFileManager defaultManager];

    NSError *error = nil;
    BOOL success = [manager moveItemAtPath:from toPath:into error:&error];

    if (!success) return [[RNFSException fromError:error] reject:reject];

    if (options.NSFileProtectionKey()) {
      NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];
      [attributes setValue:options.NSFileProtectionKey() forKey:@"NSFileProtectionKey"];
      BOOL updateSuccess = [manager setAttributes:attributes ofItemAtPath:into error:&error];

      if (!updateSuccess) return [[RNFSException fromError:error] reject:reject];
    }

    resolve(nil);
  }
  @finally {
    if (allowed) [url stopAccessingSecurityScopedResource];
  }
}


RCT_EXPORT_METHOD(copyFile:(NSString *)from
                  into:(NSString *)into
                  options:(JS::NativeReactNativeFs::FileOptionsT & )options
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSError *error = nil;
  NSURL *url = [ReactNativeFs pathToUrl:from error:&error];
  if (error) return [[RNFSException fromError:error] reject:reject];

  BOOL allowed = [url startAccessingSecurityScopedResource];

  @try {
    NSFileManager *manager = [NSFileManager defaultManager];

    BOOL success = [manager copyItemAtURL:url
                                    toURL:[NSURL fileURLWithPath:into]
                                    error:&error];

    if (!success) return [[RNFSException fromError:error] reject:reject];

    if (options.NSFileProtectionKey()) {
      NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];
      [attributes setValue:options.NSFileProtectionKey() forKey:@"NSFileProtectionKey"];
      success = [manager setAttributes:attributes ofItemAtPath:into error:&error];
      if (!success) return [[RNFSException fromError:error] reject:reject];
    }

    resolve(nil);
  }
  @finally {
    if (allowed) [url stopAccessingSecurityScopedResource];
  }
}

- (NSArray<NSString *> *)supportedEvents
{
    return @[@"UploadBegin",@"UploadProgress",@"DownloadBegin",@"DownloadProgress",@"DownloadResumable"];
}

RCT_EXPORT_METHOD(downloadFile:(JS::NativeReactNativeFs::NativeDownloadFileOptionsT &)options
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  RNFSDownloadParams* params = [RNFSDownloadParams alloc];

  NSNumber* jobId = [NSNumber numberWithDouble:options.jobId()];
  params.fromUrl = options.fromUrl();
  params.toFile = options.toFile();
  NSDictionary* headers = options.headers();
  params.headers = headers;
  params.background = options.background();
  params.discretionary = options.discretionary();
  params.cacheable = options.cacheable();
  NSNumber* progressInterval= [NSNumber numberWithDouble:options.progressInterval()];
  params.progressInterval = progressInterval;
  NSNumber* progressDivider = [NSNumber numberWithDouble:options.progressDivider()];
  params.progressDivider = progressDivider;
  NSNumber* readTimeout = [NSNumber numberWithDouble:options.readTimeout()];
  params.readTimeout = readTimeout;
  NSNumber* backgroundTimeout = [NSNumber numberWithDouble:options.backgroundTimeout()];
  params.backgroundTimeout = backgroundTimeout;
  bool hasBeginCallback = options.hasBeginCallback();
  bool hasProgressCallback = options.hasProgressCallback();
  bool hasResumableCallback = options.hasResumableCallback();

  __block BOOL callbackFired = NO;

  params.completeCallback = ^(NSNumber* statusCode, NSNumber* bytesWritten) {
    if (callbackFired) {
      return;
    }
    callbackFired = YES;

    NSMutableDictionary* result = [[NSMutableDictionary alloc] initWithDictionary: @{@"jobId": jobId}];
    if (statusCode) {
      [result setObject:statusCode forKey: @"statusCode"];
    }
    if (bytesWritten) {
      [result setObject:bytesWritten forKey: @"bytesWritten"];
    }
    return resolve(result);
  };

  params.errorCallback = ^(NSError* error) {
    if (callbackFired) {
      return;
    }
    callbackFired = YES;
    return [[RNFSException fromError:error] reject:reject];
  };

  if (hasBeginCallback) {
    params.beginCallback = ^(NSNumber* statusCode, NSNumber* contentLength, NSDictionary* headers) {
        if (self.bridge != nil)
            [self sendEventWithName:@"DownloadBegin" body:@{@"jobId": jobId,
                                                                                            @"statusCode": statusCode,
                                                                                            @"contentLength": contentLength,
                                                                                            @"headers": headers ?: [NSNull null]}];
    };
  }

  if (hasProgressCallback) {
    params.progressCallback = ^(NSNumber* contentLength, NSNumber* bytesWritten) {
        if (self.bridge != nil)
          [self sendEventWithName:@"DownloadProgress"
                                                  body:@{@"jobId": jobId,
                                                          @"contentLength": contentLength,
                                                          @"bytesWritten": bytesWritten}];
    };
  }

  if (hasResumableCallback) {
    params.resumableCallback = ^() {
        if (self.bridge != nil)
            [self sendEventWithName:@"DownloadResumable" body:@{@"jobId": jobId}];
    };
  }

  if (!self.downloaders) self.downloaders = [[NSMutableDictionary alloc] init];

  RNFSDownloader* downloader = [RNFSDownloader alloc];

  NSString *uuid = [downloader downloadFile:params];

  [self.downloaders setValue:downloader forKey:[jobId stringValue]];
    if (uuid) {
        if (!self.uuids) self.uuids = [[NSMutableDictionary alloc] init];
        [self.uuids setValue:uuid forKey:[jobId stringValue]];
    }
}

RCT_EXPORT_METHOD(stopDownload:(double)jobId)
{
  RNFSDownloader* downloader = [self.downloaders objectForKey:[[NSNumber numberWithDouble:jobId] stringValue]];

  if (downloader != nil) {
    [downloader stopDownload];
  }
}

RCT_EXPORT_METHOD(resumeDownload:(double)jobId)
{
    RNFSDownloader* downloader = [self.downloaders objectForKey:[[NSNumber numberWithDouble:jobId] stringValue]];

    if (downloader != nil) {
        [downloader resumeDownload];
    }
}

RCT_EXPORT_METHOD(isResumable:(double)jobId
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject
)
{
    RNFSDownloader* downloader = [self.downloaders objectForKey:[[NSNumber numberWithDouble:jobId] stringValue]];

    if (downloader != nil) {
        resolve([NSNumber numberWithBool:[downloader isResumable]]);
    } else {
        resolve([NSNumber numberWithBool:NO]);
    }
}

RCT_EXPORT_METHOD(completeHandlerIOS:(double)jobId)
{
    if (self.uuids) {
        NSNumber *jid = [NSNumber numberWithDouble:jobId];
        NSString *uuid = [self.uuids objectForKey:[jid stringValue]];
      [RNFSBackgroundDownloads complete:uuid];
    }
}

RCT_EXPORT_METHOD(uploadFiles:(JS::NativeReactNativeFs::NativeUploadFileOptionsT &)options
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  RNFSUploadParams* params = [RNFSUploadParams alloc];

  NSNumber* jobId = [NSNumber numberWithDouble:options.jobId()];
  params.toUrl = options.toUrl();
  params.files = options.files();

  if (options.binaryStreamOnly().has_value()) {
    params.binaryStreamOnly = options.binaryStreamOnly().value();
  }

  NSDictionary* headers = options.headers();
  NSDictionary* fields = options.fields();
  NSString* method = options.method();
  params.headers = headers;
  params.fields = fields;
  params.method = method;
  bool hasBeginCallback = options.hasBeginCallback();
  bool hasProgressCallback = options.hasProgressCallback();

  params.completeCallback = ^(NSString* body, NSURLResponse *resp) {
    [self.uploaders removeObjectForKey:[jobId stringValue]];

    NSMutableDictionary* result = [[NSMutableDictionary alloc] initWithDictionary: @{@"jobId": jobId,
                                                                                     @"body": body}];
    if ([resp isKindOfClass:[NSHTTPURLResponse class]]) {
      [result setValue:((NSHTTPURLResponse *)resp).allHeaderFields forKey:@"headers"];
      [result setValue:[NSNumber numberWithUnsignedInteger:((NSHTTPURLResponse *)resp).statusCode] forKey:@"statusCode"];
    }
    return resolve(result);
  };

  params.errorCallback = ^(NSError* error) {
    [self.uploaders removeObjectForKey:[jobId stringValue]];
    return [[RNFSException fromError:error] reject:reject];
  };

  if (hasBeginCallback) {
    params.beginCallback = ^() {
        if (self.bridge != nil)
          [self sendEventWithName:@"UploadBegin"
                                                  body:@{@"jobId": jobId}];
    };
  }

  if (hasProgressCallback) {
    params.progressCallback = ^(NSNumber* totalBytesExpectedToSend, NSNumber* totalBytesSent) {
        if (self.bridge != nil)
            [self sendEventWithName:@"UploadProgress"
                                                  body:@{@"jobId": jobId,
                                                          @"totalBytesExpectedToSend": totalBytesExpectedToSend,
                                                          @"totalBytesSent": totalBytesSent}];
    };
  }

  if (!self.uploaders) self.uploaders = [[NSMutableDictionary alloc] init];

  RNFSUploader* uploader = [RNFSUploader alloc];

  [uploader uploadFiles:params];

  [self.uploaders setValue:uploader forKey:[jobId stringValue]];
}

RCT_EXPORT_METHOD(stopUpload:(double)jobId)
{
  RNFSUploader* uploader = [self.uploaders objectForKey:[[NSNumber numberWithDouble:jobId] stringValue]];

  if (uploader != nil) {
    [uploader stopUpload];
  }
}

RCT_EXPORT_METHOD(pathForBundle:(NSString *)bundleNamed
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSString *path = [[NSBundle mainBundle].bundlePath stringByAppendingFormat:@"/%@.bundle", bundleNamed];
  NSBundle *bundle = [NSBundle bundleWithPath:path];

  if (!bundle) {
    bundle = [NSBundle bundleForClass:NSClassFromString(bundleNamed)];
    path = bundle.bundlePath;
  }

  if (!bundle.isLoaded) {
    [bundle load];
  }

  if (path) {
    resolve(path);
  } else {
    NSError *error = [NSError errorWithDomain:NSPOSIXErrorDomain
                                         code:NSFileNoSuchFileError
                                     userInfo:nil];

    return [[RNFSException fromError:error] reject:reject];
  }
}

RCT_EXPORT_METHOD(pathForGroup:(nonnull NSString *)groupId
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  NSURL *groupURL = [[NSFileManager defaultManager]containerURLForSecurityApplicationGroupIdentifier: groupId];

  if (!groupURL) {
    return reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no directory for group '%@' found", groupId], nil);
  } else {
    resolve([groupURL path]);
  }
}

RCT_EXPORT_METHOD(getFSInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
  unsigned long long totalSpace = 0;
  unsigned long long totalFreeSpace = 0;

  __autoreleasing NSError *error = nil;
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  NSDictionary *dictionary = [[NSFileManager defaultManager] attributesOfFileSystemForPath:[paths lastObject] error:&error];

  if (dictionary) {
    NSNumber *fileSystemSizeInBytes = [dictionary objectForKey: NSFileSystemSize];
    NSNumber *freeFileSystemSizeInBytes = [dictionary objectForKey:NSFileSystemFreeSize];
    totalSpace = [fileSystemSizeInBytes unsignedLongLongValue];
    totalFreeSpace = [freeFileSystemSizeInBytes unsignedLongLongValue];

    resolve(@{
      @"totalSpace": [NSNumber numberWithUnsignedLongLong:totalSpace],
      @"freeSpace": [NSNumber numberWithUnsignedLongLong:totalFreeSpace]
    });
  } else {
    [[RNFSException fromError:error] reject:reject];
  }
}

/**
 * iOS Only: copy images from the assets-library (camera-roll) to a specific path, asuming
 * JPEG-Images.
 *
 * Video-Support:
 *
 * One can use this method also to create a thumbNail from a video.
 * Currently it is impossible to specify a concrete position, the OS will decide wich
 * Thumbnail you'll get then.
 * To copy a video from assets-library and save it as a mp4-file, use the method
 * copyAssetsVideoIOS.
 *
 * It is also supported to scale the image via scale-factor (0.0-1.0) or with a specific
 * width and height. Also the resizeMode will be considered.
 */
RCT_EXPORT_METHOD(copyAssetsFileIOS: (NSString *) imageUri
                  destPath: (NSString *) destination
                  width: (double) width
                  height: (double) height
                  scale: (double) scale
                  compression: (double) compression
                  resizeMode: (NSString*) resizeMode
                  resolve: (RCTPromiseResolveBlock) resolve
                  reject: (RCTPromiseRejectBlock) reject)

{
// [PHAsset fetchAssetsWithALAssetURLs] is deprecated and not supported in Mac Catalyst
# if !TARGET_OS_UIKITFORMAC && !TARGET_OS_OSX
    CGSize size = CGSizeMake(width, height);

    NSURL* url = [NSURL URLWithString:imageUri];
    PHFetchResult *results = nil;
    if ([url.scheme isEqualToString:@"ph"]) {
        results = [PHAsset fetchAssetsWithLocalIdentifiers:@[[imageUri substringFromIndex: 5]] options:nil];
    } else {
        results = [PHAsset fetchAssetsWithALAssetURLs:@[url] options:nil];
    }

    if (results.count == 0) {
        NSString *errorText = [NSString stringWithFormat:@"Failed to fetch PHAsset with local identifier %@ with no error message.", imageUri];

        NSMutableDictionary* details = [NSMutableDictionary dictionary];
        [details setValue:errorText forKey:NSLocalizedDescriptionKey];
        NSError *error = [NSError errorWithDomain:@"RNFS" code:500 userInfo:details];
      return [[RNFSException fromError:error] reject:reject];
    }

    PHAsset *asset = [results firstObject];
    PHImageRequestOptions *imageOptions = [PHImageRequestOptions new];

    // Allow us to fetch images from iCloud
    imageOptions.networkAccessAllowed = YES;


    // Note: PhotoKit defaults to a deliveryMode of PHImageRequestOptionsDeliveryModeOpportunistic
    // which means it may call back multiple times - we probably don't want that
    imageOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;

    BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero);
    CGSize targetSize;
    if (useMaximumSize) {
        targetSize = PHImageManagerMaximumSize;
        imageOptions.resizeMode = PHImageRequestOptionsResizeModeNone;
    } else {
        targetSize = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(scale, scale));
        imageOptions.resizeMode = PHImageRequestOptionsResizeModeExact;
    }

    PHImageContentMode contentMode = PHImageContentModeAspectFill;
    if ([resizeMode compare:@"contain"] == NSOrderedSame) {
        contentMode = PHImageContentModeAspectFit;
    }

    // PHImageRequestID requestID =
    [[PHImageManager defaultManager] requestImageForAsset:asset
                                               targetSize:targetSize
                                              contentMode:contentMode
                                                  options:imageOptions
                                            resultHandler:^(UIImage *result, NSDictionary<NSString *, id> *info) {
        if (result) {

            NSData *imageData = UIImageJPEGRepresentation(result, compression );
            [imageData writeToFile:destination atomically:YES];
            resolve(destination);

        } else {
            NSMutableDictionary* details = [NSMutableDictionary dictionary];
            [details setValue:info[PHImageErrorKey] forKey:NSLocalizedDescriptionKey];
            NSError *error = [NSError errorWithDomain:@"RNFS" code:501 userInfo:details];
          [[RNFSException fromError:error] reject:reject];
        }
    }];
# else
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"copyAssetsFileIOS() is not supported for macOS"];
# endif
}

/**
 * iOS Only: copy videos from the assets-library (camera-roll) to a specific path as mp4-file.
 *
 * To create a thumbnail from the video, refer to copyAssetsFileIOS
 */
RCT_EXPORT_METHOD(copyAssetsVideoIOS: (NSString *) imageUri
                  destPath: (NSString *) destination
                  resolve: (RCTPromiseResolveBlock) resolve
                  reject: (RCTPromiseRejectBlock) reject)
{
// [PHAsset fetchAssetsWithALAssetURLs] is deprecated and not supported in Mac Catalyst
# if !TARGET_OS_UIKITFORMAC && !TARGET_OS_OSX
  NSURL* url = [NSURL URLWithString:imageUri];
  //unused?
  //__block NSURL* videoURL = [NSURL URLWithString:destination];
  __block NSError *error = nil;

  PHFetchResult *phAssetFetchResult = nil;
  if ([url.scheme isEqualToString:@"ph"]) {
      phAssetFetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[[imageUri substringFromIndex: 5]] options:nil];
  } else {
      phAssetFetchResult = [PHAsset fetchAssetsWithALAssetURLs:@[url] options:nil];
  }

  PHAsset *phAsset = [phAssetFetchResult firstObject];

  PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
  options.networkAccessAllowed = YES;
  options.version = PHVideoRequestOptionsVersionOriginal;
  options.deliveryMode = PHVideoRequestOptionsDeliveryModeAutomatic;

  dispatch_group_t group = dispatch_group_create();
  dispatch_group_enter(group);

  [[PHImageManager defaultManager] requestAVAssetForVideo:phAsset options:options resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {

    if ([asset isKindOfClass:[AVURLAsset class]]) {
      NSURL *url = [(AVURLAsset *)asset URL];
      NSLog(@"Final URL %@",url);
      BOOL writeResult = false;
        
      if (@available(iOS 9.0, *)) {
          NSURL *destinationUrl = [NSURL fileURLWithPath:destination relativeToURL:nil];
          writeResult = [[NSFileManager defaultManager] copyItemAtURL:url toURL:destinationUrl error:&error];
      } else {
          NSData *videoData = [NSData dataWithContentsOfURL:url];
          writeResult = [videoData writeToFile:destination options:NSDataWritingAtomic error:&error];
      }
        
      if(writeResult) {
        NSLog(@"video success");
      }
      else {
        NSLog(@"video failure");
      }
      dispatch_group_leave(group);
    }
  }];
  dispatch_group_wait(group,  DISPATCH_TIME_FOREVER);

  if (error) {
    NSLog(@"RNFS: %@", error);
    return [[RNFSException fromError:error] reject:reject];
  }

  return resolve(destination);
# else
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"copyAssetsVideoIOS() is not supported for macOS"];
# endif
}

RCT_EXPORT_METHOD(touch:(NSString*)filepath
                  options:(JS::NativeReactNativeFs::TouchOptions &) options
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
    NSFileManager *manager = [NSFileManager defaultManager];
    BOOL exists = [manager fileExistsAtPath:filepath isDirectory:NULL];

    if (!exists) {
        return reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no such file, open '%@'", filepath], nil);
    }

    NSMutableDictionary *attr = [NSMutableDictionary dictionary];

    if (options.mtime().has_value()) {
      // NOTE: Mind that the timestamp from the JS layer is in milliseconds,
      // and NSDate constructor expects seconds, thus the division by 1000.
      NSDate *mtime = [NSDate dateWithTimeIntervalSince1970:options.mtime().value() / 1000];
      [attr setValue:mtime forKey:NSFileModificationDate];
    }
    if (options.ctime().has_value()) {
      NSDate *ctime = [NSDate dateWithTimeIntervalSince1970:options.ctime().value() / 1000];
      [attr setValue:ctime forKey:NSFileCreationDate];
    }

    NSError *error = nil;
    BOOL success = [manager setAttributes:attr ofItemAtPath:filepath error:&error];

  if (!success) return [[RNFSException fromError:error] reject:reject];

    resolve(nil);
}

- (NSNumber *)dateToTimeIntervalNumber:(NSDate *)date
{
  return @([date timeIntervalSince1970]);
}

- (NSString *)getPathForDirectory:(NSSearchPathDirectory)directory
{
  NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
  return [paths firstObject];
}

- (NSDictionary *)constantsToExport
{
  return @{
           @"MainBundlePath": [[NSBundle mainBundle] bundlePath],
           @"CachesDirectoryPath": [self getPathForDirectory:NSCachesDirectory],
           @"DocumentDirectoryPath": [self getPathForDirectory:NSDocumentDirectory],
           @"ExternalDirectoryPath": [NSNull null],
           @"ExternalStorageDirectoryPath": [NSNull null],
           @"TemporaryDirectoryPath": NSTemporaryDirectory(),
           @"LibraryDirectoryPath": [self getPathForDirectory:NSLibraryDirectory],
           @"FileTypeRegular": NSFileTypeRegular,
           @"FileTypeDirectory": NSFileTypeDirectory,
           @"FileProtectionComplete": NSFileProtectionComplete,
           @"FileProtectionCompleteUnlessOpen": NSFileProtectionCompleteUnlessOpen,
           @"FileProtectionCompleteUntilFirstUserAuthentication": NSFileProtectionCompleteUntilFirstUserAuthentication,
           @"FileProtectionNone": NSFileProtectionNone
          };
}

- (NSDictionary *) getConstants {
  return [self constantsToExport];
}

- (void)copyFileAssets:(NSString *)from into:(NSString *)into resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { 
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"copyFileAssets()"];
}

- (void)copyFileRes:(NSString *)from into:(NSString *)into resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { 
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"copyFileRes()"];
}

- (void)copyFolder:(NSString *)from into:(NSString *)into resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { 
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"copyFolder()"];
}

- (void)existsAssets:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"existsAssets()"];
}

- (void)existsRes:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { 
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"existsRes()"];
}

- (void)getAllExternalFilesDirs:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { 
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"getAllExternalFilesDirs()"];
}

- (void)readFileAssets:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { 
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"readFileAssets()"];
}

- (void)readFileRes:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { 
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"readFileRes()"];
}

- (void)scanFile:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { 
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"scanFile()"];
}

- (void)scanFile:(NSString *)filepath readable:(BOOL)readable ownerOnly:(BOOL)ownerOnly resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"scanFile()"];
}

- (void)readDirAssets:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"readDirAssets()"];
}


- (void)setReadable:(NSString *)filepath readable:(BOOL)readable ownerOnly:(BOOL)ownerOnly resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  [[RNFSException NOT_IMPLEMENTED] reject:reject details:@"setReadable()"];
}

- (void)documentPicker:(UIDocumentPickerViewController *)picker
didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
{
  NSValue *id = [NSValue valueWithPointer:(void*)picker];
  NSArray *promise = pendingPickFilePromises[id];
  if (promise != nil) {
    [pendingPickFilePromises removeObjectForKey:id];
    RCTPromiseResolveBlock resolve = promise[0];
    NSMutableArray *res = [NSMutableArray arrayWithCapacity:urls.count];
    for (int i = 0; i < urls.count; ++i) {
      NSURL *url = urls[i];

      BOOL allowed = [url startAccessingSecurityScopedResource];

      NSURLBookmarkCreationOptions options = 0;

#     if TARGET_OS_MACCATALYST
      options = NSURLBookmarkCreationWithSecurityScope;
#     endif // TARGET_OS_MACCATALYST

      NSError *error = nil;
      NSData *data = [url bookmarkDataWithOptions:options
                   includingResourceValuesForKeys:nil
                                    relativeToURL:nil
                                            error:&error];

      if (allowed) [url stopAccessingSecurityScopedResource];
      if (error) return [[RNFSException fromError:error] reject:promise[1]];

      NSString *bookmark = [data base64EncodedStringWithOptions:0];
      bookmark = [NSString stringWithFormat:@"%@%@", BOOKMARK, bookmark];
      [res addObject:bookmark];
    }
    resolve(res);
  }
  // TODO: Should crash here, as it is a fatal error.
}

- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)picker
{
  NSValue *id = [NSValue valueWithPointer:(void*)picker];
  NSArray *promise = pendingPickFilePromises[id];
  if (promise != nil) {
    [pendingPickFilePromises removeObjectForKey:id];
    RCTPromiseResolveBlock resolve = promise[0];
    resolve(@[]);
  }
  // TODO: Should crash here, as it is a fatal error.
}

RCT_EXPORT_METHOD(
#ifdef RCT_NEW_ARCH_ENABLED
                  pickFile:(JS::NativeReactNativeFs::PickFileOptionsT &)options
#else
                  pickFile:(NSDictionary*)options
#endif
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject
) {
  // NOTE: We must copy options into a local variable, so that (especially with
  // the new, bridgeless architecture) it is correctly detained by the async
  // block below, not crushing the app.
  // See: https://github.com/birdofpreyru/react-native-fs/issues/44
# ifdef RCT_NEW_ARCH_ENABLED
  JS::NativeReactNativeFs::PickFileOptionsT o = options;
# else
  NSDictionary* o = options;
# endif
  dispatch_async(dispatch_get_main_queue(), ^() {
    @try {
      UIDocumentPickerViewController *picker;

#   ifdef RCT_NEW_ARCH_ENABLED
      facebook::react::LazyVector<NSString*> mimeTypes = o.mimeTypes();
      int numMimeTypes = mimeTypes.size();
#   else
      NSArray<NSString*>* mimeTypes = o[@"mimeTypes"];
      int numMimeTypes = mimeTypes.count;
#   endif

      if (@available(iOS 14.0, *)) {
        NSMutableArray<UTType*> *types = [NSMutableArray arrayWithCapacity:numMimeTypes];
        for (int i = 0; i < numMimeTypes; ++i) {
          NSString *mime = mimeTypes[i];
          UTType *type;
          if ([mime isEqual:@"*/*"]) type = UTTypeItem;
          else type = [UTType typeWithMIMEType:mime];
          [types addObject:type];
        }
        picker = [[UIDocumentPickerViewController alloc]
                  initForOpeningContentTypes:types];
      } else {
        // TODO: There is no UTType object on iOS < 14.0, just UTType strings that
        // can be found here:
        // https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html#//apple_ref/doc/uid/TP40009259-SW1
        // though I have not found a function for converting MIME types into UTTypes
        // on iOS < 14.0. If the only option is to implement this conversion ourselves,
        // at least for now we can leave without iOS < 14.0 support (RN presumably
        // supports iOS 13.4 and above, but according to Wiki iOS 13.x are considered
        // obsolete by now, and presumably all devices running iOS 13.x originally
        // have been upgraded to iOS 14+ by now).
        [[RNFSException NOT_IMPLEMENTED]
         reject:reject details:@"pickFile() is implemented for iOS 14+ only"];
        return;
      }

      UIViewController *root = RCTPresentedViewController();

      // Note: This is needed because the module overall runs on a dedicated queue
      // (see its methodQueue() method below), while interaction with UI should be
      // done on the main thread queue.

      picker.delegate = self;
      [pendingPickFilePromises setObject:@[resolve, reject]
                                  forKey:[NSValue valueWithPointer:(void*)picker]];
      [root presentViewController:picker animated:YES completion:nil];
    }
    @catch (NSException *e) {
      [[RNFSException fromException:e] reject:reject];
    }
  });
}

/**
 * Given a path string converts it into NSURL object.
 */
+ (NSURL*) pathToUrl:(NSString*)path error:(NSError**)error
{
  NSURL *res = nil;

  if ([path hasPrefix:BOOKMARK]) {
    // If path is a "Bookmark URL".
    // See BOOKMARK description for details.
    path = [path substringFromIndex:BOOKMARK.length];
    NSData *data = [[NSData alloc] initWithBase64EncodedString:path options:0];

    NSURLBookmarkResolutionOptions options = 0;

#   if TARGET_OS_MACCATALYST
    options = NSURLBookmarkResolutionWithSecurityScope;
#   endif // TARGET_OS_MACCATALYST

    BOOL isStale = NO;
    res = [NSURL URLByResolvingBookmarkData:data
                                    options:options
                              relativeToURL:nil
                        bookmarkDataIsStale:&isStale
                                      error:error];
  } else {
    // If path is just a regular path.
    res = [NSURL fileURLWithPath:path];
  }

  return res;
}

- (dispatch_queue_t)methodQueue
{
  return dispatch_queue_create("pe.lum.newrnfs", DISPATCH_QUEUE_SERIAL);
}

+ (BOOL)requiresMainQueueSetup
{
  return NO;
}

// Don't compile this code when we build for the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params
{
    return std::make_shared<facebook::react::NativeReactNativeFsSpecJSI>(params);
}
#endif

@end