/*
   ODBCAdaptor.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"

//
// The unique ODBCEnvironment variable. Shared by
// all the instances of ODBCAdaptor
//
static HENV odbcEnvironment = NULL;

@implementation ODBCAdaptor

+ (void)initialize
{
    //
    // Initialize once for all the odbcEnvironment variable
    //
    if (!odbcEnvironment && SQLAllocEnv(&odbcEnvironment) != SQL_SUCCESS)
        [NSException raise:EOGeneralAdaptorException format:@"Unable to allocate ODBC environment"];
}

- initWithName:(NSString *)name
{
    //
    // This method does nothing. I kept it here
    // only as a reminder: this is the designated 
    // initializer
    //
    [super initWithName:name];
    return self;
}

- (EOAdaptorContext *)createAdaptorContext
{
    return [[[ODBCContext allocWithZone:[self zone]] initWithAdaptor:self] autorelease];
}

- (Class)defaultExpressionClass
{
    return [ODBCSQLExpression class];
}

- (BOOL)isValidQualifierType:(NSString *)typeName model:(EOModel *)model
{
    NSDictionary *dict = [ODBCAdaptor typeInfoForModel:model];
    NSDictionary *typeInfo;

    typeInfo = [dict objectForKey:typeName];

    if(!typeInfo) {
        return NO;
    }

    return [[typeInfo objectForKey:@"isSearchable"] isEqualToString:@"YES"];
}

- (void)assertConnectionDictionaryIsValid
{
    // If there is already an open channel, there is no need to
    // retest the connection
    //
    if(![self hasOpenChannels]) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        ODBCContext *context = [self createAdaptorContext];
        ODBCChannel *channel = [context createAdaptorChannel];
        
        [channel openChannel];
        [channel closeChannel]; // Just try to connect to the database

        [pool release];
        // The channel is autoreleased, so everything should go away
        // with the pool
    }
}

- (NSString *)odbcConnectionString
{
    NSString *userName, *password, *dataSource, *connectionString;

    //
    // If there is a connection string in the dictionary, just use it.
    //
    if ((connectionString = [_connectionDictionary objectForKey:connectionStringKey]) != nil) {
        return connectionString;
    }

    //
    // Get connection information 	
    //
    dataSource = [_connectionDictionary objectForKey:dataSourceKey];
    userName = [_connectionDictionary objectForKey:userNameKey];
    password = [_connectionDictionary objectForKey:passwordKey];

    // construct connection string
    return [NSString stringWithFormat:@"DSN=%@;UID=%@;PWD=%@", dataSource ? dataSource: @"", userName ? userName : @"", password ? password : @""];
}

- (void *)odbcEnvironment { return odbcEnvironment; }

@end

@implementation ODBCAdaptor (ODBCTypeMapping)

+ (NSArray *)externalTypesWithModel:(EOModel *)model
{
    //
    // Just extract the information from the model file
    //
    return [[ODBCAdaptor typeInfoForModel:model] allKeys];
}


//
// The following table describe, for every ODBC type, the preferred
// corresponding Foundation class.
//
typedef struct {
    int odbcType;
    NSString *odbcTypeString;
    NSString *internalType;
    NSString *valueType; // For NSNumber, mostly
} typeStruct;

static typeStruct defaultCorrespondingTypes[] = {
    { SQL_VARCHAR,       @"VARCHAR",        @"NSString", nil },
    { SQL_CHAR,          @"CHAR",           @"NSString", nil },
    { SQL_LONGVARCHAR,   @"LONG VARCHAR",   @"NSString", nil },
    { SQL_DECIMAL,       @"DECIMAL",        @"NSDecimalNumber", nil },
    { SQL_NUMERIC,       @"NUMERIC",        @"NSDecimalNumber", nil },
    { SQL_BIGINT,        @"BIGINT",         @"NSNumber", @"l" }, // long L
    { SQL_SMALLINT,      @"SMALLINT",       @"NSNumber", @"s" }, // short S
    { SQL_INTEGER,       @"INTEGER",        @"NSNumber", @"i" }, // integer I
    { SQL_REAL,          @"REAL",           @"NSNumber", @"f" }, // float
    { SQL_FLOAT,         @"FLOAT",          @"NSNumber", @"d" }, // double
    { SQL_DOUBLE,        @"DOUBLE",         @"NSNumber", @"d" }, // double
    { SQL_BIT,           @"BIT",            @"NSNumber", @"c" }, // char
    { SQL_TINYINT,       @"TINYINT",        @"NSNumber", @"c" }, // char C
    { SQL_VARBINARY,     @"VARBINARY",      @"NSData", nil },
    { SQL_BINARY,        @"BINARY",         @"NSData", nil },
    { SQL_LONGVARBINARY, @"LONG VARBINARY", @"NSData", nil },
    { SQL_TIMESTAMP,     @"TIMESTAMP",      @"NSCalendarDate", nil },
    { SQL_DATE,          @"DATE",           @"NSCalendarDate", nil },
    { SQL_TIME,          @"TIME",           @"NSCalendarDate", nil },
    { SQL_TYPE_NULL, nil, nil}
};

//
// Possible optimisation here ! Use two map tables to hash the name to SQL
// and vice versa.
//
+ (NSString *)stringRepresentationForOdbcType:(int)type
{
    unsigned i = 0;
    while(defaultCorrespondingTypes[i].odbcType != SQL_TYPE_NULL) {
        if(defaultCorrespondingTypes[i].odbcType == type) {
           return defaultCorrespondingTypes[i].odbcTypeString;
        }
        i += 1;
    }

    return nil;
}

+ (int)odbcTypeForStringRepresentation:(NSString *)type
{
    unsigned i = 0;

    while(defaultCorrespondingTypes[i].odbcType != SQL_TYPE_NULL) {
        if([defaultCorrespondingTypes[i].odbcTypeString isEqualToString:type])
            return defaultCorrespondingTypes[i].odbcType;
        i += 1;
    }

    return SQL_TYPE_NULL;
}

+ (NSString *)odbcTypeForExternalType:(NSString *)extType model:(EOModel *)model

{
    NSDictionary *dict = [ODBCAdaptor typeInfoForModel:model];
    NSDictionary *typeInfo;

    //
    // Extract the relevant typeInfo from the model
    //
    typeInfo = [dict objectForKey:extType];
    if(!typeInfo) {
        return nil;
    }

    //
    // The first item of the list is the prefered one
    //
     return [[typeInfo objectForKey:@"defaultODBCType"] objectAtIndex:0];
}

+ (NSString *)internalTypeForExternalType:(NSString *)extType model:(EOModel *)model
{
    NSString *odbcType;
    unsigned int i = 0;

    odbcType = [self odbcTypeForExternalType:extType model:model];
    //
    // Search for it in my table.
    //
    while(defaultCorrespondingTypes[i].internalType) {
        if([defaultCorrespondingTypes[i].odbcTypeString isEqualToString:odbcType]) {
            return defaultCorrespondingTypes[i].internalType;
        }
    }

    return nil;
}

+ (NSString *)externalTypeForOdbcType:(int)type model:(EOModel *)model;
{
    NSString *odbcType = nil;
    unsigned i = 0, c;
    NSDictionary *typeInfo;
    NSArray *allKeys;
    NSString *best = nil;
    unsigned position = UINT_MAX;
    
    //
    // First, get the string representation for the odbc type
    //
    odbcType = [self stringRepresentationForOdbcType:type];
    if(!odbcType) return nil;

    //
    // Get the infoType dictionary from the model
    //
    typeInfo = [ODBCAdaptor typeInfoForModel:model];
    if(!typeInfo) return nil;;

    //
    // Look everywhere, and return the best one. Or nil if not found
    //
    allKeys = [typeInfo allKeys];
    for(i = 0, c = [allKeys count]; i < c; i++) {
        NSString *key = [allKeys objectAtIndex:i];
        NSDictionary *currentTypeInfo = [typeInfo objectForKey:key];
        NSArray *defaultODBCType = [currentTypeInfo objectForKey:@"defaultODBCType"];
        unsigned j,k;

        k = [defaultODBCType count];
        if(k > position) k = position + 1;
        for(j = 0; j < k ; j++) {
            NSString *type = [defaultODBCType objectAtIndex:j];
            if([type isEqualToString:odbcType]) {
                best = key;
                position = j;
                break;
            }
        }
        // if(position == 0) break;
    }
    return best;
}

+ (void)assignExternalInfoForAttribute:(EOAttribute *)attribute
{
    int sqlTypeArray[20];
    unsigned i = 0;
    NSString *externalType = nil;
    EOModel *model = [[attribute parent] model];
    
    //
    // Set the column name from the attribute name
    //
    [super assignExternalInfoForAttribute:attribute];

    //
    // Set the ODBC types we want to use prioritized.
    //
    switch ([attribute adaptorValueType]) {
      case EOAdaptorNumberType:
          if([attribute scale] != 0) {
              sqlTypeArray[i++] = SQL_DECIMAL;
              sqlTypeArray[i++] = SQL_NUMERIC;
              sqlTypeArray[i++] = SQL_REAL;
              sqlTypeArray[i++] = SQL_FLOAT;
              sqlTypeArray[i++] = SQL_DOUBLE;
          } else {
              // sqlTypeArray[i++] = SQL_BIT; // Useless
              // sqlTypeArray[i++] = SQL_TINYINT;
              // sqlTypeArray[i++] = SQL_SMALLINT;
              sqlTypeArray[i++] = SQL_INTEGER;
              sqlTypeArray[i++] = SQL_BIGINT;
              sqlTypeArray[i++] = SQL_DECIMAL;
              sqlTypeArray[i++] = SQL_NUMERIC;
              sqlTypeArray[i++] = SQL_REAL;
              sqlTypeArray[i++] = SQL_FLOAT;
              sqlTypeArray[i++] = SQL_DOUBLE;
          }
          break;

      case EOAdaptorDateType:
          sqlTypeArray[i++] = SQL_TIMESTAMP;
          // If there is no TIMESTAMP stuff, store it in a string
          break;
          
      case EOAdaptorBytesType:
          if([attribute width]) {
              sqlTypeArray[i++] = SQL_BINARY;
              sqlTypeArray[i++] = SQL_VARBINARY;
              sqlTypeArray[i++] = SQL_LONGVARBINARY;
          } else {
              sqlTypeArray[i++] = SQL_LONGVARBINARY;
              sqlTypeArray[i++] = SQL_VARBINARY;
              sqlTypeArray[i++] = SQL_BINARY;
          }
          break;

      case EOAdaptorCharactersType:
          if([attribute width]) {
              sqlTypeArray[i++] = SQL_CHAR;
              sqlTypeArray[i++] = SQL_VARCHAR;
              sqlTypeArray[i++] = SQL_LONGVARCHAR;
          } else {
              sqlTypeArray[i++] = SQL_LONGVARCHAR;
          }

          break;
    }
    // By default store it in a string. Since a driver must provide at least
    // CHAR and VARCHAR, we are going to find at least one matching type.
    sqlTypeArray[i++] = SQL_VARCHAR;
    sqlTypeArray[i++] = SQL_CHAR;
    sqlTypeArray[i++] = SQL_LONGVARCHAR;

    sqlTypeArray[i++] = SQL_TYPE_NULL;
    
    i = 0;
    while(sqlTypeArray[i] != SQL_TYPE_NULL) {
        NSDictionary *currentTypeInfo = nil;
        
        // Search an external type matching the ODBC type
        externalType = [self externalTypeForOdbcType:sqlTypeArray[i] model:model];

        currentTypeInfo = [[ODBCAdaptor typeInfoForModel:model] objectForKey:externalType];

        if(externalType) {
            // check precision and width and scales...
            // Reject it if it does not fit
            unsigned attPrecision;
            int attScale;
            NSString *typePrecision; // Precision or width
            NSString *typeMaxScale;
            NSString *typeMinScale;
            BOOL found = NO;
            
            typePrecision = [currentTypeInfo objectForKey:@"precision"];
            typeMaxScale = [currentTypeInfo objectForKey:@"maxScale"];
            typeMinScale = [currentTypeInfo objectForKey:@"minScale"];
            
            // If precision is undefined, just accept the type as-is
            if(!typePrecision) {
                found = YES; // Unlikely to happens
            } else {
                switch ([attribute adaptorValueType]) {
                    case EOAdaptorNumberType:
                        attPrecision = [attribute precision];
                        if(attPrecision > [typePrecision intValue]) {
                            break; // Go to the next one
                        }
                        if(!typeMinScale || !typeMaxScale) {
                            found = YES;
                            break;
                        }
                        attScale = [attribute scale];
                        if(attScale <= [typeMaxScale intValue] &&
                           attScale >= [typeMinScale intValue]) {
                            found = YES;
                        }
                        break;

                    case EOAdaptorCharactersType:
                    case EOAdaptorBytesType:
                        attPrecision = [attribute width];
                        if(attPrecision <= [typePrecision intValue]) {
                            found = YES;
                        }
                        break;

                    case EOAdaptorDateType:
                        found = YES;
                        break;
                }

            }

            if(found) break;
        }
        i++;
    }
    
    [attribute setExternalType:externalType];
}

+ (void)assignExternalInfoForEntireModel:(EOModel *)model
{
    NS_DURING
        [model setConnectionDictionary:[ODBCAdaptor resetOdbcInfoWithConnectionDictionary:[model connectionDictionary]]];
        [super assignExternalInfoForEntireModel:model];
    NS_HANDLER
        NSLog(@"Unable to convert the model. If you retry it could succeed...\n%@", localException);
    NS_ENDHANDLER
}

@end

@implementation ODBCAdaptor(_ODBCPrivate)

// In this method there is one thing to do:
// Check for the unsigned/signed crap.

- (EOAttribute *)attributeWithName:(NSString *)name columnName:(NSString *)columnName externalType:(NSString *)externalType odbcType:(int)odbcType length:(int)length precision:(int)precision scale:(int)scale nullable:(BOOL)nullable
{
    unsigned i = 0;
    EOAttribute *attribute = [[[EOAttribute alloc] init] autorelease];
    [attribute setName:name];
    [attribute setColumnName:columnName];

    [attribute setExternalType:externalType];

    while(defaultCorrespondingTypes[i].odbcType != odbcType) {
        i += 1;
    }

    [attribute setValueClassName:defaultCorrespondingTypes[i].internalType];
    [attribute setValueType:defaultCorrespondingTypes[i].valueType];

    switch([attribute adaptorValueType]) {
        case EOAdaptorNumberType:
            [attribute setPrecision:precision];
            [attribute setScale:scale];
            [attribute setWidth:0];
            break;
        case EOAdaptorDateType:
            [attribute setWidth:0];
            break;
        case EOAdaptorCharactersType:
        case EOAdaptorBytesType:
            [attribute setWidth:length];
            break;
    }

    [attribute setAllowsNull:nullable];
    return attribute;
}

@end

@implementation ODBCAdaptor(ODBCInfoAccessors)

+ (NSDictionary *)getOdbcInfoWithConnectionDictionary:(NSDictionary *)connectionDictionary
{
    NSAutoreleasePool *pool = [NSAutoreleasePool new];
    ODBCAdaptor *adaptor = [[[self alloc] initWithName:@"ODBCAdaptor"] autorelease];
    ODBCContext *context;
    ODBCChannel *channel;
    NSMutableDictionary *result;

    [adaptor setConnectionDictionary:connectionDictionary];
    context = (ODBCContext *)[adaptor createAdaptorContext];
    channel = (ODBCChannel *)[context createAdaptorChannel] ;

    result = [[connectionDictionary mutableCopy] autorelease];

    NS_DURING
        [context odbcConnect];
        [result setObject:[context odbcDriverInfo] forKey:driverInfoKey];
        [result setObject:[channel odbcTypeInfo] forKey:typeInfoKey];
        [context odbcDisconnect];
    NS_HANDLER
        // Setup two empty dictionaries in the result
        if(![result objectForKey:driverInfoKey])
            [result setObject:[NSDictionary dictionary] forKey:driverInfoKey];

        if(![result objectForKey:typeInfoKey])
            [result setObject:[NSDictionary dictionary] forKey:typeInfoKey];

    NS_ENDHANDLER

    //
    // Necessary because dealloc on the context need to access
    // the driver info dictionary.
    // We cannot use setConnectionDictionnary here, because it's raising
    // if a connection is open...
    //
    [adaptor->_connectionDictionary autorelease];
    adaptor->_connectionDictionary = [result retain];

    [result retain];
    
    [pool release]; // the adaptor/context/channel should all go away here.

    return [result autorelease];
}

+ (NSDictionary *)resetOdbcInfoWithConnectionDictionary:(NSDictionary *)connectionDictionary
{
    if([connectionDictionary objectForKey:driverInfoKey] || [connectionDictionary objectForKey:typeInfoKey]) {
        NSMutableDictionary *newDict = [[connectionDictionary mutableCopy] autorelease];

        [newDict removeObjectForKey:driverInfoKey];
        [newDict removeObjectForKey:typeInfoKey];
        
        return newDict;
    } else {
        return connectionDictionary;
    }
}

+ (NSDictionary *)typeInfoForModel:(EOModel *)model
{
    NSDictionary *typeInfo;

    typeInfo = [[model connectionDictionary] objectForKey:typeInfoKey];

    if(!typeInfo) {
        [model setConnectionDictionary:[ODBCAdaptor getOdbcInfoWithConnectionDictionary:[model connectionDictionary]]];
        typeInfo = [[model connectionDictionary] objectForKey:typeInfoKey];
    }
    return typeInfo;
}

+ (NSDictionary *)driverInfoForModel:(EOModel *)model
{
    NSDictionary *driverInfo;

    driverInfo = [[model connectionDictionary] objectForKey:driverInfoKey];

    if(!driverInfo) {
        [model setConnectionDictionary:[ODBCAdaptor getOdbcInfoWithConnectionDictionary:[model connectionDictionary]]];
        driverInfo = [[model connectionDictionary] objectForKey:driverInfoKey];
    }
    return driverInfo;
}

- (NSDictionary *)typeInfo
{
    NSDictionary *typeInfo = [_connectionDictionary objectForKey:typeInfoKey];

    if(!typeInfo) {
        // I cannot use the accessor -setConnectionDictionary: because
        // there is a raise if a channel is open...
        //
        _connectionDictionary = [[ODBCAdaptor getOdbcInfoWithConnectionDictionary:[_connectionDictionary autorelease]] retain];
        typeInfo = [_connectionDictionary objectForKey:typeInfoKey];
    }
    return typeInfo;
}

- (NSDictionary *)driverInfo
{
    NSDictionary *driverInfo = [_connectionDictionary objectForKey:driverInfoKey];

    if(!driverInfo) {
        // I cannot use the accessor -setConnectionDictionary: because
        // there is a raise if a channel is open...
        //
        _connectionDictionary = [[ODBCAdaptor getOdbcInfoWithConnectionDictionary:[_connectionDictionary autorelease]] retain];
        driverInfo = [_connectionDictionary objectForKey:driverInfoKey];
    }
    return driverInfo;
}

@end
