/*
 *  alarmdaemon.cpp  -  alarm daemon control routines
 *  Program:  KAlarm's alarm daemon (kalarmd)
 *  Copyright © 2001,2004-2007 by David Jarvie <software@astrojar.org.uk>
 *  Based on the original, (c) 1998, 1999 Preston Brown
 *
 *  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 "kalarmd.h"

#include <unistd.h>
#include <stdlib.h>

#include <tqtimer.h>
#include <tqfile.h>
#include <tqdatetime.h>

#include <tdeapplication.h>
#include <kstandarddirs.h>
#include <kprocess.h>
#include <tdeio/netaccess.h>
#include <dcopclient.h>
#include <tdeconfig.h>
#include <kdebug.h>

#include <libkcal/calendarlocal.h>
#include <libkcal/icalformat.h>

#include "adcalendar.h"
#include "adconfigdata.h"
#include "alarmguiiface.h"
#include "alarmguiiface_stub.h"
#include "alarmdaemon.moc"


#ifdef AUTOSTART_KALARM
// Number of seconds to wait before autostarting KAlarm.
// Allow plenty of time for session restoration to happen first.
static const int KALARM_AUTOSTART_TIMEOUT = 30;
#endif
static const int SECS_PER_DAY = 3600 * 24;

// KAlarm config file keys
static const TQString START_OF_DAY(TQString::fromLatin1("StartOfDay"));
static const TQString AUTOSTART_TRAY(TQString::fromLatin1("AutostartTray"));


AlarmDaemon::AlarmDaemon(bool autostart, TQObject *parent, const char *name)
	: DCOPObject(name),
	  TQObject(parent, name),
	  mAlarmTimer(0)
{
	kdDebug(5900) << "AlarmDaemon::AlarmDaemon()" << endl;
	ADConfigData::readConfig();

	ADConfigData::enableAutoStart(true);    // switch autostart on whenever the program is run

	readKAlarmConfig();       // read time-related KAlarm config items

#ifdef AUTOSTART_KALARM
	if (autostart)
	{
		/* The alarm daemon has been autostarted.
		 * Check if also KAlarm needs to be autostarted (by the daemon) in the system tray.
		 * This should ideally be handled internally by KAlarm, but is done by kalarmd
		 * to correctly handle the cases when KAlarm is restored and when it is autostarted.
		 * If the autostart request comes before the restoring one, KAlarm would not know
		 * that it is supposed to restore itself and instead would simply open a new window.
		 * So we first check if the session has been fully restored by the session manager
		 * and if so we can continue safely. If the session hasn't yet been fully restored
		 * or created, we wait for up to 30 seconds and then continue as normal.
		 */
		TDEConfig kaconfig(locate("config", "kalarmrc"));
		kaconfig.setGroup(TQString::fromLatin1("General"));
		autostart = kaconfig.readBoolEntry(AUTOSTART_TRAY, false);
		if (autostart)
		{
			bool done = false;
			DCOPClient* client = kapp->dcopClient();
			if (client->isApplicationRegistered("ksmserver"))
			{
				TQByteArray callData;
				TQCString replyType;
				TQByteArray replyData;
				for (int i=0; !done && i<KALARM_AUTOSTART_TIMEOUT; ++i)
				{
					// Check if the session has been fully created/restored. If not, wait a little and try again
					if (!kapp->dcopClient()->call("ksmserver", "ksmserver", "startupCompleted()", callData, replyType, replyData) ||
							replyType != "bool")
					{
						done = true;	// In case of DCOP call error, just continue normally
					}
					else
					{
						bool result;
						TQDataStream replyStream(replyData, IO_ReadOnly);
						replyStream >> result;
						if (result)
						{
							done = true;	// Session created/restored ==> continue
						}
						else
						{
							sleep(1); // Session not yet fully created/restored ==> wait and retry
						}
					}
				}
			// Give some extra time to KAlarm to be fully restored, then proceed as usual
			TQTimer::singleShot(3000, this, TQ_SLOT(autostartKAlarm()));
			}
		}
	}
	if (!autostart)
#endif
		startMonitoring();    // otherwise, start monitoring alarms now
}

/******************************************************************************
* DCOP call to quit the program.
*/
void AlarmDaemon::quit()
{
	kdDebug(5900) << "AlarmDaemon::quit()" << endl;
	exit(0);
}

/******************************************************************************
* Called after a timer delay to autostart KAlarm in the system tray.
*/
void AlarmDaemon::autostartKAlarm()
{
#ifdef AUTOSTART_KALARM
	if (mAlarmTimer)
	{
		kdDebug(5900) << "AlarmDaemon::autostartKAlarm(): KAlarm already registered\n";
		return;    // KAlarm has already registered with us
	}
	kdDebug(5900) << "AlarmDaemon::autostartKAlarm(): starting KAlarm\n";
	TQStringList args;
	args << TQString::fromLatin1("--tray");
	int ret = TDEApplication::tdeinitExec(TQString::fromLatin1("kalarm"), args);
	if (ret)
		kdError(5900) << "AlarmDaemon::autostartKAlarm(): error=" << ret << endl;
	else
		kdDebug(5900) << "AlarmDaemon::autostartKAlarm(): success" << endl;

	startMonitoring();
#endif
}

/******************************************************************************
* Start monitoring alarms.
*/
void AlarmDaemon::startMonitoring()
{
	// Set up the alarm timer
	mAlarmTimer = new TQTimer(this);
	connect(mAlarmTimer, TQ_SIGNAL(timeout()), TQ_SLOT(checkAlarmsSlot()));
	setTimerStatus();

	// Start monitoring calendar files.
	// They are monitored until their client application registers, upon which
	// monitoring ceases until KAlarm tells the daemon to monitor it.
	checkAlarms();
}

/******************************************************************************
* DCOP call to enable or disable monitoring of a calendar.
*/
void AlarmDaemon::enableCal(const TQString& urlString, bool enable)
{
	kdDebug(5900) << "AlarmDaemon::enableCal(" << urlString << ")" << endl;
	ADCalendar* cal = ADCalendar::getCalendar(urlString);
	if (cal)
	{
		cal->setEnabled(enable);
		notifyCalStatus(cal);    // notify KAlarm
	}
}

/******************************************************************************
* DCOP call to reload, and optionally reset, the specified calendar.
*/
void AlarmDaemon::reloadCal(const TQCString& appname, const TQString& urlString, bool reset)
{
	kdDebug(5900) << "AlarmDaemon::reloadCal(" << urlString << ")" << endl;
	ADCalendar* cal = ADCalendar::getCalendar(urlString);
	if (!cal  ||  cal->appName() != appname)
		return;
	reloadCal(cal, reset);
}

/******************************************************************************
* Reload the specified calendar.
* If 'reset' is true, the data associated with the calendar is reset.
*/
void AlarmDaemon::reloadCal(ADCalendar* cal, bool reset)
{
	kdDebug(5900) << "AlarmDaemon::reloadCal(): calendar" << endl;
	if (!cal)
		return;
	if (!cal->downloading())
	{
		cal->close();
		if (!cal->setLoadedConnected())
			connect(cal, TQ_SIGNAL(loaded(ADCalendar*, bool)), TQ_SLOT(calendarLoaded(ADCalendar*, bool)));
		cal->loadFile(reset);
	}
	else if (reset)
		cal->clearEventsHandled();
}

void AlarmDaemon::calendarLoaded(ADCalendar* cal, bool success)
{
	if (success)
		kdDebug(5900) << "Calendar reloaded" << endl;
	notifyCalStatus(cal);       // notify KAlarm
	setTimerStatus();
	checkAlarms(cal);
}

/******************************************************************************
* DCOP call to notify the daemon that an event has been handled, and optionally
* to tell it to reload the calendar.
*/
void AlarmDaemon::eventHandled(const TQCString& appname, const TQString& calendarUrl, const TQString& eventID, bool reload)
{
	TQString urlString = expandURL(calendarUrl);
	kdDebug(5900) << "AlarmDaemon::eventHandled(" << urlString << (reload ? "): reload" : ")") << endl;
	ADCalendar* cal = ADCalendar::getCalendar(urlString);
	if (!cal  ||  cal->appName() != appname)
		return;
	cal->setEventHandled(eventID);
	if (reload)
		reloadCal(cal, false);
}

/******************************************************************************
* DCOP call to add an application to the list of client applications,
* and add it to the config file.
* N.B. This method must not return a bool because DCOPClient::call() can cause
*      a hang if the daemon happens to send a notification to KAlarm at the
*      same time as KAlarm calls this DCCOP method.
*/
void AlarmDaemon::registerApp(const TQCString& appName, const TQString& appTitle,
                              const TQCString& dcopObject, const TQString& calendarUrl,
			      bool startClient)
{
	kdDebug(5900) << "AlarmDaemon::registerApp(" << appName << ", " << appTitle << ", "
	              <<  dcopObject << ", " << startClient << ")" << endl;
	KAlarmd::RegisterResult result;
	if (appName.isEmpty())
		result = KAlarmd::FAILURE;
	else if (startClient  &&  TDEStandardDirs::findExe(appName).isNull())
	{
		kdError() << "AlarmDaemon::registerApp(): app not found" << endl;
		result = KAlarmd::NOT_FOUND;
	}
	else
	{
		ADCalendar* keepCal = 0;
		ClientInfo* client = ClientInfo::get(appName);
		if (client)
		{
			// The application is already a client.
			// If it's the same calendar file, don't delete its calendar object.
			if (client->calendar()  &&  client->calendar()->urlString() == calendarUrl)
			{
				keepCal = client->calendar();
				client->detachCalendar();
			}
			ClientInfo::remove(appName);    // this deletes the calendar if not detached
		}

		if (keepCal)
			client = new ClientInfo(appName, appTitle, dcopObject, keepCal, startClient);
		else
			client = new ClientInfo(appName, appTitle, dcopObject, calendarUrl, startClient);
		client->calendar()->setUnregistered(false);
		ADConfigData::writeClient(appName, client);

		ADConfigData::enableAutoStart(true);
		setTimerStatus();
		notifyCalStatus(client->calendar());
		result = KAlarmd::SUCCESS;
	}

	// Notify the client of whether the call succeeded.
	AlarmGuiIface_stub stub(appName, dcopObject);
	stub.registered(false, result, DAEMON_VERSION_NUM);
	kdDebug(5900) << "AlarmDaemon::registerApp() -> " << result << endl;
}

/******************************************************************************
* DCOP call to change whether KAlarm should be started when an event needs to
* be notified to it.
* N.B. This method must not return a bool because DCOPClient::call() can cause
*      a hang if the daemon happens to send a notification to KAlarm at the
*      same time as KAlarm calls this DCCOP method.
*/
void AlarmDaemon::registerChange(const TQCString& appName, bool startClient)
{
	kdDebug(5900) << "AlarmDaemon::registerChange(" << appName << ", " << startClient << ")" << endl;
	KAlarmd::RegisterResult result;
	ClientInfo* client = ClientInfo::get(appName);
	if (!client)
		return;    // can't access client to tell it the result
	if (startClient  &&  TDEStandardDirs::findExe(appName).isNull())
	{
		kdError() << "AlarmDaemon::registerChange(): app not found" << endl;
		result = KAlarmd::NOT_FOUND;
	}
	else
	{
		client->setStartClient(startClient);
		ADConfigData::writeClient(appName, client);
		result = KAlarmd::SUCCESS;
	}

	// Notify the client of whether the call succeeded.
	AlarmGuiIface_stub stub(appName, client->dcopObject());
	stub.registered(true, result, DAEMON_VERSION_NUM);
	kdDebug(5900) << "AlarmDaemon::registerChange() -> " << result << endl;
}

/******************************************************************************
* DCOP call to set autostart at login on or off.
*/
void AlarmDaemon::enableAutoStart(bool on)
{
	ADConfigData::enableAutoStart(on);
}

/******************************************************************************
* Check if any alarms are pending for any enabled calendar, and display the
* pending alarms.
* Called by the alarm timer.
*/
void AlarmDaemon::checkAlarmsSlot()
{
	kdDebug(5901) << "AlarmDaemon::checkAlarmsSlot()" << endl;
	if (mAlarmTimerSyncing)
	{
		// We've synched to the minute boundary. Now set timer to the check interval.
		mAlarmTimer->changeInterval(DAEMON_CHECK_INTERVAL * 1000);
		mAlarmTimerSyncing = false;
		mAlarmTimerSyncCount = 10;    // resynch every 10 minutes, in case of glitches
	}
	else if (--mAlarmTimerSyncCount <= 0)
	{
		int interval = DAEMON_CHECK_INTERVAL + 1 - TQTime::currentTime().second();
		if (interval < DAEMON_CHECK_INTERVAL - 1)
		{
			// Need to re-synch to 1 second past the minute
			mAlarmTimer->changeInterval(interval * 1000);
			mAlarmTimerSyncing = true;
			kdDebug(5900) << "Resynching alarm timer" << endl;
		}
		else
			mAlarmTimerSyncCount = 10;
	}
	checkAlarms();
}

/******************************************************************************
* Check if any alarms are pending for any enabled calendar, and display the
* pending alarms.
*/
void AlarmDaemon::checkAlarms()
{
	kdDebug(5901) << "AlarmDaemon::checkAlarms()" << endl;
	for (ADCalendar::ConstIterator it = ADCalendar::begin();  it != ADCalendar::end();  ++it)
		checkAlarms(*it);
}

/******************************************************************************
* Check if any alarms are pending for a specified calendar, and display the
* pending alarms.
*/
void AlarmDaemon::checkAlarms(ADCalendar* cal)
{
	kdDebug(5901) << "AlarmDaemons::checkAlarms(" << cal->urlString() << ")" << endl;
	if (!cal->loaded()  ||  !cal->enabled())
		return;

	TQDateTime now  = TQDateTime::currentDateTime();
	kdDebug(5901) << "  To: " << now.toString() << endl;
	TQValueList<KCal::Alarm*> alarms = cal->alarmsTo(now);
	if (!alarms.count())
		return;
	TQValueList<KCal::Event*> eventsDone;
	for (TQValueList<KCal::Alarm*>::ConstIterator it = alarms.begin();  it != alarms.end();  ++it)
	{
		KCal::Event* event = dynamic_cast<KCal::Event*>((*it)->parent());
		if (!event  ||  eventsDone.find(event) != eventsDone.end())
			continue;   // either not an event, or the event has already been processed
		eventsDone += event;
		const TQString& eventID = event->uid();
		kdDebug(5901) << "AlarmDaemon::checkAlarms(): event " << eventID  << endl;

		// Check which of the alarms for this event are due.
		// The times in 'alarmtimes' corresponding to due alarms are set.
		// The times for non-due alarms are set invalid in 'alarmtimes'.
		bool recurs = event->doesRecur();
		const TQStringList cats = event->categories();
		bool floats = (cats.find(TQString::fromLatin1("DATE")) != cats.end());
		TQDateTime nextDateTime = event->dtStart();
		if (recurs)
		{
			TQString prop = event->customProperty("KALARM", "NEXTRECUR");
			if (prop.length() >= 8)
			{
				// The next due recurrence time is specified
				TQDate d(prop.left(4).toInt(), prop.mid(4,2).toInt(), prop.mid(6,2).toInt());
				if (d.isValid())
				{
					if (floats  &&  prop.length() == 8)
						nextDateTime = d;
					else if (!floats  &&  prop.length() == 15  &&  prop[8] == TQChar('T'))
					{
						TQTime t(prop.mid(9,2).toInt(), prop.mid(11,2).toInt(), prop.mid(13,2).toInt());
						if (t.isValid())
							nextDateTime = TQDateTime(d, t);
					}
				}
			}
		}
		if (floats)
			nextDateTime.setTime(mStartOfDay);
		TQValueList<TQDateTime> alarmtimes;
		KCal::Alarm::List alarms = event->alarms();
		for (KCal::Alarm::List::ConstIterator al = alarms.begin();  al != alarms.end();  ++al)
		{
			KCal::Alarm* alarm = *al;
			TQDateTime dt;
			if (alarm->enabled())
			{
				TQDateTime dt1;
				if (!alarm->hasTime())
				{
					// Find the latest recurrence for the alarm.
					// Need to do this for alarms with offsets in order to detect
					// reminders due for recurrences.
					int offset = alarm->hasStartOffset() ? alarm->startOffset().asSeconds()
					           : alarm->endOffset().asSeconds() + event->dtStart().secsTo(event->dtEnd());
					if (offset)
					{
						dt1 = nextDateTime.addSecs(floats ? (offset/SECS_PER_DAY)*SECS_PER_DAY : offset);
						if (dt1 > now)
							dt1 = TQDateTime();
					}
				}
				// Get latest due repetition, or the recurrence time if none
				dt = nextDateTime;
				if (nextDateTime <= now  &&  alarm->repeatCount() > 0)
				{
					int snoozeSecs = alarm->snoozeTime() * 60;
					int offset = alarm->repeatCount() * snoozeSecs;
					TQDateTime lastRepetition = nextDateTime.addSecs(floats ? (offset/SECS_PER_DAY)*SECS_PER_DAY : offset);
					if (lastRepetition <= now)
						dt = lastRepetition;
					else
					{
						int repetition = nextDateTime.secsTo(now) / snoozeSecs;
						int offset = repetition * snoozeSecs;
						dt = nextDateTime.addSecs(floats ? (offset/SECS_PER_DAY)*SECS_PER_DAY : offset);
					}
				}
				if (!dt.isValid()  ||  dt > now
				||  (dt1.isValid()  &&  dt1 > dt))  // already tested dt1 <= now
					dt = dt1;
			}
			alarmtimes.append(dt);
		}
		if (!cal->eventHandled(event, alarmtimes))
		{
			if (notifyEvent(cal, eventID))
				cal->setEventPending(event, alarmtimes);
		}
	}
}

/******************************************************************************
* Send a DCOP message to KAlarm telling it that an alarm should now be handled.
* Reply = false if the event should be held pending until KAlarm can be started.
*/
bool AlarmDaemon::notifyEvent(ADCalendar* calendar, const TQString& eventID)
{
	if (!calendar)
		return true;
	TQCString appname = calendar->appName();
	const ClientInfo* client = ClientInfo::get(appname);
	if (!client)
	{
		kdDebug(5900) << "AlarmDaemon::notifyEvent(" << appname << "): unknown client" << endl;
		return false;
	}
	kdDebug(5900) << "AlarmDaemon::notifyEvent(" << appname << ", " << eventID << "): notification type=" << client->startClient() << endl;
	TQString id = TQString::fromLatin1("ad:") + eventID;    // prefix to indicate that the notification if from the daemon

	// Check if the client application is running and ready to receive notification
	bool registered = kapp->dcopClient()->isApplicationRegistered(static_cast<const char*>(appname));
	bool ready = registered;
	if (registered)
	{
		// It's running, but check if it has created our DCOP interface yet
		QCStringList objects = kapp->dcopClient()->remoteObjects(appname);
		if (objects.find(client->dcopObject()) == objects.end())
			ready = false;
	}
	if (!ready)
	{
		// KAlarm is not running, or is not yet ready to receive notifications.
		if (!client->startClient())
		{
			if (registered)
				kdDebug(5900) << "AlarmDaemon::notifyEvent(): client not ready\n";
			else
				kdDebug(5900) << "AlarmDaemon::notifyEvent(): don't start client\n";
			return false;
		}

		// Start KAlarm, using the command line to specify the alarm
		TDEProcess p;
		TQString cmd = locate("exe", appname);
		if (cmd.isEmpty())
		{
			kdDebug(5900) << "AlarmDaemon::notifyEvent(): '" << appname << "' not found" << endl;
			return true;
		}
		p << cmd;
		p << "--handleEvent" << id << "--calendarURL" << calendar->urlString();
		p.start(TDEProcess::DontCare);
		kdDebug(5900) << "AlarmDaemon::notifyEvent(): used command line" << endl;
		return true;
	}

	// Notify the client by telling it the calendar URL and event ID
	AlarmGuiIface_stub stub(appname, client->dcopObject());
	stub.handleEvent(calendar->urlString(), id);
	if (!stub.ok())
	{
		kdDebug(5900) << "AlarmDaemon::notifyEvent(): dcop send failed" << endl;
		return false;
	}
	return true;
}

/******************************************************************************
* Starts or stops the alarm timer as necessary after a calendar is enabled/disabled.
*/
void AlarmDaemon::setTimerStatus()
{
#ifdef AUTOSTART_KALARM
	if (!mAlarmTimer)
	{
		// KAlarm is now running, so start monitoring alarms
		startMonitoring();
		return;    // startMonitoring() calls this method
	}

#endif
	// Count the number of currently loaded calendars whose names should be displayed
	int nLoaded = 0;
	for (ADCalendar::ConstIterator it = ADCalendar::begin();  it != ADCalendar::end();  ++it)
		if ((*it)->loaded())
			++nLoaded;

	// Start or stop the alarm timer if necessary
	if (!mAlarmTimer->isActive()  &&  nLoaded)
	{
		// Timeout every minute.
		// But first synchronise to one second after the minute boundary.
		int firstInterval = DAEMON_CHECK_INTERVAL + 1 - TQTime::currentTime().second();
		mAlarmTimer->start(1000 * firstInterval);
		mAlarmTimerSyncing = (firstInterval != DAEMON_CHECK_INTERVAL);
		kdDebug(5900) << "Started alarm timer" << endl;
	}
	else if (mAlarmTimer->isActive()  &&  !nLoaded)
	{
		mAlarmTimer->stop();
		kdDebug(5900) << "Stopped alarm timer" << endl;
	}
}

/******************************************************************************
* Send a DCOP message to to the client which owns the specified calendar,
* notifying it of a change in calendar status.
*/
void AlarmDaemon::notifyCalStatus(const ADCalendar* cal)
{
	ClientInfo* client = ClientInfo::get(cal);
	if (!client)
		return;
	TQCString appname = client->appName();
	if (kapp->dcopClient()->isApplicationRegistered(static_cast<const char*>(appname)))
	{
		KAlarmd::CalendarStatus change = cal->available() ? (cal->enabled() ? KAlarmd::CALENDAR_ENABLED : KAlarmd::CALENDAR_DISABLED)
		                                                  : KAlarmd::CALENDAR_UNAVAILABLE;
		kdDebug(5900) << "AlarmDaemon::notifyCalStatus() sending:" << appname << " -> " << change << endl;
		AlarmGuiIface_stub stub(appname, client->dcopObject());
		stub.alarmDaemonUpdate(change, cal->urlString());
		if (!stub.ok())
			kdError(5900) << "AlarmDaemon::notifyCalStatus(): dcop send failed:" << appname << endl;
	}
}

/******************************************************************************
* Read all relevant items from KAlarm config.
* Executed on DCOP call to notify a time related value change in the KAlarm
* config file.
*/
void AlarmDaemon::readKAlarmConfig()
{
	TDEConfig config(locate("config", "kalarmrc"));
	config.setGroup(TQString::fromLatin1("General"));
	TQDateTime defTime(TQDate(1900,1,1), TQTime());
	mStartOfDay = config.readDateTimeEntry(START_OF_DAY, &defTime).time();
	kdDebug(5900) << "AlarmDaemon::readKAlarmConfig()" << endl;
}

/******************************************************************************
* Expand a DCOP call parameter URL to a full URL.
* (We must store full URLs in the calendar data since otherwise later calls to
* reload or remove calendars won't necessarily find a match.)
*/
TQString AlarmDaemon::expandURL(const TQString& urlString)
{
	if (urlString.isEmpty())
		return TQString();
	return KURL(urlString).url();
}
