/*
 * Copyright (C) 2007-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>
using namespace std;

#include "config.h"

#ifdef ENABLE_ADDRESSBOOK

#ifdef IPHONE
# define ABAddRecord ABCAddRecord
# define ABCopyArrayOfAllPeople ABCCopyArrayOfAllPeople
# define ABGetSharedAddressBook ABCGetSharedAddressBook
# define ABMultiValueAdd ABCMultiValueAdd
# define ABMultiValueCopyLabelAtIndex ABCMultiValueCopyLabelAtIndex
# define ABMultiValueCopyValueAtIndex ABCMultiValueCopyValueAtIndex
# define ABMultiValueCount ABCMultiValueGetCount
# define ABMultiValueCreateMutable ABCMultiValueCreateMutable
// # define ABPersonCopyImageData ABCPersonCopyImageData
# define PersonCreateWrapper(_addressbook) ABCPersonCreateNewPerson(_addressbook)
/**
 * The iPhone stores photos in three (?) different sizes.
 * Storing just one copy is okay, albeit a bit inefficient:
 * it needs to be scaled down each time it is accessed.
 *
 * @todo When importing photos into the address book, create
 * all three different sizes.
 */
enum {
    IPHONE_PHOTO_SIZE_THUMBNAIL,
    IPHONE_PHOTO_SIZE_MEDIUM,
    IPHONE_PHOTO_SIZE_ORIGINAL
};
# define PersonSetImageDataWrapper(_person, _dataref) ABCPersonSetImageDataAndCropRect(_person, IPHONE_PHOTO_SIZE_THUMBNAIL, _dataref, 0,0,0,0)
# define ABRecordCopyValue ABCRecordCopyValue
# define ABRecordRemoveValue ABCRecordRemoveValue
# define ABRecordSetValue ABCRecordSetValue
# define ABRemoveRecord ABCRemoveRecord
# define ABSave ABCSave
# define kABAIMInstantProperty kABCAIMInstantProperty
# define kABAddressCityKey kABCAddressCityKey
# define kABAddressCountryKey kABCAddressCountryKey
# define kABAddressHomeLabel kABCAddressHomeLabel
# define kABAddressProperty kABCAddressProperty
# define kABAddressStateKey kABCAddressStateKey
# define kABAddressStreetKey kABCAddressStreetKey
# define kABAddressWorkLabel kABCAddressWorkLabel
# define kABAddressZIPKey kABCAddressZIPKey
# define kABAssistantLabel kABCAssistantLabel
# define kABBirthdayProperty kABCBirthdayProperty
# define kABCreationDateProperty kABCCreationDateProperty
# define kABDepartmentProperty kABCDepartmentProperty
# define kABEmailHomeLabel kABCEmailHomeLabel
# define kABEmailProperty kABCEmailProperty
# define kABEmailWorkLabel kABCEmailWorkLabel
# define kABFirstNameProperty kABCFirstNameProperty
# define kABHomePageLabel kABCHomePageLabel
/* # define kABHomePageProperty kABCHomePageProperty */
# define kABICQInstantProperty kABCICQInstantProperty
# define kABJabberHomeLabel kABCJabberHomeLabel
# define kABJabberInstantProperty kABCJabberInstantProperty
# define kABJabberWorkLabel kABCJabberWorkLabel
# define kABJobTitleProperty kABCJobTitleProperty
# define kABLastNameProperty kABCLastNameProperty
# define kABMSNInstantProperty kABCMSNInstantProperty
# define kABManagerLabel kABCManagerLabel
# define kABMiddleNameProperty kABCMiddleNameProperty
# define kABModificationDateProperty kABCModificationDateProperty
# define kABNicknameProperty kABCNicknameProperty
# define kABNoteProperty kABCNoteProperty
# define kABOrganizationProperty kABCOrganizationProperty
# define kABOtherDatesProperty kABCOtherDatesProperty
# define kABPhoneHomeFAXLabel kABCPhoneHomeFAXLabel
# define kABPhoneHomeLabel kABCPhoneHomeLabel
# define kABPhoneMainLabel kABCPhoneMainLabel
# define kABPhoneMobileLabel kABCPhoneMobileLabel
# define kABPhonePagerLabel kABCPhonePagerLabel
# define kABPhoneProperty kABCPhoneProperty
# define kABPhoneWorkFAXLabel kABCPhoneWorkFAXLabel
# define kABPhoneWorkLabel kABCPhoneWorkLabel
# define kABRelatedNamesProperty kABCRelatedNamesProperty
# define kABSpouseLabel kABCSpouseLabel
# define kABSuffixProperty kABCSuffixProperty
// # define kABTitleProperty kABCTitleProperty
// # define kABURLsProperty kABCURLsProperty
# define kABYahooInstantProperty kABCYahooInstantProperty
#else
# define PersonCreateWrapper(_addressbook) ABPersonCreate()
# define PersonSetImageDataWrapper(_person, _dataref) ABPersonSetImageData(_person, _dataref)
#endif
#include <syncevo/SyncContext.h>
#include "AddressBookSource.h"

#include <syncevo/Logging.h>
#include <common/base/util/StringBuffer.h>
#include "vocl/VConverter.h"

#include <CoreFoundation/CoreFoundation.h>

#include <syncevo/declarations.h>
SE_BEGIN_CXX

using namespace vocl;

/** converts a CFString to std::string in UTF-8 - does not free input, throws exception if conversion impossible */
static string CFString2Std(CFStringRef cfstring)
{
    const char *str = CFStringGetCStringPtr(cfstring, kCFStringEncodingUTF8);
    if (str) {
        return string(str);
    }

    CFIndex len = CFStringGetLength(cfstring) * 2 + 1;
    for (int tries = 0; tries < 3; tries++) {
        arrayptr<char> buf(new char[len], "buffer");
        if (CFStringGetCString(cfstring, buf, len, kCFStringEncodingUTF8)) {
            return string((char *)buf);
        }
        len *= 2;
    }
    SyncContext::throwError("converting CF string failed");
    return "";
}

/** converts a string in UTF-8 into a CFString - throws an exception if no valid reference can be generated */
static CFStringRef Std2CFString(const string &str)
{
    ref<CFStringRef> cfstring(CFStringCreateWithCString(NULL, str.c_str(), kCFStringEncodingUTF8), "conversion from CFString");
    return cfstring.release();
}

/** generic label for 'other' items in a multi-value list */
static const CFStringRef otherLabel(CFSTR("_$!<Other>!$_"));
/** generic label for 'work' items in a multi-value list */
static const CFStringRef workLabel(CFSTR("_$!<Work>!$_"));
/** custom label used for "TEL;PREF;WORK" */
static const CFStringRef mainWorkLabel(CFSTR("main work"));

#ifdef IPHONE

/** declarations and functions which are missing in iPhone framework */
extern "C" {
    extern const CFStringRef kABCHomePageProperty;
    extern const CFStringRef kABCURLProperty;
    
    ABPersonRef ABCPersonCreateNewPerson(ABAddressBookRef addressbook);

    ABRecordRef ABCPersonGetRecordForUniqueID(ABAddressBookRef addressBook, SInt32 uid);
    ABRecordRef ABCopyRecordForUniqueId(ABAddressBookRef addressBook, CFStringRef uniqueId) {
        SInt32 uid = CFStringGetIntValue(uniqueId);
        return ABCPersonGetRecordForUniqueID(addressBook, uid);
    }

    SInt32 ABCRecordGetUniqueId(ABRecordRef record);
    CFStringRef ABRecordCopyUniqueId(ABRecordRef record) {
        SInt32 uid = ABCRecordGetUniqueId(record);
        return CFStringCreateWithFormat(NULL, NULL, CFSTR("%d"), uid);
    }

    CFDataRef ABCPersonCopyImageData(ABPersonRef person, int format);
    bool ABCPersonSetImageData(ABPersonRef person, int format, CFDataRef data);
    bool ABCPersonSetImageDataAndCropRect(ABPersonRef person, int format, CFDataRef data, int crop_x, int crop_y, int crop_width, int crop_height);
}

#endif


/**
 * a strtok_r() which does no skip delimiters at the start and end and does 
 * not merge consecutive delimiters, i.e. returned string may be empty
 *
 * @return NULL if no further tokens
 */
static char *my_strtok_r(char *buffer, char delim, char **ptr, char **endptr)
{
    char *res;

    if (buffer) {
        *ptr = buffer;
        *endptr = buffer + strlen(buffer);
    }
    res = *ptr;
    if (res == *endptr) {
        return NULL;
    }

    while (**ptr) {
        if (**ptr == delim) {
            **ptr = 0;
            (*ptr)++;
            break;
        }
        (*ptr)++;
    }

    return res;
}

/** converts between vCard and ABPerson and back */
class vCard2ABPerson {
public:
    vCard2ABPerson(string &vcard, ABPersonRef person) :
        m_vcard(vcard),
        m_person(person) {
    }

    /** parses vcard and stores result in person */
    void toPerson() {
        std::auto_ptr<VObject> vobj(VConverter::parse((char *)m_vcard.c_str()));
        if (vobj.get() == 0) {
            throwError("parsing contact");
        }
        vobj->toNativeEncoding();

        // Remove all properties from person that we might set:
        // those still found in the vCard will be recreated.
        // Properties that we do not support are left untouched.
        for (int mapindex = 0;
             m_mapping[mapindex].m_vCardProp;
             mapindex++) {
            const mapping &map = m_mapping[mapindex];
            if (map.m_abPersonProp) {
                if (!ABRecordRemoveValue(m_person, *map.m_abPersonProp)) {
                    throwError("removing old value "
#ifndef IPHONE
                               + CFString2Std(*map.m_abPersonProp) + " " +
#endif
                               "failed");
                }
            }
        }
        for (int multi = 0; multi < MAX_MULTIVALUE; multi++) {
            if (!ABRecordRemoveValue(m_person, *m_multiProp[multi])) {
                throwError(string("removing old value ")
#ifndef IPHONE
                           + CFString2Std(*m_multiProp[multi]) + " "
#endif
                           + "failed");
            }
        }

        // walk through all properties and handle them
        int propindex = 0;
        VProperty *vprop;
        while ((vprop = vobj->getProperty(propindex)) != NULL) {
            for (int mapindex = 0;
                 m_mapping[mapindex].m_vCardProp;
                 mapindex++) {
                const mapping &map = m_mapping[mapindex];
                if (!strcmp(map.m_vCardProp, vprop->getName())) {
                    toPerson_t handler = map.m_toPerson;
                    if (!handler) {
                        handler = &vCard2ABPerson::toPersonString;
                    }
                    (this->*handler)(map, *vprop);
                    break;
                }
            }
            propindex++;
        }

        // now copy all those values to the person which did not map directly
        for (int multi = 0; multi < MAX_MULTIVALUE; multi++) {
            if (m_multi[multi]) {
                setPersonProp(*m_multiProp[multi], m_multi[multi], false);
            }
        }

        VProperty *photo = vobj->getProperty("PHOTO");
        if (photo) {
            int len;
            arrayptr<char> decoded((char *)b64_decode(len, photo->getValue()), "photo");
            ref<CFDataRef> data(CFDataCreate(NULL, (UInt8 *)(char *)decoded, len));
            if (!PersonSetImageDataWrapper(m_person, data)) {
                SyncContext::throwError("cannot set photo data");
            }
        }
    }

    /** convert person into vCard 2.1 or 3.0 and store it in string */
    void fromPerson(bool asVCard30) {
        string tmp;
                
        // VObject is so broken that it neither as a reset nor
        // an assignment operator - no, I didn't write it :-/
        //
        // Reseting m_vobj was supposed to allow repeated calls
        // to fromPerson, but this is not really necessary.
        // m_vobj = VObject();

        m_vobj.addProperty("BEGIN", "VCARD");
        m_vobj.addProperty("VERSION", asVCard30 ? "3.0" : "2.1");
        m_vobj.setVersion(asVCard30 ? "3.0" : "2.1");

        // iterate over all person properties and handle them
        for (int mapindex = 0;
             m_mapping[mapindex].m_vCardProp;
             mapindex++ ) {
            const mapping &map = m_mapping[mapindex];
            if (map.m_abPersonProp) {
#ifdef IPHONE
                // some of the properties returned on the iPhone can neither
                // be printed nor released: trying it leads to crashes, so
                // avoid it
                CFTypeRef value = ABRecordCopyValue(m_person, *map.m_abPersonProp);
#else
                ref<CFTypeRef> value(ABRecordCopyValue(m_person, *map.m_abPersonProp));
#endif
                if (value) {
                    fromPerson_t handler = map.m_fromPerson;
                    if (!handler) {
                        handler = &vCard2ABPerson::fromPersonString;
                    }
                    (this->*handler)(map, value);
                }
            }
        }

        // add properties which did not map directly
        string n;
        n += m_strings[LAST_NAME];
        n += VObject::SEMICOLON_REPLACEMENT;
        n += m_strings[FIRST_NAME];
        n += VObject::SEMICOLON_REPLACEMENT;
        n += m_strings[MIDDLE_NAME];
        n += VObject::SEMICOLON_REPLACEMENT;
        n += m_strings[TITLE];
        n += VObject::SEMICOLON_REPLACEMENT;
        n += m_strings[SUFFIX];
        m_vobj.addProperty("N", n.c_str());

        if (m_strings[ORGANIZATION].size() ||
            m_strings[DEPARTMENT].size() ) {
            string org;
            org += m_strings[ORGANIZATION];
            org += VObject::SEMICOLON_REPLACEMENT;
            org += m_strings[DEPARTMENT];
            m_vobj.addProperty("ORG", org.c_str());
        }

        ref<CFDataRef> photo;
#ifdef IPHONE
        // ask for largets size first
        for(int format = IPHONE_PHOTO_SIZE_ORIGINAL; format >= 0; format--) {
            photo.set(ABCPersonCopyImageData(m_person, format));
            if (photo) {
                break;
            }
        }
#else
        photo.set(ABPersonCopyImageData(m_person));
#endif
        if (photo) {
            StringBuffer encoded;
            b64_encode(encoded, (void *)CFDataGetBytePtr(photo), CFDataGetLength(photo));
            VProperty vprop("PHOTO");
            vprop.addParameter("ENCODING", asVCard30 ? "B" : "BASE64");
            vprop.setValue(encoded.c_str());
            m_vobj.addProperty(&vprop);
        }

        m_vobj.addProperty("END", "VCARD");
        m_vobj.fromNativeEncoding();
        arrayptr<char> finalstr(m_vobj.toString(), "VOCL string");
        m_vcard = (char *)finalstr;
    }

private:
    string &m_vcard;
    ABPersonRef m_person;
    VObject m_vobj;

    void throwError(const string &error) {
        SyncContext::throwError(string("vCard<->Addressbook conversion: ") + error);
    }

    /** intermediate storage for strings gathered from either vcard or person */
    enum {
        FIRST_NAME,
        MIDDLE_NAME,
        LAST_NAME,
        TITLE,
        SUFFIX,
        ORGANIZATION,
        DEPARTMENT,
        MAX_STRINGS
    };
    string m_strings[MAX_STRINGS];

    /** intermediate storage for multi-value data later passed to ABPerson - keep in sync with m_multiProp */
    enum {
        URLS,
        EMAILS,
        PHONES,
#ifndef IPHONE
        DATES,
        AIM,
        JABBER,
        MSN,
        YAHOO,
        ICQ,
#endif
        NAMES,
        ADDRESSES,
        MAX_MULTIVALUE
    };
    ref<ABMutableMultiValueRef, IPHONE_RELEASE> m_multi[MAX_MULTIVALUE];
    /**
     * the ABPerson property which corresponds to the m_multi array:
     * a pointer because the tool chain for the iPhone did not properly
     * handle the constants when referenced in data initialization directly
     */
    static const CFStringRef *m_multiProp[MAX_MULTIVALUE];

    struct mapping;
    /** member function which handles one specific vCard property */
    typedef void (vCard2ABPerson::*toPerson_t)(const mapping &map, VProperty &vprop);
    /** member function which handles one specific ABPerson property */
    typedef void (vCard2ABPerson::*fromPerson_t)(const mapping &map, CFTypeRef cftype);

    /** store a string in the ABPerson */
    void setPersonProp(CFStringRef property, const string &str) {
        ref<CFStringRef> cfstring(Std2CFString(str));
        setPersonProp(property, cfstring);
    }
    /** store a string in the ABPerson */
    void setPersonProp(CFStringRef property, const char *str) {
        ref<CFStringRef> cfstring(Std2CFString(str));
        setPersonProp(property, cfstring);
    }
    /**
     * store a generic property in the ABPerson
     * @param dump     avoid CFCopyDescription() for some properties (iPhone bug)
     */
    void setPersonProp(CFStringRef property, CFTypeRef cftype, bool dump = true) {
        ref<CFStringRef> descr;
        if (dump) {
            descr.set(CFCopyDescription(cftype));
        }
        if (!ABRecordSetValue(m_person, property, cftype)) {
            if (dump) {
                throwError(string("setting ") +
#ifndef IPHONE
                           CFString2Std(property) +
#else
                           "property " +
#endif
                           + " to " + CFString2Std(descr) + "'");
            } else {
                throwError(string("setting ") + 
#ifndef IPHONE
                           CFString2Std(property)
#else
                           "property"
#endif
                           );
            }
        }
    }

    /** add another label/value pair to a multi-value list */
    void toPersonMultiVal(const mapping &map, CFStringRef label, CFTypeRef value) {
        if (!m_multi[map.m_customInt]) {
            m_multi[map.m_customInt].set(ABMultiValueCreateMutable(), "multivalue");
        }
        CFStringRef res;
        if (!ABMultiValueAdd(m_multi[map.m_customInt],
                             value,
                             label,
                             &res)) {
            throwError(string("adding multi value for ") + map.m_vCardProp);
        } else {
#ifndef IPHONE
            CFRelease(res);
#endif
        }
    }

    /**
     * mapping between vCard and ABPerson properties
     */
    static const struct mapping {
        /** the name of the vCard property, e.g. "ADDR", NULL terminates array */
        const char *m_vCardProp;
        /** address of ABPerson property, NULL pointer if none matches directly */
        const CFStringRef *m_abPersonProp;
        /** called when the property is found in the VObject: default is to copy string */
        toPerson_t m_toPerson;
        /** called when the property is found in the ABPerson: default is to copy string */
        fromPerson_t m_fromPerson;
        /** custom value available to callbacks */
        int m_customInt;
        /** custom value available to callbacks */
        CFStringRef m_customString;
    } m_mapping[];

    /** copy normal string directly */
    void fromPersonString(const mapping &map, CFTypeRef cftype) {
        string value(CFString2Std((CFStringRef)cftype));
        m_vobj.addProperty(map.m_vCardProp, value.c_str());
    }

    /** copy normal string directly */
    void toPersonString(const mapping &map, VProperty &vprop) {
        const char *value = vprop.getValue();
        /*
         * Empty strings are not properly ignored by the iPhone GUI,
         * better not add empty string properties. Empty vcard
         * properties as an indication that the property is to be
         * cleared are still handled because all known properties
         * were removed in toPerson().
         */
        if (value && *value) {
            setPersonProp(*map.m_abPersonProp, value);
        }
    }

    /** remember string to compose a more complex vCard property later (e.g. "N") */
    void fromPersonStoreString(const mapping &map, CFTypeRef cftype) {
        m_strings[map.m_customInt] = CFString2Std((CFStringRef)cftype);
    }

    /**
     * add a generic string with a predefined label
     * (map.m_customString) or a work/home label to multi-value
     */
    void toPersonStore(const mapping &map, VProperty &vprop) {
        const char *value = vprop.getValue();
        if (!value || !value[0]) {
            return;
        }
        ref<CFStringRef> cfstring(Std2CFString(value));
        CFStringRef label = map.m_customString;
        if (!label) {
            // IM property: label depends on type;
            // same simplification as in fromPersonChat
            if (map.m_customString) {
                label = map.m_customString;
            } else if (vprop.isType("HOME")) {
                label = kABJabberHomeLabel;
            } else if (vprop.isType("WORK")) {
                label = kABJabberHomeLabel;
            } else {
                label = otherLabel;
            }
        }

        toPersonMultiVal(map, label, cfstring);
    }

    /** copy date */
    void fromPersonDate(const mapping &map, CFTypeRef cftype) {
        ref<CFTimeZoneRef> tz(CFTimeZoneCopyDefault());
        CFGregorianDate date = CFAbsoluteTimeGetGregorianDate(CFDateGetAbsoluteTime((CFDateRef)cftype), tz);
        char buffer[40];
        sprintf(buffer, "%04d-%02d-%02d", (int)date.year, date.month, date.day);
        m_vobj.addProperty(map.m_vCardProp, buffer);
    }

    /** copy date */
    void toPersonDate(const mapping &map, VProperty &vprop) {
        int year, month, day;
        const char *value = vprop.getValue();
        if (!value || !value[0]) {
            return;
        }
        // cppcheck-suppress invalidscanf
        if (sscanf(value, "%d-%d-%d", &year, &month, &day) == 3) {
            CFGregorianDate date;
            memset(&date, 0, sizeof(date));
            date.year = year;
            date.month = month;
            date.day = day;

            /*
             * The iPhone stores absolute times for dates, but
             * interprets them according to the current time zone.
             * The effect is that a birthday changes as the system
             * timezone is changed.
             *
             * To mitigate this problem dates are created with
             * an absolute time in the current time zone, just like
             * the iPhone GUI does.
             */
            ref<CFTimeZoneRef> tz(CFTimeZoneCopyDefault());
            ref<CFDateRef> cfdate(CFDateCreate(NULL, CFGregorianDateGetAbsoluteTime(date, tz)));
            if (cfdate) {
                // assert(map.m_abPersonProp);
                setPersonProp(*map.m_abPersonProp, cfdate);
            }
        }
    }

    /** map URL multi-value to vCard URL with different TYPEs */
    void fromPersonURLs(const mapping &map, CFTypeRef cftype) {
        int index = ABMultiValueCount((ABMultiValueRef)cftype) - 1;
        while (index >= 0) {
            ref<CFStringRef> label((CFStringRef)ABMultiValueCopyLabelAtIndex((ABMultiValueRef)cftype, index), "label");
            ref<CFStringRef> value((CFStringRef)ABMultiValueCopyValueAtIndex((ABMultiValueRef)cftype, index), "value");

            VProperty vprop("URL");
            string url = CFString2Std(value);
            vprop.setValue(url.c_str());
            if (CFStringCompare(label, (CFStringRef)kABHomePageLabel, 0) == kCFCompareEqualTo) {
                // leave type blank
            } else if (CFStringCompare(label, (CFStringRef)workLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "WORK");
            } else if (CFStringCompare(label, otherLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "OTHER");
            } else {
                string labelstr = CFString2Std(label);
                vprop.addParameter("TYPE", labelstr.c_str());
            }
            m_vobj.addProperty(&vprop);

            index--;
        }
    }

    /** iPhone: add another URL to multi-value (Mac OS X only has one string property) */
    void toPersonURLs(const mapping &map, VProperty &vprop) {
        const char *value = vprop.getValue();
        if (!value || !value[0]) {
            return;
        }
        arrayptr<char> buffer(wstrdup(value));

        ref<CFStringRef> cfvalue(Std2CFString(value));
        CFStringRef label;
        ref<CFStringRef> custom;
        const char *type = vprop.getParameterValue("TYPE");
        if (vprop.isType("WORK")) {
            label = workLabel;
        } else if(vprop.isType("HOME")) {
            label = (CFStringRef)kABHomePageLabel;
        } else if(vprop.isType("OTHER")) {
            label = otherLabel;
        } else if (type) {
            custom.set(Std2CFString(type));
            label = custom;
        } else {
            label = (CFStringRef)kABHomePageLabel;
        }
        toPersonMultiVal(map, label, cfvalue);
    }

    /** map email multi-value to vCard EMAIL with different TYPEs */
    void fromPersonEMail(const mapping &map, CFTypeRef cftype) {
        int index = ABMultiValueCount((ABMultiValueRef)cftype) - 1;
        while (index >= 0) {
            ref<CFStringRef> label((CFStringRef)ABMultiValueCopyLabelAtIndex((ABMultiValueRef)cftype, index), "label");
            ref<CFStringRef> value((CFStringRef)ABMultiValueCopyValueAtIndex((ABMultiValueRef)cftype, index), "value");
            VProperty vprop("EMAIL");

            if (CFStringCompare(label, kABEmailWorkLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "WORK");
            } else if (CFStringCompare(label, kABEmailHomeLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "HOME");
            } else {
                string labelstr = CFString2Std(label);
                vprop.addParameter("TYPE", labelstr.c_str());
            }

            string email = CFString2Std(value);
            vprop.setValue(email.c_str());
            m_vobj.addProperty(&vprop);

            index--;
        }
    }

    /** add another EMAIL to the email multi-value */
    void toPersonEMail(const mapping &map, VProperty &vprop) {
        const char *value = vprop.getValue();
        if (!value || !value[0]) {
            return;
        }
        arrayptr<char> buffer(wstrdup(value));

        ref<CFStringRef> cfvalue(Std2CFString(value));
        CFStringRef label;
        ref<CFStringRef> custom;
        const char *type = vprop.getParameterValue("TYPE");
        if (vprop.isType("WORK")) {
            label = kABEmailWorkLabel;
        } else if(vprop.isType("HOME")) {
            label = kABEmailHomeLabel;
        } else if (type) {
            custom.set(Std2CFString(type));
            label = custom;
        } else {
            label = otherLabel;
        }

        toPersonMultiVal(map, label, cfvalue);
    }

    /** map address multi-value to vCard ADR with different TYPEs */
    void fromPersonAddr(const mapping &map, CFTypeRef cftype) {
        int index = ABMultiValueCount((ABMultiValueRef)cftype) - 1;
        while (index >= 0) {
            ref<CFStringRef> label((CFStringRef)ABMultiValueCopyLabelAtIndex((ABMultiValueRef)cftype, index), "label");
            ref<CFDictionaryRef> value((CFDictionaryRef)ABMultiValueCopyValueAtIndex((ABMultiValueRef)cftype, index), "value");
            CFStringRef part;
            VProperty vprop((char *)map.m_vCardProp);

            string adr;
            // no PO box
            adr += VObject::SEMICOLON_REPLACEMENT;
            // no extended address
            adr += VObject::SEMICOLON_REPLACEMENT;
            // street
            part = (CFStringRef)CFDictionaryGetValue(value, kABAddressStreetKey);
            if (part) {
                adr += CFString2Std(part);
            }
            adr += VObject::SEMICOLON_REPLACEMENT;
            // city
            part = (CFStringRef)CFDictionaryGetValue(value, kABAddressCityKey);
            if (part) {
                adr += CFString2Std(part);
            }
            adr += VObject::SEMICOLON_REPLACEMENT;
            // region
            part = (CFStringRef)CFDictionaryGetValue(value, kABAddressStateKey);
            if (part) {
                adr += CFString2Std(part);
            }
            adr += VObject::SEMICOLON_REPLACEMENT;
            // ZIP code
            part = (CFStringRef)CFDictionaryGetValue(value, kABAddressZIPKey);
            if (part) {
                adr += CFString2Std(part);
            }
            adr += VObject::SEMICOLON_REPLACEMENT;
            // country
            part = (CFStringRef)CFDictionaryGetValue(value, kABAddressCountryKey);
            if (part) {
                adr += CFString2Std(part);
            }
            adr += VObject::SEMICOLON_REPLACEMENT;

            // not supported: kABAddressCountryCodeKey

            if (CFStringCompare(label, kABAddressWorkLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "WORK");
            } else if (CFStringCompare(label, kABAddressHomeLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "HOME");
            }

            vprop.setValue(adr.c_str());
            m_vobj.addProperty(&vprop);

            index--;
        }
    }

    /** add another ADR to address multi-value */
    void toPersonAddr(const mapping &map, VProperty &vprop) {
        const char *value = vprop.getValue();
        if (!value || !value[0]) {
            return;
        }
        arrayptr<char> buffer(wstrdup(value));
        char *saveptr, *endptr;

        ref<CFMutableDictionaryRef> dict(CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));

        // cannot store PO box and extended address
        /* char *pobox = */ my_strtok_r(buffer, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        /* char *extadr = */ my_strtok_r(NULL, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);

        char *street = my_strtok_r(NULL, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        if (street && *street) {
            ref<CFStringRef> cfstring(Std2CFString(street));
            CFDictionarySetValue(dict, kABAddressStreetKey, cfstring);
        }
        char *city = my_strtok_r(NULL, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        if (city && *city) {
            ref<CFStringRef> cfstring(Std2CFString(city));
            CFDictionarySetValue(dict, kABAddressCityKey, cfstring);
        }
        char *region = my_strtok_r(NULL, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        if (region && *region) {
            ref<CFStringRef> cfstring(Std2CFString(region));
            CFDictionarySetValue(dict, kABAddressStateKey, cfstring);
        }
        char *zip = my_strtok_r(NULL, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        if (zip && *zip) {
            ref<CFStringRef> cfstring(Std2CFString(zip));
            CFDictionarySetValue(dict, kABAddressZIPKey, cfstring);
        }
        char *country = my_strtok_r(NULL, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        if (country && *country) {
            ref<CFStringRef> cfstring(Std2CFString(country));
            CFDictionarySetValue(dict, kABAddressCountryKey, cfstring);
        }

        CFStringRef label;
        if (vprop.isType("WORK")) {
            label = kABAddressWorkLabel;
        } else if(vprop.isType("HOME")) {
            label = kABAddressHomeLabel;
        } else {
            label = otherLabel;
        }

        toPersonMultiVal(map, label, dict);
    }

    /** map phone multi-value to vCard TEL with different TYPEs */
    void fromPersonPhone(const mapping &map, CFTypeRef cftype) {
        int index = ABMultiValueCount((ABMultiValueRef)cftype) - 1;
        while (index >= 0) {
            ref<CFStringRef> label((CFStringRef)ABMultiValueCopyLabelAtIndex((ABMultiValueRef)cftype, index), "label");
            ref<CFStringRef> value((CFStringRef)ABMultiValueCopyValueAtIndex((ABMultiValueRef)cftype, index), "value");
            VProperty vprop("TEL");

            if (CFStringCompare(label, kABPhoneWorkLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "WORK");
                vprop.addParameter("TYPE", "VOICE");
            } else if (CFStringCompare(label, mainWorkLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "WORK");
                vprop.addParameter("TYPE", "PREF");
            } else if (CFStringCompare(label, kABPhoneHomeLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "HOME");
                vprop.addParameter("TYPE", "VOICE");
            } else if (CFStringCompare(label, kABPhoneMobileLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "CELL");
            } else if (CFStringCompare(label, kABPhoneMainLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "PREF");
                vprop.addParameter("TYPE", "VOICE");
            } else if (CFStringCompare(label, kABPhoneHomeFAXLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "HOME");
                vprop.addParameter("TYPE", "FAX");
            } else if (CFStringCompare(label, kABPhoneWorkFAXLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "WORK");
                vprop.addParameter("TYPE", "FAX");
            } else if (CFStringCompare(label,kABPhonePagerLabel , 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "PAGER");
            } else {
                // custom phone types not supported
                vprop.addParameter("TYPE", "VOICE");
            }

            string phone = CFString2Std(value);
            vprop.setValue(phone.c_str());
            m_vobj.addProperty(&vprop);

            index--;
        }
    }

    /** add another phone to the multi-value */
    void toPersonPhone(const mapping &map, VProperty &vprop) {
        const char *value = vprop.getValue();
        if (!value || !value[0]) {
            return;
        }
        arrayptr<char> buffer(wstrdup(value));

        ref<CFStringRef> cfvalue(Std2CFString(value));
        CFStringRef label;
        if (vprop.isType("WORK")) {
            if (vprop.isType("FAX")) {
                label = kABPhoneWorkFAXLabel;
            } else if (vprop.isType("PREF")) {
                label = mainWorkLabel;
            } else {
                label = kABPhoneWorkLabel;
            }
        } else if(vprop.isType("HOME")) {
            if (vprop.isType("FAX")) {
                label = kABPhoneHomeFAXLabel;
            } else {
                label = kABPhoneHomeLabel;
            }
        } else if(vprop.isType("PREF") || vprop.isType("VOICE")) {
            label = kABPhoneMainLabel;
        } else if(vprop.isType("PAGER")) {
            label = kABPhonePagerLabel;
        } else if(vprop.isType("CELL")) {
            label = kABPhoneMobileLabel;
        } else {
            label = otherLabel;
        }

        toPersonMultiVal(map, label, cfvalue);
    }

    /**
     * map chat contact multi-value to respective vCard X- properties
     *
     * complementary operation is toPersonStore()
     */
    void fromPersonChat(const mapping &map, CFTypeRef cftype) {
        int index = ABMultiValueCount((ABMultiValueRef)cftype) - 1;
        while (index >= 0) {
            ref<CFStringRef> label((CFStringRef)ABMultiValueCopyLabelAtIndex((ABMultiValueRef)cftype, index), "label");
            ref<CFStringRef> value((CFStringRef)ABMultiValueCopyValueAtIndex((ABMultiValueRef)cftype, index), "value");
            VProperty vprop((char *)map.m_vCardProp);

            // this is a slight over-simplification:
            // the assumption is that the labels for all IM properties are interchangeable
            // although the header file has different constants for them
            if (CFStringCompare(label, kABJabberWorkLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "WORK");
            } else if (CFStringCompare(label, kABJabberHomeLabel, 0) == kCFCompareEqualTo) {
                vprop.addParameter("TYPE", "HOME");
            } else {
                // custom IM types not supported
            }

            string im = CFString2Std(value);
            vprop.setValue(im.c_str());
            m_vobj.addProperty(&vprop);

            index--;
        }
    }

    /** map related names multi-value to some vCard extension properties */
    void fromPersonNames(const mapping &map, CFTypeRef cftype) {
        int index = ABMultiValueCount((ABMultiValueRef)cftype) - 1;
        while (index >= 0) {
            ref<CFStringRef> label((CFStringRef)ABMultiValueCopyLabelAtIndex((ABMultiValueRef)cftype, index), "label");
            ref<CFStringRef> value((CFStringRef)ABMultiValueCopyValueAtIndex((ABMultiValueRef)cftype, index), "value");
            string name = CFString2Std(value);

            // there are no standard fields for all these related names:
            // use the ones from Evolution because some SyncML servers have
            // been extended to support them
            if (CFStringCompare(label, kABManagerLabel, 0) == kCFCompareEqualTo) {
                m_vobj.addProperty("X-EVOLUTION-MANAGER", name.c_str());
            } else if (CFStringCompare(label, kABAssistantLabel, 0) == kCFCompareEqualTo) {
                m_vobj.addProperty("X-EVOLUTION-ASSISTANT", name.c_str());
            } else if (CFStringCompare(label, kABSpouseLabel, 0) == kCFCompareEqualTo) {
                m_vobj.addProperty("X-EVOLUTION-SPOUSE", name.c_str());
            } else {
                // many related names not supported
            }

            index--;
        }
    }

    /**
     * decode vCard N and store in person properties
     *
     * complementary operation is fromPersonStoreString()
     */
    void toPersonName(const mapping &map, VProperty &vprop) {
        const char *value = vprop.getValue();
        if (!value || !value[0]) {
            return;
        }
        arrayptr<char> buffer(wstrdup(value));
        char *saveptr, *endptr;

        char *last = my_strtok_r(buffer, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        if (last && *last) {
            setPersonProp(kABLastNameProperty, last);
        }

        char *first = my_strtok_r(NULL, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        if (first && *first) {
            setPersonProp(kABFirstNameProperty, first);
        }

        char *middle = my_strtok_r(NULL, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        if (middle && *middle) {
            setPersonProp(kABMiddleNameProperty, middle);
        }

        char *prefix = my_strtok_r(NULL, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
#ifndef IPHONE
        if (prefix && *prefix) {
            setPersonProp(kABTitleProperty, prefix);
        }
#endif

        char *suffix = my_strtok_r(NULL, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        if (suffix && *suffix) {
            setPersonProp(kABSuffixProperty, suffix);
        }
    }

    /**
     * decode ORG and store in person properties
     *
     * complementary operation is fromPersonStoreString()
     */
    void toPersonOrg(const mapping &map, VProperty &vprop) {
        const char *value = vprop.getValue();
        if (!value || !value[0]) {
            return;
        }
        arrayptr<char> buffer(wstrdup(value));
        char *saveptr, *endptr;

        char *company = my_strtok_r(buffer, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        if (company && *company) {
            setPersonProp(kABOrganizationProperty, company);
        }

        char *department = my_strtok_r(NULL, VObject::SEMICOLON_REPLACEMENT, &saveptr, &endptr);
        if (department && *department) {
            setPersonProp(kABDepartmentProperty, department);
        }
    }
};

const CFStringRef *vCard2ABPerson::m_multiProp[MAX_MULTIVALUE] = {
#ifdef IPHONE
    &kABCURLProperty,
#else
    (CFStringRef*)&kABURLsProperty,
#endif
    &kABEmailProperty,
    &kABPhoneProperty,
#ifndef IPHONE
    &kABOtherDatesProperty,
    &kABAIMInstantProperty,
    &kABJabberInstantProperty,
    &kABMSNInstantProperty,
    &kABYahooInstantProperty,
    &kABICQInstantProperty,
#endif
    &kABRelatedNamesProperty,
    &kABAddressProperty
};

const vCard2ABPerson::mapping vCard2ABPerson::m_mapping[] = {
    { "", &kABFirstNameProperty, NULL, &vCard2ABPerson::fromPersonStoreString, FIRST_NAME },
    { "", &kABLastNameProperty, NULL, &vCard2ABPerson::fromPersonStoreString, LAST_NAME },
    { "", &kABMiddleNameProperty, NULL, &vCard2ABPerson::fromPersonStoreString, MIDDLE_NAME },
#ifndef IPHONE
    { "", &kABTitleProperty, NULL, &vCard2ABPerson::fromPersonStoreString, TITLE },
#endif
    { "", &kABSuffixProperty, NULL, &vCard2ABPerson::fromPersonStoreString, SUFFIX },
    { "N", 0, &vCard2ABPerson::toPersonName },
    /* "FN" */
    /* kABFirstNamePhoneticProperty */
    /* kABLastNamePhoneticProperty */
    /* kABMiddleNamePhoneticProperty */
    { "BDAY", &kABBirthdayProperty, &vCard2ABPerson::toPersonDate, &vCard2ABPerson::fromPersonDate },

    { "", &kABOrganizationProperty, NULL, &vCard2ABPerson::fromPersonStoreString, ORGANIZATION },
    { "", &kABDepartmentProperty, NULL, &vCard2ABPerson::fromPersonStoreString, DEPARTMENT },
    { "ORG", 0, &vCard2ABPerson::toPersonOrg },

    { "TITLE", &kABJobTitleProperty },
    /* "ROLE" */

#ifdef IPHONE
    { "URL", &kABCURLProperty, &vCard2ABPerson::toPersonURLs, &vCard2ABPerson::fromPersonURLs, URLS },
#else
    /**
     * bug in the header files for kABHomePageProperty and kABURLsProperty,
     * typecast required
     */
    { "URL", (CFStringRef *)&kABHomePageProperty },
    { "", (CFStringRef *)&kABURLsProperty, NULL, &vCard2ABPerson::fromPersonURLs },
#endif
#if 0
kABHomePageLabel
#endif

    { "EMAIL", &kABEmailProperty, &vCard2ABPerson::toPersonEMail, &vCard2ABPerson::fromPersonEMail, EMAILS },
#if 0
kABEmailWorkLabel
kABEmailHomeLabel
#endif
        
    { "ADR", &kABAddressProperty, &vCard2ABPerson::toPersonAddr, &vCard2ABPerson::fromPersonAddr, ADDRESSES },
#if 0
kABAddressWorkLabel
kABAddressHomeLabel

kABAddressStreetKey
kABAddressCityKey
kABAddressStateKey
kABAddressZIPKey
kABAddressCountryKey
kABAddressCountryCodeKey
#endif
    /* LABEL */

    { "TEL", &kABPhoneProperty, &vCard2ABPerson::toPersonPhone, &vCard2ABPerson::fromPersonPhone, PHONES },

#if 0
kABPhoneWorkLabel
kABPhoneHomeLabel
kABPhoneMobileLabel
kABPhoneMainLabel
kABPhoneHomeFAXLabel
kABPhoneWorkFAXLabel
kABPhonePagerLabel
#endif
#ifndef IPHONE
    { "X-AIM", &kABAIMInstantProperty, &vCard2ABPerson::toPersonStore, &vCard2ABPerson::fromPersonChat, AIM },
    { "X-JABBER", &kABJabberInstantProperty, &vCard2ABPerson::toPersonStore, &vCard2ABPerson::fromPersonChat, JABBER },
    { "X-MSN", &kABMSNInstantProperty, &vCard2ABPerson::toPersonStore, &vCard2ABPerson::fromPersonChat, MSN },
    { "X-YAHOO", &kABYahooInstantProperty, &vCard2ABPerson::toPersonStore, &vCard2ABPerson::fromPersonChat, YAHOO },
    { "X-ICQ", &kABICQInstantProperty, &vCard2ABPerson::toPersonStore, &vCard2ABPerson::fromPersonChat, ICQ },
#endif
    /* "X-GROUPWISE */
    { "NOTE", &kABNoteProperty },
    { "NICKNAME", &kABNicknameProperty },
    
    /* kABMaidenNameProperty */
    /* kABOtherDatesProperty */
#ifndef IPHONE
    { "", &kABRelatedNamesProperty, NULL, &vCard2ABPerson::fromPersonNames },
#endif
#if 0
kABMotherLabel
kABFatherLabel
kABParentLabel
kABSisterLabel
kABBrotherFAXLabel
kABChildLabel
kABFriendLabel
kABSpouseLabel
kABPartnerLabel
kABAssistantLabel
kABManagerLabel
#endif
    { "X-EVOLUTION-MANAGER", 0, &vCard2ABPerson::toPersonStore, NULL, NAMES, kABManagerLabel },
    { "X-EVOLUTION-ASSISTANT", 0, &vCard2ABPerson::toPersonStore, NULL, NAMES, kABAssistantLabel },
    { "X-EVOLUTION-SPOUSE", 0, &vCard2ABPerson::toPersonStore, NULL, NAMES, kABSpouseLabel },

    /* kABPersonFlags */
    /* X-EVOLUTION-FILE-AS */
    /* CATEGORIES */
    /* CALURI */
    /* FBURL */
    /* X-EVOLUTION-VIDEO-URL */
    /* X-MOZILLA-HTML */
    /* X-EVOLUTION-ANNIVERSARY */

    { NULL }
};


string AddressBookSource::getModTime(ABRecordRef record)
{
    double absolute;
#ifdef IPHONE
    absolute = (double)(int)ABRecordCopyValue(record,
                                              kABModificationDateProperty);
#else
    ref<CFDateRef> itemModTime((CFDateRef)ABRecordCopyValue(record,
                                                            kABModificationDateProperty));
    if (!itemModTime) {
        itemModTime.set((CFDateRef)ABRecordCopyValue(record,
                                                     kABCreationDateProperty));
    }
    if (!itemModTime) {
        throwError("extracting time stamp");
    }
    absolute = CFDateGetAbsoluteTime(itemModTime);
#endif

    // round up to next full second:
    // together with a sleep of 1 second in endSyncThrow() this ensures
    // that our time stamps are always >= the stored time stamp even if
    // the time stamp is rounded in the database
    char buffer[128];
    sprintf(buffer, "%.0f", ceil(absolute));
    return buffer;
}


AddressBookSource::AddressBookSource(const EvolutionSyncSourceParams &params, bool asVCard30) :
    TrackingSyncSource(params),
    m_addressbook(0),
    m_asVCard30(asVCard30)
{
}

EvolutionSyncSource::Databases AddressBookSource::getDatabases()
{
    Databases result;

    result.push_back(Database("<<system>>", ""));
    return result;
}

void AddressBookSource::open()
{
    m_addressbook = ABGetSharedAddressBook();
    if (!m_addressbook) {
        throwError("opening address book");
    }
}

void AddressBookSource::listAllItems(RevisionMap_t &revisions)
{
    ref<CFArrayRef> allPersons(ABCopyArrayOfAllPeople(m_addressbook), "list of all people");

    for (CFIndex i = 0; i < CFArrayGetCount(allPersons); i++) {
        ref<CFStringRef> cfuid(ABRecordCopyUniqueId((ABRecordRef)CFArrayGetValueAtIndex(allPersons, i)), "reading UID");
        string uid(CFString2Std(cfuid));

        revisions[uid] = getModTime((ABRecordRef)CFArrayGetValueAtIndex(allPersons, i));
    }
}

void AddressBookSource::close()
{
    if (m_addressbook && !hasFailed()) {
        SE_LOG_DEBUG(getDisplayName(), "flushing address book");
        // store changes persistently
        if (!ABSave(m_addressbook)) {
            throwError("saving address book");
        }

        // time stamps are rounded to next second,
        // so to prevent changes in that range of inaccurracy
        // sleep a bit before returning control
        sleep(2);

        SE_LOG_DEBUG(getDisplayName(), "done with address book");
    }
    
    m_addressbook = NULL;
}

void AddressBookSource::exportData(ostream &out)
{
    ref<CFArrayRef> allPersons(ABCopyArrayOfAllPeople(m_addressbook), "list of all people");

    for (CFIndex i = 0; i < CFArrayGetCount(allPersons); i++) {
        ABRecordRef person = (ABRecordRef)CFArrayGetValueAtIndex(allPersons, i);
        // CFStringRef descr = CFCopyDescription(person);
        ref<CFStringRef> cfuid(ABRecordCopyUniqueId(person), "reading UID");
        string uid(CFString2Std(cfuid));
        cxxptr<SyncItem> item(createItem(uid, true), "sync item");

        out << (char *)item->getData() << "\n";
    }
}

SyncItem *AddressBookSource::createItem(const string &uid, bool asVCard30)
{
    logItem(uid, "extracting from address book", true);

    ref<CFStringRef> cfuid(Std2CFString(uid));
    ref<ABPersonRef> person((ABPersonRef)ABCopyRecordForUniqueId(m_addressbook, cfuid), "contact");
    auto_ptr<SyncItem> item(new SyncItem(uid.c_str()));

#ifdef USE_ADDRESS_BOOK_VCARD
    ref<CFDataRef> vcard(ABPersonCopyVCardRepresentation(person), "vcard");
    SE_LOG_DEBUG(getDisplayName(), "%*s", (int)CFDataGetLength(vcard), (const char *)CFDataGetBytePtr(vcard));
    item->setData(CFDataGetBytePtr(vcard), CFDataGetLength(vcard));
#else
    string vcard;
    try {
        vCard2ABPerson conv(vcard, person);
        conv.fromPerson(asVCard30);
    } catch (const std::exception &ex) {
        throwError("creating vCard for " + uid + " failed: " + ex.what());
    }
    item->setData(vcard.c_str(), vcard.size());
#endif

    item->setDataType(getMimeType());
    item->setModificationTime(0);

    return item.release();
}

AddressBookSource::InsertItemResult AddressBookSource::insertItem(const string &luid, const SyncItem &item)
{
    bool update = !luid.empty();
    string newluid = luid;
    string data = (const char *)item.getData();
    ref<ABPersonRef> person;

#ifdef USE_ADDRESS_BOOK_VCARD
    if (uid) {
        // overwriting the UID of a new contact failed - resort to deleting the old contact and inserting a new one
        deleteItem(uid);
    }

    ref<CFDataRef> vcard(CFDataCreate(NULL, (const UInt8 *)data.c_str(), data.size()), "vcard");
    person.set((ABPersonRef)ABPersonCreateWithVCardRepresentation(vcard));
    if (!person) {
        throwError(string("parsing vcard ") + data);
    }
#else
    if (update) {
        // overwrite existing contact
        ref<CFStringRef> cfuid(Std2CFString(luid));
        person.set((ABPersonRef)ABCopyRecordForUniqueId(m_addressbook, cfuid), "contact");
    } else {
        // new contact
        person.set(PersonCreateWrapper(m_addressbook), "contact");
    }
    try {
        SE_LOG_DEBUG(getDisplayName(), "storing vCard for %s:\n%s",
                  update ? luid.c_str() : "new contact",
                  data.c_str());
        vCard2ABPerson converter(data, person);
        converter.toPerson();
    } catch (const std::exception &ex) {
        throwError(string("storing vCard for ") + (update ? luid : "new contact") + " failed: " + ex.what());
    }
#endif


    // make sure we have a modification time stamp, otherwise the address book
    // sets one at random times
    CFAbsoluteTime nowabs = CFAbsoluteTimeGetCurrent();
#ifdef IPHONE
    void *now = (void *)(int)round(nowabs);
#else
    ref<CFDateRef> now(CFDateCreate(NULL, nowabs), "current time");
#endif
    if (!ABRecordSetValue(person, kABModificationDateProperty, now)) {
        throwError("setting mod time");
    }

    // existing contacts do not have to (and cannot) be added (?)
    if (update || ABAddRecord(m_addressbook, person)) {
        // need to save to get UID (iPhone) and final modification time (Mac OS X)?
        ABSave(m_addressbook);

        ref<CFStringRef> cfuid(ABRecordCopyUniqueId(person), "uid");
        newluid = CFString2Std(cfuid);
    } else {
        throwError("storing new contact");
    }
    string modtime = getModTime(person);

    return InsertItemResult(newluid, modtime, false);
}

void AddressBookSource::deleteItem(const string &uid)
{
    ref<CFStringRef> cfuid(Std2CFString(uid.c_str()));
    ref<ABPersonRef> person((ABPersonRef)ABCopyRecordForUniqueId(m_addressbook, cfuid));

    if (person) {
        if (!ABRemoveRecord(m_addressbook, person)) {
            throwError(string("deleting contact ") + uid);
        }
    } else {
        SE_LOG_DEBUG(getDisplayName(), "%s: %s: request to delete non-existant contact ignored",
                  getName(), uid.c_str());
    }
}

void AddressBookSource::logItem(const string &uid, const string &info, bool debug)
{
    if (getLevel() >= (debug ? Logger::DEBUG : Logger::INFO)) {
        string line;

#if 0
        // TODO

        if (e_book_get_contact( m_addressbook,
                                uid.c_str(),
                                &contact,
                                &gerror )) {
            const char *fileas = (const char *)e_contact_get_const( contact, E_CONTACT_FILE_AS );
            if (fileas) {
                line += fileas;
            } else {
                const char *name = (const char *)e_contact_get_const( contact, E_CONTACT_FULL_NAME );
                if (name) {
                    line += name;
                } else {
                    line += "<unnamed contact>";
                }
            }
        } else {
            line += "<name unavailable>";
        }
#endif

        line += " (";
        line += uid;
        line += "): ";
        line += info;
        
        SE_LOG(getDisplayName(), debug ? Logger::DEBUG : Logger::INFO, "%s", line.c_str() );
    }
}

void AddressBookSource::logItem(const SyncItem &item, const string &info, bool debug)
{
    if (getLevel() >= (debug ? Logger::DEBUG : Logger::INFO)) {
        string line;
        const char *data = (const char *)item.getData();
        int datasize = item.getDataSize();
        if (datasize <= 0) {
            data = "";
            datasize = 0;
        }
        string vcard( data, datasize );

        size_t offset = vcard.find( "FN:");
        if (offset != vcard.npos) {
            int len = vcard.find( "\r", offset ) - offset - 3;
            line += vcard.substr( offset + 3, len );
        } else {
            line += "<unnamed contact>";
        }

        if (!item.getKey() ) {
            line += ", NULL UID (?!)";
        } else if (!strlen( item.getKey() )) {
            line += ", empty UID";
        } else {
            line += ", ";
            line += item.getKey();

#if 0
            // TODO
            EContact *contact;
            GError *gerror = NULL;
            if (e_book_get_contact( m_addressbook,
                                    item.getKey(),
                                    &contact,
                                    &gerror )) {
                line += ", EV ";
                const char *fileas = (const char *)e_contact_get_const( contact, E_CONTACT_FILE_AS );
                if (fileas) {
                    line += fileas;
                } else {
                    const char *name = (const char *)e_contact_get_const( contact, E_CONTACT_FULL_NAME );
                    if (name) {
                        line += name;
                    } else {
                        line += "<unnamed contact>";
                    }
                }
            } else {
                line += ", not in Evolution";
            }
#endif
        }
        line += ": ";
        line += info;
        
        SE_LOG(getDisplayName(), debug ? Logger::DEBUG : Logger::INFO, "%s", line.c_str() );
    }
}

SE_END_CXX

#endif /* ENABLE_ADDRESSBOOK */
