/*
        Copyright (c) 1996, NeXT Software, Inc.
        All rights reserved.

        You may freely copy, distribute and reuse the code in this example.
        NeXT disclaims any warranty of any kind, expressed or implied,
        as to its fitness for any particular use.
*/
#import "FlatFileContext.h"
#import "FlatFileAdaptor.h"
#import "FlatFileChannel.h"
#import <FileScanning.h>

#define DataKey           @"data"
#define ColumnInfoKey     @"infos"
#define ColumnIndexMapKey @"nameToIndexMap"
#define ColumnHeaderKey   @"columnHeaders"

@interface FlatFileContext (FlatFileContextPrivate)

- (NSMutableDictionary *)infoForTable:(NSString *)tableName;
- (NSMutableDictionary *)newInfoForTable:(NSString *)tableName;
- (NSDictionary *)columnInfoFromHeader:(char *)header length:(int)length;
- (NSMutableData *)newDataForTable:(NSString *)tableName;

- (void)stripHeadersFromData:(NSMutableData *)data;

- (void)saveDirtyTables;
- (void)dropDirtyTables;

@end

@implementation FlatFileContext

- initWithFlatFileAdaptor:(FlatFileAdaptor *)adaptor
{
    [super initWithAdaptor:adaptor];
    return self;
}

- (void)dealloc
{
    [tableNameToInfoMap release];
    [dirtyTables release];

    [super dealloc];
}

- (void)beginTransaction
{
    if ([self transactionNestingLevel]) {
        [NSException raise:NSInternalInconsistencyException format:@"%@ -- %@ 0x%x: attempted to begin a transaction within a transaction", NSStringFromSelector(_cmd), NSStringFromClass([self class]), self];
    }

    if (_delegateRespondsTo.shouldBegin) {
        if (![_delegate adaptorContextShouldBegin:self]) return;
    }

    // a normal adaptor would actually do something to begin the transaction here
    
    [self transactionDidBegin];

    if (_delegateRespondsTo.didBegin)
        [_delegate adaptorContextDidBegin:self];
}

- (void)commitTransaction
{
    if (![self transactionNestingLevel])
        [NSException raise:NSInternalInconsistencyException format:@"%@ -- %@ 0x%x:illegal attempt to commit a transaction when there are none in progress", NSStringFromSelector(_cmd), NSStringFromClass([self class]), self];

    if (_delegateRespondsTo.shouldCommit) {
        if (![_delegate adaptorContextShouldCommit:self]) return;
    }

    [self saveDirtyTables];
    [self transactionDidCommit];

    if (_delegateRespondsTo.didCommit)
        [_delegate adaptorContextDidCommit:self];
}

- (void)rollbackTransaction
{
    if (![self transactionNestingLevel])
        [NSException raise:NSInternalInconsistencyException format:@"%@ -- %@ 0x%x: attempted to commit a transaction when there are none in progress", NSStringFromSelector(_cmd), NSStringFromClass([self class]), self];

    if (_delegateRespondsTo.shouldRollback) {
        if (![_delegate adaptorContextShouldRollback:self]) return;
    }

    [self dropDirtyTables];
    [self transactionDidRollback];

    if (_delegateRespondsTo.didRollback)
        [_delegate adaptorContextDidRollback:self];
}

- (BOOL)canNestTransactions
{
    return NO;
}

- (EOAdaptorChannel *)createAdaptorChannel
{
    return [[[FlatFileChannel alloc] initWithFlatFileContext:self] autorelease];
}

- (BOOL)autoBeginTransaction {
    if (![self transactionNestingLevel]) {
        [self beginTransaction];
        didAutoBegin = YES;
        return YES;
    }
    return NO;
}

- (BOOL)autoCommitTransaction {
    if (didAutoBegin) {
        [self commitTransaction];
        didAutoBegin = NO;
        return YES;
    }
    return NO;
}

- (NSString *)path
{
    NSDictionary *dict = [[self adaptor] connectionDictionary];
    return [dict objectForKey:FlatFilePathKey];
}

- (NSString *)rowSeparator
{
    NSString *value = [[[self adaptor] connectionDictionary] objectForKey:FlatFileRowSeparatorKey];
    return value ? value : [FlatFileAdaptor defaultRowSeparator];
}

- (NSString *)columnSeparator
{
    NSString *value = [[[self adaptor] connectionDictionary] objectForKey:FlatFileColumnSeparatorKey];
    return value ? value : [FlatFileAdaptor defaultColumnSeparator];
}

- (BOOL)useColumnHeaders
{
    NSString *value = [[[self adaptor] connectionDictionary] objectForKey:FlatFileUseHeadersKey];
    return value ? [value isEqualToString:@"Y"] : YES;
}

- (NSMutableData *)dataForTable:(NSString *)tableName
{
    NSMutableDictionary *tableInfo = [self infoForTable:tableName];
    NSMutableData *data;

    data = [tableInfo objectForKey:DataKey];
    if (!data) {
        data = [self newDataForTable:tableName];
        [tableInfo setObject:data forKey:DataKey];
    }
    return data;
}

- (NSMutableData *)newDataForTable:(NSString *)tableName
{
    NSMutableData *data;
    NSString *fullPath;

    fullPath = [[self path] stringByAppendingPathComponent:tableName];
    data = [NSMutableData dataWithContentsOfFile:fullPath];

    if (!data)
        [NSException raise:EOGeneralAdaptorException format:@"can't read %@", fullPath];

    if ([self useColumnHeaders])
        [self stripHeadersFromData:data];

    return data;
}

- (NSArray *)columnInfosForTable:(NSString *)tableName
{
    return [[self infoForTable:tableName] objectForKey:ColumnInfoKey];
}

- (NSDictionary *)columnIndexMapForTable:(NSString *)tableName
{
    return [[self infoForTable:tableName] objectForKey:ColumnIndexMapKey];
}

- (NSMutableDictionary *)infoForTable:(NSString *)tableName
{
    NSMutableDictionary *tableInfo = [tableNameToInfoMap objectForKey:tableName];

    if (!tableInfo) {
        tableInfo = [self newInfoForTable:tableName];
        if (tableInfo) {
            if (!tableNameToInfoMap)
                tableNameToInfoMap = [[NSMutableDictionary new] retain];

            [tableNameToInfoMap setObject:tableInfo forKey:tableName];
        }
    }
    return tableInfo;
}

- (NSMutableDictionary *)newInfoForTable:(NSString *)tableName
{
    NSString *fullPath, *columnName;
    NSDictionary *columnInfo;
    NSMutableDictionary *tableInfo, *columnIndexMap;
    NSData *columnHeaders;
    NSMutableData *data;
    NSMutableArray *columnInfos;
    char *row, *currentHeader, *nextHeader, *end, *delimiter;
    int length, index = 0;
    BOOL useHeaders = [self useColumnHeaders];
    
    fullPath = [[self path] stringByAppendingPathComponent:tableName];
    data = [NSMutableData dataWithContentsOfFile:fullPath];

    NS_DURING;
    
    if (!data)
        [NSException raise:EOGeneralAdaptorException format:@"can't read %@", fullPath];

    row = (char *)[data bytes];
    end = row + [data length] - 1;
    delimiter = (char *)[[self rowSeparator] cString];

    nextHeader = FFNextTokenIn(row, &length, end, delimiter);

    if (!length)
        [NSException raise:EOGeneralAdaptorException format:@"can't derive metadata for %@", fullPath];

    if (useHeaders)
        columnHeaders = [NSData dataWithBytes:row length:(nextHeader) ? nextHeader - row : end - row + 1];

    end = row + length - 1;
    currentHeader = row;
    
    columnInfos = [NSMutableArray array];
    columnIndexMap = [NSMutableDictionary dictionary];
    delimiter = (char *)[[self columnSeparator] cString];
    
    while (currentHeader) {
        nextHeader = FFNextTokenIn(currentHeader, &length, end, delimiter);
        if (useHeaders) {
            columnInfo = [self columnInfoFromHeader:currentHeader length:length];
            columnName = [columnInfo objectForKey:ColumnNameKey];
        } else {
            columnName = [NSString stringWithFormat:@"Attribute%d", index];
            columnInfo = [NSDictionary dictionaryWithObjectsAndKeys:columnName, ColumnNameKey, @"String", ColumnTypeKey, nil];
        }
        [columnIndexMap setObject:[NSNumber numberWithInt:index++] forKey:columnName];
        [columnInfos addObject:columnInfo];

        currentHeader = nextHeader;
    }

    tableInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:data, DataKey, columnInfos, ColumnInfoKey, columnIndexMap, ColumnIndexMapKey, nil];

    if (useHeaders) {
        [tableInfo setObject:columnHeaders forKey:ColumnHeaderKey];
        [self stripHeadersFromData:data];
    }

    NS_HANDLER;
    tableInfo = nil;
    NS_ENDHANDLER;

    return tableInfo;
}

- (NSDictionary *)columnInfoFromHeader:(char *)header length:(int)length
{
    NSString *columnName, *columnType;
    char *nextToken, *end = header + length - 1, *delimiter;

    delimiter = (char *)[[FlatFileAdaptor columnHeaderSeparator] cString];
    nextToken = FFNextTokenIn(header, &length, end, delimiter);
    columnName = [NSString stringWithCString:header length:length];

    if (nextToken) {
        header = nextToken;
        FFNextTokenIn(header, &length, end, delimiter);
        columnType = [NSString stringWithCString:header length:length];
    } else
        columnType = [FlatFileAdaptor defaultExternalType];

    return [NSDictionary dictionaryWithObjectsAndKeys:columnName, ColumnNameKey, columnType, ColumnTypeKey, nil];
}

- (void)channel:(EOAdaptorChannel *)channel didDirtyTable:(NSString *)table
{
    if (!dirtyTables)
        dirtyTables = [[NSMutableSet new] retain];

    [dirtyTables addObject:table];
}

- (void)stripHeadersFromData:(NSMutableData *)data
{
    char *firstRow = [data mutableBytes], *nextRow, *end, *delimiter;
    int rowLength, dataLength = [data length];

    if (!dataLength)
        return;

    delimiter = (char *)[[self rowSeparator] cString];
    end = firstRow + dataLength - 1;
    nextRow = FFNextTokenIn(firstRow, &rowLength, end, delimiter);

    if (nextRow) {
        memmove(firstRow, nextRow, 1 + end - nextRow);
        [data setLength:dataLength - (nextRow - firstRow)];
    } else
        [data setLength:0];
}

- (void)saveDirtyTables
{
    NSDictionary *tableInfo;
    NSMutableData *fileData, *tableData;
    NSEnumerator *enumerator;
    NSString *path, *tableName, *fullPath, *rowSeparator;
    BOOL useHeaders;

    if (!dirtyTables)
        return;

    enumerator = [dirtyTables objectEnumerator];
    useHeaders = [self useColumnHeaders];
    rowSeparator = [self rowSeparator];
    path = [self path];

    while (tableName = [enumerator nextObject]) {
        tableInfo = [self infoForTable:tableName];
        tableData = [tableInfo objectForKey:DataKey];
        fullPath = [path stringByAppendingPathComponent:tableName];
        
        if (useHeaders) {
            fileData = [[NSMutableData alloc] init];
            [fileData appendData:[tableInfo objectForKey:ColumnHeaderKey]];
            if ([tableData length])
                [fileData appendData:[tableInfo objectForKey:DataKey]];
        } else
            fileData = tableData;

        if (![fileData writeToFile:fullPath atomically:YES])
            [NSException raise:EOGeneralAdaptorException format:@"can't write %@", fullPath];

        if (fileData != tableData)
            [fileData release];
    }

    [dirtyTables autorelease];
    dirtyTables = nil;
}

- (void)dropDirtyTables
{
    NSEnumerator *enumerator;
    NSString *tableName;

    if (!dirtyTables)
        return;

    enumerator = [dirtyTables objectEnumerator];

    while (tableName = [enumerator nextObject])
        [[self infoForTable:tableName] removeObjectForKey:DataKey];

    [dirtyTables autorelease];
    dirtyTables = nil;
}

@end
