/*
  Copyright (C) 2006-2014 Inverse inc. 
  Copyright (C) 2004-2007 SKYRIX Software AG

  This file is part of SOGo.

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

  SOGo 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 Lesser General Public
  License for more details.

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

#import <Foundation/NSArray.h>
#import <Foundation/NSCharacterSet.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSException.h>
#import <Foundation/NSProcessInfo.h>
#import <Foundation/NSURL.h>
#import <Foundation/NSUserDefaults.h>

#import <NGExtensions/NSNull+misc.h>
#import <NGExtensions/NSObject+Logs.h>

#import <GDLAccess/EOAdaptorContext.h>
#import <NGExtensions/NGResourceLocator.h>

#import "GCSChannelManager.h"
#import "EOAdaptorChannel+GCS.h"
#import "GCSAlarmsFolder.h"
#import "GCSAdminFolder.h"
#import "GCSOpenIdFolder.h"
#import "GCSFolder.h"
#import "GCSFolderType.h"
#import "GCSSessionsFolder.h"
#import "GCSSpecialQueries.h"
#import "NSURL+GCS.h"

#import "GCSFolderManager.h"

/*
  Required database schema:
  
  <arbitary table>
  c_path
  c_path1, path2, path3... [quickPathCount times]
  c_foldername
  
  TODO:
  - add a local cache?
*/

@implementation GCSFolderManager

static GCSFolderManager *fm = nil;
static BOOL       debugOn                   = NO;
static BOOL       debugSQLGen               = NO;
static BOOL       debugPathTraversal        = NO;
static int        quickPathCount            = 4;
static int        randInc = 0;
static NSArray    *emptyArray               = nil;
#if 0
static NSString   *GCSPathColumnName        = @"c_path";
static NSString   *GCSTypeColumnName        = @"c_folder_type";
static NSString   *GCSTypeRecordName        = @"c_folder_type";
#endif
static NSString   *GCSPathRecordName        = @"c_path";
static NSString   *GCSGenericFolderTypeName = @"Container";
static const char *GCSPathColumnPattern     = "c_path%i";
static NSCharacterSet *asciiAlphaNumericCS  = nil;
static BOOL       _singleStoreMode           = NO;

+ (void) initialize
{
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  int seed;

  seed = ((unsigned int) [[NSDate date] timeIntervalSince1970]
	  + [[NSProcessInfo processInfo] processIdentifier]);
  srand (seed);

  debugOn     = [ud boolForKey: @"GCSFolderManagerDebugEnabled"];
  debugSQLGen = [ud boolForKey: @"GCSFolderManagerSQLDebugEnabled"];
  emptyArray  = [[NSArray alloc] init];
  if (!asciiAlphaNumericCS)
    {
      asciiAlphaNumericCS
	= [NSCharacterSet characterSetWithCharactersInString:
			    @"0123456789"
			  @"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
			  @"abcdefghijklmnopqrstuvwxyz"];
      [asciiAlphaNumericCS retain];
    }

  if ([ud stringForKey: @"OCSStoreURL"] &&
      [ud stringForKey: @"OCSAclURL"] &&
      [ud stringForKey: @"OCSCacheFolderURL"])
    _singleStoreMode = YES;
}

+ (BOOL) singleStoreMode
{
  return _singleStoreMode;
}

+ (id)defaultFolderManager {
  NSString *s;
  NSURL    *infoUrl;
  NSURL    *storeUrl;
  NSURL    *aclUrl;
  NSURL    *cacheFolderUrl;

  if (!fm)
    {
      s = [[NSUserDefaults standardUserDefaults] stringForKey:@"OCSFolderInfoURL"];
      if ([s length] == 0) {
	NSLog(@"ERROR(%s): default 'OCSFolderInfoURL' is not configured.",
	      __PRETTY_FUNCTION__);
	return nil;
      }
      if ((infoUrl = [NSURL URLWithString:s]) == nil) {
	NSLog(@"ERROR(%s): default 'OCSFolderInfoURL' is not a valid URL: '%@'",
	      __PRETTY_FUNCTION__, s);
	return nil;
      }

      if (_singleStoreMode)
        {
          s = [[NSUserDefaults standardUserDefaults] stringForKey:@"OCSStoreURL"];
          if ([s length] == 0) {
            NSLog(@"ERROR(%s): default 'OCSStoreURL' is not configured.",
                  __PRETTY_FUNCTION__);
            return nil;
          }
          if ((storeUrl = [NSURL URLWithString:s]) == nil) {
            NSLog(@"ERROR(%s): default 'OCSStoreURL' is not a valid URL: '%@'",
                  __PRETTY_FUNCTION__, s);
            return nil;
          }
          s = [[NSUserDefaults standardUserDefaults] stringForKey:@"OCSAclURL"];
          if ([s length] == 0) {
            NSLog(@"ERROR(%s): default 'OCSAclURL' is not configured.",
                  __PRETTY_FUNCTION__);
            return nil;
          }
          if ((aclUrl = [NSURL URLWithString:s]) == nil) {
            NSLog(@"ERROR(%s): default 'OCSAclURL' is not a valid URL: '%@'",
                  __PRETTY_FUNCTION__, s);
            return nil;
          }
          s = [[NSUserDefaults standardUserDefaults] stringForKey:@"OCSCacheFolderURL"];
          if ([s length] == 0) {
            NSLog(@"ERROR(%s): default 'OCSCacheFolderURL' is not configured.",
                  __PRETTY_FUNCTION__);
            return nil;
          }
          if ((cacheFolderUrl = [NSURL URLWithString:s]) == nil) {
            NSLog(@"ERROR(%s): default 'OCSCacheFolderURL' is not a valid URL: '%@'",
                  __PRETTY_FUNCTION__, s);
            return nil;
          }
        }
      else
        {
          storeUrl = nil;
          aclUrl = nil;
          cacheFolderUrl = nil;
        }

      if ((fm = [[self alloc] initWithFolderInfoLocation: infoUrl 
                                        andStoreLocation: storeUrl
                                          andAclLocation: aclUrl
                                  andCacheFolderLocation: cacheFolderUrl]) == nil) {
	NSLog(@"ERROR(%s): could not create folder manager with URLs: '%@', '%@', '%@'",
	      __PRETTY_FUNCTION__, [infoUrl absoluteString],
              [storeUrl absoluteString], [aclUrl absoluteString]);
	return nil;
      }
      if (debugOn)
        [self debugWithFormat:@"Note: setup default manager at: %@", infoUrl];
    }
      
  return fm;
}

- (NSDictionary *) loadDefaultFolderTypes: (NSString *) driver
{
  NSMutableDictionary *typeMap;
  NSArray  *types;
  unsigned i, count;
  
  
  types = [[GCSFolderType resourceLocator] lookupAllFilesWithExtension:@"ocs"
					   doReturnFullPath:NO];
  if ((count = [types count]) == 0) {
    [self logWithFormat:@"Note: no GCS folder types found."];
    return nil;
  }
  
  typeMap = [NSMutableDictionary dictionaryWithCapacity:count];
  
  [self debugWithFormat:@"Note: loading %d GCS folder types:", count];
  for (i = 0, count = [types count]; i < count; i++) {
    NSString *type;
    GCSFolderType *typeObject;
    
    type       = [[types objectAtIndex:i] stringByDeletingPathExtension];

    // We skip type files associated to drivers directly, since
    // [GCSFolderType folderTypeWithName:driver] will handle the
    // mojo directly
    if ([type rangeOfString: [NSString stringWithFormat: @"-%@", driver]].length)
      continue;

    typeObject = [GCSFolderType folderTypeWithName: type
                                            driver: driver];
    
    [self debugWithFormat:@"  %@: %s", 
	  type, [typeObject isNotNull] ? "OK" : "FAIL"];
    [typeMap setObject: typeObject forKey:type];
  }
  
  return typeMap;
}

- (id)initWithFolderInfoLocation: (NSURL *)_infoUrl 
                andStoreLocation: (NSURL *)_storeUrl
                  andAclLocation: (NSURL *)_aclUrl
          andCacheFolderLocation: (NSURL *)_cacheFolderUrl {
  if (_infoUrl == nil) {
    [self logWithFormat:@"ERROR(%s): missing folder info url!", 
	  __PRETTY_FUNCTION__];
    [self release];
    return nil;
  }

  if (_singleStoreMode)
    {
      if (_storeUrl == nil) {
        [self logWithFormat:@"ERROR(%s): missing folder store url!",
              __PRETTY_FUNCTION__];
        [self release];
        return nil;
      }
      if (_aclUrl == nil) {
        [self logWithFormat:@"ERROR(%s): missing folder acl url!",
              __PRETTY_FUNCTION__];
        [self release];
        return nil;
      }
      if (_cacheFolderUrl == nil) {
        [self logWithFormat:@"ERROR(%s): missing cache folder url!",
              __PRETTY_FUNCTION__];
        [self release];
        return nil;
      }

    }

  if ((self = [super init])) {
    channelManager = [[GCSChannelManager defaultChannelManager] retain];
    folderInfoLocation = [_infoUrl retain];

    if (_singleStoreMode)
      {
        storeLocation = [_storeUrl retain];
        aclLocation = [_aclUrl retain];
        cacheFolderLocation = [_cacheFolderUrl retain];
      }
    else
      {
        storeLocation = nil;
        aclLocation = nil;
        cacheFolderLocation = nil;
      }

    if ([[self folderInfoTableName] length] == 0) {
      [self logWithFormat:@"ERROR(%s): missing tablename in URL: %@",
            __PRETTY_FUNCTION__, [_infoUrl absoluteString]];
      [self release];
      return nil;
    }

    if (_singleStoreMode && [[self storeTableName] length] == 0) {
      [self logWithFormat:@"ERROR(%s): missing tablename in URL: %@",
            __PRETTY_FUNCTION__, [_storeUrl absoluteString]];
      [self release];
      return nil;
    }

    if (_singleStoreMode && [[self aclTableName] length] == 0) {
      [self logWithFormat:@"ERROR(%s): missing tablename in URL: %@",
            __PRETTY_FUNCTION__, [_aclUrl absoluteString]];
      [self release];
      return nil;
    }

    if (_singleStoreMode && [[self cacheFolderTableName] length] == 0) {
      [self logWithFormat:@"ERROR(%s): missing tablename in URL: %@",
            __PRETTY_FUNCTION__, [_cacheFolderUrl absoluteString]];
      [self release];
      return nil;
    }
    
    /* register default folder types */
    nameToType = [[self loadDefaultFolderTypes: [_infoUrl scheme]] copy];
  }
  return self;
}

- (void) dealloc
{
  [nameToType         release];
  [folderInfoLocation release];
  [channelManager     release];
  [super dealloc];
}

/* accessors */

- (NSURL *)folderInfoLocation {
  return folderInfoLocation;
}

- (NSString *)folderInfoTableName {
  return [[self folderInfoLocation] gcsTableName];
}

- (NSURL *)storeLocation {
  return storeLocation;
}

- (NSString *)storeTableName {
  return [[self storeLocation] gcsTableName];
}

- (NSURL *)aclLocation {
  return aclLocation;
}

- (NSString *)aclTableName {
  return [[self aclLocation] gcsTableName];
}

- (NSURL *)cacheFolderLocation {
  return cacheFolderLocation;
}

- (NSString *)cacheFolderTableName {
  return [[self cacheFolderLocation] gcsTableName];
}


/* connection */

- (GCSChannelManager *)channelManager {
  return channelManager;
}

- (EOAdaptorChannel *)acquireOpenChannel {
  EOAdaptorChannel *ch;
  
  ch = [[self channelManager] acquireOpenChannelForURL:
				[self folderInfoLocation]];

  return ch;
}
- (void)releaseChannel:(EOAdaptorChannel *)_channel {
  if (debugOn) [self debugWithFormat:@"releasing channel: %@", _channel];
  [[self channelManager] releaseChannel:_channel];
}

- (BOOL)canConnect {
  return [[self channelManager] canConnect:[self folderInfoLocation]];
}

- (NSArray *)performSQL:(NSString *)_sql {
  EOAdaptorChannel *channel;
  NSException    *ex;
  NSMutableArray *rows;
  NSDictionary   *row;
  NSArray        *attrs;
  
  /* acquire channel */
  
  if ((channel = [self acquireOpenChannel]) == nil) {
    if (debugOn) [self debugWithFormat:@"could not acquire channel!"];
    return nil;
  }
  if (debugOn) [self debugWithFormat:@"acquired channel: %@", channel];
  
  /* run SQL */
  
  if ((ex = [channel evaluateExpressionX:_sql]) != nil) {
    [self logWithFormat:@"ERROR(%s): cannot execute\n  SQL '%@':\n  %@", 
	  __PRETTY_FUNCTION__, _sql, ex];
    [self releaseChannel:channel];
    return nil;
  }
  
  /* fetch results */
  
  attrs = [channel describeResults:NO /* do not beautify names */];
  rows = [NSMutableArray arrayWithCapacity:16];
  while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil)
    [rows addObject:row];
  
  [self releaseChannel:channel];
  return rows;
}

/* row factory */

- (GCSFolder *)folderForRecord:(NSDictionary *)_record {
  GCSFolder     *folder;
  GCSFolderType *folderType;
  NSString      *folderTypeName, *locationString, *folderName, *path;
  NSNumber      *folderId;
  NSURL         *location, *quickLocation, *acl_location;
  
  if (_record == nil) return nil;
  
  folderTypeName = [_record objectForKey:@"c_folder_type"];
  if (![folderTypeName isNotNull]) {
    [self logWithFormat:@"ERROR(%s): missing type in folder: %@",
	  __PRETTY_FUNCTION__, _record];
    return nil;
  }
  if ((folderType = [self folderTypeWithName:folderTypeName]) == nil) {
    [self logWithFormat:
	    @"ERROR(%s): could not resolve type '%@' of folder: %@",
	  __PRETTY_FUNCTION__,
	  folderTypeName, [_record valueForKey:@"c_path"]];
    return nil;
  }
  
  folderId   = [_record objectForKey:@"c_folder_id"];
  folderName = [_record objectForKey:@"c_path"];
  path       = [self pathFromInternalName:folderName];

  if (!_singleStoreMode) {
    locationString = [_record objectForKey:@"c_location"];
    location = [locationString isNotNull] 
      ? [NSURL URLWithString:locationString]
      : nil;
    if (location == nil) {
      [self logWithFormat:@"ERROR(%s): missing folder location in record: %@",
            __PRETTY_FUNCTION__, _record];
      return nil;
    }

    locationString = [_record objectForKey:@"c_quick_location"];
    quickLocation = [locationString isNotNull] 
      ? [NSURL URLWithString:locationString]
      : nil;

    if (quickLocation == nil) {
      [self logWithFormat:@"WARNING(%s): missing quick location in record: %@",
            __PRETTY_FUNCTION__, _record];
    }

    locationString = [_record objectForKey:@"c_acl_location"];
    acl_location = [locationString isNotNull]
      ? [NSURL URLWithString:locationString]
      : nil;
  } else {
    location = nil;
    quickLocation = nil;
    acl_location = nil;
  }

  folder = [[GCSFolder alloc] initWithPath:path primaryKey:folderId
			      folderTypeName:folderTypeName 
			      folderType:folderType
			      location:location quickLocation:quickLocation
                              aclLocation:acl_location
			      folderManager:self];
  return [folder autorelease];
}

- (GCSAlarmsFolder *) alarmsFolder
{
  return [GCSAlarmsFolder alarmsFolderWithFolderManager: self];
}


/* sessions */
- (GCSSessionsFolder *)sessionsFolder
{
  return [GCSSessionsFolder sessionsFolderWithFolderManager: self];
}

/* admin */
- (GCSAdminFolder *) adminFolder
{
  return [GCSAdminFolder adminFolderWithFolderManager: self];
}

/* openId */
- (GCSOpenIdFolder *) openIdFolder
{
  return [GCSOpenIdFolder openIdFolderWithFolderManager: self];
}

- (NSString *)generateSQLWhereForInternalNames:(NSArray *)_names
				    exactMatch:(BOOL)_beExact orDirectSubfolderMatch:(BOOL)_directSubs
{
  /* generates a WHERE qualifier for matching the "quick" entries */
  NSMutableString *sql;
  unsigned i, count;
  
  if ((count = [_names count]) == 0) {
    [self debugWithFormat:@"WARNING(%s): passed in empty name array!",
	  __PRETTY_FUNCTION__];
    return @"1 = 2";
  }
  
  sql = [NSMutableString stringWithCapacity:(count * 8)];
  for (i = 0; i < quickPathCount; i++) {
    NSString *pathColumn;
    char buf[32];
    
    sprintf(buf, GCSPathColumnPattern, (i + 1));
    pathColumn = [[NSString alloc] initWithCString:buf];
    
    /* Note: the AND addition must be inside the if's for non-exact stuff */
    
    if (i < count) {
      /* exact match, regular column */
      if ([sql length] > 0) [sql appendString:@" AND "];
      [sql appendString:pathColumn];
      [sql appendFormat:@" = '%@'", [_names objectAtIndex:i]];
    }
    else if (_beExact) {
      /* exact match, ensure that all additional quick-cols are NULL */
      if ([sql length] > 0) [sql appendString:@" AND "];
      [sql appendString:pathColumn];
      [sql appendString:@" IS NULL"];
      if (debugPathTraversal) [self logWithFormat:@"BE EXACT, NULL columns"];
    }
    else if (_directSubs) {
      /* fetch immediate subfolders */
      if ([sql length] > 0) [sql appendString:@" AND "];
      [sql appendString:pathColumn];
      if (i == count) {
	/* if it is a direct subfolder, the next path cannot be empty */
	[sql appendString:@" IS NOT NULL"];
	if (debugPathTraversal)
	  [self logWithFormat:@"DIRECT SUBS, first level"];
      }
      else {
	/* but for 'direct' subfolders, all following things must be empty */
	[sql appendString:@" IS NULL"];
	if (debugPathTraversal) 
	  [self logWithFormat:@"DIRECT SUBS, lower level"];
      }
    }
    
    [pathColumn release];
  }
  
  if (_beExact && (count > quickPathCount)) {
    [sql appendString:@" AND c_foldername = '"];
    [sql appendString:[_names lastObject]];
    [sql appendString:@"'"];
  }
  
  return sql;
}

- (NSString *)generateSQLPathFetchForInternalNames:(NSArray *)_names
					exactMatch:(BOOL)_beExact orDirectSubfolderMatch:(BOOL)_directSubs
{
  /* fetches the 'path' subset for a given quick-names */
  NSMutableString *sql;
  NSString *ws;
  
  ws = [self generateSQLWhereForInternalNames:_names 
	     exactMatch:_beExact orDirectSubfolderMatch:_directSubs];
  if ([ws length] == 0)
    return nil;
  
  sql = [NSMutableString stringWithCapacity:256];
  [sql appendString:@"SELECT c_path FROM "];
  [sql appendString:[self folderInfoTableName]];
  [sql appendString:@" WHERE "];
  [sql appendString:ws];
  if (debugSQLGen) [self logWithFormat:@"PathFetch-SQL: %@", sql];
  return sql;
}

- (NSString *)generateSQLPathAndNameFetchForInternalNames:(NSArray *)_names
					exactMatch:(BOOL)_beExact orDirectSubfolderMatch:(BOOL)_directSubs
{
  /* fetches the 'path' subset for a given quick-names */
  NSMutableString *sql;
  NSString *ws;
  
  ws = [self generateSQLWhereForInternalNames:_names 
	     exactMatch:_beExact orDirectSubfolderMatch:_directSubs];
  if ([ws length] == 0)
    return nil;
  
  sql = [NSMutableString stringWithCapacity:256];
  [sql appendString:@"SELECT c_path, c_foldername FROM "];
  [sql appendString:[self folderInfoTableName]];
  [sql appendString:@" WHERE "];
  [sql appendString:ws];
  if (debugSQLGen) [self logWithFormat:@"PathFetch-SQL: %@", sql];
  return sql;
}

/* handling folder names */

- (BOOL)_isStandardizedPath:(NSString *)_path {
  if (![_path isAbsolutePath])                return NO;
  if ([_path rangeOfString:@".."].length > 0) return NO;
  if ([_path rangeOfString:@"~"].length  > 0) return NO;
  if ([_path rangeOfString:@"//"].length > 0) return NO;
  return YES;
}

- (NSString *) internalNameFromPath: (NSString *) _path
{
  NSString *name;

  // TODO: ensure proper path and SQL escaping!

  if ([self _isStandardizedPath:_path])
    {
      if ([_path hasSuffix:@"/"] && [_path length] > 1)
	name = [_path substringToIndex: ([_path length] - 1)];
      else
	name = _path;
    }
  else
    {
      [self debugWithFormat:@"%s: not a standardized path: '%@'", 
	    __PRETTY_FUNCTION__, _path];
      name = nil;
    }
  
  return name;
}

- (NSArray *) internalNamesFromPath: (NSString *) _path
{
  NSString *fname;
  NSArray  *fnames;

  fname = [self internalNameFromPath: _path];
  if (fname)
    {
      if ([fname hasPrefix:@"/"])
	fname = [fname substringFromIndex: 1];
      fnames = [fname componentsSeparatedByString:@"/"];
      if ([fnames count] == 0)
	fnames = nil;
    }
  else
    fnames = nil;
  
  return fnames;
}

- (NSString *) pathFromInternalName: (NSString *) _name
{
  /* for incomplete pathes, like '/Users/helge/' */
  return _name;
}

- (NSString *) pathPartFromInternalName: (NSString *) _name
{
  /* for incomplete pathes, like 'Users/' */
  return _name;
}

- (NSDictionary *) filterRecords: (NSArray *) _records
			 forPath: (NSString *) _path
{
  NSString *name;
  NSDictionary *record, *matchRecord;
  NSString     *recName;
  unsigned int i, count;

  matchRecord = nil;

  if (_records)
    {
      name = [self internalNameFromPath: _path];
      if (name)
	{
	  count = [_records count];
	  i = 0;
	  while (!matchRecord && i < count)
	    {
	      record = [_records objectAtIndex: i];
	      recName = [record objectForKey: GCSPathRecordName];
#if 0
	      [self logWithFormat:@"check '%@' vs '%@' (%@)...", 
		    name, recName, [_records objectAtIndex:i]];
#endif
	      
	      if ([name isEqualToString: recName])
		matchRecord = record;
	      else
		i++;
	    }
	}
    }

  return matchRecord;
}

- (BOOL) folderExistsAtPath: (NSString *) _path
{
  NSString *fname, *sname, *sql;
  NSArray  *fnames, *records;
  unsigned int count;
  NSDictionary *record;
  BOOL result;
  
  result = NO;

  fnames = [self internalNamesFromPath: _path];
  if (fnames)
    {
      sql = [self generateSQLPathFetchForInternalNames: fnames 
		  exactMatch: YES
		  orDirectSubfolderMatch: NO];
      if ([sql length])
	{
	  records = [self performSQL: sql];
	  if (records)
	    {
	      count = [records count];
	      if (count)
		{
		  fname = [self internalNameFromPath: _path];
		  if (count == 1)
		    {
		      record = [records objectAtIndex: 0];
		      sname = [record objectForKey: GCSPathRecordName];
		      result = [fname isEqualToString: sname];
		    }
		  else
		    [self logWithFormat: @"records: %@", records];
		}
	    }
	  else
	    [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'", 
		  __PRETTY_FUNCTION__, sql];
	}
      else
	[self debugWithFormat:@"got no SQL for names: %@", fnames];
    }
  else
    [self debugWithFormat:@"got no internal names for path: '%@'", _path];
  
  return result;
}

- (NSArray *)listSubFoldersAtPath:(NSString *)_path recursive:(BOOL)_recursive{
  NSMutableArray *result;
  NSString *fname;
  NSArray  *fnames, *records;
  NSString *sql;
  unsigned i, count;
  
  if ((fnames = [self internalNamesFromPath:_path]) == nil) {
    [self debugWithFormat:@"got no internal names for path: '%@'", _path];
    return nil;
  }
  
  sql = [self generateSQLPathFetchForInternalNames:fnames 
	      exactMatch:NO orDirectSubfolderMatch:(_recursive ? NO : YES)];
  if ([sql length] == 0) {
    [self debugWithFormat:@"got no SQL for names: %@", fnames];
    return nil;
  }
  
  if ((records = [self performSQL:sql]) == nil) {
    [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'", 
	  __PRETTY_FUNCTION__, sql];
    return nil;
  }
  
  if ((count = [records count]) == 0)
    return emptyArray;

  result = [NSMutableArray arrayWithCapacity:(count > 128 ? 128 : count)];
  
  fname = [self internalNameFromPath:_path];
  fname = [fname stringByAppendingString:@"/"]; /* add slash */
  for (i = 0; i < count; i++) {
    NSDictionary *record;
    NSString *sname, *spath;
    
    record = [records objectAtIndex:i];
    sname  = [record objectForKey:GCSPathRecordName];
    if (![sname hasPrefix:fname]) /* does not match at all ... */
      continue;
    
    /* strip prefix and following slash */
    sname = [sname substringFromIndex:[fname length]];
    spath = [self pathPartFromInternalName:sname];
    
    if (_recursive) {
      if ([spath length] > 0) [result addObject:spath];
    }
    else {
      /* direct children only, so exclude everything with a slash */
      if ([sname rangeOfString:@"/"].length == 0 && [spath length] > 0)
	[result addObject:spath];
    }
  }
  
  return result;
}

- (NSArray *)listSubFoldersAndNamesAtPath:(NSString *)_path recursive:(BOOL)_recursive{
  NSMutableArray *result;
  NSString *fname;
  NSArray  *fnames, *records;
  NSString *sql;
  unsigned i, count;
  
  if ((fnames = [self internalNamesFromPath:_path]) == nil) {
    [self debugWithFormat:@"got no internal names for path: '%@'", _path];
    return nil;
  }
  
  sql = [self generateSQLPathAndNameFetchForInternalNames:fnames 
	      exactMatch:NO orDirectSubfolderMatch:(_recursive ? NO : YES)];
  if ([sql length] == 0) {
    [self debugWithFormat:@"got no SQL for names: %@", fnames];
    return nil;
  }
  
  if ((records = [self performSQL:sql]) == nil) {
    [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'", 
	  __PRETTY_FUNCTION__, sql];
    return nil;
  }
  
  if ((count = [records count]) == 0)
    return emptyArray;

  result = [NSMutableArray arrayWithCapacity:(count > 128 ? 128 : count)];
  
  fname = [self internalNameFromPath:_path];
  fname = [fname stringByAppendingString:@"/"]; /* add slash */
  for (i = 0; i < count; i++) {
    NSDictionary *record, *folderInfo;
    NSString *sname, *spath, *foldername;
    
    record = [records objectAtIndex:i];
    sname  = [record objectForKey:GCSPathRecordName];
    if (![sname hasPrefix:fname]) /* does not match at all ... */
      continue;
    
    /* strip prefix and following slash */
    sname = [sname substringFromIndex:[fname length]];
    spath = [self pathPartFromInternalName:sname];
    
    if (_recursive) {
      if ([spath length] > 0){
        foldername = [record objectForKey: @"c_foldername"];
        folderInfo = [NSDictionary dictionaryWithObjectsAndKeys: spath, @"path", foldername, @"name", nil];
	      [result addObject:folderInfo];
      } 
    }
    else {
      /* direct children only, so exclude everything with a slash */
      if ([sname rangeOfString:@"/"].length == 0 && [spath length] > 0)
      {
        foldername = [record objectForKey: @"c_foldername"];
        folderInfo = [NSDictionary dictionaryWithObjectsAndKeys: spath, @"path", foldername, @"name", nil];
	      [result addObject:folderInfo];
      }
    }
  }
  
  return result;
}

- (NSDictionary *) recordAtPath: (NSString *) _path
{
  NSMutableString *sql;
  NSArray      *fnames, *records;
  NSString     *ws;
  NSDictionary *record;
  
  if ((fnames = [self internalNamesFromPath:_path]) == nil) {
    [self debugWithFormat:@"got no internal names for path: '%@'", _path];
    return nil;
  }
  
  /* generate SQL to fetch folder attributes */
  
  ws = [self generateSQLWhereForInternalNames:fnames 
	     exactMatch:YES orDirectSubfolderMatch:NO];
  
  sql = [NSMutableString stringWithCapacity:256];
  [sql appendString:@"SELECT "];
  [sql appendString:@"c_folder_id, "];
  [sql appendString:@"c_path, "];
  [sql appendString:@"c_location, c_quick_location, c_acl_location,"];
  [sql appendString:@" c_folder_type"];
  [sql appendString:@" FROM "];
  [sql appendString:[self folderInfoTableName]];
  [sql appendString:@" WHERE "];
  [sql appendString:ws];
  
  if (debugSQLGen) [self logWithFormat:@"folderAtPath: %@", sql];

  /* fetching */
  
  if ((records = [self performSQL:sql]) == nil) {
    [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'", 
	  __PRETTY_FUNCTION__, sql];
    return nil;
  }
  
  // TODO: need to filter on path
  //         required when we start to have deeper hierarchies
  //       => isn't that already done below?
  
  if ([records count] != 1) {
    if ([records count] == 0) {
      [self debugWithFormat:@"found no records for path: '%@'", _path];
      return nil;
    }
    
    [self logWithFormat:@"ERROR(%s): more than one row for path: '%@'", 
	  __PRETTY_FUNCTION__, _path];
    return nil;
  }
  
  if ((record = [self filterRecords:records forPath:_path]) == nil) {
    [self debugWithFormat:@"found no record for path: '%@'", _path];
    return nil;
  }

  return record;
}

- (GCSFolder *) folderAtPath: (NSString *) _path
{
  return [self folderForRecord: [self recordAtPath: _path]];
}

- (NSString *) baseTableNameWithUID: (NSString *) _uid
{
  unichar currentChar;
  unsigned int count, max, done;
  NSMutableString *newUID;

  newUID = [NSMutableString stringWithString: @"sogo"];

  max = [_uid length];
  done = 0;
  count = 0;
  while (done < 8 && count < max)
    {
      currentChar = [_uid characterAtIndex: count];
      if ([asciiAlphaNumericCS characterIsMember: currentChar])
	{
	  [newUID appendFormat: @"%c", currentChar];
	  done++;
	}
      count++;
    }
  randInc++;

  return [NSString stringWithFormat: @"%@%.3x%.8x",
		   newUID, randInc & 0xfff, (unsigned int) rand()];
}

- (NSException *) _reallyCreateFolderWithName: (NSString *) folderName
				andFolderType: (NSString *) folderType
				      andType: (GCSFolderType *) ftype
				   andChannel: (EOAdaptorChannel *) channel
				       atPath: (NSString *) path
{
  NSString *baseURL, *tableName, *quickTableName, *aclTableName, *createQuery, *sql;
  GCSSpecialQueries *specialQuery;
  EOAdaptorContext *aContext;
  NSMutableArray *paths;
  NSException *error;
  NSRange range;
  
  paths = [NSMutableArray arrayWithArray: [path componentsSeparatedByString: @"/"]];

  while ([paths count] < 5)
    [paths addObject: @"NULL"];
  
  aContext = [channel adaptorContext];
  [aContext beginTransaction];

  tableName = [self baseTableNameWithUID: [paths objectAtIndex: 2]];
  quickTableName = [tableName stringByAppendingString: @"_quick"];
  aclTableName = [tableName stringByAppendingString: @"_acl"];

  // TBD: fix SQL injection issues.
  // We no longer call stringByDeletingLastPathComponent since, since GNUstep 1.22,
  // it'll replace // characters in the URL with /, so mysql:// becomes mysql:/
  // This is to conform with recent Apple changes.
  baseURL = [folderInfoLocation absoluteString];
  range = [baseURL rangeOfString: @"/" options: NSBackwardsSearch];
  if (range.location != NSNotFound)
    baseURL = [baseURL substringToIndex: range.location];
  
  sql = [NSString stringWithFormat: @"INSERT INTO %@"
		  @"        (c_path, c_path1, c_path2, c_path3, c_path4,"
		  @"         c_foldername, c_location, c_quick_location,"
		  @"         c_acl_location, c_folder_type)"
		  @" VALUES ('%@', '%@', '%@', '%@', '%@', '%@', '%@/%@',"
		  @"         '%@/%@', '%@/%@', '%@')",
		  [self folderInfoTableName], path,
		  [paths objectAtIndex: 1], [paths objectAtIndex: 2],
		  [paths objectAtIndex: 3], [paths objectAtIndex: 4],
		  [folderName stringByReplacingString: @"'" withString: @"''"],
		  baseURL, tableName,
		  baseURL, quickTableName,
		  baseURL, aclTableName,
		  folderType];
  error = [channel evaluateExpressionX: sql];
  if (!_singleStoreMode && !error)
    {
      specialQuery = [channel specialQueries];
      createQuery = [specialQuery createFolderTableWithName: tableName];
      error = [channel evaluateExpressionX: createQuery];
      if (!error)
	{
	  sql = [ftype sqlQuickCreateWithTableName: quickTableName];
	  error = [channel evaluateExpressionX: sql];
	  if (!error)
            {
              createQuery = [specialQuery
                              createFolderACLTableWithName: aclTableName];
              error = [channel evaluateExpressionX: createQuery];
            }
	}
    }

  if (error)
    [aContext rollbackTransaction];
  else
    [aContext commitTransaction];

  return error;
}

- (NSException *) createFolderOfType: (NSString *) _type
			    withName: (NSString*) _name
			      atPath: (NSString *) _path
{
  // TBD: would be best to perform all operations as a single SQL statement.
  GCSFolderType    *ftype;
  EOAdaptorChannel *channel;
  NSException      *error;

  // TBD: fix SQL injection issue!
  if ([self folderExistsAtPath: _path])
    error = [NSException exceptionWithName: @"GCSExitingFolder"
			 reason: @"a folder already exists at that path"
			 userInfo: nil];
  else
    {
      ftype = [self folderTypeWithName:_type];
      if (ftype)
	{
	  channel = [self acquireOpenChannel];
	  if (channel)
	    {
	      error = [self _reallyCreateFolderWithName: _name
			    andFolderType: _type
			    andType: ftype andChannel: channel
			    atPath: _path];
	      if (error && [self folderExistsAtPath: _path])
		error = nil;

	      [self releaseChannel: channel];
	    }
	  else
	    error = [NSException exceptionWithName: @"GCSNoChannel"
				 reason: @"could not open channel"
				 userInfo: nil];
	}
      else
	error = [NSException exceptionWithName: @"GCSMissingFolderType"
			     reason: @"missing folder type"
			     userInfo: nil];
    }

  return error;
}
  
- (NSException *) deleteFolderAtPath: (NSString *) _path
{
  GCSFolder    *folder;
  NSArray      *fnames;
  NSString     *sql, *ws;
  EOAdaptorChannel *channel;
  NSException *ex;

  if ((folder = [self folderAtPath:_path]) == nil) {
    return [NSException exceptionWithName:@"GCSMissingFolder"
			reason:@"missing folder"
			userInfo:nil];
  }

  if ((fnames = [self internalNamesFromPath:_path]) == nil) {
    [self debugWithFormat:@"got no internal names for path: '%@'", _path];
    return nil;
  }

  ws = [self generateSQLWhereForInternalNames:fnames 
	     exactMatch:YES orDirectSubfolderMatch:NO];

  sql = [NSString stringWithFormat: @"DELETE FROM %@ WHERE %@",
		  [self folderInfoTableName], ws];
  if ((channel = [self acquireOpenChannel]) == nil) {
    return [NSException exceptionWithName:@"GCSNoChannel"
			reason:@"could not "
			userInfo:nil];
  }

  [[channel adaptorContext] beginTransaction];

  if ((ex = [channel evaluateExpressionX:sql]) != nil) {
    [self releaseChannel:channel];
    return ex;
  }

  [[channel adaptorContext] commitTransaction];
  [self releaseChannel:channel];

  return [folder deleteFolder];
}

/* folder types */

- (GCSFolderType *) folderTypeWithName: (NSString *) _name
{
  NSString *specificName;
  GCSFolderType *type;

  if ([_name length] == 0)
    _name = GCSGenericFolderTypeName;

  specificName = [NSString stringWithFormat: @"%@-%@",
			   _name, [folderInfoLocation scheme]];
  type = [nameToType objectForKey: [specificName lowercaseString]];
  if (!type)
    type = [nameToType objectForKey:[_name lowercaseString]];

  return type;
}

/* cache management */

- (void) reset
{
  /* does nothing in the moment, but we need a way to signal refreshes */
}

/* debugging */

- (BOOL) isDebuggingEnabled
{
  return debugOn;
}

/* description */

- (NSString *) description
{
  NSMutableString *ms;

  ms = [NSMutableString stringWithCapacity:256];
  [ms appendFormat:@"<0x%p[%@]:", self, NSStringFromClass([self class])];
  
  [ms appendFormat:@" url=%@", [folderInfoLocation absoluteString]];
  [ms appendFormat:@" channel-manager=0x%p", [self channelManager]];
  
  [ms appendString:@">"];

  return ms;
}

@end /* GCSFolderManager */
