/* -*-ObjC-*-
*******************************************************************************
*
* File:         XImageAliases.m
* RCS:          XImageAliases.m,v 1.2 1998/07/01 21:54:53 tom Exp
* Description:  Implementation of EnhanceXImageAliases class.
* Author:       Tom Hageman <tom@basil.icce.rug.nl>
* Created:      June 1998
* Modified:     
* Language:     Objective-C
* Package:      EnhanceMail
* Status:       Experimental
*
* Copyright (C) 1998 Tom Hageman, but otherwise this file is perfect freeware.
*
*******************************************************************************
*/
#import <appkit/appkit.h>
#import "XImageAliases.h"
#import <stdio.h>
#import <stdlib.h>
#import <ctype.h>

#ifndef MAXNAMELEN
#  define MAXNAMELEN 255
#endif

#define ALIASES_TIFF	"aliases.tiffs"
#define ALIASES_URL	"aliases.URLs"
#define ALIASES_LOCK	"aliases.lock"


@interface EnhanceXImageAliases (Private)
- (const char *)_fileForURL:(const char *)url;
- (void)_setURL:(const char *)url address:(const char *)address;
- (void)_removeURL:(const char *)url;
@end


@interface EnhanceXImageAliases (Persistency)
- (BOOL)load;
- (BOOL)reload;
- (BOOL)lock;
- (void)unlock;
- (BOOL)flushCache;
@end

@implementation	EnhanceXImageAliases

+ (EnhanceXImageAliases *)aliasesForDirectory:(const char *)dir
{
   return [[self alloc] initFromDirectory:dir];
}

- initFromDirectory:(const char *)dir
{
   if (self = [super init])
   {
      NXZone *z = [self zone];

      if (!(dir && *dir))
      {
	 fprintf(stderr, "-[%s %s]: missing directory.\n", [[self class] name], SELNAME(_cmd));
	 return [self free];
      }
      dirname = NXCopyStringBufferFromZone(dir, z);
      addr2urlHash = [[HashTable allocFromZone:z] initKeyDesc:"*" valueDesc:"*"];
      url2addrHash = [[HashTable allocFromZone:z] initKeyDesc:"*" valueDesc:"*"];
      fullpathbuf = NXZoneMalloc(z, strlen(dirname) + 1 + MAXNAMELEN + 1);

      [self load];
   }
   return self;
}

- free
{
   free(dirname);
   [addr2urlHash freeKeys:free values:free];
   [addr2urlHash free];
///[url2addrHash freeKeys:free values:free]; // shares strings with addr2urlHash.
   [url2addrHash free];
   free(fullpathbuf);
   return [super free];
}


// access methods.

- (const char *)urlForAddress:(const char *)mailAddress
{
   return [addr2urlHash valueForKey:mailAddress];
}

- (const char *)addressForURL:(const char *)url
{
   return [url2addrHash valueForKey:url];
}

- (const char *)fileForAddress:(const char *)mailAddress
{
   const char *url = [self urlForAddress:mailAddress];

   if (!url) return NULL;
   return [self _fileForURL:url];
}

- (const char *)fileForURL:(const char *)url
{
   if (![self addressForURL:url]) return NULL;
   return [self _fileForURL:url];
}

//- (const char *)urlForFile:(const char *)file;
   // XXX Lossy url -> filename mapping...

- (void)recordURL:(const char *)url forAddress:(const char *)addr
{
   if (url && *url && addr && *addr)
   {
      const char *cachedURL = [self urlForAddress:addr];
      struct stat st, cached_st;

      /* file corresponding to URL must exist in our cache directory. */
      if (stat([self getPath:fullpathbuf forURL:url], &st) == 0 &&
	  /* URL not previously cached. */
	  (!cachedURL ||
	   /* URLs differ, and file associated with cached file does not exist,
	      or cached file it is empty (and the newly requested one isn't),
	      or cached file is older than the newly requested one. */
	   (strcmp(cachedURL, url) != 0 &&
	    (stat([self getPath:fullpathbuf forURL:cachedURL], &cached_st) < 0 ||
	     (st.st_size > 0 && cached_st.st_size == 0) ||
	     st.st_mtime > cached_st.st_mtime))))
      {
	 if ([self lock])
	 {
	    [self reload];	// Make sure it is in sync with disk cache.
	    [self _setURL:url address:addr];
	    [self flushCache];
	    [self unlock];
	 }
	 // else // XXX schedule for later?
      }
   }
}

@end // EnhanceXImageAliases


@implementation EnhanceXImageAliases (PathUtils)

+ (const char *)getPathName:(char *)buf forURL:(const char *)url
{
   char *d = buf;
   char c;

   // XXX Lossy conversion, all in the name of backward compatibility...
   while ((c = *url++))
   {
      if (c == '/' || isspace(c)) c = '_';
      *d++ = c;
      if (d - buf >= MAXNAMELEN) break;
      // XXX brute way to avoid buffer-overflow attack.
   }
   *d = '\0';

   return buf;
}

- (const char *)getPath:(char *)buf forURL:(const char *)url
{
   char url_path[MAXNAMELEN+1];

   [[self class] getPathName:url_path forURL:url];
   return [self getPath:buf forFile:url_path];
}

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

@end // EnhanceXImageAliases (PathUtils)


@implementation EnhanceXImageAliases (Private)

- (const char *)_fileForURL:(const char *)url
{
   struct stat st;

   [self getPath:fullpathbuf forURL:url];

   if (stat(fullpathbuf, &st) < 0)
   {
      if ([self lock])
      {
	 [self reload];	// Make sure it is in sync with disk cache.
	 [self _removeURL:url];
	 [self flushCache];
	 [self unlock];
      }
      return NULL;
   }
   return (st.st_size > 0) ? fullpathbuf : NULL;
}

- (void)_setURL:(const char *)url address:(const char *)address
{
   NXZone *z = [self zone];
   const char *cachedURL = [addr2urlHash valueForKey:address];

   if (!cachedURL) address = NXCopyStringBufferFromZone(address, z);
   url = NXCopyStringBufferFromZone(url, z);
   [addr2urlHash insertKey:address value:(void *)url];
   [url2addrHash insertKey:url value:(void *)address];
   free((void *)cachedURL);
}

- (void)_removeURL:(const char *)url
{
   char *cachedAddr = [url2addrHash valueForKey:url];

   if (cachedAddr)
   {
      char *cachedURL = [addr2urlHash valueForKey:cachedAddr];

      if (cachedURL)
      {
	 [addr2urlHash removeKey:cachedAddr];
	 free(cachedURL);
      }
      [url2addrHash removeKey:url];
      free(cachedAddr);
   }
}

@end // EnhanceXImageAliases (Private)


@implementation EnhanceXImageAliases (Persistency)

- (BOOL)_loadFromStream:(NXStream *)stream
{
   char address[BUFSIZ], url[BUFSIZ];
   int st;

   while ((st = NXScanf(stream, "%s %[^\n]\n", address, url)) == 2)
   {
      [self _setURL:url address:address];
   }
   return (st == EOF);
}

- (BOOL)_reload:(BOOL)force
{
   char path[MAXPATHLEN+1];
   struct stat st;
   BOOL result = NO;

   if (stat([self getPath:path forFile:ALIASES_URL], &st) != 0) return result;

   if (force || st.st_mtime > stat_cache.st_mtime)
   {
      NXStream *stream = NXMapFile(path, NX_READONLY);

      if (stream == NULL) return result;

      // Clean out current cache.
      [addr2urlHash freeKeys:free values:free];
      [url2addrHash empty];

      result = [self _loadFromStream:stream];

      NXCloseMemory(stream, NX_FREEBUFFER);

      stat_cache = st;
   }

   return result;
}

- (BOOL)load
{
   return [self _reload:YES];
}

- (BOOL)reload
{
   return [self _reload:NO];
}

- (BOOL)lock
{
   char path[MAXPATHLEN+1];
   char line[BUFSIZ];
   char localhost[MAXHOSTNAMELEN];
   int fd;

   [self getPath:path forFile:ALIASES_LOCK];

   if ((fd = open(path, O_CREAT|O_EXCL|O_WRONLY, 0666)) < 0)
   {
      // If old lock, try to remove it and retry.
      if (errno == EEXIST)
      {
	 struct stat st;

	 // XXX race condition?
	 if (stat(path, &st) != 0 ||
	     ((time(NULL) - st.st_mtime) >= 60 && unlink(path) == 0))
	 {
	    fd = open(path, O_CREAT|O_EXCL|O_WRONLY, 0666);
	 }
      }
      if (fd < 0) return NO;
   }
   gethostname(localhost, sizeof(localhost));
   sprintf(line, "%s,%s.app,%d", localhost, [NXApp appName], getpid());
   write(fd, line, strlen(line));
   close(fd);

   return YES;
}

- (void)unlock
{
   char path[MAXPATHLEN+1];

   [self getPath:path forFile:ALIASES_LOCK];
   unlink(path);
}

// Support methods for flushing.

- (BOOL)_writeToFile:(const char *)filename sel:(SEL)sel
{
   BOOL result = NO;
   void (*method)(id, SEL, NXStream *, const char *, const char *) =
        (void (*)(id, SEL, NXStream *, const char *, const char *))[self methodFor:sel];
   NXHashState iterator;
   const void *addr;
   void *url;
   NXStream *stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);

   if (stream == NULL) return result;

   // Iterate over all entries in addr2url, and invoke method to write it.
   for (iterator = [addr2urlHash initState];
	[addr2urlHash nextState:&iterator key:&addr value:&url]; )
   {
      (*method)(self, sel, stream, addr, url);
   }
   NXFlush(stream);
   result = (NXSaveToFile(stream, filename) == 0);
   NXCloseMemory(stream, NX_FREEBUFFER);
   return result;
}

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

   [self getPath:path forFile:ALIASES_URL];
   if ([self _writeToFile:path sel:@selector(_writeAsURL:address:URL:)])
   {
      stat(path, &stat_cache);
      return YES;
   }
   return NO;
}

- (void)_writeAsURL:(NXStream *)stream address:(const char *)address URL:(const char *)url
{
   NXPrintf(stream, "%s %s\n", address, url);
}

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

   [self getPath:path forFile:ALIASES_TIFF];
   return [self _writeToFile:path sel:@selector(_writeAsTiff:address:URL:)];
}

- (void)_writeAsTiff:(NXStream *)stream address:(const char *)address URL:(const char *)url
{
   char buf[MAXNAMELEN+1];

   NXPrintf(stream, "%s %s\n", [[self class] getPathName:buf forURL:url], address);
}

- (BOOL)flushCache
{
   char path[MAXPATHLEN+1];
   int oldumask;
   struct stat dir_stat;
   BOOL result;

   // Propagate directory permissions to (newly created) files.
   stat([self getPath:path forFile:"."], &dir_stat);
   oldumask = umask(~dir_stat.st_mode & 0777);

   result = [self _flushURLs] & [self _flushTIFFs];

   umask(oldumask);

   return result;
}

@end // EnhanceXImageAliases (Persistency)
