/* -*-ObjC-*-
*******************************************************************************
*
* File:         MailboxSummaryList.m
* RCS:          MailboxSummaryList.m,v 1.14 1998/06/30 20:16:51 tom Exp
* Description:  
* Author:       Tom Hageman <tom@basil.icce.rug.nl>
* Created:      April 1998
* Modified:     
* Language:     Objective-C
* Package:      EnhanceMail
* Status:       Experimental
*
* Copyright (C) 1998 Tom Hageman, but otherwise this file is perfect freeware.
*
*******************************************************************************
*/
#import "MailboxSummaryList.h"
#import "MailboxSummary.h"
#import "Enumerator.h"
#import <objc/HashTable.h>
#import <sys/types.h>
#import <sys/dir.h>
#import <appkit/nextstd.h>
#import <appkit/Application.h>	 // For delayed perform.

#if DEBUG || DEBUG_SUMMARY || DEBUG_ITERATE
#  define DFPRINTF(arglist)	fprintf arglist
#else
#  define DFPRINTF(arglist)
#endif


// Versioning support.
#define CLASS_NAME	"EnhanceMailboxSummaryList"
#define CLASS_VERSION_0	0
#define CLASS_VERSION	CLASS_VERSION_0


#define DONTSCAN_FILE	".dontscan"
#define DOSCAN_FILE	".doscan"


@interface EnhanceMailboxSummaryList (PrivateSummaryHash)
+ (void)setSummaryList:(EnhanceMailboxSummaryList *)list forName:(const char *)name;
+ _summaryListForName:(const char *)name;
@end


@implementation EnhanceMailboxSummaryList

#if (CLASS_VERSION != 0)
+ initialize
{
   if (self == [EnhanceMailboxSummaryList class])
   {
      [self setVersion:CLASS_VERSION];
   }
   return self;
}
#endif

+ (const char *)getPath:(char *)buf forName:(const char *)name
{
   if (*name == '/')
   {
      strcpy(buf, name);
   }
   else
   {
      int len = [[EnhanceMailboxSummary class] mailboxDir:buf];

      if (len < 0) return NULL;
      if (*name)
      {
	 buf[len++] = '/';
	 strcpy(buf+len, name);
      }
   }
   
   return buf;
}

+ summaryListForName:(const char *)name
{
   return [self _summaryListForName:name];
}

- initWithName:(const char *)name
{
   if ((self = [super init]) != nil)
   {
      loadStatus = EMS_UNLOADED;
      [self setName:name];
      if (!fullname)
      {
	 [self free];
	 return nil;
      }
      [[self class] setSummaryList:self forName:fullname];  // record.
   }
   return self;
}

- empty
{
   [self iterateAbort];

   // Recursively free SummaryLists; Summaries are not freed.
   while ([self count] > 0)
   {
      id object = [self lastObject];

      [self removeLastObject];

      if ([object isKindOf:[EnhanceMailboxSummaryList class]])
      {
	 [object free];
      }
   }
   return [super empty];
}

- free
{
   [self empty];
   if (fullname)
   {
      [[self class] setSummaryList:nil forName:fullname];
      free(fullname);
   }
   return [super free];
}

- (const char *)name { return fullname; }

- (void)setName:(const char *)name
{
   if (name != fullname)
   {
      char path[MAXPATHLEN+1];

      fullname = (free(fullname), NULL);

      if ([[self class] getPath:path forName:name])
      {
	 fullname = NXCopyStringBufferFromZone(path, [self zone]);
      }
   }
}

- (const char *)shortName
{
   return [[EnhanceMailboxSummary class] shortNameFor:fullname];
}


// Support.

- (const char *)getPath:(char *)buf forFile:(const char *)file
{
   sprintf(buf, "%s/%s", fullname, file);
   return buf;
}

- (void)_setBool:(BOOL*)valP file:(const char *)file to:(BOOL)aValue
{
   if (aValue != (*valP))
   {
      char path[MAXPATHLEN+1];

      [self getPath:path forFile:file];

      if (((*valP) = aValue))
      {
	 close(creat(path, 000));
      }
      else
      {
	 unlink(path);
      }
   }
}


// Access methods.

- (BOOL)dontScan  { return _dontScan; }
- (BOOL)doScan	  { return _doScan; }

- (void)setDontScan:(BOOL)aValue
{
   [self _setBool:&_dontScan file:DONTSCAN_FILE to:aValue];
   if (aValue) [self setDoScan:NO];
}

- (void)setDoScan:(BOOL)aValue
{
   [self _setBool:&_doScan file:DOSCAN_FILE to:aValue];
   if (aValue) [self setDontScan:NO];
}

- (BOOL)autoUpdateCache			  { return _autoUpdateCache; }
- (void)setAutoUpdateCache:(BOOL)aValue	  { _autoUpdateCache = aValue; }


// Loading.

- (enum EMSLoadStatus)load
{
   return [self loadUnless:_suppressed];
}

- (enum EMSLoadStatus)loadUnless:(BOOL)suppressed
{
   if (loadStatus == EMS_UNLOADED) [self scandir];
   _suppressed = suppressed;
   if (loadStatus == EMS_LOADED || loadStatus == EMS_LOADSUPPRESSED)
   {
      suppressed = (suppressed || _dontScan) && !_doScan;
      [self makeObjectsPerform:_cmd with:(id)(int)suppressed];
      loadStatus = (suppressed) ? EMS_LOADSUPPRESSED : EMS_LOADED;

      if (_autoUpdateCache)
      {
	 [self storeToCache];
      }
   }
   return loadStatus;
}

- (int)scandir
{
   char path[MAXPATHLEN+1];
   DIR *dir, *subdir;
   struct direct *d;
   int i, summaries = 0;

   [self empty];

   if ((dir = opendir([self name])) == NULL)
   {
      loadStatus = EMS_LOADFAILED;
      return -1;
   }
   loadStatus = EMS_LOADED;
   _dontScan = _doScan = NO;

   while ((d = readdir(dir)) != NULL)
   {
      if (strcmp(d->d_name, ".") == 0) continue;
      if (strcmp(d->d_name, "..") == 0) continue;

      if (strcmp(d->d_name, DONTSCAN_FILE) == 0)
      {
	 _dontScan = YES;
	 continue;
      }
      if (strcmp(d->d_name, DOSCAN_FILE) == 0)
      {
	 _doScan = YES;
	 continue;
      }

      [self getPath:path forFile:d->d_name];

      if (d->d_namlen > 5 && strcmp(&d->d_name[d->d_namlen-5], ".mbox") == 0)
      {
	 [self addObject:[EnhanceMailboxSummary summaryForName:path]];
	 ++summaries;
      }
      else if ((subdir = opendir(path)) != NULL)
      {
	 closedir(subdir);
	 [self insertObject:[[[self class] allocFromZone:[self zone]]
			     initWithName:path] at:0];
      }
   }
   closedir(dir);

   // Recurse into subdirectories.
   i = [self count];

   while (--i >= 0)
   {
      id object = [self objectAt:i];

      if ([object respondsTo:_cmd])
      {
	 int result = [object scandir];

	 if (result < 0)
	 {
	    [self removeObjectAt:i];
	 }
	 else
	 {
	    summaries += result;
	 }
      }      
   }
   return summaries;
}

- (enum EMSLoadStatus)loadStatus { return loadStatus; }

- (BOOL)isLoaded		 { return loadStatus == EMS_LOADED; }

- (BOOL)isLoadSuppressed	 { return loadStatus == EMS_LOADSUPPRESSED; }


// General results.

- (unsigned long)_countPerform:(SEL)sel tag:(int)tag
{
   int i = [self count];
   int total = 0;

   while (--i >= 0)
   {
      total += (unsigned long)[[self objectAt:i] perform:sel with:(id)tag];
   }
   return total;
}


- (long)count:(int)tag
{
   return [self _countPerform:_cmd tag:tag];
}

- (unsigned long)size:(int)tag
{
   return [self _countPerform:_cmd tag:tag];
}

- (long)attachCount:(int)tag
{
   return [self _countPerform:_cmd tag:tag];
}

- (unsigned long)attachSize:(int)tag
{
   return [self _countPerform:_cmd tag:tag];
}


// Specialized results.
- (long)numUnreadMessages
{
   return [self _countPerform:_cmd tag:0];
}

- (long)numNewMessages
{
   return [self _countPerform:_cmd tag:0];
}


- (int)numMailboxesWithNewOrUnreadMessages
{
   return [self _countPerform:_cmd tag:0];
}

- (BOOL)hasPendingAppnmail
{
   int i = [self count];

   while (--i >= 0)
   {
      if ([[self objectAt:i] hasPendingAppnmail]) return YES;
   }
   return NO;
}

- (BOOL)hasNewMail
{
   int i = [self count];

   while (--i >= 0)
   {
      if ([[self objectAt:i] hasNewMail]) return YES;
   }
   return NO;
}

- (int)numMailboxesWithNewMail
{
   return [self _countPerform:_cmd tag:0];
}

@end // EnhanceMailboxSummaryList


@implementation EnhanceMailboxSummaryList (PrivateSummaryHash)

static HashTable *uniquehash = nil;

+ (void)setSummaryList:(EnhanceMailboxSummaryList *)list forName:(const char *)name
{
   if (!name) return;
   if (list)
   {
      if (!uniquehash) uniquehash = [[HashTable alloc] initKeyDesc:"*"];
      [uniquehash insertKey:name value:list];
   }
   else
   {
      [uniquehash removeKey:name];
   }
}

+ _summaryListForName:(const char *)name
{
   char path[MAXPATHLEN+1];

   if (![self getPath:path forName:name]) return nil;
	
   return [uniquehash valueForKey:path];
}

@end // EnhanceMailboxSummaryList (PrivateSummaryHash)


@implementation EnhanceMailboxSummaryList (Archiving)

- read:(NXTypedStream *)stream
{
   char *shortName;

   [super read:stream];

   NXReadType(stream, @encode(typeof(shortName)), &shortName);
   [self setName:shortName];
   free(shortName);

   NXReadType(stream, @encode(typeof(loadStatus)), &loadStatus);
   NXReadType(stream, @encode(typeof(_dontScan)), &_dontScan);
   NXReadType(stream, @encode(typeof(_doScan)), &_doScan);
   NXReadType(stream, @encode(typeof(_suppressed)), &_suppressed);

   if (!fullname)
   {
      self = [self free];
   }
   else
   {
      [[self class] setSummaryList:self forName:fullname];  // record.
   }
   return self;
}

- write:(NXTypedStream *)stream
{
   const char *shortName = [self shortName];

   [super write:stream];

   NXWriteType(stream, @encode(typeof(shortName)), &shortName);

   NXWriteType(stream, @encode(typeof(loadStatus)), &loadStatus);
   NXWriteType(stream, @encode(typeof(_dontScan)), &_dontScan);
   NXWriteType(stream, @encode(typeof(_doScan)), &_doScan);
   NXWriteType(stream, @encode(typeof(_suppressed)), &_suppressed);

   return self;
}

@end // EnhanceMailboxSummaryList (Archiving)


#define CACHE_FILE   ".enhanceSummaryCache"

@implementation EnhanceMailboxSummaryList (Caching)

static time_t lastModTime; // XXX quick hack; should be instance variable.

- (time_t)modTime
{
   time_t result = 0;
   int i = [self count];

   while (--i >= 0)
   {
      time_t time = [[self objectAt:i] modTime];

      if (time > result) result = time;
   }
   return result;
}

+ cachedSummaryListForName:(const char *)name
{
   id result = nil;
   char cachePath[MAXPATHLEN+1];

   [self getPath:cachePath forName:name];
   sprintf(cachePath + strlen(cachePath), "/%s", CACHE_FILE);

   NX_DURING
   {
      NXTypedStream *stream;

      if ((stream = NXOpenTypedStreamForFile(cachePath, NX_READONLY)) != NULL)
      {
	 DFPRINTF((stderr, "+[%s %s]: loading \"%s\"...\n", [self name], SELNAME(_cmd), cachePath));
	 result = NXReadObject(stream);
	 NXCloseTypedStream(stream);
	 DFPRINTF((stderr, "+[%s %s]: loading \"%s\"...done\n", [self name], SELNAME(_cmd), cachePath));
      }
      lastModTime = [result modTime];
   }
   NX_HANDLER
   {
      fprintf(stderr, "%s cannot load from \"%s\", reason: %s (exception %d)\n",
	      SELNAME(_cmd), cachePath, (char *)NXLocalHandler.data1, NXLocalHandler.code);
   }
   NX_ENDHANDLER

   return result;
}

+ (BOOL)storeSummaryListToCache:(EnhanceMailboxSummaryList *)list
{
   BOOL result = NO;
   char cachePath[MAXPATHLEN+1];

   if (!list) return result;

   [list getPath:cachePath forFile:CACHE_FILE];

   NX_DURING
   {
      NXTypedStream *stream;

      // XXX Race conditons?
      ///unlink(cachePath);
      if ((stream = NXOpenTypedStreamForFile(cachePath, NX_WRITEONLY)) != NULL)
      {
	 DFPRINTF((stderr, "+[%s %s]: writing \"%s\"...\n", [self name], SELNAME(_cmd), cachePath));
	 NXWriteRootObject(stream, list);
	 NXCloseTypedStream(stream);
	 DFPRINTF((stderr, "+[%s %s]: writing \"%s\"...done\n", [self name], SELNAME(_cmd), cachePath));
	 result = YES;
      }
   }
   NX_HANDLER
   {
      fprintf(stderr, "%s cannot store to \"%s\", reason: %s (exception %d)\n",
	      SELNAME(_cmd), cachePath, (char *)NXLocalHandler.data1, NXLocalHandler.code);
   }
   NX_ENDHANDLER

   return result;
}

- (BOOL)storeToCache
{
   time_t modTime = [self modTime];

   DFPRINTF((stderr, "-[%s %s] (%s) modTime = %s", [[self class] name], SELNAME(_cmd), [self name], ctime(&modTime)));

   if (modTime <= lastModTime) return YES;   // up-to-date; deemed successful.

   lastModTime = modTime;

   return [[self class] storeSummaryListToCache:self];
}

@end // EnhanceMailboxSummaryList (Caching)


@implementation EnhanceMailboxSummaryList (IterateLoad)

static int _iterateLoadInterval = 500; // Load interval in ms.

+ (int)iterateLoadInterval { return _iterateLoadInterval; }

+ (void)setIterateLoadInterval:(int)value
{
   _iterateLoadInterval = value;
}


- (void)_iterateLoad
{
   for (;;)
   {
      id object = [iterateLoadEnumerator nextObject];
      int st;

      if (object == nil)
      {
	 // Just to be sure...
///	 [self perform:_cmd with:nil afterDelay:-1 cancelPrevious:YES];	// Disabled to (hopefully) avoid NS3.3 crashes.

	 if ([iterateLoadContext respondsTo:@selector(didFinishIterateLoad:)])
	 {
	    [iterateLoadContext perform:@selector(didFinishIterateLoad:) with:self];
	 }
	 iterateLoadContext = nil;
	 iterateLoadEnumerator = nil;

	 if (_autoUpdateCache)
	 {
	    [self storeToCache];
	 }
	 return;
      }

      st = [object iterateLoad:self];

      /* suspend iteration; presumably objects context (which should be us)
         picks it up in -didFinishIterateLoad: */
      if (st < 0) return;

      /* did something: delay next iteration. */
      if (st > 0 && _iterateLoadInterval >= 0) break;

      /* did nothing; try next one immediately. */
   }

   [self perform:_cmd with:nil afterDelay:_iterateLoadInterval cancelPrevious:YES];
}

- (int)iterateLoad:sender
{
   // Refuse to do anything if previous cycle is still running.
   if ([self iterating]) return 0;

   DFPRINTF((stderr, "-[%s %s] (%s)\n", [[self class] name], SELNAME(_cmd), [self shortName]));

   if (loadStatus == EMS_UNLOADED) [self scandir];

   if (loadStatus != EMS_LOADED && loadStatus != EMS_LOADSUPPRESSED) return 0;

   iterateLoadContext = sender;
   iterateLoadEnumerator = [self enhanceReverseObjectEnumerator];

   if ([sender respondsTo:@selector(isLoadSuppressed)])
   {
      _suppressed = [sender isLoadSuppressed];
   }
   loadStatus = ((_suppressed || _dontScan) && !_doScan) ? EMS_LOADSUPPRESSED : EMS_LOADED;

   [self _iterateLoad];

   return -1;  // suspend parent iteration.
}

- (void)didFinishIterateLoad:(EnhanceMailboxSummaryList *)sender
{
   // resume suspended iteration.
   DFPRINTF((stderr, "-[%s %s] (sender = %s)\n", [[self class] name], SELNAME(_cmd), [sender shortName]));
   [self _iterateLoad];
}

- (void)newMailForSummary:(EnhanceMailboxSummary *)sender
{
   // pass it on to parent context.
   if ([iterateLoadContext respondsTo:_cmd])
   {
      [iterateLoadContext newMailForSummary:sender];
   }
}

- (BOOL)iterating
{
   return (iterateLoadEnumerator != nil);
}

- (void)iterateAbort
{
   if (![self iterating]) return;

   DFPRINTF((stderr, "-[%s %s] (%s)\n", [[self class] name], SELNAME(_cmd), [self shortName]));

   // Cancel outstanding delayed-perform (if any).
   [self perform:@selector(_iterateLoad) with:nil afterDelay:-1 cancelPrevious:YES];

   iterateLoadEnumerator = [iterateLoadEnumerator free];
   iterateLoadContext = nil;

   // ...recursively.
   [self makeObjectsPerform:_cmd];
}

@end // EnhanceMailboxSummaryList (IterateLoad)


@implementation EnhanceMailboxSummary (SummaryListExtensions)

- (int)numMailboxesWithNewOrUnreadMessages
{
   return ([self numNewOrUnreadMessages] > 0 ? 1 : 0);
}

- (int)numMailboxesWithNewMail
{
   return [self hasNewMail];
}

- (enum EMSLoadStatus)loadUnless:(BOOL)suppressed
{
   if (!suppressed) return [self load];
   [self setNeedsReload:YES];
   return loadStatus;
}

- (int)iterateLoad:(EnhanceMailboxSummaryList *)sender
{
   if (![sender isLoadSuppressed])
   {
      BOOL hadNewMail = [self hasNewMail];

      DFPRINTF((stderr, "-[%s %s] (%s)\n", [[self class] name], SELNAME(_cmd), [self shortName]));
      [self load];

      if (!hadNewMail && [self hasNewMail])
      {
	 [sender newMailForSummary:self];
      }
      return 1;
   }
   DFPRINTF((stderr, "-[%s %s] (%s) load suppressed\n", [[self class] name], SELNAME(_cmd), [self shortName]));
   [self setNeedsReload:YES];
   return 0;
}

- (time_t)modTime
{
   return stats[EMS_STAT_TOC].st_mtime;
}

- (void)iterateAbort {}

@end // EnhanceMailboxSummary (SummaryListExtensions)
