/*
copyright 2002 Alexander Malmberg <alexander@malmberg.org>
*/

#include <Foundation/NSObject.h>
#include <Foundation/NSDate.h>
#include <Foundation/NSCalendarDate.h>
#include <Foundation/NSTimeZone.h>

#include "FolderThreader.h"

#include <Pantomime/CWMIMEUtility.h>


static NSDate *parse_date(const char *date_header)
{
	char *buf;
	char *b,*c;
	const char *d;
	NSDate *date;
	NSTimeZone *tz;

	int day,month,year,hour,minute,second,ofs;

	buf=malloc(strlen(date_header)+1);
	if (!buf) return [[NSDate alloc] initWithTimeIntervalSince1970: 0];

	{
		int comma=0,paren=0;
		for (b=buf,d=date_header;*d;d++)
		{ /* TODO: escaped '(' and ')'? */
			if (*d=='(')
				paren++;
			else if (*d==')')
			{
				if (paren) paren--;
			}
			else if (paren)
				continue;
			else if (*d==',' && !comma)
			{
				comma=1;
				b=buf;
			}
			else
				*b++=*d;
		}
	}
	*b=0;

	b=buf;

	while (*b && !isdigit(*b)) b++;
	if (!*b) goto error;
	day=strtol(b,&b,10);

	while (*b && !isalpha(*b)) b++;
	if (!*b) goto error;
	for (c=b;isalpha(*c);c++) ;
	*c=0;
	     if (!strcasecmp(b,"jan")) month=1;
	else if (!strcasecmp(b,"feb")) month=2;
	else if (!strcasecmp(b,"mar")) month=3;
	else if (!strcasecmp(b,"apr")) month=4;
	else if (!strcasecmp(b,"may")) month=5;
	else if (!strcasecmp(b,"jun")) month=6;
	else if (!strcasecmp(b,"jul")) month=7;
	else if (!strcasecmp(b,"aug")) month=8;
	else if (!strcasecmp(b,"sep")) month=9;
	else if (!strcasecmp(b,"oct")) month=10;
	else if (!strcasecmp(b,"nov")) month=11;
	else if (!strcasecmp(b,"dec")) month=12;
	else month=1;
	*c=' '; /* there had to be whitespace there before */

	while (!isdigit(*b)) b++;
	if (!*b) goto error;
	year=strtol(b,&c,10);
	if (c-b==2)
	{
		if (year<50)
			year+=2000;
		else
			year+=1900;
	}
	else if (c-b==3)
		year+=1900;
	b=c;

	while (*b && !isdigit(*b)) b++;
	if (!*b) goto error;
	hour=strtol(b,&b,10);

	while (*b && !isdigit(*b)) b++;
	if (!*b) goto error;
	minute=strtol(b,&b,10);

	if (*b==':')
	{
		while (*b && !isdigit(*b)) b++;
		if (!*b) goto error;
		second=strtol(b,&b,10);
	}
	else
		second=0;

	while (isspace(*b)) b++;
	c=b;
	while (*c && !isspace(*c)) c++;
	*c=0;

	if ((b[0]=='+' || b[0]=='-'))
	{
		ofs=strtol(&b[1],&c,10);
		ofs=ofs%100+(ofs/100)*60;
		if (b[0]=='-')
			ofs=-ofs;
	}
	else
		ofs=0; /* TODO: handle others? */

	if (month<1 || month>12 ||
	    day<1 || day>31 ||
	    hour<0 || hour>23 ||
	    minute<0 || minute>59 ||
	    second<0 || second>65) /* margin for leap seconds, probably too much */
		goto error;

	free(buf);

	tz=[NSTimeZone timeZoneForSecondsFromGMT: ofs*60];
	date=[[NSCalendarDate alloc] initWithYear: year month: month day: day
		hour: hour minute: minute second: second
		timeZone: tz];

	return date;

error:
	free(buf);
	/* let NSDate have a go at it, maybe it'll extract something useful */
	return RETAIN([NSDate dateWithNaturalLanguageString: [NSString stringWithCString: c]]);
}



#define CACHE_SIZE  100  /* can't go higher than 255; entry 0 isn't used */
static int cache_hits,cache_misses;

typedef struct msg_header_cache_s
{
	NSString *subject,*from;
	NSDate *date;
	int row;
	unsigned short next,prev;
	/* Entries are in a linked list. Used entries are moved to the front,
	entries at the end are replaced as necessary. Entry #0 is the head
	of the list (and is included in the list). */
} header_cache_t;


@implementation FolderThreader


-(void) _sanity
{
	int i,j;

	for (i=0;i<num_msgs;i++)
	{
		j=msgs[i].cached;
		if (!j) continue;
		if (header_cache[j].row!=i)
		{
			fprintf(stderr,"header cache messed up, msg->cache map!\n");
			*((int *)-1)=0;
		}
	}

	for (i=0;i<CACHE_SIZE;i++)
	{
		j=header_cache[i].row;
		if (j==-1) continue;
		if (msgs[j].cached!=i)
		{
			fprintf(stderr,"header cache messed up, cache->msg map!\n");
			*((int *)-1)=0;
		}
	}

	for (i=header_cache[0].next,j=0;i;i=header_cache[i].next) j++;
	if (j!=CACHE_SIZE-1)
	{
		fprintf(stderr,"header cache messed up, linked list next!\n");
		*((int *)-1)=0;
	}

	for (i=header_cache[0].prev,j=0;i;i=header_cache[i].prev) j++;
	if (j!=CACHE_SIZE-1)
	{
		fprintf(stderr,"header cache messed up, linked list prev!\n");
		*((int *)-1)=0;
	}
}


-(void) _moveFirst: (int)e
{
	if (!e) abort();
	if (e==header_cache[0].next) return;

	/* remove from old place */
	header_cache[header_cache[e].prev].next=header_cache[e].next;
	header_cache[header_cache[e].next].prev=header_cache[e].prev;

	/* update entry */
	header_cache[e].next=header_cache[0].next;
	header_cache[e].prev=0;

	/* insert in new */
	header_cache[header_cache[0].next].prev=e;
	header_cache[0].next=e;
}

-(void) _moveLast: (int)e
{
	if (!e) abort();
	if (e==header_cache[0].prev) return;

	/* remove from old place */
	header_cache[header_cache[e].prev].next=header_cache[e].next;
	header_cache[header_cache[e].next].prev=header_cache[e].prev;

	/* update entry */
	header_cache[e].prev=header_cache[0].prev;
	header_cache[e].next=0;

	/* insert in new */
	header_cache[header_cache[0].prev].next=e;
	header_cache[0].prev=e;
}

-(void) _clearCacheEntry: (MidNotification *)n
{
	header_cache_t *h;
	int r=[self indexOf: [n mid]];
	if (r==-1) return;
	if (!msgs[r].cached) return;

	h=&header_cache[msgs[r].cached];
	msgs[r].cached=0;
	DESTROY(h->subject);
	DESTROY(h->from);
	DESTROY(h->date);
	h->row=-1;

	[self _moveLast: h-header_cache];
}


- initWithMsgDB: (MsgDB *)m  target: (id<FolderThreaderTarget>)target
{
	if (!(self=[super init])) return nil;

	ASSIGN(mdb,m);
	t=target;

	msgs=NULL;
	num_msgs=0;

	header_cache=malloc(sizeof(header_cache_t)*CACHE_SIZE);
	if (!header_cache)
	{
		RELEASE(self);
		return nil;
	}
	memset(header_cache,0,sizeof(header_cache_t)*CACHE_SIZE);
	{
		int i;
		for (i=0;i<CACHE_SIZE;i++)
		{
			header_cache[i].next=i+1;
			header_cache[i].prev=i-1;
			header_cache[i].row=-1;
		}
		i--;
		header_cache[i].next=0;
		header_cache[0].prev=i;
	}

	[[NSNotificationCenter defaultCenter]
		addObserver: self
		selector: @selector(_clearCacheEntry:)
		name: MsgDB_MsgMetaChangeNotification
		object: mdb];

	return self;
}

-(void) dealloc
{
	[[NSNotificationCenter defaultCenter]
		removeObserver: self];

	DESTROY(mdb);
	t=nil;
	if (msgs)
		free(msgs);
	if (header_cache)
	{
		int i;
		header_cache_t *h;
		for (i=0,h=header_cache;i<CACHE_SIZE;i++,h++)
		{
			DESTROY(h->subject);
			DESTROY(h->from);
			DESTROY(h->date);
		}
		free(header_cache);
		header_cache=NULL;
	}
	msgs=NULL;
	num_msgs=0;
	[super dealloc];
//	printf("  hits=%8i\nmisses=%8i\n",cache_hits,cache_misses);
}


-(int) indexOf: (msg_id_t)mid
{ /* TODO: this is probably a bottleneck */
	int i;
	msg_info_t *m;
	for (i=0,m=msgs;i<num_msgs;i++,m++)
		if (m->mid==mid) return i;
	return -1;
}


/* TODO: think hard about how to handle cross posts and references to
articles from other groups. */
-(int) addMsg: (msg_id_t)mid
{
/* Keep track of adds in progress so circular references can be detected. */
static msg_id_t *adding_stack;
static int adding_stack_num,adding_stack_size;

	msg_info_t *m;
	char *c,*d;
	const char *mh;
	char *buf;
	int after;
	msg_id_t amid;

	{
		int i;
		for (i=0;i<adding_stack_num;i++)
			if (adding_stack[i]==mid)
				return -1;
	}

	if (adding_stack_num==adding_stack_size)
	{
		adding_stack_size+=4;
		adding_stack=realloc(adding_stack,sizeof(msg_id_t)*adding_stack_size);
		if (!adding_stack)
			abort();
	}
	adding_stack[adding_stack_num++]=mid;

	if ((after=[self indexOf: mid])!=-1)
	{
		adding_stack_num--;
		return after;
	}

	mh=[mdb msg_getHeader: "References" : mid];
	if (mh && strchr(mh,'<') /* really messed up headers */ )
	{
		buf=strdup(mh);
		if (!buf) abort();
		after=-1;
		for (;(c=strrchr(buf,'<')) || *buf;*c=0)
		{
			d=strchr(c,'>');
			if (!d) break; /* broken headers */
			d[1]=0;

			amid=[mdb midForId: c];

			if (amid==0)
			{
				amid=[mdb createMessageWithId: c  source: nil];
				if (amid!=0)
				{
					*c=0;
					if (strchr(buf,'<'))
						[mdb msg_setMetaHeader: "Real-References" value: buf : amid];
				}
			}

			if (amid!=0)
			{
				after=[self indexOf: amid];
				if (after==-1)
				{
					[self addMsg: amid];
					after=[self indexOf: amid];
				}

				if (after!=-1)
					break;
			}
		}
		free(buf);
	}
	else
		after=-1;


	msgs=realloc(msgs,sizeof(msg_info_t)*(num_msgs+1));
	if (!msgs) abort();

	{
		int i,l;
		if (after==-1)
		{
			after=num_msgs-1;
			l=0;
		}
		else
		{
			l=msgs[after].level+1;
			while (after<num_msgs-1 && msgs[after+1].level>=l) after++;
		}

		for (i=num_msgs;i>after+1;i--)
			msgs[i]=msgs[i-1];
		m=&msgs[after+1];
		m->level=l;
		m->cached=0;

		/* now adjust the rows in the header cache entries */
		for (i=0;i<CACHE_SIZE;i++)
		{
			if (header_cache[i].row>after)
				header_cache[i].row++;
		}
	}

	{
		const char *c=[mdb msg_getMetaHeader: "RStatus" : mid];
		if (c)
			m->rstatus=atoi(c);
		else
			m->rstatus=0;
	}
	m->mid=mid;
	num_msgs++;

	adding_stack_num--;
	return after+1;
}

/*-(void) addMsg: (msg_id_t)mid
{
	msg_info_t *m;

	if ([self indexOf: mid]!=-1)
		return;

	msgs=realloc(msgs,sizeof(msg_info_t)*(num_msgs+1));
	if (!msgs) abort();

	m=&msgs[num_msgs];
	{
		const char *c=[mdb msg_getMetaHeader: "RStatus" : mid];
		if (c)
			m->rstatus=atoi(c);
		else
			m->rstatus=0;
	}
	m->mid=mid;

	num_msgs++;

	[list reloadData];
}*/

-(BOOL) selectFirstUnreadFrom: (int)i
{
	for (;i<num_msgs;i++)
		if (msgs[i].rstatus!=1)
			break;
	if (i<num_msgs)
	{
		[t selectMessage: i];
		return YES;
	}
	return NO;
}

-(int) findNextUnreadFrom: (int)i
{
	for (;i<num_msgs;i++)
		if (msgs[i].rstatus!=1)
			return i;
	return -1;
}

-(int) findNextMax: (int)max  from: (int)i
{
	for (;i<num_msgs;i++)
		if (msgs[i].level<=max)
			break;
	return i;
}

-(int) findPrevMax: (int)max  from: (int)i
{
	for (;i>0;i--)
		if (msgs[i].level<=max)
			break;
	return i;
}

-(void) markAsRead: (int)f : (int)to;
{
	if (f<0) f=0;
	for (;f<=to;f++)
	{
		if (msgs[f].rstatus!=1)
			[mdb msg_setMetaHeader: "RStatus"  value: "1" : msgs[f].mid];
	}
}


-(NSString *) header: (const char *)h  forMessage: (int)msg
{
	const char *c;
	NSData *d;
	NSString *s2;

	c=[mdb msg_getHeader: h : msgs[msg].mid];
	if (!c)
		return @"";

	d=[[NSData alloc] initWithBytes: c length: strlen(c)];
	s2=[CWMIMEUtility decodeHeader: d  charset: nil];
	[d release];
	return s2;
}



-(void) _fixCache: (int)msg
{
	header_cache_t *h;
	int index;

	if (msgs[msg].cached) { cache_hits++; return; }
	cache_misses++;

	index=header_cache[0].prev;
	h=&header_cache[index];

	if (h->row!=-1)
	{
		DESTROY(h->subject);
		DESTROY(h->from);
		DESTROY(h->date);
		msgs[h->row].cached=0;
	}

	h->row=msg;
	msgs[msg].cached=index;

	[self _moveFirst: index];

	{
		const char *c;
		NSString *s2;
		NSData *d;
	
		c=[mdb msg_getHeader: "Subject" : msgs[msg].mid];
		if (!c)
			h->subject=[[NSString alloc] initWithCString: [mdb msg_getMessageID: msgs[msg].mid]];
		else
		{
			d=[[NSData alloc] initWithBytes: c length: strlen(c)];
			s2=[CWMIMEUtility decodeHeader: d  charset: nil];
			[d release];
			h->subject=[s2 retain];
		}

		c=[mdb msg_getHeader: "From" : msgs[msg].mid];
		if (c)
		{
			d=[[NSData alloc] initWithBytes: c length: strlen(c)];
			s2=[CWMIMEUtility decodeHeader: d  charset: nil];
			[d release];
			h->from=[s2 retain];
		}
		else
			h->from=@"";

		c=[mdb msg_getHeader: "Date" : msgs[msg].mid];
		if (c)
			h->date=parse_date(c);
		else
			h->date=nil;
	}

	[self _sanity];
}

-(NSString *) subjectForMessage: (int)msg
{
//	if (!msgs[msg].cached)
		[self _fixCache: msg];
	return AUTORELEASE(RETAIN(header_cache[msgs[msg].cached].subject));
}

-(NSString *) fromForMessage: (int)msg
{
//	if (!msgs[msg].cached)
		[self _fixCache: msg];
	return AUTORELEASE(RETAIN(header_cache[msgs[msg].cached].from));
}

-(NSDate *) dateForMessage: (int)msg
{
//	if (!msgs[msg].cached)
		[self _fixCache: msg];
	return AUTORELEASE(RETAIN(header_cache[msgs[msg].cached].date));
}

@end

