/*
    SPDX-FileCopyrightText: 2012-2018 Daniel Vrátil <dvratil@kde.org>

    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/

#include "contactsservice.h"
#include "contact.h"
#include "contactsgroup.h"
#include "utils.h"
#include "debug.h"

#include <QDomDocument>
#include <QDomElement>
#include <QJsonDocument>
#include <QUrlQuery>

/* Qt::escape() */
#include <QTextDocument>


namespace KGAPI2
{

namespace ContactsService
{

namespace Private
{
    static QString stringFromXMLMap(const QVariantMap &map, const QString &key) {
        const QVariantMap t = map.value(key).toMap();
        return t.value(QStringLiteral("$t")).toString();
    }

    ObjectPtr JSONToContactsGroup(const QVariantMap &map);
    ObjectPtr JSONToContact(const QVariantMap& map);

    static const QUrl GoogleApisUrl(QStringLiteral("https://www.google.com"));
    static const QString ContactsBasePath(QStringLiteral("/m8/feeds/contacts"));
    static const QString ContactsGroupBasePath(QStringLiteral("/m8/feeds/groups"));
    static const QString PhotoBasePath(QStringLiteral("/m8/feeds/photos/media"));

    static QByteArray addRelSchema(const QByteArray &rel)
    {
        if (!rel.startsWith("http://schemas.google.com/g/2005#")) {
            return "http://schemas.google.com/g/2005#" + rel;
        } else {
            return rel;
        }
    }
}

ObjectsList parseJSONFeed(const QByteArray& jsonFeed, FeedData& feedData)
{
    ObjectsList output;
    QJsonDocument document = QJsonDocument::fromJson(jsonFeed);
    const QVariantMap head = document.toVariant().toMap();
    const QVariantMap feed = head.value(QStringLiteral("feed")).toMap();
    const QVariantList categories = feed.value(QStringLiteral("category")).toList();
    for (const QVariant &c : categories) {
        const QVariantMap category = c.toMap();
        bool groups = false;

        if (category.value(QStringLiteral("term")).toString() == QLatin1String("http://schemas.google.com/contact/2008#group")) {
            groups = true;
        }

        const QVariantList entries = feed.value(QStringLiteral("entry")).toList();
        for (const QVariant &e : entries) {
            if (groups) {
                output << Private::JSONToContactsGroup(e.toMap());
            } else {
                output << Private::JSONToContact(e.toMap());
            }
        }
    }

    const QVariantList links = feed.value(QStringLiteral("link")).toList();
    for (const QVariant &l : links) {
        const QVariantMap link = l.toMap();
        if (link.value(QStringLiteral("rel")).toString() == QLatin1String("next")) {
            feedData.nextPageUrl = QUrl(link.value(QStringLiteral("href")).toString());
            break;
        }
    }

    feedData.totalResults = Private::stringFromXMLMap(feed, QStringLiteral("openSearch$totalResults")).toInt();
    feedData.startIndex = Private::stringFromXMLMap(feed, QStringLiteral("openSearch$startIndex")).toInt();
    feedData.itemsPerPage = Private::stringFromXMLMap(feed, QStringLiteral("openSearch$itemsPerPage")).toInt();

    return output;
}

QUrl fetchAllContactsUrl(const QString& user, bool showDeleted)
{
    QUrl url(Private::GoogleApisUrl);
    url.setPath(Private::ContactsBasePath % QLatin1Char('/') % user % QLatin1String("/full"));
    QUrlQuery query(url);
    query.addQueryItem(QStringLiteral("alt"), QStringLiteral("json"));
    if (showDeleted) {
        query.addQueryItem(QStringLiteral("showdeleted"), QStringLiteral("true"));
    }
    url.setQuery(query);

    return url;
}

QUrl fetchContactUrl(const QString& user, const QString& contactID)
{
    QString id;
    if (contactID.contains(QLatin1Char('/'))) {
        id = contactID.mid(contactID.lastIndexOf(QLatin1Char('/')) + 1);
    } else {
        id = contactID;
    }

    QUrl url(Private::GoogleApisUrl);
    url.setPath(Private::ContactsBasePath % QLatin1Char('/') % user % QLatin1String("/full/") % id);
    QUrlQuery query(url);
    query.addQueryItem(QStringLiteral("alt"), QStringLiteral("json"));
    url.setQuery(query);
    return url;
}

QUrl createContactUrl(const QString& user)
{
    QUrl url(Private::GoogleApisUrl);
    url.setPath(Private::ContactsBasePath % QLatin1Char('/') % user % QLatin1String("/full"));
    return url;
}

QUrl updateContactUrl(const QString& user, const QString& contactID)
{
    QString id;
    if (contactID.contains(QLatin1Char('/'))) {
        id = contactID.mid(contactID.lastIndexOf(QLatin1Char('/')) + 1);
    } else {
        id = contactID;
    }

    QUrl url(Private::GoogleApisUrl);
    url.setPath(Private::ContactsBasePath % QLatin1Char('/') % user % QLatin1String("/full/") % id);
    return url;
}

QUrl removeContactUrl(const QString& user, const QString& contactID)
{
    QString id;
    if (contactID.contains(QLatin1Char('/'))) {
        id = contactID.mid(contactID.lastIndexOf(QLatin1Char('/')) + 1);
    } else {
        id = contactID;
    }

    QUrl url(Private::GoogleApisUrl);
    url.setPath(Private::ContactsBasePath % QLatin1Char('/') % user % QLatin1String("/full/") % id);
    return url;
}

QUrl fetchAllGroupsUrl(const QString &user)
{
    QUrl url(Private::GoogleApisUrl);
    url.setPath(Private::ContactsGroupBasePath % QLatin1Char('/') % user % QLatin1String("/full"));
    QUrlQuery query(url);
    query.addQueryItem(QStringLiteral("alt"), QStringLiteral("json"));
    url.setQuery(query);
    return url;
}

QUrl fetchGroupUrl(const QString &user, const QString &groupId)
{
    QString id;
    if (groupId.contains(QLatin1Char('/'))) {
        id = groupId.mid(groupId.lastIndexOf(QLatin1Char('/')) + 1);
    } else {
        id = groupId;
    }

    QUrl url(Private::GoogleApisUrl);
    url.setPath(Private::ContactsGroupBasePath % QLatin1Char('/') % user % QLatin1String("/base/") % id);
    QUrlQuery query(url);
    query.addQueryItem(QStringLiteral("alt"), QStringLiteral("json"));
    url.setQuery(query);
    return url;
}

QUrl createGroupUrl(const QString &user)
{
    QUrl url(Private::GoogleApisUrl);
    url.setPath(Private::ContactsGroupBasePath % QLatin1Char('/') % user % QLatin1String("/full"));
    return url;
}

QUrl updateGroupUrl(const QString &user, const QString &groupId)
{
    QString id;
    if (groupId.contains(QLatin1Char('/'))) {
        id = groupId.mid(groupId.lastIndexOf(QLatin1Char('/')) + 1);
    } else {
        id = groupId;
    }

    QUrl url(Private::GoogleApisUrl);
    url.setPath(Private::ContactsGroupBasePath % QLatin1Char('/') % user % QLatin1String("/full/") % id);
    return url;
}

QUrl removeGroupUrl(const QString &user, const QString &groupId)
{
    QString id;
    if (groupId.contains(QLatin1Char('/'))) {
        id = groupId.mid(groupId.lastIndexOf(QLatin1Char('/')) + 1);
    } else {
        id = groupId;
    }

    QUrl url(Private::GoogleApisUrl);
    url.setPath(Private::ContactsGroupBasePath % QLatin1Char('/') % user % QLatin1String("/full/") % id);
    return url;
}

QUrl photoUrl(const QString& user, const QString& contactID)
{
    QString id;
    if (contactID.contains(QLatin1Char('/'))) {
        id = contactID.mid(contactID.lastIndexOf(QLatin1Char('/')) + 1);
    } else {
        id = contactID;
    }

    QUrl url(Private::GoogleApisUrl);
    url.setPath(Private::PhotoBasePath % QLatin1Char('/') % user % QLatin1Char('/') % id);
    return url;
}


QString APIVersion()
{
    return QStringLiteral("3.0");
}


/*********************************** PRIVATE *************************************/

ObjectPtr Private::JSONToContactsGroup(const QVariantMap& data)
{
    ContactsGroupPtr group(new ContactsGroup);

    group->setId(Private::stringFromXMLMap(data, QStringLiteral("id")));
    group->setEtag(data.value(QStringLiteral("gd$etag")).toString());

    group->setTitle(Private::stringFromXMLMap(data, QStringLiteral("title")));
    group->setContent(Private::stringFromXMLMap(data, QStringLiteral("content")));

    group->setUpdated(QDateTime::fromString(Private::stringFromXMLMap(data, QStringLiteral("updated")), Qt::ISODate));

    if (data.contains(QLatin1String("gContact$systemGroup"))) {
        group->setIsSystemGroup(true);
    } else {
        group->setIsSystemGroup(false);
    }

    return group;
}

ContactsGroupPtr JSONToContactsGroup(const QByteArray& jsonData)
{
    QJsonDocument document = QJsonDocument::fromJson(jsonData);
    const QVariantMap data = document.toVariant().toMap();
    const QVariantMap entry = data.value(QStringLiteral("entry")).toMap();
    const QVariantList categories = entry.value(QStringLiteral("category")).toList();

    bool isGroup = false;
    for (const QVariant &c : categories) {
        const QVariantMap category = c.toMap();

        if (category.value(QStringLiteral("term")).toString() == QLatin1String("http://schemas.google.com/contact/2008#group")) {
            isGroup = true;
            break;
        }
    }
    if (!isGroup) {
        return ContactsGroupPtr();
    }

    return Private::JSONToContactsGroup(entry).staticCast<ContactsGroup>();
}

ObjectPtr Private::JSONToContact(const QVariantMap& data)
{
    ContactPtr contact(new Contact);

    /* Google contact ID */
    contact->setUid(Private::stringFromXMLMap(data, QStringLiteral("id")));

    /* Google ETAG. This can be used to identify if the item was changed remotely */
    contact->setEtag(data.value(QStringLiteral("gd$etag")).toString());

    /* Date and time when contact was updated on the remote server */
    contact->setUpdated(QDateTime::fromString(Private::stringFromXMLMap(data, QStringLiteral("updated")), Qt::ISODate));

    /* If the contact was deleted, we don't need more info about it.
     * Just store our own flag, which will be then parsed by the resource
     * itself. */
    contact->setDeleted(data.value(QStringLiteral("gd$deleted")).isValid());

    /* Store URL of the picture. The URL will be used later by PhotoJob to fetch the picture
     * itself. */
    const QVariantList links = data.value(QStringLiteral("link")).toList();
    for (const QVariant &link : links) {
        const QVariantMap linkMap = link.toMap();
        if (linkMap.value(QStringLiteral("rel")).toString() == QLatin1String("http://schemas.google.com/contacts/2008/rel#photo")) {
            contact->setPhotoUrl(linkMap.value(QStringLiteral("href")).toString());
        }
    }

    /* Name */
    if (data.contains(QLatin1String("title"))) {
        contact->setName(Private::stringFromXMLMap(data, QStringLiteral("title")));
    }

    /* Formatted name */
    if (data.contains(QStringLiteral("gd$name"))) {
        const QVariantMap name = data.value(QStringLiteral("gd$name")).toMap();

        if (name.contains(QStringLiteral("gd$fullName"))) {
            contact->setFormattedName(Private::stringFromXMLMap(name, QStringLiteral("gd$fullName")));
        }
        if (name.contains(QStringLiteral("gd$givenName"))) {
            contact->setGivenName(Private::stringFromXMLMap(name, QStringLiteral("gd$givenName")));
        }
        if (name.contains(QStringLiteral("gd$familyName"))) {
            contact->setFamilyName(Private::stringFromXMLMap(name, QStringLiteral("gd$familyName")));
        }
        if (name.contains(QStringLiteral("gd$additionalName"))) {
            contact->setAdditionalName(Private::stringFromXMLMap(name, QStringLiteral("gd$additionalName")));
        }
        if (name.contains(QStringLiteral("gd$namePrefix"))) {
            contact->setPrefix(Private::stringFromXMLMap(name, QStringLiteral("gd$namePrefix")));
        }
        if (name.contains(QStringLiteral("gd$nameSuffix"))) {
            contact->setSuffix(Private::stringFromXMLMap(name, QStringLiteral("gd$nameSuffix")));
        }
    }

    /* Note */
    if (data.contains(QLatin1String("content"))) {
        contact->setNote(Private::stringFromXMLMap(data, QStringLiteral("content")));
    }

    /* Organization (work) - KABC supports only one organization */
    if (data.contains(QLatin1String("gd$organization"))) {
        const QVariantList organizations = data.value(QStringLiteral("gd$organization")).toList();
        const QVariantMap organization = organizations.first().toMap();

        if (organization.contains(QStringLiteral("gd$orgName"))) {
            contact->setOrganization(Private::stringFromXMLMap(organization, QStringLiteral("gd$orgName")));
        }
        if (organization.contains(QStringLiteral("gd$orgDepartment"))) {
            contact->setDepartment(Private::stringFromXMLMap(organization, QStringLiteral("gd$orgDepartment")));
        }
        if (organization.contains(QStringLiteral("gd$orgTitle"))) {
            contact->setTitle(Private::stringFromXMLMap(organization, QStringLiteral("gd$orgTitle")));
        }
        if (organization.contains(QStringLiteral("gd$where"))) {
            contact->setOffice(Private::stringFromXMLMap(organization, QStringLiteral("gd$where")));
        }
    }

    /* Nickname */
    if (data.contains(QStringLiteral("gContact$nickname"))) {
        contact->setNickName(Private::stringFromXMLMap(data, QStringLiteral("gContact$nickname")));
    }

    /* Occupation (= organization/title) */
    if (data.contains(QStringLiteral("gContact$occupation"))) {
        contact->setProfession(Private::stringFromXMLMap(data, QStringLiteral("gContact$occupation")));
    }

    /* Relationships */
    if (data.contains(QStringLiteral("gContact$relation"))) {
        const QVariantList relations = data.value(QStringLiteral("gContact$relation")).toList();
        for (const QVariant &r : relations) {
            const QVariantMap relation = r.toMap();
            if (relation.value(QStringLiteral("rel")).toString() == QLatin1String("spouse")) {
                contact->setSpousesName(relation.value(QStringLiteral("$t")).toString());
                continue;
            }

            if (relation.value(QStringLiteral("rel")).toString() == QLatin1String("manager")) {
                contact->setManagersName(relation.value(QStringLiteral("$t")).toString());
                continue;
            }

            if (relation.value(QStringLiteral("rel")).toString() == QLatin1String("assistant")) {
                contact->setAssistantsName(relation.value(QStringLiteral("$t")).toString());
                continue;
            }
        }
    }

    /* Anniversary */
    if (data.contains(QStringLiteral("gContact$event"))) {
        const QVariantList events = data.value(QStringLiteral("gContact$event")).toList();
        for (const QVariant &e : events) {
            const QVariantMap event = e.toMap();

            if (event.value(QStringLiteral("rel")).toString() == QLatin1String("anniversary")) {
                QVariantMap when = event.value(QStringLiteral("gd$when")).toMap();
                contact->setAnniversary(QDate::fromString(when.value(QStringLiteral("startTime")).toString(), Qt::ISODate));
            }
        }
    }

    /* Websites */
    if (data.contains(QStringLiteral("gContact$website"))) {
        const QVariantList websites = data.value(QStringLiteral("gContact$website")).toList();
        for (const QVariant &w : websites) {
            const QVariantMap web = w.toMap();
            const auto rel = web.value(QStringLiteral("rel")).toString();
            const QUrl url(web.value(QStringLiteral("href")).toString());
            if (rel == QLatin1String("home-page")) {
                KContacts::ResourceLocatorUrl locator;
                locator.setUrl(url);
                locator.setType(KContacts::ResourceLocatorUrl::Home);
                contact->insertExtraUrl(locator);
            } else if (rel == QLatin1String("work")) {
                KContacts::ResourceLocatorUrl locator;
                locator.setUrl(url);
                locator.setType(KContacts::ResourceLocatorUrl::Work);
                contact->insertExtraUrl(locator);
            } else if (rel == QLatin1String("profile")) {
                KContacts::ResourceLocatorUrl locator;
                locator.setUrl(url);
                locator.setType(KContacts::ResourceLocatorUrl::Profile);
                contact->insertExtraUrl(locator);
            } else if (rel == QLatin1String("blog")) {
                contact->setBlogFeed(url);
            } else {
                KContacts::ResourceLocatorUrl locator;
                locator.setUrl(url);
                locator.setType(KContacts::ResourceLocatorUrl::Other);
                contact->insertExtraUrl(locator);
            }
        }
    }

    /* Emails */
    const QVariantList emails = data.value(QStringLiteral("gd$email")).toList();
    for (const QVariant & em : emails) {
        const QVariantMap email = em.toMap();
        const auto emailType = Contact::emailSchemeToProtocolName(email.value(QStringLiteral("rel")).toString());
        KContacts::Email emailAddress(email.value(QStringLiteral("address")).toString());
        emailAddress.setType(Contact::emailSchemeToProtocolType(emailType));
        emailAddress.setPreferred(email.value(QStringLiteral("primary")).toBool());
        contact->addEmail(emailAddress);
    }

    /* IMs */
    const QVariantList ims = data.value(QStringLiteral("gd$im")).toList();
    for (const QVariant & i : ims) {
        const QVariantMap im = i.toMap();
        const QString protocol = Contact::IMSchemeToProtocolName(im.value(QStringLiteral("protocol")).toString());
        contact->insertCustom(QLatin1String("messaging/") + protocol,
                             QStringLiteral("All"),
                             im.value(QStringLiteral("address")).toString());
    }

    /* Phone numbers */
    const QVariantList phones = data.value(QStringLiteral("gd$phoneNumber")).toList();
    for (const QVariant & p : phones) {
        const QVariantMap phone = p.toMap();
        KContacts::PhoneNumber phoneNumber(
            phone.value(QStringLiteral("$t")).toString(),
            Contact::phoneSchemeToType(phone.value(QStringLiteral("rel")).toString()));
        phoneNumber.setId(phoneNumber.number());
        contact->insertPhoneNumber(phoneNumber);
    }

    /* Addresses */
    const QVariantList addresses = data.value(QStringLiteral("gd$structuredPostalAddress")).toList();
    for (const QVariant &a : addresses) {
        const QVariantMap address = a.toMap();
        KContacts::Address addr;
        addr.setId(QString::number(contact->addresses().count()));
        if (!address.contains(QLatin1String("gd$city")) &&
                !address.contains(QLatin1String("gd$country")) &&
                !address.contains(QLatin1String("gd$postcode")) &&
                !address.contains(QLatin1String("gd$region")) &&
                !address.contains(QLatin1String("gd$pobox")))
        {
            addr.setExtended(Private::stringFromXMLMap(address, QStringLiteral("gd$street")));
        } else {
            if (address.contains(QLatin1String("gd$street"))) {
                addr.setStreet(Private::stringFromXMLMap(address, QStringLiteral("gd$street")));
            }
            if (address.contains(QLatin1String("gd$country"))) {
                addr.setCountry(Private::stringFromXMLMap(address, QStringLiteral("gd$country")));
            }
            if (address.contains(QLatin1String("gd$city"))) {
                addr.setLocality(Private::stringFromXMLMap(address, QStringLiteral("gd$city")));
            }
            if (address.contains(QLatin1String("gd$postcode"))) {
                addr.setPostalCode(Private::stringFromXMLMap(address, QStringLiteral("gd$postcode")));
            }
            if (address.contains(QLatin1String("gdregion"))) {
                addr.setRegion(Private::stringFromXMLMap(address, QStringLiteral("gd$region")));
            }
            if (address.contains(QLatin1String("gd$pobox"))) {
                addr.setPostOfficeBox(Private::stringFromXMLMap(address, QStringLiteral("gd$pobox")));
            }
        }
        addr.setType(Contact::addressSchemeToType(address.value(QStringLiteral("rel")).toString()));

        contact->insertAddress(addr);
    }

    /* Birthday */
    const QVariantMap bDay = data.value(QStringLiteral("gContact$birthday")).toMap();
    if (!bDay.isEmpty()) {
        QString birthday = bDay.value(QStringLiteral("when")).toString();
        /* Birthdays in format "--MM-DD" are valid and mean that no year has
         * been specified. Since KABC does not support birthdays without year,
         * we simulate that by specifying a fake year - 1900 */
        if (birthday.startsWith(QLatin1String("--"))) {
            birthday = QLatin1String("1900") + birthday.mid(1);
        }
        contact->setBirthday(QDateTime::fromString(birthday, QStringLiteral("yyyy-MM-dd")));
    }

    /* User-defined fields */
    const QVariantList userDefined = data.value(QStringLiteral("gContact$userDefinedField")).toList();
    for (const QVariant & u : userDefined) {
        const QVariantMap field = u.toMap();
        contact->insertCustom(QStringLiteral("KADDRESSBOOK"),
                             field.value(QStringLiteral("key")).toString(),
                             field.value(QStringLiteral("value")).toString());
    }

    /* Groups */
    const QVariantList groups = data.value(QStringLiteral("gContact$groupMembershipInfo")).toList();
    QStringList groupsList;
    for (const QVariant & g : groups) {
        const QVariantMap group = g.toMap();
        if (group.value(QStringLiteral("deleted")).toBool() == false) {
            groupsList.append(group.value(QStringLiteral("href")).toString());
        }
    }
    contact->insertCustom(QStringLiteral("GCALENDAR"), QStringLiteral("groupMembershipInfo"),
                         groupsList.join(QLatin1Char(',')));

    return contact;
}

ContactPtr JSONToContact(const QByteArray& jsonData)
{
    QJsonDocument document = QJsonDocument::fromJson(jsonData);
    const QVariantMap data = document.toVariant().toMap();
    const QVariantMap entry = data.value(QStringLiteral("entry")).toMap();
    const QVariantList categories = entry.value(QStringLiteral("category")).toList();

    bool isContact = false;
    for (const QVariant &c : categories) {
        const QVariantMap category = c.toMap();

        if (category.value(QStringLiteral("term")).toString() == QLatin1String("http://schemas.google.com/contact/2008#contact")) {
            isContact = true;
            break;
        }
    }
    if (!isContact) {
        return ContactPtr();
    }

    return Private::JSONToContact(entry).staticCast<Contact>();
}


QByteArray contactToXML(const ContactPtr& contact)
{
    QByteArray output;

    QStringList parsedCustoms;

    /* Name */
    output.append("<gd:name>");
    if (!contact->givenName().isEmpty()) {
        output.append("<gd:givenName>").append(contact->givenName().toHtmlEscaped().toUtf8()).append("</gd:givenName>");
    }
    if (!contact->familyName().isEmpty()) {
        output.append("<gd:familyName>").append(contact->familyName().toHtmlEscaped().toUtf8()).append("</gd:familyName>");
    }
    if (!contact->assembledName().isEmpty()) {
        output.append("<gd:fullName>").append(contact->assembledName().toHtmlEscaped().toUtf8()).append("</gd:fullName>");
    }
    if (!contact->additionalName().isEmpty()) {
        output.append("<gd:additionalName>").append(contact->additionalName().toHtmlEscaped().toUtf8()).append("</gd:additionalName>");
    }
    if (!contact->prefix().isEmpty()) {
        output.append("<gd:namePrefix>").append(contact->prefix().toHtmlEscaped().toUtf8()).append("</gd:namePrefix>");
    }
    if (!contact->suffix().isEmpty()) {
        output.append("<gd:nameSuffix>").append(contact->suffix().toHtmlEscaped().toUtf8()).append("</gd:nameSuffix>");
    }
    output.append("</gd:name>");

    /* Notes */
    if (!contact->note().isEmpty()) {
        output.append("<atom:content type='text'>").append(contact->note().toHtmlEscaped().toUtf8()).append("</atom:content>");
    }

    /* Organization (work) */
    QByteArray org;
    const QString office = contact->office();
    if (!contact->organization().isEmpty()) {
        org.append("<gd:orgName>").append(contact->organization().toHtmlEscaped().toUtf8()).append("</gd:orgName>");
    }
    if (!contact->department().isEmpty()) {
        org.append("<gd:orgDepartment>").append(contact->department().toHtmlEscaped().toUtf8()).append("</gd:orgDepartment>");
    }
    if (!contact->title().isEmpty()) {
        org.append("<gd:orgTitle>").append(contact->title().toHtmlEscaped().toUtf8()).append("</gd:orgTitle>");
    }
    if (!office.isEmpty()) {
        org.append("<gd:where>").append(office.toHtmlEscaped().toUtf8()).append("</gd:where>");
        parsedCustoms << QStringLiteral("KADDRESSBOOK-X-Office");
    }
    if (!org.isEmpty()) {
        output.append("<gd:organization rel=\"http://schemas.google.com/g/2005#work\">").append(org).append("</gd:organization>");
    }

    /* Nickname */
    if (!contact->nickName().isEmpty()) {
        output.append("<gContact:nickname>").append(contact->nickName().toHtmlEscaped().toUtf8()).append("</gContact:nickname>");
    }

    /* Occupation */
    if (!contact->profession().isEmpty()) {
        output.append("<gContact:occupation>").append(contact->profession().toHtmlEscaped().toUtf8()).append("</gContact:occupation>");
        parsedCustoms << QStringLiteral("KADDRESSBOOK-X-Profession");
    }

    /* Spouse */
    const QString spouse = contact->spousesName();
    if (!spouse.isEmpty()) {
        output.append("<gContact:relation rel=\"spouse\">").append(spouse.toHtmlEscaped().toUtf8()).append("</gContact:relation>");
        parsedCustoms << QStringLiteral("KADDRESSBOOK-X-SpousesName");
    }

    /* Manager */
    const QString manager = contact->managersName();
    if (!manager.isEmpty()) {
        output.append("<gContact:relation rel=\"manager\">").append(manager.toHtmlEscaped().toUtf8()).append("</gContact:relation>");
        parsedCustoms << QStringLiteral("KADDRESSBOOK-X-ManagersName");
    }

    /* Assistant */
    const QString assistant = contact->assistantsName();
    if (!assistant.isEmpty()) {
        output.append("<gContact:relation rel=\"assistant\">").append(assistant.toHtmlEscaped().toUtf8()).append("</gContact:relation>");
        parsedCustoms << QStringLiteral("KADDRESSBOOK-X-AssistantsName");
    }

    /* Anniversary */
    const QString anniversary = contact->anniversary().toString(Qt::ISODate);
    if (!anniversary.isEmpty()) {
        output.append(R"(<gContact:event rel="anniversary"><gd:when startTime=")").append(anniversary.toHtmlEscaped().toUtf8()).append("\" /></gContact:event>");
        parsedCustoms << QStringLiteral("KADDRESSBOOK-X-Anniversary");
    }

    /* Blog */
    const QString blog = contact->blogFeed().url();
    if (!blog.isEmpty()) {
        output.append(R"(<gContact:website rel="blog" href=")").append(blog.toHtmlEscaped().toUtf8()).append("\" />");
        parsedCustoms << QStringLiteral("KADDRESSBOOK-BlogFeed");
    }

    /* URLs */
    const auto extraUrls = contact->extraUrlList();
    for (const auto &extraUrl : extraUrls) {
        const auto type = extraUrl.type();
        QByteArray rel;
        if (type & KContacts::ResourceLocatorUrl::Other || type & KContacts::ResourceLocatorUrl::Unknown) {
            rel = "other";
        } else if (type & KContacts::ResourceLocatorUrl::Home) {
            rel = "home-page";
        } else if (type & KContacts::ResourceLocatorUrl::Work) {
            rel = "work";
        } else if (type & KContacts::ResourceLocatorUrl::Profile) {
            rel = "profile";
        }

        output.append("<gContact:website rel=\"" + rel + "\" href=\"" + extraUrl.url().toString().toHtmlEscaped().toUtf8() + "\" />");
    }

    /* Emails */
    const auto preferredEmail = contact->preferredEmail();
    const auto emailList = contact->emailList();
    for(const auto &email : emailList) {
        const auto type = email.type();
        QByteArray rel;
        if (type & KContacts::Email::Home || type & KContacts::Email::Unknown) {
            rel = Private::addRelSchema("home");
        } else if (type & KContacts::Email::Work) {
            rel = Private::addRelSchema("work");
        } else if (type & KContacts::Email::Other) {
            rel = Private::addRelSchema("other");
        }

        output.append("<gd:email rel='" + rel + "' address='" + email.mail().toHtmlEscaped().toUtf8() + "'");
        if (email.mail() == preferredEmail) {
            output.append(" primary=\"true\"");
        }
        output.append("/>");
    }

    /* IMs */
    const QString im_str = QStringLiteral("<gd:im address=\"%1\" protocol=\"%2\" rel=\"http://schemas.google.com/g/2005#other\" primary=\"%3\"/>");
    const auto imppList = contact->imppList();
    for (const auto &impp : imppList) {
        output.append(im_str.arg(impp.address().path(), Contact::IMProtocolNameToScheme(impp.serviceType()), Utils::bool2Str(impp.isPreferred())).toUtf8());
    }
    const auto customs = contact->customs();
    for (const QString &im : customs) {
        if (im.startsWith(QLatin1String("messaging/"))) {
            QString key = im.left(im.indexOf(QLatin1Char(':')));
            QString value = im.mid(im.indexOf(QLatin1Char(':')) + 1);
            QString proto = key.mid(10);
            proto.chop(4);
            bool primary = (contact->custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-IMAddress")) == value);
            output.append(im_str.arg(value, Contact::IMProtocolNameToScheme(proto), Utils::bool2Str(primary)).toUtf8());
            parsedCustoms << key;
        /* X-messaging is probably a new key (?) used by KAddressbook when importing
         * contacts from vCard. */
        } else if (im.startsWith(QLatin1String("X-messaging"))) {
            const QString key = im.left(im.indexOf(QLatin1Char(':')));
            const QString value = im.mid(im.indexOf(QLatin1Char(':')) + 1);
            QString proto = key.mid(12); /* strlen("X-messaging/") */
            if (proto.endsWith(QLatin1String("-All"))) {
                proto.chop(4);
            }
            output.append(im_str.arg(value, proto, QStringLiteral("false")).toUtf8());
            parsedCustoms << key;
        }
    }
    parsedCustoms << QStringLiteral("KADDRESSBOOK-X-IMAddress");

    /* Phone numbers */
    const QString phone_str = QStringLiteral("<gd:phoneNumber rel=\"%1\">%2</gd:phoneNumber>");
    const auto phoneNumbers = contact->phoneNumbers();
    for (const KContacts::PhoneNumber &number : phoneNumbers) {
        output.append(phone_str.arg(Contact::phoneTypeToScheme(number.type()), number.number()).toUtf8());
    }

    /* Address */
    const auto addresses = contact->addresses();
    for (const KContacts::Address &address : addresses) {
        output.append("<gd:structuredPostalAddress rel='")
        .append(Contact::addressTypeToScheme(address.type()).toUtf8())
        .append("'>");

        if (!address.locality().isEmpty()) {
            output.append("<gd:city>").append(address.locality().toHtmlEscaped().toUtf8()).append("</gd:city>");
        }
        if (!address.street().isEmpty()) {
            output.append("<gd:street>").append(address.street().toHtmlEscaped().toUtf8()).append("</gd:street>");
        }
        if (!address.region().isEmpty()) {
            output.append("<gd:region>").append(address.region().toHtmlEscaped().toUtf8()).append("</gd:region>");
        }
        if (!address.postalCode().isEmpty()) {
            output.append("<gd:postcode>").append(address.postalCode().toHtmlEscaped().toUtf8()).append("</gd:postcode>");
        }
        if (!address.country().isEmpty()) {
            output.append("<gd:country>").append(address.country().toHtmlEscaped().toUtf8()).append("</gd:country>");
        }
        if (!address.formatted(KContacts::AddressFormatStyle::Postal).isEmpty()) {
            output.append("<gd:formattedAddress>").append(address.formatted(KContacts::AddressFormatStyle::Postal).toHtmlEscaped().toUtf8()).append("</gd:formattedAddress>");
        }
        output.append("</gd:structuredPostalAddress>");
    }

    /* Birthday */
    const QDate birthday = contact->birthday().date();
    if (birthday.isValid()) {
        QString birthdayStr;
        /* We use year 1900 as a fake year for birthdays without a year specified.
            * Here we assume that nobody actually has a contact born in 1900 and so
            * we replace 1900 by "-", so that we get "--MM-dd" date, which is a valid
            * birthday date according to RFC6350 */
        if (birthday.year() == 1900) {
            birthdayStr = birthday.toString(QStringLiteral("--MM-dd"));
        } else {
            birthdayStr = birthday.toString(QStringLiteral("yyyy-MM-dd"));
        }
        output.append("<gContact:birthday when='").append(birthdayStr.toUtf8()).append("'/>");
    }

    const QStringList groups = contact->custom(QStringLiteral("GCALENDAR"), QStringLiteral("groupMembershipInfo")).split(QLatin1Char(','));
    qCDebug(KGAPIDebug) << groups;
    if ((!groups.isEmpty()) && !groups.at(0).isEmpty()) {
        for (const QString & group :groups) {
            bool removed = contact->groupIsDeleted(group);
            if (!removed) {
                output.append(QStringLiteral("<gContact:groupMembershipInfo deleted=\"false\" href=\"%2\" />").arg(group).toUtf8());
            }
        }
    }
    parsedCustoms << QStringLiteral("GCALENDAR-groupMembershipInfo");

    /* User-defined fields */
    const QStringList customsLst = contact->customs();
    const QString defined_str = QStringLiteral("<gContact:userDefinedField key=\"%1\" value=\"%2\" />");
    for (const QString &customStr : customsLst) {
        QString key = customStr.left(customStr.indexOf(QLatin1Char(':')));
        if (!parsedCustoms.contains(key)) {
            if (key.startsWith(QLatin1String("KADDRESSBOOK-"))) {
                key.remove(QStringLiteral("KADDRESSBOOK-"));
            }
            const QString value = customStr.mid(customStr.indexOf(QLatin1Char(':')) + 1);
            output.append(defined_str.arg(key.toHtmlEscaped(), value).toHtmlEscaped().toUtf8());
        }
    }

    return output;
}

QByteArray contactsGroupToXML(const ContactsGroupPtr& group)
{
    QByteArray output;

    output.append("<atom:title type=\"text\">").append(group->title().toHtmlEscaped().toUtf8()).append("</atom:title>");
    output.append("<atom:content type=\"text\">").append(group->content().toHtmlEscaped().toUtf8()).append("</atom:content>");

    return output;
}

ContactPtr XMLToContact(const QByteArray& xmlData)
{
    QByteArray xmlDoc;
    /* Document without <xml> header is not valid and Qt won't parse it */
    if (!xmlData.contains("<?xml")) {
        xmlDoc.append("<?xml version='1.0' encoding='UTF-8'?>");
    }
    xmlDoc.append(xmlData);

    QDomDocument doc;
    doc.setContent(xmlDoc);
    const QDomNodeList entry = doc.elementsByTagName(QStringLiteral("entry"));
    QDomNodeList data;
    if (!entry.isEmpty()) {
        data = entry.at(0).childNodes();
    } else {
        return ContactPtr();
    }

    bool isGroup = false;
    for (int i = 0; i < data.count(); ++i) {
        const QDomNode n = data.at(i);
        const QDomElement e = n.toElement();

        if (((e.tagName() == QLatin1String("category")) &&
             (e.attribute(QStringLiteral("term")) == QLatin1String("http://schemas.google.com/contact/2008#group"))) ||
            ((e.tagName() == QLatin1String("atom:category")) &&
             (e.attribute(QStringLiteral("term")) == QLatin1String("http://schemas.google.com/g/2005#group"))))
        {
            isGroup = true;
            break;
        }
    }

    if (isGroup) {
        return ContactPtr();
    }

    QStringList groups;
    ContactPtr contact(new Contact);

    contact->setEtag(entry.at(0).toElement().attribute(QStringLiteral("gd:etag")));

    for (int i = 0; i < data.count(); ++i) {
        const QDomNode n = data.at(i);
        const QDomElement e = n.toElement();

        if (e.tagName() == QLatin1String("id")) {
            contact->setUid(e.text());
            continue;
        }

        /* ETag */
        if (e.tagName() == QLatin1String("etag")) {
            contact->setEtag(e.text());
            continue;
        }

        if (e.tagName() == QLatin1String("gd:name")) {
            const QDomNodeList l = e.childNodes();
            for (int j = 0; j < l.length(); ++j) {
                const QDomElement el = l.at(j).toElement();

                if (el.tagName() == QLatin1String("gd:fullName")) {
                    contact->setFormattedName(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:givenName")) {
                    contact->setGivenName(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:familyName")) {
                    contact->setFamilyName(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:additionalName")) {
                    contact->setAdditionalName(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:namePrefix")) {
                    contact->setPrefix(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:nameSuffix")) {
                    contact->setSuffix(el.text());
                    continue;
                }

            }
            continue;
        }

        /* If the contact was deleted, we don't need more info about it.
        * Just store our own flag, which will be then parsed by the resource
        * itself. */
        contact->setDeleted(e.tagName() == QLatin1String("gd:deleted"));

        if (e.tagName() == QLatin1String("updated")) {
            contact->setUpdated(QDateTime::fromString(e.text(), Qt::ISODate));
        }

        /* Store URL of the picture. The URL will be used later by PhotoJob to fetch the picture
        * itself. */
        if ((e.tagName() == QLatin1String("link")) &&
            (e.attribute(QStringLiteral("rel")) == QLatin1String("http://schemas.google.com/contacts/2008/rel#photo"))) {
            contact->setPhotoUrl(e.attribute(QStringLiteral("href")));    /* URL */
            continue;
        }

        /* Name */
        if (e.tagName() == QLatin1String("title")) {
            contact->setName(e.text());
            continue;
        }

        /* Note */
        if (e.tagName() == QLatin1String("content")) {
            contact->setNote(e.text());
            continue;
        }

        /* Organization (work) - KABC supports only organization */
        if (e.tagName() == QLatin1String("gd:organization")) {
            const QDomNodeList l = e.childNodes();
            for (int i = 0; i < l.length(); ++i) {
                const QDomElement el = l.at(i).toElement();

                if (el.tagName() == QLatin1String("gd:orgName")) {
                    contact->setOrganization(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:orgDepartment")) {
                    contact->setDepartment(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:orgTitle")) {
                    contact->setTitle(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:where")) {
                    contact->setOffice(el.text());
                    continue;
                }
            }
            continue;
        }

        /* Nickname */
        if (e.tagName() == QLatin1String("gContact:nickname")) {
            contact->setNickName(e.text());
            continue;
        }

        /* Occupation (= organization/title) */
        if (e.tagName() == QLatin1String("gContact:occupation")) {
            contact->setProfession(e.text());
            continue;
        }

        /* Relationships */
        if (e.tagName() == QLatin1String("gContact:relation")) {
            if (e.attribute(QStringLiteral("rel"), QString()) == QLatin1String("spouse")) {
                contact->setSpousesName(e.text());
                continue;
            }

            if (e.attribute(QStringLiteral("rel"), QString()) == QLatin1String("manager")) {
                contact->setManagersName(e.text());
                continue;
            }

            if (e.attribute(QStringLiteral("rel"), QString()) == QLatin1String("assistant")) {
                contact->setAssistantsName(e.text());
                continue;
            }

            continue;
        }

        /* Anniversary */
        if (e.tagName() == QLatin1String("gContact:event")) {
            if (e.attribute(QStringLiteral("rel"), QString()) == QLatin1String("anniversary")) {
                QDomElement w = e.firstChildElement(QStringLiteral("gd:when"));
                contact->setAnniversary(QDate::fromString(w.attribute(QStringLiteral("startTime"), QString()), Qt::ISODate));
            }

            continue;
        }

        /* Websites */
        if (e.tagName() == QLatin1String("gContact:website")) {
            if (e.attribute(QStringLiteral("rel"), QString()) == QLatin1String("blog")) {
                contact->setBlogFeed(QUrl(e.attribute(QStringLiteral("href"), QString())));
                continue;
            }

            KContacts::ResourceLocatorUrl url;
            QString rel = e.attribute(QStringLiteral("rel")).toLower();
            if (rel == QLatin1String("home-page")) {
                url.setType(KContacts::ResourceLocatorUrl::Home);
            } else if (rel == QLatin1String("work")) {
                url.setType(KContacts::ResourceLocatorUrl::Work);
            } else if (rel == QLatin1String("profile")) {
                url.setType(KContacts::ResourceLocatorUrl::Profile);
            } else {
                url.setType(KContacts::ResourceLocatorUrl::Other);
            }

            url.setUrl(QUrl(e.attribute(QStringLiteral("href"), {})));
            contact->insertExtraUrl(url);

            continue;
        }

        /* Emails */
        if (e.tagName() == QLatin1String("gd:email")) {
            const auto emailType = Contact::emailSchemeToProtocolName(e.attribute(QStringLiteral("rel"), {}));

            KContacts::Email emailAddress(e.attribute(QStringLiteral("address")));
            emailAddress.setType(Contact::emailSchemeToProtocolType(emailType));
            emailAddress.setPreferred(e.attribute(QStringLiteral("primary")).toLower() == QLatin1String("true"));
            contact->addEmail(emailAddress);
            continue;
        }

        /* IMs */
        if (e.tagName() == QLatin1String("gd:im")) {
            contact->insertCustom(QLatin1String("messaging/") + Contact::IMSchemeToProtocolName(e.attribute(QStringLiteral("protocol"))),
                                  QStringLiteral("All"),
                                  e.attribute(QStringLiteral("address")));
            continue;
        }

        /* Phone numbers */
        if (e.tagName() == QLatin1String("gd:phoneNumber")) {
            KContacts::PhoneNumber number(e.text(), Contact::phoneSchemeToType(e.attribute(QStringLiteral("rel"))));
            number.setId(e.text());
            contact->insertPhoneNumber(number);
            continue;
        }

        /* Addresses */
        if (e.tagName() == QLatin1String("gd:structuredPostalAddress")) {
            KContacts::Address address;
            address.setId(QString::number(contact->addresses().count()));
            const QDomNodeList l = e.childNodes();
            for (int i = 0; i < l.length(); ++i) {
                const QDomElement el = l.at(i).toElement();

                if (el.tagName() == QLatin1String("gd:street")) {
                    address.setStreet(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:country")) {
                    address.setCountry(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:city")) {
                    address.setLocality(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:postcode")) {
                    address.setPostalCode(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:region")) {
                    address.setRegion(el.text());
                    continue;
                }

                if (el.tagName() == QLatin1String("gd:pobox")) {
                    address.setPostOfficeBox(el.text());
                    continue;
                }
            }

            address.setType(Contact::addressSchemeToType(e.attribute(QStringLiteral("rel")),
                                                         (e.attribute(QStringLiteral("primary")) == QLatin1String("true"))));
            contact->insertAddress(address);
            continue;
        }

        /* Birthday */
        if (e.tagName() == QLatin1String("gContact:birthday")) {
            QString birthday = e.attribute(QStringLiteral("when"));
            /* Birthdays in format "--MM-DD" are valid and mean that no year has
             * been specified. Since KABC does not support birthdays without year,
             * we simulate that by specifying a fake year - 1900 */
            if (birthday.startsWith(QLatin1String("--"))) {
                birthday = QLatin1String("1900") + birthday.mid(1);
            }
            contact->setBirthday(QDateTime::fromString(birthday, QStringLiteral("yyyy-MM-dd")));
            continue;
        }

        /* User-defined tags */
        if (e.tagName() == QLatin1String("gContact:userDefinedField")) {
            contact->insertCustom(QStringLiteral("KADDRESSBOOK"),
                                  e.attribute(QStringLiteral("key"), QString()),
                                  e.attribute(QStringLiteral("value"), QString()));
            continue;
        }

        if (e.tagName() == QLatin1String("gContact:groupMembershipInfo")) {
            if (e.hasAttribute(QStringLiteral("deleted")) || e.attribute(QStringLiteral("deleted")).toInt() == false) {
                groups.append(e.attribute(QStringLiteral("href")));
            }
        }
    }

    contact->insertCustom(QStringLiteral("GCALENDAR"), QStringLiteral("groupMembershipInfo"), groups.join(QLatin1Char(',')));

    return contact;
}

ContactsGroupPtr XMLToContactsGroup(const QByteArray& xmlData)
{
    QByteArray xmlDoc;
    /* Document without <xml> header is not valid and Qt won't parse it */
    if (!xmlData.contains("<?xml")) {
        xmlDoc.append("<?xml version='1.0' encoding='UTF-8'?>");
    }
    xmlDoc.append(xmlData);

    QDomDocument doc;
    doc.setContent(xmlDoc);
    const QDomNodeList entry = doc.elementsByTagName(QStringLiteral("entry"));
    QDomNodeList data;
    if (!entry.isEmpty()) {
        data = entry.at(0).childNodes();
    } else {
        return ContactsGroupPtr();
    }

    bool isGroup = false;
    for (int i = 0; i < data.count(); ++i) {
        const QDomNode n = data.at(i);
        const QDomElement e = n.toElement();

        if (((e.tagName() == QLatin1String("category")) &&
             (e.attribute(QStringLiteral("term")) == QLatin1String("http://schemas.google.com/contact/2008#group"))) ||
            ((e.tagName() == QLatin1String("atom:category")) &&
             (e.attribute(QStringLiteral("term")) == QLatin1String("http://schemas.google.com/g/2005#group"))))
        {
            isGroup = true;
            break;
        }
    }

    if (!isGroup) {
        return ContactsGroupPtr();
    }

    ContactsGroupPtr group(new ContactsGroup);
    QStringList groups;

    for (int i = 0; i < data.count(); ++i) {
        const QDomNode n = data.at(i);
        const QDomElement e = n.toElement();

        if (e.tagName() == QLatin1String("id")) {
            group->setId(e.text());
            continue;
        }

        if (e.tagName() == QLatin1String("updated")) {
            group->setUpdated(QDateTime::fromString(e.text(), Qt::ISODate));
            continue;
        }

        if ((e.tagName() == QLatin1String("title")) ||
            (e.tagName() == QLatin1String("atom:title"))) {
            group->setTitle(e.text());
            continue;
        }

        if ((e.tagName() == QLatin1String("content")) ||
            (e.tagName() == QLatin1String("atom:content"))) {
            group->setContent(e.text());
            continue;
        }

        if (e.tagName() == QLatin1String("gContact:systemGroup")) {
            group->setIsSystemGroup(true);
            continue;
        }
    }

    return group;
}

} // namespace ContactsService

} // namespace KGAPI2
