#import "HTTPAsyncFileResponse.h"
#import "HTTPConnection.h"
#import "HTTPLogging.h"

#import <unistd.h>
#import <fcntl.h>

#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif

/**
 * Does ARC support support GCD objects?
 * It does if the minimum deployment target is iOS 6+ or Mac OS X 8+
**/
#if TARGET_OS_IPHONE

  // Compiling for iOS

  #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 // iOS 6.0 or later
    #define NEEDS_DISPATCH_RETAIN_RELEASE 0
  #else                                         // iOS 5.X or earlier
    #define NEEDS_DISPATCH_RETAIN_RELEASE 1
  #endif

#else

  // Compiling for Mac OS X

  #if MAC_OS_X_VERSION_MIN_REQUIRED >= 1080     // Mac OS X 10.8 or later
    #define NEEDS_DISPATCH_RETAIN_RELEASE 0
  #else
    #define NEEDS_DISPATCH_RETAIN_RELEASE 1     // Mac OS X 10.7 or earlier
  #endif

#endif

// Log levels : off, error, warn, info, verbose
// Other flags: trace
static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE;

#define NULL_FD  -1

/**
 * Architecure overview:
 * 
 * HTTPConnection will invoke our readDataOfLength: method to fetch data.
 * We will return nil, and then proceed to read the data via our readSource on our readQueue.
 * Once the requested amount of data has been read, we then pause our readSource,
 * and inform the connection of the available data.
 * 
 * While our read is in progress, we don't have to worry about the connection calling any other methods,
 * except the connectionDidClose method, which would be invoked if the remote end closed the socket connection.
 * To safely handle this, we do a synchronous dispatch on the readQueue,
 * and nilify the connection as well as cancel our readSource.
 * 
 * In order to minimize resource consumption during a HEAD request,
 * we don't open the file until we have to (until the connection starts requesting data).
**/

@implementation HTTPAsyncFileResponse

- (id)initWithFilePath:(NSString *)fpath forConnection:(HTTPConnection *)parent
{
	if ((self = [super init]))
	{
		HTTPLogTrace();
		
		connection = parent; // Parents retain children, children do NOT retain parents
		
		fileFD = NULL_FD;
		filePath = [fpath copy];
		if (filePath == nil)
		{
			HTTPLogWarn(@"%@: Init failed - Nil filePath", THIS_FILE);
			
			return nil;
		}
		
		NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL];
		if (fileAttributes == nil)
		{
			HTTPLogWarn(@"%@: Init failed - Unable to get file attributes. filePath: %@", THIS_FILE, filePath);
			
			return nil;
		}
		
		fileLength = (UInt64)[[fileAttributes objectForKey:NSFileSize] unsignedLongLongValue];
		fileOffset = 0;
		
		aborted = NO;
		
		// We don't bother opening the file here.
		// If this is a HEAD request we only need to know the fileLength.
	}
	return self;
}

- (void)abort
{
	HTTPLogTrace();
	
	[connection responseDidAbort:self];
	aborted = YES;
}

- (void)processReadBuffer
{
	// This method is here to allow superclasses to perform post-processing of the data.
	// For an example, see the HTTPDynamicFileResponse class.
	// 
	// At this point, the readBuffer has readBufferOffset bytes available.
	// This method is in charge of updating the readBufferOffset.
	// Failure to do so will cause the readBuffer to grow to fileLength. (Imagine a 1 GB file...)
	
	// Copy the data out of the temporary readBuffer.
	data = [[NSData alloc] initWithBytes:readBuffer length:readBufferOffset];
	
	// Reset the read buffer.
	readBufferOffset = 0;
	
	// Notify the connection that we have data available for it.
	[connection responseHasAvailableData:self];
}

- (void)pauseReadSource
{
	if (!readSourceSuspended)
	{
		HTTPLogVerbose(@"%@[%p]: Suspending readSource", THIS_FILE, self);
		
		readSourceSuspended = YES;
		dispatch_suspend(readSource);
	}
}

- (void)resumeReadSource
{
	if (readSourceSuspended)
	{
		HTTPLogVerbose(@"%@[%p]: Resuming readSource", THIS_FILE, self);
		
		readSourceSuspended = NO;
		dispatch_resume(readSource);
	}
}

- (void)cancelReadSource
{
	HTTPLogVerbose(@"%@[%p]: Canceling readSource", THIS_FILE, self);
	
	dispatch_source_cancel(readSource);
	
	// Cancelling a dispatch source doesn't
	// invoke the cancel handler if the dispatch source is paused.
	
	if (readSourceSuspended)
	{
		readSourceSuspended = NO;
		dispatch_resume(readSource);
	}
}

- (BOOL)openFileAndSetupReadSource
{
	HTTPLogTrace();
	
	fileFD = open([filePath UTF8String], (O_RDONLY | O_NONBLOCK));
	if (fileFD == NULL_FD)
	{
		HTTPLogError(@"%@: Unable to open file. filePath: %@", THIS_FILE, filePath);
		
		return NO;
	}
	
	HTTPLogVerbose(@"%@[%p]: Open fd[%i] -> %@", THIS_FILE, self, fileFD, filePath);
	
	readQueue = dispatch_queue_create("HTTPAsyncFileResponse", NULL);
	readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fileFD, 0, readQueue);
	
	
	dispatch_source_set_event_handler(readSource, ^{
		
		HTTPLogTrace2(@"%@: eventBlock - fd[%i]", THIS_FILE, fileFD);
		
		// Determine how much data we should read.
		// 
		// It is OK if we ask to read more bytes than exist in the file.
		// It is NOT OK to over-allocate the buffer.
		
		unsigned long long _bytesAvailableOnFD = dispatch_source_get_data(readSource);
		
		UInt64 _bytesLeftInFile = fileLength - readOffset;
		
		NSUInteger bytesAvailableOnFD;
		NSUInteger bytesLeftInFile;
		
		bytesAvailableOnFD = (_bytesAvailableOnFD > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesAvailableOnFD;
		bytesLeftInFile    = (_bytesLeftInFile    > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesLeftInFile;
		
		NSUInteger bytesLeftInRequest = readRequestLength - readBufferOffset;
		
		NSUInteger bytesLeft = MIN(bytesLeftInRequest, bytesLeftInFile);
		
		NSUInteger bytesToRead = MIN(bytesAvailableOnFD, bytesLeft);
		
		// Make sure buffer is big enough for read request.
		// Do not over-allocate.
		
		if (readBuffer == NULL || bytesToRead > (readBufferSize - readBufferOffset))
		{
			readBufferSize = bytesToRead;
			readBuffer = reallocf(readBuffer, (size_t)bytesToRead);
			
			if (readBuffer == NULL)
			{
				HTTPLogError(@"%@[%p]: Unable to allocate buffer", THIS_FILE, self);
				
				[self pauseReadSource];
				[self abort];
				
				return;
			}
		}
		
		// Perform the read
		
		HTTPLogVerbose(@"%@[%p]: Attempting to read %lu bytes from file", THIS_FILE, self, (unsigned long)bytesToRead);
		
		ssize_t result = read(fileFD, readBuffer + readBufferOffset, (size_t)bytesToRead);
		
		// Check the results
		if (result < 0)
		{
			HTTPLogError(@"%@: Error(%i) reading file(%@)", THIS_FILE, errno, filePath);
			
			[self pauseReadSource];
			[self abort];
		}
		else if (result == 0)
		{
			HTTPLogError(@"%@: Read EOF on file(%@)", THIS_FILE, filePath);
			
			[self pauseReadSource];
			[self abort];
		}
		else // (result > 0)
		{
			HTTPLogVerbose(@"%@[%p]: Read %lu bytes from file", THIS_FILE, self, (unsigned long)result);
			
			readOffset += result;
			readBufferOffset += result;
			
			[self pauseReadSource];
			[self processReadBuffer];
		}
		
	});
	
	int theFileFD = fileFD;
	#if NEEDS_DISPATCH_RETAIN_RELEASE
	dispatch_source_t theReadSource = readSource;
	#endif
	
	dispatch_source_set_cancel_handler(readSource, ^{
		
		// Do not access self from within this block in any way, shape or form.
		// 
		// Note: You access self if you reference an iVar.
		
		HTTPLogTrace2(@"%@: cancelBlock - Close fd[%i]", THIS_FILE, theFileFD);
		
		#if NEEDS_DISPATCH_RETAIN_RELEASE
		dispatch_release(theReadSource);
		#endif
		close(theFileFD);
	});
	
	readSourceSuspended = YES;
	
	return YES;
}

- (BOOL)openFileIfNeeded
{
	if (aborted)
	{
		// The file operation has been aborted.
		// This could be because we failed to open the file,
		// or the reading process failed.
		return NO;
	}
	
	if (fileFD != NULL_FD)
	{
		// File has already been opened.
		return YES;
	}
	
	return [self openFileAndSetupReadSource];
}	

- (UInt64)contentLength
{
	HTTPLogTrace2(@"%@[%p]: contentLength - %llu", THIS_FILE, self, fileLength);
	
	return fileLength;
}

- (UInt64)offset
{
	HTTPLogTrace();
	
	return fileOffset;
}

- (void)setOffset:(UInt64)offset
{
	HTTPLogTrace2(@"%@[%p]: setOffset:%llu", THIS_FILE, self, offset);
	
	if (![self openFileIfNeeded])
	{
		// File opening failed,
		// or response has been aborted due to another error.
		return;
	}
	
	fileOffset = offset;
	readOffset = offset;
	
	off_t result = lseek(fileFD, (off_t)offset, SEEK_SET);
	if (result == -1)
	{
		HTTPLogError(@"%@[%p]: lseek failed - errno(%i) filePath(%@)", THIS_FILE, self, errno, filePath);
		
		[self abort];
	}
}

- (NSData *)readDataOfLength:(NSUInteger)length
{
	HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)length);
	
	if (data)
	{
		NSUInteger dataLength = [data length];
		
		HTTPLogVerbose(@"%@[%p]: Returning data of length %lu", THIS_FILE, self, (unsigned long)dataLength);
		
		fileOffset += dataLength;
		
		NSData *result = data;
		data = nil;
		
		return result;
	}
	else
	{
		if (![self openFileIfNeeded])
		{
			// File opening failed,
			// or response has been aborted due to another error.
			return nil;
		}
		
		dispatch_sync(readQueue, ^{
			
			NSAssert(readSourceSuspended, @"Invalid logic - perhaps HTTPConnection has changed.");
			
			readRequestLength = length;
			[self resumeReadSource];
		});
		
		return nil;
	}
}

- (BOOL)isDone
{
	BOOL result = (fileOffset == fileLength);
	
	HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO"));
	
	return result;
}

- (NSString *)filePath
{
	return filePath;
}

- (BOOL)isAsynchronous
{
	HTTPLogTrace();
	
	return YES;
}

- (void)connectionDidClose
{
	HTTPLogTrace();
	
	if (fileFD != NULL_FD)
	{
		dispatch_sync(readQueue, ^{
			
			// Prevent any further calls to the connection
			connection = nil;
			
			// Cancel the readSource.
			// We do this here because the readSource's eventBlock has retained self.
			// In other words, if we don't cancel the readSource, we will never get deallocated.
			
			[self cancelReadSource];
		});
	}
}

- (void)dealloc
{
	HTTPLogTrace();
	
	#if NEEDS_DISPATCH_RETAIN_RELEASE
	if (readQueue) dispatch_release(readQueue);
	#endif
	
	if (readBuffer)
		free(readBuffer);
}

@end
