/*
 * Copyright (c) 2003 Apple Computer, Inc. All rights reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 * 
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 * 
 * @APPLE_LICENSE_HEADER_END@
 */

/*!
 * @header DSoDirectory
 */


#import "DSoDirectory.h"
#import "DSoDirectoryPriv.h"

#import <unistd.h>

#import "DSoNode.h"
#import "DSoBuffer.h"
#import "DSoDataList.h"
#import "DSoException.h"
#import "DSoNodeConfig.h"
#import "DSoDataNode.h"
#import "DSoNodeBrowserItem.h"

// ----------------------------------------------------------------------------
// Public Methods.
#pragma mark ***** Public Methods *****

@implementation DSoDirectory
- (id)init
{
    [super init];
    mDirRef			= 0;
    mLocalNode		= nil;
    mSearchNode		= nil;
    proxyHostname   = nil;
    proxyUsername   = nil;
    proxyPassword   = nil;
    mDirRefLock		= [[NSRecursiveLock alloc] init];
    return self;
}

// Open a connection to the local machine.
- (id)initWithLocal
{
    @try {
        [self init];
        [self openLocalHost];
    } @catch( NSException *exception ) {
        [self release];
        @throw;
    }
    return self;
}

// Open a conection to a remote machine using DS Proxy.
- (id)initWithHost:(NSString*)hostName user:(NSString*)inUser password:(NSString*)inPassword
{
    @try {
        [self init];
        [self openHost:hostName user:inUser password:inPassword];
        // Save the hostname, username and password in case we
        // lose the connection and have to re-establish it
        proxyHostname = [hostName retain];
        proxyUsername = [inUser retain];
        proxyPassword = [inPassword retain];
    } @catch( NSException *exception ) {
        [self release];
        @throw;
    }
    return self;
}

- (void)dealloc
{
    [self close];
    [mDirRefLock release];
    if (proxyHostname != nil)
    {
        [proxyHostname release];
        [proxyUsername release];
        [proxyPassword release];
    }
	[mRecordTypes release];
	[mAttributeTypes release];

    [super dealloc];
}

- (void)finalize
{
    [self close];
    [super finalize];
}
	
				
- (DSRef)verifiedDirRef
{
    tDirStatus nError = eDSNoErr;
    
    [mDirRefLock lock];

    @try
    {
		// DirRef initialized, check its validity.
  		// Important that ref count not changed!
		if (!mDirRef || (nError = dsVerifyDirRefNum (mDirRef)))
		{
			// Reopen the reference
			[self reopen];
        }
    } @catch( NSException *exception ) {
        @throw;
    } @finally {
        [mDirRefLock unlock];
    }
    
    return mDirRef;
}

- (unsigned long)	nodeCount
{
    DSRef			refTemp		= [self verifiedDirRef];
    unsigned long	ulCount		= 0;
    tDirStatus		nError		= dsGetDirNodeCount(refTemp, &ulCount);
    
    if (nError)
        [DSoException raiseWithStatus:nError];
        
    return ulCount;
}

- (NSArray*) findNodeNames:(NSString*)inPattern matchType:(tDirPatternMatch)inType
{
    tDirStatus			nError			= eDSNoErr;
	tDataListPtr		dlpNodeName		= nil;
    tContextData		continueData	= NULL;
    DSRef				refTemp			= 0;
    DSoBuffer		   *bufNodeList		= nil;
    unsigned long		i				= 0;
	unsigned long		ulCount			= 0;
    char			   *szpTemp			= nil;
    NSString		   *sNodeName		= nil;
    NSMutableArray	   *resultList		= nil;

    refTemp = [self verifiedDirRef];
    bufNodeList = [[DSoBuffer alloc] initWithDir:self  bufferSize:1024];
    
    resultList = [NSMutableArray array];
    
	// Generate a list of matching nodes.
    do {
        if (inPattern) {
            DSoDataList *dlPattern = [[DSoDataList alloc] initWithDir:self separator:'/' pattern:inPattern];
            nError = dsFindDirNodes (refTemp, [bufNodeList dsDataBuffer], [dlPattern dsDataList],
                                        inType, &ulCount, &continueData) ;
            [dlPattern release];
        }
        else
        {
            nError = dsFindDirNodes (refTemp, [bufNodeList dsDataBuffer], NULL,
                                        inType, &ulCount, &continueData) ;
        }

        if (nError == eDSBufferTooSmall)
        {
            unsigned long size = [bufNodeList getBufferSize];
            [bufNodeList grow:size + size];
            continue;
        }
        // Validate results.
        if (nError) {
            [bufNodeList release];
            [DSoException raiseWithStatus:nError];
        }

        resultList = [NSMutableArray arrayWithCapacity:ulCount];

        for (i = 1; i <= ulCount; i++)
        {
            nError = dsGetDirNodeName (refTemp, [bufNodeList dsDataBuffer], i, &dlpNodeName);
            if (nError)
            {
                [bufNodeList release];
                [DSoException raiseWithStatus:nError];
            }

            szpTemp = dsGetPathFromList (refTemp, dlpNodeName, "/") ;
            sNodeName = [NSString stringWithUTF8String:szpTemp];
            [resultList addObject:sNodeName];

			free (szpTemp) ;
			dsDataListDeallocate (refTemp, dlpNodeName) ;
			free (dlpNodeName) ;
        }
		
    } while (continueData != NULL || nError == eDSBufferTooSmall);

    if (continueData != NULL)
        dsReleaseContinueData(refTemp, continueData);
    
    [bufNodeList release];
    return resultList; // Already autoreleased.
}


// Return an instantiated version of a particular node.
// 
- (DSoNode*) findNode: (NSString*)inPattern
{
    return [self findNode:inPattern matchType:eDSExact];
}

- (DSoNode*) findNodeViaEnum: (int)inPattern
{
    return [self findNode:nil matchType:inPattern];
}

- (DSoNode*) findNode: (NSString*)inPattern 
                     matchType: (tDirPatternMatch)inType
{
    return [self findNode:inPattern matchType:inType useFirst:YES];
}
                     
- (DSoNode*) findNode: (NSString*)inPattern 
                     matchType: (tDirPatternMatch)inType
                     useFirst: (BOOL)inUseFirst
{
    tDirStatus			nError		= eDSNoErr;
	tDirNodeReference	refNode		= 0;
	tDataListPtr		dlpNodeName = nil;
    DSRef				refTemp		= 0;
    DSoBuffer		   *bufNodeList = nil;
    unsigned long		ulCount		= 0;
    char			   *szpTemp		= nil;
    NSString		   *sNodeName   = nil;
    DSoNode			   *opTemp		= nil;
    
    
    // Find the first (should be only) exact match if we have a full name.
    
    // Before going any further, check the node list for a match.
    if (inPattern && (inType == eDSExact) && inUseFirst)
    {
        DSoDataList *dlNodeName;
        // If we have a full name, do not validate it, simply open it here.
		refTemp = [self verifiedDirRef];
		dlNodeName = [[DSoDataList alloc] initWithDir:self separator:'/' pattern:inPattern];
		nError = dsOpenDirNode (refTemp, [dlNodeName dsDataList], &refNode);
        [dlNodeName release];
        if (nError)
        {
			[DSoException raiseWithStatus:nError];
        }
		
		if ([inPattern hasPrefix:@"/Search"]) {
			opTemp = [[DSoSearchNode alloc] initWithDir:self nodeRef:refNode nodeName:inPattern];
		} else if ([inPattern hasPrefix:@"/Configure"]) {
			opTemp = [[DSoNodeConfig alloc] initWithDir:self nodeRef:refNode nodeName:inPattern];   
		} else {
			opTemp = [[DSoNode alloc] initWithDir:self nodeRef:refNode nodeName:inPattern];
		}
        
        return [opTemp autorelease];
    }
    
    refTemp = [self verifiedDirRef];
    bufNodeList = [[DSoBuffer alloc] initWithDir:self];
    
	// Generate a list of matching nodes.
	if (inPattern)
	{
		DSoDataList *dlPattern = [[DSoDataList alloc] initWithDir:self separator:'/' pattern:inPattern];
		nError = dsFindDirNodes (refTemp, [bufNodeList dsDataBuffer], [dlPattern dsDataList],
									inType, &ulCount, NULL) ;
        [dlPattern release];
	}
    else
    {
		nError = dsFindDirNodes (refTemp, [bufNodeList dsDataBuffer], NULL,
									inType, &ulCount, NULL) ;
	}

	// Validate results.
    if( nError == eDSNoErr )
    {
        if (ulCount <= 0) {
            nError = eDSUnknownNodeName;
        } else if (!inUseFirst && (ulCount > 1)) {
            nError = eDSNodeNotFound;
        }
    }
    
    if( nError ) {
        [bufNodeList release];
        [DSoException raiseWithStatus:nError];
    }

	// Get the first matching, canonical node name from DS.
	nError = dsGetDirNodeName (refTemp, [bufNodeList dsDataBuffer], 1, &dlpNodeName);
    [bufNodeList release];
    if (nError)
		[DSoException raiseWithStatus:nError];

	szpTemp = dsGetPathFromList (refTemp, dlpNodeName, "/") ;
	sNodeName = [NSString stringWithUTF8String:szpTemp];
	free (szpTemp) ;
    
	// Create a DSoNode from the first entry and add it to the map.
	nError = dsOpenDirNode (refTemp, dlpNodeName, &refNode) ;
	dsDataListDeallocate (refTemp, dlpNodeName) ;
	// Deallocate() only frees the nodes, not the tDataList.
	free (dlpNodeName) ;

    if (nError)
    {
        [DSoException raiseWithStatus:nError];
    }

	switch (inType) {
	case eDSAuthenticationSearchNodeName:
	case eDSContactsSearchNodeName:
	case eDSNetworkSearchNodeName:
		opTemp = [[DSoSearchNode alloc] initWithDir:self nodeRef:refNode nodeName:sNodeName];
		break;
	case eDSConfigNodeName:
		opTemp = [[DSoNodeConfig alloc] initWithDir:self nodeRef:refNode nodeName:inPattern];
		break;
	default:
		opTemp = [[DSoNode alloc] initWithDir:self nodeRef:refNode nodeName:sNodeName];
		break;
	}
    
    return [opTemp autorelease];
}

/*!
 * @method findNodeNames
 * @abstract Finds the list of all node names registered by DS.
 *			Returns an empty array if no matching results are found.
 * @result An array of NSString objects containing the node names.
 */
- (NSArray*) findNodeNames
{
    tDirStatus			nError			= eDSNoErr;
	tDataListPtr		dlpNodeName		= nil;
    tContextData		continueData	= NULL;
    DSRef				refTemp			= 0;
    DSoBuffer		   *bufNodeList		= nil;
    unsigned long		i				= 0;
	unsigned long		ulCount			= 0;
    char			   *szpTemp			= nil;
    NSString		   *sNodeName		= nil;
    NSMutableArray	   *resultList		= nil;

    refTemp = [self verifiedDirRef];
    bufNodeList = [[DSoBuffer alloc] initWithDir:self  bufferSize:1024];
    
    resultList = [NSMutableArray array];
    
	// Generate a list of matching nodes.
    do {
		nError = dsGetDirNodeList (refTemp, [bufNodeList dsDataBuffer], 
								   &ulCount, &continueData) ;

        if (nError == eDSBufferTooSmall)
        {
            unsigned long size = [bufNodeList getBufferSize];
            [bufNodeList grow:size + size];
            continue;
        }
        // Validate results.
        if (nError) {
            [bufNodeList release];
            [DSoException raiseWithStatus:nError];
        }

        resultList = [NSMutableArray arrayWithCapacity:ulCount];

        for (i = 1; i <= ulCount; i++)
        {
            nError = dsGetDirNodeName (refTemp, [bufNodeList dsDataBuffer], i, &dlpNodeName);
            if (nError)
            {
                [bufNodeList release];
                [DSoException raiseWithStatus:nError];
            }

            szpTemp = dsGetPathFromList (refTemp, dlpNodeName, "/") ;
            sNodeName = [NSString stringWithUTF8String:szpTemp];
            [resultList addObject:sNodeName];

			free (szpTemp) ;
			dsDataListDeallocate (refTemp, dlpNodeName) ;
			free (dlpNodeName) ;
        }
		
    } while (continueData != NULL || nError == eDSBufferTooSmall);

    if (continueData != NULL)
        dsReleaseContinueData(refTemp, continueData);
    
    [bufNodeList release];
    return resultList; // Already autoreleased.
}

/*!
 * @method nodeBrowserItems
 * @abstract Finds the all top level items for the node browser.
 * @result An array of DSoNodeBrowserItem objects.
 */
- (NSArray*) nodeBrowserItems
{
	NSMutableArray* browserItems = nil;
	NSArray* nodeNames = [self findNodeNames];
	NSMutableSet* firstComponents = [NSMutableSet set];
	NSEnumerator* nodeNameEnum = [nodeNames objectEnumerator];
	NSString* nodeName = nil;
	
	while ((nodeName = (NSString*)[nodeNameEnum nextObject]) != nil)
	{
		NSArray* pathComponents = [nodeName pathComponents];
		if ([pathComponents count] > 1)
		{
			[firstComponents addObject:[pathComponents objectAtIndex:1]];
		}
	}
	
	browserItems = [NSMutableArray arrayWithCapacity:[firstComponents count]];
	nodeNameEnum = [firstComponents objectEnumerator];
	
	while ((nodeName = (NSString*)[nodeNameEnum nextObject]) != nil)
	{
		DSoNodeBrowserItem* item = [[DSoNodeBrowserItem alloc] initWithName:nodeName directory:self];
		[browserItems addObject:item];
		[item release];
	}
	
	return browserItems; // Already autoreleased.
}

/*!
 * @method standardRecordTypes
 * @abstract Retrieves all defined standard record types from the config node.
 * @result An array of strings of the record types.
 */
- (NSArray*) standardRecordTypes
{
	if (mRecordTypes == nil) 
	{
		DSoNodeConfig* config = (DSoNodeConfig*)[[DSoNodeConfig alloc] initWithDir:self];
		mRecordTypes = [[config findRecordNames:@"dsConfigType::GetAllRecords"
										 ofType:"dsConfigType::RecordTypes"
									  matchType:eDSExact] retain];
		[config release];
	}
	return mRecordTypes;
}

/*!
 * @method standardAttributeTypes
 * @abstract Retrieves all defined standard attribute types from the config node.
 * @result An array of strings of the attribute types.
 */
- (NSArray*) standardAttributeTypes
{
	if (mAttributeTypes == nil) 
	{
		DSoNodeConfig* config = (DSoNodeConfig*)[[DSoNodeConfig alloc] initWithDir:self];
		mAttributeTypes = [[config findRecordNames:@"dsConfigType::GetAllRecords"
										 ofType:"dsConfigType::AttributeTypes"
									  matchType:eDSExact] retain];
		[config release];
	}
	return mAttributeTypes;	
}


// Convenience methods to create well-known nodes.

- (DSoNode*)			localNode
{
    return [self findNode:nil matchType:eDSLocalNodeNames useFirst:YES];
}


- (DSoSearchNode*) searchNode
{
    DSRef				refTemp		= 0;
    DSoBuffer		   *bufNodeList = nil;
    DSoSearchNode      *searchNode  = nil;
    tDataListPtr		dlpName		= 0;
    tDirNodeReference	refNode		= 0;
    tDirStatus			nError		= eDSNoErr;
    unsigned long		ulCount		= 0 ;
    char			   *szpTemp		= nil;
    NSString		   *sNodeName   = nil;
    unsigned long		ulTries		= 10;
        
	// See findNode: above for similar comments.

	for ( ; (ulTries-- && !searchNode) ; sleep (3)) {
		// function creates and returns a DSSearchNode, not a DSoNode.

		// Most of FindNode() is duplicated here because this
		// function creates and returns a DSSearchNode, similar to a DSoNode.
		refTemp = [self verifiedDirRef];
		bufNodeList = [[DSoBuffer alloc] initWithDir:self];
        
		if ((nError = dsFindDirNodes (refTemp, [bufNodeList dsDataBuffer], NULL,
								eDSSearchNodeName, &ulCount, NULL))
				|| (ulCount != 1))
        {
            [bufNodeList release];
			continue ;
        }
		if (nError = dsGetDirNodeName (refTemp, [bufNodeList dsDataBuffer], 1, &dlpName))
        {
            [bufNodeList release];
			continue ;
        }
        [bufNodeList release];
        
		szpTemp = dsGetPathFromList (refTemp, dlpName, "/") ;
		sNodeName = [NSString stringWithUTF8String:szpTemp] ;
		free (szpTemp) ;

		// Open the node manually and create an instance of DSSearchNode.
		nError = dsOpenDirNode (refTemp, dlpName, &refNode) ;
		dsDataListDeallocate (refTemp, dlpName) ;
		// Deallocate() only frees the nodes, not the tDataList.
		free (dlpName) ;

		if (!nError) {
			searchNode = [[DSoSearchNode alloc] initWithDir:self nodeRef:refNode nodeName:sNodeName];
			return [searchNode autorelease] ;
		}
		if (nError != eDSUnknownNodeName)
        {
			break ;
        }
	}

    return [searchNode autorelease];
}

- (tDirReference)dsDirRef
{
    return mDirRef;
}

@end

// ----------------------------------------------------------------------------
// Private Methods
#pragma mark ***** Private Methods *****

@implementation DSoDirectory (DSoDirectoryPrivate)

- (void)openLocalHost
{
    tDirStatus nError = eDSNoErr;

    [mDirRefLock lock];
    if (!mDirRef)
        nError = dsOpenDirService(&mDirRef);
    [mDirRefLock unlock];
    if (nError)
        [DSoException raiseWithStatus:nError];
}
- (void)openHost:(NSString*)hostName user:(NSString*)inUser password:(NSString*)inPassword
{
    DSoBuffer      *step			= nil;
    DSoBuffer      *stepResponse	= nil;
    DSoDataNode    *authMethod		= nil;
    long int		length			= 0;
    tDirStatus		status			= eDSNoErr;
	NSData		   *userNameData	= nil;
	NSData		   *passwordData	= nil;

    step = [[DSoBuffer alloc] initWithDir:nil bufferSize:2048];
    stepResponse = [[DSoBuffer alloc] initWithDir:nil bufferSize:2048];
    authMethod = [(DSoDataNode*)[DSoDataNode alloc] initWithDir:nil cString:kDSStdAuthNodeNativeClearTextOK];

	userNameData = [inUser dataUsingEncoding:NSUTF8StringEncoding];
    length = [userNameData length];
    [step setData:&length length:sizeof(long)];
    [step appendData:[userNameData bytes] length:length];

	passwordData = [inPassword dataUsingEncoding:NSUTF8StringEncoding];
    length = [passwordData length];
    [step appendData:&length length:sizeof(long)];
    [step appendData:[passwordData bytes] length:length];

    status = dsOpenDirServiceProxy(&mDirRef, [hostName cString], 0, [authMethod dsDataNode], [step dsDataBuffer], [stepResponse dsDataBuffer], NULL);

	[step release];
	[stepResponse release];
	[authMethod release];
	
    if (status != eDSNoErr) {
        [[DSoException name:@"DSOpenDirServiceErr" reason:@"Cannot open Directory Services Proxy." status:status] raise];
    }
}

- (void)close
{
    [mDirRefLock lock];
    if (mDirRef)
    {
        dsCloseDirService(mDirRef);
        mDirRef = 0;
    }
    [mDirRefLock unlock];
}

// This method assumes that the mDirRef has been invalidated
// and will not close the reference.
- (void)reopen
{
    // If the proxyUsername is not null, then we have a proxy connection.
    [self close];
    if (proxyHostname)
        [self openHost:proxyHostname user:proxyUsername password:proxyPassword];
    else
        [self openLocalHost];
}

- (DSoNodeConfig*)configNode
{
    return [[[DSoNodeConfig alloc] initWithDir:self] autorelease];
}

@end
