//
//  TranscodingHandler.m
//  CyTV
//
//  Created by Andreas Junghans on 13.01.08.
//  Copyright 2008 __MyCompanyName__. All rights reserved.
//

#import "TranscodingHandler.h"
#import "HTTPServer.h"


@implementation TranscodingHandler

- (void)dealloc {
    printf("dealloc\n");
    [_ffmpegOutPipe release];
    [_ffmpegTask release];
    [_dataQueue release];
    [_dataQueueLock release];
    [super dealloc];
}

- (void)waitForReadingFinished {
    BOOL readingFinished;
    [_dataQueueLock lock];
    readingFinished = _readingFinished;
    [_dataQueueLock unlock];
    if (readingFinished) {
        [self release];
    } else {
        [self performSelector:@selector(waitForReadingFinished) withObject:nil afterDelay:0.1];
        [_ffmpegTask terminate];
    }
}

- (void)shutDownAndRelease {
    printf("shutDownAndRelease\n");
    _shutDown = TRUE;
    [_dataQueueLock lock];
    [_dataQueue removeAllObjects];
    _ignorePendingData = TRUE;
    [_dataQueueLock unlock];
    [self waitForReadingFinished];
}

- (BOOL)checkParam:(NSString *)param {
    int i;
    for (i = 0; i < [param length]; ++i) {
        unichar chr = [param characterAtIndex:i];
        if (chr >= 'a' && chr <= 'z' || chr >= 'A' && chr <= 'Z' || chr == '.' || chr == '_' ||
            chr >= '0' && chr <= '9') {
            // OK
        } else {
            return FALSE;
        }
    }
    return TRUE;
}

- (void)handleRequest {
    NSMutableString *localURL = [NSMutableString stringWithCapacity:100];
    [localURL appendFormat:@"http://localhost:%hu", [_asyncSocket localPort]];
    [localURL appendString:_path];
    BOOL firstParam = TRUE;
    NSString *key;
    NSEnumerator *keyEnumerator = [_paramsDict keyEnumerator];
    while ((key = [keyEnumerator nextObject])) {
        if ([key rangeOfString:@"transcode" options:NSCaseInsensitiveSearch].location != 0) {
            if (firstParam) {
                [localURL appendString:@"?"];
                firstParam = FALSE;
            } else {
                [localURL appendString:@"&"];
            }
            [localURL appendString:[key stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
            [localURL appendString:@"="];
            [localURL appendString:[[_paramsDict objectForKey:key] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
        }
    }
    if (firstParam) {
        [localURL appendString:@"?"];
    } else {
        [localURL appendString:@"&"];
    }
    [localURL appendString:@"fromTranscode=true"];
    // TODO: add authentication token to the URL (when authentication is implemented)
    printf("Local URL: %s\n", [localURL UTF8String]);
    
    // set transcode options
    NSMutableArray *transcodeOptions = [[[NSMutableArray alloc] init] autorelease];
    [transcodeOptions addObject:@"-i"];
    [transcodeOptions addObject:localURL];
    [transcodeOptions addObject:@"-v"];
    [transcodeOptions addObject:@"0"];
    NSString *transcodeWidthString = [_paramsDict objectForKey:@"transcodeWidth"];
    unsigned long transcodeWidth = 0;
    if (transcodeWidthString != nil && [transcodeWidthString compare:@"original" options:NSCaseInsensitiveSearch] != NSOrderedSame) {
        transcodeWidth = (unsigned long)[transcodeWidthString intValue];
        if (transcodeWidth < 32) {
            transcodeWidth = 32;
        } else if (transcodeWidth > 1920) {
            transcodeWidth = 1920;
        }
    }
    NSString *transcodeHeightString = [_paramsDict objectForKey:@"transcodeHeight"];
    unsigned long transcodeHeight = 0;
    if (transcodeHeightString != nil && [transcodeHeightString compare:@"original" options:NSCaseInsensitiveSearch] != NSOrderedSame) {
        transcodeHeight = (unsigned long)[transcodeHeightString intValue];
        if (transcodeHeight < 32) {
            transcodeHeight = 32;
        } else if (transcodeHeight > 1200) {
            transcodeHeight = 1200;
        }
    }
    if (transcodeWidth != 0 && transcodeHeight != 0) {
        [transcodeOptions addObject:@"-s"];
        [transcodeOptions addObject:[NSString stringWithFormat:@"%ux%u", transcodeWidth, transcodeHeight]];
    }
    NSString *transcodeFrameRate = [_paramsDict objectForKey:@"transcodeFrameRate"];
    if (transcodeFrameRate != nil) {
        if ([transcodeFrameRate compare:@"original" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
            transcodeFrameRate = nil;
        } else if ([transcodeFrameRate doubleValue] > 100) {
            transcodeFrameRate = nil;
        } else if (![self checkParam:transcodeFrameRate]) {
            transcodeFrameRate = nil;
        }
    }
    if (transcodeFrameRate != nil) {
        [transcodeOptions addObject:@"-r"];
        [transcodeOptions addObject:transcodeFrameRate];
    }
    NSString *transcodeVideoBitrateString = [_paramsDict objectForKey:@"transcodeVideoBitrate"];
    unsigned long transcodeVideoBitrate = 0;
    if (transcodeVideoBitrateString != nil) {
        transcodeVideoBitrate = (unsigned long)[transcodeVideoBitrateString intValue];
    }
    if (transcodeVideoBitrate < 128) {
        transcodeVideoBitrate = 128;
    } else if (transcodeVideoBitrate > 8192) {
        transcodeVideoBitrate = 8192;
    }
    [transcodeOptions addObject:@"-b"];
    [transcodeOptions addObject:[NSString stringWithFormat:@"%u", transcodeVideoBitrate]];
    NSString *transcodeAudioBitrateString = [_paramsDict objectForKey:@"transcodeAudioBitrate"];
    unsigned long transcodeAudioBitrate = 0;
    if (transcodeAudioBitrateString != nil) {
        transcodeAudioBitrate = (unsigned long)[transcodeAudioBitrateString intValue];
    }
    if (transcodeAudioBitrate < 32) {
        transcodeAudioBitrate = 32;
    } else if (transcodeAudioBitrate > 512) {
        transcodeAudioBitrate = 512;
    }
    [transcodeOptions addObject:@"-ab"];
    [transcodeOptions addObject:[NSString stringWithFormat:@"%u", transcodeAudioBitrate]];
    NSString *transcodeAudioSampleRateString = [_paramsDict objectForKey:@"transcodeAudioSampleRate"];
    if (transcodeAudioSampleRateString != nil) {
        unsigned long transcodeAudioSampleRate = (unsigned long)[transcodeAudioSampleRateString intValue];
        if (transcodeAudioSampleRate < 22050) {
            transcodeAudioSampleRate = 22050;
        } else if (transcodeAudioSampleRate > 96000) {
            transcodeAudioSampleRate = 96000;
        }
        [transcodeOptions addObject:@"-ar"];
        [transcodeOptions addObject:[NSString stringWithFormat:@"%u", transcodeAudioSampleRate]];
    }
    NSString *transcodeMono = [_paramsDict objectForKey:@"transcodeMono"];
    if (transcodeMono != nil && [transcodeMono compare:@"true" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
        [transcodeOptions addObject:@"-ac"];
        [transcodeOptions addObject:@"1"];
    }
    NSString *transcodeAudioSync = [_paramsDict objectForKey:@"transcodeAudioSync"];
    if (transcodeAudioSync != nil && [transcodeAudioSync compare:@"true" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
        [transcodeOptions addObject:@"-async"];
        [transcodeOptions addObject:@"1"];
    }
    [transcodeOptions addObject:@"-vcodec"];
    NSString *transcodeVideoCodec = [_paramsDict objectForKey:@"transcodeVideoCodec"];
    if (transcodeVideoCodec != nil && [self checkParam:transcodeVideoCodec] && [transcodeVideoCodec length] > 0) {
        [transcodeOptions addObject:transcodeVideoCodec];
    } else {
        [transcodeOptions addObject:@"h263p"];
    }
    [transcodeOptions addObject:@"-acodec"];
    NSString *transcodeAudioCodec = [_paramsDict objectForKey:@"transcodeAudioCodec"];
    if (transcodeAudioCodec != nil && [self checkParam:transcodeAudioCodec] && [transcodeAudioCodec length] > 0) {
        [transcodeOptions addObject:transcodeAudioCodec];
    } else {
        [transcodeOptions addObject:@"mp2"];
    }
    [transcodeOptions addObject:@"-f"];
    NSString *transcodeContainerFormat = [_paramsDict objectForKey:@"transcodeContainerFormat"];
    if (transcodeContainerFormat != nil && [self checkParam:transcodeContainerFormat] && [transcodeContainerFormat length] > 0) {
        [transcodeOptions addObject:transcodeContainerFormat];
    } else {
        [transcodeOptions addObject:@"avi"];
    }
    [transcodeOptions addObject:@"-"];
    
    _dataQueue = [[NSMutableArray alloc] init];
    _dataQueueLock = [[NSLock alloc] init];
    _ffmpegTask = [[NSTask alloc] init];
    NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
    NSString *ffmpegPath = [resourcePath stringByAppendingString:@"/ffmpeg"];
    [_ffmpegTask setLaunchPath:ffmpegPath];
    [_ffmpegTask setArguments:transcodeOptions];
        /*[NSArray arrayWithObjects:
        @"-i", localURL,
        @"-v", @"0",
        @"-vcodec", @"h263p",
        @"-acodec", @"mp2",
        @"-f", @"avi",
        @"-b", @"1150",
        @"-s", @"160x128",
        @"-ab", @"64",
        @"-ar", @"22050",
        @"-async", @"1",
        //@"-vn",
        @"-", nil]];*/
    _ffmpegOutPipe = [[NSPipe pipe] retain];
    [_ffmpegTask setStandardOutput:_ffmpegOutPipe];
    _ffmpegOutHandle = [_ffmpegOutPipe fileHandleForReading];
    [NSThread detachNewThreadSelector:@selector(readDataInBackground:) toTarget:self withObject:nil];

    [_ffmpegTask launch];
    
    CFHTTPMessageRef response = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1);
    CFHTTPMessageSetHeaderFieldValue(response, CFSTR("Transfer-Encoding"), CFSTR("chunked"));
    //CFHTTPMessageSetHeaderFieldValue(response, CFSTR("Content-Type"), CFSTR("video/avi"));
    NSData *responseData = [(NSData *)CFHTTPMessageCopySerializedMessage(response) autorelease];
    [_asyncSocket writeData:responseData withTimeout:WRITE_BODY_TIMEOUT tag:HTTP_PARTIAL_RESPONSE];
    CFRelease(response);
}

- (void)readDataInBackground:(id)param {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    BOOL terminate = FALSE;
    int i;
    int queueCount;
    while (!terminate) {
        for (i = 0; i < 100; ++i) {
            NSData *data;
            @try {
                data = [_ffmpegOutHandle availableData];
            } @catch (NSException *e) {
                data = [NSData data];
            }
            terminate = ([data length] == 0);
            [_dataQueueLock lock];
            queueCount = [_dataQueue count];
            if (!_ignorePendingData) {
                [_dataQueue addObject:data];
            }
            _readingFinished = terminate;
            [_dataQueueLock unlock];
            if (terminate) {
                printf("Terminating background thread\n");
                break;
            }
            while (queueCount >= 10) {
                usleep(100000);
                [_dataQueueLock lock];
                queueCount = [_dataQueue count];
                [_dataQueueLock unlock];
            }
        }
        [pool release];
        if (!terminate) {
            pool = [[NSAutoreleasePool alloc] init];
        }
    }
}

- (void)onSocketCanWrite:(AsyncSocket *)sock {
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onSocketCanWrite:) object:sock];
    if (_shutDown) {
        printf("Writing while shut down!\n");
        return;
    }
    [_dataQueueLock lock];
    NSArray *dataToWrite = [NSArray arrayWithArray:_dataQueue];
    [_dataQueue removeAllObjects];
    [_dataQueueLock unlock];
    int dataCount = [dataToWrite count];
    if (dataCount == 0) {
        [self performSelector:@selector(onSocketCanWrite:) withObject:sock afterDelay:0.1];
    } else {
        int i;
        for (i = 0; i < dataCount; ++i) {
            NSData *data = [dataToWrite objectAtIndex:i];
            if ([data length] == 0) {
                // write final data
                [_asyncSocket writeData:[NSData dataWithBytes:"0\r\n" length:3] withTimeout:WRITE_BODY_TIMEOUT tag:HTTP_RESPONSE];
                break;  // shouldn't be necessary, but doesn't hurt
            } else {
                // write current data
                char chunkLength[50];
                sprintf(chunkLength, "%llx\r\n", (uint64_t)[data length]);
                [_asyncSocket writeData:[NSData dataWithBytes:chunkLength length:strlen(chunkLength)]
                            withTimeout:WRITE_BODY_TIMEOUT tag:HTTP_PARTIAL_RESPONSE];
                [_asyncSocket writeData:data withTimeout:WRITE_BODY_TIMEOUT tag:HTTP_PARTIAL_RESPONSE];
                [_asyncSocket writeData:[NSData dataWithBytes:"\r\n" length:2] withTimeout:WRITE_BODY_TIMEOUT tag:HTTP_PARTIAL_RESPONSE];
            }
        }
    }
}

@end
