/*
 *  commandoptions.cpp  -  extract command line options
 *  Program:  kalarm
 *  Copyright © 2001-2011 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"      //krazy:exclude=includes (kalarm.h must be first)
#include "commandoptions.h"
#include "functions.h"
#include "kalarmapp.h"
#include "kamail.h"

#include <kalarmcal/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),
      mDisableAll(false)
{
    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 (checkCommand("edit-new-audio", EDIT_NEW, EditAlarmDlg::AUDIO))
    {
        mEditType      = EditAlarmDlg::AUDIO;
        mEditAction    = KAEvent::AUDIO;
        mEditActionSet = true;
    }
    if (mError.isEmpty()  &&  mCommand == NONE)
    {
        if (!mArgs->count())
        {
            if (checkCommand("play", NEW) || checkCommand("play-repeat", NEW))
            {
                mEditType      = EditAlarmDlg::AUDIO;
                mEditAction    = KAEvent::AUDIO;
                mEditActionSet = true;
            }
        }
        else
        {
            kDebug() << "Message";
            mCommand       = NEW;
            mCommandName   = "message";
            mEditType      = EditAlarmDlg::DISPLAY;
            mEditAction    = KAEvent::MESSAGE;
            mEditActionSet = true;
            mText          = mArgs->arg(0);
        }
    }
    if (mEditActionSet  &&  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")));
#ifdef USE_AKONADI
            KCalCore::Person::Ptr person(new KCalCore::Person(QString(), addr));
            mAddressees += person;
#else
            mAddressees += KCal::Person(QString(), addr);
#endif
        }
        params = mArgs->getOptionList("attach");
        for (int i = 0, count = params.count();  i < count;  ++i)
            mAttachments += params[i];
        if (mArgs->count())
            mText = mArgs->arg(0);
    }
    if (mArgs->isSet("disable-all"))
    {
        if (mCommand == TRIGGER_EVENT)
            setErrorIncompatible("--disable-all", mCommandName);
        mDisableAll = true;
    }

    // Check that other options are only specified for the
    // correct main command options.
    checkEditType(EditAlarmDlg::DISPLAY, "color");
    checkEditType(EditAlarmDlg::DISPLAY, "colorfg");
    checkEditType(EditAlarmDlg::DISPLAY, EditAlarmDlg::AUDIO, "play");
    checkEditType(EditAlarmDlg::DISPLAY, EditAlarmDlg::AUDIO, "play-repeat");
    checkEditType(EditAlarmDlg::DISPLAY, EditAlarmDlg::AUDIO, "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))
                    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 Jovie", QLatin1String("--speak")));
            }
            bool onceOnly = mArgs->isSet("reminder-once");
            if (mArgs->isSet("reminder")  ||  onceOnly)
            {
                // Issue a reminder alarm in advance of or after the main alarm
                if (onceOnly  &&  mArgs->isSet("reminder"))
                    setErrorIncompatible("--reminder", "--reminder-once");
                const char* opt = onceOnly ? "--reminder-once" : "--reminder";
                KARecurrence::Type recurType;
                QByteArray optval = mArgs->getOption(onceOnly ? "reminder-once" : "reminder").toLocal8Bit();
                bool after = (optval[0] == '+');
                if (after)
                    optval = optval.right(1);
                if (!convInterval(optval, 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 (after)
                    mReminderMinutes = -mReminderMinutes;
                if (onceOnly)
                    mFlags |= KAEvent::REMINDER_ONCE;
            }

            if (mArgs->isSet("late-cancel"))
            {
                KARecurrence::Type recurType;
                bool ok = convInterval(mArgs->getOption("late-cancel").toLocal8Bit(), recurType, mLateCancel);
                if (!ok)
                    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")    //krazy:exclude=strings (it's a QByteArray)
        o1.prepend("--");
    QByteArray o2 = opt2;
    if (!opt2.startsWith("--")  &&  opt2 != "message")    //krazy:exclude=strings (it's a QByteArray)
        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 type1, EditAlarmDlg::Type type2, const QByteArray& optName)
{
    if (mArgs->isSet(optName)  &&  mCommand != NONE
    &&  ((mCommand != NEW && mCommand != EDIT_NEW)  ||  (mEditType != type1 && (type2 == EditAlarmDlg::NO_TYPE || mEditType != type2))))
        setErrorIncompatible(optName, mCommandName);
}

/******************************************************************************
* Convert a non-zero positive 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;
    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);
    return ok && interval;
}

// vim: et sw=4:
