/* idxthread.c contains routines to from the ezmlm-idx subject index build */
/* a structure of unique subjects as well as a table of messages with      */
/* pointers to the subject. This leads to information on message threads   */
/* arranged chronologically within the thread, and with the threads        */
/* arranged chronologically by the first message within the range.         */
/* idx_mkthreads() will arrange the author list in a similar manner. This  */
/* saves some space, and takes a little extra time. It's needed when       */
/* generating an author index. */

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "error.h"
#include "alloc.h"
#include "str.h"
#include "stralloc.h"
#include "strerr.h"
#include "lock.h"
#include "open.h"
#include "getln.h"
#include "scan.h"
#include "byte.h"
#include "die.h"
#include "idx.h"
#include "messages.h"
#include "substdio.h"
#include "fmt.h"
#include "readwrite.h"
#include "makehash.h"
#include "yyyymm.h"

#define DATENO 100
static stralloc line = {0};		/* primary input */
static stralloc authline = {0};		/* second line of primary input */
static stralloc dummyind = {0};

static substdio ssindex;
static char indexbuf[1024];

struct stat st;
			/* if no data, these may be the entire table, so */
			/* need to be static */
static subentry sdummy;
static authentry adummy;

int fdlock;

/* NOTE: These do NOT prevent double locking */
static void lockup(void)
{
    fdlock = lockfile("lock");
}

static void unlock(void)
{
    close(fdlock);
}

static void newsub(subentry *psubt,const char *subject,unsigned int sublen,
		   unsigned long msg)
/* Initializes subentry pointed to by psubt, adds a '\0' to subject,    */
/* allocates space and copies in subject, and puts a pointer to it in   */
/* the entry. */
{
  const char *cpfrom;
  char *cpto;
  unsigned int cpno;

  psubt->higher = (subentry *) 0;
  psubt->lower = (subentry *) 0;
  psubt->firstmsg = msg;
  psubt->lastmsg = msg;
  psubt->msginthread = 1;
  if (!(psubt->sub = alloc ((sublen) * sizeof(char))))
    die_nomem();
  cpto = psubt->sub;
  cpno = sublen;
  cpfrom = subject;
  while (cpno--) *(cpto++) = *(cpfrom++);
  psubt->sublen = sublen;
}

static void newauth(authentry *pautht,		/* entry for current message */
		    const char *author,/* pointer to author string (not sz!) */
		    unsigned int authlen,	/* lenth of author */
		    unsigned long msg)		/* sz */
/* Allocates space for author of length authlen+1 adding a terminal '\0' */
/* and puts the pointer in pautht->auth. Analog to newsub().             */
{
  const char *cpfrom;
  char *cpto;
  unsigned int cpno;

  pautht->higher = (subentry *) 0;
  pautht->lower = (subentry *) 0;
  pautht->firstmsg = msg;
  if (!(pautht->auth = alloc ((authlen) * sizeof(char))))
    die_nomem();
  cpto = pautht->auth;
  cpno = authlen;
  cpfrom = author;
  while (cpno--) *(cpto++) = *(cpfrom++);
  pautht->authlen = authlen;
}

static void init_dummy(void)
{
  unsigned int i;

  if (!stralloc_ready(&dummyind,HASHLEN + 1)) die_nomem();
  for (i = 0; i< HASHLEN; i++)
    dummyind.s[i] = 'a';
  dummyind.len = HASHLEN;
  if (!stralloc_append(&dummyind,' ')) die_nomem();
}

void idx_mkthreads(msgentry **pmsgtable,	/* table of message<->subject */
		   subentry **psubtable,	/* subject no, len, str char * */
		   authentry **pauthtable,	/* author no, len, str char* */
		   dateentry **pdatetable,	/* message per date */
		   unsigned long msg_from,	/* first message in range */
		   unsigned long msg_to,	/* last message in range */
		   unsigned long msg_latest,	/* latest message in archive (for locking) */
		   int locked)			/* if already locked */

/* Threads messages msg_from -> msg_to into pmsgtable & psubtable. When  */
/* reading the latest index file (containing msg_latest) it locks the    */
/* directory, unless it is already locked (as in digest creation).       */
/* msgtable has the subject number 1.. (0 if there is no subject match,  */
/* which should happen only if the subject index is corrupt.)            */

/* 19971107 Changed to deal with index files that are missing, or have   */
/* missing entries, not necessarily reflecting missing archive files.    */
/* This all to make ezmlm-get more robust to get maximal info out of     */
/* corrupted archives.                                                   */
{
  unsigned long idxlatest;	/* need to lock for this (last) index file */
  unsigned long msg;		/* current msg number */
  unsigned long endmsg;		/* max msg in this idx file */
  unsigned long tmpmsg;		/* index entry's msg number */
  unsigned long idx;		/* current index file no */
  unsigned long idxto;		/* index containing end of range */
  unsigned long ulmrange;	/* total # of messages in range */
  char *subject;		/* subject on line */
  unsigned int sublen;		/* length of subject */
  char *auth = 0;
  unsigned int authlen = 0;
  unsigned int pos = 0;
  unsigned int posa;
  subentry *psubnext;		/* points to next entry in subtable */
  subentry *psubt;		/* points to entry in subtable */
  authentry *pauthnext;		/* points to next entry in authtable */
  authentry *pautht;		/* points to entry in authtable */
  int fd;			/* index file handle */
  int flagmissingindex;		/* current index file is missing */
  int flagauth = 0;		/* read index entry has author info */
  int hasauth;			/* current msg's entry has author info */
  msgentry *pmsgt;
  int res;
  int match;
  unsigned int datepos,datemax;
  unsigned int datetablesize,datetableunit;
  unsigned int lastdate = 0;
  unsigned int thisdate;
  msgentry *x, *y;
  char strnum[FMT_ULONG];

				/* a few unnecessary sanity checks */
  if (msg_to > msg_latest)
    msg_to = msg_latest;
  if (msg_to < msg_from)
    strerr_die2x(100,FATAL,"Program error: bad range in idx_mkthreads");
  ulmrange = msg_to - msg_from + 1;
  if (!(*pmsgtable = (msgentry *) alloc(ulmrange * sizeof(msgentry))))
        die_nomem();
  y = *pmsgtable;
  x = y + ulmrange;		/* clear */
  while (--x >= y) {
    x->subnum = 0;
    x->authnum = 0;
    x->date = 0;
  }
				/* max entries - acceptable waste for now */
  if (!(*psubtable = (subentry *) alloc((ulmrange+1) * sizeof(subentry))))
        die_nomem();

  if (!(*pauthtable = (authentry *) alloc((ulmrange+1) * sizeof(authentry))))
        die_nomem();
  datetableunit = DATENO * sizeof(dateentry);
  datetablesize = datetableunit;
  if (!(*pdatetable = (dateentry *) alloc(datetablesize)))
        die_nomem();
  datepos = 0;
  datemax = DATENO - 2;		/* entry 0 and end marker */
  lastdate = 0;

  idxlatest = msg_latest / 100;
  idxto = msg_to / 100;
  psubnext = *psubtable;	/* dummy node to get tree going. Basically, */
  psubt = &sdummy;		/* assure that subject > psubt-sub and that */
  init_dummy();			/* below ok unless HASHLEN > 40 */
  psubt->sub = (char*)"                                       ";
  psubt->sublen = 40;		/* there is something to hold psubt->higher */
  psubt->higher = (subentry *) 0;
  psubt->lower = (subentry *) 0;
  pauthnext = *pauthtable;
  pautht = &adummy;
  pautht->auth = psubt->sub;
  pautht->authlen = psubt->sublen;
  pautht->higher = (authentry *) 0;
  pautht->lower = (authentry *) 0;
  for (idx = msg_from / 100; idx <= idxto; idx++) {
				/* make index file name */
    if (!stralloc_copys(&line,"archive/")) die_nomem();
    if (!stralloc_catb(&line,strnum,fmt_ulong(strnum,idx))) die_nomem();
    if (!stralloc_cats(&line,"/index")) die_nomem();
    if (!stralloc_0(&line)) die_nomem();
    if (!locked && idx == idxlatest)
      lockup();
    flagmissingindex = 0;
    fd = open_read(line.s);
    if (fd == -1) {
      if (errno == error_noent) {	/* this means the index is not here */
					/* but the lists is supposedly indexed*/
        flagmissingindex = 1;
      } else
        strerr_die2sys(111,FATAL,MSG1(ERR_OPEN,line.s));
    } else
      substdio_fdbuf(&ssindex,read,fd,indexbuf,sizeof(indexbuf));

    msg = 100L * idx;			/* current msg# */
    endmsg = msg + 99L;			/* max msg in this index */
    if (!msg) msg = 1L;			/* for start to make msg > tmpmsg */
    tmpmsg = 0L;			/* msg number of read index line */
    if (endmsg > msg_to)		/* skip non-asked for subjects */
      endmsg = msg_to;
    for (; msg <= endmsg; msg++) {
      if (!flagmissingindex && (msg > tmpmsg)) {
        flagauth = 0;
        if (getln(&ssindex,&line,&match,'\n') == -1)
          strerr_die2sys(111,FATAL,MSG1(ERR_READ,"index"));
        if (!match)
          flagmissingindex = 1;
        else {
          pos = scan_ulong(line.s,&tmpmsg);
          if (line.s[pos++] == ':') {
            if (getln(&ssindex,&authline,&match,'\n') == -1)
              strerr_die2sys(111,FATAL,MSG1(ERR_READ,"index"));
            if (!match)
              flagmissingindex = 1;
            else {
              flagauth = 1;
	    }
            pos++;
          }
        }
      }
      if (msg < msg_from)	/* Nothing before start of range */
        continue;
      if (msg == tmpmsg) {
        subject = line.s + pos;
        sublen = line.len - pos;
	if (sublen <= HASHLEN)
	  strerr_die2x(100,FATAL,MSG(ERR_BAD_INDEX));
        hasauth = flagauth;
      } else {
        subject = dummyind.s;
        sublen = dummyind.len;
        hasauth = 0;
      }
      for(;;) {		/* search among already known subjects */
        res = str_diffn(psubt->sub,subject,HASHLEN);
        if (res < 0) {
          if (psubt->higher)
            psubt = psubt->higher;
          else {
            newsub(psubnext,subject,sublen,msg);
            psubt->higher = psubnext;
            psubt = psubnext;
            psubnext++;
            break;
          }
        } else if (res > 0) {
          if (psubt->lower)
            psubt = psubt->lower;
          else {
            newsub(psubnext,subject,sublen,msg);
            psubt->lower = psubnext;
            psubt = psubnext;
            psubnext++;
            break;
          }
        } else {
          psubt->lastmsg = msg;
	  (psubt->msginthread)++;	/* one more message in thread */
	  break;
        }
      }
				/* first subnum =1 (=0 is empty for thread) */
      pmsgt = *pmsgtable + msg - msg_from;
      pmsgt->subnum = (unsigned int) (psubt - *psubtable + 1);
      pmsgt->date = lastdate;
      if (hasauth) {
	pos = 0;
	while (pos < authline.len && authline.s[pos] != ' ') pos++;
	if (++pos < authline.len) {
	  thisdate = date2yyyymm(authline.s + pos);
	  if (thisdate) pmsgt->date = thisdate;
	  if (pmsgt->date > lastdate) {
	    lastdate = pmsgt->date;
	    if (datepos >= datemax) {		/* more space */
	      datemax += DATENO;
	      if (!alloc_re((void**)pdatetable,
			    datetablesize,datetablesize+datetableunit))
		die_nomem();
	    }
	    (*pdatetable)[datepos].msg = msg;	/* first msg this mo */
	    (*pdatetable)[datepos].date = lastdate;
	    datepos++;
	  }
	  posa = byte_chr(authline.s,authline.len,';');
	  if (authline.len > posa + HASHLEN + 1 && authline.s[pos+1] != ' ') {
					/* old: "; auth", new: ";hash auth" */
	    auth = authline.s + posa + 1;
	    authlen = authline.len - posa - 1;
	  } else {
	    auth = dummyind.s;
	    authlen = dummyind.len;
	  }
	}
			/* allright! Same procedure, but for author */
        for (;;) {	/* search among already known authors */
          res = str_diffn(pautht->auth,auth,HASHLEN);
          if (res < 0) {
            if (pautht->higher)
              pautht = pautht->higher;
            else {
              newauth(pauthnext,auth,authlen,msg);
              pautht->higher = pauthnext;
              pautht = pauthnext;
              pauthnext++;
              break;
            }
          } else if (res > 0) {
            if (pautht->lower)
              pautht = pautht->lower;
            else {
              newauth(pauthnext,auth,authlen,msg);
              pautht->lower = pauthnext;
              pautht = pauthnext;
              pauthnext++;
              break;
            }
          } else {
            break;
          }
        }			/* link from message to this author */
        pmsgt->authnum = (unsigned int) (pautht - *pauthtable + 1);
        pautht = *pauthtable;
      }

      psubt = *psubtable;	/* setup psubt. Done here rather than before */
				/* the for loop, so that we can start off    */
				/* the dummy node. */
    }
    if (fd != -1)
      close(fd);
    if (!locked && idx == idxlatest)
      unlock();			/* 'locked' refers to locked before calling */
  }
  psubnext->sub = (char *) 0;		/* end of table marker */
  pauthnext->auth = (char *) 0;		/* end of table marker */
  (*pdatetable)[datepos].msg = msg_to + 1;
  (*pdatetable)[datepos].date = lastdate + 1;
}


void idx_mkthread(msgentry **pmsgtable,		/* pointer to table of message<->subject */
		  subentry **psubtable,		/* ptr to tbl of subject no, len, str char * */
		  authentry **pauthtable,
		  unsigned long msg_from,	/* first message in range */
		  unsigned long msg_to,		/* last message in range */
		  unsigned long msg_master,	/* master message for single thread, else 0*/
		  unsigned long msg_latest,	/* latest message in archive (for locking) */
		  int locked)			/* if already locked */

/* Works like idx_mkthreads, except that it finds the subject for message   */
/* msg_master, then identifies messages in the range that have the same     */
/* subject. msgtable entries with subject 0 do not match, with '1' do match.*/
{
  unsigned long idxlatest;	/* need to lock for this (last) index file */
  unsigned long idxto;		/* index for last msg in range */
  unsigned long idx;		/* current index file no */
  unsigned long msg;		/* index entry's msg number */
  unsigned long ulmrange;	/* total # of messages in range */
  subentry *psubt;		/* points to last entry in subtable */
  int ffound;			/* msg subject was found in subtable */
  int flagauth;			/* there is author info */
  int firstfound = 1;		/* = 1 until first message in thread found */
  int res;			/* comparison result */
  char *auth;
  unsigned int authlen;
  authentry *pauthnext;		/* points to next entry in authtable */
  authentry *pautht;		/* points to entry in authtable */
  unsigned int pos;
  int fd;			/* index file handle */
  int match;
  msgentry *pmsgt;
  msgentry *x,*y;
  char strnum[FMT_ULONG];

  if ((ulmrange = msg_to - msg_from +1) <= 0)
    strerr_die2x(100,FATAL,"Program error: bad range in idx_mkthreads");
  if (!(*pmsgtable = (msgentry *) alloc(ulmrange * sizeof(msgentry))))
         die_nomem();
  y = *pmsgtable;
  x = y + ulmrange;
  while (--x >= y) {
    x->subnum = 0;
    x->authnum = 0;
    x->date = 0;
  }

  if (!(*psubtable = (subentry *) alloc(2 * sizeof(subentry))))
          die_nomem();

  if (!(*pauthtable = (authentry *) alloc((ulmrange + 1) * sizeof(authentry))))
          die_nomem();

  pauthnext = *pauthtable;
  pautht = &adummy;
  init_dummy();
  pautht->auth = (char*)"                     ";
  pautht->authlen = 21;
  pautht->higher = (authentry *) 0;
  pautht->lower = (authentry *) 0;
  idxlatest = msg_latest / 100;
  idxto = msg_to / 100;
  idx = msg_master / 100;	/* index for master subject */

				/* Get master subject */
  if (!stralloc_copys(&line,"archive/")) die_nomem();
  if (!stralloc_catb(&line,strnum,fmt_ulong(strnum,idx))) die_nomem();
  if (!stralloc_cats(&line,"/index")) die_nomem();
  if (!stralloc_0(&line)) die_nomem();
  ffound = 0;
  if (!locked && idx == idxlatest)
    lockup();
  fd = open_read(line.s);
  psubt = *psubtable;
  if (fd == -1) {
    if (errno != error_noent)
      strerr_die2sys(111,FATAL,MSG1(ERR_OPEN,line.s));
    else
      strerr_die2x(111,FATAL,MSG(ERR_NOINDEX));	/* temp - admin can fix! */
  } else {
    substdio_fdbuf(&ssindex,read,fd,indexbuf,sizeof(indexbuf));
    for(;;) {
      if (getln(&ssindex,&line,&match,'\n') == -1)
          strerr_die2sys(111,FATAL,MSG1(ERR_OPEN,"index"));
      if (!match)
        break;
      pos=scan_ulong(line.s,&msg);
      if (line.s[pos++] == ':') {	 /* marker for author info */
        pos++;
        flagauth = 1;
      } else
        flagauth = 0;
      if (msg == msg_master) {
        newsub(psubt,line.s+pos,line.len-pos,msg);
					/* need to update msg later! */
        ffound = 1;
        break;
      }
      if (flagauth) {			/* skip author line */
        if (getln(&ssindex,&line,&match,'\n') == -1)
          strerr_die2sys(111,FATAL,MSG1(ERR_OPEN,"index"));
      if (!match)
        break;
      }
    }
    close(fd);
  }
  if (!locked && idx == idxlatest)
    unlock();
  if (!ffound)
      strerr_die2x(100,FATAL,MSG(ERR_NOINDEX));
  for (idx = msg_from / 100; idx <= idxto; idx++) {
		/* make index file name */
    if (!stralloc_copys(&line,"archive/")) die_nomem();
    if (!stralloc_catb(&line,strnum,fmt_ulong(strnum,idx))) die_nomem();
    if (!stralloc_cats(&line,"/index")) die_nomem();
    if (!stralloc_0(&line)) die_nomem();
    if (!locked && idx == idxlatest)
      lockup();
    fd = open_read(line.s);
    if (fd == -1) {
      if (errno != error_noent)
        strerr_die2sys(111,FATAL,MSG1(ERR_OPEN,line.s));
    } else {
      substdio_fdbuf(&ssindex,read,fd,indexbuf,sizeof(indexbuf));
      for(;;) {
        if (getln(&ssindex,&line,&match,'\n') == -1)
          strerr_die2sys(111,FATAL,MSG1(ERR_READ,"index"));
        if (!match)
          break;
        pos=scan_ulong(line.s,&msg);
        if (line.s[pos++] == ':') {
          pos++;
          flagauth = 1;
          if (getln(&ssindex,&authline,&match,'\n') == -1)
            strerr_die2sys(111,FATAL,MSG1(ERR_READ,"index"));
          if (!match)
            break;
        } else
          flagauth = 0;
        if (msg < msg_from)	/* Nothing before start of range */
          continue;
        if (msg > msg_to)	/* Don't do anything after range */
          break;
        if (!str_diffn(psubt->sub,line.s+pos,HASHLEN)) {
          pmsgt = *pmsgtable + msg - msg_from;
          if (firstfound) {	/* update to first message with this subj */
            psubt->firstmsg = msg;
            firstfound = 0;
          }
	  psubt->lastmsg = msg;
          pmsgt->subnum = 1;
          if (flagauth) {
	    if (authline.len > 0)
	      pmsgt->date = date2yyyymm(authline.s + 1);
            pos = byte_chr(authline.s,authline.len,';');
	    if (authline.len > pos + HASHLEN + 1 && authline.s[pos+1] != ' ') {
					/* old: "; auth", new: ";hash auth" */
	      auth = authline.s + pos + 1;
	      authlen = authline.len - pos - 1;
	    } else {
	      auth = dummyind.s;
	      authlen = dummyind.len;
	    }
	    for (;;) {		/* search among already known authors */
	      res = str_diffn(pautht->auth,auth,HASHLEN);
	      if (res < 0) {
	        if (pautht->higher)
	          pautht = pautht->higher;
	        else {
	          newauth(pauthnext,auth,authlen,msg);
	          pautht->higher = pauthnext;
	          pautht = pauthnext;
	          pauthnext++;
	          break;
	        }
	      } else if (res > 0) {
	        if (pautht->lower)
	          pautht = pautht->lower;
	        else {
	          newauth(pauthnext,auth,authlen,msg);
	          pautht->lower = pauthnext;
	          pautht = pauthnext;
	          pauthnext++;
	          break;
	        }
	      } else {
	        break;
	      }
	    }			/* link from message to this author */
	    pmsgt->authnum = (unsigned int) (pautht - *pauthtable + 1);
            pautht = *pauthtable;
	  }

        }
      }
      close(fd);
    }
    if (!locked && idx == idxlatest)
      unlock();
  }
  ++psubt;
  psubt->sub = (char *) 0;	/* end of table marker */
  pauthnext->auth = (char *) 0;	/* end of table marker */
}

void idx_mklist(msgentry **pmsgtable,	/* pointer to table of message<->subject */
		subentry **psubtable,	/* ptr to tbl of subject no, len, str char * */
		authentry **pauthtable,
		unsigned long msg_from,		/* first message in range */
		unsigned long msg_to)		/* last message in range */
/* Like mkthreads, except that it works without a subject index. The result */
/* is just a dummy subject and a sequential list of messages. This to allow */
/* use of the same routines when creating digest from lists that have no    */
/* subject index (for whatever reason). */
{
  unsigned long ulmrange;
  msgentry *x,*y;
  subentry *psubt;
  authentry *pautht;

  if ((ulmrange = msg_to - msg_from +1) <= 0)
    strerr_die2x(111,FATAL,"bad range in idx_mkthreads :");

  if (!(*pmsgtable = (msgentry *) alloc(ulmrange * sizeof(msgentry))))
         die_nomem();

  y = *pmsgtable;
  x = y + ulmrange;
  while (--x >= y) {
    x->subnum = 1;
    x->authnum = 0;
    x->date = 0;
  }

  if (!(*psubtable = (subentry *) alloc(2 * sizeof(subentry))))
          die_nomem();
  psubt = *psubtable;
  newsub(psubt,dummyind.s,dummyind.len,msg_from);
  psubt->lastmsg = msg_to;
  ++psubt;
  psubt->sub = (char *) 0;
  if (!(*pauthtable = (authentry *) alloc(sizeof(authentry))))
          die_nomem();		/* nodata. Avoid dangling ptr. */
  pautht = *pauthtable;
  pautht->auth = 0;		/* tells app that there are no author data */
  pautht->higher = (authentry *) 0;
  pautht->lower = (authentry *) 0;
}

void idx_destroythread(msgentry *msgtable,
		       subentry *subtable,
		       authentry *authtable)
/* Frees space allocated by idxthread routines. This is needed only if */
/* one does several threadings in one program run. Otherwise, exit()   */
/* should free all allocated memory, which will be faster. */
{
  subentry *psubt;
  authentry *pautht;

  psubt = subtable;		/* free subjects */
  while(psubt->sub) {
    alloc_free(psubt->sub);
    psubt++;
  }

  pautht = authtable;		/* free authors */
  while(pautht->auth) {
    alloc_free(pautht->auth);
    pautht++;
  }

  alloc_free(subtable);		/* free subtable */
  alloc_free(authtable);	/* free authtable */
  alloc_free(msgtable);		/* free msgtable */
  subtable = (subentry *) 0;	/* kill pointers */
  authtable = (authentry *) 0;
  msgtable = (msgentry *) 0;
}
