/*
 *  commandoptions.cpp  -  extract command line options
 *  Program:  kalarm
 *  Copyright © 2001-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 "commandoptions.h"
#include "functions.h"
#include "kalarmapp.h"
#include "kamail.h"
#include "identities.h"

#include <kcmdlineargs.h>

#include <iostream>

static bool convInterval(const QByteArray& timeParam, KARecurrence::Type&, int& timeInterval, bool allowMonthYear = false);

void CommandOptions::setError(const QString& error)
{
	if (mError.isEmpty())
		mError = error;
}

CommandOptions::CommandOptions()
	: mCommand(NONE),
	  mEditActionSet(false),
	  mRecurrence(0),
	  mRepeatCount(0),
	  mRepeatInterval(0),
	  mLateCancel(0),
	  mBgColour(Preferences::defaultBgColour()),
	  mFgColour(Preferences::defaultFgColour()),
	  mReminderMinutes(0),
	  mAudioVolume(-1),
	  mFromID(0),
	  mFlags(KAEvent::DEFAULT_FONT)
{
	mArgs = KCmdLineArgs::parsedArgs();
#ifndef NDEBUG
	if (mArgs->isSet("test-set-time"))
	{
		QString time = mArgs->getOption("test-set-time");
		if (!KAlarm::convertTimeString(time.toLatin1(), mSimulationTime, KDateTime::realCurrentLocalDateTime(), true))
			setErrorParameter("--test-set-time");
	}
#endif
	if (checkCommand("tray", TRAY))
	{
	}
	if (checkCommand("triggerEvent", TRIGGER_EVENT))
	{
		mEventId = mArgs->getOption(mCommandName);
	}
	if (checkCommand("cancelEvent", CANCEL_EVENT))
	{
		mEventId = mArgs->getOption(mCommandName);
	}
	if (checkCommand("edit", EDIT))
	{
		mEventId = mArgs->getOption(mCommandName);
	}
	if (checkCommand("edit-new-preset", EDIT_NEW_PRESET))
	{
		mTemplateName = mArgs->getOption(mCommandName);
	}
	if (checkCommand("file", NEW))
	{
		mEditType      = EditAlarmDlg::DISPLAY;
		mEditAction    = KAEvent::FILE;
		mEditActionSet = true;
		mText          = mArgs->getOption(mCommandName);
	}
	if (checkCommand("exec-display", NEW))
	{
		mEditType      = EditAlarmDlg::DISPLAY;
		mEditAction    = KAEvent::COMMAND;
		mEditActionSet = true;
		mFlags        |= KAEvent::DISPLAY_COMMAND;
		mText          = mArgs->getOption(mCommandName);
		int n = mArgs->count();
		for (int i = 0;  i < n;  ++i)
		{
			mText += ' ';
			mText += mArgs->arg(i);
		}
	}
	if (checkCommand("exec", NEW))
	{
		mEditType      = EditAlarmDlg::COMMAND;
		mEditAction    = KAEvent::COMMAND;
		mEditActionSet = true;
		mText          = mArgs->getOption(mCommandName);
		int n = mArgs->count();
		for (int i = 0;  i < n;  ++i)
		{
			mText += ' ';
			mText += mArgs->arg(i);
		}
	}
	if (checkCommand("mail", NEW))
	{
		mEditType      = EditAlarmDlg::EMAIL;
		mEditAction    = KAEvent::EMAIL;
		mEditActionSet = true;
	}
	if (checkCommand("edit-new-display", EDIT_NEW, EditAlarmDlg::DISPLAY))
	{
		mEditType = EditAlarmDlg::DISPLAY;
		if (!mEditActionSet  ||  (mEditAction != KAEvent::COMMAND && mEditAction != KAEvent::FILE))
		{
			mEditAction    = KAEvent::MESSAGE;
			mEditActionSet = true;
		}
		if (mArgs->count())
			mText = mArgs->arg(0);
	}
	if (checkCommand("edit-new-command", EDIT_NEW))
	{
		mEditType      = EditAlarmDlg::COMMAND;
		mEditAction    = KAEvent::COMMAND;
		mEditActionSet = true;
	}
	if (checkCommand("edit-new-email", EDIT_NEW, EditAlarmDlg::EMAIL))
	{
		mEditType      = EditAlarmDlg::EMAIL;
		mEditAction    = KAEvent::EMAIL;
		mEditActionSet = true;
	}
	if (mError.isEmpty()  &&  mCommand == NONE  &&  mArgs->count())
	{
		kDebug() << "Message";
		mCommand       = NEW;
		mCommandName   = "message";
		mEditType      = EditAlarmDlg::DISPLAY;
		mEditAction    = KAEvent::MESSAGE;
		mEditActionSet = true;
		if (mArgs->count())
			mText = mArgs->arg(0);
	}
	if (mEditAction == KAEvent::EMAIL)
	{
		if (mArgs->isSet("subject"))
			mSubject = mArgs->getOption("subject");
		if (mArgs->isSet("from-id"))
			mFromID = Identities::identityUoid(mArgs->getOption("from-id"));
		QStringList params = mArgs->getOptionList("mail");
		for (int i = 0, count = params.count();  i < count;  ++i)
		{
			QString addr = params[i];
			if (!KAMail::checkAddress(addr))
				setError(i18nc("@info:shell", "<icode>%1</icode>: invalid email address", QLatin1String("--mail")));
			mAddressees += KCal::Person(QString(), addr);
		}
		params = mArgs->getOptionList("attach");
		for (int i = 0, count = params.count();  i < count;  ++i)
			mAttachments += params[i];
		if (mArgs->count())
			mText = mArgs->arg(0);
	}

	// Check that other options are only specified for the
	// correct main command options.
	checkEditType(EditAlarmDlg::DISPLAY, "color");
	checkEditType(EditAlarmDlg::DISPLAY, "colorfg");
	checkEditType(EditAlarmDlg::DISPLAY, "play");
	checkEditType(EditAlarmDlg::DISPLAY, "play-repeat");
	checkEditType(EditAlarmDlg::DISPLAY, "volume");
	checkEditType(EditAlarmDlg::DISPLAY, "speak");
	checkEditType(EditAlarmDlg::DISPLAY, "beep");
	checkEditType(EditAlarmDlg::DISPLAY, "reminder");
	checkEditType(EditAlarmDlg::DISPLAY, "reminder-once");
	checkEditType(EditAlarmDlg::DISPLAY, "ack-confirm");
	checkEditType(EditAlarmDlg::DISPLAY, "auto-close");
	checkEditType(EditAlarmDlg::EMAIL, "subject");
	checkEditType(EditAlarmDlg::EMAIL, "from-id");
	checkEditType(EditAlarmDlg::EMAIL, "attach");
	checkEditType(EditAlarmDlg::EMAIL, "bcc");

	switch (mCommand)
	{
		case EDIT_NEW:
			if (mArgs->isSet("disable"))
				setErrorIncompatible("--disable", mCommandName);
			// Fall through to NEW
		case NEW:
		{
			// Display a message or file, execute a command, or send an email
			if (mArgs->isSet("color"))
			{
				// Background colour is specified
				QString colourText = mArgs->getOption("color");
				if (colourText[0] == '0' && colourText[1].toLower() == 'x')
					colourText.replace(0, 2, "#");
				mBgColour.setNamedColor(colourText);
				if (!mBgColour.isValid())
					setErrorParameter("--color");
			}
			if (mArgs->isSet("colorfg"))
			{
				// Foreground colour is specified
				QString colourText = mArgs->getOption("colorfg");
				if (colourText[0] == '0' && colourText[1].toLower() == 'x')
					colourText.replace(0, 2, "#");
				mFgColour.setNamedColor(colourText);
				if (!mFgColour.isValid())
					setErrorParameter("--colorfg");
			}

			if (mArgs->isSet("time"))
			{
				QByteArray dateTime = mArgs->getOption("time").toLocal8Bit();
				if (!KAlarm::convertTimeString(dateTime, mAlarmTime))
					setErrorParameter("--time");
			}
			else
				mAlarmTime = KDateTime::currentLocalDateTime();

			bool haveRecurrence = mArgs->isSet("recurrence");
			if (haveRecurrence)
			{
				if (mArgs->isSet("login"))
					setErrorIncompatible("--login", "--recurrence");
				else if (mArgs->isSet("until"))
					setErrorIncompatible("--until", "--recurrence");
				QString rule = mArgs->getOption("recurrence");
				mRecurrence = new KARecurrence;
				mRecurrence->set(rule);
			}
			if (mArgs->isSet("interval"))
			{
				// Repeat count is specified
				int count = 0;
				KDateTime endTime;
				if (mArgs->isSet("login"))
					setErrorIncompatible("--login", "--interval");
				bool ok;
				if (mArgs->isSet("repeat"))
				{
					count = mArgs->getOption("repeat").toInt(&ok);
					if (!ok || !count || count < -1 || (count < 0 && haveRecurrence))
						setErrorParameter("--repeat");
				}
				else if (haveRecurrence)
					setErrorRequires("--interval", "--repeat");
				else if (mArgs->isSet("until"))
				{
					count = 0;
					QByteArray dateTime = mArgs->getOption("until").toLocal8Bit();
					bool ok;
					if (mArgs->isSet("time"))
						ok = KAlarm::convertTimeString(dateTime, endTime, mAlarmTime);
					else
						ok = KAlarm::convertTimeString(dateTime, endTime);
					if (!ok)
						setErrorParameter("--until");
					else if (mAlarmTime.isDateOnly()  &&  !endTime.isDateOnly())
						setError(i18nc("@info:shell", "Invalid <icode>%1</icode> parameter for date-only alarm", QLatin1String("--until")));
					if (!mAlarmTime.isDateOnly()  &&  endTime.isDateOnly())
						endTime.setTime(QTime(23,59,59));
					if (endTime < mAlarmTime)
						setError(i18nc("@info:shell", "<icode>%1</icode> earlier than <icode>%2</icode>", QLatin1String("--until"), QLatin1String("--time")));
				}
				else
					count = -1;

				// Get the recurrence interval
				int interval;
				KARecurrence::Type recurType;
				if (!convInterval(mArgs->getOption("interval").toLocal8Bit(), recurType, interval, !haveRecurrence)
				||  interval < 0)
					setErrorParameter("--interval");
				else if (mAlarmTime.isDateOnly()  &&  recurType == KARecurrence::MINUTELY)
					setError(i18nc("@info:shell", "Invalid <icode>%1</icode> parameter for date-only alarm", QLatin1String("--interval")));

				if (haveRecurrence)
				{
					if (mRecurrence)
					{
						// There is a also a recurrence specified, so set up a sub-repetition
						int longestInterval = mRecurrence->longestInterval();
						if (count * interval > longestInterval)
							setError(i18nc("@info:shell", "Invalid <icode>%1</icode> and <icode>%2</icode> parameters: repetition is longer than <icode>%3</icode> interval",
							               QLatin1String("--interval"), QLatin1String("--repeat"), QLatin1String("--recurrence")));
						mRepeatCount    = count;
						mRepeatInterval = interval;
					}
				}
				else
				{
					// There is no other recurrence specified, so convert the repetition
					// parameters into a KCal::Recurrence
					mRecurrence = new KARecurrence;
					mRecurrence->set(recurType, interval, count, mAlarmTime, endTime);
				}
			}
			else
			{
				if (mArgs->isSet("repeat"))
					setErrorRequires("--repeat", "--interval");
				else if (mArgs->isSet("until"))
					setErrorRequires("--until", "--interval");
			}

			bool audioRepeat = mArgs->isSet("play-repeat");
			if (audioRepeat  ||  mArgs->isSet("play"))
			{
				// Play a sound with the alarm
				const char* opt = audioRepeat ? "--play-repeat" : "--play";
				if (audioRepeat  &&  mArgs->isSet("play"))
					setErrorIncompatible("--play", "--play-repeat");
				if (mArgs->isSet("beep"))
					setErrorIncompatible("--beep", opt);
				else if (mArgs->isSet("speak"))
					setErrorIncompatible("--speak", opt);
				mAudioFile = mArgs->getOption(audioRepeat ? "play-repeat" : "play");
				if (mArgs->isSet("volume"))
				{
					bool ok;
					int volumepc = mArgs->getOption("volume").toInt(&ok);
					if (!ok  ||  volumepc < 0  ||  volumepc > 100)
						setErrorParameter("--volume");
					mAudioVolume = static_cast<float>(volumepc) / 100;
				}
			}
			else if (mArgs->isSet("volume"))
				setErrorRequires("--volume", "--play", "--play-repeat");
			if (mArgs->isSet("speak"))
			{
				if (mArgs->isSet("beep"))
					setErrorIncompatible("--beep", "--speak");
				else if (!theApp()->speechEnabled())
					setError(i18nc("@info:shell", "<icode>%1</icode> requires speech synthesis to be configured using KTTSD", QLatin1String("--speak")));
			}
			bool onceOnly = mArgs->isSet("reminder-once");
			if (mArgs->isSet("reminder")  ||  onceOnly)
			{
				// Issue a reminder alarm in advance of the main alarm
				if (onceOnly  &&  mArgs->isSet("reminder"))
					setErrorIncompatible("--reminder", "--reminder-once");
				const char* opt = onceOnly ? "--reminder-once" : "--reminder";
				KARecurrence::Type recurType;
				QString optval = mArgs->getOption(onceOnly ? "reminder-once" : "reminder");
				if (!convInterval(mArgs->getOption(onceOnly ? "reminder-once" : "reminder").toLocal8Bit(), recurType, mReminderMinutes))
					setErrorParameter(opt);
				else if (recurType == KARecurrence::MINUTELY  &&  mAlarmTime.isDateOnly())
					setError(i18nc("@info:shell", "Invalid <icode>%1</icode> parameter for date-only alarm", QLatin1String(opt)));
				if (onceOnly)
					mReminderMinutes = -mReminderMinutes;
			}

			if (mArgs->isSet("late-cancel"))
			{
				KARecurrence::Type recurType;
				bool ok = convInterval(mArgs->getOption("late-cancel").toLocal8Bit(), recurType, mLateCancel);
				if (!ok  ||  mLateCancel <= 0)
					setErrorParameter("--late-cancel");
			}
			else if (mArgs->isSet("auto-close"))
				setErrorRequires("--auto-close", "--late-cancel");

			if (mArgs->isSet("ack-confirm"))
				mFlags |= KAEvent::CONFIRM_ACK;
			if (mArgs->isSet("auto-close"))
				mFlags |= KAEvent::AUTO_CLOSE;
			if (mArgs->isSet("beep"))
				mFlags |= KAEvent::BEEP;
			if (mArgs->isSet("speak"))
				mFlags |= KAEvent::SPEAK;
			if (mArgs->isSet("korganizer"))
				mFlags |= KAEvent::COPY_KORGANIZER;
			if (mArgs->isSet("disable"))
				mFlags |= KAEvent::DISABLED;
			if (audioRepeat)
				mFlags |= KAEvent::REPEAT_SOUND;
			if (mArgs->isSet("login"))
				mFlags |= KAEvent::REPEAT_AT_LOGIN;
			if (mArgs->isSet("bcc"))
				mFlags |= KAEvent::EMAIL_BCC;
			if (mAlarmTime.isDateOnly())
				mFlags |= KAEvent::ANY_TIME;
			break;
		}
		case NONE:
			// No arguments - run interactively & display the main window
			if (!mError.isEmpty())
				break;
			kDebug() << "Interactive";
			if (mArgs->isSet("ack-confirm"))
				mError += QLatin1String("--ack-confirm ");
			if (mArgs->isSet("attach"))
				mError += QLatin1String("--attach ");
			if (mArgs->isSet("auto-close"))
				mError += QLatin1String("--auto-close ");
			if (mArgs->isSet("bcc"))
				mError += QLatin1String("--bcc ");
			if (mArgs->isSet("beep"))
				mError += QLatin1String("--beep ");
			if (mArgs->isSet("color"))
				mError += QLatin1String("--color ");
			if (mArgs->isSet("colorfg"))
				mError += QLatin1String("--colorfg ");
			if (mArgs->isSet("disable"))
				mError += QLatin1String("--disable ");
			if (mArgs->isSet("from-id"))
				mError += QLatin1String("--from-id ");
			if (mArgs->isSet("korganizer"))
				mError += QLatin1String("--korganizer ");
			if (mArgs->isSet("late-cancel"))
				mError += QLatin1String("--late-cancel ");
			if (mArgs->isSet("login"))
				mError += QLatin1String("--login ");
			if (mArgs->isSet("play"))
				mError += QLatin1String("--play ");
			if (mArgs->isSet("play-repeat"))
				mError += QLatin1String("--play-repeat ");
			if (mArgs->isSet("reminder"))
				mError += QLatin1String("--reminder ");
			if (mArgs->isSet("reminder-once"))
				mError += QLatin1String("--reminder-once ");
			if (mArgs->isSet("speak"))
				mError += QLatin1String("--speak ");
			if (mArgs->isSet("subject"))
				mError += QLatin1String("--subject ");
			if (mArgs->isSet("time"))
				mError += QLatin1String("--time ");
			if (mArgs->isSet("volume"))
				mError += QLatin1String("--volume ");
			if (!mError.isEmpty())
				mError += i18nc("@info:shell", ": option(s) only valid with an appropriate action option or message");
			break;
		default:
			break;
	}

	mArgs->clear();      // free up memory

	if (!mError.isEmpty())
	{
		printError(mError);
		mCommand = CMD_ERROR;
	}
}

void CommandOptions::printError(const QString& errmsg)
{
	// Note: we can't use mArgs->usage() since that also quits any other
	// running 'instances' of the program.
	std::cerr << errmsg.toLocal8Bit().data()
		  << i18nc("@info:shell", "\nUse --help to get a list of available command line options.\n").toLocal8Bit().data();
}

/******************************************************************************
* Check if the given command option is specified, and if so set mCommand etc.
* If another command option has also been detected, issue an error.
* If 'allowedEditType' is set, supersede any previous specification of that
* edit type with the given command option - this allows, e.g., --mail to be
* used along with --edit-new-email so the user can specify addressees.
*/
bool CommandOptions::checkCommand(const QByteArray& command, Command code, EditAlarmDlg::Type allowedEditType)
{
	if (!mError.isEmpty()
	||  !mArgs->isSet(command))
		return false;
	if (mCommand != NONE
	&&  (allowedEditType == EditAlarmDlg::NO_TYPE
	  || (allowedEditType != EditAlarmDlg::NO_TYPE  &&  (mCommand != NEW || mEditType != allowedEditType))))
		setErrorIncompatible(mCommandName, "--" + command);
	kDebug().nospace() << " --" << command;
	mCommand = code;
	mCommandName = command;
	return true;
}

// Set the error message to "--opt requires --opt2" or "--opt requires --opt2 or --opt3".
void CommandOptions::setErrorRequires(const char* opt, const char* opt2, const char* opt3)
{
	if (!opt3)
		setError(i18nc("@info:shell", "<icode>%1</icode> requires <icode>%2</icode>", QLatin1String(opt), QLatin1String(opt2)));
	else
		setError(i18nc("@info:shell", "<icode>%1</icode> requires <icode>%2</icode> or <icode>%3</icode>", QLatin1String(opt), QLatin1String(opt2), QLatin1String(opt3)));
}

void CommandOptions::setErrorParameter(const char* opt)
{
	setError(i18nc("@info:shell", "Invalid <icode>%1</icode> parameter", QLatin1String(opt)));
}

void CommandOptions::setErrorIncompatible(const QByteArray& opt1, const QByteArray& opt2)
{
	QByteArray o1 = opt1;
	if (!opt1.startsWith("--")  &&  opt1 != "message")
		o1.prepend("--");
	QByteArray o2 = opt2;
	if (!opt2.startsWith("--")  &&  opt2 != "message")
		o2.prepend("--");
	setError(i18nc("@info:shell", "<icode>%1</icode> incompatible with <icode>%2</icode>", QString::fromLatin1(o1), QString::fromLatin1(o2)));
}

void CommandOptions::checkEditType(EditAlarmDlg::Type type, const QByteArray& optName)
{
	if (mArgs->isSet(optName)  &&  mCommand != NONE
	&&  ((mCommand != NEW && mCommand != EDIT_NEW)  ||  mEditType != type))
		setErrorIncompatible(optName, mCommandName);
}

/******************************************************************************
* Convert a time interval command line parameter.
* 'timeInterval' receives the count for the recurType. If 'allowMonthYear' is
* false, weeks are converted to days in 'timeInterval'.
* Reply = true if successful.
*/
static bool convInterval(const QByteArray& timeParam, KARecurrence::Type& recurType, int& timeInterval, bool allowMonthYear)
{
	QByteArray timeString = timeParam;
	// Get the recurrence interval
	bool ok = true;
	uint interval = 0;
	bool negative = (timeString[0] == '-');
	if (negative)
		timeString = timeString.right(1);
	uint length = timeString.length();
	switch (timeString[length - 1])
	{
		case 'Y':
			if (!allowMonthYear)
				ok = false;
			recurType = KARecurrence::ANNUAL_DATE;
			timeString = timeString.left(length - 1);
			break;
		case 'W':
			recurType = KARecurrence::WEEKLY;
			timeString = timeString.left(length - 1);
			break;
		case 'D':
			recurType = KARecurrence::DAILY;
			timeString = timeString.left(length - 1);
			break;
		case 'M':
		{
			int i = timeString.indexOf('H');
			if (i < 0)
			{
				if (!allowMonthYear)
					ok = false;
				recurType = KARecurrence::MONTHLY_DAY;
				timeString = timeString.left(length - 1);
			}
			else
			{
				recurType = KARecurrence::MINUTELY;
				interval = timeString.left(i).toUInt(&ok) * 60;
				timeString = timeString.mid(i + 1, length - i - 2);
			}
			break;
		}
		default:       // should be a digit
			recurType = KARecurrence::MINUTELY;
			break;
	}
	if (ok)
		interval += timeString.toUInt(&ok);
	if (!allowMonthYear)
	{
		// Convert time interval to minutes
		switch (recurType)
		{
			case KARecurrence::WEEKLY:
				interval *= 7;
				// fall through to DAILY
			case KARecurrence::DAILY:
				interval *= 24*60;
				break;
			default:
				break;
		}
	}
	timeInterval = static_cast<int>(interval);
	if (negative)
		timeInterval = -timeInterval;
	return ok;
}
