/*
   ODBCContext.m

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

   IMPORTANT:  This NeXT software is supplied to you in consideration of your agreement
   to the terms of the NeXT Software License Agreement stated in the file ODBC_LICENSE.rtf
   provided with the software.  Your use of the software is governed by such terms.
   Do not use, install, or make copies of the software if you do not agree to such terms.

   THIS SOFTWARE IS FURNISHED ON AN "AS-IS" BASIS. NeXT MAKES NO WARRANTIES OF ANY KIND,
   EITHER EXPRESS OR IMPLIED, AS TO ANY MATTER WHATSOEVER, INCLUDING WITHOUT LIMITATION
   THE CONDITION, MERCHANTABILITY, OR FITNESS FOR ANY PARTICULAR PURPOSE OF THIS SOFTWARE.
   NeXT DOES NOT ASSUME ANY LIABILITY REGARDING USE OF, OR ANY DEFECT IN, THIS SOFTWARE.
   IN NO EVENT SHALL NeXT BE LIABLE FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL
   DAMAGES, EVEN IF IT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#import "ODBCPrivate.h"

@implementation ODBCContext(_ODBCErrorNotification)

- (void)odbcErrorWithChannel:(ODBCChannel *)channel string:(NSString *)location raise:(BOOL)flag
{
    HENV henv   = [(ODBCAdaptor *)_adaptor odbcEnvironment];
    HDBC hdbc   = _odbcDatabaseConnection;
    HSTMT hstmt = (channel ? [channel odbcStatement]: SQL_NULL_HSTMT);
    UCHAR sqlState[SQL_SQLSTATE_SIZE+1];
    SDWORD nativeError;    
    UCHAR errorMsg[SQL_MAX_MESSAGE_LENGTH + 1];
    SWORD errorMsgMax = SQL_MAX_MESSAGE_LENGTH - 1;
    SWORD msgLength;
    RETCODE retCode;
    NSMutableString *returnString = [NSMutableString string];

    [returnString appendString:location];
    
    if(!henv) henv = SQL_NULL_HENV;
    if(!hdbc) hdbc = SQL_NULL_HDBC;

    while(1) { // Break or return will exit the loop
        retCode = SQLError(henv, hdbc, hstmt, sqlState, &nativeError, errorMsg, errorMsgMax, &msgLength);

        if(retCode == SQL_NO_DATA_FOUND) {
            // No more errors.
            break;
        } else if (retCode == SQL_ERROR) {
            [returnString appendString:@" - Unable to access the ODBC error description"];
            break;
        } else if (retCode == SQL_INVALID_HANDLE) {
            [returnString appendString:@" - General ODBC adaptor Failure"];
            break;
        } else if( (retCode == SQL_SUCCESS) || (retCode == SQL_SUCCESS_WITH_INFO) ) {
            [returnString appendFormat:@"\n%s-%d: %s", sqlState, nativeError, errorMsg];
        }
    }

    if(flag) {
        // We raise here
        [NSException raise:EOGeneralAdaptorException format:@"%@", returnString];
    }

    if(_debug) {
        NSLog(@"ODBC INFO: %@", returnString);
    }
}

@end

@implementation ODBCContext

- initWithAdaptor:(EOAdaptor *)anAdaptor
{
    [super initWithAdaptor:anAdaptor];
    _odbcDatabaseConnection = NULL;
    _openChannelCount = 0;
    _isConnected = NO;
    _fetchesInProgress = 0;
    return self;
}

- (BOOL)_driverCrashOnFreeConnect
{
    NSDictionary *driverInfo = [(ODBCAdaptor *)_adaptor driverInfo];
    NSString *driverName = [driverInfo objectForKey:@"DRIVER_NAME"];
    BOOL isBogus = NO;

    if([driverName isEqual:@"odbcjt32.dll"]) {
        // I did not check all the versions, but the version
        // 3.40.2829 of this dll is not working.
        // In doubt, let say that they are all bogus.
        //
        isBogus = YES;
    }
    
    if(_debug && isBogus) NSLog(@"Warning: Leaking the database connection due to a bug in the ODBC driver.");

    return isBogus;
}

- (void)dealloc
{
    RETCODE result;

    if(_isConnected) [self odbcDisconnect];

    //
    // The Microsoft Jet engine is crashing on SQLFreeConnect().
    // This engine is used by the following ODBC drivers: Access, FoxPro, dBase, text, Excel and Paradox...
    // If we are using one of these drivers, we just leak the connection.
    //
    if([self _driverCrashOnFreeConnect]) {
        result = SQL_SUCCESS;
    } else {
        result = SQLFreeConnect(_odbcDatabaseConnection);
    }
    
    _odbcDatabaseConnection = NULL;

    if ( result != SQL_SUCCESS ) {
        [self odbcErrorWithChannel:nil string:@"SQLFreeConnect in -[ODBCContext odbcDisconnect]" raise: (result != SQL_SUCCESS_WITH_INFO)];
    }

    [super dealloc];
}

- (void)odbcConnect
{
    RETCODE result;
    HENV odbcEnvironmentHandle = [(ODBCAdaptor *)_adaptor odbcEnvironment];
    UCHAR *connectionString;
    UCHAR connectionStringOut[1024];
    SWORD connectionStringOutLength;

    //
    // If already connected then return
    //
    if (_isConnected) return;

    if (_delegateRespondsTo.shouldConnect) {
        if (![_delegate adaptorContextShouldConnect:self]) {
            return;
        }
    }
    
    //
    // Do a simple check on the connection dictionnary
    //
    if (![_adaptor connectionDictionary])
        [NSException raise:EOGeneralAdaptorException format:@"Unable to connect to server"];

    //
    // Allocate the connnection if it's the first connect call
    //
    if(!_odbcDatabaseConnection) {
        result = SQLAllocConnect (odbcEnvironmentHandle, &_odbcDatabaseConnection);

        if ( result != SQL_SUCCESS ) {
            [self odbcErrorWithChannel:nil string:@"SQLAllocConnect in -[ODBCContext odbcConnect]" raise: (result != SQL_SUCCESS_WITH_INFO)];
        }
    }

    //
    // Connect to the driver. Note: SQLDriverConnect is not supported by all the drivers...
    //
    connectionString = (UCHAR *)[[(ODBCAdaptor *)_adaptor odbcConnectionString] cString];

    if(_debug) NSLog(@"connect with: %s", connectionString);
    
    result = SQLDriverConnect (_odbcDatabaseConnection, NULL, connectionString, strlen(connectionString), connectionStringOut, sizeof(connectionStringOut), &connectionStringOutLength, SQL_DRIVER_NOPROMPT);

    if ( result != SQL_SUCCESS ) {
            [self odbcErrorWithChannel:nil string:@"SQLDriverConnect in -[ODBCContext odbcConnect]" raise: (result != SQL_SUCCESS_WITH_INFO)];
    }
    _isConnected = YES;

}

- (void)odbcDisconnect
{
    RETCODE result;

    //
    // If already disconected then return
    //
    if (!_isConnected) return;

    //
    // Check the current transaction count
    //
    if (_transactionNestingLevel){
        [NSException raise:NSInternalInconsistencyException format:@"%@ -- %@ 0x%x: attempt to disconnect while a transaction was in progress", NSStringFromSelector(_cmd), NSStringFromClass([self class]), self];
    }

    //
    // Disconnect from the datasource and free the handle.
    // NOTE: Some drivers don't support SQLDisconnect...
    //
    if(_debug) NSLog(@"disconnect");


    result = SQLDisconnect(_odbcDatabaseConnection);

    if ( result != SQL_SUCCESS ) {
        [self odbcErrorWithChannel:nil string:@"SQLDisconnect in -[ODBCContext odbcDisconnect]" raise: (result != SQL_SUCCESS_WITH_INFO)];
    }
    _isConnected = NO;
    
}

- (void)beginTransaction
{
    //
    // If not connected then raise exception
    //
    if (!_isConnected) {
        [NSException raise:NSInternalInconsistencyException format:@"%@ -- %@ 0x%x: attempted to begin a transaction without connecting", NSStringFromSelector(_cmd), NSStringFromClass([self class]), self];
    }

    //
    // If within a transaction then raise exception
    //
    if (_transactionNestingLevel) {
        [NSException raise:NSInternalInconsistencyException format:@"%@ -- %@ 0x%x: attempted to begin a transaction within a transaction", NSStringFromSelector(_cmd), NSStringFromClass([self class]), self];
    }

    //
    // Notify the delegate with should
    //
    if (_delegateRespondsTo.shouldBegin) {
        if(![_delegate adaptorContextShouldBegin:self])
            return;
    }

    //
    // This is a little bit of a lie, since it does nothing.  In ODBC, the
    // transaction begins at login time, or after a commit or rollback.
    //
    [self transactionDidBegin];

    //
    // Notify the delegate with did
    //
    if (_delegateRespondsTo.didBegin)
        [_delegate adaptorContextDidBegin:self];
}

- (void)commitTransaction
{
    RETCODE result;

    //
    // If not within a traction then raise execption
    //
    if (!_transactionNestingLevel) {
        [NSException raise:NSInternalInconsistencyException format:@"%@ -- %@ 0x%x: attempted to commit a transaction without beginning one", NSStringFromSelector(_cmd), NSStringFromClass([self class]), self];
    }

    //
    // If already fetching then raise exception
    //
    if (_fetchesInProgress) {
        [NSException raise:NSInternalInconsistencyException format:@"%@ -- %@ 0x%x: attempted to commit a transaction while a fetch was in progress", NSStringFromSelector(_cmd), NSStringFromClass([self class]), self];
    }

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

    //
    // Commit the transaction
    //
    result = SQLTransact(SQL_NULL_HENV, _odbcDatabaseConnection, SQL_COMMIT);

    if ( result != SQL_SUCCESS ) {
        [self odbcErrorWithChannel:nil string:@"SQLTransact in -[ODBCContext commitTransaction]" raise: (result != SQL_SUCCESS_WITH_INFO)];
    }

    [self transactionDidCommit];

    //
    // Notify the delegate
    //
    if (_delegateRespondsTo.didCommit)
        [_delegate adaptorContextDidCommit:self];
}

- (void)rollbackTransaction
{
    RETCODE result;

    //
    // If not within transaction then raise exception
    //
    if (!_transactionNestingLevel) {
        [NSException raise:NSInternalInconsistencyException format:@"%@ -- %@ 0x%x: attempted to rollback a transaction without beginning one", NSStringFromSelector(_cmd), NSStringFromClass([self class]), self];
    }

    //
    // If already fetching the raise exception
    //
    if (_fetchesInProgress) {
        [NSException raise:NSInternalInconsistencyException format:@"%@ -- %@ 0x%x: attempted to rollback a transaction while a fetch was in progress", NSStringFromSelector(_cmd), NSStringFromClass([self class]), self];
    }

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

    result = SQLTransact (SQL_NULL_HENV, _odbcDatabaseConnection, SQL_ROLLBACK);

    if ( result != SQL_SUCCESS ) {
        [self odbcErrorWithChannel:nil string:@"SQLTransact in -[ODBCContext rollbackTransaction]" raise: (result != SQL_SUCCESS_WITH_INFO)];
    }

    [self transactionDidRollback];

    //
    // Notify the delegate
    //
    if (_delegateRespondsTo.didRollback)
        [_delegate adaptorContextDidRollback:self];
}

- (BOOL)canNestTransactions
{
    return NO;
}

- (unsigned)transactionNestingLevel
{
    return _transactionNestingLevel;
}

- (EOAdaptorChannel *)createAdaptorChannel
{
    return [[[ODBCChannel allocWithZone:[self zone]] initWithAdaptorContext:self] autorelease];
}

- (void *)odbcDatabaseConnection
{
    return _odbcDatabaseConnection;
}

- (void)setOdbcDatabaseConnection:(void *)odbcDatabaseConnection
{
    _odbcDatabaseConnection = odbcDatabaseConnection;
    _isConnected = YES;
}

- (void)channelWillOpen
{
    if(!_openChannelCount) {
        [self odbcConnect];
    }
    _openChannelCount += 1;
}

- (void)channelDidClose
{
    if(_openChannelCount) _openChannelCount -= 1;
    if(!_openChannelCount) {
        [self odbcDisconnect];
    }
}

- (void)channelWillBeginFetching
{
    _fetchesInProgress++;
}

- (void)channelDidEndFetching
{
    _fetchesInProgress--;

}

- (NSString *)_getStringInfoForKey:(unsigned int)key
{
    RETCODE result;
    char charValue[1024];
    short lengthReturned;

    result = SQLGetInfo(_odbcDatabaseConnection, key , charValue, sizeof(charValue), &lengthReturned);

    if ( result != SQL_SUCCESS ) {
        [(ODBCContext *)self odbcErrorWithChannel:nil string:@"SQLGetInfo in -[ODBCContext odbcGetInfo]" raise: (result != SQL_SUCCESS_WITH_INFO)];
    }

    return [NSString stringWithCString:charValue];
}

- (unsigned short)_getShortInfoForKey:(unsigned int)key
{
    RETCODE result;
    unsigned short shortValue;

    result = SQLGetInfo(_odbcDatabaseConnection, key , &shortValue, sizeof(shortValue), NULL);

    if ( result != SQL_SUCCESS ) {
        [(ODBCContext *)self odbcErrorWithChannel:nil string:@"SQLGetInfo in -[ODBCContext odbcGetInfo]" raise: (result != SQL_SUCCESS_WITH_INFO)];
    }
    return shortValue;
}

- (NSDictionary *)odbcDriverInfo
{
    NSMutableDictionary *driverInfo = [NSMutableDictionary new];
    BOOL automaticConnection = NO;

    if (_debug) NSLog (@" *** [%@ odbcDriverInfo]\n", self);

    if(!_isConnected) {
        automaticConnection = YES;
        [self odbcConnect];
    }

    NS_DURING
        // Database name and version
        [driverInfo setObject:[self _getStringInfoForKey:SQL_DBMS_NAME] forKey:@"DBMS_NAME"];
    [driverInfo setObject:[self _getStringInfoForKey:SQL_DBMS_VER] forKey:@"DBMS_VER"];

    // ODBC Driver name and version
    [driverInfo setObject:[self _getStringInfoForKey:SQL_DRIVER_NAME] forKey:@"DRIVER_NAME"];
    [driverInfo setObject:[self _getStringInfoForKey:SQL_DRIVER_VER] forKey:@"DRIVER_VER"];

    // Version of ODBC supported by the driver
    [driverInfo setObject:[self _getStringInfoForKey:SQL_DRIVER_ODBC_VER] forKey:@"DRIVER_ODBC_VER"];

    // API Conformance level
    switch([self _getShortInfoForKey:SQL_ODBC_API_CONFORMANCE]) {
        case SQL_OAC_NONE:
            [driverInfo setObject:@"NONE" forKey:@"ODBC_API_CONFORMANCE"];
            break;

        case SQL_OAC_LEVEL1:
            [driverInfo setObject:@"LEVEL1" forKey:@"ODBC_API_CONFORMANCE"];
            break;

        case SQL_OAC_LEVEL2:
            [driverInfo setObject:@"LEVEL2" forKey:@"ODBC_API_CONFORMANCE"];
            break;
    }

    // SQL Conformance level
    switch([self _getShortInfoForKey:SQL_ODBC_SQL_CONFORMANCE]) {
        case SQL_OAC_NONE:
            [driverInfo setObject:@"MINIMUM" forKey:@"ODBC_SQL_CONFORMANCE"];
            break;

        case SQL_OAC_LEVEL1:
            [driverInfo setObject:@"CORE" forKey:@"ODBC_SQL_CONFORMANCE"];
            break;

        case SQL_OAC_LEVEL2:
            [driverInfo setObject:@"EXTENDED" forKey:@"ODBC_SQL_CONFORMANCE"];
            break;
    }
            
    
    // Support for NOT NULL in CREATE TABLE statement.
    if([self _getShortInfoForKey:SQL_NON_NULLABLE_COLUMNS] == SQL_NNC_NON_NULL) {
        [driverInfo setObject:@"Y" forKey:@"NON_NULLABLE_COLUMNS"];
    }

    NS_HANDLER
        if(automaticConnection) {
            [self odbcDisconnect];
        }
        [localException raise];
    NS_ENDHANDLER
    
    if(automaticConnection) {
        [self odbcDisconnect];
    }

    return driverInfo;
}


@end
