#include "common.h"
#include <ctype.h>
#include <plumb.h>
#include <libsec.h>
#include "dat.h"

enum {
	Buffersize = 64*1024,
};

typedef struct Inbuf Inbuf;
struct Inbuf
{
	int	fd;
	uchar	*lim;
	uchar	*rptr;
	uchar	*wptr;
	uchar	data[Buffersize+7];
};

static void
addtomessage(Message *m, uchar *p, int n, int done)
{
	int i, len;

	// add to message (+1 in malloc is for a trailing NUL)
	if(m->lim - m->end < n){
		if(m->start != nil){
			i = m->end-m->start;
			if(done)
				len = i + n;
			else
				len = (4*(i+n))/3;
			m->start = erealloc(m->start, len + 1);
			m->end = m->start + i;
		} else {
			if(done)
				len = n;
			else
				len = 2*n;
			m->start = emalloc(len + 1);
			m->end = m->start;
		}
		m->lim = m->start + len;
		*m->lim = '\0';
	}

	memmove(m->end, p, n);
	m->end += n;
	*m->end = '\0';
}

//
//  read in a single message
//
static int
readmessage(Message *m, Inbuf *inb)
{
	int i, n, done;
	uchar *p, *np;
	char sdigest[SHA1dlen*2+1];
	char tmp[64];

	for(done = 0; !done;){
		n = inb->wptr - inb->rptr;
		if(n < 6){
			if(n)
				memmove(inb->data, inb->rptr, n);
			inb->rptr = inb->data;
			inb->wptr = inb->rptr + n;
			i = read(inb->fd, inb->wptr, Buffersize);
			if(i < 0){
				if(fd2path(inb->fd, tmp, sizeof tmp) < 0)
					strcpy(tmp, "unknown mailbox");
				fprint(2, "error reading '%s': %r\n", tmp);
				return -1;
			}
			if(i == 0){
				if(n != 0)
					addtomessage(m, inb->rptr, n, 1);
				if(m->end == m->start)
					return -1;
				break;
			}
			inb->wptr += i;
		}

		// look for end of message
		for(p = inb->rptr; p < inb->wptr; p = np+1){
			// first part of search for '\nFrom '
			np = memchr(p, '\n', inb->wptr - p);
			if(np == nil){
				p = inb->wptr;
				break;
			}

			/*
			 *  if we've found a \n but there's
			 *  not enough room for '\nFrom ', don't do
			 *  the comparison till we've read in more.
			 */
			if(inb->wptr - np < 6){
				p = np;
				break;
			}

			if(strncmp((char*)np, "\nFrom ", 6) == 0){
				done = 1;
				p = np+1;
				break;
			}
		}

		// add to message (+ 1 in malloc is for a trailing null)
		n = p - inb->rptr;
		addtomessage(m, inb->rptr, n, done);
		inb->rptr += n;
	}

	// if it doesn't start with a 'From ', this ain't a mailbox
	if(strncmp(m->start, "From ", 5) != 0)
		return -1;

	// dump trailing newline, make sure there's a trailing null
	// (helps in body searches)
	if(*(m->end-1) == '\n')
		m->end--;
	*m->end = 0;
	m->bend = m->rbend = m->end;

	// digest message
	sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
	for(i = 0; i < SHA1dlen; i++)
		sprint(sdigest+2*i, "%2.2ux", m->digest[i]);
	m->sdigest = s_copy(sdigest);

	return 0;
}


// throw out deleted messages.  return number of freshly deleted messages
int
purgedeleted(Mailbox *mb)
{
	Message *m, *next;
	int newdels;

	// forget about what's no longer in the mailbox
	newdels = 0;
	for(m = mb->root->part; m != nil; m = next){
		next = m->next;
		if(m->deleted && m->refs == 0){
			if(m->inmbox)
				newdels++;
			delmessage(mb, m);
		}
	}
	return newdels;
}

//
//  read in the mailbox and parse into messages.
//
static char*
_readmbox(Mailbox *mb, int doplumb, Mlock *lk)
{
	int fd, n;
	String *tmp;
	Dir *d;
	static char err[Errlen];
	Message *m, **l;
	Inbuf *inb;
	char *x;

	l = &mb->root->part;

	/*
	 *  open the mailbox.  If it doesn't exist, try the temporary one.
	 */
	n = 0;
retry:
	fd = open(mb->path, OREAD);
	if(fd < 0){
		rerrstr(err, sizeof(err));
		if(strstr(err, "exclusive lock") != 0 && n++ < 20){
			sleep(500);	/* wait for lock to go away */
			goto retry;
		}
		if(strstr(err, "exist") != 0){
			tmp = s_copy(mb->path);
			s_append(tmp, ".tmp");
			if(sysrename(s_to_c(tmp), mb->path) == 0){
				s_free(tmp);
				goto retry;
			}
			s_free(tmp);
		}
		return err;
	}

	/*
	 *  a new qid.path means reread the mailbox, while
	 *  a new qid.vers means read any new messages
	 */
	d = dirfstat(fd);
	if(d == nil){
		close(fd);
		errstr(err, sizeof(err));
		return err;
	}
	if(mb->d != nil){
		if(d->qid.path == mb->d->qid.path && d->qid.vers == mb->d->qid.vers){
			close(fd);
			free(d);
			return nil;
		}
		if(d->qid.path == mb->d->qid.path){
			while(*l != nil)
				l = &(*l)->next;
			seek(fd, mb->d->length, 0);
		}
		free(mb->d);
	}
	mb->d = d;
	mb->vers++;
	henter(PATH(0, Qtop), mb->name,
		(Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);

	inb = emalloc(sizeof(Inbuf));
	inb->rptr = inb->wptr = inb->data;
	inb->fd = fd;

	//  read new messages
	snprint(err, sizeof err, "reading '%s'", mb->path);
	logmsg(err, nil);
	for(;;){
		if(lk != nil)
			syslockrefresh(lk);
		m = newmessage(mb->root);
		m->mallocd = 1;
		m->inmbox = 1;
		if(readmessage(m, inb) < 0){
			delmessage(mb, m);
			mb->root->subname--;
			break;
		}

		// merge mailbox versions
		while(*l != nil){
			if(memcmp((*l)->digest, m->digest, SHA1dlen) == 0){
				// matches mail we already read, discard
				logmsg("duplicate", *l);
				delmessage(mb, m);
				mb->root->subname--;
				m = nil;
				l = &(*l)->next;
				break;
			} else {
				// old mail no longer in box, mark deleted
				logmsg("disappeared", *l);
				if(doplumb)
					mailplumb(mb, *l, 1);
				(*l)->inmbox = 0;
				(*l)->deleted = 1;
				l = &(*l)->next;
			}
		}
		if(m == nil)
			continue;

		x = strchr(m->start, '\n');
		if(x == nil)
			m->header = m->end;
		else
			m->header = x + 1;
		m->mheader = m->mhend = m->header;
		parseunix(m);
		parse(m, 0, mb, 0);
		logmsg("new", m);

		/* chain in */
		*l = m;
		l = &m->next;
		if(doplumb)
			mailplumb(mb, m, 0);

	}
	logmsg("mbox read", nil);

	// whatever is left has been removed from the mbox, mark deleted
	while(*l != nil){
		if(doplumb)
			mailplumb(mb, *l, 1);
		(*l)->inmbox = 0;
		(*l)->deleted = 1;
		l = &(*l)->next;
	}

	close(fd);
	free(inb);
	return nil;
}

static void
_writembox(Mailbox *mb, Mlock *lk)
{
	Dir *d;
	Message *m;
	String *tmp;
	int mode, errs;
	Biobuf *b;

	tmp = s_copy(mb->path);
	s_append(tmp, ".tmp");

	/*
	 * preserve old files permissions, if possible
	 */
	d = dirstat(mb->path);
	if(d != nil){
		mode = d->mode&0777;
		free(d);
	} else
		mode = MBOXMODE;

	sysremove(s_to_c(tmp));
	b = sysopen(s_to_c(tmp), "alc", mode);
	if(b == 0){
		fprint(2, "can't write temporary mailbox %s: %r\n", s_to_c(tmp));
		return;
	}

	logmsg("writing new mbox", nil);
	errs = 0;
	for(m = mb->root->part; m != nil; m = m->next){
		if(lk != nil)
			syslockrefresh(lk);
		if(m->deleted)
			continue;
		logmsg("writing", m);
		if(Bwrite(b, m->start, m->end - m->start) < 0)
			errs = 1;
		if(Bwrite(b, "\n", 1) < 0)
			errs = 1;
	}
	logmsg("wrote new mbox", nil);

	if(sysclose(b) < 0)
		errs = 1;

	if(errs){
		fprint(2, "error writing temporary mail file\n");
		s_free(tmp);
		return;
	}

	sysremove(mb->path);
	if(sysrename(s_to_c(tmp), mb->path) < 0)
		fprint(2, "%s: can't rename %s to %s: %r\n", argv0,
			s_to_c(tmp), mb->path);
	s_free(tmp);
	if(mb->d != nil)
		free(mb->d);
	mb->d = dirstat(mb->path);
}

char*
plan9syncmbox(Mailbox *mb, int doplumb)
{
	Mlock *lk;
	char *rv;

	lk = nil;
	if(mb->dolock){
		lk = syslock(mb->path);
		if(lk == nil)
			return "can't lock mailbox";
	}

	rv = _readmbox(mb, doplumb, lk);		/* interpolate */
	if(purgedeleted(mb) > 0)
		_writembox(mb, lk);

	if(lk != nil)
		sysunlock(lk);

	return rv;
}

//
//  look to see if we can open this mail box
//
char*
plan9mbox(Mailbox *mb, char *path)
{
	static char err[Errlen];
	String *tmp;

	if(access(path, AEXIST) < 0){
		errstr(err, sizeof(err));
		tmp = s_copy(path);
		s_append(tmp, ".tmp");
		if(access(s_to_c(tmp), AEXIST) < 0){
			s_free(tmp);
			return err;
		}
		s_free(tmp);
	}

	mb->sync = plan9syncmbox;
	return nil;
}
