/*
    kfc - Kurt's Free Calendar
    Copyright 1995 Kurt Werle
  
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
  
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
  
    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#import "anAppointment.h"

@implementation anAppointment

- init
{
    [super init];
    microsecond = second = 0;
    minute = hour = day = month = year = -1;

    appointmentText = [[MiscString alloc] init];
    firstLine = [[MiscString alloc] init];
    untilDate = nil;
    weekDay = -1;
    back = 0;
    delta = 0;
    repeat = 0;
    priority = 0;
    tdelta = 0;
    trepeat = 0;
    return self;
}

- setYear:(int)t
{
    if (t > -1)
    {
	if (t < 70)
	{
	    t += 2000;
	}
	if (t < 100)
	{
	    t += 1900;
	}
    }
    [super setYear:t];
    return self;
}

- initWithAppointmentString:(char *)AppointmentText
{
    int                 Day,
                        Month,
                        Year;
    char               *TheDate = "10/26/94";

    [self init];
    sscanf (TheDate, "%d/%d/%d", &Month, &Day, &Year);
    [self setDay:Day];
    [self setMonth:Month];
    [self setYear:Year];
    [appointmentText setStringValue:AppointmentText];
    return self;
}

- setAppointment:(MiscString *) theAppointment
    minute:(int)theminute
    hour:(int)thehour
    day:(int)theday
    month:(int)themonth
    year:(int)theyear
{
    [self setMinute:theminute];
    [self setHour:thehour];
    [self setDay:theday];
    [self setMonth:themonth];
    [self setYear:theyear];
    [appointmentText free];
    appointmentText = [theAppointment copy];
    return self;
}

- setAppointmentText:(const char *)newstring
{
    [appointmentText setStringValue:newstring];
    return self;
}

- setWeekDay:(int)newWeekDay
{
    weekDay = newWeekDay;
    return self;
}

- setBack:(int)newback
{
    back = newback;
    return self;
}

- setDelta:(int)newdelta
{
    delta = newdelta;
    return self;
}

- setRepeat:(int)newrepeat
{
    repeat = newrepeat;
    return self;
}

- setPriority:(int)newpriority
{
    priority = newpriority;
    return self;
}

- setTdelta:(int)newtdelta
{
    tdelta = newtdelta;
    return self;
}

- setTrepeat:(int)newtrepeat
{
    trepeat = newtrepeat;
    return self;
}

- clearUntilDate
{
    if (untilDate != nil)
    {
	[untilDate free];
	untilDate = nil;
    }
    return self;
}

- setUntilYear:(int)newYear Month:(int)newMonth Day:(int)newDay
{
    if (untilDate == nil)
    {
	untilDate = [[MiscTime alloc] init];
	[untilDate setHour:12];
	[untilDate setMinute:0];
    }
    if (newYear > -1)
    {
	if (newYear < 70)
	{
	    newYear += 2000;
	}
	if (newYear < 100)
	{
	    newYear += 1900;
	}
    }
    [untilDate setYear:newYear];
    [untilDate setMonth:newMonth];
    [untilDate setDay:newDay];
    return self;
}

- setIsEditable:(BOOL)editable
{
    userEditable = editable;
    return self;
}

/* Return the first line of the appointment. */
- (const char *)appointmentTextFirstLine
{
    [firstLine setString:[appointmentText stringValue]];
    [firstLine replaceEveryOccurrenceOfRegex:"\n.*" with:""];
    return[firstLine stringValue];
}

/* Return the text of the appointment, sans the first line. */
- (const char *)appointmentTextSansFirstLine
{
    [firstLine setString:[appointmentText stringValue]];
    [firstLine replaceRegex:"^.*$" with:""];
    [firstLine replaceRegex:"^\n" with:""];
    return[firstLine stringValue];
}

- appointmentText
{
    return appointmentText;
}

/* Find the earliest hit date.  This includes back days, but not warning days. */
- earliestHitDate
{
    id                  earlyDate;

    earlyDate = [[anAppointment alloc] init];
    [earlyDate copyTimeFrom:self];
    if ([self weekDay] != -1)
    {
	int                 daysToAdd = [self weekDay];

	daysToAdd -= [earlyDate dayOfWeek];

    /*
       Because I use an hour of -1 for 'no hour' it can mess up dayOfTheWeek
       by one day (backwards) 
    */
	if ([earlyDate hour] == -1)
	    daysToAdd--;

    /*
       If it's a day previous to this one, we must add a week to put it in
       the future. 
    */
	if (daysToAdd < 0)
	{
	    daysToAdd += 7;
	}
	[earlyDate addDays:daysToAdd];
    }
    [earlyDate subtractDays:back];

    return earlyDate;
}

- (BOOL)isExpiredForDay:(int)aDay Month:(int)aMonth Year:(int)aYear
{

    if (untilDate)
    {
	if ([untilDate year] < aYear)
	{
	    return YES;
	}

	if (([untilDate month] < aMonth) && ([untilDate year] == aYear))
	{
	    return YES;
	}

	if (([untilDate day] < aDay) &&
	    ([untilDate month] == aMonth) && ([untilDate year] == aYear))
	{
	    return YES;
	}
    }

    if ((day != -1) && (month != -1) && (year != -1) && (!repeat))
    {
	id                  firstHitDate = [self earliestHitDate];
	BOOL                returnValue = NO;

	if ([firstHitDate year] < aYear)
	{
	    returnValue = YES;
	}

	if (([firstHitDate month] < aMonth) && ([firstHitDate year] == aYear))
	{
	    returnValue = YES;
	}

	if (([firstHitDate day] < aDay) &&
	 ([firstHitDate month] == aMonth) && ([firstHitDate year] == aYear))
	{
	    returnValue = YES;
	}

	[firstHitDate free];
	return returnValue;
    }

    return NO;
}

- (BOOL)isExpiredForMonth:(int)aMonth Year:(int)aYear
{
    return[self isExpiredForDay:0 Month:aMonth Year:aYear];
}

 /*
    I should rewrite this as a function.  It has no place being a method.  It
    does NOTHING with the object self. 
 */
- (BOOL)datesAreWithin:fromDate and:toDate days:(int)numberdays
{
    if (([toDate intValue] - [fromDate intValue]) <=
	(numberdays * 24 * 60 * 60))
    {
	return YES;
    }
    return NO;
}

/* Return -1 if this appointment should not generate a warning today. */
/* Return 0 if this appointment should generate a warning now. */
/* Return seconds until warning if this appointment should generate a warning latter today. */
- (int)warnForDay:aDay
{
    id                  hitDate;/* The date hit by this reminder */

 /* Check if the note has expired due to the until date... */
    if ([self isExpiredForDay:[aDay day] Month:[aDay month] Year:[aDay year]])
    {
	return -1;
    }

    hitDate = [self earliestHitDate];

 /* First handle repeats (Full date spec (day and month and year). */
    if ((day != -1) && (month != -1) && (year != -1))
    {

    /*
       This entire block could probably be rewritten to be about 8 lines.  I
       may do that sometime... (Yeah, it works, so I should fix it ;-) 
    */


    /*
       Let's get to a date after 'aDay' so we're warning about something in
       the future, not the past. 
    */
	while (([aDay isAfter:hitDate]) && (repeat))
	{
	    [hitDate addDays:repeat];
	}

/* If it's today, and there's no time portion, it's valid. */
	if (([hitDate day] == [aDay day]) &&
	    ([hitDate month] == [aDay month]) &&
	    ([hitDate year] == [aDay year]) &&
	    (hour == -1))
	{
	    [hitDate free];
	    return 0;
	}

    /*
       If there's no repeat and the hitDate is not after aDay then it must be
       previous to aDay, and we should not warn about past events!! 
    */
	if ([aDay isAfter:hitDate])
	{
	    [hitDate free];
	    return -1;
	}

    /* Are we on the same day as the warning? */
	if (([hitDate day] == [aDay day]) &&
	    ([hitDate month] == [aDay month]) &&
	    ([hitDate year] == [aDay year]))
	{

	    [hitDate subtractMinutes:tdelta];

	/*
	   It is the minutes between the warning time and the appointment
	   time? 
	*/
	    if ([aDay isAfter:hitDate])
	    {
		[hitDate free];
		return 0;
	    }
	    else		/* It's not yet warning time. */
	    {
		int                 seconds_till_warning;

		seconds_till_warning = [hitDate intValue] - [aDay intValue];

		[hitDate free];
		return seconds_till_warning;
	    }
	}

    /*
       We know that the appointment is not today, and we know that it is not
       in the past.  It must be in the future.  The only way it could apply
       to us is if it has a warning period (delta). 
    */
	if (delta)
	{
	    int                 return_value;

	    [hitDate subtractDays:delta];
	    [hitDate setDelta:0];

	/*
	   Are we, in fact, past the first warning time (not including
	   minutes)? We can't do minutes here because it's too tricky.  We'll
	   do it in a couple of lines.  
	*/
	    if ([aDay isAfter:hitDate])
	    {
		[hitDate free];
		return 0;
	    }

	/*
	   OK, we are not within the delta.  Perhaps we are on the same day,
	   but the time of the appointment is later? 
	*/
	    return_value = [hitDate warnForDay:aDay];
	    [hitDate free];
	    return return_value;
	}
    }
 /* Finished handling repeats. */
 /* Now for 'partial dates'. */
    else
    {
	int                 yearstocheck;

    /*
       Make a fake appointment so we run it through this method with all the
       dates set.		    
    */

	[hitDate free];
	hitDate = [self copy];

    /*
       I'm rather proud of the following for statement structure.  It says:
       If year is not set (is -1) then check This year and next year;
       otherwise just check the set year. 
    */
	for (yearstocheck = ((year == -1) ?[aDay year] : year);
	     yearstocheck <= ((year == -1) ?[aDay year] + 1 : year);
	     yearstocheck++)
	{

	/* Have we not passed the year? */
	    if (yearstocheck >= [aDay year])
	    {
		int                 monthstocheck,
		                    daystocheck;

	    /* This is ugly, and slow?, but it works well. */
		for (monthstocheck = ((month == -1) ? 0 : month);
		     monthstocheck <= ((month == -1) ? 11 : month);
		     monthstocheck++)
		{
		    for (daystocheck = ((day == -1) ? 0 : day);
			 daystocheck <= ((day == -1) ?[MiscTime daysInMonth : monthstocheck ofYear:yearstocheck] :day);
			 daystocheck++)
		    {
			int                 return_value;

			[hitDate setYear:yearstocheck];
			[hitDate setMonth:monthstocheck];
			[hitDate setDay:daystocheck];
			return_value = [hitDate warnForDay:aDay];
			if (return_value > -1)
			{
			    [hitDate free];
			    return return_value;
			}
		    } /* Check all days. */
		} /* Check all months. */
	    }
	} /* Check for a set year (same or later than [aDay year]). */
    } /* Finished checking for partial dates. */

    [hitDate free];
    return -1;
} /* (int)warnForDay:aDay */

/* Return a list of times to display a given appointment in the given mo/yr. */
- (List *) displayTimesForMonth:(int)aMonth andYear:(int)aYear
{
    List               *myList;
    id                  hitDate;/* The date hit by this reminder */

 /* Check if the note has expired due to the until date... */
    if ([self isExpiredForMonth:aMonth Year:aYear])
    {
	return nil;
    }

 /* The note has not expired due to the until date. */
    {
	id                  earlyDate = [self earliestHitDate];

	hitDate = [self copy];	/* To get the untilDate info... */
	[hitDate copyTimeFrom:earlyDate];
	[earlyDate free];
    }

 /* See if we've reached the earliest possible hit date */
    if ([hitDate year] > aYear)
    {
	[hitDate free];
	return nil;
    }
    else
    {
	if (([hitDate month] > aMonth) && ([hitDate year] == aYear))
	{
	    [hitDate free];
	    return nil;
	}
    }
 /* The earliest possible hit date is not after our dates. */

 /* We will probably have at least one date at this point. */
    myList = [[List alloc] init];

 /* First handle repeats. */
    if ((day != -1) && (month != -1) && (year != -1))
    {

    /* Let's get it to the right year first. */
	while (([hitDate year] < aYear) && (repeat))
	{
	    [hitDate addDays:repeat];
	}

    /* Get it to the right month (at least) */
	while (([hitDate year] == aYear) && ([hitDate month] < aMonth) && (repeat))
	{
	    [hitDate addDays:repeat];
	}

    /* Here are the hits for days with repeat!! */
	while (([hitDate year] == aYear) &&
	       ([hitDate month] == aMonth) &&
	 (![hitDate isExpiredForDay:[hitDate day] Month:aMonth Year:aYear]))
	{
	    id                  displayTime;

	    displayTime = [[MiscTime alloc] init];
	    [displayTime copyTimeFrom:hitDate];
	    [myList addObject:displayTime];
	    if (!repeat)
	    {
		break;
	    }
	    else
	    {
		[hitDate addDays:repeat];
	    }
	}

    }
 /* Finished handling repeats. */
 /* Now for 'partial dates'. */
    else
    {
	int                 yearstocheck;

    /*
       I'm rather proud of the following for statement structure.  It says:
       If year is not set (is -1) then check This year and next year;
       otherwise just check the set year. 
    */
	for (yearstocheck = ((year == -1) ? aYear : year);
	     yearstocheck <= ((year == -1) ? aYear + 1 : year);
	     yearstocheck++)
	{
	/* Have we not passed the year? */
	    if (yearstocheck >= aYear)
	    {
		int                 monthstocheck,
		                    daystocheck;

	    /* This is ugly, and slow?, but it works well. */
		for (monthstocheck = ((month == -1) ? aMonth : month);
		 monthstocheck <= ((month == -1) ? ((aMonth) % 12) : month);
		     monthstocheck++)
		{
		    for (daystocheck = ((day == -1) ? 0 : day);
			 daystocheck <= ((day == -1) ?[MiscTime daysInMonth : monthstocheck ofYear:yearstocheck] :day);
			 daystocheck += ((weekDay == -1) ? 1 : 7))
		    {

		    /*
		       Make a fake appointment so we can subtract days on it.
		       Also, if it's a hit we can just pass that appointment
		       to be displayed. 
		    */
			[hitDate copyTimeFrom:self];
			[hitDate setYear:yearstocheck];
			[hitDate setMonth:monthstocheck];
			[hitDate setDay:daystocheck];
			if (weekDay != -1)
			{
			    int                 daysToAdd = [self weekDay];

			    daysToAdd -= [hitDate dayOfWeek];

			/*
			   Because I use an hour of -1 for 'no hour' it can
			   mess up dayOfTheWeek by one day (backwards) 
			*/
			    if ([hitDate hour] == -1)
				daysToAdd--;

			/*
			   If it's a day previous to this one, we must add a
			   week to put it in the future. 
			*/
			    if (daysToAdd < 0)
			    {
				daysToAdd += 7;
			    }
			    [hitDate addDays:daysToAdd];
			}

			[hitDate subtractDays:back];

			if (([hitDate month] == aMonth) &&
			    ([hitDate year] == aYear) &&
			    (![self isExpiredForDay:[hitDate day] Month:[hitDate month] Year:[hitDate year]]))
			{
			    id                  displayTime;

			    displayTime = [[MiscTime alloc] init];
			    [displayTime copyTimeFrom:hitDate];
			    [myList addObject:displayTime];

			} /* Right month and year. */
		    } /* Check all the right days. */
		} /* Check all the right months. */
	    } /* Check for a set year (same or later than aYear). */
	}
    } /* Finished checking for partial dates. */

    [hitDate free];
    return myList;
}

- (int)priority
{
    return priority;
}

- (int)weekDay
{
    return weekDay;
}

- (int)back
{
    return back;
}

- (int)delta;
{
    return delta;
}

- (int)repeat;
{
    return repeat;
}

- (int)tdelta;
{
    return tdelta;
}

- (int)trepeat;
{
    return trepeat;
}

- untilYear:(int *)theYear Month:(int *)theMonth Day:(int *)theDay
{
    if (untilDate == nil)
    {
	*theYear = -1;
	*theMonth = -1;
	*theDay = -1;
	return self;
    }
    *theYear = [untilDate year];
    *theMonth = [untilDate month];
    *theDay = [untilDate day];
    return self;
}

- (BOOL)wantsPanel
{
    switch (priority)
    {
    case PANELWARNING:
    case BOTHWARNING:
	{
	    return YES;
	}
    }
    return NO;
}

- (BOOL)wantsMailing
{
    switch (priority)
    {
    case MAILWARNING:
    case BOTHWARNING:
	{
	    return YES;
	}
    }
    return NO;
}

- (BOOL)isEditable
{
    return userEditable;
}

- copy
{
    id                  newCopy = [[[self class] alloc] init];

    [newCopy copyTimeFrom:self];
    [newCopy setAppointmentText:[appointmentText stringValue]];
    if (untilDate != nil)
    {
	[newCopy setUntilYear:[untilDate year]
	 Month:[untilDate month]
	 Day:[untilDate day]];
    }
    [newCopy setWeekDay:weekDay];
    [newCopy setBack:back];
    [newCopy setDelta:delta];
    [newCopy setRepeat:repeat];
    [newCopy setPriority:priority];
    [newCopy setTdelta:tdelta];
    [newCopy setTrepeat:trepeat];
    return newCopy;
}

- free
{
    if (appointmentText != nil)
    {
	[appointmentText free];
    }
    if (firstLine != nil)
    {
	[firstLine free];
    }
    if (untilDate != nil)
    {
	[untilDate free];
    }
    return[super free];
}

@end
