/*
    SPDX-FileCopyrightText: 2015-2017 Krzysztof Nowicki <krissn@op.pl>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "ewspropertyfield.h"

#include <QString>

#include "ewsclient_debug.h"

static constexpr auto distinguishedPropSetIdNames = std::to_array({
    QLatin1StringView("Meeting"),
    QLatin1StringView("Appointment"),
    QLatin1StringView("Common"),
    QLatin1StringView("PublicStrings"),
    QLatin1StringView("Address"),
    QLatin1StringView("InternetHeaders"),
    QLatin1StringView("CalendarAssistant"),
    QLatin1StringView("UnifiedMessaging"),
});

static constexpr auto propertyTypeNames = std::to_array({
    QLatin1StringView("ApplicationTime"),
    QLatin1StringView("ApplicationTimeArray"),
    QLatin1StringView("Binary"),
    QLatin1StringView("BinaryArray"),
    QLatin1StringView("Boolean"),
    QLatin1StringView("CLSID"),
    QLatin1StringView("CLSIDArray"),
    QLatin1StringView("Currency"),
    QLatin1StringView("CurrencyArray"),
    QLatin1StringView("Double"),
    QLatin1StringView("DoubleArray"),
    QLatin1StringView("Error"),
    QLatin1StringView("Float"),
    QLatin1StringView("FloatArray"),
    QLatin1StringView("Integer"),
    QLatin1StringView("IntegerArray"),
    QLatin1StringView("Long"),
    QLatin1StringView("LongArray"),
    QLatin1StringView("Null"),
    QLatin1StringView("Object"),
    QLatin1StringView("ObjectArray"),
    QLatin1StringView("Short"),
    QLatin1StringView("ShortArray"),
    QLatin1StringView("SystemTime"),
    QLatin1StringView("SystemTimeArray"),
    QLatin1StringView("String"),
    QLatin1StringView("StringArray"),
});

class EwsPropertyFieldPrivate : public QSharedData
{
public:
    EwsPropertyFieldPrivate()
        : mPropType(EwsPropertyField::UnknownField)
        , mIndex(0)
        , mPsIdType(DistinguishedPropSet)
        , mPsDid(EwsPropSetMeeting)
        , mIdType(PropName)
        , mId(0)
        , mHasTag(false)
        , mTag(0)
        , mType(EwsPropTypeNull)
        , mHash(0)
    {
    }

    enum PropSetIdType {
        DistinguishedPropSet,
        RealPropSet,
    };

    enum PropIdType {
        PropName,
        PropId,
    };

    EwsPropertyField::Type mPropType;

    QString mUri;
    unsigned mIndex;

    PropSetIdType mPsIdType;
    EwsDistinguishedPropSetId mPsDid;
    QString mPsId;

    PropIdType mIdType;
    unsigned mId;
    QString mName;

    bool mHasTag;
    unsigned mTag;

    EwsPropertyType mType;

    uint mHash; // Precalculated hash for the qHash() function.

    void recalcHash();
};

void EwsPropertyFieldPrivate::recalcHash()
{
    mHash = 0;
    switch (mPropType) {
    case EwsPropertyField::Field:
        mHash = 0x00000000 | (qHash(mUri) & 0x3FFFFFFF);
        break;
    case EwsPropertyField::IndexedField:
        mHash = 0x80000000 | ((qHash(mUri) ^ mIndex) & 0x3FFFFFFF);
        break;
    case EwsPropertyField::ExtendedField:
        if (mHasTag) {
            mHash = 0x40000000 | mTag;
        } else {
            if (mPsIdType == DistinguishedPropSet) {
                mHash |= mPsDid << 16;
            } else {
                mHash |= (qHash(mPsId) & 0x1FFF) << 16;
            }

            if (mIdType == PropId) {
                mHash |= mId & 0xFFFF;
            } else {
                mHash |= (qHash(mName) & 0xFFFF);
            }
            mHash |= 0xC0000000;
        }
        break;
    default:
        break;
    }
}

EwsPropertyField::EwsPropertyField()
    : d(new EwsPropertyFieldPrivate())
{
}

EwsPropertyField::EwsPropertyField(const QString &uri)
    : d(new EwsPropertyFieldPrivate())
{
    d->mPropType = Field;
    d->mUri = uri;
    d->recalcHash();
}

EwsPropertyField::EwsPropertyField(const QString &uri, unsigned index)
    : d(new EwsPropertyFieldPrivate())
{
    d->mPropType = IndexedField;
    d->mUri = uri;
    d->mIndex = index;
    d->recalcHash();
}

EwsPropertyField::EwsPropertyField(EwsDistinguishedPropSetId psid, unsigned id, EwsPropertyType type)
    : d(new EwsPropertyFieldPrivate())
{
    d->mPropType = ExtendedField;

    d->mPsIdType = EwsPropertyFieldPrivate::DistinguishedPropSet;
    d->mPsDid = psid;

    d->mIdType = EwsPropertyFieldPrivate::PropId;
    d->mId = id;

    d->mType = type;
    d->recalcHash();
}

EwsPropertyField::EwsPropertyField(EwsDistinguishedPropSetId psid, const QString &name, EwsPropertyType type)
    : d(new EwsPropertyFieldPrivate())
{
    d->mPropType = ExtendedField;

    d->mPsIdType = EwsPropertyFieldPrivate::DistinguishedPropSet;
    d->mPsDid = psid;

    d->mIdType = EwsPropertyFieldPrivate::PropName;
    d->mName = name;

    d->mType = type;
    d->recalcHash();
}

EwsPropertyField::EwsPropertyField(const QString &psid, unsigned id, EwsPropertyType type)
    : d(new EwsPropertyFieldPrivate())
{
    d->mPropType = ExtendedField;

    d->mPsIdType = EwsPropertyFieldPrivate::RealPropSet;
    d->mPsId = psid;

    d->mIdType = EwsPropertyFieldPrivate::PropId;
    d->mId = id;

    d->mType = type;
    d->recalcHash();
}

EwsPropertyField::EwsPropertyField(const QString &psid, const QString &name, EwsPropertyType type)
    : d(new EwsPropertyFieldPrivate())
{
    d->mPropType = ExtendedField;

    d->mPsIdType = EwsPropertyFieldPrivate::RealPropSet;
    d->mPsId = psid;

    d->mIdType = EwsPropertyFieldPrivate::PropName;
    d->mName = name;

    d->mType = type;
    d->recalcHash();
}

EwsPropertyField::EwsPropertyField(unsigned tag, EwsPropertyType type)
    : d(new EwsPropertyFieldPrivate())
{
    d->mPropType = ExtendedField;

    d->mHasTag = true;
    d->mTag = tag;

    d->mType = type;
    d->recalcHash();
}

EwsPropertyField::EwsPropertyField(const EwsPropertyField &other)
    : d(other.d)
{
}

EwsPropertyField::EwsPropertyField(EwsPropertyField &&other)
    : d(other.d)
{
}

EwsPropertyField &EwsPropertyField::operator=(EwsPropertyField &&other)
{
    d = other.d;
    return *this;
}

EwsPropertyField::~EwsPropertyField()
{
}

EwsPropertyField &EwsPropertyField::operator=(const EwsPropertyField &other)
{
    d = other.d;
    return *this;
}

bool EwsPropertyField::operator==(const EwsPropertyField &other) const
{
    if (d == other.d) {
        return true;
    }

    const EwsPropertyFieldPrivate *od = other.d;

    if (d->mPropType != od->mPropType) {
        return false;
    }

    switch (d->mPropType) {
    case UnknownField:
        return true;
    case Field:
        return d->mUri == od->mUri;
    case IndexedField:
        return (d->mUri == od->mUri) && (d->mIndex == od->mIndex);
    case ExtendedField:
        if (d->mType != od->mType) {
            return false;
        }

        if (d->mHasTag != od->mHasTag) {
            return false;
        } else if (d->mHasTag) {
            return d->mTag == od->mTag;
        }

        if (d->mPsIdType != od->mPsIdType) {
            return false;
        } else if ((d->mPsIdType == EwsPropertyFieldPrivate::DistinguishedPropSet) && (d->mPsDid != od->mPsDid)) {
            return false;
        } else if ((d->mPsIdType == EwsPropertyFieldPrivate::RealPropSet) && (d->mPsId != od->mPsId)) {
            return false;
        }

        if (d->mIdType != od->mIdType) {
            return false;
        } else if ((d->mIdType == EwsPropertyFieldPrivate::PropId) && (d->mId != od->mId)) {
            return false;
        } else if ((d->mIdType == EwsPropertyFieldPrivate::PropName) && (d->mName != od->mName)) {
            return false;
        }
        return true;
    default:
        return false;
    }

    // Shouldn't get here.
    return false;
}

void EwsPropertyField::write(QXmlStreamWriter &writer) const
{
    switch (d->mPropType) {
    case Field:
        writer.writeStartElement(ewsTypeNsUri, QStringLiteral("FieldURI"));
        writer.writeAttribute(QStringLiteral("FieldURI"), d->mUri);
        writer.writeEndElement();
        break;
    case IndexedField: {
        writer.writeStartElement(ewsTypeNsUri, QStringLiteral("IndexedFieldURI"));
        writer.writeAttribute(QStringLiteral("FieldURI"), d->mUri);
        QStringList tokens = d->mUri.split(QLatin1Char(':'));
        writer.writeAttribute(QStringLiteral("FieldIndex"), tokens[1] + QString::number(d->mIndex));
        writer.writeEndElement();
        break;
    }
    case ExtendedField:
        writer.writeStartElement(ewsTypeNsUri, QStringLiteral("ExtendedFieldURI"));
        if (d->mHasTag) {
            writer.writeAttribute(QStringLiteral("PropertyTag"), QStringLiteral("0x") + QString::number(d->mTag, 16));
        } else {
            if (d->mPsIdType == EwsPropertyFieldPrivate::DistinguishedPropSet) {
                writer.writeAttribute(QStringLiteral("DistinguishedPropertySetId"), distinguishedPropSetIdNames[d->mPsDid]);
            } else {
                writer.writeAttribute(QStringLiteral("PropertySetId"), d->mPsId);
            }

            if (d->mIdType == EwsPropertyFieldPrivate::PropId) {
                writer.writeAttribute(QStringLiteral("PropertyId"), QString::number(d->mId));
            } else {
                writer.writeAttribute(QStringLiteral("PropertyName"), d->mName);
            }
        }
        writer.writeAttribute(QStringLiteral("PropertyType"), propertyTypeNames[d->mType]);
        writer.writeEndElement();
        break;
    case UnknownField:
        break;
    }
}

bool EwsPropertyField::read(QXmlStreamReader &reader)
{
    if (reader.namespaceUri() != ewsTypeNsUri) {
        qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - invalid namespace.");
        return false;
    }

    QXmlStreamAttributes attrs = reader.attributes();
    bool ok;

    // First check the property type
    if (reader.name() == QLatin1String("FieldURI")) {
        if (!attrs.hasAttribute(QStringLiteral("FieldURI"))) {
            qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing %1 attribute.").arg(QStringLiteral("FieldURI"));
            return false;
        }
        d->mPropType = Field;
        d->mUri = attrs.value(QStringLiteral("FieldURI")).toString();
    } else if (reader.name() == QLatin1String("IndexedFieldURI")) {
        if (!attrs.hasAttribute(QStringLiteral("FieldURI"))) {
            qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing %1 attribute.").arg(QStringLiteral("FieldURI"));
            return false;
        }
        if (!attrs.hasAttribute(QStringLiteral("FieldIndex"))) {
            qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing %1 attribute.").arg(QStringLiteral("FieldIndex"));
            return false;
        }
        QString uri = attrs.value(QStringLiteral("FieldURI")).toString();
        QStringList tokens = uri.split(QLatin1Char(':'));
        QString indexStr = attrs.value(QStringLiteral("FieldIndex")).toString();
        if (!indexStr.startsWith(tokens[1])) {
            qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - malformed %1 attribute.").arg(QStringLiteral("FieldIndex"));
            return false;
        }
        unsigned index = QStringView(indexStr).mid(tokens[1].size()).toUInt(&ok, 0);
        if (!ok) {
            qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - error reading %1 attribute.").arg(QStringLiteral("FieldIndex"));
            return false;
        }
        d->mPropType = IndexedField;
        d->mUri = uri;
        d->mIndex = index;
    } else if (reader.name() == QLatin1String("ExtendedFieldURI")) {
        if (!attrs.hasAttribute(QStringLiteral("PropertyType"))) {
            qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing %1 attribute.").arg(QStringLiteral("PropertyType"));
            return false;
        }
        auto propTypeText = attrs.value(QStringLiteral("PropertyType"));
        unsigned i;
        EwsPropertyType propType;
        for (i = 0; i < sizeof(propertyTypeNames) / sizeof(propertyTypeNames[0]); ++i) {
            if (propTypeText == propertyTypeNames[i]) {
                propType = static_cast<EwsPropertyType>(i);
                break;
            }
        }
        if (i == sizeof(propertyTypeNames) / sizeof(propertyTypeNames[0])) {
            qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - error reading %1 attribute.").arg(QStringLiteral("PropertyType"));
            return false;
        }

        if (attrs.hasAttribute(QStringLiteral("PropertyTag"))) {
            unsigned tag = attrs.value(QStringLiteral("PropertyTag")).toUInt(&ok, 0);
            if (!ok) {
                qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - error reading %1 attribute.").arg(QStringLiteral("PropertyTag"));
                return false;
            }
            d->mHasTag = true;
            d->mTag = tag;
        } else {
            EwsPropertyFieldPrivate::PropSetIdType psIdType = EwsPropertyFieldPrivate::DistinguishedPropSet;
            EwsDistinguishedPropSetId psDid = EwsPropSetMeeting;
            QString psId;

            EwsPropertyFieldPrivate::PropIdType idType = EwsPropertyFieldPrivate::PropName;
            unsigned id = 0;
            QString name;
            if (attrs.hasAttribute(QStringLiteral("PropertyId"))) {
                id = attrs.value(QStringLiteral("PropertyId")).toUInt(&ok, 0);
                if (!ok) {
                    qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - error reading %1 attribute.").arg(QStringLiteral("PropertyId"));
                    return false;
                }
                idType = EwsPropertyFieldPrivate::PropId;
            } else if (attrs.hasAttribute(QStringLiteral("PropertyName"))) {
                name = attrs.value(QStringLiteral("PropertyName")).toString();
                idType = EwsPropertyFieldPrivate::PropName;
            } else {
                qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing one of %1 or %2 attributes.")
                                               .arg(QStringLiteral("PropertyId").arg(QStringLiteral("PropertyName")));
                return false;
            }

            if (attrs.hasAttribute(QStringLiteral("DistinguishedPropertySetId"))) {
                auto didText = attrs.value(QStringLiteral("DistinguishedPropertySetId"));
                unsigned i;
                for (i = 0; i < sizeof(distinguishedPropSetIdNames) / sizeof(distinguishedPropSetIdNames[0]); ++i) {
                    if (didText == distinguishedPropSetIdNames[i]) {
                        psDid = static_cast<EwsDistinguishedPropSetId>(i);
                        break;
                    }
                }
                if (i == sizeof(distinguishedPropSetIdNames) / sizeof(distinguishedPropSetIdNames[0])) {
                    qCWarningNC(EWSCLI_LOG)
                        << QStringLiteral("Error reading property field - error reading %1 attribute.").arg(QStringLiteral("DistinguishedPropertySetId"));
                    return false;
                }
                psIdType = EwsPropertyFieldPrivate::DistinguishedPropSet;
            } else if (attrs.hasAttribute(QStringLiteral("PropertySetId"))) {
                psId = attrs.value(QStringLiteral("PropertySetId")).toString();
                psIdType = EwsPropertyFieldPrivate::RealPropSet;
            } else {
                qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing one of %1 or %2 attributes.")
                                               .arg(QStringLiteral("DistinguishedPropertySetId").arg(QStringLiteral("PropertySetId")));
                return false;
            }
            d->mPsIdType = psIdType;
            d->mPsDid = psDid;
            d->mPsId = psId;
            d->mIdType = idType;
            d->mId = id;
            d->mName = name;
        }

        d->mType = propType;
        d->mPropType = ExtendedField;
    }
    d->recalcHash();
    return true;
}

uint qHash(const EwsPropertyField &prop, uint seed)
{
    return prop.d->mHasTag ^ seed;
}

QDebug operator<<(QDebug debug, const EwsPropertyField &prop)
{
    QDebugStateSaver saver(debug);
    QDebug d = debug.nospace().noquote();
    d << QStringLiteral("EwsPropertyField(");

    switch (prop.d->mPropType) {
    case EwsPropertyField::Field:
        d << QStringLiteral("FieldUri: ") << prop.d->mUri;
        break;
    case EwsPropertyField::IndexedField:
        d << QStringLiteral("IndexedFieldUri: ") << prop.d->mUri << '@' << prop.d->mIndex;
        break;
    case EwsPropertyField::ExtendedField:
        d << QStringLiteral("ExtendedFieldUri: ");
        if (prop.d->mHasTag) {
            d << QStringLiteral("tag: 0x") << QString::number(prop.d->mTag, 16);
        } else {
            if (prop.d->mPsIdType == EwsPropertyFieldPrivate::DistinguishedPropSet) {
                d << QStringLiteral("psdid: ") << distinguishedPropSetIdNames[prop.d->mPsDid];
            } else {
                d << QStringLiteral("psid: ") << prop.d->mPsId;
            }
            d << QStringLiteral(", ");

            if (prop.d->mIdType == EwsPropertyFieldPrivate::PropId) {
                d << QStringLiteral("id: 0x") << QString::number(prop.d->mId, 16);
            } else {
                d << QStringLiteral("name: ") << prop.d->mName;
            }
        }
        d << QStringLiteral(", type: ") << propertyTypeNames[prop.d->mType];
        break;
    case EwsPropertyField::UnknownField:
        d << QStringLiteral("Unknown");
        break;
    }
    d << ')';
    return debug;
}

bool EwsPropertyField::writeWithValue(QXmlStreamWriter &writer, const QVariant &value) const
{
    switch (d->mPropType) {
    case Field: {
        QStringList tokens = d->mUri.split(QLatin1Char(':'));
        if (tokens.size() != 2) {
            qCWarningNC(EWSCLI_LOG) << QStringLiteral("Invalid field URI: %1").arg(d->mUri);
            return false;
        }
        writer.writeStartElement(ewsTypeNsUri, tokens[1]);
        writeValue(writer, value);
        writer.writeEndElement();
        break;
    }
    case IndexedField: {
        QStringList tokens = d->mUri.split(QLatin1Char(':'));
        if (tokens.size() != 2) {
            qCWarningNC(EWSCLI_LOG) << QStringLiteral("Invalid field URI: %1").arg(d->mUri);
            return false;
        }
        writer.writeStartElement(ewsTypeNsUri, tokens[1] + QStringLiteral("es"));
        writer.writeStartElement(ewsTypeNsUri, QStringLiteral("Entry"));
        writer.writeAttribute(QStringLiteral("Key"), tokens[1] + QString::number(d->mIndex));
        writeValue(writer, value);
        writer.writeEndElement();
        writer.writeEndElement();
        break;
    }
    case ExtendedField:
        writer.writeStartElement(ewsTypeNsUri, QStringLiteral("ExtendedProperty"));
        write(writer);
        writeExtendedValue(writer, value);
        writer.writeEndElement();
        break;
    default:
        return false;
    }

    return true;
}

void EwsPropertyField::writeValue(QXmlStreamWriter &writer, const QVariant &value) const
{
    switch (value.userType()) {
    case QMetaType::QStringList: {
        const QStringList list = value.toStringList();
        for (const QString &str : list) {
            writer.writeTextElement(ewsTypeNsUri, QStringLiteral("String"), str);
        }
        break;
    }
    case QMetaType::QString:
        writer.writeCharacters(value.toString());
        break;
    default:
        qCWarning(EWSCLI_LOG) << QStringLiteral("Unknown variant type to write: %1").arg(QString::fromLatin1(value.typeName()));
    }
}

void EwsPropertyField::writeExtendedValue(QXmlStreamWriter &writer, const QVariant &value) const
{
    switch (value.userType()) {
    case QMetaType::QStringList: {
        const QStringList list = value.toStringList();
        writer.writeStartElement(ewsTypeNsUri, QStringLiteral("Values"));
        for (const QString &str : list) {
            writer.writeTextElement(ewsTypeNsUri, QStringLiteral("Value"), str);
        }
        writer.writeEndElement();
        break;
    }
    case QMetaType::QString:
        writer.writeStartElement(ewsTypeNsUri, QStringLiteral("Value"));
        writer.writeCharacters(value.toString());
        writer.writeEndElement();
        break;
    default:
        qCWarning(EWSCLI_LOG) << QStringLiteral("Unknown variant type to write: %1").arg(QString::fromLatin1(value.typeName()));
    }
}

EwsPropertyField::Type EwsPropertyField::type() const
{
    return d->mPropType;
}

QString EwsPropertyField::uri() const
{
    if (d->mPropType == Field || d->mPropType == IndexedField) {
        return d->mUri;
    } else {
        return QString();
    }
}
