#import "ConduitCURLInterface.h"
#import <dispatch/dispatch.h>

// I type this a LOT in here:
#define c(STRING) [STRING cStringUsingEncoding:NSUTF8StringEncoding]

typedef struct
{
    dispatch_source_t readSource;
    dispatch_source_t writeSource;
}
CurlSocketContext_S;


@interface ConduitCURLInterface ()

@property (readonly) dispatch_queue_t disQ;

@end


@implementation ConduitCURLInterface
{
    dispatch_queue_t disQ;
    dispatch_source_t timeoutSource;
    
    CURLM * curlMulti;
    NSMutableArray * handleContexts;
        
    __unsafe_unretained id<ConduitCURLDelegate> delegate;
}

- (ConduitCURLHandleData *)addUploadWithURL:(NSString *)aURL
                                       file:(NSString *)aFile
                                   username:(NSString *)aUsername
                                   password:(NSString *)aPassword
                                   usingSSL:(BOOL)shouldUseSSL
                                    context:(id)aContext
{
    if (!aURL || !aFile) {
        NSLog(@"[CURLInterface] Invalid upload data, URL = %@, file = %@", aURL, aFile);
        return nil;
    }
    
    NSData * fileData = [NSData dataWithContentsOfFile:aFile];
    if (!fileData) return nil;

    // Make a data object to associate with the easy-handle.
    ConduitCURLHandleData * handleData = [[ConduitCURLHandleData alloc] init];
    [handleData setDataRef:fileData];
    [handleData setContext:aContext];
    [handleData setInterface:self];
    [handleData setFile:aFile];
    [handleContexts addObject:handleData];
    
    // Set up the easy-handle.
    CURL * curlEasy = curl_easy_init();
    if (!curlEasy) return nil;
    
    curl_easy_setopt(curlEasy, CURLOPT_PRIVATE, handleData); // Attach our data thingy.
    
    curl_easy_setopt(curlEasy, CURLOPT_URL, c(aURL));
    curl_easy_setopt(curlEasy, CURLOPT_READFUNCTION, &read_function);
    curl_easy_setopt(curlEasy, CURLOPT_READDATA, handleData); // We need access to the handle's data here too.
    
    curl_easy_setopt(curlEasy, CURLOPT_INFILESIZE_LARGE, [fileData length]);
    
    curl_easy_setopt(curlEasy, CURLOPT_NOPROGRESS, NO); // This is such a backwards option.
    curl_easy_setopt(curlEasy, CURLOPT_PROGRESSFUNCTION, &progress_callback);
    curl_easy_setopt(curlEasy, CURLOPT_PROGRESSDATA, handleData); // …and here…
    
    curl_easy_setopt(curlEasy, CURLOPT_VERBOSE, [self debug]);
    curl_easy_setopt(curlEasy, CURLOPT_DEBUGFUNCTION, &debug_callback);
    
    // These two hook up the error handling to our data object:
    curl_easy_setopt(curlEasy, CURLOPT_DEBUGDATA, [handleData debugOutput]);
    curl_easy_setopt(curlEasy, CURLOPT_ERRORBUFFER, [handleData errorString]);
    
    curl_easy_setopt(curlEasy, CURLOPT_SOCKOPTFUNCTION, &socket_function);
    curl_easy_setopt(curlEasy, CURLOPT_SOCKOPTDATA, curlEasy);
    
    curl_easy_setopt(curlEasy, CURLOPT_NOSIGNAL, YES);
    
    curl_easy_setopt(curlEasy, CURLOPT_USERNAME, c(aUsername));
    curl_easy_setopt(curlEasy, CURLOPT_PASSWORD, c(aPassword));
    
    if (shouldUseSSL) {
        curl_easy_setopt(curlEasy, CURLOPT_USE_SSL, CURLUSESSL_ALL);
    }
    else {
        curl_easy_setopt(curlEasy, CURLOPT_USE_SSL, CURLUSESSL_NONE);
    }
    
    curl_easy_setopt(curlEasy, CURLOPT_UPLOAD, YES);
    
    dispatch_async(disQ, ^{
        curl_multi_add_handle(curlMulti, curlEasy);
        if ([[self delegate] respondsToSelector:@selector(uploadDidStartWithHandleData:)]) {
            BOOL proceed = [[self delegate] uploadDidStartWithHandleData:handleData];
            
            if (!proceed) {
                [handleData setCancelled:YES];
            }
        }
        
        NSLog(@"[CURLInterface] Added handle to curlMulti.");
    });
    
    return handleData;
}

#pragma mark - Doing stuff

// This is where the interesting stuff happens.
- (void)performPartialUploadForSocket:(curl_socket_t)aSocket usingActions:(int)someActions
{
    int running_handles;
    int pending;
    
    CURLMcode status = curl_multi_socket_action(curlMulti, aSocket, someActions, &running_handles);
    if (status) NSLog(@"multi-status: %d", status);
    
    // So we need to read through all of the available info for the handles in the multiHandle.
    CURLMsg * message = NULL;
    while ((message = curl_multi_info_read(curlMulti, &pending))) {
        if (message->msg != CURLMSG_DONE) {
            // This should never happen, so we'll just log and skip this info.
            NSLog(@"[CURLInterface] Strange CURLMSG: %d (Should be 1/CURLMSG_DONE)", message->msg);
            continue;
        }
        
        char * privateData = NULL;
        CURLcode retrievedPrivate = curl_easy_getinfo(message->easy_handle, CURLINFO_PRIVATE, &privateData);
        if (retrievedPrivate != CURLE_OK) {
            NSLog(@"Failed to retrieve private data pointer from libcurl. Ack.");
            continue;
        }
        
        // Now THIS is a pretty line of code:
        ConduitCURLHandleData * currentHandleData = (__bridge ConduitCURLHandleData *)((void *)privateData);
        
        [currentHandleData setErrorCode:(message->data.result)];
        
        if ([currentHandleData errorCode] == CURLE_OK) {
            if ([[self delegate] respondsToSelector:@selector(uploadDidCompleteWithHandleData:)]) {
                [[self delegate] uploadDidCompleteWithHandleData:currentHandleData];
            }
        }
        else {
            if ([[self delegate] respondsToSelector:@selector(uploadDidFailWithHandleData:)]) {
                [[self delegate] uploadDidFailWithHandleData:currentHandleData];
            }
        }
        
        // Do some final cleanup of the completed handle.
        curl_multi_remove_handle(curlMulti, message->easy_handle);
        curl_easy_cleanup(message->easy_handle);
        [handleContexts removeObject:currentHandleData]; // This will allow the context to be cleaned up.
    }
}

#pragma mark - Utility

@synthesize disQ;

- (CURLM *)multiHandle
{
    return curlMulti;
}

- (dispatch_source_t)attachSourceOfType:(dispatch_source_type_t)aType
                              forSocket:(curl_socket_t)aSocket
                            usingAction:(int)anAction
{
    dispatch_source_t source = dispatch_source_create(aType, aSocket, 0, disQ);

    dispatch_source_set_event_handler(source, ^{
        //NSLog(@"socket %d: %s", aSocket, (anAction == CURL_CSELECT_IN) ? "read" : "write");
        [self performPartialUploadForSocket:aSocket usingActions:anAction];
    });

    dispatch_source_set_cancel_handler(source, ^{
        NSLog(@"cancelling %d", aSocket);
        dispatch_release(source);
    });

    dispatch_resume(source);
    
    return source;
}

#pragma mark - CURL easy callbacks

size_t read_function(void * buf, size_t size, size_t itemCount, void * data)
/// @param data: Used here as a pointer to the appropriate handleData.
{
    NSLog(@"hi read");
    ConduitCURLHandleData * hData = (__bridge ConduitCURLHandleData *)data;
    
    if ([hData cancelled]) return CURL_READFUNC_ABORT;
    if ([hData currentByte] >= [[hData dataRef] length]) return 0; // We're done!
    
    NSUInteger length = MIN([[hData dataRef] length] - [hData currentByte], size * itemCount);
    [[hData dataRef] getBytes:buf range:NSMakeRange([hData currentByte], length)];
    [hData setCurrentByte:([hData currentByte] + length)];
    
    return length;
}

int progress_callback(void * data, double dltotal, double dlnow, double ultotal, double ulnow)
/// @param data: Used here as a pointer to the appropriate handleData.
{
    ConduitCURLHandleData * hData = (__bridge ConduitCURLHandleData *)data;
    id delegate = [[hData interface] delegate];
    
    // Don't call the delegate on the first few useless calls of this function, or if the total size failed to get set.
    if (ultotal <= 0) return 0;
    
    if ([delegate respondsToSelector:@selector(uploadDidProgressTo:withContext:)]) {
        BOOL proceed = [delegate uploadDidProgressTo:@((ulnow / ultotal) * 100.0) withHandleData:hData];
        
        if (!proceed) {
            [hData setCancelled:YES];
            return 1; // May as well bail out here.
        }
    }
    
    return 0;
}

int debug_callback(CURL * theCURL, curl_infotype infoType, char * debugInfo, size_t length, void * debugStorage)
/*** Omitted irrelevant function ***/

int socket_function(void * userData, curl_socket_t curlfd, curlsocktype purpose)
{
    CURL * handle = (CURL *)userData;
    
    if (purpose == CURLSOCKTYPE_IPCXN)
    {
        char* url = NULL;
        curl_easy_getinfo(handle, CURLINFO_EFFECTIVE_URL, &url);
        
        if (strncmp(url, "ftp:", 4) == 0)
        {
            int keepAlive = 1;
            socklen_t keepAliveLen = sizeof(keepAlive);
            int result = setsockopt(curlfd, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, keepAliveLen);
            if (result)
            {
                NSLog(@"Unable to set FTP control connection keepalive with error: %i", result);
            }
        }
    }
    
    return 0;
}

#pragma mark - CURL multi-socket callbacks

int socket_callback(CURL * easyHandle, curl_socket_t socket, int action, void * userData, void * socketData)
/// @param userData: Used here as an alias to this ConduitCURLInterface.
/// @param socketData: Used here as a pointer to the structure that maintains the dispatch sources for the given socket.
{
    ConduitCURLInterface * interface = (__bridge ConduitCURLInterface *)userData;
    CurlSocketContext_S * currentContext = (CurlSocketContext_S *)socketData;
    
    if (action & CURL_POLL_IN || action & CURL_POLL_OUT) {
        if (!currentContext) {
            NSLog(@"Creating CurlSocketContext_S for %d", socket);
            // If this socket doesn't have a context attached yet, make one:
            currentContext = malloc(sizeof(CurlSocketContext_S));
            memset(currentContext, 0, sizeof(CurlSocketContext_S));
            
            curl_multi_assign([interface multiHandle], socket, currentContext);
        }
    }
    
    if (action & CURL_POLL_IN) {
        NSLog(@"CURL_POLL_IN for %d", socket);
        currentContext->readSource = [interface attachSourceOfType:DISPATCH_SOURCE_TYPE_READ
                                                         forSocket:socket
                                                       usingAction:CURL_CSELECT_IN];
    }
    
    if (action & CURL_POLL_OUT) {
        NSLog(@"CURL_POLL_OUT for %d", socket);
        currentContext->writeSource = [interface attachSourceOfType:DISPATCH_SOURCE_TYPE_WRITE
                                                          forSocket:socket
                                                        usingAction:CURL_CSELECT_OUT];
    }
    
    if ((action & CURL_POLL_REMOVE) && currentContext) {
        NSLog(@"CURL_POLL_REMOVE for %d", socket);
        if (currentContext->readSource) {
            dispatch_source_cancel(currentContext->readSource);
        }
        if (currentContext->writeSource) {
            dispatch_source_cancel(currentContext->writeSource);
        }
        
        free(currentContext);
        
        curl_multi_assign([interface multiHandle], socket, NULL);
    }
    
    return 0; // Apparently it has to.
}

void timer_callback(CURLM * multiHandle, long timeout_ms, void * userData)
/// @param userData: Used here as an alias to timeoutSource.
{
    if (timeout_ms <= 0) {
        timeout_ms = 1;
    }
    NSLog(@"new timeout = %ld", timeout_ms);
    int64_t timeout_ns = timeout_ms * NSEC_PER_MSEC;
    dispatch_source_set_timer((dispatch_source_t)userData, DISPATCH_TIME_NOW, timeout_ns, timeout_ns / 100);
}

#pragma mark - Plumbing

@synthesize debug, delegate;

- (id)init
{
    if (!(self = [super init])) return nil;
    
    if (curl_global_init(CURL_GLOBAL_NOTHING)) return nil;
    
    curlMulti = curl_multi_init(); // Doing this first to avoid having to roll-back if it fails.
    if (!curlMulti) return nil;
    
    // Get a dispatch queue and add the timeout source.
    disQ = dispatch_queue_create(DISPATCH_QUEUE_SERIAL, 0);
    timeoutSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, disQ);
    dispatch_source_set_event_handler(timeoutSource, ^{
        [self performPartialUploadForSocket:CURL_SOCKET_TIMEOUT usingActions:0];
    });
    
    dispatch_resume(timeoutSource);
    
    // Set up the cURL multi-handle.
    curl_multi_setopt(curlMulti, CURLMOPT_SOCKETFUNCTION, &socket_callback);
    curl_multi_setopt(curlMulti, CURLMOPT_SOCKETDATA, self);
    curl_multi_setopt(curlMulti, CURLMOPT_TIMERFUNCTION, &timer_callback);
    curl_multi_setopt(curlMulti, CURLMOPT_TIMERDATA, timeoutSource);
    
    handleContexts = [NSMutableArray array];
    
    return self;
}

- (void)dealloc
/// @todo: Clean up any remaining handles first.
{
    curl_multi_cleanup(curlMulti);
}

@end
