/* -*-ObjC-*-
*******************************************************************************
*
* File:         MailboxSummary.m
* RCS:          MailboxSummary.m,v 1.15 1998/06/14 01:33:30 tom Exp
* Description:  
* Author:       Tom Hageman <tom@basil.icce.rug.nl>
* Created:      October 1996
* Modified:     April 1998, extracted from TransferPanel.
* Language:     Objective-C
* Package:      EnhanceMail
* Status:       Experimental
*
* Copyright (C) 1996-1998 Tom Hageman, but otherwise this file is perfect freeware.
*
*******************************************************************************
*/
#import <libc.h>
#import <limits.h>
#define _MAILTOC_DEFINES   1
#import "mailtoc.h"
#import "MailboxSummary.h"
#import <objc/HashTable.h>
#import <defaults/defaults.h>
#import <appkit/nextstd.h>

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


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


#define FILESTOSTAT  MBOX_TOC_FILE

#define STAT_EQ_FILE(s1, s2)	((s1)->st_ino == (s2)->st_ino && \
				 ((s1)->st_dev == (s2)->st_dev || \
				  /* Kludge to allow restoration from cache. */ \
				  (s1)->st_dev == 0 || (s2)->st_dev == 0))

#define STAT_EQ(s1, s2)		(STAT_EQ_FILE((s1), (s2)) && \
				 (s1)->st_mtime == (s2)->st_mtime && \
				 (s1)->st_size == (s2)->st_size)

#define NEW_OR_UNREAD(array)	((array)[EMS_TAG_NEW] + (array)[EMS_TAG_UNREAD])


@implementation EnhanceMailboxSummary

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

+ (const char *)getPath:(char *)buf forName:(const char *)name
{
   int len;

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

   len = strlen(buf);
   if ((len < 5) || (strcmp(buf+len-5, ".mbox")) != 0) strcpy(buf+len, ".mbox");
   
   return buf;
}

- initWithName:(const char *)name
{
   if (self = [super init])
   {
      loadStatus = EMS_UNLOADED;
      [self setName:name];
      if (!fullname)
      {
	 self = [self free];
      }
   }
   return self;
}

- free
{
   if (fullname)
   {
      [[self class] setSummary: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 [[self class] shortNameFor:fullname];
}


// Configuration.

- (void)setCollectAll:(BOOL)yn
{
   if (yn && !collectAll) accurate = NO;  // force reload.
   collectAll = yn;
}

- (BOOL)collectAll { return collectAll; }


static BOOL doPendingAppnmailCheck = YES;

+ (BOOL)doPendingAppnmailCheck { return doPendingAppnmailCheck; }

+ (void)setDoPendingAppnmailCheck:(BOOL)value
{
   doPendingAppnmailCheck = value;
}

static BOOL doInSyncCheck = YES;

+ (BOOL)doInSyncCheck { return doInSyncCheck; }

+ (void)setDoInSyncCheck:(BOOL)value
{
   doInSyncCheck = value;
}


// Support methods

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

- (void)_reset
{
   int tag = EMS_NUMTAGS;

   while (--tag >= 0)
   {
      counts[tag] = 0;
      sizes[tag] = 0;
      attachCounts[tag] = 0;
      attachSizes[tag] = 0;
   }
}

- (enum EMSLoadStatus)_loadTocFrom:(off_t)seekOffset
{
   char path[MAXPATHLEN+1];
   FILE *tocf;

   DFPRINTF((stderr, "-[%s %s]: (%s) offset=%ld loadStatus=%d\n", [[self class] name], SELNAME(_cmd), [self shortName], seekOffset, loadStatus));

   loadStatus = EMS_UNLOADED;
   accurate = NO;

   if (seekOffset == 0) [self _reset];

   if ((tocf = fopen([self getPath:path forFile:MBOX_TOC_FILE], "rb")) != NULL)
   {
      struct table_of_contents_header *th;
#if 0 /* Does not work since setvbuf is not in Mail.app's symbol table :~( */
      // Enlarge stdio buffer size to read toc in one fell swoop.
#   define MAX_BUFSIZ   (((128 * 1024) + BUFSIZ - 1) / BUFSIZ * BUFSIZ)

      size_t bufsiz = (stats[EMS_STAT_TOC].st_size + BUFSIZ - 1) / BUFSIZ * BUFSIZ;

      if (bufsiz == 0) bufsiz = BUFSIZ;
      else if (bufsiz > MAX_BUFSIZ) bufsiz = MAX_BUFSIZ;

      setvbuf(tocf, NULL, _IOFBF, bufsiz);
#endif

      if ((th = get_table_of_contents_header(tocf, NO)) == NULL)
      {
	 [self _reset];
	 loadStatus = EMS_LOADFAILED;
      }
      else
      {
	 int msgCount;

	 // Assume success.
	 loadStatus = EMS_LOADED;
	 accurate = YES;

	 if (doInSyncCheck)
	 {
	    // For consistency, check if mbox timestamp is in sync.
	    struct stat mbox_stat;

	    if (stat([self getPath:path forFile:MBOX_CONTENTS_FILE], &mbox_stat) < 0 ||
		th->mbox_time != mbox_stat.st_mtime)
	    {
	       loadStatus = EMS_LOADFAILED;
	       accurate = NO;
	    }
	 }

  restart:

	 msgCount = th->num_msgs;

	 if (seekOffset != 0)
	 {
	    if ((msgCount -= counts[EMS_TAG_TOTAL]) < 0)
	    {
	       seekOffset = 0;
	       goto restart;  // Ho. Hum.
	    }
	    fseek(tocf, seekOffset, SEEK_SET);
	 }

	 // Tally read, new and deleted messages.
	 while (--msgCount >= 0)
	 {
	    struct message_index *mi;
	    int tag;

	    if ((mi = get_message_index(tocf)) == NULL)
	    {
	       if (seekOffset != 0 && !ferror(tocf))
	       {
		  // Non-fatal error: retry reading toc from the start.
		  seekOffset = 0;
		  fseek(tocf, sizeof(*th), SEEK_SET);
		  [self _reset];
		  goto restart;	// See?  I told you it was getting messy...
	       }
	       loadStatus = EMS_LOADFAILED;
	       accurate = NO;
	       break;
	    }

	    switch (mi->status)
	    {
	    case MT_STATUS_NEW:
	       tag = EMS_TAG_NEW;
	       break;
	    case MT_STATUS_DELETED_1:
	    case MT_STATUS_DELETED_2:
	       tag = EMS_TAG_DELETED;
	       break;
	    case MT_STATUS_UNREAD_1:
	    case MT_STATUS_UNREAD_2:
	       tag = EMS_TAG_UNREAD;
	       break;
	    case MT_STATUS_FLAGGED:
	       tag = EMS_TAG_FLAGGED;
	       break;
	    default:
	       tag = EMS_TAG_READ;
	       break;
	    }

	    counts[tag]++;
	    counts[EMS_TAG_TOTAL]++;

	    if (collectAll)
	    {
	       int size = mi->mes_length;
	       int attachSize = message_attachsize(mi);

	       if (attachSize > 0)
	       {
		  attachCounts[tag]++;
		  attachSizes[tag] += attachSize;
		  attachCounts[EMS_TAG_TOTAL]++;
		  attachSizes[EMS_TAG_TOTAL] += attachSize;

		  size += attachSize;
	       }
	       sizes[tag] += size;
	       sizes[EMS_TAG_TOTAL] += size;
	    }
	    free(mi);
	 }

	 // We should be at end-of-file by now, otherwise TOC is inconsistent.
	 /* {{Disabled for now since outside agent may have appended new
	     messages in the meantime.}} */
	 ///if (getc(tocf) != EOF) loadStatus = EMS_LOADFAILED;

	 free(th);
      }
      fclose(tocf);
   }
   return loadStatus;
}

- (enum EMSLoadStatus)load
{
   struct stat oldstat = stats[EMS_STAT_TOC];    // XXX Ugh!
   BOOL shouldLoad = [self shouldLoad];	     // XXX Knows this reloads stats[].
   // Optimization: start reading at previous EOF if TOC grew.
   BOOL seekToEnd = (loadStatus == EMS_LOADED && accurate &&
		     STAT_EQ_FILE(&oldstat, &stats[EMS_STAT_TOC]) &&
		     oldstat.st_mtime <= stats[EMS_STAT_TOC].st_mtime &&
		     oldstat.st_size < stats[EMS_STAT_TOC].st_size &&
		     oldstat.st_size > sizeof(struct table_of_contents_header));
   // XXX Ugh galore!  This is getting very messy indeed...
   long oldNewOrUnread = (loadStatus == EMS_LOADED) ? NEW_OR_UNREAD(counts) : LONG_MAX;

   // conditionally check for pending appnmail.
   hasPendingAppnmail = (doPendingAppnmailCheck ? [self checkPendingAppnmail] : NO);

   needsReload = NO;

   if ([self suppressLoad])
   {
      [self _reset];
      loadStatus = EMS_LOADSUPPRESSED;
      accurate = NO;
      hasNewMail = NO;
      return loadStatus;
   }
   // Should also load if suppression status has changed.
   if (!(shouldLoad || loadStatus == EMS_LOADSUPPRESSED))
   {
      return loadStatus;
   }

   if ([self _loadTocFrom:(seekToEnd ? oldstat.st_size : 0)] != EMS_LOADED)
   {
      hasNewMail = NO;
      return loadStatus;
   }

   if (NEW_OR_UNREAD(counts) > oldNewOrUnread) hasNewMail = YES;
   else if (NEW_OR_UNREAD(counts) < oldNewOrUnread) hasNewMail = NO;

   return loadStatus;
}

- (enum EMSLoadStatus)loadStatus { return loadStatus; }

- (BOOL)isLoaded { return loadStatus != EMS_UNLOADED && loadStatus != EMS_LOADFAILED; }

- (BOOL)shouldLoad
{
   char path[MAXPATHLEN+1];
   struct stat newstats[EMS_NUMSTATS];
   static const char * const filesToStat[EMS_NUMSTATS] = { FILESTOSTAT };
   BOOL result = NO;
   int i;

   for (i = 0;  i < EMS_NUMSTATS;  i++)
   {
      memset(&newstats[i], 0, sizeof(struct stat));
      stat([self getPath:path forFile:filesToStat[i]], &newstats[i]);
      if (!STAT_EQ(&newstats[i], &stats[i]))
      {
	 result = YES;
	 ///break; // NOT!!! must stat all for accurate cache.
      }
      stats[i] = newstats[i];
   }
   return result;
}

- (BOOL)suppressLoad { return NO; }

- (BOOL)needsReload		    { return needsReload; }
- (void)setNeedsReload:(BOOL)value  { needsReload = value; }


// General results.

- (long)count:(int)tag
{
   NX_ASSERT((unsigned)tag < EMS_NUMTAGS, "tag < EMS_NUMTAGS");
   return counts[tag];
}

- (unsigned long)size:(int)tag
{
   NX_ASSERT((unsigned)tag < EMS_NUMTAGS, "tag < EMS_NUMTAGS");
   return sizes[tag];
}

- (long)attachCount:(int)tag
{
   NX_ASSERT((unsigned)tag < EMS_NUMTAGS, "tag < EMS_NUMTAGS");
   return attachCounts[tag];
}

- (unsigned long)attachSize:(int)tag
{
   NX_ASSERT((unsigned)tag < EMS_NUMTAGS, "tag < EMS_NUMTAGS");
   return attachSizes[tag];
}


// Specialized results.

- (long)numUnreadMessages		{ return counts[EMS_TAG_UNREAD]; }
- (void)setUnreadMessages:(long)value	{ counts[EMS_TAG_UNREAD] = value; accurate = NO; hasNewMail = NO; }
- (long)numNewMessages			{ return counts[EMS_TAG_NEW]; }
- (void)setNewMessages:(long)value	{ counts[EMS_TAG_NEW] = value; accurate = NO; hasNewMail = NO; }
- (long)numFlaggedMessages		{ return counts[EMS_TAG_FLAGGED]; }
- (void)setFlaggedMessages:(long)value	{ counts[EMS_TAG_FLAGGED] = value; accurate = NO; }

- (long)numNewOrUnreadMessages		{ return NEW_OR_UNREAD(counts); }

- (BOOL)hasNewMail			{ return hasNewMail; }
- (void)setHasNewMail:(BOOL)value	{ hasNewMail = value; }

- (BOOL)hasIndex
{
   char path[MAXPATHLEN+1];

   return access([self getPath:path forFile:MBOX_INDEX_FILE], F_OK) == 0;
}

- (unsigned long)indexSize
{
   char path[MAXPATHLEN+1];
   struct stat st;

   return (stat([self getPath:path forFile:MBOX_INDEX_FILE], &st) < 0) ? 0 : st.st_size;
}

- (unsigned long)tableOfContentsSize   { return stats[EMS_STAT_TOC].st_size; }


// Appnmail extensions.

- (BOOL)hasPendingAppnmail	    { return hasPendingAppnmail; }

/* NB. This does not work if invoked from Mail.app itself... */
- (BOOL)incorporatePendingAppnmail:(BOOL)wait
{
   char command[60 + 3*MAXPATHLEN + 1];
   const char *s;
   char *d = command;

   for (s = "PATH=/usr/local/bin:$PATH; exec appnmail -i </dev/null '"; *s; ) *d++ = *s++;
   // Escape single quote characters.
   for (s = [self name]; *s; )
   {
	   if ((*d++ = *s++) == '\'')
	   {
		   *d++ = '\\', *d++ = '\'', *d++ = '\'';
	   }
   }
   *d++ = '\'';
   if (!wait) *d++ = '&';
   *d = '\0';

   return (system(command) == EXIT_SUCCESS);
}

#define APPNMAIL_TOC	"appnmail_"MBOX_TOC_FILE
#define APPNMAIL_MBOX	"appnmail_"MBOX_CONTENTS_FILE

- (BOOL)checkPendingAppnmail
{
   char path[MAXPATHLEN+1];

   hasPendingAppnmail = (access([self getPath:path forFile:APPNMAIL_MBOX], F_OK) == 0 &&
			 access([self getPath:path forFile:APPNMAIL_TOC], F_OK) == 0);
   return hasPendingAppnmail;
}

@end // EnhanceMailboxSummary


// ripped off/adapted from mailapp-utilities/mailutil.c:

extern const char *NXHomeDirectory(void);

@implementation EnhanceMailboxSummary (PathUtils)

+ (int)mailboxDir:(char *)buf
{
   int len;
   const char *str;

   if ((str=NXGetDefaultValue("Mail","MailDir")) != NULL)
   {
      strcpy(buf,str);
   }
   else
   {
      strcpy(buf,"~/Mailboxes");
   }

   if ((buf[0]=='~') && (buf[1]=='/'))
   {
      char temp[MAXPATHLEN];
      if (!(str=NXHomeDirectory()) && !(str=getenv("HOME"))) return -1;
      strcpy(temp,buf+1);
      strcpy(buf,str);
      strcat(buf,temp);
   }

   len=strlen(buf);

   if (buf[len-1]=='/')
   {
      len--;
      buf[len]='\0';
   }
   
   return len;
}

+ (const char *)shortNameFor:(const char *)path
{
   char mailboxDir[MAXPATHLEN+1];
   int len = [self mailboxDir:mailboxDir];

   if (strncmp(mailboxDir, path, len) == 0)
   {
      switch (path[len])
      {
      case '/':	  path += len + 1; break;
      case '\0':  path += len;     break;
      default:			   break;
      }
   }
   return path;
}

@end // EnhanceMailboxSummary (PathUtils)


@implementation EnhanceMailboxSummary (SummaryHash)

static HashTable *uniquehash = nil;

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

+ summaryForName:(const char *)name create:(BOOL)create
{
   char path[MAXPATHLEN+1];
   id sum;

   if (![self getPath:path forName:name]) return nil;
	
   sum = [uniquehash valueForKey:path];
   if (!sum && create)
   {
      // XXX handle symlinks to mboxes.
      sum = [[self alloc] initWithName:path];
      [self setSummary:sum forName:[sum name]];
   }
   return sum;
}

+ summaryForName:(const char *)name
{
   return [self summaryForName:name create:YES];
}

@end // EnhanceMailboxSummary (SummaryHash)
