/*
 * Copyright (C) 2010 Intel Corporation
 */

#include "config.h"

#ifdef ENABLE_DAV

// include first, it sets HANDLE_LIBICAL_MEMORY for us
#include <syncevo/icalstrdup.h>

#include "CalDAVSource.h"

#include <boost/algorithm/string/replace.hpp>

#include <syncevo/declarations.h>
SE_BEGIN_CXX

/**
 * @return "<master>" if subid is empty, otherwise subid
 */
static std::string SubIDName(const std::string &subid)
{
    return subid.empty() ? "<master>" : subid;
}

/** remove X-SYNCEVOLUTION-EXDATE-DETACHED from VEVENT */
static void removeSyncEvolutionExdateDetached(icalcomponent *parent)
{
    icalproperty *prop = icalcomponent_get_first_property(parent, ICAL_ANY_PROPERTY);
    while (prop) {
        icalproperty *next = icalcomponent_get_next_property(parent, ICAL_ANY_PROPERTY);
        const char *xname = icalproperty_get_x_name(prop);
        if (xname &&
            !strcmp(xname, "X-SYNCEVOLUTION-EXDATE-DETACHED")) {
            icalcomponent_remove_property(parent, prop);
            icalproperty_free(prop);
        }
        prop = next;
    }
}

CalDAVSource::CalDAVSource(const SyncSourceParams &params,
                           const std::shared_ptr<Neon::Settings> &settings) :
    WebDAVSource(params, settings)
{
    SyncSourceLogging::init(InitList<std::string>("SUMMARY") + "LOCATION",
                            ", ",
                            m_operations);
    // override default backup/restore from base class with our own
    // version
    m_operations.m_backupData = [this] (const SyncSource::Operations::ConstBackupInfo &oldBackup,
                                        const SyncSource::Operations::BackupInfo &newBackup,
                                        BackupReport &backupReport) {
        backupData(oldBackup, newBackup, backupReport);
    };
    m_operations.m_restoreData = [this] (const SyncSource::Operations::ConstBackupInfo &oldBackup,
                                         bool dryrun,
                                         SyncSourceReport &report) {
        restoreData(oldBackup, dryrun, report);
    };
}

void CalDAVSource::listAllSubItems(SubRevisionMap_t &revisions)
{
    revisions.clear();

    const std::string query =
        "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
        "<C:calendar-query xmlns:D=\"DAV:\"\n"
        "xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n"
        "<D:prop>\n"
        "<D:getetag/>\n"

        // In practice, peers always return the full data dump
        // even if asked to return only a subset. Therefore we use this
        // REPORT to populate our m_cache instead of sending lots of GET
        // requests later on: faster sync, albeit with higher
        // memory consumption.
        //
        // Because incremental syncs typically don't use listAllSubItems(),
        // this looks like a good trade-off.
#ifdef SHORT_ALL_SUB_ITEMS_DATA
        "<C:calendar-data>\n"
        "<C:comp name=\"VCALENDAR\">\n"
        "<C:prop name=\"VERSION\"/>\n"
        "<C:comp name=\"VEVENT\">\n"
        "<C:prop name=\"SUMMARY\"/>\n"
        "<C:prop name=\"UID\"/>\n"
        "<C:prop name=\"RECURRENCE-ID\"/>\n"
        "<C:prop name=\"SEQUENCE\"/>\n"
        "</C:comp>\n"
        "<C:comp name=\"VTIMEZONE\"/>\n"
        "</C:comp>\n"
        "</C:calendar-data>\n"
#else
        "<C:calendar-data/>\n"
#endif

        "</D:prop>\n"
        // filter expected by Yahoo! Calendar
        "<C:filter>\n"
        "<C:comp-filter name=\"VCALENDAR\">\n"
        "<C:comp-filter name=\"VEVENT\">\n"
        "</C:comp-filter>\n"
        "</C:comp-filter>\n"
        "</C:filter>\n"
        "</C:calendar-query>\n";
    Timespec deadline = createDeadline();
    getSession()->startOperation("REPORT 'meta data'", deadline);
    while (true) {
        string data;
        Neon::XMLParser parser;
        auto process = [this, &revisions, &data] (const std::string &href, const std::string &etag, const std::string &status) {
            return appendItem(revisions, href, etag, data);
        };
        parser.initReportParser(process);
        m_cache.clear();
        m_cache.m_initialized = false;
        parser.pushHandler(Neon::XMLParser::accept("urn:ietf:params:xml:ns:caldav", "calendar-data"),
                           Neon::XMLParser::append(data));
        Neon::Request report(*getSession(), "REPORT", getCalendar().m_path, query, parser);
        report.addHeader("Depth", "1");
        report.addHeader("Content-Type", "application/xml; charset=\"utf-8\"");
        if (report.run()) {
            break;
        }
    }

    m_cache.m_initialized = true;
}

void CalDAVSource::updateAllSubItems(SubRevisionMap_t &revisions)
{
    // list items to identify new, updated and removed ones
    const std::string query =
        "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
        "<C:calendar-query xmlns:D=\"DAV:\"\n"
        "xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n"
        "<D:prop>\n"
        "<D:getetag/>\n"
        "</D:prop>\n"
        // filter expected by Yahoo! Calendar
        "<C:filter>\n"
        "<C:comp-filter name=\"VCALENDAR\">\n"
        "<C:comp-filter name=\"VEVENT\">\n"
        "</C:comp-filter>\n"
        "</C:comp-filter>\n"
        "</C:filter>\n"
        "</C:calendar-query>\n";
    Timespec deadline = createDeadline();
    StringMap items;
    getSession()->startOperation("updateAllSubItems REPORT 'list items'", deadline);
    while (true) {
        string data;
        Neon::XMLParser parser;
        items.clear();
        auto process = [this, &items] (const std::string &href, const std::string &etag, const std::string &status) {
            std::string davLUID = path2luid(Neon::URI::parse(href).m_path);
            items[davLUID] = ETag2Rev(etag);
        };
        parser.initReportParser(process);
        Neon::Request report(*getSession(), "REPORT", getCalendar().m_path, query, parser);
        report.addHeader("Depth", "1");
        report.addHeader("Content-Type", "application/xml; charset=\"utf-8\"");
        if (report.run()) {
            break;
        }
    }

    // remove obsolete entries
    auto it = revisions.begin();
    while (it != revisions.end()) {
        auto next = it;
        ++next;
        if (items.find(it->first) == items.end()) {
            revisions.erase(it);
        }
        it = next;
    }

    // build list of new or updated entries,
    // copy others to cache
    m_cache.clear();
    m_cache.m_initialized = false;
    std::list<std::string> mustRead;
    for (const auto &item: items) {
        auto it = revisions.find(item.first);
        if (it == revisions.end() ||
            it->second.m_revision != item.second) {
            // read current information below
            SE_LOG_DEBUG(NULL, "updateAllSubItems(): read new or modified item %s", item.first.c_str());
            mustRead.push_back(item.first);
            // The server told us that the item exists. We still need
            // to deal with the situation that the server might fail
            // to deliver the item data when we ask for it below.
            //
            // There are two reasons when this can happen: either an
            // item was removed in the meantime or the server is
            // confused.  The latter started to happen reliably with
            // the Google Calendar server sometime in January/February
            // 2012.
            //
            // In both cases, let's assume that the item is really gone
            // (and not just unreadable due to that other Google Calendar
            // bug, see loadItem()+REPORT workaround), and therefore let's
            // remove the entry from the revisions.
            if (it != revisions.end()) {
                revisions.erase(it);
            }
            m_cache.erase(item.first);
        } else {
            // copy still relevant information
            SE_LOG_DEBUG(NULL, "updateAllSubItems(): unmodified item %s", it->first.c_str());
            addSubItem(it->first, it->second);
        }
    }

    // request dump of these items, add to cache and revisions
    //
    // Failures to find or read certain items will be
    // ignored. appendItem() will only be called for actually
    // retrieved items. This is partly intentional: Google is known to
    // have problems with providing all of its data via GET or the
    // multiget REPORT below. It returns a 404 error for items that a
    // calendar-query includes (see loadItem()). Such items are
    // ignored and thus will be silently skipped. This is not
    // perfect, but better than failing the sync.
    //
    // Unfortunately there are other servers (Radicale, I'm looking at
    // you) which simply return neither data nor errors for the
    // requested hrefs. To handle that we try the multiget first,
    // record retrieved or failed responses, then follow up with
    // individual requests for anything that wasn't mentioned.
    if (!mustRead.empty()) {
        std::stringstream buffer;
        buffer << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
            "<C:calendar-multiget xmlns:D=\"DAV:\"\n"
            "   xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n"
            "<D:prop>\n"
            "   <D:getetag/>\n"
            "   <C:calendar-data/>\n"
            "</D:prop>\n";
        for (const std::string &luid: mustRead) {
            buffer << "<D:href>" << luid2path(luid) << "</D:href>\n";
        }
        buffer << "</C:calendar-multiget>";
        std::string query = buffer.str();
        std::set<std::string> results; // LUIDs of all hrefs returned by report
        getSession()->startOperation("updateAllSubItems REPORT 'multiget new/updated items'", deadline);
        while (true) {
            string data;
            Neon::XMLParser parser;
            auto process = [this, &revisions, &results, &data] (const std::string &href, const std::string &etag, const std::string &status) {
                // record which items were seen in the response...
                results.insert(path2luid(href));
                // and store information about them
                return appendItem(revisions, href, etag, data);
            };
            parser.initReportParser(process);
            parser.pushHandler(Neon::XMLParser::accept("urn:ietf:params:xml:ns:caldav", "calendar-data"),
                               Neon::XMLParser::append(data));
            Neon::Request report(*getSession(), "REPORT", getCalendar().m_path,
                                 query, parser);
            report.addHeader("Depth", "1");
            report.addHeader("Content-Type", "application/xml; charset=\"utf-8\"");
            if (report.run()) {
                break;
            }
        }
        // Workaround for Radicale 0.6.4: it simply returns nothing (no error, no data).
        // Fall back to GET of items with no response.
        for (const std::string &luid: mustRead) {
            if (results.find(luid) == results.end()) {
                getSession()->startOperation(StringPrintf("GET item %s not returned by 'multiget new/updated items'", luid.c_str()),
                                             deadline);
                std::string path = luid2path(luid);
                std::string data;
                std::string etag;
                while (true) {
                    data.clear();
                    Neon::Request req(*getSession(), "GET", path,
                                      "", data);
                    req.addHeader("Accept", contentType());
                    if (req.run()) {
                        etag = getETag(req);
                        break;
                    }
                }
                appendItem(revisions, path, etag, data);
            }
        }
    }
}

int CalDAVSource::appendItem(SubRevisionMap_t &revisions,
                             const std::string &href,
                             const std::string &etag,
                             std::string &data)
{
    // Ignore responses with no data: this is not perfect (should better
    // try to figure out why there is no data), but better than
    // failing.
    //
    // One situation is the response for the collection itself,
    // which comes with a 404 status and no data with Google Calendar.
    if (data.empty()) {
        return 0;
    }

    Event::unescapeRecurrenceID(data);
    eptr<icalcomponent> calendar(icalcomponent_new_from_string((char *)data.c_str()), // cast is a hack for broken definition in old libical
                                 "iCalendar 2.0");
    Event::fixIncomingCalendar(calendar.get());
    std::string davLUID = path2luid(Neon::URI::parse(href).m_path);
    SubRevisionEntry &entry = revisions[davLUID];
    entry.m_revision = ETag2Rev(etag);
    long maxSequence = 0;
    std::string uid;
    entry.m_subids.clear();
    for (icalcomponent *comp = icalcomponent_get_first_component(calendar, ICAL_VEVENT_COMPONENT);
         comp;
         comp = icalcomponent_get_next_component(calendar, ICAL_VEVENT_COMPONENT)) {
        std::string subid = Event::getSubID(comp);
        uid = Event::getUID(comp);
        long sequence = Event::getSequence(comp);
        if (sequence > maxSequence) {
            maxSequence = sequence;
        }
        entry.m_subids.insert(subid);
    }
    entry.m_uid = uid;

    // Ignore items which contain no VEVENT. Happens with Google Calendar
    // after using it for a while. Deleting them via DELETE doesn't seem
    // to have an effect either, so all we really can do is ignore them.
    if (entry.m_subids.empty()) {
        SE_LOG_DEBUG(NULL, "ignoring broken item %s (is empty)", davLUID.c_str());
        revisions.erase(davLUID);
        m_cache.erase(davLUID);
        data.clear();
        return 0;
    }

    if (!m_cache.m_initialized) {
        auto event = std::make_shared<Event>();
        event->m_DAVluid = davLUID;
        event->m_UID = uid;
        event->m_etag = entry.m_revision;
        event->m_subids = entry.m_subids;
        event->m_sequence = maxSequence;
#ifndef SHORT_ALL_SUB_ITEMS_DATA
        // we got a full data dump, use it
        for (icalcomponent *comp = icalcomponent_get_first_component(calendar, ICAL_VEVENT_COMPONENT);
             comp;
             comp = icalcomponent_get_next_component(calendar, ICAL_VEVENT_COMPONENT)) {
        }
        event->m_calendar = calendar;
#endif
        m_cache.insert(make_pair(davLUID, event));
    }

    // reset data for next item
    data.clear();
    return 0;
}

void CalDAVSource::addSubItem(const std::string &luid,
                              const SubRevisionEntry &entry)
{
    std::shared_ptr<Event> &event = m_cache[luid];
    event.reset(new Event);
    event->m_DAVluid = luid;
    event->m_etag = entry.m_revision;
    event->m_UID = entry.m_uid;
    // We don't know sequence and last-modified. This
    // information will have to be filled in by loadItem()
    // when some operation on this event needs it.
    event->m_subids = entry.m_subids;
}

void CalDAVSource::setAllSubItems(const SubRevisionMap_t &revisions)
{
    if (!m_cache.m_initialized) {
        // populate our cache (without data) from the information cached
        // for us
        for (const auto &subentry: revisions) {
            addSubItem(subentry.first,
                       subentry.second);
        }
        m_cache.m_initialized = true;
    }
}

SubSyncSource::SubItemResult CalDAVSource::insertSubItem(const std::string &luid, const std::string &callerSubID,
                                                         const std::string &item)
{
    SubItemResult subres;

    // parse new event
    auto newEvent = std::make_shared<Event>();
    newEvent->m_calendar.set(icalcomponent_new_from_string((char *)item.c_str()), // hack for old libical
                             "parsing iCalendar 2.0");

    // Google Calendar adds a default alarm each time a VEVENT is
    // added anew. Avoid that by adding a dummy VALARM with
    // ACTION=NONE. Must include a fake TRIGGER. This is what iOS
    // does.
    icalcomponent *event;
    if (settings().googleAlarmHack() &&
        (event = icalcomponent_get_first_component(newEvent->m_calendar, ICAL_VEVENT_COMPONENT)) != nullptr &&
        !icalcomponent_get_first_component(event, ICAL_VALARM_COMPONENT)) {
        static eptr<icalcomponent> alarm(icalcomponent_new_from_string("BEGIN:VALARM\r\n"
                                                                       "ACTION:NONE\r\n"
                                                                       "DESCRIPTION:ignore me\r\n"
                                                                       "TRIGGER:-PT1H\r\n"
                                                                       "END:VALARM"),
                                         "dummy VALARM");
        icalcomponent_add_component(event, icalcomponent_new_clone(alarm));
    }

    struct icaltimetype lastmodtime = icaltime_null_time();
    icalcomponent *firstcomp = nullptr;
    for (icalcomponent *comp = firstcomp = icalcomponent_get_first_component(newEvent->m_calendar, ICAL_VEVENT_COMPONENT);
         comp;
         comp = icalcomponent_get_next_component(newEvent->m_calendar, ICAL_VEVENT_COMPONENT)) {
        std::string subid = Event::getSubID(comp);
        EventCache::iterator it;
        // remove X-SYNCEVOLUTION-EXDATE-DETACHED, could be added by
        // the engine's read/modify/write cycle when resolving a
        // conflict
        removeSyncEvolutionExdateDetached(comp);
        if (!luid.empty() &&
            (it = m_cache.find(luid)) != m_cache.end()) {
            // Additional sanity check: ensure that the expected UID is set.
            // Necessary if the peer we synchronize with (aka the local
            // data storage) doesn't support foreign UIDs. Maemo 5 calendar
            // backend is one example.
            Event::setUID(comp, it->second->m_UID);
            newEvent->m_UID = it->second->m_UID;
        } else {
            newEvent->m_UID = Event::getUID(comp);
            if (newEvent->m_UID.empty()) {
                // create new UID
                newEvent->m_UID = UUID();
                Event::setUID(comp, newEvent->m_UID);
            }
        }
        newEvent->m_sequence = Event::getSequence(comp);
        newEvent->m_subids.insert(subid);

        // set DTSTAMP to LAST-MODIFIED in replacement
        //
        // Needed because Google insists on replacing the original
        // DTSTAMP and checks it (409, "Can only store an event with
        // a newer DTSTAMP").
        //
        // According to RFC 2445, the property is set once when the
        // event is created for the first time. RFC 5545 extends this
        // and states that without a METHOD property (the case with
        // CalDAV), DTSTAMP is identical to LAST-MODIFIED, so Google
        // is right.
        icalproperty *dtstamp = icalcomponent_get_first_property(comp, ICAL_DTSTAMP_PROPERTY);
        if (dtstamp) {
            icalproperty *lastmod = icalcomponent_get_first_property(comp, ICAL_LASTMODIFIED_PROPERTY);
            if (lastmod) {
                lastmodtime = icalproperty_get_lastmodified(lastmod);
                icalproperty_set_dtstamp(dtstamp, lastmodtime);
            }
        }
    }
    if (newEvent->m_subids.size() != 1) {
        SE_THROW("new CalDAV item did not contain exactly one VEVENT");
    }
    std::string subid = *newEvent->m_subids.begin();

    // Determine whether we already know the merged item even though
    // our caller didn't.
    std::string davLUID = luid;
    std::string knownSubID = callerSubID;
    if (davLUID.empty()) {
        auto it = m_cache.findByUID(newEvent->m_UID);
        if (it != m_cache.end()) {
            davLUID = it->first;
            knownSubID = subid;
        }
    }

    if (davLUID.empty()) {
        // New VEVENT; should not be part of an existing merged item
        // ("meeting series").
        //
        // If another app created a resource with the same UID,
        // then two things can happen:
        // 1. server merges the data (Google)
        // 2. adding the item is rejected (standard compliant CalDAV server)
        //
        // If the UID is truely new, then
        // 3. the server may rename the item
        //
        // The following code deals with case 3 and also covers case
        // 1, but our usual Google workarounds (for example, no
        // patching of SEQUENCE) were not applied and thus sending the
        // item might fail.
        //
        // Case 2 is not currently handled and causes the sync to fail.
        // This is in line with the current design ("concurrency detected,
        // causes error, fixed by trying again in slow sync").
        InsertItemResult res;
        // Yahoo expects resource names to match UID + ".ics".
        std::string name = newEvent->m_UID + ".ics";
        std::string buffer;
        SE_LOG_DEBUG(getDisplayName(), "inserting new VEVENT");
        eptr<char> icalstr(ical_strdup(icalcomponent_as_ical_string(newEvent->m_calendar)));
        std::string data = icalstr.get();
        res = insertItem(name, data, true);
        subres.m_mainid = res.m_luid;
        subres.m_uid = newEvent->m_UID;
        subres.m_subid = subid;
        subres.m_revision = res.m_revision;

        auto it = m_cache.find(res.m_luid);
        if (it != m_cache.end()) {
            // merge into existing Event
            Event &event = loadItem(*it->second);
            event.m_etag = res.m_revision;
            if (event.m_subids.find(subid) != event.m_subids.end()) {
                // was already in that item but caller didn't seem to know,
                // and now we replaced the data on the CalDAV server
                subres.m_state = ITEM_REPLACED;
            } else {
                // add to merged item
                event.m_subids.insert(subid);                
            }
            icalcomponent_merge_component(event.m_calendar,
                                          newEvent->m_calendar.release()); // function destroys merged calendar
        } else {
            // add to cache without further changes
            newEvent->m_DAVluid = res.m_luid;
            newEvent->m_etag = res.m_revision;
            m_cache[newEvent->m_DAVluid] = newEvent;
        }
    } else {
        if (!subid.empty() && subid != knownSubID) {
            SE_THROW(StringPrintf("new CalDAV item does not have right RECURRENCE-ID: item %s != expected %s",
                                  subid.c_str(), knownSubID.c_str()));
        }
        Event &event = loadItem(davLUID);

        if (subid.empty() && subid != knownSubID) {
            // fix incomplete iCalendar 2.0 item: should have had a RECURRENCE-ID
            icalcomponent *newcomp =
                icalcomponent_get_first_component(newEvent->m_calendar, ICAL_VEVENT_COMPONENT);
            icalproperty *prop = icalcomponent_get_first_property(newcomp, ICAL_RECURRENCEID_PROPERTY);
            if (prop) {
                icalcomponent_remove_property(newcomp, prop);
                icalproperty_free(prop);
            }

            // reconstruct RECURRENCE-ID with known value and TZID from start time of
            // the parent event or (if not found) the current event
            eptr<icalproperty> rid(icalproperty_new_recurrenceid(icaltime_from_string(knownSubID.c_str())),
                                   "new rid");
            icalproperty *dtstart = nullptr;
            icalcomponent *comp;
            // look for parent first
            for (comp = icalcomponent_get_first_component(event.m_calendar, ICAL_VEVENT_COMPONENT);
                 comp && !dtstart;
                 comp = icalcomponent_get_next_component(event.m_calendar, ICAL_VEVENT_COMPONENT)) {
                if (!icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY)) {
                    dtstart = icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
                }
            }
            // fall back to current event
            if (!dtstart) {
                dtstart = icalcomponent_get_first_property(newcomp, ICAL_DTSTART_PROPERTY);
            }
            // ignore missing TZID
            if (dtstart) {
                icalparameter *tzid = icalproperty_get_first_parameter(dtstart, ICAL_TZID_PARAMETER);
                if (tzid) {
                    icalproperty_set_parameter(rid, icalparameter_new_clone(tzid));
                }
            }

            // finally add RECURRENCE-ID and fix newEvent's meta information
            icalcomponent_add_property(newcomp, rid.release());
            subid = knownSubID;
            newEvent->m_subids.erase("");
            newEvent->m_subids.insert(subid);
        }

        // no changes expected yet, copy previous attributes
        subres.m_mainid = davLUID;
        subres.m_uid = event.m_UID;
        subres.m_subid = subid;
        subres.m_revision = event.m_etag;

        // Google hack: increase sequence number if smaller or equal to
        // sequence on server. Server rejects update otherwise.
        // See http://code.google.com/p/google-caldav-issues/issues/detail?id=26
        if (settings().googleUpdateHack()) {
            // always bump SEQ by one before PUT
            event.m_sequence++;
            if (newEvent->m_sequence < event.m_sequence) {
                // override in new event, existing ones will be updated below
                Event::setSequence(firstcomp, event.m_sequence);
            } else {
                // new event sequence is equal or higher, use that
                event.m_sequence = newEvent->m_sequence;
            }
        }

        // update cache: find old VEVENT and remove it before adding new one,
        // update last modified time of all other components
        icalcomponent *removeme = nullptr;
        for (icalcomponent *comp = icalcomponent_get_first_component(event.m_calendar, ICAL_VEVENT_COMPONENT);
             comp;
             comp = icalcomponent_get_next_component(event.m_calendar, ICAL_VEVENT_COMPONENT)) {
            if (Event::getSubID(comp) == subid) {
                removeme = comp;
            } else if (settings().googleUpdateHack()) {
                // increase modification time stamps to that of the new item,
                // Google rejects the whole update otherwise
                if (!icaltime_is_null_time(lastmodtime)) {
                    icalproperty *dtstamp = icalcomponent_get_first_property(comp, ICAL_DTSTAMP_PROPERTY);
                    if (dtstamp) {
                        icalproperty_set_dtstamp(dtstamp, lastmodtime);
                    }
                    icalproperty *lastmod = icalcomponent_get_first_property(comp, ICAL_LASTMODIFIED_PROPERTY);
                    if (lastmod) {
                        icalproperty_set_lastmodified(lastmod, lastmodtime);
                    }
                }
                // set SEQ to the one increased above
                Event::setSequence(comp, event.m_sequence);
            }
        }
        if (davLUID != luid) {
            // caller didn't know final UID: if found, then tell him to
            // merge the data and try again
            if (removeme) {
                subres.m_state = ITEM_NEEDS_MERGE;
                goto done;
            } else {
                event.m_subids.insert(subid);
            }
        } else {
            if (removeme) {
                // this is what we expect when the caller mentions the DAV LUID
                icalcomponent_remove_component(event.m_calendar, removeme);
                icalcomponent_free(removeme);
            } else {
                // caller confused?!
                SE_THROW("event not found");
            }
        }

        icalcomponent_merge_component(event.m_calendar,
                                      newEvent->m_calendar.release()); // function destroys merged calendar
        eptr<char> icalstr(ical_strdup(icalcomponent_as_ical_string(event.m_calendar)));
        std::string data = icalstr.get();

        // TODO: avoid updating item on server immediately?
        try {
            SE_LOG_DEBUG(getDisplayName(), "updating VEVENT");
            InsertItemResult res = insertItem(event.m_DAVluid, data, true);
            if (res.m_state != ITEM_OKAY ||
                res.m_luid != event.m_DAVluid) {
                // should not merge with anything, if so, our cache was invalid
                SE_THROW("CalDAV item not updated as expected");
            }
            event.m_etag = res.m_revision;
            subres.m_revision = event.m_etag;
        } catch (const TransportStatusException &ex) {
            if (ex.syncMLStatus() == 403 &&
                strstr(ex.what(), "You don't have access to change that event")) {
                // Google Calendar sometimes refuses writes for specific items,
                // typically meetings organized by someone else.
#if 1
                // Treat like a temporary, per item error to avoid aborting the
                // whole sync session. Doesn't really solve the problem (client
                // and server remain out of sync and will run into this again and
                // again), but better than giving up on all items or ignoring the
                // problem.
                SE_THROW_EXCEPTION_STATUS(StatusException,
                                          "CalDAV peer rejected updated with 403, keep trying",
                                          SyncMLStatus(417));
#else
                // Assume that the item hasn't changed and mark it as "merged".
                // This is incorrect. The 403 error has been seen in cases where
                // a detached recurrence had to be added to an existing meeting
                // series. Ignoring the problem means would keep the detached
                // recurrence out of the server permanently.
                SE_LOG_INFO(getDisplayName(), "%s: not updated because CalDAV server refused write access for it",
                            getSubDescription(event, subid).c_str());
                subres.m_merged = true;
                subres.m_revision = event.m_etag;
#endif
            } else if (ex.syncMLStatus() == 409 &&
                       strstr(ex.what(), "Can only store an event with a newer DTSTAMP")) {
                SE_LOG_DEBUG(NULL, "resending VEVENT with updated SEQUENCE/LAST-MODIFIED/DTSTAMP to work around 409");

                // Sometimes a PUT of two linked events updates one of them on the server
                // (visible in modified SEQUENCE and LAST-MODIFIED values) and then
                // fails with 409 because, presumably, the other item now has
                // too low SEQUENCE/LAST-MODIFIED/DTSTAMP values.
                //
                // An attempt with splitting the PUT in advance worked for some cases,
                // but then it still happened for others. So let's use brute force and
                // try again once more after reading the updated event anew.
                eptr<icalcomponent> fullcal = event.m_calendar;
                loadItem(event);
                event.m_sequence++;
                lastmodtime = icaltime_from_timet_with_zone(event.m_lastmodtime, false, icaltimezone_get_utc_timezone());
                event.m_calendar = fullcal;
                for (icalcomponent *comp = icalcomponent_get_first_component(event.m_calendar, ICAL_VEVENT_COMPONENT);
                     comp;
                     comp = icalcomponent_get_next_component(event.m_calendar, ICAL_VEVENT_COMPONENT)) {
                    if (!icaltime_is_null_time(lastmodtime)) {
                        icalproperty *dtstamp = icalcomponent_get_first_property(comp, ICAL_DTSTAMP_PROPERTY);
                        if (dtstamp) {
                            icalproperty_set_dtstamp(dtstamp, lastmodtime);
                        }
                        icalproperty *lastmod = icalcomponent_get_first_property(comp, ICAL_LASTMODIFIED_PROPERTY);
                        if (lastmod) {
                            icalproperty_set_lastmodified(lastmod, lastmodtime);
                        }
                    }
                    Event::setSequence(comp, event.m_sequence);
                }
                eptr<char> icalstr(ical_strdup(icalcomponent_as_ical_string(event.m_calendar)));
                std::string data = icalstr.get();
                InsertItemResult res = insertItem(event.m_DAVluid, data, true);
                if (res.m_state != ITEM_OKAY ||
                    res.m_luid != event.m_DAVluid) {
                    // should not merge with anything, if so, our cache was invalid
                    SE_THROW("CalDAV item not updated as expected");
                }
                event.m_etag = res.m_revision;
                subres.m_revision = event.m_etag;
            } else {
                throw;
            }
        }
    }

 done:
    return subres;
}

void CalDAVSource::readSubItem(const std::string &davLUID, const std::string &subid, std::string &item)
{
    Event &event = loadItem(davLUID);
    if (event.m_subids.size() == 1) {
        // simple case: convert existing VCALENDAR
        if (*event.m_subids.begin() == subid) {
            eptr<char> icalstr(ical_strdup(icalcomponent_as_ical_string(event.m_calendar)));
            item = icalstr.get();
        } else {
            SE_THROW("event not found");
        }
    } else {
        // complex case: create VCALENDAR with just the VTIMEZONE definition(s)
        // and the one event, then convert that
        eptr<icalcomponent> calendar(icalcomponent_new(ICAL_VCALENDAR_COMPONENT), "VCALENDAR");
        for (icalcomponent *tz = icalcomponent_get_first_component(event.m_calendar, ICAL_VTIMEZONE_COMPONENT);
             tz;
             tz = icalcomponent_get_next_component(event.m_calendar, ICAL_VTIMEZONE_COMPONENT)) {
            eptr<icalcomponent> clone(icalcomponent_new_clone(tz), "VTIMEZONE");
            icalcomponent_add_component(calendar, clone.release());
        }
        bool found = false;
        icalcomponent *parent = nullptr;
        for (icalcomponent *comp = icalcomponent_get_first_component(event.m_calendar, ICAL_VEVENT_COMPONENT);
             comp;
             comp = icalcomponent_get_next_component(event.m_calendar, ICAL_VEVENT_COMPONENT)) {
            if (Event::getSubID(comp) == subid) {
                eptr<icalcomponent> clone(icalcomponent_new_clone(comp), "VEVENT");
                if (subid.empty()) {
                    parent = clone.get();
                }
                icalcomponent_add_component(calendar, clone.release());
                found = true;
                break;
            }
        }

        if (!found) {
            SE_THROW("event not found");
        }

        // tell engine and peers about EXDATEs implied by
        // RECURRENCE-IDs in detached recurrences by creating
        // X-SYNCEVOLUTION-EXDATE-DETACHED in the parent
        if (parent && event.m_subids.size() > 1) {
            // remove all old X-SYNCEVOLUTION-EXDATE-DETACHED (just in case)
            removeSyncEvolutionExdateDetached(parent);

            // now populate with RECURRENCE-IDs of detached recurrences
            for (icalcomponent *comp = icalcomponent_get_first_component(event.m_calendar, ICAL_VEVENT_COMPONENT);
                 comp;
                 comp = icalcomponent_get_next_component(event.m_calendar, ICAL_VEVENT_COMPONENT)) {
                icalproperty *prop = icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
                if (prop) {
                    eptr<char> rid(ical_strdup(icalproperty_get_value_as_string(prop)));
                    icalproperty *exdate = icalproperty_new_from_string(StringPrintf("X-SYNCEVOLUTION-EXDATE-DETACHED:%s", rid.get()).c_str());
                    if (exdate) {
                        icalparameter *tzid = icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER);
                        if (tzid) {
                            icalproperty_add_parameter(exdate, icalparameter_new_clone(tzid));
                        }
                        icalcomponent_add_property(parent, exdate);
                    }
                }
            }
        }

        eptr<char> icalstr(ical_strdup(icalcomponent_as_ical_string(calendar)));
        item = icalstr.get();
    }
}

void CalDAVSource::Event::escapeRecurrenceID(std::string &data)
{
    boost::replace_all(data,
                       "\nRECURRENCE-ID",
                       "\nX-SYNCEVOLUTION-RECURRENCE-ID");
}

void CalDAVSource::Event::unescapeRecurrenceID(std::string &data)
{
    boost::replace_all(data,
                       "\nX-SYNCEVOLUTION-RECURRENCE-ID",
                       "\nRECURRENCE-ID");
}

std::string CalDAVSource::removeSubItem(const string &davLUID, const std::string &subid)
{
    auto it = m_cache.find(davLUID);
    if (it == m_cache.end()) {
        // gone already
        throwError(SE_HERE, STATUS_NOT_FOUND, "deleting item: " + davLUID);
        return "";
    }
    // use item as it is, load only if it is not going to be removed entirely
    Event &event = *it->second;

    if (event.m_subids.size() == 1) {
        // remove entire merged item, nothing will be left after removal
        if (*event.m_subids.begin() != subid) {
            SE_LOG_DEBUG(getDisplayName(), "%s: request to remove the %s recurrence: only the %s recurrence exists",
                         davLUID.c_str(),
                         SubIDName(subid).c_str(),
                         SubIDName(*event.m_subids.begin()).c_str());
            throwError(SE_HERE, STATUS_NOT_FOUND, "remove sub-item: " + SubIDName(subid) + " in " + davLUID);
            return event.m_etag;
        } else {
            try {
                removeItem(event.m_DAVluid);
            } catch (const TransportStatusException &ex) {
                if (ex.syncMLStatus() == 409 &&
                    strstr(ex.what(), "Can't delete a recurring event")) {
                    // Google CalDAV:
                    // HTTP/1.1 409 Can't delete a recurring event except on its organizer's calendar
                    //
                    // Workaround: remove RRULE and EXDATE before deleting
                    bool updated = false;
                    icalcomponent *comp = icalcomponent_get_first_component(event.m_calendar, ICAL_VEVENT_COMPONENT);
                    if (comp) {
                        icalproperty *prop;
                        while ((prop = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY)) != nullptr) {
                            icalcomponent_remove_property(comp, prop);
                            icalproperty_free(prop);
                            updated = true;
                        }
                        while ((prop = icalcomponent_get_first_property(comp, ICAL_EXDATE_PROPERTY)) != nullptr) {
                            icalcomponent_remove_property(comp, prop);
                            icalproperty_free(prop);
                            updated = true;
                        }
                    }
                    if (updated) {
                        SE_LOG_DEBUG(getDisplayName(), "Google recurring event delete hack: remove RRULE before deleting");
                        eptr<char> icalstr(ical_strdup(icalcomponent_as_ical_string(event.m_calendar)));
                        insertSubItem(davLUID, subid, icalstr.get());
                        // It has been observed that trying the DELETE immediately
                        // failed again with the same "Can't delete a recurring event"
                        // error although the event no longer has an RRULE. Seems
                        // that the Google server sometimes need a bit of time until
                        // changes really trickle through all databases. Let's
                        // try a few times before giving up.
                        for (int retry = 0; retry < 5; retry++) {
                            try {
                                SE_LOG_DEBUG(getDisplayName(), "Google recurring event delete hack: remove event, attempt #%d", retry);
                                removeSubItem(davLUID, subid);
                                break;
                            } catch (const TransportStatusException &ex2) {
                                if (ex2.syncMLStatus() == 409 &&
                                    strstr(ex2.what(), "Can't delete a recurring event")) {
                                    SE_LOG_DEBUG(getDisplayName(), "Google recurring event delete hack: try again in a second");
                                    Sleep(1);
                                } else {
                                    throw;
                                }
                            }
                        }
                    } else {
                        SE_LOG_DEBUG(getDisplayName(), "Google recurring event delete hack not applicable, giving up");
                        throw;
                    }
                } else {
                    throw;
                }
            }
        }
        m_cache.erase(davLUID);
        return "";
    } else {
        loadItem(event);
        bool found = false;
        for (icalcomponent *comp = icalcomponent_get_first_component(event.m_calendar, ICAL_VEVENT_COMPONENT);
             comp;
             comp = icalcomponent_get_next_component(event.m_calendar, ICAL_VEVENT_COMPONENT)) {
            if (Event::getSubID(comp) == subid) {
                icalcomponent_remove_component(event.m_calendar, comp);
                icalcomponent_free(comp);
                found = true;
            }
        }
        if (!found) {
            throwError(SE_HERE, STATUS_NOT_FOUND, "remove sub-item: " + SubIDName(subid) + " in " + davLUID);
            return event.m_etag;
        }
        event.m_subids.erase(subid);
        // TODO: avoid updating the item immediately
        eptr<char> icalstr(ical_strdup(icalcomponent_as_ical_string(event.m_calendar)));
        InsertItemResult res = insertItem(davLUID, icalstr.get(), true);
        if (res.m_state != ITEM_OKAY ||
            res.m_luid != davLUID) {
            SE_THROW("unexpected result of removing sub event");
        }
        event.m_etag = res.m_revision;
        return event.m_etag;
    }
}

void CalDAVSource::removeMergedItem(const std::string &davLUID)
{
    auto it = m_cache.find(davLUID);
    if (it == m_cache.end()) {
        // gone already, no need to do anything
        SE_LOG_DEBUG(getDisplayName(), "%s: ignoring request to delete non-existent item",
                     davLUID.c_str());
        return;
    }
    // use item as it is, load only if it is not going to be removed entirely
    Event &event = *it->second;

    // remove entire merged item, nothing will be left after removal
    try {
        removeItem(event.m_DAVluid);
    } catch (const TransportStatusException &ex) {
        if (ex.syncMLStatus() == 409 &&
            strstr(ex.what(), "Can't delete a recurring event")) {
            // Google CalDAV:
            // HTTP/1.1 409 Can't delete a recurring event except on its organizer's calendar
            //
            // Workaround: use the workarounds from removeSubItem()
            std::set<std::string> subids = event.m_subids;
            for (const auto &luid: reverse(subids)) {
                removeSubItem(davLUID, luid);
            }
        } else {
            throw;
        }
    }

    m_cache.erase(davLUID);
}

void CalDAVSource::flushItem(const string &davLUID)
{
    // TODO: currently we always flush immediately, so no need to send data here
    auto it = m_cache.find(davLUID);
    if (it != m_cache.end()) {
        it->second->m_calendar.set(nullptr);
    }
}

std::string CalDAVSource::getSubDescription(const string &davLUID, const string &subid)
{
    auto it = m_cache.find(davLUID);
    if (it == m_cache.end()) {
        // unknown item, return empty string for fallback
        return "";
    } else {
        return getSubDescription(*it->second, subid);
    }
}

std::string CalDAVSource::getSubDescription(Event &event, const string &subid)
{
    if (!event.m_calendar) {
        // Don't load (expensive!) only to provide the description.
        // Returning an empty string will trigger the fallback (logging the ID).
        return "";
    }
    for (icalcomponent *comp = icalcomponent_get_first_component(event.m_calendar, ICAL_VEVENT_COMPONENT);
         comp;
         comp = icalcomponent_get_next_component(event.m_calendar, ICAL_VEVENT_COMPONENT)) {
        if (Event::getSubID(comp) == subid) {
            std::string descr;

            const char *summary = icalcomponent_get_summary(comp);
            if (summary && summary[0]) {
                descr += summary;
            }
        
            if (true /* is event */) {
                const char *location = icalcomponent_get_location(comp);
                if (location && location[0]) {
                    if (!descr.empty()) {
                        descr += ", ";
                    }
                    descr += location;
                }
            }
            // TODO: other item types
            return descr;
        }
    }
    return "";
}

std::string CalDAVSource::getDescription(const string &luid)
{
    StringPair ids = MapSyncSource::splitLUID(luid);
    return getSubDescription(ids.first, ids.second);
}

CalDAVSource::Event &CalDAVSource::findItem(const std::string &davLUID)
{
    auto it = m_cache.find(davLUID);
    if (it == m_cache.end()) {
        throwError(SE_HERE, STATUS_NOT_FOUND, "finding item: " + davLUID);
    }
    return *it->second;
}

CalDAVSource::Event &CalDAVSource::loadItem(const std::string &davLUID)
{
    Event &event = findItem(davLUID);
    return loadItem(event);
}

CalDAVSource::Event &CalDAVSource::loadItem(Event &event)
{
    if (!event.m_calendar) {
        std::string item;
        try {
            readItem(event.m_DAVluid, item, true);
        } catch (const TransportStatusException &ex) {
            if (ex.syncMLStatus() == 404) {
                // Someone must have created a detached recurrence on
                // the server without the master event. We avoid that
                // with the "Google Child Hack", but have no control
                // over other clients. So let's deal with this problem
                // after logging it.
                Exception::log();

                // We know about the event because it showed up in a REPORT.
                // So let's use such a REPORT to retrieve the desired item.
                // Not as efficient as a GET (and thus not the default), but
                // so be it.
#if 0
                // This would be fairly efficient, but runs into the same 404 error as a GET.
                std::string query =
                    StringPrintf("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
                                 "<C:calendar-multiget xmlns:D=\"DAV:\"\n"
                                 "   xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n"
                                 "<D:prop>\n"
                                 "   <C:calendar-data/>\n"
                                 "</D:prop>\n"
                                 "<D:href><[CDATA[%s]]></D:href>\n"
                                 "</C:calendar-multiget>",
                                 event.m_DAVluid.c_str());
                Neon::XMLParser parser;
                std::string href, etag;
                item = "";
                parser.initReportParser(href, etag);
                parser.pushHandler(Neon::XMLParser::accept("urn:ietf:params:xml:ns:caldav", "calendar-data"),
                                   Neon::XMLParser::append(item));
                Neon::Request report(*getSession(), "REPORT", getCalendar().m_path, query, parser);
                report.addHeader("Content-Type", "application/xml; charset=\"utf-8\"");
                report.run();
#else
                std::string query =
                    StringPrintf("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
                                 "<C:calendar-query xmlns:D=\"DAV:\"\n"
                                 "xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n"
                                 "<D:prop>\n"
                                 "<D:getetag/>\n"
                                 "<C:calendar-data/>\n"
                                 "</D:prop>\n"
                                 // filter expected by Yahoo! Calendar
                                 "<C:filter>\n"
                                 "<C:comp-filter name=\"VCALENDAR\">\n"
                                 "<C:comp-filter name=\"VEVENT\">\n"
                                 "<C:prop-filter name=\"UID\">\n"
                                 "<C:text-match collation=\"i;octet\"><![CDATA[%s]]></C:text-match>\n"
                                 "</C:prop-filter>\n"
                                 "</C:comp-filter>\n"
                                 "</C:comp-filter>\n"
                                 "</C:filter>\n"
                                 "</C:calendar-query>\n",
                                 event.m_UID.c_str());
                Timespec deadline = createDeadline();
                getSession()->startOperation("REPORT 'single item'", deadline);
                while (true) {
                    Neon::XMLParser parser;
                    std::string data;

                    auto store = [this, &event, &item, &data] (const std::string &href, const std::string &etag, const std::string &status) {
                        std::string luid = path2luid(Neon::URI::parse(href).m_path);
                        if (luid == event.m_DAVluid) {
                            SE_LOG_DEBUG(NULL, "got item %s via REPORT fallback", luid.c_str());
                            item = data;
                        }
                        data.clear();
                        return 0;
                    };
                    parser.initReportParser(store);
                    parser.pushHandler(Neon::XMLParser::accept("urn:ietf:params:xml:ns:caldav", "calendar-data"),
                                       Neon::XMLParser::append(data));
                    Neon::Request report(*getSession(), "REPORT", getCalendar().m_path, query, parser);
                    report.addHeader("Depth", "1");
                    report.addHeader("Content-Type", "application/xml; charset=\"utf-8\"");
                    if (report.run()) {
                        break;
                    }
                }
#endif
            } else {
                throw;
            }
        }
        Event::unescapeRecurrenceID(item);
        event.m_calendar.set(icalcomponent_new_from_string((char *)item.c_str()), // hack for old libical
                             "parsing iCalendar 2.0");
        Event::fixIncomingCalendar(event.m_calendar.get());

        // Sequence number/last-modified might have been increased by last save.
        // Or the cache was populated by setAllSubItems(), which doesn't give
        // us the information. In that case, UID might also still be unknown.
        // Either way, check it again.
        for (icalcomponent *comp = icalcomponent_get_first_component(event.m_calendar, ICAL_VEVENT_COMPONENT);
             comp;
             comp = icalcomponent_get_next_component(event.m_calendar, ICAL_VEVENT_COMPONENT)) {
            if (event.m_UID.empty()) {
                event.m_UID = Event::getUID(comp);
            }
            long sequence = Event::getSequence(comp);
            if (sequence > event.m_sequence) {
                event.m_sequence = sequence;
            }
            icalproperty *lastmod = icalcomponent_get_first_property(comp, ICAL_LASTMODIFIED_PROPERTY);
            if (lastmod) {
                icaltimetype lastmodtime = icalproperty_get_lastmodified(lastmod);
                time_t mod = icaltime_as_timet(lastmodtime);
                if (mod > event.m_lastmodtime) {
                    event.m_lastmodtime = mod;
                }
            }
        }
    }
    return event;
}

void CalDAVSource::Event::fixIncomingCalendar(icalcomponent *calendar)
{
    // Evolution has a problem when the parent event uses a time
    // zone and the RECURRENCE-ID uses UTC (can happen in Exchange
    // meeting invitations): then Evolution and/or libical do not
    // recognize that the detached recurrence overrides the
    // regular recurrence and display both.
    //
    // As a workaround, remember time zone of DTSTART in parent event
    // in the first loop iteration. Then below transform the RECURRENCE-ID
    // time.
    bool ridInUTC = false;
    const icaltimezone *zone = nullptr;

    for (icalcomponent *comp = icalcomponent_get_first_component(calendar, ICAL_VEVENT_COMPONENT);
         comp;
         comp = icalcomponent_get_next_component(calendar, ICAL_VEVENT_COMPONENT)) {
        // remember whether we need to convert RECURRENCE-ID
        struct icaltimetype rid = icalcomponent_get_recurrenceid(comp);
        if (icaltime_is_utc(rid)) {
            ridInUTC = true;
        }

        // is parent event? -> remember time zone unless it is UTC
        static const struct icaltimetype null = { 0 };
        if (!memcmp(&rid, &null, sizeof(null))) {
            struct icaltimetype dtstart = icalcomponent_get_dtstart(comp);
            if (!icaltime_is_utc(dtstart)) {
                zone = icaltime_get_timezone(dtstart);
            }
        }

        // remove useless X-LIC-ERROR
        icalproperty *prop = icalcomponent_get_first_property(comp, ICAL_ANY_PROPERTY);
        while (prop) {
            icalproperty *next = icalcomponent_get_next_property(comp, ICAL_ANY_PROPERTY);
            const char *name = icalproperty_get_property_name(prop);
            if (name && !strcmp("X-LIC-ERROR", name)) {
                icalcomponent_remove_property(comp, prop);
                icalproperty_free(prop);
            }
            prop = next;
        }
    }

    // now update RECURRENCE-ID?
    if (zone && ridInUTC) {
        for (icalcomponent *comp = icalcomponent_get_first_component(calendar, ICAL_VEVENT_COMPONENT);
             comp;
             comp = icalcomponent_get_next_component(calendar, ICAL_VEVENT_COMPONENT)) {
            icalproperty *prop = icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
            if (prop) {
                struct icaltimetype rid = icalproperty_get_recurrenceid(prop);
                if (icaltime_is_utc(rid)) {
                    rid = icaltime_convert_to_zone(rid, const_cast<icaltimezone *>(zone)); // icaltime_convert_to_zone should take a "const timezone" but doesn't
                    icalproperty_set_recurrenceid(prop, rid);
                    icalproperty_remove_parameter_by_kind(prop, ICAL_TZID_PARAMETER);
                    icalparameter *param = icalparameter_new_from_value_string(ICAL_TZID_PARAMETER,
                                                                               icaltimezone_get_tzid(const_cast<icaltimezone *>(zone)));
                    icalproperty_set_parameter(prop, param);
                }
            }
        }
    }
}

std::string CalDAVSource::Event::icalTime2Str(const icaltimetype &tt)
{
    static const struct icaltimetype null = { 0 };
    if (!memcmp(&tt, &null, sizeof(null))) {
        return "";
    } else {
        eptr<char> timestr(ical_strdup(icaltime_as_ical_string(tt)));
        if (!timestr) {
            SE_THROW("cannot convert to time string");
        }
        return timestr.get();
    }
}

std::string CalDAVSource::Event::getSubID(icalcomponent *comp)
{
    struct icaltimetype rid = icalcomponent_get_recurrenceid(comp);
    return icalTime2Str(rid);
}

std::string CalDAVSource::Event::getUID(icalcomponent *comp)
{
    std::string uid;
    icalproperty *prop = icalcomponent_get_first_property(comp, ICAL_UID_PROPERTY);
    if (prop) {
        uid = icalproperty_get_uid(prop);
    }
    return uid;
}

void CalDAVSource::Event::setUID(icalcomponent *comp, const std::string &uid)
{
    icalproperty *prop = icalcomponent_get_first_property(comp, ICAL_UID_PROPERTY);
    if (prop) {
        icalproperty_set_uid(prop, uid.c_str());
    } else {
        icalcomponent_add_property(comp, icalproperty_new_uid(uid.c_str()));
    }
}

int CalDAVSource::Event::getSequence(icalcomponent *comp)
{
    int sequence = 0;
    icalproperty *prop = icalcomponent_get_first_property(comp, ICAL_SEQUENCE_PROPERTY);
    if (prop) {
        sequence = icalproperty_get_sequence(prop);
    }
    return sequence;
}

void CalDAVSource::Event::setSequence(icalcomponent *comp, int sequence)
{
    icalproperty *prop = icalcomponent_get_first_property(comp, ICAL_SEQUENCE_PROPERTY);
    if (prop) {
        icalproperty_set_sequence(prop, sequence);
    } else {
        icalcomponent_add_property(comp, icalproperty_new_sequence(sequence));
    }
}

CalDAVSource::EventCache::iterator CalDAVSource::EventCache::findByUID(const std::string &uid)
{
    for (iterator it = begin();
         it != end();
         ++it) {
        if (it->second->m_UID == uid) {
            return it;
        }
    }
    return end();
}

void CalDAVSource::backupData(const SyncSource::Operations::ConstBackupInfo &oldBackup,
                              const SyncSource::Operations::BackupInfo &newBackup,
                              BackupReport &backupReport)
{
    contactServer();

    // If this runs as part of the sync preparations, then we might
    // use the result to populate our m_cache. But because dumping
    // data is typically disabled, this optimization isn't really
    // worth that much.

    ItemCache cache;
    cache.init(oldBackup, newBackup, false);

    // stream directly from REPORT with full data into backup
    const std::string query =
        "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
        "<C:calendar-query xmlns:D=\"DAV:\"\n"
        "xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n"
        "<D:prop>\n"
        "<D:getetag/>\n"
        "<C:calendar-data/>\n"
        "</D:prop>\n"
        // filter expected by Yahoo! Calendar
        "<C:filter>\n"
        "<C:comp-filter name=\"VCALENDAR\">\n"
        "<C:comp-filter name=\"VEVENT\">\n"
        "</C:comp-filter>\n"
        "</C:comp-filter>\n"
        "</C:filter>\n"
        "</C:calendar-query>\n";
    string data;
    Neon::XMLParser parser;
    auto process = [this, &cache, &data] (const std::string &href, const std::string &etag, const std::string &status) {
        // detect and ignore empty items, like we do in appendItem()
        eptr<icalcomponent> calendar(icalcomponent_new_from_string((char *)data.c_str()), // cast is a hack for broken definition in old libical
                                     "iCalendar 2.0");
        if (icalcomponent_get_first_component(calendar, ICAL_VEVENT_COMPONENT)) {
            Event::unescapeRecurrenceID(data);
            std::string luid = path2luid(Neon::URI::parse(href).m_path);
            std::string rev = ETag2Rev(etag);
            cache.backupItem(data, luid, rev);
        } else {
            SE_LOG_DEBUG(NULL, "ignoring broken item %s during backup (is empty)", href.c_str());
        }

        // reset data for next item
        data.clear();
        return 0;
    };
    parser.initReportParser(process);
    parser.pushHandler(Neon::XMLParser::accept("urn:ietf:params:xml:ns:caldav", "calendar-data"),
                       Neon::XMLParser::append(data));
    Timespec deadline = createDeadline();
    getSession()->startOperation("REPORT 'full calendar'", deadline);
    while (true) {
        Neon::Request report(*getSession(), "REPORT", getCalendar().m_path, query, parser);
        report.addHeader("Depth", "1");
        report.addHeader("Content-Type", "application/xml; charset=\"utf-8\"");
        if (report.run()) {
            break;
        }
        cache.reset();
    }
    cache.finalize(backupReport);
}


void CalDAVSource::restoreData(const SyncSource::Operations::ConstBackupInfo &oldBackup,
                               bool dryrun,
                               SyncSourceReport &report)
{
    // TODO: implement restore
    throw("not implemented");
}

bool CalDAVSource::typeMatches(const StringMap &props) const
{
    auto it = props.find("urn:ietf:params:xml:ns:caldav:supported-calendar-component-set");
    if (it != props.end() &&
        it->second.find("<urn:ietf:params:xml:ns:caldavcomp name='VEVENT'></urn:ietf:params:xml:ns:caldavcomp>") != std::string::npos) {
        return true;
    } else {
        return false;
    }
}

SE_END_CXX

#endif // ENABLE_DAV
