/* 
   MySQLChannel.m

   Copyright (C) 2000 Free Software Foundation, Inc.

   Author: Stephen Peters <portnoy@portnoy.org>
	      based on the Postgres95 Adaptor written by 
	   Mircea Oancea <mircea@jupiter.elcom.pub.ro>
   Date: August 2000

   This file is part of the GNUstep Database Library.

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

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public
   License along with this library; see the file COPYING.LIB.
   If not, write to the Free Software Foundation,
   59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

#include <Foundation/NSArray.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSString.h>
#include <Foundation/NSObjCRuntime.h>
#include <Foundation/NSUtilities.h>

#include <extensions/NSException.h>
#include <extensions/exceptions/GeneralExceptions.h>

#include <eoaccess/common.h>
#include <eoaccess/EOModel.h>
#include <eoaccess/EOEntity.h>
#include <eoaccess/EOAttribute.h>
#include <eoaccess/EONull.h>
#include <eoaccess/EOCustomValues.h>
#include <eoaccess/EOSQLExpression.h>
#include <eoaccess/EOQualifier.h>
#include <eoaccess/exceptions/EOFExceptions.h>

#include <eoadaptors/MySQL/MySQLChannel.h>
#include <eoadaptors/MySQL/MySQLExceptions.h>
#include <eoadaptors/MySQL/MySQLValues.h>

static void __dummy_function_used_for_linking (void)
{
    extern __mysql_values_linking_function (void);

    __mysql_values_linking_function ();
    __dummy_function_used_for_linking ();
}

static void
attributeSetDefaultClassForMySQLType (EOAttribute* attribute,
				      int type)
{
    NSString* valueClass = nil;
    NSString* valueType = nil;
    
    switch (type) {
    case FIELD_TYPE_TINY:
	valueClass=@"NSNumber", valueType=@"c"; break;
    case FIELD_TYPE_SHORT:
    case FIELD_TYPE_INT24:
    case FIELD_TYPE_LONG:
	valueClass=@"NSNumber", valueType=@"i"; break;
    case FIELD_TYPE_LONGLONG:
	valueClass=@"NSNumber", valueType=@"l"; break;
    case FIELD_TYPE_FLOAT:
	valueClass=@"NSNumber", valueType=@"f"; break;
    case FIELD_TYPE_DOUBLE:
	valueClass=@"NSNumber", valueType=@"d"; break;
    case FIELD_TYPE_TIMESTAMP:
    case FIELD_TYPE_DATE:
    case FIELD_TYPE_TIME:
    case FIELD_TYPE_DATETIME:
    case FIELD_TYPE_YEAR:
	valueClass=@"NSCalendarDate"; break;
    case FIELD_TYPE_DECIMAL:
    case FIELD_TYPE_STRING:
    case FIELD_TYPE_VAR_STRING:
	valueClass=@"NSString"; break;
    case FIELD_TYPE_BLOB:
    case FIELD_TYPE_TINY_BLOB:
    case FIELD_TYPE_MEDIUM_BLOB:
    case FIELD_TYPE_LONG_BLOB:
	valueClass=@"NSData"; break;
    }

    if (valueClass) {
	[attribute setValueType:valueType];
	[attribute setValueClassName:valueClass];
    } else
	NSLog (@"warning: value class name was not set for attribute %@ with "
	       @"MySQL type %d", [attribute name], type);
}

@implementation MySQLChannel

- initWithAdaptorContext:(EOAdaptorContext*)_adaptorContext
{
    [super initWithAdaptorContext:_adaptorContext];
    ASSIGN(adaptor, (MySQLAdaptor*)[_adaptorContext adaptor]);
    return self;
}

- (void)dealloc
{
    if([self isOpen])
	[self closeChannel];
    [adaptor release];
    [sqlExpression release];
    [super dealloc];
}

- (BOOL)openChannel
{
    if(![super openChannel])
	return NO;
    mysqlHandle = [adaptor newMysqlHandle];
    return (mysqlHandle != NULL);
}

- (void)closeChannel
{
    [super closeChannel];
    [self cancelResults];
    [adaptor releaseMysqlHandle:mysqlHandle force:NO];
    mysqlHandle = NULL;
}

- (MYSQL*)mysqlHandle
{
    return mysqlHandle;
}

- (MYSQL_RES*)mysqlResult
{
    return mysqlResult;
}

- (void)cancelFetch
{
    [super cancelFetch];
    [self cancelResults];
}

- (void)cancelResults
{
    if (mysqlResult) {
	mysqlResult = NULL;
    }
}

- (NSString *)mysqlServerVersion 
{
    NSString* str = NULL;
    if (mysqlHandle) {
	str = [NSString stringWithCString: mysql_get_server_info(mysqlHandle)];
    }
    return str;
}

- (BOOL)advanceRow
{
    // If there's no results, so we can't advance the row.
    if (mysqlResult == NULL)
	return NO;
    
    // fetch the next row.
    currentRow = mysql_fetch_row(mysqlResult);
    
    if (currentRow) {
	return YES;
    } else {
	[self cancelFetch];
	return NO;
    }
}

- (BOOL)_evaluateCommandsUntilAFetch
{
    BOOL ret = NO;

    isFetchInProgress = NO;

    // Perform a store_result, 
    mysqlResult = mysql_use_result(mysqlHandle);
    if (mysqlResult) {
	isFetchInProgress = YES;
	ret = YES;
    } else {
	// no result, so is this something we should get results for?
	if (mysql_field_count(mysqlHandle) == 0) {
	    // no fields; we're not supposed to return data.
	    ret = YES;
	} else {
	    // this was an error
	    NSString *errString = 
		[NSString stringWithFormat: 
			      @"SQL expression '%@' caused %s",
			  sqlExpression, mysql_error(mysqlHandle)];
	    [adaptor reportError: errString];
	    return NO;
	}
    }
        
    if (ret == YES) {
	unsigned long long insertRow = mysql_insert_id(mysqlHandle);
	if ( insertRow && mysqlDelegateRespondsTo.mysqlInsertedRowId) 
	    [delegate mysqlChannel:self 
		      insertedRowWithId: 
			  [NSNumber numberWithUnsignedLongLong: insertRow]];
    }
      
    
    if ([self isDebugEnabled]) {
	id message = [NSString stringWithCString:mysql_info(mysqlHandle)];
	if (!message) {
	    if (isFetchInProgress)
		message = [NSString stringWithFormat:
					@"Returned %d rows with %d columns ",
				    mysql_num_rows(mysqlResult),
				    mysql_field_count(mysqlHandle)];
	    else 
		message = [NSString stringWithFormat:
					@"No response for SQL: '%@'",
				    sqlExpression];
	    NSLog (@"MySQLAdaptor: %@", message);
	}
    }

    return ret;
}

- (BOOL)evaluateExpression:(NSString*)expression
{
    expression = [[expression mutableCopy] autorelease];
    ASSIGN(sqlExpression, expression);
    if(delegateRespondsTo.willEvaluateExpression) {
	EODelegateResponse response
	    = [delegate adaptorChannel:self
			willEvaluateExpression:(NSMutableString*)expression];
	if(response == EODelegateRejects)
	    return NO;
	else if(response == EODelegateOverrides)
	    return YES;
    }

    if ([self isOpen] == NO) {
	NSLog (@"MySQLChannel: tried to evaluate expression when channel closed\n");
	return NO;
    }
    
    if ([self isDebugEnabled])
	NSLog (@"MySQLAdaptor: execute command:\n%@\n", expression);

    /* cancel results from previous query */
    [self cancelResults];

    /* Send the expression to the SQL server */
    if (mysql_query(mysqlHandle, (char*)[expression cString])) {
	// error
	[adaptor privateReportError:mysqlHandle];
	return NO;
    }

    /* Check command results */
    if ([self _evaluateCommandsUntilAFetch] == NO)
	return NO;

    if(delegateRespondsTo.didEvaluateExpression)
	[delegate adaptorChannel:self didEvaluateExpression:expression];

    return YES;
}

- (NSMutableDictionary*)primaryFetchAttributes:(NSArray*)attributes 
  withZone:(NSZone*)zone
{
    NSMutableDictionary* row;
    int i, count = [attributes count];
    BOOL owned;
    unsigned long* lengths;
    
    if ([self advanceRow] == NO)
	// Return nil to indicate that the fetch operation was finished
	return nil;
    
    row = [[[NSMutableDictionary allocWithZone:zone]
		initWithCapacity:count]
		autorelease];
    
    if (count > mysql_num_fields(mysqlResult))
	THROW([[MySQLException alloc] initWithFormat:
	    @"attempt to read %d attributes "
	    @"when the result set has only %d columns",
	    count, mysql_num_fields(mysqlResult)]);

    // load lengths for use in loop.
    lengths = mysql_fetch_lengths(mysqlResult);

    for (i = 0; i < count; i++) {
	EOAttribute* attr = [attributes objectAtIndex:i];
	NSString* attrName = [attr name];
	NSString* valueClassName;
	Class class;
	int length;
	const char* string;
	id value;

	// If the column has the NULL value insert EONull in row
	if (!currentRow[i]) {
	    [row setObject:[EONull null] forKey:attrName];
	    continue;
	}
	string = currentRow[i];
	length = lengths[i];
	valueClassName = [attr valueClassName];
	class = NSClassFromString (valueClassName);

	if (!class) {
	    NSLog(@"Problematic class %@, %@", attrName,valueClassName);
	    THROW([(UnknownClassException*)[UnknownClassException alloc]
			setClassName:valueClassName]);
	}

	value = [class mysqlValueFromBytes:(char*)string
			length:length
		        owned: NO
			attribute:attr
			adaptorChannel:self
			zone:zone];
	if (value == nil)
	    THROW([[DataTypeMappingNotSupportedException alloc]
		    initWithFormat:@"MySQL could make not make value "
		    @"from class %@ for attribute %@ entity",
		    [attr valueClassName], [attr name], [[attr entity] name]]);
	[row setObject:value forKey:attrName];
    }

    return row;
}

- (NSArray*)describeResults
{
    int i, colsNumber = mysqlResult ? mysql_num_fields(mysqlResult): 0;
    id* attributes;

    if (![self isFetchInProgress])
	THROW ([AdaptorIsNotFetchingException new]);

    if (colsNumber == 0)
	return [[[NSArray alloc] init] autorelease];

    attributes = alloca (colsNumber * sizeof(id));

    for (i = 0; i < colsNumber; i++) {
	EOAttribute* attribute = [[EOAttribute new] autorelease];
	NSString* externalName;
	NSString* valueClass = @"NSString";
	NSString* valueType = nil;
	
	// mysql_fetch_field returns fields in order
	MYSQL_FIELD* fieldInfo = mysql_fetch_field(mysqlResult);
	externalName = [adaptor externalTypeNameForCode: fieldInfo->type];

	[attribute setName:[NSString stringWithFormat:@"attribute%d", i]];
	[attribute setColumnName:
		       [NSString stringWithCString: fieldInfo->name]];
	[attribute setExternalType:externalName];

	attributeSetDefaultClassForMySQLType (attribute, fieldInfo->type);
	attributes[i] = attribute;
    }

    return [[[NSArray alloc] initWithObjects:attributes count:colsNumber]
		autorelease];
}

/* The methods used to generate an model from the meta-information kept by
   the database. */

- (NSArray*)describeTableNames
{
    NSMutableArray *tableNames = nil;
    NSArray* results;
    NSString* attributeName;
    NSMutableDictionary* row;
    
    if (![self evaluateExpression:@"show tables"])
	return nil;

    results = [self describeResults];
    attributeName = [(EOAttribute*)[results objectAtIndex:0] name];
    tableNames = [NSMutableArray array];
    
    while ((row = [self fetchAttributes:results withZone:NULL]))
	[tableNames addObject:[row objectForKey:attributeName]];
    
    return tableNames;
}

- (EOEntity*)_entityForTableNamed:(NSString*)tableName
{
    EOEntity* entity;
    NSString* fieldName;
    NSString* typeName;
    NSArray* results;
    NSMutableDictionary* row;
    
    entity = [[EOEntity new] autorelease];
    
    if (![self evaluateExpression: [NSString stringWithFormat: @"desc %@",
					     tableName]]) 
	return nil;

    [entity setName:[tableName capitalizedString]];
    [entity setClassName:@"EOGenericRecord"];
    [entity setExternalName:tableName];

    results = [self describeResults];
    fieldName = [(EOAttribute*)[results objectAtIndex:0] name];
    typeName = [(EOAttribute*)[results objectAtIndex:1] name];
    
    while ((row = [self fetchAttributes:results withZone:NULL])) {
	NSString *colName = [row objectForKey: fieldName];
	EOAttribute *attr = 
	    [[EOAttribute alloc] initWithName: colName ];
	NSString *externalType = [row objectForKey: typeName];
	int code = [adaptor mysqlCodeForType: externalType];
	
	[attr setColumnName: colName];
	[attr setExternalType: externalType];
	
	attributeSetDefaultClassForMySQLType (attr, code);
	[entity addAttribute: attr];
    }

    return entity;
}

- (EOModel*)describeModelWithTableNames:(NSArray*)tableNames
{
    int i, j, count2, count = [tableNames count];
    EOModel* model = [[EOModel new] autorelease];
    EOEntity* entity;
    NSArray* attributes;
    NSString* tableName;

    for (i = 0; i < count; i++) {
	tableName = [tableNames objectAtIndex:i];
	entity = [self _entityForTableNamed: tableName];
	[model addEntity:entity];
    }

    [model setAdaptorName:@"MySQL"];
    [model setAdaptorClassName:@"MySQLAdaptor"];
    [model setConnectionDictionary:[adaptor connectionDictionary]];

    return model;
}

- (void)setDelegate:_delegate
{
    [super setDelegate:_delegate];
    mysqlDelegateRespondsTo.mysqlInsertedRowId = 
	[delegate respondsToSelector:
		      @selector(mysqlChannel:insertedRowWithId:)];
}

@end /* MySQLChannel */
