/*
**  LocalStore.m
**
**  Copyright (c) 2001, 2002, 2003
**
**  Author: Ludovic Marcotte <ludovic@Sophos.ca>
**
**  This library 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.1 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
**  Lesser General Public License for more details.
**  
**  You should have received a copy of the GNU Lesser General Public
**  License along with this library; if not, write to the Free Software
**  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/

#include <Pantomime/LocalStore.h>

#include <Pantomime/Constants.h>
#include <Pantomime/LocalFolder.h>
#include <Pantomime/URLName.h>

#include <Foundation/NSAutoreleasePool.h>
#include <Foundation/NSPathUtilities.h>
#include <Foundation/NSValue.h>

@implementation LocalStore

//
//
//
- (void) dealloc
{
  RELEASE(path);
  RELEASE(folders);
  RELEASE(fileManager);
  RELEASE(folderArray);
  
  [super dealloc];
}


//
//
//
- (id) initWithPathToDirectory: (NSString *) thePath
{
  BOOL isDirectory;
  
  self = [super init];
  
  [self setPath: thePath];
  
  // We initialize our cache of opened folders
  folders = [[NSMutableDictionary alloc] init];
  // Create an array to cache our folder structure
  folderArray = [[NSMutableArray alloc] initWithArray:
	  [[fileManager enumeratorAtPath:
		  [self path]] allObjects]];
  
   
  fileManager = [NSFileManager defaultManager];
  RETAIN(fileManager);
  
  if ( [fileManager fileExistsAtPath: [self path]
		    isDirectory: &isDirectory] )
    {
      if ( !isDirectory )
	{
	  AUTORELEASE(self);
	  return nil;
	}
    }
  else
    {
      AUTORELEASE(self);
      return nil;
    }

  // Just before returning, we finally enforce our file attributes
  [self _enforceFileAttributes];


  return self;
}


//
//
//
- (id) initWithURL: (NSString *) theURL;
{
  URLName *aURLName;

  aURLName = [[URLName alloc] initWithString: theURL];
  
  self = [self initWithPathToDirectory: [aURLName path]];

  RELEASE(aURLName);
  
  return self;
}


//
// This method will open automatically Inbox (case-insensitive).
// It may return nil if the opening failed or Inbox wasn't found.
//
- (id) defaultFolder
{
  return [self folderForName: @"Inbox"];
}


//
// This method is used to open the folder theName in the current
// directory of this local store.
//
- (id) folderForName: (NSString *) theName
{
  NSEnumerator *anEnumerator;
  NSString *aString;
  Folder *cachedFolder;

  anEnumerator = [self folderEnumerator];
  cachedFolder = [folders objectForKey: theName];
  
  if ( [self folderForNameIsOpen: theName] )
    {
      return nil;
    }
  
  if ( !cachedFolder )
    {
      while ( (aString = [anEnumerator nextObject]) )
	{
	  if ( [aString compare: theName] == NSOrderedSame )
	    {
	      LocalFolder *aFolder;

		  aFolder = [[LocalFolder alloc] initWithPathToFile:
					       [NSString stringWithFormat:@"%@/%@",
							 [self path], aString]];

	      if ( aFolder )
		{
		  [aFolder setStore: (Store *)self];
		  [aFolder setName: theName];
		  [aFolder parse];

		  // We now cache it and return it
		  [folders setObject: AUTORELEASE(aFolder)
			   forKey: theName];
		}

	      return aFolder;
	    }
	}
      
      return nil; // Never reached?
    }
  else
    {
	  return cachedFolder;
    }
}


//
//
//
- (id) folderForURL: (NSString *) theURL;
{
  URLName *urlName;
  id aFolder;

  urlName = [[URLName alloc] initWithString: theURL];

  aFolder = [self folderForName: [urlName foldername]];

  RELEASE(urlName);
  
  return aFolder;
}


//
// This method returns the list of folders contained in 
// a specific directory. It'll currently ignore some things
// like Netscape Mail's summary files and Pantomime's local
// cache files.
//
- (NSEnumerator *) folderEnumerator
{
  if ( [folderArray count] > 0 )
    {
      return [folderArray objectEnumerator];
    }

  return [self _rebuildFolderEnumerator];
}


//
//
//
- (NSEnumerator *) subscribedFolderEnumerator
{
  return [self folderEnumerator];
}


//
//
//
- (NSString *) path
{
  return path;
}


//
//
//
- (void) setPath: (NSString *) thePath
{
  RETAIN(thePath);
  RELEASE(path);
  path = thePath;
}


//
//
//
- (void) close
{
  NSEnumerator *anEnumerator;
  LocalFolder *aFolder;

  anEnumerator = [self openedFoldersEnumerator];
  
  while ( (aFolder = [anEnumerator nextObject]) )
    {
      [aFolder close];
    }
}


//
//
//
- (NSEnumerator *) openedFoldersEnumerator
{
  return [folders objectEnumerator];
}


//
//
//
- (void) removeFolderFromOpenedFolders: (Folder *) theFolder
{
  [folders removeObjectForKey: [(LocalFolder *)theFolder name]];
}


//
//
//
- (BOOL) folderForNameIsOpen: (NSString *) theName
{
  NSEnumerator *anEnumerator;
  LocalFolder *aFolder;
  
  anEnumerator = [self openedFoldersEnumerator];

  while ( (aFolder = [anEnumerator nextObject]) )
    {
      if ( [[aFolder name] compare: theName] == NSOrderedSame)
	{
	  return YES;
	}
    }

  return NO;
}


//
//
//
- (int) folderTypeForFolderName: (NSString *) theName
{
  NSString *aString;
  BOOL isDir;

  aString = [NSString stringWithFormat: @"%@/%@", [self path], theName];
  
  [[NSFileManager defaultManager] fileExistsAtPath: aString
				  isDirectory: &isDir];
  
  if ( isDir )
    {
      // This could be a maildir store. Check for maildir specific subfolders.
      aString = [NSString stringWithFormat: @"%@/%@/cur", [self path], theName];
      
      if ( [[NSFileManager defaultManager] fileExistsAtPath: aString
					   isDirectory: &isDir] && isDir )
	{
	  return PantomimeHoldsMessages;
	}
      else
	{
	  return PantomimeHoldsFolders;
	}
    }

  return PantomimeHoldsMessages;
}


//
//
//
- (NSString *) folderSeparator
{
  return @"/";
}


//
// theName must be the full path of the mailbox.
//
// Returns YES on success. NO otherwise.
//
- (BOOL) createFolderWithName: (NSString *) theName 
			 type: (int) theType
		     contents: (NSData *) theContents
{
  NSString *aName, *pathToFile;
  BOOL aBOOL, isDir;

  NSFileManager *aFileManager;
  NSEnumerator *anEnumerator;

  aFileManager = [NSFileManager defaultManager];
  anEnumerator = [self folderEnumerator];
  
  pathToFile = [NSString stringWithFormat: @"%@/%@", [self path], theName];
  pathToFile = [pathToFile substringToIndex: ([pathToFile length] - [[pathToFile lastPathComponent] length] - 1)];
  
  // We verify if the folder with that name does already exist
  while ( (aName = [anEnumerator nextObject]) )
    {
      if ([aName compare: theName
		 options: NSCaseInsensitiveSearch] == NSOrderedSame)
	{
	  return NO;
	}
    }
  
  // Ok, the folder doesn't already exist.
  // Check if we want to create a simple folder/directory.
  if (theType == MAILBOX_FORMAT_FOLDER)
    {
      NSString *aString;

      aString = [NSString stringWithFormat: @"%@/%@", [self path], theName];
      aBOOL = [aFileManager createDirectoryAtPath: aString  attributes: nil];
      
      if ( aBOOL )
	{
	  [self enforceMode: 0700  atPath: aString];
	  
	  // rebuild the folder list
	  [self _rebuildFolderEnumerator];
	}

      return aBOOL;
    }
  
  aBOOL = NO;

  // We want to create a mailbox store; check if it already exists.
  if ( [aFileManager fileExistsAtPath: pathToFile
		     isDirectory: &isDir] )
    {
      int size;
      
      size = [[[aFileManager fileAttributesAtPath: pathToFile
			     traverseLink: NO] objectForKey: NSFileSize] intValue];

      // If we got an empty file or simply a directory...
      if ( size == 0 || isDir )
	{
	  NSString *aString;
	  
	  // If the size is 0, that means we have an empty file. We first convert this
	  // file to a directory.
	  if ( size == 0 )
	    {
	      [aFileManager removeFileAtPath: pathToFile  handler: nil];
	      [aFileManager createDirectoryAtPath: pathToFile  attributes: nil];
	    }
	  
	  // We can now proceed with the creation of our store.
	  // Check the type of store we want to create
	  switch (theType)
	    {
	    case MAILBOX_FORMAT_MAILDIR:
	      // Create the main maildir directory
	      aString = [NSString stringWithFormat: @"%@/%@", [self path], theName];  
	      aBOOL = [aFileManager createDirectoryAtPath: aString  attributes: nil];
	      [self enforceMode: 0700  atPath: aString];
								    
	      // Now create the cur, new, and tmp sub-directories.
	      aString = [NSString stringWithFormat: @"%@/%@/cur", [self path], theName];
	      aBOOL = aBOOL & [aFileManager createDirectoryAtPath: aString  attributes: nil];
	      [self enforceMode: 0700  atPath: aString];
	      
	      // new
	      aString = [NSString stringWithFormat: @"%@/%@/new", [self path], theName];
	      aBOOL = aBOOL & [aFileManager createDirectoryAtPath: aString  attributes: nil];
	      [self enforceMode: 0700  atPath: aString];

	      // tmp
	      aString = [NSString stringWithFormat: @"%@/%@/tmp", [self path], theName];
	      aBOOL = aBOOL & [aFileManager createDirectoryAtPath: aString  attributes: nil];
	      [self enforceMode: 0700  atPath: aString];
	      break;
	      
	    case MAILBOX_FORMAT_MBOX:
	    default:
	      aBOOL = [aFileManager createFileAtPath: [NSString stringWithFormat: @"%@/%@",
								[self path], theName]
				    contents: theContents
				    attributes: nil ];
	      
	      // We now enforce the mode (0600) on this new mailbox
	      [self enforceMode: 0600
		    atPath: [NSString stringWithFormat: @"%@/%@", [self path], theName]];
	      break;				  
	    }
	  
	  // rebuild the folder list
	  [self _rebuildFolderEnumerator];
	}
      else
	{
	  aBOOL = NO;
	}
    }
	      
  return aBOOL;
}


//
// theName must be the full path of the mailbox.
//
// Returns YES on success. NO otherwise.
//
- (BOOL) deleteFolderWithName: (NSString *) theName
{
  NSFileManager *aFileManager;
  BOOL aBOOL, isDir;
  
  aFileManager = [NSFileManager defaultManager];
  aBOOL = NO;

  if ( [aFileManager fileExistsAtPath: [NSString stringWithFormat: @"%@/%@", [self path], theName]
		     isDirectory: &isDir] )
    {
      if ( isDir )
	{
	  NSEnumerator *theEnumerator;
	  NSArray *theEntries, *dirContents;
	  
	  theEnumerator = [aFileManager enumeratorAtPath: [NSString stringWithFormat: @"%@/%@",
								    [self path], theName]];
	  
	  // FIXME - Verify the Store's path.
	  // If it doesn't contain any mailboxes and it's actually not or Store's path, we remove it.
	  theEntries = [theEnumerator allObjects];
	  dirContents = [aFileManager directoryContentsAtPath: [NSString stringWithFormat: @"%@/%@",
								    [self path], theName]];
	  if ( [theEntries count] == 0 )
	    {
	      aBOOL = [aFileManager removeFileAtPath: [NSString stringWithFormat: @"%@/%@",
							       [self path], theName]
				   handler: nil];
	      
	      // Rebuild the folder tree
	      if(aBOOL == YES)
		  [self _rebuildFolderEnumerator];

	      return (aBOOL);

	    }
	  // We could also be trying to delete a maildir mailbox which
	  // has a directory structure with 3 sub-directories: cur, new, tmp
	  else if ( [aFileManager fileExistsAtPath: [NSString stringWithFormat: @"%@/%@/cur", [self path], theName]
		     isDirectory: &isDir])
	    {
	      // Make sure that these are the maildir directories and not something else.
	      if ( ![aFileManager fileExistsAtPath: [NSString stringWithFormat: @"%@/%@/new", [self path], theName]
				  isDirectory: &isDir] )
		{
		  return NO;
		}
	      if ( ![aFileManager fileExistsAtPath: [NSString stringWithFormat: @"%@/%@/tmp", [self path], theName]
				  isDirectory: &isDir] )
		{
		  return NO;
		}
	    }
	  else
	    {
	      return NO;
	    }
	}

      // We remove the mbox or maildir store
      aBOOL = [aFileManager removeFileAtPath: [NSString stringWithFormat: @"%@/%@",
							[self path], theName]
			    handler: nil];
      
      // We remove the cache, if the store deletion was successful
      if ( aBOOL )
	{
	  NSString *aString;

	  aString = [theName lastPathComponent];
	  
	  [[NSFileManager defaultManager] removeFileAtPath: [NSString stringWithFormat: @"%@/%@.%@.cache",
								      [self path],
								      [theName substringToIndex:
										 ([theName length] - [aString length])],
								      aString]
					  handler: nil];
	}

	  // Rebuild the folder tree
	  [self _rebuildFolderEnumerator];
    }
  
  return aBOOL;
}


//
// theName and theNewName MUST be the full path of those mailboxes.
//
// Returns YES on success. NO otherwise.
//
- (BOOL) renameFolderWithName: (NSString *) theName
                       toName: (NSString *) theNewName
{
  NSFileManager *aFileManager;
  BOOL aBOOL, isDir;
  
  aFileManager = [NSFileManager defaultManager];
  aBOOL = NO;

  // We verify if the destination path exists. If it does, we abort the rename operation
  // since we don't want to overwrite the folder.
  if ( [aFileManager fileExistsAtPath: [NSString stringWithFormat: @"%@/%@", [self path], theNewName]] )
    {
      return NO;
    }

  // We verify if the source path is valid
  if ( [aFileManager fileExistsAtPath: [NSString stringWithFormat: @"%@/%@", [self path], theName]
		     isDirectory: &isDir] )
    {
      if ( isDir )
	{
	  NSEnumerator *theEnumerator;
	  NSArray *theEntries;
	  
	  theEnumerator = [aFileManager enumeratorAtPath: [NSString stringWithFormat: @"%@/%@",
								    [self path], theName]];
	  
	  // FIXME - Verify the Store's path.
	  // If it doesn't contain any mailboxes and it's actually not or Store's path, we rename it.
	  theEntries = [theEnumerator allObjects];
	  
	  if ( [theEntries count] == 0 )
	    {
	      return [aFileManager movePath: [NSString stringWithFormat: @"%@/%@",[self path], theName]
				   toPath:  [NSString stringWithFormat: @"%@/%@",  [self path], theNewName]
				   handler: nil];
	    }
	  // We could also be trying to delete a maildir mailbox which
	  // has a directory structure with 3 sub-directories: cur, new, tmp
	  else if ( [aFileManager fileExistsAtPath: [NSString stringWithFormat: @"%@/%@/cur", [self path], theName]
				  isDirectory: &isDir])
	    {
	      // Make sure that these are the maildir directories and not something else.
	      if ( ![aFileManager fileExistsAtPath: [NSString stringWithFormat: @"%@/%@/new", [self path], theName]
				  isDirectory: &isDir] )
		{
		  return NO;
		}
	      if ( ![aFileManager fileExistsAtPath: [NSString stringWithFormat: @"%@/%@/tmp", [self path], theName]
				  isDirectory: &isDir] )
		{
		  return NO;
		}
	  }
	  else
	    {
	      return NO;
	    }
	}
      
      // We rename the mail store
      aBOOL = [aFileManager movePath: [NSString stringWithFormat: @"%@/%@", [self path], theName]
			    toPath: [NSString stringWithFormat: @"%@/%@", [self path], theNewName]
			    handler: nil];
      
      // We remove the cache, if the store rename was successful
      if ( aBOOL )
	{
	  NSString *str1, *str2;

	  str1 = [theName lastPathComponent];
	  str2 = [theNewName lastPathComponent];
	  
	  [[NSFileManager defaultManager] movePath: [NSString stringWithFormat: @"%@/%@.%@.cache",
							      [self path],
							      [theName substringToIndex:
									 ([theName length] - [str1 length])],
							      str1]
					  toPath: [NSString stringWithFormat: @"%@/%@.%@.cache",
							    [self path],
							    [theNewName substringToIndex:
									  ([theNewName length] - [str2 length])],
							    str2]
					  handler: nil];
	}
      
      // Rebuild the folder tree
      [self _rebuildFolderEnumerator];
    }
  
  return aBOOL;
}


//
//
//
- (void) enforceMode: (unsigned long) theMode
	      atPath: (NSString *) thePath

{
  NSMutableDictionary *currentFileAttributes;
  
  unsigned long current_attributes, desired_attributes;
  
  
  currentFileAttributes = [[NSMutableDictionary alloc] initWithDictionary: [fileManager fileAttributesAtPath: thePath
											traverseLink: YES]];
  
  current_attributes = [currentFileAttributes filePosixPermissions];
  desired_attributes = theMode;
  
  if ( current_attributes != desired_attributes )
    {
      [currentFileAttributes setObject: [NSNumber numberWithUnsignedLong: desired_attributes]
			     forKey: NSFilePosixPermissions];
      
      [fileManager changeFileAttributes: currentFileAttributes
		   atPath: thePath];
    }

  RELEASE(currentFileAttributes);
}


//
//
//
- (BOOL) isConnected
{
  return YES;
}

@end


//
// Private interface
//
@implementation LocalStore (Private)

- (void) _enforceFileAttributes
{
  NSEnumerator *anEnumerator;
  NSAutoreleasePool *pool;
  NSString *aString;

  pool = [[NSAutoreleasePool alloc] init];

  //
  // We verify if our Store path's mode is 0700
  //
  [self enforceMode: 0700  atPath: [self path]];
  
  //
  // We ensure that all subdirectories are using mode 0700 and files are using mode 0600
  //
  anEnumerator = [self folderEnumerator];

  while ( (aString = [anEnumerator nextObject]) )
    {
      BOOL isDir;
      
      aString = [NSString stringWithFormat: @"%@/%@", [self path], aString];
      
      if ( [fileManager fileExistsAtPath: aString
			isDirectory: &isDir] )
	{
	  if ( isDir )
	    {
	      [self enforceMode: 0700
		    atPath: aString];
	    }
	  else
	    {
	      [self enforceMode: 0600
		    atPath: aString];
	    }
	}
    } 
 
  RELEASE(pool);
}


//
// Rebuild the folder hierarchy
//
- (NSEnumerator *) _rebuildFolderEnumerator
{
  NSString *aString, *lastPathComponent, *pathToFolder;	
  NSEnumerator *tmpEnumerator;
  NSArray *tmpArray;
  int i;
  
  // Clear out our cached folder structure and refresh from the file system
  [folderArray removeAllObjects];
  [folderArray setArray: [[fileManager enumeratorAtPath: [self path]] allObjects]];
  
  //
  // We iterate through our array. If mbox A and .A.summary (or .A.cache) exists, we
  // remove .A.summary (or .A.cache) from our mutable array.
  // We do this in two runs:
  // First run: remove maildir sub-directory structure so that is appears as a regular folder.
  // Second run: remove other stuff like *.cache, *.summary
  //
  for (i = 0; i< [folderArray count]; i++)
    {
      BOOL bIsMailDir;
      
      aString = [folderArray objectAtIndex: i];
      
      lastPathComponent = [aString lastPathComponent];
      pathToFolder = [aString substringToIndex: ([aString length] - [lastPathComponent length])];
    
      //
      // First run:
      // If this is a maildir directory, remove its sub-directory structure from the list,
      // so that the maildir store appears just like a regular mail store.
      //
      if ( [[NSFileManager defaultManager] fileExistsAtPath: [NSString stringWithFormat: @"%@/%@/cur", 
								       [self path], aString] 
					   isDirectory: &bIsMailDir] && bIsMailDir )
	{
	  NSDirectoryEnumerator *maildirEnumerator;
	  NSArray *subpaths;
	
	  // Wust ensure 700 mode un cur/new/tmp folders and 600 on all files (ie., messages)
	  [self enforceMode: 0700
		atPath: [NSString stringWithFormat: @"%@/%@/cur", [self path], aString]];

	  [self enforceMode: 0700
		atPath: [NSString stringWithFormat: @"%@/%@/new", [self path], aString]];
	  
	  [self enforceMode: 0700
		atPath: [NSString stringWithFormat: @"%@/%@/tmp", [self path], aString]];
	  
	  
	  // Get all the children of this directory an remove them from our mutable array.
	  maildirEnumerator = [[NSFileManager defaultManager] enumeratorAtPath: 
								[NSString stringWithFormat: @"%@/%@", 
									  [self path], aString]];
	  
	  subpaths = [[NSFileManager defaultManager] subpathsAtPath: [NSString stringWithFormat: @"%@/%@", 
									       [self path], aString]];
	  [folderArray removeObjectsInRange: NSMakeRange(i+1,[subpaths count])];
	}
    }
  
  //
  // Second Run: Get rid of cache, summary and OS specific stuff
  //
  tmpArray = [[NSArray alloc] initWithArray: folderArray];
  AUTORELEASE(tmpArray);
  tmpEnumerator = [tmpArray objectEnumerator];
  
  while ( (aString = [tmpEnumerator nextObject]) )
    {
      lastPathComponent = [aString lastPathComponent];
      pathToFolder = [aString substringToIndex: ([aString length] - [lastPathComponent length])];
      
      // We remove Netscape/Mozilla summary file.
      [folderArray removeObject: [NSString stringWithFormat: @"%@.%@.summary", pathToFolder, lastPathComponent]];
      
      // We remove Pantomime's cache file. Before doing so, we ensure it's 600 mode.
      [folderArray removeObject: [NSString stringWithFormat: @"%@.%@.cache", pathToFolder, lastPathComponent]];
      [self enforceMode: 0600
	    atPath: [NSString stringWithFormat: @"%@/%@.%@.cache", [self path], pathToFolder, lastPathComponent]];

      // We also remove Apple Mac OS X .DS_Store directory
      [folderArray removeObject: [NSString stringWithFormat: @"%@.DS_Store", pathToFolder]];
    }
  
  return [folderArray objectEnumerator];
}

@end
