/*
 * Copyright (C) 2005-2009 Patrick Ohly <patrick.ohly@gmx.de>
 * Copyright (C) 2009 Intel Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) version 3.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#include <memory>
#include <map>
#include <sstream>
#include <list>

#include "config.h"
#include "EvolutionSyncSource.h"
#include <syncevo/IdentityProvider.h>

#ifdef ENABLE_EBOOK

#include "EvolutionContactSource.h"
#include <syncevo/util.h>
#include <syncevo/Exception.h>
#include <syncevo/Logging.h>

#ifdef USE_EDS_CLIENT
#include <boost/range/algorithm/find.hpp>
#endif
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/join.hpp>
#include <boost/algorithm/string/predicate.hpp>

#include <syncevo/declarations.h>

SE_GLIB_TYPE(EBookQuery, e_book_query)

SE_BEGIN_CXX

inline bool IsContactNotFound(const GError *gerror) {
    return gerror &&
#ifdef USE_EDS_CLIENT
        gerror->domain == E_BOOK_CLIENT_ERROR &&
        gerror->code == E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND
#else
        gerror->domain == E_BOOK_ERROR &&
        gerror->code == E_BOOK_ERROR_CONTACT_NOT_FOUND
#endif
        ;
}


const EvolutionContactSource::extensions EvolutionContactSource::m_vcardExtensions;
const EvolutionContactSource::unique EvolutionContactSource::m_uniqueProperties;

EvolutionContactSource::EvolutionContactSource(const SyncSourceParams &params,
                                               EVCardFormat vcardFormat) :
    EvolutionSyncSource(params),
    m_vcardFormat(vcardFormat)
{
#ifdef USE_EDS_CLIENT
    m_cacheMisses =
        m_cacheStalls =
        m_contactReads =
        m_contactsFromDB =
        m_contactQueries = 0;
    m_readAheadOrder = READ_NONE;
    const char *mode = getEnv("SYNCEVOLUTION_EDS_ACCESS_MODE", "");
    m_accessMode = boost::iequals(mode, "synchronous") ? SYNCHRONOUS :
        boost::iequals(mode, "batched") ? BATCHED :
        DEFAULT;
#endif
    SyncSourceLogging::init(InitList<std::string>("N_FIRST") + "N_MIDDLE" + "N_LAST",
                            " ",
                            m_operations);
}

EvolutionContactSource::~EvolutionContactSource()
{
    // Don't close while we have pending operations.  They might
    // complete after we got destroyed, causing them to use an invalid
    // "this" pointer. We also don't know how well EDS copes with
    // closing the address book while it has pending operations - EDS
    // maintainer mcrha wasn't sure.
    //
    // TODO: cancel the operations().
    finishItemChanges();
    close();

#ifdef USE_EDS_CLIENT
    // logCacheStats(Logger::DEBUG);
#endif
}

#ifdef USE_EDS_CLIENT
static EClient *newEBookClient(ESource *source,
                               GError **gerror)
{
    return E_CLIENT(e_book_client_new(source, gerror));
}
#endif

EvolutionSyncSource::Databases EvolutionContactSource::getDatabases()
{
    Databases result;
#ifdef USE_EDS_CLIENT
    getDatabasesFromRegistry(result,
                             E_SOURCE_EXTENSION_ADDRESS_BOOK,
                             e_source_registry_ref_default_address_book);
#else
    ESourceList *sources = nullptr;
    if (!e_book_get_addressbooks(&sources, nullptr)) {
        Exception::throwError(SE_HERE, "unable to access address books");
    }

    Databases secondary;
    for (GSList *g = e_source_list_peek_groups (sources); g; g = g->next) {
        ESourceGroup *group = E_SOURCE_GROUP (g->data);
        for (GSList *s = e_source_group_peek_sources (group); s; s = s->next) {
            ESource *source = E_SOURCE (s->data);
            eptr<char> uri(e_source_get_uri(source));
            string uristr;
            if (uri) {
                uristr = uri.get();
            }
            Database entry(e_source_peek_name(source),
                           uristr,
                           false);
            if (boost::starts_with(uristr, "couchdb://")) {
                // Append CouchDB address books at the end of the list,
                // otherwise preserving the order of address books.
                //
                // The reason is Moblin Bugzilla #7877 (aka CouchDB
                // feature request #479110): the initial release of
                // evolution-couchdb in Ubuntu 9.10 is unusable because
                // it does not support the REV property.
                //
                // Reordering the entries ensures that the CouchDB
                // address book is not used as the default database by
                // SyncEvolution, as it happened in Ubuntu 9.10.
                // Users can still pick it intentionally via
                // "evolutionsource".
                secondary.push_back(entry);
            } else {
                result.push_back(entry);
            }
        }
    }
    result.insert(result.end(), secondary.begin(), secondary.end());

    // No results? Try system address book (workaround for embedded Evolution Dataserver).
    if (!result.size()) {
        eptr<EBook, GObject> book;
        GErrorCXX gerror;
        const char *name;

        name = "<<system>>";
        book = e_book_new_system_addressbook (gerror);
        gerror.clear();
        if (!book) {
            name = "<<default>>";
            book = e_book_new_default_addressbook (gerror);
        }

        if (book) {
            const char *uri = e_book_get_uri (book);
            result.push_back(Database(name, uri, true));
        }
    } else {
        //  the first DB found is the default
        result[0].m_isDefault = true;
    }
#endif

    return result;
}

void EvolutionContactSource::open()
{
#ifdef USE_EDS_CLIENT
    m_addressbook.reset(E_BOOK_CLIENT(openESource(E_SOURCE_EXTENSION_ADDRESS_BOOK,
                                                  e_source_registry_ref_builtin_address_book,
                                                  newEBookClient).get()));
#else
    GErrorCXX gerror;
    bool created = false;
    bool onlyIfExists = false; // always try to create address book, because even if there is
                               // a source there's no guarantee that the actual database was
                               // created already; the original logic below for only setting
                               // this when explicitly requesting a new address book
                               // therefore failed in some cases
    ESourceList *tmp;
    if (!e_book_get_addressbooks(&tmp, gerror)) {
        throwError(SE_HERE, "unable to access address books", gerror);
    }
    ESourceListCXX sources(tmp, TRANSFER_REF);

    string id = getDatabaseID();
    ESource *source = findSource(sources, id);
    if (!source) {
        // might have been special "<<system>>" or "<<default>>", try that and
        // creating address book from file:// URI before giving up
        if (id.empty() || id == "<<system>>") {
            m_addressbook.set( e_book_new_system_addressbook (gerror), "system address book" );
        } else if (id.empty() || id == "<<default>>") {
            m_addressbook.set( e_book_new_default_addressbook (gerror), "default address book" );
        } else if (boost::starts_with(id, "file://")) {
            m_addressbook.set(e_book_new_from_uri(id.c_str(), gerror), "creating address book");
        } else {
            throwError(SE_HERE, string(getName()) + ": no such address book: '" + id + "'");
        }
        created = true;
    } else {
        m_addressbook.set( e_book_new( source, gerror ), "address book" );
    }
 
    if (!e_book_open( m_addressbook, onlyIfExists, gerror) ) {
        if (created) {
            // opening newly created address books often fails, try again once more
            sleep(5);
            if (!e_book_open(m_addressbook, onlyIfExists, gerror)) {
                throwError(SE_HERE, "opening address book", gerror);
            }
        } else {
            throwError(SE_HERE, "opening address book", gerror);
        }
    }

    // users are not expected to configure an authentication method,
    // so pick one automatically if the user indicated that he wants authentication
    // by setting user or password
    UserIdentity identity = getUser();
    InitStateString passwd = getPassword();
    if (identity.wasSet() || passwd.wasSet()) {
        GList *authmethod;
        if (!e_book_get_supported_auth_methods(m_addressbook, &authmethod, gerror)) {
            throwError(SE_HERE, "getting authentication methods", gerror);
        }
        while (authmethod) {
            // map identity + password to plain username/password credentials
            Credentials cred = IdentityProviderCredentials(identity, passwd);
            const char *method = (const char *)authmethod->data;
            SE_LOG_DEBUG(getDisplayName(), "trying authentication method \"%s\", user %s, password %s",
                         method,
                         identity.wasSet() ? "configured" : "not configured",
                         passwd.wasSet() ? "configured" : "not configured");
            if (e_book_authenticate_user(m_addressbook,
                                         cred.m_username.c_str(),
                                         cred.m_password.c_str(),
                                         method,
                                         gerror)) {
                SE_LOG_DEBUG(getDisplayName(), "authentication succeeded");
                break;
            } else {
                SE_LOG_ERROR(getDisplayName(), "authentication failed: %s", gerror->message);
            }
            authmethod = authmethod->next;
        }
    }

    g_signal_connect_after(m_addressbook,
                           "backend-died",
                           G_CALLBACK(Exception::fatalError),
                           (void *)"Evolution Data Server has died unexpectedly, contacts no longer available.");
#endif
}

bool EvolutionContactSource::isEmpty()
{
    // TODO: add more efficient implementation which does not
    // depend on actually pulling all items from EDS
    RevisionMap_t revisions;
    listAllItems(revisions);
    return revisions.empty();
}

#ifdef USE_EDS_CLIENT
class EBookClientViewSyncHandler {
    public:
        typedef std::function<void (const GSList *list)> Process_t;

        EBookClientViewSyncHandler(const EBookClientViewCXX &view,
                                   const Process_t &process) :
            m_process(process),
            m_view(view)
        {}

        bool process(GErrorCXX &gerror) {
            // Listen for view signals
            m_view.connectSignal<EBookClientView *,
                                 const GSList *>()("objects-added",
                                                   [this] (EBookClientView *, const GSList *list) { m_process(list); } );
            m_view.connectSignal<EBookClientView *,
                                 const GError *>()("complete",
                                                   [this] (EBookClientView *, const GError *gerror) { completed(gerror); });

            // Start the view
            e_book_client_view_start (m_view, m_error);
            if (m_error) {
                std::swap(gerror, m_error);
                return false;
            }

            // Async -> Sync
            m_loop.run();
            e_book_client_view_stop (m_view, nullptr); 

            if (m_error) {
                std::swap(gerror, m_error);
                return false;
            } else {
                return true;
            }
        }
 
        void completed(const GError *error) {
            m_error = error;
            m_loop.quit();
        }

    public:
        // Event loop for Async -> Sync
        EvolutionAsync m_loop;

    private:
        // Process list callback
        std::function<void (const GSList *list)> m_process;
        // View watched
        EBookClientViewCXX m_view;
        // Possible error while watching the view
        GErrorCXX m_error;
};
#endif

void EvolutionContactSource::listAllItems(RevisionMap_t &revisions)
{
#ifdef USE_EDS_CLIENT
    GErrorCXX gerror;
    EBookClientView *view;

    EBookQueryCXX allItemsQuery(e_book_query_any_field_contains(""), TRANSFER_REF);
    PlainGStr buffer(e_book_query_to_string (allItemsQuery.get()));
    const char *sexp = getenv("SYNCEVOLUTION_EBOOK_QUERY");
    if (sexp) {
        SE_LOG_INFO(NULL, "restricting item set to items matching %s", sexp);
    } else {
        sexp = buffer;
    }

    if (!e_book_client_get_view_sync(m_addressbook, sexp, &view, nullptr, gerror)) {
        throwError(SE_HERE, "getting the view" , gerror);
    }
    EBookClientViewCXX viewPtr = EBookClientViewCXX::steal(view);

    // Optimization: set fields_of_interest (UID / REV)
    GListCXX<const char, GSList> interesting_field_list;
    interesting_field_list.push_back(e_contact_field_name (E_CONTACT_UID));
    interesting_field_list.push_back(e_contact_field_name (E_CONTACT_REV));
    e_book_client_view_set_fields_of_interest (viewPtr, interesting_field_list, gerror);
    if (gerror) {
        SE_LOG_ERROR(getDisplayName(), "e_book_client_view_set_fields_of_interest: %s", (const char*)gerror);
        gerror.clear();
    }

    auto process = [&revisions] (const GSList *contacts) {
        const GSList *l;

        for (l = contacts; l; l = l->next) {
            EContact *contact = E_CONTACT(l->data);
            if (!contact) {
                SE_THROW("contact entry without data");
            }
            const char *uid = (const char *)e_contact_get_const(contact,
                                                                E_CONTACT_UID);
            if (!uid || !uid[0]) {
                SE_THROW("contact entry without UID");
            }
            const char *rev = (const char *)e_contact_get_const(contact,
                                                                E_CONTACT_REV);
            if (!rev || !rev[0]) {
                SE_THROW(string("contact entry without REV: ") + uid);
            }
            revisions[uid] = rev;
        }
    };
    EBookClientViewSyncHandler handler(viewPtr, process);
    if (!handler.process(gerror)) {
        throwError(SE_HERE, "watching view", gerror);
    }
#else
    GErrorCXX gerror;
    eptr<EBookQuery> allItemsQuery(e_book_query_any_field_contains(""), "query");
    GList *nextItem;
    if (!e_book_get_contacts(m_addressbook, allItemsQuery, &nextItem, gerror)) {
        throwError(SE_HERE, "reading all items", gerror );
    }
    eptr<GList> listptr(nextItem);
    while (nextItem) {
        EContact *contact = E_CONTACT(nextItem->data);
        if (!contact) {
            throwError(SE_HERE, "contact entry without data");
        }
        pair<string, string> revmapping;
        const char *uid = (const char *)e_contact_get_const(contact,
                                                            E_CONTACT_UID);
        if (!uid || !uid[0]) {
            throwError(SE_HERE, "contact entry without UID");
        }
        revmapping.first = uid;
        const char *rev = (const char *)e_contact_get_const(contact,
                                                            E_CONTACT_REV);
        if (!rev || !rev[0]) {
            throwError(SE_HERE, string("contact entry without REV: ") + revmapping.first);
        }
        revmapping.second = rev;
        revisions.insert(revmapping);
        nextItem = nextItem->next;
    }
#endif
}

void EvolutionContactSource::close()
{
    m_addressbook.reset();
}

string EvolutionContactSource::getRevision(const string &luid)
{
    if (!needChanges()) {
        return "";
    }

    EContact *contact;
    GErrorCXX gerror;
    if (
#ifdef USE_EDS_CLIENT
        !e_book_client_get_contact_sync(m_addressbook,
                                        luid.c_str(),
                                        &contact,
                                        nullptr,
                                        gerror)
#else
        !e_book_get_contact(m_addressbook,
                            luid.c_str(),
                            &contact,
                            gerror)
#endif
        ) {
        if (IsContactNotFound(gerror)) {
            throwError(SE_HERE, STATUS_NOT_FOUND, string("retrieving item: ") + luid);
        } else {
            throwError(SE_HERE, string("reading contact ") + luid,
                       gerror);
        }
    }
    eptr<EContact, GObject> contactptr(contact, "contact");
    const char *rev = (const char *)e_contact_get_const(contact,
                                                        E_CONTACT_REV);
    if (!rev || !rev[0]) {
        throwError(SE_HERE, string("contact entry without REV: ") + luid);
    }
    return rev;
}

#ifdef USE_EDS_CLIENT
class ContactCache : public std::map<std::string, EContactCXX>
{
public:
    /** Asynchronous method call still pending. */
    bool m_running;
    /** The last luid requested in this query. Needed to start with the next contact after it. */
    std::string m_lastLUID;
    /** Result of batch read. Any error here means that the call failed completely. */
    GErrorCXX m_gerror;
    /** A debug logging name for this query. */
    std::string m_name;
};

void EvolutionContactSource::setReadAheadOrder(ReadAheadOrder order,
                                               const ReadAheadItems &luids)
{
    SE_LOG_DEBUG(getDisplayName(), "reading: set order '%s', %ld luids",
                 order == READ_NONE ? "none" :
                 order == READ_ALL_ITEMS ? "all" :
                 order == READ_CHANGED_ITEMS ? "changed" :
                 order == READ_SELECTED_ITEMS ? "selected" :
                 "???",
                 (long)luids.size());
    m_readAheadOrder = order;
    m_nextLUIDs = luids;

    // Be conservative and throw away all cached data. Not doing so
    // can confuse our "cache miss" counting, for example when it uses
    // a cache where some entries have been removed in
    // invalidateCachedContact() and then mistakes the gaps for cache
    // misses.
    //
    // Another reason is that we want to use fairly recent data (in
    // case of concurrent changes in the DB, which currently is not
    // detected by the cache).
    m_contactCache.reset();
    m_contactCacheNext.reset();
}

void EvolutionContactSource::getReadAheadOrder(ReadAheadOrder &order,
                                               ReadAheadItems &luids)
{
    order = m_readAheadOrder;
    luids = m_nextLUIDs;
}

void EvolutionContactSource::checkCacheForError(std::shared_ptr<ContactCache> &cache)
{
    if (cache->m_gerror) {
        GErrorCXX gerror;
        std::swap(gerror, cache->m_gerror);
        cache.reset();
        throwError(SE_HERE, StringPrintf("reading contacts %s", cache->m_name.c_str()), gerror);
    }
}

void EvolutionContactSource::invalidateCachedContact(const std::string &luid)
{
    invalidateCachedContact(m_contactCache, luid);
    invalidateCachedContact(m_contactCacheNext, luid);
}

void EvolutionContactSource::invalidateCachedContact(std::shared_ptr<ContactCache> &cache, const std::string &luid)
{
    if (cache) {
        auto it = cache->find(luid);
        if (it != cache->end()) {
            SE_LOG_DEBUG(getDisplayName(), "reading: remove contact %s from cache because of remove or update", luid.c_str());
            // If we happen to read that contact (unlikely), it'll be
            // considered a cache miss. That's okay. Together with
            // counting cache misses it'll help us avoid using
            // read-ahead when the Synthesis engine is randomly
            // accessing contacts.
            cache->erase(it);
        }
    }
}

bool EvolutionContactSource::getContact(const string &luid, EContact **contact, GErrorCXX &gerror)
{
    SE_LOG_DEBUG(getDisplayName(), "reading: getting contact %s", luid.c_str());

    // Use switch and let compiler tell us when we don't cover a case.
    ReadAheadOrder order
#ifndef __clang_analyzer__
        // scan-build would complain: value stored to 'order' during
        // its initialization is never read.  But we need to keep it
        // otherwise, to avoid: 'order' may be used uninitialized in
        // this function [-Werror=maybe-uninitialized]
        = m_readAheadOrder
#endif
        ;
    switch (m_accessMode) {
    case SYNCHRONOUS:
        order = READ_NONE;
        break;
    case BATCHED:
    case DEFAULT:
        order = m_readAheadOrder;
        break;
    };

    m_contactReads++;
    if (order == READ_NONE) {
        m_contactsFromDB++;
        m_contactQueries++;
        return e_book_client_get_contact_sync(m_addressbook,
                                              luid.c_str(),
                                              contact,
                                              nullptr,
                                              gerror);
    } else {
        return getContactFromCache(luid, contact, gerror);
    }
}

bool EvolutionContactSource::getContactFromCache(const string &luid, EContact **contact, GErrorCXX &gerror)
{
    *contact = nullptr;

    // Use ContactCache.
    if (m_contactCache) {
        SE_LOG_DEBUG(getDisplayName(), "reading: active cache %s", m_contactCache->m_name.c_str());
        // Ran into a problem?
        checkCacheForError(m_contactCache);

        // Does the cache cover our item?
        auto it = m_contactCache->find(luid);
        if (it == m_contactCache->end()) {
            if (m_contactCacheNext) {
                SE_LOG_DEBUG(getDisplayName(), "reading: not in cache, try cache %s",
                             m_contactCacheNext->m_name.c_str());
                // Throw away old cache, try with next one. This is not
                // a cache miss (yet).
                m_contactCache = m_contactCacheNext;
                m_contactCacheNext.reset();
                return getContactFromCache(luid, contact, gerror);
            } else {
                SE_LOG_DEBUG(getDisplayName(), "reading: not in cache, nothing pending -> start reading");
                // Throw away cache, start new read below.
                m_contactCache.reset();
            }
        } else {
            SE_LOG_DEBUG(getDisplayName(), "reading: in %s cache", m_contactCache->m_running ? "running" : "loaded");
            if (m_contactCache->m_running) {
                m_cacheStalls++;
                GRunWhile([this] () { return this->m_contactCache->m_running; });
            }
            // Problem?
            checkCacheForError(m_contactCache);

            SE_LOG_DEBUG(getDisplayName(), "reading: in cache, %s", it->second ? "available" : "not found");
            if (it->second) {
                // Got it.
                *contact = it->second.ref();
            } else {
                // Delay throwing error. We need to go through the read-ahead code below.
                gerror.take(g_error_new(E_BOOK_CLIENT_ERROR, E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
                                        "uid %s not found in batch read", luid.c_str()));
            }
        }
    }

    // No current cache? In that case we must read and block.
    if (!m_contactCache) {
        m_contactCache = startReading(luid, START);
        // Call code above recursively, which will block.
        return getContactFromCache(luid, contact, gerror);
    }

    // Can we read ahead?
    if (!m_contactCacheNext && !m_contactCache->m_running) {
        m_contactCacheNext = startReading(m_contactCache->m_lastLUID, CONTINUE);
    }

    // Everything is okay when we get here. Either we have the contact or
    // it wasn't in the database.
    SE_LOG_DEBUG(getDisplayName(), "reading: read %s: %s", luid.c_str(), gerror ? gerror->message : "<<okay>>");
    logCacheStats(Logger::DEBUG);
    return !gerror;
}

static int MaxBatchSize()
{
    int maxBatchSize = atoi(getEnv("SYNCEVOLUTION_EDS_BATCH_SIZE", "50"));
    if (maxBatchSize < 1) {
        maxBatchSize = 1;
    }
    return maxBatchSize;
}

std::shared_ptr<ContactCache> EvolutionContactSource::startReading(const std::string &luid, ReadingMode mode)
{
    SE_LOG_DEBUG(getDisplayName(), "reading: %s contact %s",
                 mode == START ? "must read" :
                 mode == CONTINUE ? "continue after" :
                 "???",
                 luid.c_str());

    static int maxBatchSize = MaxBatchSize();
    std::vector<EBookQueryCXX> uidQueries;
    uidQueries.resize(maxBatchSize);
    std::vector<const std::string *> uids;
    uids.resize(maxBatchSize);
    int size = 0;
    bool found = false;

    switch (m_readAheadOrder) {
    case READ_ALL_ITEMS:
    case READ_CHANGED_ITEMS: {
        const Items_t &items = getAllItems();
        const Items_t &newItems = getNewItems();
        const Items_t &updatedItems = getUpdatedItems();
        auto it = items.find(luid);

        // Always read the requested item, even if not found in item list?
        if (mode == START) {
            uids[0] = &luid;
            size++;
        }
        // luid is dealt with, either way.
        if (it != items.end()) {
            // Check that it is a valid candidate for caching, else
            // we have a cache miss prediction.
            if (m_readAheadOrder == READ_ALL_ITEMS ||
                newItems.find(luid) != newItems.end() ||
                updatedItems.find(luid) != updatedItems.end()) {
                found = true;
            }
            ++it;
        }
        while (size < maxBatchSize &&
               it != items.end()) {
            const std::string &luid = *it;
            if (m_readAheadOrder == READ_ALL_ITEMS ||
                newItems.find(luid) != newItems.end() ||
                updatedItems.find(luid) != updatedItems.end()) {
                uids[size] = &luid;
                ++size;
            }
            ++it;
        }
        break;
    }
    case READ_SELECTED_ITEMS: {
        auto it = boost::find(std::make_pair(m_nextLUIDs.begin(), m_nextLUIDs.end()), luid);
        // Always read the requested item, even if not found in item list?
        if (mode == START) {
            uids[0] = &luid;
            size++;
        }
        // luid is dealt with, either way.
        if (it != m_nextLUIDs.end()) {
            found = true;
            ++it;
        }
        while (size < maxBatchSize &&
               it != m_nextLUIDs.end()) {
            uids[size] = &*it;
            ++size;
            ++it;
        }
        break;
    }
    case READ_NONE:
        // May be reached when read-ahead was turned off while
        // preparing for it.
        if (mode == START) {
            uids[0] = &luid;
            size++;
        }
        break;
    }

    if (m_readAheadOrder != READ_NONE &&
        mode == START &&
        !found) {
        // The requested contact was not on our list. Consider this
        // a cache miss (or rather, cache prediction failure) and turn
        // of the read-ahead.
        m_cacheMisses++;
        SE_LOG_DEBUG(getDisplayName(), "reading: disable read-ahead due to cache miss");
        m_readAheadOrder = READ_NONE;
    }

    std::shared_ptr<ContactCache> cache;
    if (size) {
        // Prepare parameter for EDS C call. Ownership of query instances is in uidQueries array.
        auto queries = std::make_unique<EBookQuery *[]>(size);
        for (int i = 0; i < size; i++) {
            // This shouldn't compile because we don't specify how ownership is handled.
            // The reset() method always bumps the ref count, which is not what we want here!
            // uidQueries[i].reset(e_book_query_field_test(E_CONTACT_UID, E_BOOK_QUERY_IS, it->c_str()));
            //
            // Take over ownership.
            uidQueries[i] = EBookQueryCXX::steal(e_book_query_field_test(E_CONTACT_UID, E_BOOK_QUERY_IS, uids[i]->c_str()));
            queries[i] = uidQueries[i].get();
        }
        EBookQueryCXX query(e_book_query_or(size, queries.get(), false), TRANSFER_REF);
        PlainGStr sexp(e_book_query_to_string(query.get()));

        cache.reset(new ContactCache);
        cache->m_running = true;
        cache->m_name = StringPrintf("%s-%s (%d)", uids[0]->c_str(), uids[size - 1]->c_str(), size);
        cache->m_lastLUID = *uids[size - 1];
        for (const std::string *uid: boost::make_iterator_range(uids.begin(), uids.begin() + size)) {
            (*cache)[*uid] = EContactCXX();
        }
        m_contactsFromDB += size;
        m_contactQueries++;
        auto process = [this, cachePtr=std::weak_ptr<ContactCache>(cache)] (gboolean success, GSList *contactsPtr, const GError *gerror) noexcept {
            try {
                using ContactListCXX = GListCXX< EContact, GSList, GObjectDestructor<EContact> >;
                ContactListCXX contacts(contactsPtr); // transfers ownership
                std::shared_ptr<ContactCache> cache = cachePtr.lock();
                if (!cache) {
                    SE_LOG_DEBUG(getDisplayName(), "reading: contact read finished, results no longer needed: %s", gerror ? gerror->message : "<<successful>>");
                    return;
                }

                SE_LOG_DEBUG(getDisplayName(), "reading: contact read %s finished: %s",
                             cache->m_name.c_str(),
                             gerror ? gerror->message : "<<successful>>");
                if (success) {
                    for (EContact *contact: contacts) {
                        const char *uid = (const char *)e_contact_get_const(contact, E_CONTACT_UID);
                        SE_LOG_DEBUG(getDisplayName(), "reading: contact read %s got %s", cache->m_name.c_str(), uid);
                        (*cache)[uid] = EContactCXX(contact, ADD_REF);
                    }
                } else {
                    cache->m_gerror = gerror;
                }
                cache->m_running = false;
            } catch (...) {
                Exception::handle(HANDLE_EXCEPTION_FATAL);
            }
        };
        SYNCEVO_GLIB_CALL_ASYNC(e_book_client_get_contacts, process,
                                m_addressbook, sexp, nullptr);
        SE_LOG_DEBUG(getDisplayName(), "reading: started contact read %s", cache->m_name.c_str());
    }
    return cache;
}

void EvolutionContactSource::logCacheStats(Logger::Level level)
{
    SE_LOG(getDisplayName(), level,
           "requested %d, retrieved %d from DB in %d queries, misses %d/%d (%d%%), stalls %d",
           m_contactReads,
           m_contactsFromDB,
           m_contactQueries,
           m_cacheMisses, m_contactReads, m_contactReads ? m_cacheMisses * 100 / m_contactReads : 0,
           m_cacheStalls);
}

#endif

void EvolutionContactSource::readItem(const string &luid, std::string &item, bool raw)
{
    EContact *contact;
    GErrorCXX gerror;
    if (
#ifdef USE_EDS_CLIENT
        !getContact(luid, &contact, gerror)
#else
        !e_book_get_contact(m_addressbook,
                            luid.c_str(),
                            &contact,
                            gerror)
#endif
        ) {
        if (IsContactNotFound(gerror)) {
            throwError(SE_HERE, STATUS_NOT_FOUND, string("reading contact: ") + luid);
        } else {
            throwError(SE_HERE, string("reading contact ") + luid,
                       gerror);
        }
    }

    eptr<EContact, GObject> contactptr(contact, "contact");

    // Inline PHOTO data if exporting, leave VALUE=uri references unchanged
    // when processing inside engine (will be inlined by engine as needed).
    // The function for doing the inlining was added in EDS 3.4.
    // In compatibility mode, we must check the function pointer for non-nullptr.
    // In direct call mode, the existence check is done by configure.
    if (raw) {
#if defined(HAVE_E_CONTACT_INLINE_LOCAL_PHOTOS)
        if (!e_contact_inline_local_photos(contactptr, gerror)) {
            throwError(SE_HERE, string("inlining PHOTO file data in ") + luid, gerror);
        }
#endif
    }

    eptr<char> vcardstr(e_vcard_to_string(&contactptr->parent,
                                          EVC_FORMAT_VCARD_30));
    if (!vcardstr) {
        throwError(SE_HERE, string("failure extracting contact from Evolution " ) + luid);
    }

    item = vcardstr.get();
}

#ifdef USE_EDS_CLIENT
TrackingSyncSource::InsertItemResult EvolutionContactSource::checkBatchedInsert(const std::shared_ptr<Pending> &pending)
{
    SE_LOG_DEBUG(pending->m_name, "checking operation: %s", pending->m_status == MODIFYING ? "waiting" : "inserted");
    if (pending->m_status == MODIFYING) {
        return TrackingSyncSource::InsertItemResult([this, pending] () { return checkBatchedInsert(pending); });
    }
    if (pending->m_gerror) {
        pending->m_gerror.throwError(SE_HERE, pending->m_name);
    }
    string newrev = getRevision(pending->m_uid);
    return TrackingSyncSource::InsertItemResult(pending->m_uid, newrev, ITEM_OKAY);
}

void EvolutionContactSource::flushItemChanges()
{
    if (!m_batchedAdd.empty()) {
        SE_LOG_DEBUG(getDisplayName(), "batch add of %d contacts starting", (int)m_batchedAdd.size());
        m_numRunningOperations++;
        GListCXX<EContact, GSList> contacts;
        // Iterate backwards, push to front (cheaper for single-linked list) -> same order in the end.
        for (const auto &pending: reverse(m_batchedAdd)) {
            contacts.push_front(pending->m_contact.get());
        }
        // Transfer content without copying and then copy only the shared pointer.
        auto batched = std::make_shared<PendingContainer_t>();
        std::swap(*batched, m_batchedAdd);
        auto process = [this, batched] (gboolean success, GSList *uids, const GError *gerror) noexcept {
            try {
                // The destructor ensures that the pending operations complete
                // before destructing the instance, so our "this" pointer is
                // always valid here.
                SE_LOG_DEBUG(getDisplayName(), "batch add of %d contacts completed", (int)batched->size());
                m_numRunningOperations--;
                auto it = (*batched).begin();
                GSList *uid = uids;
                while (it != (*batched).end() && uid) {
                    SE_LOG_DEBUG((*it)->m_name, "completed: %s",
                                 success ? "<<successfully>>" :
                                 gerror ? gerror->message :
                                 "<<unknown failure>>");
                    if (success) {
                        (*it)->m_uid = static_cast<gchar *>(uid->data);
                        // Get revision when engine checks the item.
                        (*it)->m_status = REVISION;
                    } else {
                        (*it)->m_status = DONE;
                        (*it)->m_gerror = gerror;
                    }
                    ++it;
                    uid = uid->next;
                }

                while (it != (*batched).end()) {
                    // Should never happen.
                    SE_LOG_DEBUG((*it)->m_name, "completed: missing uid?!");
                    (*it)->m_status = DONE;
                    ++it;
                }

                g_slist_free_full(uids, g_free);
            } catch (...) {
                Exception::handle(HANDLE_EXCEPTION_FATAL);
            }
        };
        SYNCEVO_GLIB_CALL_ASYNC(e_book_client_add_contacts, process,
                                m_addressbook, contacts,
#ifdef HAVE_E_BOOK_OPERATION_FLAGS
                                E_BOOK_OPERATION_FLAG_NONE,
#endif
                                nullptr);
    }
    if (!m_batchedUpdate.empty()) {
        SE_LOG_DEBUG(getDisplayName(), "batch update of %d contacts starting", (int)m_batchedUpdate.size());
        m_numRunningOperations++;
        GListCXX<EContact, GSList> contacts;
        for (const auto &pending: reverse(m_batchedUpdate)) {
            contacts.push_front(pending->m_contact.get());
        }
        auto batched = std::make_shared<PendingContainer_t>();
        std::swap(*batched, m_batchedUpdate);
        auto process = [this, batched] (gboolean success, const GError *gerror) noexcept {
            try {
                SE_LOG_DEBUG(getDisplayName(), "batch update of %d contacts completed", (int)batched->size());
                m_numRunningOperations--;
                auto it = (*batched).begin();
                while (it != (*batched).end()) {
                    SE_LOG_DEBUG((*it)->m_name, "completed: %s",
                                 success ? "<<successfully>>" :
                                 gerror ? gerror->message :
                                 "<<unknown failure>>");
                    if (success) {
                        (*it)->m_status = REVISION;
                    } else {
                        (*it)->m_status = DONE;
                        (*it)->m_gerror = gerror;
                    }
                    ++it;
                }
            } catch (...) {
                Exception::handle(HANDLE_EXCEPTION_FATAL);
            }
        };
        SYNCEVO_GLIB_CALL_ASYNC(e_book_client_modify_contacts, process,
                                m_addressbook, contacts,
#ifdef HAVE_E_BOOK_OPERATION_FLAGS
                                E_BOOK_OPERATION_FLAG_NONE,
#endif
                                nullptr);
    }
}

void EvolutionContactSource::finishItemChanges()
{
    if (m_numRunningOperations) {
        SE_LOG_DEBUG(getDisplayName(), "waiting for %d pending operations to complete", m_numRunningOperations.get());
        while (m_numRunningOperations) {
            g_main_context_iteration(nullptr, true);
        }
        SE_LOG_DEBUG(getDisplayName(), "pending operations completed");
    }
}

#endif

TrackingSyncSource::InsertItemResult
EvolutionContactSource::insertItem(const string &uid, const std::string &item, bool raw)
{
    EContactCXX contact(e_contact_new_from_vcard(item.c_str()), TRANSFER_REF);
    if (contact) {
        e_contact_set(contact, E_CONTACT_UID,
                      uid.empty() ?
                      nullptr :
                      const_cast<char *>(uid.c_str()));
        GErrorCXX gerror;
#ifdef USE_EDS_CLIENT
        invalidateCachedContact(uid);
        switch (m_accessMode) {
        case SYNCHRONOUS:
            if (uid.empty()) {
                gchar* newuid;
                if (!e_book_client_add_contact_sync(m_addressbook, contact,
#ifdef HAVE_E_BOOK_OPERATION_FLAGS
                                                    E_BOOK_OPERATION_FLAG_NONE,
#endif
                                                    &newuid, nullptr, gerror)) {
                    throwError(SE_HERE, "add new contact", gerror);
                }
                PlainGStr newuidPtr(newuid);
                string newrev = getRevision(newuid);
                return InsertItemResult(newuid, newrev, ITEM_OKAY);
            } else {
                if (!e_book_client_modify_contact_sync(m_addressbook, contact,
#ifdef HAVE_E_BOOK_OPERATION_FLAGS
                                                       E_BOOK_OPERATION_FLAG_NONE,
#endif
                                                       nullptr, gerror)) {
                    throwError(SE_HERE, "updating contact "+ uid, gerror);
                }
                string newrev = getRevision(uid);
                return InsertItemResult(uid, newrev, ITEM_OKAY);
            }
            break;
        case BATCHED:
        case DEFAULT:
            std::string name = StringPrintf("%s: %s operation #%d",
                                            getDisplayName().c_str(),
                                            uid.empty() ? "add" : ("insert " + uid).c_str(),
                                            m_asyncOpCounter++);
            SE_LOG_DEBUG(name, "queueing for batched %s", uid.empty() ? "add" : "update");
            auto pending = std::make_shared<Pending>();
            pending->m_name = name;
            pending->m_contact = contact;
            pending->m_uid = uid;
            if (uid.empty()) {
                m_batchedAdd.push_back(pending);
            } else {
                m_batchedUpdate.push_back(pending);
            }
            // SyncSource is going to live longer than Synthesis
            // engine, so using "this" is safe here.
            return InsertItemResult([this, pending] () { return checkBatchedInsert(pending); });
            break;
        }
#else
        if (uid.empty() ?
            e_book_add_contact(m_addressbook, contact, gerror) :
            e_book_commit_contact(m_addressbook, contact, gerror)) {
            const char *newuid = (const char *)e_contact_get_const(contact, E_CONTACT_UID);
            if (!newuid) {
                throwError(SE_HERE, "no UID for contact");
            }
            string newrev = getRevision(newuid);
            return InsertItemResult(newuid, newrev, ITEM_OKAY);
        } else {
            throwError(SE_HERE, uid.empty() ?
                       "storing new contact" :
                       string("updating contact ") + uid,
                       gerror);
        }
#endif
    } else {
        throwError(SE_HERE, string("failure parsing vcard " ) + item);
    }
    // not reached!
    return InsertItemResult("", "", ITEM_OKAY);
}

void EvolutionContactSource::removeItem(const string &uid)
{
    GErrorCXX gerror;
    if (
#ifdef USE_EDS_CLIENT
        (invalidateCachedContact(uid),
         !e_book_client_remove_contact_by_uid_sync(m_addressbook, uid.c_str(),
#ifdef HAVE_E_BOOK_OPERATION_FLAGS
                                                   E_BOOK_OPERATION_FLAG_NONE,
#endif
                                                   nullptr, gerror))
#else
        !e_book_remove_contact(m_addressbook, uid.c_str(), gerror)
#endif
        ) {
        if (IsContactNotFound(gerror)) {
            throwError(SE_HERE, STATUS_NOT_FOUND, string("deleting contact: ") + uid);
        } else {
            throwError(SE_HERE, string( "deleting contact " ) + uid,
                        gerror);
        }
    }
}

std::string EvolutionContactSource::getDescription(const string &luid)
{
    try {
        EContact *contact;
        GErrorCXX gerror;
        if (
#ifdef USE_EDS_CLIENT
            !getContact(luid,
                        &contact,
                        gerror)
#else
            !e_book_get_contact(m_addressbook,
                                luid.c_str(),
                                &contact,
                                gerror)
#endif
            ) {
            throwError(SE_HERE, string("reading contact ") + luid,
                       gerror);
        }
        eptr<EContact, GObject> contactptr(contact, "contact");
        const char *name = (const char *)e_contact_get_const(contact, E_CONTACT_FULL_NAME);
        if (name) {
            return name;
        }
        const char *fileas = (const char *)e_contact_get_const(contact, E_CONTACT_FILE_AS);
        if (fileas) {
            return fileas;
        }
        EContactName *names =
            (EContactName *)e_contact_get(contact, E_CONTACT_NAME);
        std::list<std::string> buffer;
        if (names) {
            try {
                if (names->given && names->given[0]) {
                    buffer.push_back(names->given);
                }
                if (names->additional && names->additional[0]) {
                    buffer.push_back(names->additional);
                }
                if (names->family && names->family[0]) {
                    buffer.push_back(names->family);
                }
            } catch (...) {
            }
            e_contact_name_free(names);
        }
        return boost::join(buffer, " ");
    } catch (...) {
        // Instead of failing we log the error and ask
        // the caller to log the UID. That way transient
        // errors or errors in the logging code don't
        // prevent syncs.
        handleException();
        return "";
    }
}

std::string EvolutionContactSource::getMimeType() const
{
    switch( m_vcardFormat ) {
     case EVC_FORMAT_VCARD_21:
        return "text/x-vcard";
        break;
     case EVC_FORMAT_VCARD_30:
     default:
        return "text/vcard";
        break;
    }
}

std::string EvolutionContactSource::getMimeVersion() const
{
    switch( m_vcardFormat ) {
     case EVC_FORMAT_VCARD_21:
        return "2.1";
        break;
     case EVC_FORMAT_VCARD_30:
     default:
        return "3.0";
        break;
    }
}

SE_END_CXX

#endif /* ENABLE_EBOOK */

#ifdef ENABLE_MODULES
# include "EvolutionContactSourceRegister.cpp"
#endif
