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

#define ODBC_MAX_RETLEN  NSRoundDownToMultipleOfPageSize(65535)

@implementation ODBCColumn

static ODBCColumn *uniquePlaceHolderColumn = nil;

+ allocWithZone:(NSZone *)zone
{
    if ([self class] != [ODBCColumn class]) {
        return [super allocWithZone:zone];
    }

    if (!uniquePlaceHolderColumn) {
        uniquePlaceHolderColumn = [[super allocWithZone:zone] init];
    }
    
    return uniquePlaceHolderColumn;
}

+ (Class)columnClassForAttribute:(EOAttribute *)attribute
{
    switch ([attribute adaptorValueType]) {

        case EOAdaptorNumberType:
            return [ODBCNumberColumn class];

        case EOAdaptorCharactersType:
        case EOAdaptorBytesType:

            if(![attribute width])
                return [ODBCLongByteColumn class];

            if([attribute width] > ODBC_MAX_RETLEN)
                return [ODBCLongByteColumn class];

            return [ODBCByteColumn class];


        case EOAdaptorDateType:
            return [ODBCDateColumn class];
    }
    return [ODBCByteColumn class];
}

- initWithAttribute:(EOAttribute *)attribute channel:(ODBCChannel *)channel
{
    if (self == uniquePlaceHolderColumn) {
        Class realColumnClass = [ODBCColumn columnClassForAttribute:attribute];

        return [[realColumnClass alloc] initWithAttribute:attribute channel:channel];
    }

    //
    // Overide in subclass
    //
    _cType = SQL_C_CHAR;
    _valueLength = [attribute width];
    _returnedLength = SQL_NO_TOTAL;

    //
    // Used only if we are NOT binding
    //
    _column = 0;
    _statement = 0;

    //
    // Buffer to hold the data
    //
    _value = NULL;

    //
    // Cached info
    //
    _attribute = [attribute retain];
    _adaptorValueType = [attribute adaptorValueType];
    _encoding = [[[channel adaptorContext] adaptor] databaseEncoding];
    _channel = channel;
    return self;
}

- (void)dealloc
{
    [_attribute release];
    [self freeValue];
    [super dealloc];
}

- (void)allocateValue
{
    if(_value) {
        [NSException raise: EOGeneralAdaptorException format: @"-[ODBCColumn allocateValue]: memory already allocated"];
    }
    
    _value = NSZoneMalloc ([self zone], _valueLength + 1);

    if(!_value) {
        [NSException raise: EOGeneralAdaptorException format: @"-[ODBCColumn allocateValue]: Unable to allocate memory"];
    }
}

- (void)freeValue
{
    if(!_value) {
        [NSException raise: EOGeneralAdaptorException format: @"-[ODBCColumn freeValue]: Nothing to free !"];
    }

    NSZoneFree ([self zone], _value);
    _value = NULL;
}

- (BOOL)couldBind
{
    return YES;
}

- (void)connectToColumn:(unsigned)index ofStatement:(void *)statement useBinding:(BOOL)useBinding;
{
    if(useBinding && [self couldBind]) {
        [self bindToColumn:index ofStatement:statement];
    } else {
        // Allocate memory to bind and keep track of column
        if(!_value) [self allocateValue];
        _column = index;
        _statement = statement;
    }
}


- (void)bindToColumn:(unsigned)column ofStatement:(void *)statement
{
    RETCODE result;

    // Allocate memory to bind
    if(!_value) [self allocateValue];

    // bind memory to specified column
    result = SQLBindCol (statement, column, _cType, _value, _valueLength, &_returnedLength);

    if(result != SQL_SUCCESS) {
        if(result == SQL_SUCCESS_WITH_INFO) {
            [self freeValue];
        }
        [(ODBCContext *)[_channel adaptorContext] odbcErrorWithChannel:_channel string:@"SQLBindCol in -[ODBCColumn bindToColumn:ofStatement:]" raise:(result != SQL_SUCCESS_WITH_INFO)];
    }
}

- (id)buildValueFromSQLValue:(const void *)value length:(unsigned)length zone:(NSZone *)zone
{
    [NSException raise:EOGeneralAdaptorException format:@"-[ODBCColumn buildValueFromSQLValue:length:zone:] should be implemented by the subclass"];
    return nil;
}

- (id)fetchWithZone:(NSZone *)zone
{
    if(!_column) {
        // The column is bound to _value.
        return [self buildValueFromSQLValue:_value length:_returnedLength zone:zone];
    } else {
        RETCODE result;
        NSMutableData *data;
        id value;

        // Try to get first chunk of data
        _returnedLength = SQL_NO_TOTAL;
        result = SQLGetData(_statement, _column, _cType, _value, _valueLength, &_returnedLength);
        if(result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
            [(ODBCContext *)[_channel adaptorContext] odbcErrorWithChannel:_channel string:@"SQLGetData in -[ODBCLongByteColumn fetchWithZone:]" raise:YES];
        }

        // We need to make sure here, if we get SQL_SUCCESS_WITH_INFO,
        // that one of the reasons is REALLY 01004 (Data Truncated)

        
        if(_returnedLength == SQL_NULL_DATA) return [EONull null];

        // Check if we got the entire contents of the field on the first try
        if (result == SQL_SUCCESS) { // Test is bogus...
            return [self buildValueFromSQLValue:_value length:_returnedLength zone:zone];
        }

        // Allocate and initialize data with bytes
        data = [[NSMutableData alloc] initWithBytes:_value length:_returnedLength];

        // Keep on getting the rest of the data and appending it
        do {
            _returnedLength = SQL_NO_TOTAL;
            result = SQLGetData (_statement, _column, _cType, _value, _valueLength, &_returnedLength);
            if(result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO && result != SQL_NO_DATA_FOUND) {
                [data release];
                [(ODBCContext *)[_channel adaptorContext] odbcErrorWithChannel:_channel string:@"SQLGetData in -[ODBCLongByteColumn fetchWithZone:]" raise:YES];
            }
            [data appendBytes:_value length:_returnedLength];
        } while( result != SQL_SUCCESS && result != SQL_NO_DATA_FOUND);

        value = [self buildValueFromSQLValue:[data bytes] length:[data length] zone:zone];
        [data release];
        return value;
    }
}


- (void)bindAttribute:(EOAttribute *)attribute forInputColumn:(unsigned)column ofStatement:(void *)statement
{
    RETCODE result;
    int odbcType = [attribute odbcType];
    int precision = 0;
    
    switch(_adaptorValueType) {
        case EOAdaptorNumberType:
            precision = [attribute precision];
            break;
        case EOAdaptorCharactersType:
        case EOAdaptorBytesType:
            precision = [attribute width];
            break;
        case EOAdaptorDateType:
            // I have no clue !
            precision = 23; // whatever !
    }

    // Allocate memory to bind
    if(!_value) [self allocateValue];

    // bind memory to specified column
    result = SQLBindParameter(statement, column, SQL_PARAM_INPUT, _cType, odbcType , precision, [attribute scale], _value, _valueLength, &_returnedLength);

    if(result != SQL_SUCCESS) {
        if(result == SQL_SUCCESS_WITH_INFO) {
            [self freeValue];
        }
        [(ODBCContext *)[_channel adaptorContext] odbcErrorWithChannel:_channel string:@"SQLBindCol in -[ODBCColumn bindToColumn:ofStatement:]" raise: (result != SQL_SUCCESS_WITH_INFO)];
    }
}


- (void)takeInputValue:(id)value
{
    [NSException raise:EOGeneralAdaptorException format:@"takeInputValue: should be implemented by the subclass"];
}


@end

@implementation ODBCNumberColumn

- initWithAttribute:(EOAttribute *)attribute channel:(ODBCChannel *)channel
{
    [super initWithAttribute:attribute channel:channel];

    // Initialize value type and length
    if ([[attribute valueClassName] isEqualToString:@"NSDecimalNumber"]) {
        // we read decimal number as strings
        _cType = SQL_C_CHAR;
        _valueLength = (sizeof(char) * [attribute precision]) + 10; // sign, exponent, this kind of stuff...
    } else {
        switch ((unichar) [[attribute valueType] characterAtIndex:0]) {
          case 'c':
          case 's':
              //_cType = SQL_C_SSHORT;
              _cType = SQL_C_SHORT;
              //_odbcType = SQL_SMALLINT;
              _valueLength = sizeof (short);
              break;
          case 'i':
          case 'l':
              //_cType = SQL_C_SLONG;
              _cType = SQL_C_LONG;
              //_odbcType = SQL_BIGINT;
             _valueLength = sizeof (long);
              break;
          case 'C':
          case 'S':
              //_cType = SQL_C_USHORT;
              _cType = SQL_C_SHORT;
              //_odbcType = SQL_SMALLINT;
              _valueLength = sizeof (unsigned short);
              break;
          case 'I':
          case 'L':
              //_cType = SQL_C_ULONG;
              _cType = SQL_C_LONG;
              //_odbcType = SQL_BIGINT;
              _valueLength = sizeof (unsigned long);
              break;
          case 'f':
              _cType = SQL_C_FLOAT;
              //_odbcType = SQL_FLOAT;
              _valueLength = sizeof (float);
              break;
          case 'd':
              _cType = SQL_C_DOUBLE;
              //_odbcType = SQL_DOUBLE;
              _valueLength = sizeof (double);
              break;
          default:
              [NSException raise:NSInternalInconsistencyException
                        format:@"*** -[%@ %s] unknown number type: \"%@\"",
                         NSStringFromClass([self class]), sel_getName(_cmd),
                        [attribute valueType]];
        }
    }
    return self;
}

- (id)buildValueFromSQLValue:(const void *)value length:(unsigned)length zone:(NSZone *)zone
{
    // Check for nulls
    if (length == SQL_NULL_DATA) return [EONull null];

    // Check against _cType to determine how to construct the number.
    switch (_cType) {
    case SQL_C_SHORT :
    case SQL_C_SSHORT :
        return [[NSNumber allocWithZone:zone] initWithShort:*(short *)value];
    case SQL_C_USHORT :
        return [[NSNumber allocWithZone:zone] initWithUnsignedShort:*(unsigned short *)value];
    case SQL_C_LONG :
    case SQL_C_SLONG :
        return [[NSNumber allocWithZone:zone] initWithLong:*(long *)value];
    case SQL_C_ULONG :
        return [[NSNumber allocWithZone:zone] initWithLong:*(unsigned long *)value];
    case SQL_C_FLOAT :
        return [[NSNumber allocWithZone:zone] initWithFloat:*(float *)value];
    case SQL_C_DOUBLE :
        return [[NSNumber allocWithZone:zone] initWithDouble:*(double *)value];
    case SQL_C_CHAR :
        return [[NSDecimalNumber allocWithZone:zone] initWithString: [NSString stringWithCString:value length:length]];
    }
    // Should never get here
    return [EONull null];
}

- (void)takeInputValue:(id)value
{
    // Check for nulls
    if(value == [EONull null]) {
        _returnedLength = SQL_NULL_DATA;
        return;
    }

    // Check against _cType to determine how to build the number.
    switch (_cType) {
    case SQL_C_SHORT :
    case SQL_C_SSHORT :
        *(short *)_value = [value shortValue];
        _returnedLength = sizeof(short);
        break;
    case SQL_C_USHORT :
        *(unsigned short *)_value = [value unsignedShortValue];
        _returnedLength = sizeof(unsigned short);
        break;
    case SQL_C_LONG :
    case SQL_C_SLONG :
        *(long *)_value = [value longValue];
        _returnedLength = sizeof(long);
        break;
    case SQL_C_ULONG :
        *(unsigned long *)_value = [value unsignedLongValue];
        _returnedLength = sizeof(unsigned long);
        break;
    case SQL_C_FLOAT :
        *(float *)_value = [value floatValue];
        _returnedLength = sizeof(float);
        break;
    case SQL_C_DOUBLE :
        *(double *)_value = [value doubleValue];
        _returnedLength = sizeof(double);
        break;
    case SQL_C_CHAR : {
        const char *cString =[[value stringValue] cString];
        memcpy(_value, cString, strlen(cString) + 1);
        _returnedLength = SQL_NTS;
        break;
        }
    }
}

@end

@implementation ODBCDateColumn

- initWithAttribute:(EOAttribute *)attribute channel:(ODBCChannel *)channel
{
    [super initWithAttribute:attribute channel:channel];
    _valueLength = sizeof (TIMESTAMP_STRUCT);
    _cType = SQL_C_TIMESTAMP;
    return self;
}

- (id)buildValueFromSQLValue:(const void *)value length:(unsigned)length zone:(NSZone *)zone
{
    // Check if we got a value
    if (length == SQL_NULL_DATA) return [EONull null];

    return [_attribute newDateForYear:((TIMESTAMP_STRUCT*)value)->year
                                month:((TIMESTAMP_STRUCT*)value)->month
                                  day:((TIMESTAMP_STRUCT*)value)->day
                                 hour:((TIMESTAMP_STRUCT*)value)->hour
                               minute:((TIMESTAMP_STRUCT*)value)->minute
                               second:((TIMESTAMP_STRUCT*)value)->second
                          millisecond:0 /*((TIMESTAMP_STRUCT*)value)->fraction*/
                             timezone:nil
                                 zone:zone];
}

- (void)takeInputValue:(id)value
{
    TIMESTAMP_STRUCT *date;
    
    // Check for nulls
    if(value == [EONull null]) {
        _returnedLength = SQL_NULL_DATA;
        return;
    }

    date = (void *)_value;

    date->year = [value yearOfCommonEra];
    date->month = [value monthOfYear];
    date->day = [value dayOfMonth];
    date->hour = [value hourOfDay];
    date->minute = [value minuteOfHour];
    date->second = [value secondOfMinute];
    date->fraction = 0 /*[value milliseconds]*/;

    _returnedLength = sizeof(TIMESTAMP_STRUCT);
}

@end

@implementation ODBCByteColumn

- initWithAttribute:(EOAttribute *)attribute channel:(ODBCChannel *)channel
{
    [super initWithAttribute:attribute channel:channel];
    
    if(_adaptorValueType == EOAdaptorCharactersType) {
        _cType = SQL_C_CHAR;
        _valueLength += 1; // make some space for \0.
    } else {
        _cType = SQL_C_BINARY;
    }

    return self;
}

- (id)buildValueFromSQLValue:(const void *)value length:(unsigned)length zone:(NSZone *)zone
{
    // Check for NULL first by looking at the number of bytes returned
    if (length == SQL_NULL_DATA) return [EONull null];

    // create a value of type NSString if the adaptorValueType is a string
    if (_adaptorValueType == EOAdaptorCharactersType) {
        // remove trailing spaces
        while(length && (((char *)value)[length-1] == ' ')) length -= 1;
        return [_attribute newValueForBytes:value length:length encoding:_encoding];
    } else {
        return [_attribute newValueForBytes:value length:length];
    }
}

- (void)takeInputValue:(id)value
{
    // Check for nulls
    if(value == [EONull null]) {
        _returnedLength = SQL_NULL_DATA;
        return;
    }

    // Check against _cType to determine how to build the number.
    switch (_cType) {
        case SQL_C_CHAR : {
            const char *cString = [value cString];
            memcpy(_value, cString, strlen(cString) + 1);
            _returnedLength = SQL_NTS;
            break;
        }

        case SQL_C_BINARY: {
            memcpy(_value, [value bytes], [value length]);
            _returnedLength = [value length];
            break;
        }
    }
}

@end

@implementation ODBCLongByteColumn

- initWithAttribute:(EOAttribute *)attribute channel:(ODBCChannel *)channel
{
    [super initWithAttribute:attribute channel:channel];

    // Make the column length the largest multiple of a page
    _valueLength = ODBC_MAX_RETLEN;

    return self;
}

- (BOOL)couldBind
{
    return NO;
}

- (void)takeInputValue:(id)value
{
    // Check for nulls
    if(value == [EONull null]) {
        _returnedLength = SQL_NULL_DATA;
        return;
    }

    // Check against _cType to determine how to build the value.
    switch (_cType) {
        case SQL_C_CHAR : {
            const char *cString = [value cString];
            if(_valueLength < (strlen(cString) + 1)) {
                [self freeValue];
                _valueLength = strlen(cString) + 1;
                [self allocateValue];
            }
            
            memcpy(_value, cString, strlen(cString) + 1);
            _returnedLength = SQL_NTS;
            break;
        }

        case SQL_C_BINARY: {
            if(_valueLength < [value length]) {
                [self freeValue];
                _valueLength = [value length];
                [self allocateValue];
            }
            memcpy(_value, [value bytes], [value length]);
            _returnedLength = [value length];
            break;
        }
    }
}

@end
