/*
 *  alarmcalendar.cpp  -  KAlarm calendar file access
 *  Program:  kalarm
 *  Copyright © 2001-2006,2009 by David Jarvie <djarvie@kde.org>
 *
 *  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.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "kalarm.h"
#include <unistd.h>
#include <time.h>

#include <tqfile.h>
#include <tqtextstream.h>
#include <tqregexp.h>
#include <tqtimer.h>

#include <tdelocale.h>
#include <tdemessagebox.h>
#include <tdestandarddirs.h>
#include <kstaticdeleter.h>
#include <tdeconfig.h>
#include <tdeaboutdata.h>
#include <tdeio/netaccess.h>
#include <tdefileitem.h>
#include <tdetempfile.h>
#include <tdefiledialog.h>
#include <dcopclient.h>
#include <kdebug.h>

extern "C" {
#include <libical/ical.h>
}

#include <libkcal/vcaldrag.h>
#include <libkcal/vcalformat.h>
#include <libkcal/icalformat.h>

#include "calendarcompat.h"
#include "daemon.h"
#include "functions.h"
#include "kalarmapp.h"
#include "mainwindow.h"
#include "preferences.h"
#include "startdaytimer.h"
#include "alarmcalendar.moc"

using namespace KCal;

TQString AlarmCalendar::icalProductId()
{
	return TQString::fromLatin1("-//K Desktop Environment//NONSGML " KALARM_NAME " %1//EN").arg(KAlarm::currentCalendarVersionString());
}

static const KAEvent::Status eventTypes[AlarmCalendar::NCALS] = {
	KAEvent::ACTIVE, KAEvent::EXPIRED, KAEvent::DISPLAYING, KAEvent::TEMPLATE
};
static const TQString calendarNames[AlarmCalendar::NCALS] = {
	TQString::fromLatin1("calendar.ics"),
	TQString::fromLatin1("expired.ics"),
	TQString::fromLatin1("displaying.ics"),
	TQString::fromLatin1("template.ics")
};
static KStaticDeleter<AlarmCalendar> calendarDeleter[AlarmCalendar::NCALS];    // ensure that the calendar destructors are called

AlarmCalendar* AlarmCalendar::mCalendars[NCALS] = { 0, 0, 0, 0 };


/******************************************************************************
* Initialise the alarm calendars, and ensure that their file names are different.
* There are 4 calendars:
*  1) A user-independent one containing the active alarms;
*  2) A historical one containing expired alarms;
*  3) A user-specific one which contains details of alarms which are currently
*     being displayed to that user and which have not yet been acknowledged;
*  4) One containing alarm templates.
* Reply = true if success, false if calendar name error.
*/
bool AlarmCalendar::initialiseCalendars()
{
	TDEConfig* config = tdeApp->config();
	config->setGroup(TQString::fromLatin1("General"));
	TQString activeKey   = TQString::fromLatin1("Calendar");
	TQString expiredKey  = TQString::fromLatin1("ExpiredCalendar");
	TQString templateKey = TQString::fromLatin1("TemplateCalendar");
	TQString displayCal, activeCal, expiredCal, templateCal;
	calendarDeleter[ACTIVE].setObject(mCalendars[ACTIVE], createCalendar(ACTIVE, config, activeCal, activeKey));
	calendarDeleter[EXPIRED].setObject(mCalendars[EXPIRED], createCalendar(EXPIRED, config, expiredCal, expiredKey));
	calendarDeleter[DISPLAY].setObject(mCalendars[DISPLAY], createCalendar(DISPLAY, config, displayCal));
	calendarDeleter[TEMPLATE].setObject(mCalendars[TEMPLATE], createCalendar(TEMPLATE, config, templateCal, templateKey));

	TQString errorKey1, errorKey2;
	if (activeCal == displayCal)
		errorKey1 = activeKey;
	else if (expiredCal == displayCal)
		errorKey1 = expiredKey;
	else if (templateCal == displayCal)
		errorKey1 = templateKey;
	if (!errorKey1.isNull())
	{
		kdError(5950) << "AlarmCalendar::initialiseCalendars(): '" << errorKey1 << "' calendar name = display calendar name\n";
		TQString file = config->readPathEntry(errorKey1);
		KAlarmApp::displayFatalError(i18n("%1: file name not permitted: %2").arg(errorKey1).arg(file));
		return false;
	}
	if (activeCal == expiredCal)
	{
		errorKey1 = activeKey;
		errorKey2 = expiredKey;
	}
	else if (activeCal == templateCal)
	{
		errorKey1 = activeKey;
		errorKey2 = templateKey;
	}
	else if (expiredCal == templateCal)
	{
		errorKey1 = expiredKey;
		errorKey2 = templateKey;
	}
	if (!errorKey1.isNull())
	{
		kdError(5950) << "AlarmCalendar::initialiseCalendars(): calendar names clash: " << errorKey1 << ", " << errorKey2 << endl;
		KAlarmApp::displayFatalError(i18n("%1, %2: file names must be different").arg(errorKey1).arg(errorKey2));
		return false;
	}
	if (!mCalendars[ACTIVE]->valid())
	{
		TQString path = mCalendars[ACTIVE]->path();
		kdError(5950) << "AlarmCalendar::initialiseCalendars(): invalid name: " << path << endl;
		KAlarmApp::displayFatalError(i18n("Invalid calendar file name: %1").arg(path));
		return false;
	}
	return true;
}

/******************************************************************************
* Create an alarm calendar instance.
* If 'configKey' is non-null, the calendar will be converted to ICal format.
*/
AlarmCalendar* AlarmCalendar::createCalendar(CalID type, TDEConfig* config, TQString& writePath, const TQString& configKey)
{
	static TQRegExp vcsRegExp(TQString::fromLatin1("\\.vcs$"));
	static TQString ical = TQString::fromLatin1(".ics");

	if (configKey.isNull())
	{
		writePath = locateLocal("appdata", calendarNames[type]);
		return new AlarmCalendar(writePath, type);
	}
	else
	{
		TQString readPath = config->readPathEntry(configKey, locateLocal("appdata", calendarNames[type]));
		writePath = readPath;
		writePath.replace(vcsRegExp, ical);
		return new AlarmCalendar(readPath, type, writePath, configKey);
	}
}

/******************************************************************************
* Terminate access to all calendars.
*/
void AlarmCalendar::terminateCalendars()
{
	for (int i = 0;  i < NCALS;  ++i)
	{
		calendarDeleter[i].destructObject();
		mCalendars[i] = 0;
	}
}

/******************************************************************************
* Return a calendar, opening it first if not already open.
* Reply = calendar instance
*       = 0 if calendar could not be opened.
*/
AlarmCalendar* AlarmCalendar::calendarOpen(CalID id)
{
	AlarmCalendar* cal = mCalendars[id];
	if (!cal->mPurgeDays)
		return 0;     // all events are automatically purged from the calendar
	if (cal->open())
		return cal;
	kdError(5950) << "AlarmCalendar::calendarOpen(" << calendarNames[id] << "): open error\n";
	return 0;
}

/******************************************************************************
* Find and return the event with the specified ID.
* The calendar searched is determined by the calendar identifier in the ID.
*/
const KCal::Event* AlarmCalendar::getEvent(const TQString& uniqueID)
{
	if (uniqueID.isEmpty())
		return 0;
	CalID calID;
	switch (KAEvent::uidStatus(uniqueID))
	{
		case KAEvent::ACTIVE:      calID = ACTIVE;  break;
		case KAEvent::TEMPLATE:    calID = TEMPLATE;  break;
		case KAEvent::EXPIRED:     calID = EXPIRED;  break;
		case KAEvent::DISPLAYING:  calID = DISPLAY;  break;
		default:
			return 0;
	}
	AlarmCalendar* cal = calendarOpen(calID);
	if (!cal)
		return 0;
	return cal->event(uniqueID);
}


/******************************************************************************
* Constructor.
* If 'icalPath' is non-null, the file will be always be saved in ICal format.
* If 'configKey' is also non-null, that config file entry will be updated when
* the file is saved in ICal format.
*/
AlarmCalendar::AlarmCalendar(const TQString& path, CalID type, const TQString& icalPath,
                             const TQString& configKey)
	: mCalendar(0),
	  mConfigKey(icalPath.isNull() ? TQString() : configKey),
	  mType(eventTypes[type]),
	  mPurgeDays(-1),      // default to not purging
	  mOpen(false),
	  mPurgeDaysQueued(-1),
	  mUpdateCount(0),
	  mUpdateSave(false)
{
	mUrl.setPath(path);       // N.B. constructor mUrl(path) doesn't work with UNIX paths
	mICalUrl.setPath(icalPath.isNull() ? path : icalPath);
	mVCal = (icalPath.isNull() || path != icalPath);    // is the calendar in ICal or VCal format?
}

AlarmCalendar::~AlarmCalendar()
{
	close();
}

/******************************************************************************
* Open the calendar file if not already open, and load it into memory.
*/
bool AlarmCalendar::open()
{
	if (mOpen)
		return true;
	if (!mUrl.isValid())
		return false;

	kdDebug(5950) << "AlarmCalendar::open(" << mUrl.prettyURL() << ")\n";
	if (!mCalendar)
		mCalendar = new CalendarLocal(TQString::fromLatin1("UTC"));
	mCalendar->setLocalTime();    // write out using local time (i.e. no time zone)

	// Check for file's existence, assuming that it does exist when uncertain,
	// to avoid overwriting it.
	if (!TDEIO::NetAccess::exists(mUrl, true, MainWindow::mainMainWindow()))
	{
		// The calendar file doesn't yet exist, so create it
		if (create())
			load();
	}
	else
	{
		// Load the existing calendar file
		if (load() == 0)
		{
			if (create())       // zero-length file - create a new one
				load();
		}
	}
	if (!mOpen)
	{
		delete mCalendar;
		mCalendar = 0;
	}
	return mOpen;
}

/******************************************************************************
* Private method to create a new calendar file.
* It is always created in iCalendar format.
*/
bool AlarmCalendar::create()
{
	if (mICalUrl.isLocalFile())
		return saveCal(mICalUrl.path());
	else
	{
		KTempFile tmpFile;
		return saveCal(tmpFile.name());
	}
}

/******************************************************************************
* Load the calendar file into memory.
* Reply = 1 if success
*       = 0 if zero-length file exists.
*       = -1 if failure to load calendar file
*       = -2 if instance uninitialised.
*/
int AlarmCalendar::load()
{
	if (!mCalendar)
		return -2;

	kdDebug(5950) << "AlarmCalendar::load(): " << mUrl.prettyURL() << endl;
	TQString tmpFile;
	if (!TDEIO::NetAccess::download(mUrl, tmpFile, MainWindow::mainMainWindow()))
	{
		kdError(5950) << "AlarmCalendar::load(): Load failure" << endl;
		KMessageBox::error(0, i18n("Cannot open calendar:\n%1").arg(mUrl.prettyURL()));
		return -1;
	}
	kdDebug(5950) << "AlarmCalendar::load(): --- Downloaded to " << tmpFile << endl;
	mCalendar->setTimeZoneId(TQString());   // default to the local time zone for reading
	bool loaded = mCalendar->load(tmpFile);
	mCalendar->setLocalTime();                 // write using local time (i.e. no time zone)
	if (!loaded)
	{
		// Check if the file is zero length
		TDEIO::NetAccess::removeTempFile(tmpFile);
		TDEIO::UDSEntry uds;
		TDEIO::NetAccess::stat(mUrl, uds, MainWindow::mainMainWindow());
		KFileItem fi(uds, mUrl);
		if (!fi.size())
			return 0;     // file is zero length
		kdError(5950) << "AlarmCalendar::load(): Error loading calendar file '" << tmpFile << "'" << endl;
		KMessageBox::error(0, i18n("Error loading calendar:\n%1\n\nPlease fix or delete the file.").arg(mUrl.prettyURL()));
		// load() could have partially populated the calendar, so clear it out
		mCalendar->close();
		delete mCalendar;
		mCalendar = 0;
		return -1;
	}
	if (!mLocalFile.isEmpty())
		TDEIO::NetAccess::removeTempFile(mLocalFile);   // removes it only if it IS a temporary file
	mLocalFile = tmpFile;

	CalendarCompat::fix(*mCalendar, mLocalFile);   // convert events to current KAlarm format for when calendar is saved
	mOpen = true;
	return 1;
}

/******************************************************************************
* Reload the calendar file into memory.
*/
bool AlarmCalendar::reload()
{
	if (!mCalendar)
		return false;
	kdDebug(5950) << "AlarmCalendar::reload(): " << mUrl.prettyURL() << endl;
	close();
	bool result = open();
	return result;
}

/******************************************************************************
* Save the calendar from memory to file.
* If a filename is specified, create a new calendar file.
*/
bool AlarmCalendar::saveCal(const TQString& newFile)
{
	if (!mCalendar || (!mOpen && newFile.isNull()))
		return false;

	kdDebug(5950) << "AlarmCalendar::saveCal(\"" << newFile << "\", " << mType << ")\n";
	TQString saveFilename = newFile.isNull() ? mLocalFile : newFile;
	if (mVCal  &&  newFile.isNull()  &&  mUrl.isLocalFile())
		saveFilename = mICalUrl.path();
	if (!mCalendar->save(saveFilename, new ICalFormat))
	{
		kdError(5950) << "AlarmCalendar::saveCal(" << saveFilename << "): failed.\n";
		KMessageBox::error(0, i18n("Failed to save calendar to\n'%1'").arg(mICalUrl.prettyURL()));
		return false;
	}

	if (!mICalUrl.isLocalFile())
	{
		if (!TDEIO::NetAccess::upload(saveFilename, mICalUrl, MainWindow::mainMainWindow()))
		{
			kdError(5950) << "AlarmCalendar::saveCal(" << saveFilename << "): upload failed.\n";
			KMessageBox::error(0, i18n("Cannot upload calendar to\n'%1'").arg(mICalUrl.prettyURL()));
			return false;
		}
	}

	if (mVCal)
	{
		// The file was in vCalendar format, but has now been saved in iCalendar format.
		// Save the change in the config file.
		if (!mConfigKey.isNull())
		{
			TDEConfig* config = tdeApp->config();
			config->setGroup(TQString::fromLatin1("General"));
			config->writePathEntry(mConfigKey, mICalUrl.path());
			config->sync();
		}
		mUrl  = mICalUrl;
		mVCal = false;
	}

	mUpdateSave = false;
	emit calendarSaved(this);
	return true;
}

/******************************************************************************
* Delete any temporary file at program exit.
*/
void AlarmCalendar::close()
{
	if (!mLocalFile.isEmpty())
	{
		TDEIO::NetAccess::removeTempFile(mLocalFile);   // removes it only if it IS a temporary file
		mLocalFile = "";
	}
	if (mCalendar)
	{
		mCalendar->close();
		delete mCalendar;
		mCalendar = 0;
	}
	mOpen = false;
}

/******************************************************************************
* Import alarms from an external calendar and merge them into KAlarm's calendar.
* The alarms are given new unique event IDs.
* Parameters: parent = parent widget for error message boxes
* Reply = true if all alarms in the calendar were successfully imported
*       = false if any alarms failed to be imported.
*/
bool AlarmCalendar::importAlarms(TQWidget* parent)
{
	KURL url = KFileDialog::getOpenURL(TQString::fromLatin1(":importalarms"),
	                                   TQString::fromLatin1("*.vcs *.ics|%1").arg(i18n("Calendar Files")), parent);
	if (url.isEmpty())
	{
		kdError(5950) << "AlarmCalendar::importAlarms(): Empty URL" << endl;
		return false;
	}
	if (!url.isValid())
	{
		kdDebug(5950) << "AlarmCalendar::importAlarms(): Invalid URL" << endl;
		return false;
	}
	kdDebug(5950) << "AlarmCalendar::importAlarms(" << url.prettyURL() << ")" << endl;

	bool success = true;
	TQString filename;
	bool local = url.isLocalFile();
	if (local)
	{
		filename = url.path();
		if (!TDEStandardDirs::exists(filename))
		{
			kdDebug(5950) << "AlarmCalendar::importAlarms(): File '" << url.prettyURL() << "' not found" << endl;
			KMessageBox::error(parent, i18n("Could not load calendar '%1'.").arg(url.prettyURL()));
			return false;
		}
	}
	else
	{
		if (!TDEIO::NetAccess::download(url, filename, MainWindow::mainMainWindow()))
		{
			kdError(5950) << "AlarmCalendar::importAlarms(): Download failure" << endl;
			KMessageBox::error(parent, i18n("Cannot download calendar:\n%1").arg(url.prettyURL()));
			return false;
		}
		kdDebug(5950) << "--- Downloaded to " << filename << endl;
	}

	// Read the calendar and add its alarms to the current calendars
	CalendarLocal cal(TQString::fromLatin1("UTC"));
	cal.setLocalTime();    // write out using local time (i.e. no time zone)
	success = cal.load(filename);
	if (!success)
	{
		kdDebug(5950) << "AlarmCalendar::importAlarms(): error loading calendar '" << filename << "'" << endl;
		KMessageBox::error(parent, i18n("Could not load calendar '%1'.").arg(url.prettyURL()));
	}
	else
	{
		CalendarCompat::fix(cal, filename);
		bool saveActive   = false;
		bool saveExpired  = false;
		bool saveTemplate = false;
		AlarmCalendar* active  = activeCalendar();
		AlarmCalendar* expired = expiredCalendar();
		AlarmCalendar* templat = 0;
		AlarmCalendar* acal;
		Event::List events = cal.rawEvents();
		for (Event::List::ConstIterator it = events.begin();  it != events.end();  ++it)
		{
			const Event* event = *it;
			if (event->alarms().isEmpty()  ||  !KAEvent(*event).valid())
				continue;    // ignore events without alarms, or usable alarms
			KAEvent::Status type = KAEvent::uidStatus(event->uid());
			switch (type)
			{
				case KAEvent::ACTIVE:
					acal = active;
					saveActive = true;
					break;
				case KAEvent::EXPIRED:
					acal = expired;
					saveExpired = true;
					break;
				case KAEvent::TEMPLATE:
					if (!templat)
						templat = templateCalendarOpen();
					acal = templat;
					saveTemplate = true;
					break;
				default:
					continue;
			}
			if (!acal)
				continue;

			Event* newev = new Event(*event);

			// If there is a display alarm without display text, use the event
			// summary text instead.
			if (type == KAEvent::ACTIVE  &&  !newev->summary().isEmpty())
			{
				const Alarm::List& alarms = newev->alarms();
				for (Alarm::List::ConstIterator ait = alarms.begin();  ait != alarms.end();  ++ait)
				{
					Alarm* alarm = *ait;
					if (alarm->type() == Alarm::Display  &&  alarm->text().isEmpty())
						alarm->setText(newev->summary());
				}
				newev->setSummary(TQString());   // KAlarm only uses summary for template names
			}
			// Give the event a new ID and add it to the calendar
			newev->setUid(KAEvent::uid(CalFormat::createUniqueId(), type));
			if (!acal->mCalendar->addEvent(newev))
				success = false;
		}

		// Save any calendars which have been modified
		if (saveActive)
			active->saveCal();
		if (saveExpired)
			expired->saveCal();
		if (saveTemplate)
			templat->saveCal();
	}
	if (!local)
		TDEIO::NetAccess::removeTempFile(filename);
	return success;
}

/******************************************************************************
* Flag the start of a group of calendar update calls.
* The purpose is to avoid multiple calendar saves during a group of operations.
*/
void AlarmCalendar::startUpdate()
{
	++mUpdateCount;
}

/******************************************************************************
* Flag the end of a group of calendar update calls.
* The calendar is saved if appropriate.
*/
bool AlarmCalendar::endUpdate()
{
	if (mUpdateCount > 0)
		--mUpdateCount;
	if (!mUpdateCount)
	{
		if (mUpdateSave)
			return saveCal();
	}
	return true;
}

/******************************************************************************
* Save the calendar, or flag it for saving if in a group of calendar update calls.
*/
bool AlarmCalendar::save()
{
	if (mUpdateCount)
	{
		mUpdateSave = true;
		return true;
	}
	else
		return saveCal();
}

#if 0
/******************************************************************************
* If it is VCal format, convert the calendar URL to ICal and save the new URL
* in the config file.
*/
void AlarmCalendar::convertToICal()
{
	if (mVCal)
	{
		if (!mConfigKey.isNull())
		{
			TDEConfig* config = tdeApp->config();
			config->setGroup(TQString::fromLatin1("General"));
			config->writePathEntry(mConfigKey, mICalUrl.path());
			config->sync();
		}
		mUrl  = mICalUrl;
		mVCal = false;
	}
}
#endif

/******************************************************************************
* Set the number of days to keep alarms.
* Alarms which are older are purged immediately, and at the start of each day.
*/
void AlarmCalendar::setPurgeDays(int days)
{
	if (days != mPurgeDays)
	{
		int oldDays = mPurgeDays;
		mPurgeDays = days;
		if (mPurgeDays <= 0)
			StartOfDayTimer::disconnect(this);
		if (oldDays < 0 || (days >= 0 && days < oldDays))
		{
			// Alarms are now being kept for less long, so purge them
			if (open())
				slotPurge();
		}
		else if (mPurgeDays > 0)
			startPurgeTimer();
	}
}

/******************************************************************************
* Called at the start of each day by the purge timer.
* Purge all events from the calendar whose end time is longer ago than 'mPurgeDays'.
*/
void AlarmCalendar::slotPurge()
{
	purge(mPurgeDays);
	startPurgeTimer();
}

/******************************************************************************
* Purge all events from the calendar whose end time is longer ago than
* 'daysToKeep'. All events are deleted if 'daysToKeep' is zero.
*/
void AlarmCalendar::purge(int daysToKeep)
{
	if (mPurgeDaysQueued < 0  ||  daysToKeep < mPurgeDaysQueued)
		mPurgeDaysQueued = daysToKeep;

	// Do the purge once any other current operations are completed
	theApp()->processQueue();
}

/******************************************************************************
* This method must only be called from the main KAlarm queue processing loop,
* to prevent asynchronous calendar operations interfering with one another.
*
* Purge all events from the calendar whose end time is longer ago than 'daysToKeep'.
* All events are deleted if 'daysToKeep' is zero.
* The calendar must already be open.
*/
void AlarmCalendar::purgeIfQueued()
{
	if (mPurgeDaysQueued >= 0)
	{
		if (open())
		{
			kdDebug(5950) << "AlarmCalendar::purgeIfQueued(" << mPurgeDaysQueued << ")\n";
			bool changed = false;
			TQDate cutoff = TQDate::currentDate().addDays(-mPurgeDaysQueued);
			Event::List events = mCalendar->rawEvents();
			for (Event::List::ConstIterator it = events.begin();  it != events.end();  ++it)
			{
				Event* kcalEvent = *it;
				if (!mPurgeDaysQueued  ||  kcalEvent->created().date() < cutoff)
				{
					mCalendar->deleteEvent(kcalEvent);
					changed = true;
				}
			}
			if (changed)
			{
				saveCal();
				emit purged();
			}
			mPurgeDaysQueued = -1;
		}
	}
}


/******************************************************************************
* Start the purge timer to expire at the start of the next day (using the user-
* defined start-of-day time).
*/
void AlarmCalendar::startPurgeTimer()
{
	if (mPurgeDays > 0)
		StartOfDayTimer::connect(this, TQ_SLOT(slotPurge()));
}

/******************************************************************************
* Add the specified event to the calendar.
* If it is the active calendar and 'useEventID' is false, a new event ID is
* created. In all other cases, the event ID is taken from 'event'.
* 'event' is updated with the actual event ID.
* Reply = the KCal::Event as written to the calendar.
*/
Event* AlarmCalendar::addEvent(KAEvent& event, bool useEventID)
{
	if (!mOpen)
		return 0;
	TQString id = event.id();
	Event* kcalEvent = new Event;
	if (mType == KAEvent::ACTIVE)
	{
		if (id.isEmpty())
			useEventID = false;
		if (!useEventID)
			event.setEventID(kcalEvent->uid());
	}
	else
	{
		if (id.isEmpty())
			id = kcalEvent->uid();
		useEventID = true;
	}
	if (useEventID)
	{
		id = KAEvent::uid(id, mType);
		event.setEventID(id);
		kcalEvent->setUid(id);
	}
	event.updateKCalEvent(*kcalEvent, false, (mType == KAEvent::EXPIRED), true);
	mCalendar->addEvent(kcalEvent);
	event.clearUpdated();
	return kcalEvent;
}

/******************************************************************************
* Update the specified event in the calendar with its new contents.
* The event retains the same ID.
*/
void AlarmCalendar::updateEvent(const KAEvent& evnt)
{
	if (mOpen)
	{
		Event* kcalEvent = event(evnt.id());
		if (kcalEvent)
		{
			evnt.updateKCalEvent(*kcalEvent);
			evnt.clearUpdated();
			if (mType == KAEvent::ACTIVE)
				Daemon::savingEvent(evnt.id());
			return;
		}
	}
	if (mType == KAEvent::ACTIVE)
		Daemon::eventHandled(evnt.id(), false);
}

/******************************************************************************
* Delete the specified event from the calendar, if it exists.
* The calendar is then optionally saved.
*/
bool AlarmCalendar::deleteEvent(const TQString& eventID, bool saveit)
{
	if (mOpen)
	{
		Event* kcalEvent = event(eventID);
		if (kcalEvent)
		{
			mCalendar->deleteEvent(kcalEvent);
			if (mType == KAEvent::ACTIVE)
				Daemon::savingEvent(eventID);
			if (saveit)
				return save();
			return true;
		}
	}
	if (mType == KAEvent::ACTIVE)
		Daemon::eventHandled(eventID, false);
	return false;
}

/******************************************************************************
* Emit a signal to indicate whether the calendar is empty.
*/
void AlarmCalendar::emitEmptyStatus()
{
	emit emptyStatus(events().isEmpty());
}

/******************************************************************************
* Return the event with the specified ID.
*/
KCal::Event* AlarmCalendar::event(const TQString& uniqueID)
{
	return mCalendar ?  mCalendar->event(uniqueID) : 0;
}

/******************************************************************************
* Return all events in the calendar which contain usable alarms.
*/
KCal::Event::List AlarmCalendar::events()
{
	if (!mCalendar)
		return KCal::Event::List();
	KCal::Event::List list = mCalendar->rawEvents();
	KCal::Event::List::Iterator it = list.begin();
	while (it != list.end())
	{
		KCal::Event* event = *it;
		if (event->alarms().isEmpty()  ||  !KAEvent(*event).valid())
			it = list.remove(it);
		else
			++it;
	}
	return list;
}

/******************************************************************************
* Return all events which have alarms falling within the specified time range.
*/
Event::List AlarmCalendar::eventsWithAlarms(const TQDateTime& from, const TQDateTime& to)
{
	kdDebug(5950) << "AlarmCalendar::eventsWithAlarms(" << from.toString() << " - " << to.toString() << ")\n";
	Event::List evnts;
	TQDateTime dt;
	Event::List allEvents = events();   // ignore events without usable alarms
	for (Event::List::ConstIterator it = allEvents.begin();  it != allEvents.end();  ++it)
	{
		Event* e = *it;
		bool recurs = e->doesRecur();
		int  endOffset = 0;
		bool endOffsetValid = false;
		const Alarm::List& alarms = e->alarms();
		for (Alarm::List::ConstIterator ait = alarms.begin();  ait != alarms.end();  ++ait)
		{
			Alarm* alarm = *ait;
			if (alarm->enabled())
			{
				if (recurs)
				{
					if (alarm->hasTime())
						dt = alarm->time();
					else
					{
						// The alarm time is defined by an offset from the event start or end time.
						// Find the offset from the event start time, which is also used as the
						// offset from the recurrence time.
						int offset = 0;
						if (alarm->hasStartOffset())
							offset = alarm->startOffset().asSeconds();
						else if (alarm->hasEndOffset())
						{
							if (!endOffsetValid)
							{
								endOffset = e->hasDuration() ? e->duration() : e->hasEndDate() ? e->dtStart().secsTo(e->dtEnd()) : 0;
								endOffsetValid = true;
							}
							offset = alarm->endOffset().asSeconds() + endOffset;
						}
						// Adjust the 'from' date/time and find the next recurrence at or after it
						TQDateTime pre = from.addSecs(-offset - 1);
						if (e->doesFloat()  &&  pre.time() < Preferences::startOfDay())
							pre = pre.addDays(-1);    // today's recurrence (if today recurs) is still to come
						dt = e->recurrence()->getNextDateTime(pre);
						if (!dt.isValid())
							continue;
						dt = dt.addSecs(offset);
					}
				}
				else
					dt = alarm->time();
				if (dt >= from  &&  dt <= to)
				{
					kdDebug(5950) << "AlarmCalendar::events() '" << e->summary()
					              << "': " << dt.toString() << endl;
					evnts.append(e);
					break;
				}
			}
		}
	}
	return evnts;
}
