#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <errno.h>
#include "lib.h"
#define DMID 14
#include "debug.h"
#include "lprintf.h"
#include "dstr.h"

#ifndef FALSE
#define FALSE 0
#endif
#ifndef TRUE
#define TRUE (!FALSE)
#endif


#include "ical.h"
#define ICALSZ 9000
int dl_add(Datelist *dl, time_t t);
int ical_ruletime(Datelist *dl, Ical *ic, char *var, time_t start, time_t end);
int dl_subtract(Datelist *dl, Datelist *xl);
int dl_compress(Datelist *dl);
int dl_sort(Datelist *dl) ;

int  imsg(const char * arg_list, ...);
int  emsg(const char * arg_list, ...);

// function to handle 'ical' files....

/*

 RDATE;VALUE=DATE:19970304,19970504,19970704,19970904

syntax of each line:
	  field[;parameter=string;parameter2=value]:text

    BEGIN:VCALENDAR
       VERSION:2.0
       PRODID:-//hacksw/handcal//NONSGML v1.0//EN
       BEGIN:VEVENT
       UID:19970610T172345Z-AF23B2@example.com
       DTSTAMP:19970610T172345Z
       DTSTART:19970714T170000Z
       DTEND:19970715T040000Z
       SUMMARY:Bastille Day Party
       END:VEVENT
    END:VCALENDAR


So,  we have
	fields
	Objects
		fields
		Objects...

Fields = 
	field,parameters,value


When contains:
	DTSTART
	DTEND
    RRULE
	RDATE
	EXDATE

typedef struct ICAL {
	Pair *
} Ical;
int ical_load(char *fname, char *reason)
{
	FILE *f;
	f = fopen(fname,"rb");
	if (f==NULL) {lprintf(reason,BFSZ-1,"Open failed %s %s",fname,strerror(errno)); return FALSE;}
	for (;!feof(f);) {
		
	}
	fclose(f);
	return TRUE;
}


typedef struct ICAL {
	char *name; // vevent, vtodo, vjournal, vfreebusy, valarm
	Ipair *fields;
	Ical *sub; // object within this object (events within calender)
	Ical *next; // List of objects, if this is a 'vevent'  then 
} Ical;

*/


int ical_readline(FILE *f, char *var, char *val, char *attrib, char *nextline, int valsz)
{
	char *s;
	char *c;
	char bf[BFSZ];
	int sanity_check = 0;
again:
	if (feof(f))return FALSE;
	bf[0] = 0;
	if (lib_strlen(nextline)>0) {
		ncpy(bf,nextline,BFSZ-1);
		strcpy(nextline,"");
	}
	
	for (;TRUE;) {
		if (fgets(nextline,BFSZ-1,f)==NULL) break;
		lib_strip_crlf(nextline);
		if(++sanity_check > 10000){
			// Just for security make sure we can't loop forever
			break;	
		}
		if (*nextline==' ' || *nextline=='\t') { 
			// unfold up to 1KB of cal content
			ncat(bf,nextline+1,BFSZ-1);
		} else break;
	}
	strcpy(attrib,"");

	c = strchr(bf,':');
	if (!c) goto again;
	*c = 0;
	s = strchr(bf,';');
	if (s && (s<c)) {
		*s = 0;
		ncpy(var,bf,BFSZ-1);
		ncpy(attrib,s+1,BFSZ-1);		
	} else {
		ncpy(var,bf,BFSZ-1);
	}
	ncpy(val,c+1,valsz-1);

	return TRUE;
}
Ical *ical_read_real(char *fname, char *file, int line)
{
	// Read file into ical structure...
	FILE *f;
	Pair *end=NULL;
	Ical *ic=NULL;
	char var[BFSZ];
	char val[ICALSZ];
	char nextline[BFSZ];
	char attrib[BFSZ];
	char **laststr=NULL;
	f = fopen(fname,"rb");
	if (f==NULL) return NULL;
	strcpy(nextline,"");
	for (;!feof(f);) {
		if (!ical_readline(f,var,val,attrib,nextline,ICALSZ)) break;
		if (strcmpnc(var,"begin")==0) {
			ic = ical_read_object(f,val,nextline,file,line);
			if (ic!=NULL) break; // chrisp addition, only read one object as only one is returned, otherwise memory leaks.
		}
	}
	fclose(f);
	return ic;
}
Ical *ical_read2_real(FILE *f, char *file, int line)
{
	Pair *end=NULL;
	Ical *ic=NULL;
	char var[BFSZ];
	char val[ICALSZ];
	char nextline[BFSZ];
	char attrib[BFSZ];
	char **laststr=NULL;
	if (f==NULL) return NULL;
	strcpy(nextline,"");
	for (;!feof(f);) {
		if (!ical_readline(f,var,val,attrib,nextline,ICALSZ)) break;
		if (strcmpnc(var,"begin")==0) {
			ic = ical_read_object(f,val,nextline,file,line);
			if (ic!=NULL) break; // otherwise it would leak memory.
		}
	}
	fclose(f);
	return ic;
}

void ical_add_sub(Ical *ic, Ical *sub)
{
	Ical **at = &ic->sub;
	for (ic = ic->sub;ic!=NULL; ic=ic->next ) {
		at = &ic->next;
	}
	*at = sub;
}
int ipair_add_toend(Ipair **pp, Ipair **next, char *var, char *val, char *attrib)
{
	char bf[BFSZ];
	Ipair *p=*pp;
	// moved to stop collision in tracking. 
	p = malloc(sizeof(Pair));
	p->next = (*next) ? (*next)->next : NULL;
	p->var = strdup(var);
	lib_strip_text(val,"&#13;",bf, BFSZ);

	p->val = strdup(bf);
	p->attrib = strdup(attrib);
	if (*pp==NULL) {
		*pp = p;
		*next = p;
	} else {
		(*next)->next = p;
		*next = p;
	}
	return TRUE;
}
int ipair_set_value(Ipair **pp, char *var, char *val, char*attrib)
{	
	Ipair *p = *pp, *replace=NULL, *end=*pp;
	if (var==NULL) return FALSE;
	if (val==NULL) return FALSE;
	if (attrib==NULL) attrib = "";


	for (;p!=NULL;p=p->next) {
		if (strcmp(var,p->var)==0) replace=p;
		end=p;
	}
	if(replace){
		p=replace;
		if(strcmp(p->val,val)==0 && strcmp(p->attrib,attrib)==0)
			return FALSE;

		free(p->val); 
		p->val = strdup(val);
		free(p->attrib);
		p->attrib = strdup(attrib);
		return TRUE;
	}
	ipair_add_toend(pp,&end,var,val,attrib);
	return TRUE;
}
int ipair_addmatch_value(Ipair **pp, char *var, char *val, char*attrib)
{	
	Ipair *p = *pp, *insert=NULL, *end=*pp;
	for (;p!=NULL;p=p->next) {
		if (strcmp(var,p->var)==0) insert=p;
		end=p;
	}
	if(insert){
		ipair_add_toend(pp,&insert,var,val,attrib);
		return TRUE;
	}
	ipair_add_toend(pp,&end,var,val,attrib);
	return TRUE;
}

int ipair_delete_any(Ipair **pp, char *var)
{
	Ipair *p;
	Ipair *pnext, **pprev=NULL;
	if (pp==NULL) return TRUE;
	p = *pp;
	for (;p!=NULL;p=pnext) {
		pnext = p->next;
		if(strcmp(p->var,var)!=0) {
			pprev=&p->next;
			continue;
		}
		if(pprev==NULL){
			*pp = p->next;
		}else{
			*pprev=p->next;
		}
		free(p->var);
		free(p->val);
		free(p->attrib);
		free(p);
	}
	return TRUE;
}

int ipair_delete_specific(Ipair **pp, char *var, char *val)
{
	Ipair *p;
	Ipair *pnext, **pprev=NULL;
	if (pp==NULL) return TRUE;
	p = *pp;
	for (;p!=NULL;p=pnext) {
		pnext = p->next;
		if(strcmp(p->var,var)!=0 || strcmp(p->val,val)!=0) {
			pprev=&p->next;
			continue;
		}
		if(pprev==NULL){
			*pp = p->next;
		}else{
			*pprev=p->next;
		}
		free(p->var);
		free(p->val);
		free(p->attrib);
		free(p);
	}
	return TRUE;
}


Ical *ical_read_object(FILE *f, char *objname, char*nextline, char *file, int line)
{
	// Read file into ical structure...
	char var[BFSZ];
	char val[ICALSZ];
	char attrib[BFSZ];
//	char nextline[BFSZ];
	char bf[BFSZ];
	Ipair *end=NULL;
	Ical *ic= ical_newx(file,line); //d_callocid(DMID,1,sizeof(Ical),file,line);
	ic->name = strdup(objname);
//	strcpy(nextline,"");
	for (;!feof(f);) {
		if (!ical_readline(f,var,val,attrib,nextline,ICALSZ)) 
			break;
		if (strcmpnc(var,"end")==0) 
			break;
		if (strcmpnc(var,"begin")==0) {
			Ical *sub=ical_read_object(f,var,nextline,file,line); // read until matching 'end' 
			if (!sub) continue;
			sub->name_extra = strdup(val);
			ical_add_sub(ic,sub);
			continue;
		}
		lib_strip_text(val,"&#13;",bf, BFSZ);

		ipair_add_toend(&ic->fields,&end,var,bf,attrib);
	}
	return ic;
}
int ipair_free(Ipair **pp)
{
	Ipair *p;
	Ipair *pnext;
	if (pp==NULL) return TRUE;
	p=*pp;
	for (;p!=NULL;p=pnext) {
		pnext = p->next;
		free(p->var);
		free(p->val);
		free(p->attrib);
		free(p);
	}
	*pp = NULL;
	return TRUE;
}

int ical_free_one(Ical *x, char *source, int line, int depth)
{
	int nfreed = 0;
	Ical *ic,*next;
	if (x==NULL) return TRUE;
	// First free the sub cal entries...
	imsg("ical_free: Freeing list of sub structures\n");
	for (ic = x->sub;ic!=NULL; ic=next ) {
		next = ic->next;
		ical_free_one(ic,source,line,depth+1); 
		nfreed++;
	}
	imsg("ical_free: Freed %d sub structures\n",nfreed);
	x->sub = NULL;

	// Then free this one.
	ipair_free(&x->fields);
	free(x->name);
	ffree(x->name_extra);
	ffree(x->cal_id);
	ffree(x->rid);
	x->fields = NULL;
	x->name = NULL;
	free(x); // free the structure itself, chrisp added!!!  
	return TRUE;
}
int ical_free(Ical *x, char *source, int line, int depth)
{
	Ical *ic,*next;
	int nfreed=0;
	if (x==NULL) return TRUE;
	// For each one in list free it. 
	imsg("ical_free: Freeing ical from %s:%d depth %d\n",source,line,depth);
	for (ic = x;ic!=NULL; ic=next ) {
		next = ic->next;
		ical_free_one(ic->sub,source,line,depth+1); 
		nfreed++;
	}
	imsg("ical_free: Freed %d structures\n",nfreed);
	return TRUE;
}



int ical_free_old(Ical *x, char *source, int line, int depth)
{
	Ical *ic;
	if (x==NULL) return TRUE;
	for (ic = x;ic!=NULL; ic=ic->next ) {
		ical_free(ic->sub,source,line,depth+1); ic->sub = NULL;

	}
	ipair_free(&x->fields);
	free(x->name);
	ffree(x->name_extra);
	ffree(x->cal_id);
	ffree(x->rid);
	x->fields = NULL;
	x->name = NULL;
	if (x->next!=NULL) {
		emsg("ical_free: Pointer to 'next' structure is not NULL %s:%d depth %d\n",source,line,depth);
	}
	free(x); // free the structure itself, chrisp added!!!  
	return TRUE;
}
int ical_ffree_real(Ical **x, char *source, int line)
{
	if(*x==NULL) return TRUE;
	ical_free(*x, source,line,0);
	*x=NULL;
	return TRUE;
}

/*
	  recur-rule-part = ( "FREQ" "=" freq )
                       / ( "UNTIL" "=" enddate )
                       / ( "COUNT" "=" 1*DIGIT )
                       / ( "INTERVAL" "=" 1*DIGIT )
                       / ( "BYSECOND" "=" byseclist )
                       / ( "BYMINUTE" "=" byminlist )
                       / ( "BYHOUR" "=" byhrlist )
                       / ( "BYDAY" "=" bywdaylist )
                       / ( "BYMONTHDAY" "=" bymodaylist )
                       / ( "BYYEARDAY" "=" byyrdaylist )
                       / ( "BYWEEKNO" "=" bywknolist )
                       / ( "BYMONTH" "=" bymolist )
                       / ( "BYSETPOS" "=" bysplist )
                       / ( "WKST" "=" weekday )

     freq        = "SECONDLY" / "MINUTELY" / "HOURLY" / "DAILY"
                   / "WEEKLY" / "MONTHLY" / "YEARLY"

       enddate     = date / date-time

       byseclist   = ( seconds *("," seconds) )

       seconds     = 1*2DIGIT       ;0 to 60

       byminlist   = ( minutes *("," minutes) )

       minutes     = 1*2DIGIT       ;0 to 59

       byhrlist    = ( hour *("," hour) )

       hour        = 1*2DIGIT       ;0 to 23

       bywdaylist  = ( weekdaynum *("," weekdaynum) )

       weekdaynum  = [[plus / minus] ordwk] weekday

       plus        = "+"

       minus       = "-"

       ordwk       = 1*2DIGIT       ;1 to 53

       weekday     = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
       ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
       ;FRIDAY, and SATURDAY days of the week.


       bymodaylist = ( monthdaynum *("," monthdaynum) )

       monthdaynum = [plus / minus] ordmoday

       ordmoday    = 1*2DIGIT       ;1 to 31

       byyrdaylist = ( yeardaynum *("," yeardaynum) )

       yeardaynum  = [plus / minus] ordyrday

       ordyrday    = 1*3DIGIT      ;1 to 366

       bywknolist  = ( weeknum *("," weeknum) )

       weeknum     = [plus / minus] ordwk

       bymolist    = ( monthnum *("," monthnum) )

       monthnum    = 1*2DIGIT       ;1 to 12

       bysplist    = ( setposday *("," setposday) )

       setposday   = yeardaynum

Examples: http://www.kanzaki.com/docs/ical/rrule.html


Every 20 minutes from 9:00 AM to 4:40 PM every day:

  DTSTART;TZID=US-Eastern:19970902T090000
  RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40
  or
  RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16


*/
char *ical_find_value(Ical *ic, char *var)
{
	Ipair *p = ic->fields;
	for (;p!=NULL;p=p->next) {
		if (strcmp(var,p->var)==0)
			return p->val;
	}
	return "";
}
int ical_find_both(Ical *ic, char *var, char **val, char **attrib)
{
	Ipair *p = ic->fields;
	*val = *attrib = "";
	for (;p!=NULL;p=p->next) {
		if (strcmp(var,p->var)==0) {
			*val = p->val;
			*attrib = p->attrib;
			return TRUE;
		}
	}
	return FALSE;
}
char* ical_mfind_attrib(Ical *ic, char *var, char *val)
{
	Ipair *p = ic->fields;
	for (;p!=NULL;p=p->next) {
		if (strcmp(var,p->var)==0) {
			if (strcmp(val,p->val)==0) {
				return p->attrib;
			}
		}
	}
	return "";
}

int dl_clear(Datelist *dl)
{
	dl->nevents = 0;
	return TRUE;
}
int ical_during(Ical *ic, time_t range_dtstart, time_t range_dtend)
{
	// Figure out if this icalender event occurs during this time period...
	// Relevant fields I need....
	// RDATE (list of date/time/periods) on which the event takes place.
	// EXDATE (list of date/times) exceptions  
	// EXRULE (same as 'rrule' but used as exclusion list)
	// DTSTART;TZID=US-Eastern:19970902T090000
	// RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5
	Datelist dl,ex;
	time_t dtstart;

	dl_clear(&dl);
	dl_clear(&ex);
	ical_datetime(&dl,ic,"RDATE",range_dtstart,range_dtend);
	dtstart = ical_time(ical_find_value(ic,"DTSTART"),0);

	ical_ruletime(&dl,ic,"RRULE",range_dtstart,range_dtend);

	ical_ruletime(&ex,ic,"EXRULE",range_dtstart,range_dtend);

	dl_subtract(&dl,&ex);
	dl_clear(&ex);
	ical_datetime(&ex,ic,"EXDATE",range_dtstart,range_dtend);
	dl_subtract(&dl,&ex);

	// Now 'subtract' exdate and exrule events from rrule events...
	dl_sort(&dl);

	return TRUE;
}
// Ok I think each function will generate  linked list of 'event' dates within a specified range.


int ical_datetime(Datelist *dl, Ical *ic, char *var, time_t start, time_t dtend)
{
	// RDATE;VALUE=DATE:19970304,19970504,19970704,19970904
	// RDATE:19750223T020000
	// RDATE:19970714T123000Z
    // RDATE;TZID=America/New_York:19970714T083000
    // RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H
    // RDATE;VALUE=DATE:19970101,19970120,19970217,19970421,19970526,19970704,19970901,19971014,19971128,19971129,19971225
	char *s;
	char *pval,*attrib;
	char *val;
	char *upto;
	int gmtoffset=0;
	// Split up 'list' string and add to 'datelist' array.
	// first get the attrib an value parts.
	if (!ical_find_both(ic,var,&pval,&attrib)) return FALSE;
	// Now split up value, and translate to time using attributes...

	val = strdup(pval);
	s = mystrtok(val,", ",&upto);
	for (;s!=NULL;s=mystrtok(NULL,", ",&upto)) {
		time_t t;
		t = ical_time(s,gmtoffset);
		if ((t>=start && t<=dtend) || (t>=start && dtend==0) ) {
			if (!dl_add(dl,t)) break;	
		}
	}
	return TRUE;
}

// FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1

/*
recur-rule-part = ( "FREQ" "=" freq )
                       / ( "UNTIL" "=" enddate )
                       / ( "COUNT" "=" 1*DIGIT )
                       / ( "INTERVAL" "=" 1*DIGIT )
                       / ( "BYSECOND" "=" byseclist )
                       / ( "BYMINUTE" "=" byminlist )
                       / ( "BYHOUR" "=" byhrlist )
                       / ( "BYDAY" "=" bywdaylist )
                       / ( "BYMONTHDAY" "=" bymodaylist )
                       / ( "BYYEARDAY" "=" byyrdaylist )
                       / ( "BYWEEKNO" "=" bywknolist )
                       / ( "BYMONTH" "=" bymolist )
                       / ( "BYSETPOS" "=" bysplist )
                       / ( "WKST" "=" weekday )

	byminute, 60, 
	byhour, 3600
	by

*/
#define ASECOND 1
#define AMINUTE 60
#define ANHOUR (60*60)
#define ADAY (60*60*24)
#define AWEEK (60*60*24*7)
#define AMONTH (60*60*24*32)
#define AYEAR (60*60*24*365)

enum {R_SECONDLY=0,R_MINUTELY,R_HOURLY,R_DAILY,R_WEEKLY,R_MONTHLY,R_YEARLY};
typedef struct FTABLE {char *period; int seconds; int rule;} Ftable;
Ftable ftablex[]={
	"SECONDLY",1,R_SECONDLY
	,"MINUTELY",AMINUTE,R_MINUTELY
	,"HOURLY",ANHOUR,R_HOURLY
	,"DAILY",ADAY,R_DAILY
	,"WEEKLY",AWEEK,R_WEEKLY
	,"MONTHLY",AMONTH,R_MONTHLY
	,"YEARLY",AYEAR,R_YEARLY
	,NULL,0,0
};
enum {BY_SECOND=1,BY_MINUTE,BY_HOUR,BY_DAY,BY_MONTHDAY,BY_YEARDAY,BY_WEEKNO,BY_MONTH};

int ical_freq_find(char *f, int *rule)
{
	int i=0;
	for (;ftablex[i].period!=NULL;i++) {
		if (strcmpnc(ftablex[i].period,f)==0) {
			*rule = ftablex[i].rule;
			return ftablex[i].seconds;
		}
	}
	return 0;
}
int dl_add(Datelist *dl, time_t t)
{
	if (dl->nevents>=MAXREPEAT-2) return FALSE;
	dl->event[dl->nevents++] = t;
	return TRUE;
}
int bl_add(Bylist *dl, int t, int flags)
{
	if (dl->nevents>=MAXREPEAT-2) return FALSE;
	dl->prefix[dl->nevents] = flags;
	dl->event[dl->nevents++] = t;
	return TRUE;
}
int ical_generate(Datelist *dl, time_t dtstart,time_t min, time_t dtend, int period)
{
	// Generate a list of events within the specified range starting at the specified point...
	// If period is 'month' then do it using time/mktime 

	time_t t;
	struct tm *when;
	struct tm tbf;
	if (dtend==0) dtend = time(NULL) + 365*5; // 5 years time
	dl_clear(dl);
	if (period>=AMONTH) {
		for (t=dtstart;t<=dtend;) {
			if (!dl_add(dl,t)) break;
			when = safe_localtime(&t,&tbf); if (when==NULL) break;
			if (period==AMONTH) when->tm_mon++;
			if (period==AYEAR) when->tm_year++;
			t = mktime(when);
		}
	} else {
		for (t=dtstart;t<=dtend;t+=period) {
			if (!dl_add(dl,t)) break;
		}
	}
	return TRUE;
}
static char *days[]={"su","mo","tu","we","th","fr","sa",NULL};
int dl_find_day(char *s, int *prefix, int byday)
{
	int i;
	*prefix = 0;
	if (!byday) return atoi(s);
	if (*s=='-') {*prefix = atoi(s); s++;}
	if (*s=='+') {*prefix = atoi(s); s++;}
	if (sisdigit(*s)) {*prefix = atoi(s); }
	for (;sisdigit(*s);s++) ;
	for (i=0;i<7;i++) if (strncmpnc(s,days[i],2)==0) return i;
	return 0;
}
int ical_bylist(Bylist *dl, char *in, int byday)
{
	char *s;
	char *upto;
	int prefix;
	char bf[BFSZ];
	int d;
	dl->nevents=0;
	ncpy(bf,in,BFSZ-1);
	for (s=mystrtok(bf,", ",&upto);s!=NULL;s=mystrtok(NULL,", ",&upto)) {
		d = dl_find_day(s,&prefix,byday);
		bl_add(dl,d,prefix);
	}
	return TRUE;
}
int dl_copy(Datelist *dst, Datelist *src)
{
	int i;
	dl_clear(dst);
	for (i=0; i<src->nevents;i++) {
		dl_add(dst,src->event[i]);
	}
	return TRUE;
}
int time_wday(time_t x)
{
	struct tm *when;
	struct tm tbf;
	time_t t=x;
	when = safe_localtime(&t,&tbf);
	if (when==NULL) return 0;
	return when->tm_wday;
}
int ical_expand(Datelist *dl, Bylist *by, int byint, int byperiod, int runits, int byyear )
{
	// Go through dl list, add each 'by' entry.
	Datelist out;
	int range;
	int i;
	int j;
	time_t t;
	struct tm *when;
	struct tm tbf;

	dl_clear(&out);

	// Rules we need to deal with still.	
		// ( "BYDAY" "=" bywdaylist )
		// ( "BYMONTHDAY" "=" bymodaylist )
		// ( "BYYEARDAY" "=" byyrdaylist )
		// ( "BYWEEKNO" "=" bywknolist )
		// ( "BYMONTH" "=" bymolist )
		// ( "BYSETPOS" "=" bysplist )

	if (byint==BY_MONTH) {
		for (i=0; i<dl->nevents;i++) {
			for (j=0; j<by->nevents;j++) {			
				when = safe_localtime(&dl->event[i],&tbf); if (when==NULL) break;
				when->tm_mon += by->event[j];
				t = mktime(when);				
				dl_add(&out,t);
			}
		}
		
	} if (byint==BY_DAY) {
		// cope with the various days of the week rules for +- etc....
		for (i=0; i<dl->nevents;i++) {
			time_t d;
			int week;
			int dc=0;
			if (byyear) range = days_in_year(dl->event[i]);
			else range = days_in_month(dl->event[i]);

			// Now step through the 'month or year' one day at a time, and add any event that counts... 
			for (d=dl->event[i],week=1;d<dl->event[i]+range;d+= ADAY,dc++) {
				if (dc==7) {dc = 0; week++;}
				for (j=0; j<by->nevents;j++) {
					if (by->prefix[j]==0) { // Every monday for this DAY
						if (by->event[j]==time_wday(d)) dl_add(&out,d); // every monday for this 'month' or 'year'... 
					} else {
						if (by->prefix[j]>0) {
							if (by->prefix[j] == week) 	
								if (by->event[j]==time_wday(d)) dl_add(&out,d); // every monday for this 'month' or 'year'... 
						}
					}
				}
			}
			for (d=dl->event[i]+range,week=1;d>dl->event[i];d-= ADAY,dc++) {
				if (dc==7) {dc = 0; week++;}
				for (j=0; j<by->nevents;j++) {
						if (by->prefix[j]<0) {
							if (-by->prefix[j] == week) 	
								if (by->event[j]==time_wday(d)) dl_add(&out,d); // every monday for this 'month' or 'year'... 
						}
				}
			}
		}
	} else { // simple ones...   BYSECOND, BYMINUTE and BYHOUR

		// IF period is fixed (hour,day,week) then do it this way
		for (i=0; i<dl->nevents;i++) {
			for (j=0; j<by->nevents;j++) {
				dl_add(&out,(dl->event[i] + by->event[j]) * byperiod); 
			}
		}
	}
	dl_copy(dl,&out); // Copy onto original 'dl' 
	return TRUE;
}
int week_of_month(time_t now)
{
	struct tm *when;
	struct tm tbf;
	int day;
	when = safe_localtime(&now,&tbf);
	if (when==NULL) return 0;
	day = when->tm_wday;
	return day / 7 + 1;
}
int week_of_month_reverse(time_t now)
{
	struct tm *when;
	struct tm tbf;
	int day;
	when = safe_localtime(&now,&tbf);
	if (when==NULL) return 0;
	day = days_in_month(now) - when->tm_wday;
	return day / 7 + 1;
}
int week_of_year(time_t now)
{
	struct tm *when;
	struct tm tbf;
	int day;
	when = safe_localtime(&now,&tbf);
	if (when==NULL) return 0;
	day = when->tm_yday;
	return day / 7 + 1;
}
int week_of_year_reverse(time_t now)
{
	struct tm *when;
	struct tm tbf;
	int day;
	when = safe_localtime(&now,&tbf);
	if (when==NULL) return 0;
	day = days_in_year(now) - when->tm_yday;
	return day / 7 + 1;
}
int dl_oneof(Bylist *by, int n, int runits, int now)
{
	int i;
	int e;
	for (i=0;i<by->nevents;i++) {
		e = by->event[i];
		if (e==n) {
			if (by->prefix[i]==0) return TRUE;
			// Else checking for the 'nth' week of the month or year
			if (runits==R_MONTHLY)  {
				if (by->prefix[i]>0) {
					if (week_of_month(now)==by->prefix[i]) return TRUE;
				} else {
					if (week_of_month_reverse(now)==by->prefix[i]) return TRUE;
				}
			}
			if (runits==R_YEARLY)  {
				// Figure out the 'week of the year' 
				if (by->prefix[i]>0) {
					if (week_of_year(now)==by->prefix[i])  return TRUE;
				} else {
					if (week_of_year_reverse(now)==by->prefix[i])  return TRUE;
				}
			}
		}
	}
	return FALSE;
}

int days_in_month(time_t t)
{
	int days_in_monthx[]={31,28,31,30,31,30,31,31,30,31,30,31};
	struct tm *when;
	struct tm tbf;
	int year;
	when = safe_localtime(&t,&tbf);
	if (when==NULL) return 31;

	year = when->tm_year + 1900;
	if(year% 4 == 0 && year%100 != 0 || year%400 == 0) days_in_monthx[1] = 29;
	else days_in_monthx[1] = 28;
	return days_in_monthx[when->tm_mon];
}
int days_in_year(time_t t)
{
	int days_in_monthx[]={31,28,31,30,31,30,31,31,30,31,30,31};
	struct tm *when;
	struct tm tbf;
	int year;
	when = safe_localtime(&t,&tbf);
	if (when==NULL) return 31;

	year = when->tm_year + 1900;
	if(year% 4 == 0 && year%100 != 0 || year%400 == 0) return 366;
	else return 365;
}
int weeks_in_year(time_t t)
{
	return 52; 
}

int dl_total(time_t t, int units)
{
		if (units==BY_SECOND) return 60;
		if (units==BY_MINUTE) return 60;
		if (units==BY_HOUR) return 24;
		if (units==BY_DAY) return 7;
		if (units==BY_MONTHDAY) return days_in_month(t);
		if (units==BY_YEARDAY) return days_in_year(t);
		if (units==BY_WEEKNO) return weeks_in_year(t);
		if (units==BY_MONTH) return 12;
		return 0;
}
int dl_time_part(struct tm *when, int bytime)
{
	if (bytime==BY_SECOND) return when->tm_sec;
	if (bytime==BY_MINUTE) return when->tm_min;
	if (bytime==BY_HOUR) return when->tm_hour;
	if (bytime==BY_DAY) return when->tm_wday;
	if (bytime==BY_MONTHDAY) return when->tm_mday;
	if (bytime==BY_YEARDAY) return when->tm_yday;
	if (bytime==BY_WEEKNO) return when->tm_wday;
	if (bytime==BY_MONTH) return when->tm_mon;
	return -999;
}
int ical_limit(Datelist *dl, Bylist *by, int byint, int byperiod, int runits, int byyear )
{
	// Go through 'dl' and remove any that 'by' does not include....
	Datelist out;
	int i;
	time_t t;
	struct tm *when;
	struct tm tbf;

	dl_clear(&out);

	// Improve the limit handling so we take account of  first/last monday of month etc...
	for (i=0; i<dl->nevents;i++) {
		// Check each entry is valid for all 'by' events...
		int part;
		t = dl->event[i];
		when = safe_localtime(&t,&tbf); if (when==NULL) break;
		part = dl_time_part(when,byint);
		if (!dl_oneof(by,part,runits,(int)t)) continue; 
		dl_add(&out,t);
	}
	dl_copy(dl,&out); // Copy onto original 'dl' 
	return TRUE;
}
/*
                       / ( "BYSECOND" "=" byseclist )
                       / ( "BYMINUTE" "=" byminlist )
                       / ( "BYHOUR" "=" byhrlist )
                       / ( "BYDAY" "=" bywdaylist )
                       / ( "BYMONTHDAY" "=" bymodaylist )
                       / ( "BYYEARDAY" "=" byyrdaylist )
                       / ( "BYWEEKNO" "=" bywknolist )
                       / ( "BYMONTH" "=" bymolist )
                       / ( "BYSETPOS" "=" bysplist ) -- NOT SUPPORTING THIS AS i CAN'T FIGURE OUT WHAT IT MEANS. 
*/

typedef struct EXPAND {char *by; int limit; int expand; int byint; int byperiod;} Expand;
static Expand exrules[]={
	"BYMONTH",R_MONTHLY,R_YEARLY,BY_MONTH,AMONTH
	,"BYWEEKNO",0,0,BY_WEEKNO,AWEEK
	,"BYYEARDAY",R_HOURLY,R_YEARLY,BY_YEARDAY,ADAY
	,"BYMONTHDAY",R_DAILY,R_MONTHLY,BY_MONTHDAY,ADAY
	,"BYDAY",R_DAILY,R_WEEKLY,BY_DAY,ADAY
	,"BYHOUR",R_HOURLY,R_DAILY,BY_HOUR,ANHOUR
	,"BYMINUTE",R_MINUTELY,R_HOURLY,BY_MINUTE,AMINUTE
	,"BYSECOND",R_YEARLY,R_YEARLY,BY_SECOND,ASECOND
	,NULL,0,0,0,0
};
int ds_cmp(const void *e1, const void *e2)
{
	int a = *((int *) e1);
	int b = *((int *) e2);
	return a-b;
}
int dl_sort(Datelist *ds) 
{
	if (ds->nevents>1) qsort(ds->event,ds->nevents,sizeof(ds->event[1]),ds_cmp);
	return TRUE;
}
int dl_compress(Datelist *dl)
{
	int i;
	int out=0;
	for (i=0;i<dl->nevents;i++) {
		if (dl->event[i]) dl->event[out++] = dl->event[i];
	}
	dl->nevents = out;
	return TRUE;
}
int dl_subtract(Datelist *dl, Datelist *xl)
{
	int i,j;
	for (i=0;i<dl->nevents;i++) {
		time_t now = dl->event[i];
		for (j=0; j<xl->nevents; j++) {
			if (xl->event[j]==now) dl->event[i] = 0;
		}
	}
	dl_compress(dl);
	return TRUE;
}
int ical_ruletime(Datelist *dl, Ical *ic, char *var, time_t start, time_t dtend)
{
	char *pval,*attrib;
	Pair *list=NULL;
	char *freq;
	int units;
	int  byyear;
	time_t dtstart;
	int period;
	time_t until;
	Datelist ds;
	Bylist by;
	int count;
	int i;
	dl_clear(&ds);
	by.nevents = 0;

	// DTSTART;TZID=US-Eastern:19970902T090000
	
	if (!ical_find_both(ic,var,&pval,&attrib)) return FALSE;
	// pval = "FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1"

	// Split up based on ';'... into a pair list.
	list = pair_split(pval,";");

	until = ical_time(pair_find(list,"until"),0);
	if (until!=0) if (until<dtend) dtend = until;

	freq = pair_find(list,"FREQ");
	period = ical_freq_find(freq,&units);
	if (!ical_find_both(ic,"DTSTART",&pval,&attrib)) goto failed;
	dtstart = ical_time(pval,0);
	// DTSTART;TZID=US-Eastern:19970902T090000
	ical_generate(&ds,dtstart,start,dtend,period);

	/*
	evaluated occurrences in the
      following order: BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY,
      BYHOUR, BYMINUTE, BYSECOND and BYSETPOS; then COUNT and UNTIL are
      evaluated.
	*/

	/*
	The numeric value in a
      BYDAY rule part with the FREQ rule part set to YEARLY corresponds
      to an offset within the month when the BYMONTH rule part is
      present, and corresponds to an offset within the year when the
      BYWEEKNO or BYMONTH rule parts are present.  
	 */

	byyear = TRUE;
	if (lib_strlen(pair_find(list,"BYMONTH"))>0) byyear = FALSE;

	for (i=0; exrules[i].by!=NULL;i++) {
		char *bystr = pair_find(list,exrules[i].by);
		if (lib_strlen(bystr)==0) continue;
		ical_bylist(&by,bystr,exrules[i].byint==BY_DAY); // Break into an array of integers and flags
		if (period>=exrules[i].expand)  { // Then expand event by the following months...
			ical_expand(&ds,&by,exrules[i].byint,exrules[i].byperiod,units,byyear);
		} else if (period<=exrules[i].limit) {
			ical_limit(&ds,&by,exrules[i].byint,exrules[i].byperiod,units,byyear);
		}
	}
	
	// Sort 'ds' array into order.  
	dl_sort(&ds);

	// Now apply count or until rules to limit the results.
	count = atoi(pair_find(list,"count"));
	if (count>0) {		if (ds.nevents>count) ds.nevents = count; }

	// Now apply 'until' rule...
	until = ical_time(pair_find(list,"until"),0);
	for (i=0; i<ds.nevents;i++) {
		if (ds.event[i]>until) {ds.nevents = i; break;}
	}


	pair_free(&list);
	return TRUE;
failed:
	pair_free(&list);
	return FALSE;

}


/*

23.2 Appendix 2 - ISO 8601 Date and Time Profile

   The creationdate property specifies the use of the ISO 8601 date
   format [ISO-8601].  This section defines a profile of the ISO 8601
   date format for use with this specification.  This profile is quoted
   from an Internet-Draft by Chris Newman, and is mentioned here to
   properly attribute his work.

   date-time       = full-date "T" full-time

   full-date       = date-fullyear "-" date-month "-" date-mday
   full-time       = partial-time time-offset

   date-fullyear   = 4DIGIT
   date-month      = 2DIGIT  ; 01-12
   date-mday       = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on
   month/year
   time-hour       = 2DIGIT  ; 00-23
   time-minute     = 2DIGIT  ; 00-59
   time-second     = 2DIGIT  ; 00-59, 00-60 based on leap second rules
   time-secfrac    = "." 1*DIGIT
   time-numoffset  = ("+" / "-") time-hour ":" time-minute
   time-offset     = "Z" / time-numoffset

   partial-time    = time-hour ":" time-minute ":" time-second
                    [time-secfrac]

   Numeric offsets are calculated as local time minus UTC (Coordinated
   Universal Time).  So the equivalent time in UTC can be determined by
   subtracting the offset from the local time.  For example, 18:50:00-
   04:00 is the same time as 22:58:00Z.

   If the time in UTC is known, but the offset to local time is unknown,
   this can be represented with an offset of "-00:00".  This differs
   from an offset of "Z" which implies that UTC is the preferred
   reference point for the specified time.


*/
time_t ical_time(char *s, int timezone_offset)
{
	// yyyymmddThhmmss[.ss]Z	"20051111T111100"
	// yyyymmddThhmmss+12:00	"20051111T111100"
	// yyyymmddThhmmss-hh:mm	"20051111T111100"
	char offsetmin[BFSZ];
	char offsetsec[BFSZ];
	char tmp[BFSZ];
	struct tm x;
	int offset=timezone_offset;
	memset(&x,0,sizeof(struct tm));

	if (lib_strlen(s)<8) return 0;

	ncpy(tmp,s,4); 	x.tm_year = atoi(tmp) - 1900;
	ncpy(tmp,s+4,2); x.tm_mon = atoi(tmp) - 1;	// Marijn fixed 15/6/18 : tm_mon is zero based!
	ncpy(tmp,s+6,2); x.tm_mday = atoi(tmp);

	if (s[8]!='T') goto done;

	ncpy(tmp,s+9,2); 	x.tm_hour = atoi(tmp);
	ncpy(tmp,s+11,2); 	x.tm_min = atoi(tmp);
	ncpy(tmp,s+13,2); 	x.tm_sec = atoi(tmp);

	if (lib_strlen(s)>=15) s = s + 15;
	if (*s=='.') {
		s += 2;
	}
	offset = timezone_offset;
	if (*s=='Z') offset=0;
	if (*s=='+' || *s=='-') {
		char p = *s++;
		ncpy(offsetmin,s,2); s+=2;
		ncpy(offsetsec,s,2); s+=2;
		offset = atoi(offsetmin) * 60 + atoi(offsetsec);
		if (p=='-') offset = -offset;
	}
done:
	return mktime(&x)+offset;
}
int ical_test(char *fname)
{
	Ical *ic;
	// Load the calender 'fname' and print recurring events for it.
	ic = ical_read(fname);
	ical_during(ic,0,0);

	ical_ffree(&ic);
	return TRUE;
}

int ical_count_events(Ical *ic)
{
	Ical *icp, *icp2;
	int ne=0, np=0;
	for(icp=ic ; icp!=NULL ; icp=icp->next){
		ne++;
		for(icp2=icp->sub;icp2;icp2=icp2->next){	// Possible repeat instances during time period
			np++;
		}
	}
	return np;
}

int ical_event_index(Ical *ic, Ical**index, int np, int clean, int offset)
{
	Ical *icp, *icp2, *icp_main=NULL;
	int i=0, repeat;

	for(icp=ic ; icp!=NULL ; icp=icp->next){
		Ical *icp_main=NULL;
		if(!icp || !icp->sub) continue;
		repeat=0;
		for(icp2 = icp->sub; icp2 ; icp2 = icp2->next){
			if(!(lib_starts_with(icp2->name_extra,"VEVENT") || lib_starts_with(icp2->name_extra,"VTODO"))) 
				continue;
			if(i>np) continue;
			index[i]=icp2;
			i++;
			if(!clean) continue;
			ical_clean_one_entry(icp2, icp, &repeat, &icp_main, offset);
		}
		if(clean && repeat>0 && icp_main && icp_main->repeat==0) {
			// First item in repeat events does not have a RECURRANCE-ID! so if we have
			// repeat events displayed manally set the first time to be a repeat event. 
			// This at least updates the display nicely clientside, does not catch the
			// warning of moving this / all events but that is acceptable. Not worth
			// fetching all event data (which seems the only way of getting that) just 
			// for this trivial feature
			icp_main->repeat = ++repeat;
		}
	}
	return i;
}

int ical_alert_offset(char *txt)
{
	int num = 0;
	int mult = 1;
	char *s;
	// "PT0S" "-PT5M" "-PT15M" "-PT30M" "-PT1H" "-PT2H" "-P1D" "-P2D"
	if(strncmpnc(txt,"-PT",3)==0){
		s=txt+3;
		num=atoi(s);
		while(*s && sisdigit(*s)) s++;
		if(strncmpnc(s,"S",1)==0){
			mult=-1;
		} else if(strncmpnc(s,"M",1)==0){
			mult=-60;
		} else if(strncmpnc(s,"H",1)==0){
			mult=-60*60;
		}
	}else if(strncmpnc(txt,"-P",2)==0){
		s=txt+2;
		num=atoi(s);
		while(*s && sisdigit(*s)) s++;
		if(strncmpnc(s,"D",1)==0){
			mult=-60*60*24;
		}
	}
	return num*mult;
}

int ical_clean_one_entry(Ical *icp2, Ical*icp, int *repeat, Ical**icp_main, int offset)
{
	char *pval,*attrib, tmp[BFSZ], t1[BFSZ];
	Ical *icp3;
	int alert_offset=0;

	// Start epoch
	ical_find_both(icp2,"DTSTART",&pval,&attrib);
	ncpy(t1,pval,BFSZ-1);
	lib_strip_crlf(t1);
	icp2->start=ical_timestamp(t1, offset);

	for(icp3=icp2->sub; icp3; icp3=icp3->next){
		ical_find_both(icp3,"TRIGGER",&pval,&attrib);
		if(strlen(pval)==0) continue;
		alert_offset = ical_alert_offset(pval);
//		Remove to make "at the time of event" alerts work
//		if (alert_offset==0) continue;
		icp2->alerts[icp2->alert_count] = icp2->start + alert_offset;
		icp2->alert_count++;
		if(icp2->alert_count==MAX_ALERTS) break;
	}


	// Work out recurrance ID info
	ical_find_both(icp2,"RECURRENCE-ID",&pval,&attrib);
	ncpy(tmp,pval,BFSZ-1);
	lib_strip_crlf(tmp);
	if(lib_strlen(tmp)>0) {
		icp2->repeat = ++(*repeat);
		icp2->rid = strdup(tmp);
	}else{
		icp2->repeat=0;
		*icp_main=icp2;
	}

	// Plus some extra calendar info
	icp2->readonly=icp->readonly;
	if (!icp2->cal_id) icp2->cal_id=strdup(icp->cal_id);

	return TRUE;
}

// 13/3/21 Marijn: Add a version that works with CALDAV REPORT command. Maybe ical_timestamp should be fixed, but have no 
// idea if that would have additional side effects!! So lets do minimum impact fix. QQQTZ
// 15/8/21 Marijn: Further fixes to restore two of three original code paths
int ical_timestamp_report(char *txt, int offset)
{
	time_t t=0, tt=0;
	struct tm tm;
	char tmp[BFSZ];

	if(!txt || lib_strlen(txt)==0) return 0;

	memset(&tm,0,sizeof(struct tm));
	if(lib_strlen(txt)==8){
		// This was needed for full day event display in surgeweb calendar. Restore to behaviour in ical_timestamp. Maybe fix further in the future??
		// Date in YYYYMMDD calendar local time
		ncpy(tmp,txt,4); tm.tm_year = atoi(tmp)-1900;
		ncpy(tmp,txt+4,2); tm.tm_mon = atoi(tmp)-1;
		ncpy(tmp,txt+6,2); tm.tm_mday = atoi(tmp);
		t=mktime(&tm);
		tm.tm_isdst = -1;
		tt = t+lib_gmt_offset_at(t)-offset;

	}else if(lib_strlen(txt)==15){
		// Not sure what this code path is used for. Restore to behaviour in ical_timestamp. Maybe fix further in the future??
		// Date in YYYYMMDDTHHMMSS calendar local time
		ncpy(tmp,txt,4); tm.tm_year = atoi(tmp)-1900;
		ncpy(tmp,txt+4,2); tm.tm_mon = atoi(tmp)-1;
		ncpy(tmp,txt+6,2); tm.tm_mday = atoi(tmp);
		ncpy(tmp,txt+9,2); tm.tm_hour = atoi(tmp);
		ncpy(tmp,txt+11,2); tm.tm_min = atoi(tmp);
		ncpy(tmp,txt+13,2); tm.tm_sec = atoi(tmp);
		tm.tm_isdst = -1;
		t=mktime(&tm);
		tt = t+lib_gmt_offset_at(t)-offset;	

	}else if(lib_strlen(txt)==16){
		// THIS IS the routine fixed on 13/3/21
		// Date in YYYYMMDDTHHMMSSZ UTC time
		ncpy(tmp,txt,4); tm.tm_year = atoi(tmp)-1900;
		ncpy(tmp,txt+4,2); tm.tm_mon = atoi(tmp)-1;
		ncpy(tmp,txt+6,2); tm.tm_mday = atoi(tmp);
		ncpy(tmp,txt+9,2); tm.tm_hour = atoi(tmp);
		ncpy(tmp,txt+11,2); tm.tm_min = atoi(tmp);
		ncpy(tmp,txt+13,2); tm.tm_sec = atoi(tmp);
		t=mktime(&tm);
		tt = t + lib_gmt_offset_at( time(NULL) );			// QQQTZ
	}

	return (int)tt;
}

// marijn (17/6/18): Old function does not use tm_isdst!, does use lib_gmt_offset_at. Not sure if this needs 
//  further fixing? (serverside / clientside / both??), surgeweb calendar results are right though currently so leaving alone for now
int ical_timestamp(char *txt, int offset)
{
	time_t t=0, tt=0;
	struct tm tm;
	char tmp[BFSZ];

	if(!txt || lib_strlen(txt)==0) return 0;

	memset(&tm,0,sizeof(struct tm));
	if(lib_strlen(txt)==8){
		// Date in YYYYMMDD calendar local time
		ncpy(tmp,txt,4); tm.tm_year = atoi(tmp)-1900;
		ncpy(tmp,txt+4,2); tm.tm_mon = atoi(tmp)-1;
		ncpy(tmp,txt+6,2); tm.tm_mday = atoi(tmp);
		t=mktime(&tm);
		tm.tm_isdst = -1;
		tt = t+lib_gmt_offset_at(t)-offset;

	}else if(lib_strlen(txt)==15){
		// Date in YYYYMMDDTHHMMSS calendar local time
		ncpy(tmp,txt,4); tm.tm_year = atoi(tmp)-1900;
		ncpy(tmp,txt+4,2); tm.tm_mon = atoi(tmp)-1;
		ncpy(tmp,txt+6,2); tm.tm_mday = atoi(tmp);
		ncpy(tmp,txt+9,2); tm.tm_hour = atoi(tmp);
		ncpy(tmp,txt+11,2); tm.tm_min = atoi(tmp);
		ncpy(tmp,txt+13,2); tm.tm_sec = atoi(tmp);
		tm.tm_isdst = -1;
		t=mktime(&tm);
		tt = t+lib_gmt_offset_at(t)-offset;	

	}else if(lib_strlen(txt)==16){
		// Date in YYYYMMDDTHHMMSSZ UTC time
		ncpy(tmp,txt,4); tm.tm_year = atoi(tmp)-1900;
		ncpy(tmp,txt+4,2); tm.tm_mon = atoi(tmp)-1;
		ncpy(tmp,txt+6,2); tm.tm_mday = atoi(tmp);
		ncpy(tmp,txt+9,2); tm.tm_hour = atoi(tmp);
		ncpy(tmp,txt+11,2); tm.tm_min = atoi(tmp);
		ncpy(tmp,txt+13,2); tm.tm_sec = atoi(tmp);
		tm.tm_isdst = -1;
		t=mktime(&tm);
		tt = t+lib_gmt_offset_at(t);
	}
	return (int)tt;
}
// marijn (17/6/18): also see comment for ical_timestamp. This used for the invite handling date format code in "user time", ignoring 
//  zone info like "Pacific/Auckland". NEEDS tm_isdst, to match DST settings in formatting code! (lib_nice_daterange_compact)
int ical_timestamp_local(char *txt)
{
	time_t t, tt=0;
	struct tm tm;
	char tmp[BFSZ];

	if(!txt || lib_strlen(txt)==0) return 0;

	memset(&tm,0,sizeof(struct tm));
	if(lib_strlen(txt)==8){
		// Date in YYYYMMDD calendar local time
		ncpy(tmp,txt,4); tm.tm_year = atoi(tmp)-1900;
		ncpy(tmp,txt+4,2); tm.tm_mon = atoi(tmp)-1;
		ncpy(tmp,txt+6,2); tm.tm_mday = atoi(tmp);
		tm.tm_isdst = -1;
		t=mktime(&tm);
		tt = t;

	}else if(lib_strlen(txt)==15){
		// Date in YYYYMMDDTHHMMSS calendar local time
		ncpy(tmp,txt,4); tm.tm_year = atoi(tmp)-1900;
		ncpy(tmp,txt+4,2); tm.tm_mon = atoi(tmp)-1;
		ncpy(tmp,txt+6,2); tm.tm_mday = atoi(tmp);
		ncpy(tmp,txt+9,2); tm.tm_hour = atoi(tmp);
		ncpy(tmp,txt+11,2); tm.tm_min = atoi(tmp);
		ncpy(tmp,txt+13,2); tm.tm_sec = atoi(tmp);
		tm.tm_isdst = -1;
		t=mktime(&tm);
		tt = t;	

	}else if(lib_strlen(txt)==16){
		emsg("ical_timestamp_local not sure this codepath is meaningful / valid");
		// Date in YYYYMMDDTHHMMSSZ UTC time
		ncpy(tmp,txt,4); tm.tm_year = atoi(tmp)-1900;
		ncpy(tmp,txt+4,2); tm.tm_mon = atoi(tmp)-1;
		ncpy(tmp,txt+6,2); tm.tm_mday = atoi(tmp);
		ncpy(tmp,txt+9,2); tm.tm_hour = atoi(tmp);
		ncpy(tmp,txt+11,2); tm.tm_min = atoi(tmp);
		ncpy(tmp,txt+13,2); tm.tm_sec = atoi(tmp);

		tm.tm_isdst = -1;
		t=mktime(&tm);
		tt = t + lib_gmt_offset_at(t);
	}
	return (int)tt;
}

char *ical_timestamp_txt(time_t t,int utc,char *bf)
{
	struct tm tm;
	if(utc) t-=lib_gmt_offset_at(t);
	safe_localtime(&t,&tm);
	lprintf(bf,BFSZ,"%04d%02d%02dT%02d%02d%02d%s", tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, 
									 tm.tm_hour, tm.tm_min, tm.tm_sec, (utc?"Z":""));
	return bf;
}

Dstr *ical_generate_ics(Ical *ic, Dstr *dstr)
{
	Ipair *p;
	Ical *icp;
	char bf[BFSZ],tmp[BFSZ], *s, *section;
	int sanity=1000;

	if(dstr==NULL) {
		dstr_new(&dstr);
		dstr_nodisk(dstr,TRUE);
	}
	section=ic->name;
	if(strcmp(section,"BEGIN")==0) section=ic->name_extra;

	lprintf(bf,BFSZ,"BEGIN:%s\n",section);
	dstr_addsn(dstr,bf);
	for (p=ic->fields;p!=NULL;p=p->next) {
		ncpy(bf,p->var,BFSZ-1);
		if(lib_strlen(p->attrib)>0){
			ncat(bf,";",BFSZ-1);
			ncat(bf,p->attrib,BFSZ-1);
		}
		ncat(bf,":",BFSZ-1);
		if(strcmp(p->var,"DESCRIPTION")==0){
			ncat(bf,ical_encode(p->val,tmp),BFSZ-1);
		}else{
			ncat(bf,p->val,BFSZ-1);
		}
		s=bf;
again:
		ncpy(tmp,s,BFSZ-1);
		if(lib_strlen(tmp)>=100) {
			tmp[100]=0;
			dstr_addsn(dstr,tmp);
			dstr_addsn(dstr,"\n ");
			s+=100;
			sanity--; if(sanity==0)break;
			goto again;
		}
		dstr_addsn(dstr,tmp);
		dstr_addsn(dstr,"\n");
	}

	for(icp=ic->sub;icp;icp=icp->next){
		if(icp->del) continue;
		ical_generate_ics(icp,dstr);
	}
	lprintf(bf,BFSZ,"END:%s\n",section);
	dstr_addsn(dstr,bf);

	return dstr;
}
Ical *ical_default_ics(Ical **ic_out, int offset, int is_todo)
{
	char tmp[BFSZ];
	Ical *ic=NULL,*ev=NULL;
	Ipair *end=NULL;

	ic= ical_new(); //calloc(1,sizeof(Ical));
	ic->name=strdup("VCALENDAR");
	end=ic->fields;
	ipair_add_toend(&ic->fields,&end,"CALSCALE","GREGORIAN","");
	ipair_add_toend(&ic->fields,&end,"PRODID","","");
	ipair_add_toend(&ic->fields,&end,"VERSION","2.0","");

	ev= ical_new(); // calloc(1,sizeof(Ical));
	ev->name=strdup(is_todo?"VTODO":"VEVENT");
	ic->sub=ev;
	ipair_add_toend(&ev->fields,&end,"UID","","");
	ipair_add_toend(&ev->fields,&end,"CREATED",ical_timestamp_txt(time(NULL),TRUE,tmp),"");
	ipair_add_toend(&ev->fields,&end,"LAST-MODIFIED","","");
	ipair_add_toend(&ev->fields,&end,"DTSTAMP","","");
	ipair_add_toend(&ev->fields,&end,"SEQUENCE","0","");
	ipair_add_toend(&ev->fields,&end,"SUMMARY","","");

	if(is_todo){
/*	
		UID:0DDBE1EF-11AC-4757-AF62-CBF6321059B7
		CREATED:20120907T030642Z
		LAST-MODIFIED:20120907T041755Z
		DTSTAMP:20120907T042107Z
		SUMMARY:No date but complete
		SEQUENCE:0

		DTSTART;TZID=Pacific/Auckland:20120910T090000
		DUE;TZID=Pacific/Auckland:20120910T090000

		PERCENT-COMPLETE:100
		COMPLETED:20120907T041752Z
*/
	}else{
		ipair_add_toend(&ev->fields,&end,"DTSTART","","");
		ipair_add_toend(&ev->fields,&end,"DTEND","","");
		ipair_add_toend(&ev->fields,&end,"TRANSP","OPAQUE","");
		ipair_add_toend(&ev->fields,&end,"ORGANIZER","","");	
		ipair_add_toend(&ev->fields,&end,"LOCATION","","");
		//ipair_add_toend(&ev->fields,&end,"INVITEES","","");	
		ipair_add_toend(&ev->fields,&end,"DESCRIPTION","","");
	}

	*ic_out = ic;
	return ev;
}
char *ical_field_get(Ical *ic, char *var)
{
	return ical_find_value(ic, var);
}
void ical_lock(Ical *ic,char *var, char *file, int line)
{
	if (ic->fields_locked) {
		emsg("PANIC, ical_lock detected concurrent set operation %s:%d %s\n",file,line,var);
		emsg("PANIC, ical_lock detected concurrent set guary %d\n",ic->guard);
		emsg("PANIC, ical_lock detected concurrent set locked by %s:%d\n",ic->locked_file,ic->locked_line);
		emsg("PANIC, ical_lock detected concurrent set created %s:%d\n",ic->created_file,ic->created_line);
		lib_abort();
	}
	ic->locked_file = file;
	ic->locked_line = line;
	ic->fields_locked = TRUE;

}
void ical_unlock(Ical *ic)
{
	ic->fields_locked = FALSE;
}
int ical_field_setx(Ical *ic, char *var, char *val, char*attrib, char *file, int line)
{
	int r;
	if (val==NULL) return FALSE;
	if (var==NULL) return FALSE;

	ical_lock(ic,var,file,line);
	r = ipair_set_value(&ic->fields, var, val, attrib);
	ical_unlock(ic);
	return r;
}
int ical_field_remove(Ical *ic, char *var)
{
	return ipair_delete_any(&ic->fields, var);
}
int ical_mfield_remove(Ical *ic, char *var, char *val)
{
	return ipair_delete_specific(&ic->fields, var, val);
}
int ical_field_addmatch(Ical *ic, char *var, char *val, char*attrib)
{
	return ipair_addmatch_value(&ic->fields, var, val, attrib);
}

void ical_copy_top(Ical *src, Ical *dst)
{
	dst->name = strdup(src->name);
	dst->name_extra = strdup(src->name_extra);
	dst->cal_id = strdup(src->cal_id);
}
// eg change:
//	CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=marijn@netwinsite.com;X-NUM-GUESTS=0
//	PARTSTAT=DECLINED | ACCEPTED | TENTATIVE
void cal_update_string(char *src, char *var, char *val, char *dst)
{
	char *s;
	char *upto;
	char bf[BFSZ], tmp[BFSZ];
	int found=FALSE;
	ncpy(bf,src,BFSZ-1);
	ncpy(dst,"",BFSZ-1);
	for (s=mystrtok(bf,";",&upto);s!=NULL;s=mystrtok(NULL,";",&upto)) {
		if(strncmpnc(s,var,lib_strlen(var))==0){
			//match, so replace
			lprintf(tmp,BFSZ-1,"%s=%s",var,val);
			ncat_sep(dst,";",tmp,BFSZ-1);
			found = true;
		} else{
			ncat_sep(dst,";",s,BFSZ-1);
		}
	}
	if(!found){
		lprintf(tmp,BFSZ-1,"%s=%s",var,val);
		ncat_sep(dst,";",tmp,BFSZ-1);
	}
}

void cal_update_str(char *str, char *var, char *val)
{
	char tmp[BFSZ];
	cal_update_string(str, var, val, tmp);
	ncpy(str, tmp, BFSZ-1);
}

Ical *cal_event_sub(Ical*cal)
{
	Ical *sub = NULL, *c;
	if(cal==NULL || cal->sub==NULL){
		return sub;
	}
	for(c = cal->sub; c; c = c->next )
	{
		if(strcmp(c->name_extra,"VEVENT")==0){
			sub = c;
			break;
		}
	}
	return sub;
}
Ical *ical_newx(char *src, int line)
{
	Ical *c = calloc(1,sizeof(Ical));
	c->created_file = src;
	c->created_line = line;
	c->guard = 123456;
	return c;
}

Ical *ical_confirmation(Ical *src, char *email, char *action)	// cf ical_read_object, sw_update_event_fields (action=DECLINED | ACCEPTED | TENTATIVE)
{
	Ical *dst, *dst2;
	Ical *event_sub = NULL;
	Ipair *field;
	Ipair *dst_end, *sub_end;
	int matched = FALSE; 
	char tmp[BFSZ];

	if(!src || !src->sub) return NULL;
	dst=ical_new(); //calloc(1,sizeof(Ical));
	dst2 = ical_new(); //  calloc(1,sizeof(Ical));
	if(!dst || !dst2) return NULL;

	dst->sub = dst2;
	
	dst_end = dst->fields;
	sub_end = dst2->fields;

	ical_copy_top(src, dst);
	for(field = src->fields; field; field=field->next) {
		if(strcmpnc(field->var, "PRODID")==0){
			ipair_add_toend(&dst->fields,&dst_end,field->var,"Surge SAM","");
		} else if(strcmpnc(field->var, "METHOD")==0) {
			ipair_add_toend(&dst->fields,&dst_end,field->var,"REPLY","");				
		} else {
			ipair_add_toend(&dst->fields,&dst_end,field->var,field->val,field->attrib);
		}
	}
	event_sub = cal_event_sub(src);
	if(event_sub==NULL){
		// No EVENT part found?!
		ical_ffree(&dst); return NULL;
	}
	
	// MAYBE we need to be smarter and copy timezone etc subparts too??
	ical_copy_top(event_sub, dst->sub);

	for(field = event_sub->fields; field; field=field->next)
	{
		if(strcmpnc(field->var, "ATTENDEE")==0){
			// for each Attendee field. If matches us, update it with suitable reply, else remove it
			if(strstrnc(field->val, email) || strstrnc(field->attrib, email))
			{
				char updated[BFSZ];
				cal_update_string(field->attrib, "PARTSTAT", action, updated);
				ipair_add_toend(&dst->fields,&dst_end,field->var,field->val,updated);	
				matched = true;
			}
		}else if(strcmpnc(field->var, "STATUS")==0){
			ipair_add_toend(&dst->sub->fields,&sub_end,field->var,"CONFIRMED","");				
		}else {
			ipair_add_toend(&dst->sub->fields,&sub_end,field->var,field->val,field->attrib);
		}
	}

	if(!matched){
		// No address match so ical confirmation is not valid!!
		// ical_ffree(&dst); return NULL;

		// Nope, just create a new attendee now! (logic from sw_update_attendees)
		char email_lwr[BFSZ], email_key[BFSZ], attr[BFSZ];

		ncpy(email_lwr, email, BFSZ-1);
		strlwr(email_lwr);
		lprintf(email_key, BFSZ, "mailto:%s", email_lwr);
		attr[0]=0;
		cal_update_str(attr, "CUTYPE", "INDIVIDUAL");
		cal_update_str(attr, "ROLE", "REQ-PARTICIPANT");
		cal_update_str(attr, "PARTSTAT", action);
		cal_update_str(attr, "RSVP", "TRUE");
		cal_update_str(attr, "CN", email_lwr);
		cal_update_str(attr, "X-NUM-GUESTS", "0");

		ical_field_addmatch(dst->sub, "ATTENDEE", email_key, attr);
	}
	
	ical_field_set(dst->sub,"LAST-MODIFIED",ical_timestamp_txt(time(NULL),TRUE,tmp),"");

	return dst;
}

int ical_update_event(Ical *ev, Ical *cal_request, char *action, int ev_is_new)
{
	char tmp[BFSZ];

	// propagate email based ical updates to existing events
	ical_field_set(cal_request->sub,"LAST-MODIFIED",ical_timestamp_txt(time(NULL),TRUE,tmp),"");
	return TRUE;
}

int ical_mfield_set(Ical *ic, char *var, char *val, char*attrib)	// cf ical_field_set
{
	//more like: ipair_addmatch_value
	return ipair_addmatch_value(&ic->fields, var, val, attrib);
}
char *ical_partstat_status(char *attrib)
{
	char *result = "unknown";
	if (strstrnc(attrib,"ACCEPTED")) result="yes";
	if (strstrnc(attrib,"TENTATIVE")) result="maybe";
	if (strstrnc(attrib,"DECLINED")) result="no";
	return result;
}

char *ical_encode(char *in,char *out)
{
	char *pin,*pout;
	int outbfsz=BFSZ-1;

	if(!in) return "";
	for(pin=in,pout=out;*pin;pin++) {
		if((pout-out)>=(outbfsz-6)) break;
		switch(*pin) {
			case '\r' : strcpy(pout,"\\r"); pout+=2;break;
			case '\n': strcpy(pout,"\\n"); pout+=2; break;
			//case ',': 
			//case ';': 
			//case ':':  *pout++='\\';*pout++=*pin;break;
			default:	
				*pout++=*pin;break;
		}
	}
	*pout++='\0';
	return out;

}
