/*  $Revision: 1.2+histcache$
**
**  History file routines.
*/
#include <stdio.h>
#include <sys/types.h>
#include "configdata.h"
#include "clibrary.h"
#include "innd.h"
#include "dbz.h"
#if	defined(TIMER)
#include "timer.h"
#endif	/* defined(TIMER) */

/*
 * history lookup cache
 */
#define HISCACHESIZE  (64 * 1024L)    /* num elements in lookup cache array */
typedef struct {
    long              MessageIDHash;
    BOOL              Found;
} _HIScache;
STATIC _HIScache      HIScache[HISCACHESIZE];
unsigned long         HIScachehits;
unsigned long         HIScachelookups;

STATIC char	HIShistpath[] = _PATH_HISTORY;
STATIC FILE	*HISwritefp;
STATIC int	HISreadfd;
STATIC int	HISdirty;
STATIC int	HISincore = INND_DBZINCORE;

#include "timer.h"


/*
**  Set up the history files.
*/
void
HISsetup()
{
    if (HISwritefp == NULL) {
	/* Open the history file for appending formatted I/O. */
	if ((HISwritefp = fopen(HIShistpath, "a")) == NULL) {
	    syslog(L_FATAL, "%s cant fopen %s %m", LogName, HIShistpath);
	    exit(1);
	}
	CloseOnExec((int)fileno(HISwritefp), TRUE);

	/* Open the history file for reading. */
	if ((HISreadfd = open(HIShistpath, O_RDONLY)) < 0) {
	    syslog(L_FATAL, "%s cant open %s %m", LogName, HIShistpath);
	    exit(1);
	}
	CloseOnExec(HISreadfd, TRUE);

	/* Open the DBZ file. */
	(void)dbzincore(HISincore);
	(void)dbzwritethrough(1);
	if (dbminit(HIShistpath) < 0) {
	    syslog(L_FATAL, "%s cant dbminit %s %m", HIShistpath, LogName);
	    exit(1);
	}
    }
    /* initialize history lookup cache data */
    memset((void *)&HIScache, 0, sizeof(HIScache));
    HIScachehits = HIScachelookups = 0;
}


/*
**  Synchronize the in-core history file (flush it).
*/
void
HISsync()
{
#if	defined(TIMER)
    TMRstart(TMR_HISSYNC);
#endif	/* defined(TIMER) */
    if (HISdirty) {
	if (dbzsync()) {
	    syslog(L_FATAL, "%s cant dbzsync %m", LogName);
	    exit(1);
	}
	HISdirty = 0;
    }
#if	defined(TIMER)
    TMRstop(TMR_HISSYNC);
#endif	/* defined(TIMER) */
}


/*
**  Close the history files.
*/
void
HISclose()
{
    if (HISwritefp != NULL) {
	/* Since dbmclose calls dbzsync we could replace this line with
	 * "HISdirty = 0;".  Oh well, it keeps the abstraction clean. */
	HISsync();
	if (dbmclose() < 0)
	    syslog(L_ERROR, "%s cant dbmclose %m", LogName);
	if (fclose(HISwritefp) == EOF)
	    syslog(L_ERROR, "%s cant fclose history %m", LogName);
	HISwritefp = NULL;
	if (close(HISreadfd) < 0)
	    syslog(L_ERROR, "%s cant close history %m", LogName);
	HISreadfd = -1;
    }

    if (HIScachelookups > 0)
        syslog(L_NOTICE, "SERVER history cache final: %lu lookups, %lu hits",
       	       HIScachelookups, HIScachehits);
}


/*
**  File in the DBZ datum for a Message-ID, making sure not to copy any
**  illegal characters.
*/
STATIC void
HISsetkey(p, keyp)
    register char	*p;
    datum		*keyp;
{
    static BUFFER	MessageID;
    register char	*dest;
    register int	i;

    /* Get space to hold the ID. */
    i = strlen(p);
    if (MessageID.Data == NULL) {
	MessageID.Data = NEW(char, i + 1);
	MessageID.Size = i;
    }
    else if (MessageID.Size < i) {
	RENEW(MessageID.Data, char, i + 1);
	MessageID.Size = i;
    }

    for (keyp->dptr = dest = MessageID.Data; *p; p++)
	if (*p == HIS_FIELDSEP || *p == '\n')
	    *dest++ = HIS_BADCHAR;
	else
	    *dest++ = *p;
    *dest = '\0';

    keyp->dsize = dest - MessageID.Data + 1;
}


/*
**  Get the list of files under which a Message-ID is stored.
*/
char *
HISfilesfor(MessageID)
    char		*MessageID;
{
    static BUFFER	Files;
    char		*dest;
    datum		key;
    datum		val;
    long		offset;
    register char	*p;
    register int	i;

    /* Get the seek value into the history file. */
#if	defined(TIMER)
    TMRstart(TMR_HISGREP);
#endif	/* defined(TIMER) */
    HISsetkey(MessageID, &key);
    val = dbzfetch(key);
#if	defined(TIMER)
    TMRstop(TMR_HISGREP);
#endif	/* defined(TIMER) */
    if (val.dptr == NULL || val.dsize != sizeof offset)
	return NULL;

    /* Get space. */
    if (Files.Data == NULL) {
	Files.Size = BUFSIZ;
	Files.Data = NEW(char, Files.Size);
    }

    /* Copy the value to an aligned spot. */
    for (p = val.dptr, dest = (char *)&offset, i = sizeof offset; --i >= 0; )
	*dest++ = *p++;
    if (lseek(HISreadfd, offset, SEEK_SET) == -1)
	return NULL;

    /* Read the text until \n or EOF. */
    for (Files.Used = 0; ; ) {
	i = read(HISreadfd,
		&Files.Data[Files.Used], Files.Size - Files.Used - 1);
	if (i <= 0)
	    return NULL;
	Files.Used += i;
	Files.Data[Files.Used] = '\0';
	if ((p = strchr(Files.Data, '\n')) != NULL) {
	    *p = '\0';
	    break;
	}

	/* If we have half our buffer left, get more space. */
	if (Files.Size - Files.Used < Files.Size / 2) {
	    Files.Size += BUFSIZ;
	    RENEW(Files.Data, char, Files.Size);
	}
    }

    /* Move past the first two fields -- Message-ID and date info. */
    if ((p = strchr(Files.Data, HIS_FIELDSEP)) == NULL)
	return NULL;
    if ((p = strchr(p + 1, HIS_FIELDSEP)) == NULL)
	return NULL;

    /* Translate newsgroup separators to slashes, return the fieldstart. */
    for (dest = ++p; *p; p++)
	if (*p == '.')
	    *p = '/';
    return dest;
}


/*
**  Have we already seen an article?
*/
BOOL
HIShavearticle(MessageID)
    char	*MessageID;
{
    datum	key;
    datum	val;
    long	hash_pos, hash_val;


    /* hash msg id */
    hash_val = dbzhash(MessageID, strlen(MessageID));
    hash_pos = hash_val % HISCACHESIZE;

    /* check history lookup cache for hashed msg id */
    HIScachelookups++;
    if (HIScache[hash_pos].MessageIDHash == hash_val) {
        HIScachehits++;
        return HIScache[hash_pos].Found;
    }

    /* check dbz history for msg id */
#if	defined(TIMER)
    TMRstart(TMR_HISHAVE);
#endif	/* defined(TIMER) */
    HISsetkey(MessageID, &key);
    val = dbzfetch(key);

    /* store hashed msg id, lookup result in history lookup cache */
    HIScache[hash_pos].MessageIDHash = hash_val;
    HIScache[hash_pos].Found = (char)(val.dptr != NULL);

#if	defined(TIMER)
    TMRstop(TMR_HISHAVE);
#endif	/* defined(TIMER) */

    /* return result of lookup */
    return val.dptr != NULL;
}


/*
**  Turn a history filename entry from slashes to dots.  It's a pity
**  we have to do this.
*/
STATIC void
HISslashify(p)
    register char	*p;
{
    register char	*last;

    for (last = NULL; *p; p++) {
	if (*p == '/') {
	    *p = '.';
	    last = p;
	}
	else if (*p == ' ' && last != NULL)
	    *last = '/';
    }
    if (last)
	*last = '/';
}


/*
**  Write a history entry.
*/
BOOL
HISwrite(Data, paths)
    ARTDATA		*Data;
    char		*paths;
{
    static char		NOPATHS[] = "";
    long		offset, hash_val, hash_pos;
    datum		key;
    datum		val;
    int			i;

#if	defined(TIMER)
    TMRstart(TMR_HISWRITE);
#endif	/* defined(TIMER) */
    HISsetkey(Data->MessageID, &key);
    if (paths != NULL && paths[0] != '\0')
	HISslashify(paths);
    else
	paths = NOPATHS;

    offset = ftell(HISwritefp);
    if (Data->Expires > 0)
	i = fprintf(HISwritefp, "%s%c%lu%c%lu%c%lu%c%s\n",
                    key.dptr, HIS_FIELDSEP,
                    (unsigned long)Data->Arrived, HIS_SUBFIELDSEP,
                    (unsigned long)Data->Expires,
		    HIS_SUBFIELDSEP, (unsigned long)Data->Posted, HIS_FIELDSEP,
                    paths);
    else
	i = fprintf(HISwritefp, "%s%c%lu%c%s%c%lu%c%s\n",
                    key.dptr, HIS_FIELDSEP,
                    (unsigned long)Data->Arrived,
                    HIS_SUBFIELDSEP, HIS_NOEXP,
		    HIS_SUBFIELDSEP, (unsigned long)Data->Posted, HIS_FIELDSEP,
                    paths);
    if (i == EOF || fflush(HISwritefp) == EOF) {
	/* The history line is now an orphan... */
	i = errno;
	syslog(L_ERROR, "%s cant write history %m", LogName);
	IOError("history", i);
#if	defined(TIMER)
        TMRstop(TMR_HISWRITE);
#endif	/* defined(TIMER) */
	return FALSE;
    }

    /* Set up the database values and write them. */
    val.dptr = (char *)&offset;
    val.dsize = sizeof offset;
    if (dbzstore(key, val) < 0) {
	i = errno;
	syslog(L_ERROR, "%s cant dbzstore %m", LogName);
	IOError("history database", i);
#if	defined(TIMER)
	TMRstop(TMR_HISWRITE);
#endif	/* defined(TIMER) */
	return FALSE;
    }
#if	defined(TIMER)
    TMRstop(TMR_HISWRITE);
#endif	/* defined(TIMER) */
    /* store hashed message id value in history lookup cache */
    hash_val = dbzhash(Data->MessageID, strlen(Data->MessageID));
    hash_pos = hash_val % HISCACHESIZE;
    HIScache[hash_pos].MessageIDHash = hash_val;
    HIScache[hash_pos].Found = TRUE;

    if (++HISdirty >= ICD_SYNC_COUNT)
	HISsync();
    return TRUE;
}
