/* 
   EOAttribute.m

   Copyright (C) 1996 Free Software Foundation, Inc.

   Author: Ovidiu Predescu <ovidiu@bx.logicnet.ro>
   Date: August 1996

   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 <ctype.h>

#include <Foundation/NSUtilities.h>
#include <Foundation/NSObjCRuntime.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/EORelationship.h>
#include <eoaccess/EOExpressionArray.h>
#include <eoaccess/EOCustomValues.h>
#include <eoaccess/EONull.h>
#include <eoaccess/exceptions/EOFExceptions.h>

@implementation EOAttribute

static NSString* defaultCalendarFormat = @"%b %d %Y %H:%M";

- initWithName:(NSString*)_name
{
    ASSIGN(name,_name);
    return self;
}

- (void)dealloc
{
    [name release];
    [calendarFormat release];
    [clientTimeZone release];
    [serverTimeZone release];
    [columnName release];
    [definition release];
    [externalType release];
    [valueClassName release];
    [valueType release];
    [insertFormat release];
    [selectFormat release];
    [updateFormat release];
    [userDictionary release];
	[docComment release];
    [super dealloc];
}

- (void)gcDecrementRefCountOfContainedObjects
{
    [entity gcDecrementRefCount];
    [(id)definitionArray gcDecrementRefCount];
    [realAttribute gcDecrementRefCount];
}

- (BOOL)gcIncrementRefCountOfContainedObjects
{
    if(![super gcIncrementRefCountOfContainedObjects])
	return NO;

    [entity gcIncrementRefCount];
    [(id)definitionArray gcIncrementRefCount];
    [realAttribute gcIncrementRefCount];

    [entity gcIncrementRefCountOfContainedObjects];
    [(id)definitionArray gcIncrementRefCountOfContainedObjects];
    [realAttribute gcIncrementRefCountOfContainedObjects];

    return YES;
}

// Is equal only if same name; used to make aliasing ordering stable
- (unsigned)hash
{
    return [name hash];
}

-(NSString*)description
{
  NSString* dscr=[NSString stringWithFormat:@"<%s %p - name=%@ entity=%@ columnName=%@ definition=%@ ",
						   object_get_class_name(self),
						   (void*)self,
						   [self name],
						   [[self entity] name],
						   [self columnName],
						   [self definition]];
  dscr=[dscr stringByAppendingFormat:@"valueClassName=%@ valueType=%@ isReadOnly=%s isDerived=%s isFlattened=%s>",
			 [self valueClassName],
			 [self valueType],
			 ([self isReadOnly] ? "YES" : "NO"),
			 ([self isDerived] ? "YES" : "NO"),
			 ([self isFlattened] ? "YES" : "NO")];
  return dscr;
};
 
- (BOOL)setName:(NSString*)_name
{
    if([name isEqual:_name])
	return YES;

    if([entity attributeNamed:_name])
	return NO;

    ASSIGN(name, _name);
    return YES;
}

+ (BOOL)isValidName:(NSString*)_name
{
    return [EOEntity isValidName:_name];
}

- (BOOL)setReadOnly:(BOOL)flag
{
    if(!flag && ([self isDerived] || [self isFlattened]))
	return NO;
    flags.isReadOnly = flag;
    return YES;
}

- (BOOL)referencesProperty:property
{
    if (flags.isDerived)
	return [definitionArray indexOfObject:property] != NSNotFound;
    else
	return NO;
}

- (void)setDefinition:(NSString*)def
{
    NSArray* defArray;
    int i, count;
    EOEntity* currentEntity;
    id realAttributeName;

    if(!def)
	THROW([InvalidArgumentException new]);

    flags.isDerived = YES;
    flags.isFlattened = NO;

    ASSIGN(definition, def);

    if([definition isNameOfARelationshipPath]) {
	flags.isFlattened = YES;
	defArray = [definition componentsSeparatedByString:@"."];
	count = [defArray count];
	definitionArray = [[GCMutableArray alloc] initWithCapacity:count];
	NS_DURING {
	    currentEntity = entity;
	    for(i = 0; i < count - 1; i++) {
		id relationshipName = [defArray objectAtIndex:i];
		id relationship;
    
		if(![EOEntity isValidName:relationshipName])
		    THROW([[InvalidNameException alloc]
				initWithName:relationshipName]);
    
		relationship
			= [currentEntity relationshipNamed:relationshipName];
		if(!relationship)
		    THROW([[InvalidPropertyException alloc]
				initWithName:relationshipName
				entity:currentEntity]);
		if([relationship isToMany])
		    THROW([[RelationshipMustBeToOneException alloc]
				initWithName:relationshipName
				entity:currentEntity]);
		[definitionArray addObject:relationship];
		currentEntity = [relationship destinationEntity];
	    }
	    realAttributeName = [defArray lastObject];
	    realAttribute = [[currentEntity attributeNamed:realAttributeName]
				retain];
	    if(!realAttribute)
		THROW([[InvalidPropertyException alloc]
			initWithName:realAttributeName entity:currentEntity]);
	    [definitionArray addObject:realAttribute];
	}
	NS_HANDLER {
	    [definitionArray release];
	    definitionArray = nil;
	    [localException raise];
	}
	NS_ENDHANDLER
    }
    else {
	definitionArray = [[EOExpressionArray parseExpression:definition
					     entity:entity
					     replacePropertyReferences:YES]
				retain];
    }
}

- (NSString*)expressionValueForContext:(id<EOExpressionContext>)context
{
    if(context)
	return [context expressionValueForAttribute:self];
    else return columnName;
}

- (void)setEntity:(EOEntity*)_entity	{ ASSIGN(entity, _entity); }
- (void)setCalendarFormat:(NSString*)format { ASSIGN(calendarFormat, format); }
- (void)setClientTimeZone:(NSTimeZone*)tz { ASSIGN(clientTimeZone, tz); }
- (void)setServerTimeZone:(NSTimeZone*)tz { ASSIGN(serverTimeZone, tz); }
- (void)setColumnName:(NSString*)_name	{ ASSIGN(columnName, _name); }
- (void)setExternalType:(NSString*)type	{ ASSIGN(externalType, type); }
- (void)setValueClassName:(NSString*)_name { ASSIGN(valueClassName, _name); }
- (void)setValueType:(NSString*)type	{ ASSIGN(valueType, type); }
- (void)setInsertFormat:(NSString*)string { ASSIGN(insertFormat, string); }
- (void)setSelectFormat:(NSString*)string { ASSIGN(selectFormat, string); }
- (void)setUpdateFormat:(NSString*)string { ASSIGN(updateFormat, string); }
- (void)setUserDictionary:(NSDictionary*)dict { ASSIGN(userDictionary, dict); }
- (void)setDocComment:(NSString*)comment
{
    ASSIGN(docComment,comment);
}

+ (NSString*)defaultCalendarFormat	{ return defaultCalendarFormat; }
- (EOEntity*)entity			{ return entity; }
- (NSString*)name			{ return name; }
- (NSString*)calendarFormat		{ return calendarFormat; }
- (NSTimeZone*)clientTimeZone		{ return clientTimeZone; }
- (NSTimeZone*)serverTimeZone		{ return serverTimeZone; }
- (NSString*)columnName			{ return columnName; }
- (NSString*)definition			{ return definition; }
- (GCMutableArray*)definitionArray	{ return definitionArray; }
- (BOOL)isDerived			{ return flags.isDerived; }
- (BOOL)isFlattened			{ return flags.isFlattened; }
- (NSString*)insertFormat		{ return insertFormat; }
- (NSString*)selectFormat		{ return selectFormat; }
- (NSString*)updateFormat		{ return updateFormat; }
- (NSDictionary*)userDictionary		{ return userDictionary; }
- (NSString*)docComment { return docComment; }

- (BOOL)isReadOnly
{
    if (flags.isDerived)
	return YES;
    return flags.isReadOnly;
}

- (NSString*)valueClassName
{
    if (!valueClassName && flags.isFlattened)
	return [realAttribute valueClassName];
    return valueClassName;
}

- (NSString*)externalType
{
    if (!externalType && flags.isFlattened)
	return [realAttribute externalType];
    return externalType;
}

- (NSString*)valueType
{
    if (!valueType && flags.isFlattened)
	return [realAttribute valueType];
    return valueType;
}
@end /* EOAttribute */


@implementation EOAttribute (EOAttributePrivate)

+ (EOAttribute*)attributeFromPropertyList:(id)propertyList
{
    EOAttribute* attribute = [[EOAttribute new] autorelease];
    NSString* timeZoneName;

    [attribute setName:[propertyList objectForKey:@"name"]];
    [attribute setCalendarFormat:
		[propertyList objectForKey:@"calendarFormat"]];
    timeZoneName = [propertyList objectForKey:@"clientTimeZone"];
    if (timeZoneName)
	[attribute setClientTimeZone:
		    [NSTimeZone timeZoneWithName:timeZoneName]];
    timeZoneName = [propertyList objectForKey:@"serverTimeZone"];
    if (timeZoneName)
	[attribute setServerTimeZone:
		    [NSTimeZone timeZoneWithName:timeZoneName]];
    [attribute setColumnName:[propertyList objectForKey:@"columnName"]];
    [attribute setExternalType:[propertyList objectForKey:@"externalType"]];
    [attribute setValueClassName:
		[propertyList objectForKey:@"valueClassName"]];
    [attribute setValueType:[propertyList objectForKey:@"valueType"]];
    [attribute setInsertFormat:[propertyList objectForKey:@"insertFormat"]];
    [attribute setSelectFormat:[propertyList objectForKey:@"selectFormat"]];
    [attribute setUpdateFormat:[propertyList objectForKey:@"updateFormat"]];
    [attribute setUserDictionary:
		[propertyList objectForKey:@"userDictionary"]];
	[attribute setDocComment:[propertyList objectForKey:@"docComment"]];

    [attribute setReadOnly:
		[[propertyList objectForKey:@"isReadOnly"] isEqual:@"Y"]];

    /* Don't call setDefinition: now. The attributes array in
       entity is not yet set. */
    attribute->definition = [[propertyList objectForKey:@"definition"] retain];

    return attribute;
}

/* WARNING: You should call this method from entity after the relationships
   were constructed and after the `attributes' array contains the real
   attributes. */
- (void)replaceStringsWithObjects
{
    if(definition) {
	TRY {
	    [self setDefinition:definition];
	} END_TRY
	CATCH(PropertyDefinitionException) {
	    [[entity model] errorInReadingWithError:[localException reason]];
	}
	END_CATCH
    }
}

- (id)propertyList
{
    id propertyList = [[NSMutableDictionary new] autorelease];

    if(name)
	[propertyList setObject:name forKey:@"name"];
    if(calendarFormat)
	[propertyList setObject:calendarFormat forKey:@"calendarFormat"];
    if(clientTimeZone)
	[propertyList setObject:[clientTimeZone timeZoneName]
		      forKey:@"clientTimeZone"];
    if(serverTimeZone)
	[propertyList setObject:[serverTimeZone timeZoneName]
		      forKey:@"serverTimeZone"];
    if(columnName)
	[propertyList setObject:columnName forKey:@"columnName"];
    if(definition)
	[propertyList setObject:definition forKey:@"definition"];
    if(externalType)
	[propertyList setObject:externalType forKey:@"externalType"];
    if(valueClassName)
	[propertyList setObject:valueClassName forKey:@"valueClassName"];
    if(valueType)
	[propertyList setObject:valueType forKey:@"valueType"];
    if(insertFormat)
	[propertyList setObject:insertFormat forKey:@"insertFormat"];
    if(selectFormat)
	[propertyList setObject:selectFormat forKey:@"selectFormat"];
    if(updateFormat)
	[propertyList setObject:updateFormat forKey:@"updateFormat"];
    if(userDictionary)
	[propertyList setObject:userDictionary forKey:@"userDictionary"];
    if(docComment)
	[propertyList setObject:docComment forKey:@"docComment"];

    if(flags.isReadOnly)
	[propertyList setObject:[NSString stringWithCString:"Y"]
		      forKey:@"isReadOnly"];

    return propertyList;
}

@end /* EOAttribute (EOAttributePrivate) */

@implementation EOAttribute(ValuesConversion)

- convertValue:aValue toClass:(Class)aClass forType:(NSString*)aValueType
{
    // Check nil/EONull
    if (aValue == nil)
	return nil;
    if (aValue == [EONull null])
	return aValue;
    
    // Check if we need conversion; we use is kind of because 
    // a string is not a NSString but some concrete class, so is NSData,
    // NSNumber and may be other classes
    if ([aValue isKindOfClass:aClass])
	return aValue;
    
    // We have to convert the aValue
    
    // Try EOCustomValues
    if ([aValue respondsToSelector:@selector(stringForType:)]) {
	// Special case if aClass is NSNumber
	if (aClass == [NSNumber class]) {
	    return [NSNumber
	    	numberWithString:[aValue stringForType:aValueType]
		type:aValueType];
	}
	
	// Even more Special case if aClass is NSCalendar date
	if (aClass == [NSCalendarDate class]) {
	    id format, date;
	    
	    format = [self calendarFormat];
	    if (format == nil)
		format = [EOAttribute defaultCalendarFormat];
	    date = [NSCalendarDate 
	    	dateWithString:[aValue stringForType:aValueType]
		calendarFormat:format];
	    [date setCalendarFormat:format];
	    return date;
	}
	
	// See if we can alloc a new aValue and initilize it
	if ([aClass instancesRespondToSelector:
					    @selector(initWithString:type:)]) {
	    return [[aClass alloc] 
	    	initWithString:[aValue stringForType:aValueType]
		type:aValueType];
	}
    }
    
    // Try EODatabaseCustomValues
    if ([aValue respondsToSelector:@selector(dataForType:)]) {
	// See if we can alloc a new aValue and initilize it
	if ([aClass instancesRespondToSelector:
					    @selector(initWithData:type:)]) {
	    return [[aClass alloc] 
	    	initWithData:[aValue dataForType:aValueType]
		type:aValueType];
	}
    }
    
    // Could not convert if got here
    return nil;
}

- convertValueToModel:aValue
{
    id aValueClassName;
    Class aValueClass;
    
    // Check value class from attribute
    aValueClassName = [self valueClassName];
    aValueClass = NSClassFromString(aValueClassName);
    if (aValueClass == Nil)
	return aValue;
    
    return [self convertValue:aValue 
	toClass:aValueClass forType:[self valueType]];
}

@end

@implementation NSString (EOAttributeTypeCheck)

- (BOOL)isNameOfARelationshipPath
{
    const char* s = [self cString];
    BOOL result = NO;

    if(!isalnum(*s) && *s != '@' && *s != '_' && *s != '#')
	return NO;

    for(++s; *s; s++) {
	if(!isalnum(*s) && *s != '@' && *s != '_' && *s != '#' && *s != '$'
		&& *s != '.')
	    return NO;
	if(*s == '.')
	    result = YES;
    }

    return result;
}

@end
