/*
 * Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
 *
 * 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 "CalDAV.h"
#include "MainWindow.h"
#include "Athlete.h"

CalDAV::CalDAV(Context *context) : context(context), mode(None)
{
    nam = new QNetworkAccessManager(this);
    connect(nam, SIGNAL(finished(QNetworkReply*)), this, SLOT(requestReply(QNetworkReply*)));

    connect(nam, SIGNAL(authenticationRequired(QNetworkReply*,QAuthenticator*)), this,
            SLOT(userpass(QNetworkReply*,QAuthenticator*)));
    connect(nam, SIGNAL(sslErrors(QNetworkReply*,QList<QSslError>)), this,
            SLOT(sslErrors(QNetworkReply*,QList<QSslError>)));

    googleCalDAVurl = "https://apidata.googleusercontent.com/caldav/v2/%1/events/";

    getConfig();
}


void CalDAV::getConfig() {

    int t = appsettings->cvalue(context->athlete->cyclist, GC_DVCALDAVTYPE, "0").toInt();
    if (t == 0) {
        calDavType = Standard;
    } else {
        calDavType = Google;
    };
}


//
// GET event directory listing
//

bool
CalDAV::download(bool ignoreErrors)
{
    getConfig();
    ignoreDownloadErrors = ignoreErrors;
    mode = Events;

    if (calDavType == Standard) {
        return doDownload();
    } else { // calDavType = GOOGLE
        // after having the token the function defined in "mode" will be executed
        requestGoogleAccessTokenToExecute();
    }
    return true;
}

bool
CalDAV::doDownload()
{

    QString url; QString calID;
    if (calDavType == Standard) {
        url = appsettings->cvalue(context->athlete->cyclist, GC_DVURL, "").toString();
    } else { // calDavType = GOOGLE
        calID = appsettings->cvalue(context->athlete->cyclist, GC_DVGOOGLE_CALID, "").toString();
        url =  googleCalDAVurl.arg(calID);
    }

    // check if we have an useful URL (not space and not the Google Default without CalID
    if ((url == "" && calDavType == Standard) || (calID == "" && calDavType == Google)) {
        if (!ignoreDownloadErrors) {
             QMessageBox::warning(context->mainWindow, tr("Missing Preferences"), tr("CalID or CalDAV Url is missing in preferences"));
        }
        mode = None;
        return false;
    }

    QNetworkRequest request = QNetworkRequest(QUrl(url));

    QByteArray *queryText = new QByteArray( "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
                                            "<C:calendar-query xmlns:D=\"DAV:\""
                                            "                 xmlns:C=\"urn:ietf:params:xml:ns:caldav\">"
                                            " <D:prop>"
                                            "   <D:getetag/>"
                                            "   <C:calendar-data/>"
                                            " </D:prop>"
                                            " <C:filter>"
                                            "   <C:comp-filter name=\"VCALENDAR\">"
                                            "     <C:comp-filter name=\"VEVENT\">"
                                            "       <C:time-range end=\"20200101T000000Z\" start=\"20000101T000000Z\"/>"
                                            "     </C:comp-filter>"
                                            "   </C:comp-filter>"
                                            " </C:filter>"
                                            "</C:calendar-query>\r\n");

    request.setRawHeader("Depth", "0");
    request.setRawHeader("Content-Type", "application/xml; charset=\"utf-8\"");
    request.setRawHeader("Content-Length", (QString("%1").arg(queryText->size())).toLatin1());
    if (calDavType == Google && googleCalendarAccessToken != "") {
        request.setRawHeader("Authorization", "Bearer "+googleCalendarAccessToken );
    }


    QBuffer *query = new QBuffer(queryText);

    mode = Events;
    QNetworkReply *reply = nam->sendCustomRequest(request, "REPORT", query);
    if (reply->error() != QNetworkReply::NoError) {
        if (!ignoreDownloadErrors) {
            QMessageBox::warning(context->mainWindow, tr("CalDAV REPORT url error"), reply->errorString());
        }
        mode = None;
        return false;
    }
    return true;
}

//
// Get OPTIONS available
//
//bool
//CalDAV::options()
//{
//    QString url = appsettings->cvalue(context->athlete->cyclist, GC_DVURL, "").toString();
//    if (url == "") return false; // not configured

//    QNetworkRequest request = QNetworkRequest(QUrl(url));


//    QByteArray *queryText = new QByteArray("<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
//                                           "<D:options xmlns:D=\"DAV:\">"
//                                           "  <C:calendar-home-set xmlns:C=\"urn:ietf:params:xml:ns:caldav\"/>"
//                                           "</D:options>");

//    request.setRawHeader("Depth", "0");
//    request.setRawHeader("Content-Type", "text/xml; charset=\"utf-8\"");
//    request.setRawHeader("Content-Length", (QString("%1").arg(queryText->size())).toLatin1());

//    QBuffer *query = new QBuffer(queryText);

//    mode = Options;
//    QNetworkReply *reply = nam->sendCustomRequest(request, "OPTIONS", query);
//    if (reply->error() != QNetworkReply::NoError) {
//        QMessageBox::warning(context->mainWindow, tr("CalDAV OPTIONS url error"), reply->errorString());
//        mode = None;
//        return false;
//    }
//    return true;
//}

//
// Get URI Properties via PROPFIND
//
//bool
//CalDAV::propfind()
//{
//    QString url = appsettings->cvalue(context->athlete->cyclist, GC_DVURL, "").toString();
//    if (url == "") return false; // not configured

//    QNetworkRequest request = QNetworkRequest(QUrl(url));


//    QByteArray *queryText = new QByteArray( "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
//                                            "<D:propfind xmlns:D=\"DAV:\""
//                                            "                 xmlns:C=\"urn:ietf:params:xml:ns:caldav\">"
//                                            "  <D:prop>"
//                                            "    <D:displayname/>"
//                                            "    <C:calendar-timezone/> "
//                                            "    <C:supported-calendar-component-set/> "
//                                            "  </D:prop>"
//                                            "</D:propfind>\r\n");

//    request.setRawHeader("Content-Type", "text/xml; charset=\"utf-8\"");
//    request.setRawHeader("Content-Length", (QString("%1").arg(queryText->size())).toLatin1());
//    request.setRawHeader("Depth", "0");

//    QBuffer *query = new QBuffer(queryText);

//    mode = PropFind;
//    QNetworkReply *reply = nam->sendCustomRequest(request, "PROPFIND" , query);
//    if (reply->error() != QNetworkReply::NoError) {
//        QMessageBox::warning(context->mainWindow, tr("CalDAV OPTIONS url error"), reply->errorString());
//        mode = None;
//        return false;
//    }
//    return true;
//}


////
//// REPORT of "all" VEVENTS
////
//bool
//CalDAV::report()
//{
//    QString url = appsettings->cvalue(context->athlete->cyclist, GC_DVURL, "").toString();
//    if (url == "") return false; // not configured

//    QNetworkRequest request = QNetworkRequest(QUrl(url));
//    QByteArray *queryText = new QByteArray("<x1:calendar-query xmlns:x1=\"urn:ietf:params:xml:ns:caldav\">"
//                     "<x0:prop xmlns:x0=\"DAV:\">"
//                     "<x0:getetag/>"
//                     "<x1:calendar-data/>"
//                     "</x0:prop>"
//                     "<x1:filter>"
//                     "<x1:comp-filter name=\"VCALENDAR\">"
//                     "<x1:comp-filter name=\"VEVENT\">"
//                     "<x1:time-range end=\"21001231\" start=\"20000101T000000Z\"/>"
//                     "</x1:comp-filter>"
//                     "</x1:comp-filter>"
//                     "</x1:filter>"
//                     "</x1:calendar-query>");

//    QBuffer *query = new QBuffer(queryText);

//    mode = Report;
//    QNetworkReply *reply = nam->sendCustomRequest(request, "REPORT", query);
//    if (reply->error() != QNetworkReply::NoError) {
//        QMessageBox::warning(context->mainWindow, tr("CalDAV REPORT url error"), reply->errorString());
//        mode = None;
//        return false;
//    }
//    return true;
//}

// utility function to create a VCALENDAR from a single RideItem
static
icalcomponent *createEvent(RideItem *rideItem)
{
    // calendar
    icalcomponent *root = icalcomponent_new(ICAL_VCALENDAR_COMPONENT);

    // calendar version
    icalproperty *version = icalproperty_new_version("2.0");
    icalcomponent_add_property(root, version);


    icalcomponent *event = icalcomponent_new(ICAL_VEVENT_COMPONENT);

    //
    // Unique ID
    //
    QString id = rideItem->ride()->id();
    if (id == "") {
        id = QUuid::createUuid().toString() + "@" + "goldencheetah.org";
        rideItem->ride()->setId(id);
        rideItem->notifyRideMetadataChanged();
        rideItem->setDirty(true); // need to save this!
    }
    icalproperty *uid = icalproperty_new_uid(id.toLatin1());
    icalcomponent_add_property(event, uid);

    //
    // START DATE
    //
    struct icaltimetype atime;
    QDateTime utc = rideItem->dateTime.toUTC();
    atime.year = utc.date().year();
    atime.month = utc.date().month();
    atime.day = utc.date().day();
    atime.hour = utc.time().hour();
    atime.minute = utc.time().minute();
    atime.second = utc.time().second();
    atime.is_utc = 1; // this is UTC is_utc is redundant but kept for completeness
    atime.is_date = 0; // this is a date AND time
    atime.is_daylight = 0; // no daylight savings - its UTC
    atime.zone = icaltimezone_get_utc_timezone(); // set UTC timezone
    icalproperty *dtstart = icalproperty_new_dtstart(atime);
    icalcomponent_add_property(event, dtstart);

    //
    // DURATION
    //

    // override values?
    QMap<QString,QString> lookup;
    lookup = rideItem->ride()->metricOverrides.value("workout_time");
    int secs = lookup.value("value", "0.0").toDouble();

    // from last - first timestamp?
    if (!rideItem->ride()->dataPoints().isEmpty() && rideItem->ride()->dataPoints().last() != NULL) {
        if (!secs) secs = rideItem->ride()->dataPoints().last()->secs;
    }

    // ok, got secs so now create in vcard
    struct icaldurationtype dur;
    dur.is_neg = 0;
    dur.days = dur.weeks = 0;
    dur.hours = secs/3600;
    dur.minutes = secs%3600/60;
    dur.seconds = secs%60;
    icalcomponent_set_duration(event, dur);

    // set title & description
    QString title = rideItem->ride()->getTag("Title", ""); // *new* 'special' metadata field
    if (title == "") title = rideItem->ride()->getTag("Sport", "") + " Workout";
    icalcomponent_set_summary(event, title.toLatin1());

    // set description using standard stuff
    icalcomponent_set_description(event, rideItem->ride()->getTag("Calendar Text", "").toLatin1());

    // attach ridefile
    // google doesn't support attachments yet. There is a labs option to use google docs
    // but it is only available to Google Apps customers.

    // put the event into root
    icalcomponent_add_component(root, event);
    return root;
}

// extract <calendar-data> entries and concatenate
// into a single string. This is from a query response
// where the VEVENTS are embedded within an XML document
static QString extractComponents(QString document)
{
    QString returning = "";

    // parse the document and extract the multistatus node (there is only one of those)
    QDomDocument doc;
    if (document == "" || doc.setContent(document) == false) return "";
    QDomNode multistatus = doc.documentElement();
    if (multistatus.isNull())  return "";

    // Google Calendar retains the namespace prefix in the results
    // Apple MobileMe doesn't. This means the element names will
    // possibly need a prefix...
    // Google CalDav API V2 does not deliver C: but caldav: as a prefix
    // so try both - just in case - to get the data

    QString Dprefix = "";
    QString Cprefix = "";
    QString Cprefix_Google = "";
    if (multistatus.nodeName().startsWith("D:")) {
        Dprefix = "D:";
        Cprefix = "C:";
        Cprefix_Google = "caldav:";
    }

    // read all the responses within the multistatus
    for (QDomNode response = multistatus.firstChildElement(Dprefix + "response");
         response.nodeName() == (Dprefix + "response"); response = response.nextSiblingElement(Dprefix + "response")) {

        // skate over the nest of crap to get at the calendar-data
        QDomNode propstat = response.firstChildElement(Dprefix + "propstat");
        QDomNode prop = propstat.firstChildElement(Dprefix + "prop");
        QDomNode calendardata = prop.firstChildElement(Cprefix + "calendar-data");
        if (calendardata.isNull()) {
            calendardata = prop.firstChildElement(Cprefix_Google + "calendar-data");
        };

        // extract the calendar entry - top and tail the other crap
        QString text = calendardata.toElement().text();
        int start = text.indexOf("BEGIN:VEVENT");
        int stop = text.indexOf("END:VEVENT");

        if (start == -1 || stop == -1) continue;

        returning += text.mid(start, stop-start+10) + "\n";
    }
    return returning;
}

//
// PUT a ride item
//

bool
CalDAV::upload(RideItem *rideItem)
{
    getConfig();
    mode = Put;
    if (calDavType == Standard) {
        return doUpload(rideItem);
    } else { // calDavType = GOOGLE
        // after having the token the function defined in "mode" will be executed
        itemForUpload = rideItem;
        requestGoogleAccessTokenToExecute();
    }
    return true;
}


bool
CalDAV::doUpload(RideItem *rideItem)
{
    // is this a valid ride?
    if (!rideItem || !rideItem->ride()) return false;

    QString url; QString calID;
    if (calDavType == Standard) {
        url = appsettings->cvalue(context->athlete->cyclist, GC_DVURL, "").toString();
    } else { // calDavType = GOOGLE
        calID = appsettings->cvalue(context->athlete->cyclist, GC_DVGOOGLE_CALID, "").toString();
        url =  googleCalDAVurl.arg(calID);
    }

    // check if we have an useful URL (not space and not the Google Default without CalID
    if ((url == "" && calDavType == Standard) || (calID == "" && calDavType == Google)) {
        QMessageBox::warning(context->mainWindow, tr("Missing Preferences"), tr("CalID or CalDAV Url is missing in preferences"));
        mode = None;
        return false;
    }

    // if URL does not end with "/" - just  add it (for convenience)
    if (!url.endsWith("/")) {
        url += "/";
    }
    // lets upload to calendar
    url += rideItem->fileName;
    url += ".ics";

    // form the request
    QNetworkRequest request = QNetworkRequest(QUrl(url));
    request.setRawHeader("Content-Type", "text/calendar");
    request.setRawHeader("Content-Length", "xxxx");
    if (calDavType == Google && googleCalendarAccessToken != "") {
        request.setRawHeader("Authorization", "Bearer "+googleCalendarAccessToken );
    }

    // create the ICal event
    icalcomponent *vcard = createEvent(rideItem);
    QByteArray vcardtext(icalcomponent_as_ical_string(vcard));
    icalcomponent_free(vcard);

    mode = Put;
    QNetworkReply *reply = nam->put(request, vcardtext);
    if (reply->error() != QNetworkReply::NoError) {
        mode = None;
        QMessageBox::warning(context->mainWindow, tr("CalDAV Calendar url error"), reply->errorString());
        return false;
    }
    return true;
}

//
// All queries/commands respond here
//
void
CalDAV::requestReply(QNetworkReply *reply)
{
    QString response = reply->readAll();

    if (reply->error() != QNetworkReply::NoError) {
        if (!(mode == Events && ignoreDownloadErrors)) {
            QMessageBox::warning(context->mainWindow, tr("CalDAV Calendar API reply error"), reply->errorString());
        }
        mode = None;
        return; // silently
    }

    switch (mode) {
    case Report:
    case Events:
        context->athlete->rideCalendar->refreshRemote(extractComponents(response));
        break;
    default:
    case Options:
    case PropFind:
    case Put:
        //nothing at the moment
        break;
    }
    mode = None;
}

//
// Provide user credentials, called when receive a 401
//
void
CalDAV::userpass(QNetworkReply*,QAuthenticator*a)
{
    QString user = appsettings->cvalue(context->athlete->cyclist, GC_DVUSER, "").toString();
    QString pass = appsettings->cvalue(context->athlete->cyclist, GC_DVPASS, "").toString();
    a->setUser(user);
    a->setPassword(pass);
}

//
// Trap SSL errors
//
void
CalDAV::sslErrors(QNetworkReply* reply ,QList<QSslError> errors)
{
    QString errorString = "";
    foreach (const QSslError e, errors ) {
        if (!errorString.isEmpty())
            errorString += ", ";
        errorString += e.errorString();
    }
    if (!(mode == Events && ignoreDownloadErrors)) {
        QMessageBox::warning(context->mainWindow, tr("HTTP"), tr("SSL error(s) has occurred: %1").arg(errorString));
        mode = None;
        reply->ignoreSslErrors();
    }
}



//
// gets Google Calendar Access Token (from Refresh Token)
//
void
CalDAV::requestGoogleAccessTokenToExecute() {

    QString refresh_token = appsettings->cvalue(context->athlete->cyclist, GC_GOOGLE_CALENDAR_REFRESH_TOKEN, "").toString();
    if (refresh_token == "") {
        if (!(mode == Events && ignoreDownloadErrors)) {
            QMessageBox::warning(context->mainWindow, tr("Missing Preferences"), tr("Authorization for Google CalDAV is missing in preferences"));
        }
        mode = None;
        return;
    }

    // get a valid access token
    QByteArray data;
#if QT_VERSION > 0x050000
    QUrlQuery params;
#else
    QUrl params;
#endif
    QString urlstr = "https://www.googleapis.com/oauth2/v3/token?";
    params.addQueryItem("client_id", GC_GOOGLE_CALENDAR_CLIENT_ID);
#ifdef GC_GOOGLE_CALENDAR_CLIENT_SECRET
    params.addQueryItem("client_secret", GC_GOOGLE_CALENDAR_CLIENT_SECRET);
#endif
    params.addQueryItem("refresh_token", refresh_token);
    params.addQueryItem("grant_type", "refresh_token");

#if QT_VERSION > 0x050000
    data.append(params.query(QUrl::FullyEncoded));
#else
    data=params.encodedQuery();
#endif

    // get a new Access Token (since they are just temporarily valid)
    QUrl url = QUrl(urlstr);
    QNetworkRequest request = QNetworkRequest(url);
    request.setHeader(QNetworkRequest::ContentTypeHeader,"application/x-www-form-urlencoded");

    // now get the final token
    googleNetworkAccessManager = new QNetworkAccessManager(this);
    connect(googleNetworkAccessManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(googleNetworkRequestFinished(QNetworkReply*)));
    googleNetworkAccessManager->post(request, data);

}

//
// extract the token and call the requested CALDAV function
//
void
CalDAV::googleNetworkRequestFinished(QNetworkReply* reply)   {

    googleCalendarAccessToken = "";
    if (reply->error() == QNetworkReply::NoError) {
        QByteArray payload = reply->readAll(); // JSON
        QByteArray token_type = "\"access_token\":";
        int token_length = 15;

        // get the token
        int at = payload.indexOf(token_type);
        if (at >=0 ) {
            int from = at + token_length; // first char after ":"
            int next = payload.indexOf("\"", from);
            from = next + 1;
            int to = payload.indexOf("\"", from);
            googleCalendarAccessToken = payload.mid(from, to-from);

        } else { // something failed

            if (!(mode == Events && ignoreDownloadErrors)) {
                QMessageBox::warning(context->mainWindow, tr("Authorization Error"), tr("Error requesting access token"));
            };
            mode = None;
            return;
        }
    }

    // now we have a token and can do the requested jobs
    if (mode == Put) doUpload(itemForUpload);
    if (mode == Events) doDownload();
}

