#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 DDLogLevel httpLogLevel = DDLogLevelWarning; // | 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
