/******************************************************************************
 * konsolekalendar.cpp                                                        *
 *                                                                            *
 * KonsoleKalendar is a command line interface to KDE calendars               *
 * SPDX-FileCopyrightText: 2002-2004 Tuukka Pasanen <illuusio@mailcity.com>   *
 * SPDX-FileCopyrightText: 2003-2005 Allen Winter <winter@kde.org>            *
 *                                                                            *
 * SPDX-License-Identifier: GPL-2.0-or-later WITH LicenseRef-Qt-Commercial-exception-1.0 *
 *                                                                            *
 ******************************************************************************/
/**
 * @file konsolekalendar.cpp
 * Provides the KonsoleKalendar class definition.
 * @author Tuukka Pasanen
 * @author Allen Winter
 */
#include "konsolekalendar.h"
#include "konsolekalendaradd.h"
#include "konsolekalendarchange.h"
#include "konsolekalendardelete.h"
#include "konsolekalendarexports.h"

#include <KLocalizedString>

#include <Akonadi/AgentInstance>
#include <Akonadi/AgentInstanceCreateJob>
#include <Akonadi/AgentManager>
#include <Akonadi/Collection>
#include <Akonadi/CollectionFetchJob>
#include <Akonadi/CollectionFetchScope>

#include <QDBusInterface>
#include <QDBusReply>
#include <QEventLoop>
#include <QFile>
#include <QFileInfo>
#include <QTextStream>

#include <cstdio>
#include <cstdlib>
#include <iostream>

using namespace KCalendarCore;
using namespace std;

KonsoleKalendar::KonsoleKalendar(KonsoleKalendarVariables *variables)
{
    m_variables = variables;
}

KonsoleKalendar::~KonsoleKalendar() = default;

bool KonsoleKalendar::importCalendar()
{
    KonsoleKalendarAdd add(m_variables);

    return add.addImportedCalendar();
}

bool KonsoleKalendar::printCalendarList()
{
    auto job = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive);
    const QStringList mimeTypes = QStringList() << QStringLiteral("text/calendar") << KCalendarCore::Event::eventMimeType()
                                                << KCalendarCore::Todo::todoMimeType() << KCalendarCore::Journal::journalMimeType();
    job->fetchScope().setContentMimeTypes(mimeTypes);
    QEventLoop loop;
    QObject::connect(job, &Akonadi::CollectionFetchJob::result, &loop, &QEventLoop::quit);
    job->start();
    loop.exec();

    if (job->error() != 0) {
        return false;
    }

    const Akonadi::Collection::List collections = job->collections();

    if (collections.isEmpty()) {
        cout << i18n("There are no calendars available.").toLocal8Bit().data() << '\n';
    } else {
        cout << "--------------------------" << '\n';
        auto mimeTypeSet = QSet<QString>(mimeTypes.begin(), mimeTypes.end()); // set changes by run method intersect
        for (const Akonadi::Collection &collection : collections) {
            const QStringList contentMimeTypes = collection.contentMimeTypes();
            auto collectionMimeTypeSet = QSet<QString>(contentMimeTypes.begin(), contentMimeTypes.end());

            if (mimeTypeSet.intersects(collectionMimeTypeSet)) {
                QString colId = QString::number(collection.id()).leftJustified(6, QLatin1Char(' '));
                colId += QLatin1StringView("- ");

                bool const readOnly = !(collection.rights() & Akonadi::Collection::CanCreateItem || collection.rights() & Akonadi::Collection::CanChangeItem
                                        || collection.rights() & Akonadi::Collection::CanDeleteItem);

                QString const readOnlyString = readOnly ? i18nc("calendar is read-only", "(Read only)") + QLatin1Char(' ') : QString();

                cout << colId.toLocal8Bit().data() << readOnlyString.toLocal8Bit().constData() << collection.displayName().toLocal8Bit().data() << '\n';
            }
        }
    }

    return true;
}

bool KonsoleKalendar::createAkonadiResource(const QString &icalFileUri)
{
    const QString fileName = QFileInfo(icalFileUri).baseName();
    Akonadi::AgentType const type = Akonadi::AgentManager::self()->type(QStringLiteral("akonadi_ical_resource"));
    auto job = new Akonadi::AgentInstanceCreateJob(type);
    job->exec();
    if (job->error() != 0) {
        cout << i18n("Unable to create calendar %1: %2", fileName, job->errorString()).toLocal8Bit().data();
        cout << i18n("Make sure the akonadiserver is running properly.").toLocal8Bit().data();
        return false;
    }
    auto inst = job->instance();
    inst.setName(fileName);
    QDBusInterface iface(QStringLiteral("org.freedesktop.Akonadi.Resource.") + inst.identifier(), QStringLiteral("/Settings"));
    QDBusReply<void> reply = iface.call(QStringLiteral("setDisplayName"), fileName);
    if (!reply.isValid()) {
        cout << i18n("Unable to create calendar %1: DBus setDisplayName failed: %2", fileName, reply.error().message()).toLocal8Bit().data();
        return false;
    }
    reply = iface.call(QStringLiteral("setPath"), icalFileUri);
    if (!reply.isValid()) {
        cout << i18n("Unable to create calendar %1: DBus setPath call failed: %2", fileName, reply.error().message()).toLocal8Bit().data();
        return false;
    }
    reply = iface.call(QStringLiteral("save"));
    if (!reply.isValid()) {
        cout << i18n("Unable to create calendar %1: %2", fileName, reply.error().message()).toLocal8Bit().data();
        return false;
    }
    inst.reconfigure();
    return true;
}

bool KonsoleKalendar::createCalendar()
{
    bool status = false;

    const QString filename = m_variables->getCalendarFile();

    if (m_variables->isDryRun()) {
        cout << i18n("Dry Run: Create Calendar: %1", filename).toLocal8Bit().data() << '\n';
    } else {
        if (m_variables->isVerbose()) {
            cout << i18n("Create Calendar: %1", filename).toLocal8Bit().data() << '\n';
        }

        status = createAkonadiResource(QStringLiteral("file://%1").arg(filename));
    }

    return status;
}

bool KonsoleKalendar::showInstance()
{
    bool status = true;
    QFile f;
    Event::Ptr event;
    const auto timeZone = m_variables->getCalendar()->timeZone();
    Akonadi::CalendarBase::Ptr const calendar = m_variables->getCalendar();

    if (m_variables->isDryRun()) {
        cout << qPrintable(i18n("Dry Run: View Events:")) << '\n';
        printSpecs();
    } else {
        if (m_variables->isExportFile()) {
            f.setFileName(m_variables->getExportFile());
            if (!f.open(QIODevice::WriteOnly)) {
                status = false;
            }
        } else {
            std::ignore = f.open(stdout, QIODevice::WriteOnly);
        }

        if (status) {
            if (m_variables->isVerbose()) {
                cout << i18n("View Event:").toLocal8Bit().data() << '\n';
                printSpecs();
            }

            QTextStream ts(&f);

            if (m_variables->getAll()) {
                const Event::List sortedList = calendar->events(EventSortStartDate);
                if (!sortedList.isEmpty()) {
                    // The code that was here before the akonadi port was really slow with 200 events
                    // this is much faster:
                    for (const KCalendarCore::Event::Ptr &ev : sortedList) {
                        status &= printEvent(&ts, ev, ev->dtStart().date());
                    }
                }
            } else if (m_variables->isUID()) {
                // TODO: support a list of UIDs
                event = calendar->event(m_variables->getUID());
                // If this UID represents a recurring Event,
                // only the first day of the Event will be printed
                status = printEvent(&ts, event, event->dtStart().date());
            } else if (m_variables->isNext()) {
                QDateTime datetime = m_variables->getStartDateTime();
                datetime = datetime.addDays(720);

                QDate dt;
                for (dt = m_variables->getStartDateTime().date(); dt <= datetime.date(); dt = dt.addDays(1)) {
                    Event::List events = calendar->events(dt, timeZone, EventSortStartDate, SortDirectionAscending);
                    // finished here when we get the next event
                    if (!events.isEmpty()) {
                        status = printEvent(&ts, events.first(), dt);
                        f.close();
                        return status;
                    }
                }
            } else {
                QDate dt;
                for (dt = m_variables->getStartDateTime().date(); dt <= m_variables->getEndDateTime().date(); dt = dt.addDays(1)) {
                    Event::List events = calendar->events(dt, timeZone, EventSortStartDate, SortDirectionAscending);
                    status &= printEventList(&ts, &events, dt);
                }
            }

            f.close();
        }
    }
    return status;
}

bool KonsoleKalendar::printEventList(QTextStream *ts, Event::List *eventList, QDate date)
{
    bool status = true;

    if (!eventList->isEmpty()) {
        Event::Ptr singleEvent;
        Event::List::ConstIterator it;

        for (it = eventList->constBegin(); it != eventList->constEnd() && status != false; ++it) {
            singleEvent = *it;

            status = printEvent(ts, singleEvent, date);
        }
    }

    return status;
}

bool KonsoleKalendar::printEvent(QTextStream *ts, const Event::Ptr &event, QDate dt)
{
    bool status = false;
    KonsoleKalendarExports const exports;

    if (event) {
        switch (m_variables->getExportType()) {
        case ExportTypeCSV:
            status = exports.exportAsCSV(ts, event, dt);
            break;

        case ExportTypeTextShort: {
            bool sameDay = true;
            if (dt.daysTo(m_saveDate)) {
                sameDay = false;
                m_saveDate = dt;
            }
            status = exports.exportAsTxtShort(ts, event, dt, sameDay);
        } break;

        default: // Default export-type is ExportTypeText
            status = exports.exportAsTxt(ts, event, dt);
            break;
        }
    }
    return status;
}

bool KonsoleKalendar::addEvent()
{
    KonsoleKalendarAdd add(m_variables);
    return add.addEvent();
}

bool KonsoleKalendar::changeEvent()
{
    KonsoleKalendarChange change(m_variables);
    return change.changeEvent();
}

bool KonsoleKalendar::deleteEvent()
{
    KonsoleKalendarDelete del(m_variables);
    return del.deleteEvent();
}

bool KonsoleKalendar::isEvent(const QDateTime &startdate, const QDateTime &enddate, const QString &summary)
{
    // Search for an event with specified start and end datetime stamp and summary

    Event::Ptr event;
    Event::List::ConstIterator it;

    bool found = false;

    const auto timeZone = m_variables->getCalendar()->timeZone();
    Event::List const eventList(m_variables->getCalendar()->rawEventsForDate(startdate.date(), timeZone, EventSortStartDate, SortDirectionAscending));
    for (it = eventList.constBegin(); it != eventList.constEnd(); ++it) {
        event = *it;
        if (event->dtEnd().toTimeZone(timeZone) == enddate && event->summary() == summary) {
            found = true;
            break;
        }
    }
    return found;
}

void KonsoleKalendar::printSpecs()
{
    cout << i18n("  What:  %1", m_variables->getSummary()).toLocal8Bit().data() << '\n';

    cout << i18n("  Begin: %1", m_variables->getStartDateTime().toString(Qt::TextDate)).toLocal8Bit().data() << '\n';

    cout << i18n("  End:   %1", m_variables->getEndDateTime().toString(Qt::TextDate)).toLocal8Bit().data() << '\n';

    if (m_variables->getFloating() == true) {
        cout << i18n("  No Time Associated with Event").toLocal8Bit().data() << '\n';
    }

    cout << i18n("  Desc:  %1", m_variables->getDescription()).toLocal8Bit().data() << '\n';

    cout << i18n("  Location:  %1", m_variables->getLocation()).toLocal8Bit().data() << '\n';
}
