#import <Foundation/Foundation.h>
#import "RNFileCacheDownloaderTask.h"
#import "RNFileCacheErrorCode.h"
#import <React/RCTLog.h>

static NSMutableDictionary *taskMap;
static NSMutableDictionary *urlMap;

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

@implementation RNFileCacheDownloaderTask {
  NSURLSessionDataTask *dataTask;
  dispatch_semaphore_t sem;
  NSString *path;
  NSException *exception;
  NSString *workPath;
  NSFileHandle *workHandle;
}

+ (NSNumber *)freeDiskSpace
{
    NSDictionary *fattributes = [[NSFileManager defaultManager] attributesOfFileSystemForPath:NSHomeDirectory() error:nil];
    return [fattributes objectForKey:NSFileSystemFreeSize];
}

- (id)initWithTask:(NSURLSessionDataTask *)task path:(NSString *)_path
{
  self = [super init];
  if(taskMap == nil) {
    taskMap = [[NSMutableDictionary alloc] init];
  }
  if(self != nil) {
    dataTask = task;
    path = _path;
    sem = dispatch_semaphore_create(0);
    @synchronized(taskMap) {
      [taskMap setObject:self forKey:[NSNumber numberWithUnsignedInteger:[task taskIdentifier]]];
    }
    @synchronized(urlMap) {
      [urlMap setObject:self forKey:task.currentRequest.URL];
    }
  }
  return self;
}

- (void)didReceiveResponse:(NSURLResponse *)response 
         completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

  // check statusCode
  if(httpResponse.statusCode >= 400) {
    exception = [NSException
      exceptionWithName:RNFileCacheErrorDomain
                 reason:@"Erroneous HTTP status code"
               userInfo:@{
                @"code": [NSNumber numberWithInteger:RNFileCacheErrorHTTPError],
          @"statusCode": [NSNumber numberWithLong: httpResponse.statusCode],
   @"statusDescription": [NSHTTPURLResponse localizedStringForStatusCode:httpResponse.statusCode]
               }];
  }

  // check diskspace
  uint64_t expectedDataLength = [httpResponse expectedContentLength];
  long long expected = @(expectedDataLength).longLongValue;
  if([RNFileCacheDownloaderTask freeDiskSpace].longLongValue < expected && expected != -1) {
    exception = [NSException
      exceptionWithName:RNFileCacheErrorDomain
                 reason:@"Not enough free disk space"
               userInfo:@{
                @"code": [NSNumber numberWithInteger: RNFileCacheErrorNotEnoughFreeDiskSpace]
               }];
  }

  // open temp file
  workPath = [NSTemporaryDirectory() stringByAppendingString:[[NSProcessInfo processInfo] globallyUniqueString]];
  BOOL created = [[NSFileManager defaultManager] createFileAtPath:workPath contents:nil attributes:nil];
  if(!created) {
    exception = [NSException
      exceptionWithName:RNFileCacheErrorDomain
                 reason:@"Failed creaate temp file"
               userInfo:@{
                @"code": [NSNumber numberWithInteger: RNFileCacheErrorFileError]
               }];
  } else {
    workHandle = [NSFileHandle fileHandleForWritingAtPath:workPath];
  }

  if(!exception) {
    completionHandler(NSURLSessionResponseAllow);
  } else {
    completionHandler(NSURLSessionResponseCancel);
  }
}

- (void)didReceiveData:(NSData *)data
{
  @try {
    [workHandle writeData:data];
  }
  @catch(NSException *e) {
    exception = [NSException
      exceptionWithName:RNFileCacheErrorDomain
                 reason:@"Failed writeData"
               userInfo:@{
                @"code": [NSNumber numberWithInteger: RNFileCacheErrorFileError],
              @"reason": e.reason
               }];
    [dataTask cancel];
  }
}

- (NSString *)path
{
  return path;
}

- (void)download
{
  [dataTask resume];
  dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
  if(exception != nil) {
    @throw exception;
  }
}

- (void)cancel
{
  [dataTask cancel];
}

- (void)didCompleteWithError:(NSError *)error
{
  [workHandle closeFile];

  if(error && exception == nil) {
    exception = [NSException
      exceptionWithName:RNFileCacheErrorDomain
                 reason:[error localizedDescription]
               userInfo:[error userInfo]];
  }

  // copy tmp to path
  if(!exception) {
    NSError *error;
    [[NSFileManager defaultManager] copyItemAtPath:workPath toPath:path error:&error];
    if(error != nil) {
      exception = [NSException
        exceptionWithName:RNFileCacheErrorDomain
                   reason:[error localizedDescription]
                 userInfo:[error userInfo]];
    }
  }
  // remove tmp file
  {
    NSError *error;
    [[NSFileManager defaultManager] removeItemAtPath:workPath error:&error];
    if(error != nil) {
      // not fatal
      RCTLogInfo(@"failed remove tempFile:%@(%@)", workPath, [error localizedDescription]);
    }
  }

  dispatch_semaphore_signal(sem);

  @synchronized(urlMap) {
    [urlMap removeObjectForKey:dataTask.currentRequest.URL];
  }
  @synchronized(taskMap) {
    [taskMap removeObjectForKey:[NSNumber numberWithUnsignedInteger:[dataTask taskIdentifier]]];
  }
}

+ (RNFileCacheDownloaderTask *)task:(NSUInteger)taskIdentifier
{
  @synchronized(taskMap) {
    return [taskMap objectForKey:[NSNumber numberWithUnsignedInteger:taskIdentifier]];
  }
}

+ (RNFileCacheDownloaderTask *)taskForURL:(NSURL *)url
{
  @synchronized(urlMap) {
    return [urlMap objectForKey:url];
  }
}

@end
