/*
    DAAC_ConversionTask.m

    Copyright 2008,2009 Josh Freeman
    http://www.twilightedge.com

    This file is part of DAA Converter v1.3 for Mac OS X.

    DAA Converter is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#import "DAAC_ConversionTask.h"

#import "DAAC_FilenameUtilities.h"
#import "DAAC_DAA2ISOTaskOutputParsing.h"
#import "DAAC_Strings.h"


#define kDAA2ISOExecutableName      @"daa2iso"


typedef enum
{
    kTaskStatus_HasNotBegunConverting = 0,
    kTaskStatus_WaitingForPassword,
    kTaskStatus_Converting,

    // any additional unfinished status types must go above this line

    kTaskStatus_Finished

} TaskStatus;


static NSString *gDAA2ISOToolFilepath = nil;
static bool gConversionTaskGlobalsAreInitialized = NO;


@interface DAAC_ConversionTask (PrivateMethods)

- (void) failWithErrorMessage: (NSString *) errorMessage;

- (bool) beginDAA2ISOTaskWithDAAFile: (NSString *) daaFilepath
                            ISOFile: (NSString *) isoFilepath;

- (void) finishDAA2ISOTaskWithSuccess: (bool) taskDidFinishSuccessfully;

- (void) handleNotificationOfOutputDataFromDAA2ISOTask: (NSNotification *) notification;
- (void) parseDAA2ISOTaskOutputData: (NSData *) outputData;

- (void) writeEncryptionPasswordToDAA2ISOTaskInput: (NSString *) password;

- (void) notifyDelegateDidCompleteSuccessfully;
- (void) notifyDelegateDidFailOnDAAFile: (NSString *) filename
                        withErrorMessage: (NSString *) errorMessage;
- (void) notifyDelegateDidOpenDAAFile;
- (void) notifyDelegateDidProgress;
- (void) notifyDelegateEncryptionPasswordIsRequired;

@end

@implementation DAAC_ConversionTask

+ (void) initialize
{
    if (self != [DAAC_ConversionTask class])
        return;

    gDAA2ISOToolFilepath =
            [[[NSBundle mainBundle] pathForAuxiliaryExecutable: kDAA2ISOExecutableName] retain];

    gConversionTaskGlobalsAreInitialized = (gDAA2ISOToolFilepath) ? YES : NO;
}

+ conversionTaskWithSourceDAAFile: (NSString *) daaFilepath
                    targetISOFile: (NSString *) isoFilepath
                    delegate: (id) delegate
{
    return [[[self alloc] initWithSourceDAAFile: daaFilepath
                                targetISOFile: isoFilepath
                                delegate: delegate]
                autorelease];
}

- initWithSourceDAAFile: (NSString *) daaFilepath
        targetISOFile: (NSString *) isoFilepath
        delegate: (id) delegate
{
    self = [super init];

    if (self)
    {
        _delegate = delegate;

        if (![self beginDAA2ISOTaskWithDAAFile: daaFilepath ISOFile: isoFilepath])
            goto ERROR;
    }

    return self;

ERROR:
    [self release];

    return nil;
}

- init
{
    return [self initWithSourceDAAFile: nil targetISOFile: nil delegate: nil];
}

- (void) dealloc
{
    if (_daa2isoTask)
        [self finishDAA2ISOTaskWithSuccess: NO];

    [super dealloc];
}

- (void) cancel
{
    [self finishDAA2ISOTaskWithSuccess: NO];

    _delegate = nil;
}

- (void) submitEncryptionPassword: (NSString *) password
{
    if (_taskStatus == kTaskStatus_WaitingForPassword)
        [self writeEncryptionPasswordToDAA2ISOTaskInput: password];
}

- (void) failWithErrorMessage: (NSString *) errorMessage
{
    NSString *failedDAAFilename = [[_currentDAAFilename retain] autorelease];

    [self finishDAA2ISOTaskWithSuccess: NO];

    [self notifyDelegateDidFailOnDAAFile: failedDAAFilename
                        withErrorMessage: errorMessage];
}

+ (NSString *) daa2isoCredits
{
    NSTask *task;
    NSPipe *outputPipe;
    NSData *outputData;
    NSString *outputString, *daa2isoCredits;

    if (!gConversionTaskGlobalsAreInitialized)
        goto ERROR;

    task = [[[NSTask alloc] init] autorelease];
    outputPipe = [NSPipe pipe];

    if (!task || !outputPipe)
        goto ERROR;

    [task setLaunchPath: gDAA2ISOToolFilepath];
    [task setStandardOutput: outputPipe];
    [task setStandardError: outputPipe];

    [task launch];
    [task waitUntilExit];

    outputData = [[outputPipe fileHandleForReading] readDataToEndOfFile];

    if (![outputData length])
        goto ERROR;

    outputString = [[[NSString alloc] initWithData: outputData
                                        encoding: NSASCIIStringEncoding]
                            autorelease];

    if (!DAA2ISOTaskOutput_RetrieveCredits(outputString, &daa2isoCredits))
        goto ERROR;

    return daa2isoCredits;

ERROR:
    return @" ";
}

#pragma mark DAA2ISO Task

- (bool) beginDAA2ISOTaskWithDAAFile: (NSString *) daaFilepath
                            ISOFile: (NSString *) isoFilepath
{
    NSString *daaFileParentDirectoryPath, *daaFilename, *isoFilename = nil;
    NSTask *daa2isoTask;
    NSArray *daa2isoTaskArguments;
    NSPipe *daa2isoTaskInputPipe, *daa2isoTaskOutputPipe;
    NSFileHandle *daa2isoTaskOutputFileHandle;

    if (!gConversionTaskGlobalsAreInitialized || _daa2isoTask
        || ![daaFilepath length] || ![isoFilepath length])
    {
        goto ERROR;
    }

    // the daa2iso tool requires the path to the DAA archive's primary (first) file
    // (an archive may be split across multiple files)
    daaFilepath = PrimaryDAAFilepathForDAAFilepath(daaFilepath);

    daaFileParentDirectoryPath = [daaFilepath stringByDeletingLastPathComponent];
    daaFilename = [daaFilepath lastPathComponent];

    if (![daaFileParentDirectoryPath length] || ![daaFilename length])
        goto ERROR;

    if ([[isoFilepath stringByDeletingLastPathComponent]
                                                isEqualToString: daaFileParentDirectoryPath])
    {
        isoFilename = [isoFilepath lastPathComponent];
    }

    if (![isoFilename length])
        isoFilename = isoFilepath;

    daa2isoTask = [[[NSTask alloc] init] autorelease];
    daa2isoTaskArguments = [NSArray arrayWithObjects: daaFilename, isoFilename, nil];
    daa2isoTaskInputPipe = [NSPipe pipe];
    daa2isoTaskOutputPipe = [NSPipe pipe];
    daa2isoTaskOutputFileHandle = [daa2isoTaskOutputPipe fileHandleForReading];

    if (!daa2isoTask || !daa2isoTaskArguments || !daa2isoTaskInputPipe
            || !daa2isoTaskOutputPipe || !daa2isoTaskOutputFileHandle)
    {
        goto ERROR;
    }

    _daa2isoTask = [daa2isoTask retain];
    _daa2isoTaskOutputPipe = [daa2isoTaskOutputPipe retain];
    _daa2isoTaskInputPipe = [daa2isoTaskInputPipe retain];

    _currentDAAFilename = [daaFilename retain];
    _isoFilepath = [isoFilepath retain];

    [_daa2isoTask setLaunchPath: gDAA2ISOToolFilepath];
    [_daa2isoTask setCurrentDirectoryPath: daaFileParentDirectoryPath];
    [_daa2isoTask setArguments: daa2isoTaskArguments];
    [_daa2isoTask setStandardOutput: _daa2isoTaskOutputPipe];
    [_daa2isoTask setStandardError: _daa2isoTaskOutputPipe];
    [_daa2isoTask setStandardInput: _daa2isoTaskInputPipe];

    [[NSNotificationCenter defaultCenter] addObserver: self
                                            selector:
                                    @selector(handleNotificationOfOutputDataFromDAA2ISOTask:)
                                                name: NSFileHandleReadCompletionNotification
                                            object: daa2isoTaskOutputFileHandle];

    [_daa2isoTask launch];

    [daa2isoTaskOutputFileHandle readInBackgroundAndNotify];

    return YES;

ERROR:
    return NO;
}

- (void) finishDAA2ISOTaskWithSuccess: (bool) taskDidFinishSuccessfully
{
    _taskStatus = kTaskStatus_Finished;

    if (!_daa2isoTask)
        return;

    [[NSNotificationCenter defaultCenter] removeObserver: self
                                                name: NSFileHandleReadCompletionNotification
                                                object: nil];

    if ([_daa2isoTask isRunning])
        [_daa2isoTask terminate];

    if (!taskDidFinishSuccessfully
            && [[NSFileManager defaultManager] isDeletableFileAtPath: _isoFilepath])
    {
        [[NSFileManager defaultManager] removeFileAtPath: _isoFilepath handler: nil];
    }

    [_daa2isoTask release];
    _daa2isoTask = nil;

    [_daa2isoTaskOutputPipe release];
    _daa2isoTaskOutputPipe = nil;

    [_daa2isoTaskInputPipe release];
    _daa2isoTaskInputPipe = nil;

    [_currentDAAFilename release];
    _currentDAAFilename = nil;

    [_isoFilepath release];
    _isoFilepath = nil;
}

- (void) handleNotificationOfOutputDataFromDAA2ISOTask: (NSNotification *) notification
{
    [self parseDAA2ISOTaskOutputData:
                    [[notification userInfo] valueForKey: NSFileHandleNotificationDataItem]];

    if (_taskStatus >= kTaskStatus_Finished)
        return;

    if ([_daa2isoTask isRunning])
    {
        [[_daa2isoTaskOutputPipe fileHandleForReading] readInBackgroundAndNotify];
    }
    else
    {
        // _daa2isoTask is no longer running, but _taskStatus doesn't yet reflect this,
        // so flush any remaining data from the output pipe and parse it to see if that updates
        // the status...
        [self parseDAA2ISOTaskOutputData:
                        [[_daa2isoTaskOutputPipe fileHandleForReading] readDataToEndOfFile]];

        // if parsing the remaining data didn't fix _taskStatus, do it manually...
        if (_taskStatus < kTaskStatus_Finished)
            [self failWithErrorMessage: kErrorMessage_UnknownError];
    }
}

- (void) parseDAA2ISOTaskOutputData: (NSData *) outputData
{
    NSString *outputString, *newDAAFilename, *errorMessage;
    unsigned newCompletionPercentage;

    if ((_taskStatus >= kTaskStatus_Finished) || ![outputData length])
        return;

    outputString = [[[NSString alloc] initWithData: outputData
                                        encoding: NSASCIIStringEncoding]
                            autorelease];

    if (![outputString length])
        return;

    // using new DAA source file
    if (DAA2ISOTaskOutput_HasNewDAASourceFile(outputString, &newDAAFilename))
    {
        [_currentDAAFilename release];
        _currentDAAFilename = [newDAAFilename retain];

        [self notifyDelegateDidOpenDAAFile];
    }

    if (_taskStatus != kTaskStatus_Converting) // conversion hasn't begun yet
    {
        // unknown DAA version alert
        if (DAA2ISOTaskOutput_HasUnknownDAAVersionAlert(outputString))
            _archiveHasUnknownDAAVersion = YES;

        // password request
        if (DAA2ISOTaskOutput_HasPasswordRequest(outputString))
        {
            _taskStatus = kTaskStatus_WaitingForPassword;
            [self notifyDelegateEncryptionPasswordIsRequired];

            return;
        }

        // beginning of conversion
        if (DAA2ISOTaskOutput_HasConversionBegun(outputString))
            _taskStatus = kTaskStatus_Converting;
    }

    // error message (must go after checks for new source file & unknown DAA version)
    if (DAA2ISOTaskOutput_HasErrorMessage(outputString, &errorMessage))
    {
        // if the archive has an unknown DAA version, assume that's the cause of the error
        if (_archiveHasUnknownDAAVersion)
            errorMessage = kErrorMessage_UnknownDAAVersion;

        [self failWithErrorMessage: errorMessage];

        return;
    }

    if (_taskStatus == kTaskStatus_Converting) // conversion has begun
    {
        // conversion progress
        if (DAA2ISOTaskOutput_HasConversionProgress(outputString, &newCompletionPercentage)
            && (newCompletionPercentage > _conversionCompletionPercentage))
        {
            _conversionCompletionPercentage = newCompletionPercentage;

            [self notifyDelegateDidProgress];
        }

        // conversion completion
        if ((_conversionCompletionPercentage == 100)
            && DAA2ISOTaskOutput_HasConversionFinished(outputString))
        {
            [self finishDAA2ISOTaskWithSuccess: YES];
            [self notifyDelegateDidCompleteSuccessfully];
        }
    }
}

- (void) writeEncryptionPasswordToDAA2ISOTaskInput: (NSString *) password
{
    NSData *passwordData;
    NSFileHandle *daa2isoTaskInputFileHandle;

    // the task won't process the input until it receives a linefeed, so append one to the end
    password = [password stringByAppendingString: @"\n"];

    if (!password)
        password = @"\n";

    passwordData = [password dataUsingEncoding: NSUTF8StringEncoding];
    daa2isoTaskInputFileHandle = [_daa2isoTaskInputPipe fileHandleForWriting];

    if (!passwordData || !daa2isoTaskInputFileHandle)
        goto ERROR;

    [daa2isoTaskInputFileHandle writeData: passwordData];

    if (_taskStatus == kTaskStatus_WaitingForPassword)
        _taskStatus = kTaskStatus_HasNotBegunConverting;

    return;

ERROR:
    [self failWithErrorMessage: kErrorMessage_PasswordError];
}

#pragma mark Delegate Notifiers

- (void) notifyDelegateDidCompleteSuccessfully
{
    if ([_delegate respondsToSelector: @selector(conversionTaskCompletedSuccessfully:)])
    {
        [[self retain] autorelease];    // delays deallocation if delegate releases

        [_delegate conversionTaskCompletedSuccessfully: self];
    }
}

- (void) notifyDelegateDidFailOnDAAFile: (NSString *) filename
                        withErrorMessage: (NSString *) errorMessage
{
    if ([_delegate
            respondsToSelector: @selector(conversionTask:failedOnDAAFile:withErrorMessage:)])
    {
        [[self retain] autorelease];    // delays deallocation if delegate releases

        [_delegate conversionTask: self
                failedOnDAAFile: filename
                withErrorMessage: errorMessage];
    }
}

- (void) notifyDelegateDidOpenDAAFile
{
    if ([_delegate respondsToSelector: @selector(conversionTask:openedDAAFile:)])
    {
        [[self retain] autorelease];    // delays deallocation if delegate releases

        [_delegate conversionTask: self openedDAAFile: _currentDAAFilename];
    }
}

- (void) notifyDelegateDidProgress
{
    if ([_delegate respondsToSelector: @selector(conversionTask:didProgress:)])
    {
        [[self retain] autorelease];    // delays deallocation if delegate releases

        [_delegate conversionTask: self didProgress: _conversionCompletionPercentage];
    }
}

- (void) notifyDelegateEncryptionPasswordIsRequired
{
    if ([_delegate respondsToSelector: @selector(conversionTaskRequiresEncryptionPassword:)])
    {
        [[self retain] autorelease];    // delays deallocation if delegate releases

        [_delegate conversionTaskRequiresEncryptionPassword: self];
    }
}

@end
