#import "HTTPFileResponse.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

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

#define NULL_FD  -1


@implementation HTTPFileResponse

- (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] stringByResolvingSymlinksInPath];
        if (filePath == nil)
        {
            HTTPLogWarn(@"%@: Init failed - Nil filePath", THIS_FILE);
            
            return nil;
        }
        
        NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
        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;
}

- (BOOL)openFile
{
    HTTPLogTrace();
    
    fileFD = open([filePath UTF8String], O_RDONLY);
    if (fileFD == NULL_FD)
    {
        HTTPLogError(@"%@[%p]: Unable to open file. filePath: %@", THIS_FILE, self, filePath);
        
        [self abort];
        return NO;
    }
    
    HTTPLogVerbose(@"%@[%p]: Open fd[%i] -> %@", THIS_FILE, self, fileFD, filePath);
    
    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 openFile];
}

- (UInt64)contentLength
{
    HTTPLogTrace();
    
    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;
    
    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 (![self openFileIfNeeded])
    {
        // File opening failed,
        // or response has been aborted due to another error.
        return nil;
    }
    
    // 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.
    
    UInt64 bytesLeftInFile = fileLength - fileOffset;
    
    NSUInteger bytesToRead = (NSUInteger)MIN(length, bytesLeftInFile);
    
    // Make sure buffer is big enough for read request.
    // Do not over-allocate.
    
    if (buffer == NULL || bufferSize < bytesToRead)
    {
        bufferSize = bytesToRead;
        buffer = reallocf(buffer, (size_t)bufferSize);
        
        if (buffer == NULL)
        {
            HTTPLogError(@"%@[%p]: Unable to allocate buffer", THIS_FILE, self);
            
            [self abort];
            return nil;
        }
    }
    
    // Perform the read
    
    HTTPLogVerbose(@"%@[%p]: Attempting to read %lu bytes from file", THIS_FILE, self, (unsigned long)bytesToRead);
    
    ssize_t result = read(fileFD, buffer, bytesToRead);
    
    // Check the results
    
    if (result < 0)
    {
        HTTPLogError(@"%@: Error(%i) reading file(%@)", THIS_FILE, errno, filePath);
        
        [self abort];
        return nil;
    }
    else if (result == 0)
    {
        HTTPLogError(@"%@: Read EOF on file(%@)", THIS_FILE, filePath);
        
        [self abort];
        return nil;
    }
    else // (result > 0)
    {
        HTTPLogVerbose(@"%@[%p]: Read %ld bytes from file", THIS_FILE, self, (long)result);
        
        fileOffset += result;
        
        return [NSData dataWithBytes:buffer length:result];
    }
}

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

- (NSString *)filePath
{
    return filePath;
}

- (void)dealloc
{
    HTTPLogTrace();
    
    if (fileFD != NULL_FD)
    {
        HTTPLogVerbose(@"%@[%p]: Close fd[%i]", THIS_FILE, self, fileFD);
        
        close(fileFD);
    }
    
    if (buffer)
        free(buffer);
    
}

@end
