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

/* ODBC SQL notes:

   ***
   The create statement includes the referential integrity constraints, there
   is no portable ways to add such constraints later

   create table  EMPLOYEE ( EMP_ID int NOT NULL, DEPT_ID int, LAST_NAME VARCHAR(40) NOT NULL, PHONE VARCHAR(12), HIRE_DATE timestamp  null, SALARY numeric(7,2), PRIMARY KEY (EMP_ID), FOREIGN KEY (DEPT_ID) REFERENCES DEPARTMENT (DEPT_ID))

   I have no clue on how to specify cross table foreign key ! You need to create a table before the other...

   ***
   the locking mechanism is weird, it needs the complete list of columns:

   SELECT [DISTINCT] xx.foo, yy.bar FROM xx, yy WHERE xx.foo = yy.foo
   FOR UPDATE OF xx.foo, yy.bar;

   ***

   Outer joins are also funny:

     SELECT EMPLOYEE.NAME, DEPT.DEPTNAME
     FROM --(*vendor(Microsoft),product(ODBC) oj
          EMPLOYEE LEFT OUTER JOIN DEPT ON EMPLOYEE.DEPTID=DEPT.DEPTID*)--
     WHERE EMPLOYEE.PROJID=544

   'LEFT' could be replaced by 'RIGHT' or 'FULL'.

   ***
   - There is no support for primary keys. 

   ***
   It's easier to implement bindings than to deal with stuff like this:

   UPDATE EMPLOYEE
     SET BIRTHDAY=--(*vendor(Microsoft),product(ODBC) d '1967-01-15' *)--
     WHERE NAME='Smith, John'


   'd' (date format "yyyy-mm-dd") could be replaced by 't' (time, format "hh:mm:ss")
   or 'ts' (timestamp, format “yyyy-mm-dd hh:mm:ss[.f...]”). This is a simple and
   efficient way to solve the formatting issues !

   ***
   BTW, one could replace --(*vendor... by '{' and *)-- by "}"...

*/

//
// The UPDATE, DELETE and INSERT statements are implemented on the superclass
// and there is no need to alter those. The only change is to format the
// attributes nicely.
//
// The SELECT statement is another story !
//
@implementation ODBCSQLExpression

+ (BOOL)useBindVariables { return YES; }
- (BOOL)shouldUseBindVariableForAttribute:(EOAttribute *)att { return YES; }
- (BOOL)mustUseBindVariableForAttribute:(EOAttribute *)att { return YES; }

- (NSString *)lockClause
{
    if(_sybaseLocking)
        return @"HOLDLOCK";
    else
        return @"FOR UPDATE"; // Add OF + columns names...
}

- (NSString *)tableListWithRootEntity:(EOEntity *)entity
{
    //
    // This method build the table list in a select statement
    //
    // We are overiding this method because ODBC is expressing outer joins with
    // a unique syntax. If there is outer joins in the expression we
    // are currently evaluating, we need to generate some weird SQL.
    //
    NSString *outerJoinName = nil;
    NSMutableString *string = [NSMutableString string];
    NSMutableDictionary *backup = _aliasesByRelationshipPath;
    
    //
    // First we check if there are some outer joins.
    //
    // We need at least two tables to check the joins !
    //
    if ([_aliasesByRelationshipPath count] > 1) {
        NSEnumerator *enumerator = [_aliasesByRelationshipPath keyEnumerator];
        NSString *destPath;
        EORelationship *relationship;

        //
        // Look at all the paths
        //
        while (destPath = [enumerator nextObject]) {
            if ([destPath isEqualToString:@""])
                continue; // No join

            relationship = [entity relationshipForPath:destPath];

            if([relationship joinSemantic] != EOInnerJoin) {
                if(outerJoinName) {
                    [NSException raise:EOGeneralAdaptorException format:@"Multiple outer joins in a select statement is not supported (entity: %@)", [entity name]];
                }
                outerJoinName = destPath;
            }
        }
    }

    if(outerJoinName) {
        EOEntity *sourceEntity;
        EOEntity *destinationEntity;
        NSString *sourceAlias;
        NSString *destinationAlias;
        EORelationship *rel;
        NSString *sourcePath;
        NSArray *joins;
        unsigned i,c;
        BOOL first = YES;
        NSString *outer;
        
        if(!_inSelect) {
            [NSException raise:EOGeneralAdaptorException format:@"Impossible to use an outer joins outside a select statement"];
        }

        //
        // Lookup for all the necessary info
        //

        // First, the followed relationship
        rel = [entity relationshipForPath:outerJoinName];

        // Find the source information: entity and alias
        sourcePath = [outerJoinName relationshipPathByDeletingLastComponent];
        if (!sourcePath)
            sourcePath = @"";

        sourceEntity = [rel entity];
        sourceAlias = [_aliasesByRelationshipPath objectForKey:sourcePath];

        // Find the destination information
        destinationEntity = [rel destinationEntity];
        destinationAlias = [_aliasesByRelationshipPath objectForKey:outerJoinName];

        // Check if it's a LEFT, FULL or RIGHT Outer join
        switch([rel joinSemantic]) {
            case EOFullOuterJoin: outer = @"FULL"; break;
            case EOLeftOuterJoin: outer = @"LEFT"; break;
            case EORightOuterJoin: outer = @"RIGHT"; break;
            default: outer = nil; break; // should not happen !
        }

        //
        // Build the beginning of the string
        //
        [string appendFormat:@" { oj %@ %@ %@ OUTER JOIN %@ %@ ON ", [sourceEntity externalName], sourceAlias, outer, [destinationEntity externalName], destinationAlias];

        //
        // Add the joins
        //
        joins = [rel joins];
        for (i = 0, c = [joins count]; i < c; i++) {
            id join = [joins objectAtIndex:i];
            id sourceAttr = [join sourceAttribute];
            id destAttr = [join destinationAttribute];

            // Build the attributes names with aliases in front
            id sourceName = [self _aliasForRelatedAttribute:sourceAttr relationshipPath:sourcePath];
            id destName = [self _aliasForRelatedAttribute:destAttr relationshipPath:outerJoinName];

            if(first) {
                first = NO;
            } else {
                [string appendFormat:@" AND"];
            }
            [string appendFormat:@" %@", [self assembleJoinClauseWithLeftName:sourceName rightName:destName joinSemantic:EOInnerJoin]];
        }
        //
        // Close the string
        //
        [string appendFormat:@" } "];

        //
        // We are going to let the superclass perform the work for all
        // the non outer joins. But before we need to remove the source and
        // destination used by the outer join from the list
        //
        // remove sourcePath and destPath from the list
        // 
        _aliasesByRelationshipPath = [NSMutableDictionary dictionaryWithDictionary:_aliasesByRelationshipPath];
        [_aliasesByRelationshipPath removeObjectForKey:sourcePath];
        [_aliasesByRelationshipPath removeObjectForKey:outerJoinName];
        
    }

    {
        NSEnumerator *enumerator;
        NSString *key, *tableName;

        if (![self useAliases]) {
            [string appendFormat:@"%@ %@", [entity externalName], _sybaseLocking ? [self lockClause] : @""];
        } else {
            enumerator = [_aliasesByRelationshipPath keyEnumerator];
            while (key = [enumerator nextObject]) {
                if ([key isEqual:@""])
                    tableName = [entity externalName];
                else
                    tableName = [[[entity relationshipForPath:key] destinationEntity] externalName];

                if ([string length]) [string appendString:@", "];
                [string appendFormat:@"%@ %@ %@", tableName, [_aliasesByRelationshipPath objectForKey:key], _sybaseLocking ? [self lockClause] : @""];
            }
        }
    }

    //
    // Restore the list
    //
    _aliasesByRelationshipPath = backup;

    return string;
}

- (void)addJoinClauseWithLeftName:(NSString *)leftName rightName:(NSString *)rightName joinSemantic:(EOJoinSemantic)semantic
{
    if(semantic == EOInnerJoin) {
        return [super addJoinClauseWithLeftName:leftName rightName:rightName joinSemantic:semantic];
    }
    //
    // The outer join generation is done in the table list generation.
    //
}
- (NSMutableDictionary *)bindVariableDictionaryForAttribute:(EOAttribute *)att value:value
{
    return [NSMutableDictionary dictionaryWithObjectsAndKeys:[att name], EOBindVariableNameKey, @"?", EOBindVariablePlaceHolderKey, att, EOBindVariableAttributeKey, value, EOBindVariableValueKey, nil];
}

+ (NSString *)formatValue:(id)value forAttribute:(EOAttribute *)attribute
{
    return @"NULL"; // This SQL expression is using bindings everywhere.
                    // There is no support for unbound arguments...
}

- (NSString *)description
{
    if ([_bindings count]) {
        NSMutableString *string = [NSMutableString string];
        unsigned i,c;

        [string appendFormat:@"("];
        for(i = 0, c = [_bindings count]; i < c; i++) {
            id curr = [_bindings objectAtIndex:i];

            if(i != 0) [string appendFormat:@", "];
            [string appendFormat:@"%d:%@(%@)", i+1, [curr valueForKey:EOBindVariableValueKey], [curr valueForKey:EOBindVariableNameKey]];
        }
        [string appendFormat:@"("];

        return [NSString stringWithFormat:@"<%@: \"%@\" withBindings:%@>", [self class], [self statement], string];
    } else {
        return [NSString stringWithFormat:@"<%@: \"%@\">", [self class], [self statement]];
    }
}


- (void)prepareSelectExpressionWithAttributes:(NSArray *)attributes lock:(BOOL)lock fetchSpecification:(EOFetchSpecification *)fetchSpec
{
    if(lock) {
        NSDictionary *driverInfo = [ODBCAdaptor driverInfoForModel:[[[attributes objectAtIndex:0] parent] model]];
        NSString *databaseName = [driverInfo objectForKey:@"DBMS_NAME"];
        NSString *driverName = [driverInfo objectForKey:@"DRIVER_NAME"];

        if([databaseName isEqual:@"Microsoft SQL Server"]) {
            _sybaseLocking = 1;
        } else if([driverName isEqual:@"odbcjt32.dll"]) {
            lock = NO;
        }
    }

    _inSelect = YES;
    [super prepareSelectExpressionWithAttributes:attributes lock:lock fetchSpecification:fetchSpec];
    _inSelect = NO;

    _sybaseLocking = 0;
}


- (NSString *)assembleSelectStatementWithAttributes:(NSArray *)attributes lock:(BOOL)lock qualifier:(EOQualifier *)qualifier fetchOrder:(NSArray *)fetchOrder selectString:(NSString *)selectString columnList:(NSString *)columnList tableList:(NSString *)tableList whereClause:(NSString *)whereClause joinClause:(NSString *)joinClause orderByClause:(NSString *)orderByClause lockClause:(NSString *)lockClause
{
    NSMutableString *string;

    string = [NSMutableString stringWithFormat:@"%@ %@ %@ %@", selectString, columnList, @"FROM", tableList];

    if (lock) {
        if(!_sybaseLocking)
            [string appendFormat:@" %@", lockClause];
    }

    if (whereClause && [whereClause length])
        [string appendFormat:@" %@ %@", @"WHERE", whereClause];

    if (joinClause && [joinClause length]) {
        if (whereClause && [whereClause length])
            [string appendFormat:@" %@ %@", @"AND", joinClause];
        else
            [string appendFormat:@" %@ %@", @"WHERE", joinClause];
    }

    if (orderByClause && [orderByClause length])
        [string appendFormat:@" %@ %@", @"ORDER BY", orderByClause];

    return string;
}


@end

@implementation ODBCSQLExpression(EOSchemaGeneration)

+ (NSArray *)primaryKeyConstraintStatementsForEntityGroup:(NSArray *)entityGroup
{
    return [NSArray array]; // No primary key constraints for now
}

+ (NSArray *)primaryKeySupportStatementsForEntityGroup:(NSArray *)entityGroup
{
    return [NSArray array];
}

+ (NSArray *)dropPrimaryKeySupportStatementsForEntityGroup:(NSArray *)entityGroup
{
    return [NSArray array];
}

- (NSString *)columnTypeStringForAttribute:(EOAttribute *)attribute
{
    NSString *externalType = [attribute externalType];
    NSDictionary *typeInfo = [[ODBCAdaptor typeInfoForModel:[[attribute parent] model]] objectForKey:externalType];
    int createParams = [[typeInfo objectForKey:@"createParams"] intValue];
    
    // Should provide the external type with the correct number of arguments
    //
    // 3 possible cases:
    switch(createParams) {
        case 2:
            // scale + precision
            return [NSString stringWithFormat:@"%@(%d,%d)", [attribute externalType], [attribute precision], [attribute scale]];

        case 1:
            // Width
            return [NSString stringWithFormat:@"%@(%d)", [attribute externalType], [attribute width]];

        default:
            // Nothing
            return [attribute externalType];
    }
    // not reachable
}

- (NSString *)allowsNullClauseForConstraint:(BOOL)allowsNull
{
    return @"";
    // return (allowsNull) ? @"" : @"NOT NULL";
}
@end
