#import <React/RCTConvert.h>
#import <React/RCTLog.h>
#import "RNFileCache.h"
#import "RNFileCacheDownloader.h"
#import "RNFileCacheTagManager.h"
#import "RNFileCacheErrorCode.h"

static NSString *RNFileCacheErrorDomain = @"react-native-filecache";

@implementation RNFileCache {
  NSMutableDictionary *tagMap;
  NSMutableDictionary *uriSemaphoreMap;
  RNFileCacheTagManager *tagManager;
}

+ (NSNumber *)getFileSize:(NSString *)path
{
  NSError *attributesError;
  NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&attributesError];
  if(attributesError != nil) {
    RCTLogError(@"failed fetch fileAttributes:%@", [attributesError localizedDescription]);
    return [NSNumber numberWithLongLong:-1];
  }
  return  [fileAttributes objectForKey:NSFileSize];
}

- (void)sendEvent:(NSString *)type
             data:(NSDictionary *)data {
  NSMutableDictionary *map = [NSMutableDictionary dictionaryWithDictionary: data];
  [map setValue:type forKey:@"type"];
  [self sendEventWithName:@"RNFileCacheEvent" body:map];
}

- (NSDictionary *)constantsToExport
{
  return @{
           @"ERR_HTTP": [NSNumber numberWithLong:RNFileCacheErrorHTTPError],
           @"ERR_NOTENOUGHFREEDISKSPACE": [NSNumber numberWithLong:RNFileCacheErrorNotEnoughFreeDiskSpace],
           @"ERR_FILE": [NSNumber numberWithLong:RNFileCacheErrorFileError],
           @"ERR_INVALIDARGUMENT": [NSNumber numberWithLong:RNFileCacheErrorInvalidArgument],
           @"ERR_UNKNOWN": [NSNumber numberWithLong:RNFileCacheErrorUnknown]
           };
}

- (NSArray<NSString *> *)supportedEvents
{
  return @[@"RNFileCacheEvent"];
}

- (void)createDir:(NSString *)path
{
  if([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:nil] == NO) {
    [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
  }
}

- (NSString *)cacheRoot
{
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  NSString *cacheDirectory = [[paths objectAtIndex:0] stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]];
  NSString *ret = [cacheDirectory stringByAppendingPathComponent: @"cache"];
  [self createDir:ret];
  return ret;
}

- (NSString *)getPath:(NSString *)hash
{
  return [[self cacheRoot] stringByAppendingPathComponent: hash];
}

- (NSDictionary *)cache:(id)json tag:(NSString *)tag
{
  NSString *uri = (NSString *)[json objectForKey:@"uri"];
  if(uri == nil) {
    @throw [NSException
            exceptionWithName:@"InvalidArguement"
            reason:@"uri not found."
            userInfo:@{
                       @"code": [NSNumber numberWithLong:RNFileCacheErrorInvalidArgument]
                       }];
  }
  NSString *hash = [[NSNumber numberWithInteger:[uri hash]] stringValue];
  NSString *path = [self getPath:hash];
  BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:nil];
  if(!exists) {
    [RNFileCacheDownloader download:[NSURL URLWithString:uri] to:path];
  }
  return @{
           @"path": path,
           @"uri": uri,
           @"size": [RNFileCache getFileSize:path],
           @"fromcache": [NSNumber numberWithBool:exists]
           };
}

- (void)cancelCache:(id)json
{
  NSString *uri = (NSString *)[json objectForKey:@"uri"];
  if(uri == nil) {
    @throw [NSException
            exceptionWithName:@"InvalidArguement"
            reason:@"uri not found."
            userInfo:@{
                       @"code": [NSNumber numberWithLong:RNFileCacheErrorInvalidArgument]
                       }];
  }
  [RNFileCacheDownloader cancelDownload:[NSURL URLWithString:uri]];
}

- (void)restore
{
  if(tagMap != nil) return;
  tagManager = [[RNFileCacheTagManager alloc] initWithPath:[self getPath:@"cache.plist"]];
  @synchronized(tagMap) {
    tagMap = [tagManager restore];
  }
}

- (void)save
{
  @synchronized(tagMap) {
    [tagManager save:tagMap];
  }
}

- (void)setTag:(NSString *)tag forHash:(NSString *)hash
{
  [self restore];
  @synchronized(tagMap) {
    [tagMap setObject:tag forKey:hash];
  }
  [self save];
}

- (void)removeTagForHash:(NSString *)hash
{
  [self restore];
  @synchronized(tagMap) {
    [tagMap removeObjectForKey:hash];
  }
  [self save];
}

- (void)clearAllTag
{
  [self restore];
  @synchronized(tagMap) {
    [tagMap removeAllObjects];
  }
  [self save];
}

- (void)clearTag:(NSString *)tag
{
  [self restore];
  @synchronized(tagMap) {
    NSEnumerator *it = [tagMap keyEnumerator];
    NSString *hash;
    NSMutableDictionary *newTagMap = [NSMutableDictionary dictionary];
    while(hash = [it nextObject]) {
      NSString *tagObj = [tagMap objectForKey:hash];
      if([tag isEqual:tagObj] == NO) {
        [newTagMap setObject:tagObj forKey:hash];
      }
    }
    tagMap = newTagMap;
  }
  [self save];
}

- (NSArray *)getHashListTagged:(NSString *)tag
{
  NSMutableArray *ret = [NSMutableArray array];
  [self restore];
  @synchronized(tagMap) {
    NSEnumerator *it = [tagMap keyEnumerator];
    NSString *hash;
    while(hash = [it nextObject]) {
      if([tag isEqual:[tagMap objectForKey:hash]]) [ret addObject:hash];
    }
  }
  return ret;
}

- (BOOL)isTagged:(NSString *)filename
{
  [self restore];
  BOOL isTagged = NO;
  @synchronized(tagMap) {
    isTagged = [tagMap objectForKey:filename] != nil;
  }
  return isTagged;
}

RCT_EXPORT_MODULE();

RCT_REMAP_METHOD(fetch,
                 fetch:(nonnull id)json
                 tag:(nonnull NSString *)tag
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @try {
      NSDictionary *cached = [self cache:json tag:tag];
      NSString *path = [cached objectForKey:@"path"];
      NSString *fromcache = [cached objectForKey:@"fromcache"];
      NSString *uri = (NSString *)[json objectForKey:@"uri"];
      NSString *hash = [[NSNumber numberWithInteger:[uri hash]] stringValue];
      [self setTag:tag forHash:hash];
      NSData *data = [NSData dataWithContentsOfFile:path];
      resolve([data base64EncodedStringWithOptions:0]);
    }
    @catch (NSException *exception) {
      NSError *error = [[NSError alloc]
                        initWithDomain: RNFileCacheErrorDomain
                        code:[[exception.userInfo objectForKey:@"code"] longValue]
                        userInfo:exception.userInfo];
      reject(@"fetch", @"failed caching", error);
    }
  });
}

RCT_REMAP_METHOD(cache,
                 cache:(nonnull id)json
                 tag:(nonnull NSString *)tag
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @try {
      NSDictionary *cached = [self cache:json tag:tag];
      NSString *path = [cached objectForKey:@"path"];
      NSString *fromcache = [cached objectForKey:@"fromcache"];
      NSString *uri = (NSString *)[json objectForKey:@"uri"];
      NSString *hash = [[NSNumber numberWithInteger:[uri hash]] stringValue];
      [self setTag:tag forHash:hash];
      resolve(cached);
    }
    @catch (NSException *exception) {
      NSError *error = [[NSError alloc]
                        initWithDomain: RNFileCacheErrorDomain
                        code: [[exception.userInfo objectForKey:@"code"] longValue]
                        userInfo: exception.userInfo];
      reject(@"cache", @"failed caching", error);
    }
  });
}

RCT_REMAP_METHOD(cancelCache,
                 cancelCache:(nonnull id)json
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @try {
      [self cancelCache:json];
      resolve(@{});
    }
    @catch (NSException *exception) {
      NSError *error = [[NSError alloc]
                        initWithDomain: RNFileCacheErrorDomain
                        code: [[exception.userInfo objectForKey:@"code"] longValue]
                        userInfo: exception.userInfo];
      reject(@"cache", @"failed cancelCaching", error);
    }
  });
}

RCT_REMAP_METHOD(clear,
                 clear:(nonnull id)json
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @try {
      NSString *uri = (NSString *)[json objectForKey:@"uri"];
      NSString *hash = [[NSNumber numberWithInteger:[uri hash]] stringValue];
      NSString *path = [self getPath:hash];
      if([[NSFileManager defaultManager] isDeletableFileAtPath:path]) {
        NSError *error;
        [[NSFileManager defaultManager] removeItemAtPath:path error:&error];
        if(error != nil) {
          @throw [NSException
                  exceptionWithName:@"failed removing item"
                  reason:error.localizedDescription
                  userInfo:@{
                             @"code": [NSNumber numberWithLong:RNFileCacheErrorFileError]
                             }];
        }
      }
      [self removeTagForHash:hash];
      resolve(@{});
    }
    @catch (NSException *exception) {
      NSError *error = [[NSError alloc]
                        initWithDomain: RNFileCacheErrorDomain
                        code: RNFileCacheErrorUnknown
                        userInfo: @{
                                    @"name": exception.name,
                                    @"reason": exception.reason
                                    }];
      reject(@"clear", @"failed removing file", error);
    }
  });
}

RCT_REMAP_METHOD(clearAll,
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @try {
      NSString *path = [self cacheRoot];
      if ([[NSFileManager defaultManager] isDeletableFileAtPath:path]) {
        NSError *error;
        [[NSFileManager defaultManager] removeItemAtPath:path error:&error];
        if(error != nil) {
          @throw [NSException
                  exceptionWithName:@"failed removing item"
                  reason:error.localizedDescription
                  userInfo:@{
                             @"code": [NSNumber numberWithLong:RNFileCacheErrorFileError]
                             }];
        }
      }
      [self clearAllTag];
      resolve(@{});
    }
    @catch (NSException *exception) {
      NSError *error = [[NSError alloc]
                        initWithDomain: RNFileCacheErrorDomain
                        code: RNFileCacheErrorUnknown
                        userInfo: @{
                                    @"name": exception.name,
                                    @"reason": exception.reason
                                    }];
      reject(@"clearAll", @"failed removing file", error);
    }
  });
}

RCT_REMAP_METHOD(clearTagged,
                 clearTagged:(nonnull NSString *)tag
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @try {
      NSArray* hashes = [self getHashListTagged:tag];
      for(int i=0; i<[hashes count]; i++) {
        NSString *hash = [hashes objectAtIndex:i];
        NSError *error;
        [[NSFileManager defaultManager] removeItemAtPath:[self getPath:hash] error:&error];
        if(error != nil) {
          @throw [NSException
                  exceptionWithName:@"failed removing item"
                  reason:error.localizedDescription
                  userInfo:@{
                             @"code": [NSNumber numberWithLong:RNFileCacheErrorFileError]
                             }];
        }
      }
      [self clearTag:tag];
      resolve(@{});
    }
    @catch (NSException *exception) {
      NSError *error = [[NSError alloc]
                        initWithDomain: RNFileCacheErrorDomain
                        code: RNFileCacheErrorUnknown
                        userInfo: @{
                                    @"name": exception.name,
                                    @"reason": exception.reason
                                    }];
      reject(@"clearAll", @"failed removing file", error);
    }
  });
}

RCT_REMAP_METHOD(fetchCachedSizeTagged,
                 fetchCachedSizeTagged:(nonnull NSString *)tag
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @try {
      long size = 0;
      NSArray *properties = [NSArray arrayWithObjects: NSURLLocalizedNameKey,
                             NSURLCreationDateKey,
                             NSURLLocalizedTypeDescriptionKey,
                             nil];
      NSError *error;
      NSArray *files = [[NSFileManager defaultManager]
                        contentsOfDirectoryAtURL:[NSURL fileURLWithPath:[self cacheRoot]]
                        includingPropertiesForKeys:properties
                        options:(NSDirectoryEnumerationSkipsHiddenFiles)
                        error:&error];
      if(error != nil) {
        @throw [NSException
                exceptionWithName:@"failed listing items"
                reason:error.localizedDescription
                userInfo:@{
                           @"code": [NSNumber numberWithLong:RNFileCacheErrorFileError]
                           }];
      }
      for (NSURL *item in files) {
        NSString *hash = [[item absoluteString] lastPathComponent];
        BOOL isTagged = [self isTagged:hash];
        if(isTagged) {
          NSError *error;
          size += [[[[NSFileManager defaultManager]
                     attributesOfItemAtPath:[item path] error:&error]
                    objectForKey:NSFileSize] unsignedIntegerValue];
          if(error != nil) {
            @throw [NSException
                    exceptionWithName:@"failed fetching item attribute"
                    reason:error.localizedDescription
                    userInfo:@{
                               @"code": [NSNumber numberWithLong:RNFileCacheErrorFileError]
                               }];
          }
        }
      }
      resolve(@{ @"size": [NSNumber numberWithLong: size] });
    }
    @catch (NSException *exception) {
      NSError *error = [[NSError alloc]
                        initWithDomain: RNFileCacheErrorDomain
                        code: RNFileCacheErrorUnknown
                        userInfo: @{
                                    @"name": exception.name,
                                    @"reason": exception.reason
                                    }];
      reject(@"fetchCachedSizeTagged", @"failed fetching size", error);
    }
  });
}

RCT_REMAP_METHOD(fetchCachedSize,
                 fetchCachedSize:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @try {
      long size = 0;
      NSArray *properties = [NSArray arrayWithObjects: NSURLLocalizedNameKey,
                             NSURLCreationDateKey,
                             NSURLLocalizedTypeDescriptionKey,
                             nil];
      NSError *error;
      NSArray *files = [[NSFileManager defaultManager]
                        contentsOfDirectoryAtURL:[NSURL fileURLWithPath:[self cacheRoot]]
                        includingPropertiesForKeys:properties
                        options:(NSDirectoryEnumerationSkipsHiddenFiles)
                        error:&error];
      if(error != nil) {
        @throw [NSException
                exceptionWithName:@"failed listing items"
                reason:error.localizedDescription
                userInfo:@{
                           @"code": [NSNumber numberWithLong:RNFileCacheErrorFileError]
                           }];
      }
      for (NSURL *item in files) {
        NSError *error;
        size += [[[[NSFileManager defaultManager]
                   attributesOfItemAtPath:[item path] error:&error]
                  objectForKey:NSFileSize] unsignedIntegerValue];
        if(error != nil) {
          @throw [NSException
                  exceptionWithName:@"failed fetching item attribute"
                  reason:error.localizedDescription
                  userInfo:@{
                             @"code": [NSNumber numberWithLong:RNFileCacheErrorFileError]
                             }];
        }
      }
      resolve(@{ @"size": [NSNumber numberWithLong: size] });
    }
    @catch (NSException *exception) {
      NSError *error = [[NSError alloc]
                        initWithDomain: RNFileCacheErrorDomain
                        code: RNFileCacheErrorUnknown
                        userInfo: @{
                                    @"name": exception.name,
                                    @"reason": exception.reason
                                    }];
      reject(@"fetchCachedSize", @"failed fetching size", error);
    }
  });
}

RCT_REMAP_METHOD(setTimeoutIntervalForRequest,
                 timeoutIntervalForRequest:(int)timeoutIntervalForRequest)
{
  [RNFileCacheDownloader setTimeoutIntervalForRequest:timeoutIntervalForRequest];
}

RCT_REMAP_METHOD(setTimeoutIntervalForResource,
                 timeoutIntervalForResource:(int)timeoutIntervalForResource)
{
  [RNFileCacheDownloader setTimeoutIntervalForResource:timeoutIntervalForResource];
}

RCT_REMAP_METHOD(getBPSWithin,
                 from:(nonnull NSNumber *)from
                 to:(nonnull NSNumber *)to
                 fetchCachedSize:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  double bps = [RNFileCacheDownloader bpsFrom:[from doubleValue] to:[to doubleValue]];
  resolve(@{ @"bps": [NSNumber numberWithDouble: bps] });
}

@end


