/**
 * Original work: Copyright (c) 2014 Sergey Skoblikov
 * Modified work: Copyright (c) 2015-2019 Dmitry Ivanov
 *
 * This file is a part of QEverCloud project and is distributed under the terms
 * of MIT license:
 * https://opensource.org/licenses/MIT
 */

#ifndef QEVERCLOUD_THRIFT_H
#define QEVERCLOUD_THRIFT_H

#include <Exceptions.h>
#include <Helpers.h>

#include <QByteArray>
#include <QtEndian>

#include <cstring>
#include <limits>

namespace qevercloud {

////////////////////////////////////////////////////////////////////////////////

// Use this to get around strict aliasing rules.
// For example, uint64_t i = bitwise_cast<uint64_t>(returns_double());
// The most obvious implementation is to just cast a pointer,
// but that doesn't work.

// http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
// http://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule
// http://stackoverflow.com/questions/2906365/gcc-strict-aliasing-and-casting-through-a-union

// taken from thrift sources
template <typename To, typename From>
static inline To bitwise_cast(From from)
{
    Q_STATIC_ASSERT_X(sizeof(From) == sizeof(To), "bitwise_cast: incompatible types");

    // BAD!!!  These are all broken with -O2.
    //return *reinterpret_cast<To*>(&from);  // BAD!!!
    //return *static_cast<To*>(static_cast<void*>(&from));  // BAD!!!
    //return *(To*)(void*)&from;  // BAD!!!

    // Super clean and partially blessed by section 3.9 of the standard.
    //unsigned char c[sizeof(from)];
    //memcpy(c, &from, sizeof(from));
    //To to;
    //memcpy(&to, c, sizeof(c));
    //return to;

    // Slightly more questionable.
    // Same code emitted by GCC.
    //To to;
    //memcpy(&to, &from, sizeof(from));
    //return to;

    // Technically undefined, but almost universally supported,
    // and the most efficient implementation.
    union {
        From f;
        To t;
    } u;
    u.f = from;
    return u.t;
}

////////////////////////////////////////////////////////////////////////////////

enum class ThriftFieldType
{
    T_STOP       = 0,
    T_VOID       = 1,
    T_BOOL       = 2,
    T_BYTE       = 3,
    T_I08        = 3,
    T_I16        = 6,
    T_I32        = 8,
    T_U64        = 9,
    T_I64        = 10,
    T_DOUBLE     = 4,
    T_STRING     = 11,
    T_UTF7       = 11,
    T_STRUCT     = 12,
    T_MAP        = 13,
    T_SET        = 14,
    T_LIST       = 15,
    T_UTF8       = 16,
    T_UTF16      = 17
};

////////////////////////////////////////////////////////////////////////////////

enum class ThriftMessageType
{
    T_CALL       = 1,
    T_REPLY      = 2,
    T_EXCEPTION  = 3,
    T_ONEWAY     = 4
};

////////////////////////////////////////////////////////////////////////////////

static const qint32 VERSION_1 = ((qint32)0x80010000);
static const qint32 VERSION_MASK = ((qint32)0xffff0000);

////////////////////////////////////////////////////////////////////////////////

class ThriftBinaryBufferWriter
{
private:
    QByteArray m_buf;
    bool m_strict;

    void write(const quint8* bytes, const int bytesCount)
    {
        m_buf.append(reinterpret_cast<const char*>(bytes), bytesCount);
    }

public:
    ThriftBinaryBufferWriter() :
        m_strict(true)
    {}

    void setStrictMode(bool on) { m_strict = on; }

    QByteArray buffer() { return m_buf; }

    quint32 writeMessageBegin(
        QString name, const ThriftMessageType messageType,
        const qint32 seqid)
    {
        if (m_strict)
        {
            qint32 version = (VERSION_1) | ((qint32)messageType);
            quint32 wsize = 0;
            wsize += writeI32(version);
            wsize += writeString(name);
            wsize += writeI32(seqid);
            return wsize;
        }
        else
        {
            quint32 wsize = 0;
            wsize += writeString(name);
            wsize += writeByte((qint8)messageType);
            wsize += writeI32(seqid);
            return wsize;
        }
    }

    quint32 writeMessageEnd() { return 0; }

    inline quint32 writeStructBegin(QString name)
    {
        Q_UNUSED(name)
        return 0;
    }

    inline quint32 writeStructEnd() { return 0; }

    inline quint32 writeFieldBegin(
        QString name, const ThriftFieldType fieldType,
        const qint16 fieldId)
    {
        Q_UNUSED(name);
        quint32 wsize = 0;
        wsize += writeByte(static_cast<qint8>(fieldType));
        wsize += writeI16(fieldId);
        return wsize;
    }

    inline quint32 writeFieldEnd() { return 0; }

    inline quint32 writeFieldStop()
    {
        return writeByte((qint8)ThriftFieldType::T_STOP);
    }

    inline quint32 writeMapBegin(
        const ThriftFieldType keyType, const ThriftFieldType valType,
        const qint32 size)
    {
        quint32 wsize = 0;
        wsize += writeByte(static_cast<qint8>(keyType));
        wsize += writeByte(static_cast<qint8>(valType));
        wsize += writeI32(size);
        return wsize;
    }

    inline quint32 writeMapEnd() { return 0; }

    inline quint32 writeListBegin(
        const ThriftFieldType elemType, const qint32 size)
    {
        quint32 wsize = 0;
        wsize += writeByte(static_cast<qint8>(elemType));
        wsize += writeI32(size);
        return wsize;
    }

    inline quint32 writeListEnd() { return 0; }

    inline quint32 writeSetBegin(
        const ThriftFieldType elemType, const qint32 size)
    {
        return writeListBegin(elemType, size);
    }

    inline quint32 writeSetEnd() { return 0; }

    inline quint32 writeBool(bool value)
    {
        quint8 tmp = value ? 1 : 0;
        write(&tmp, 1);
        return 1;
    }

    inline quint32 writeByte(const qint8 byte)
    {
        write(reinterpret_cast<const quint8*>(&byte), 1);
        return 1;
    }

    inline quint32 writeI16(const qint16 i16)
    {
        qint16 net;
        qToBigEndian(i16, reinterpret_cast<uchar*>(&net));
        write(reinterpret_cast<const quint8*>(&net), 2);
        return 2;
    }

    inline quint32 writeI32(const qint32 i32)
    {
        qint32 net;
        qToBigEndian(i32, reinterpret_cast<uchar*>(&net));
        write(reinterpret_cast<const quint8*>(&net), 4);
        return 4;
    }

    inline quint32 writeI64(const qint64 i64)
    {
        qint64 net;
        qToBigEndian(i64, reinterpret_cast<uchar*>(&net));
        write(reinterpret_cast<const quint8*>(&net), 8);
        return 8;
    }

    inline quint32 writeDouble(const double dub)
    {
        Q_STATIC_ASSERT_X(
            sizeof(double) == sizeof(qint64) &&
            std::numeric_limits<double>::is_iec559,
            "incompatible double type");

        quint64 bits = bitwise_cast<quint64>(dub);
        qToBigEndian(bits, reinterpret_cast<uchar*>(&bits));
        write(reinterpret_cast<quint8*>(&bits), 8);
        return 8;
    }

    inline quint32 writeString(const QString & str)
    {
        QByteArray utf8 = str.toUtf8();
        return writeBinary(utf8);
    }

    inline quint32 writeBinary(const QByteArray & bytes)
    {
        qint32 size = static_cast<qint32>(bytes.length());
        if (size > std::numeric_limits<qint32>::max()) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("The data is too big"));
        }

        quint32 result = writeI32(static_cast<qint32>(size));
        if (size > 0) {
            write((const quint8*)bytes.constData(), size);
        }

        return result + static_cast<quint32>(size);
    }
};

////////////////////////////////////////////////////////////////////////////////

class ThriftBinaryBufferReader
{
private:
    QByteArray m_buf;
    qint32 m_pos;
    qint32 m_stringLimit;
    bool m_strict;

    void read(quint8 * dest, qint32 bytesCount)
    {
        if (Q_UNLIKELY((m_pos + bytesCount) > m_buf.length())) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("Unexpected end of data"));
        }

        if (Q_UNLIKELY(bytesCount < 0)) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("Negative bytes count"));
        }

        std::memcpy(
            dest,
            m_buf.mid(m_pos, bytesCount).constData(),
            static_cast<std::size_t>(bytesCount));

        m_pos += bytesCount;
    }

public:
    ThriftBinaryBufferReader(QByteArray buffer) :
        m_buf(buffer),
        m_pos(0),
        m_stringLimit(0),m_strict(false)
    {}

    void setStringLimit(qint32 limit) { m_stringLimit = limit; }
    void setStrictMode(bool on) { m_strict = on; }

    quint32 readMessageBegin(
        QString & name, ThriftMessageType & messageType, qint32 & seqid)
    {
        quint32 result = 0;
        qint32 sz;
        result += readI32(sz);

        if (sz < 0)
        {
            // Check for correct version number
            qint32 version = sz & VERSION_MASK;
            if (version != VERSION_1) {
                throw ThriftException(
                    ThriftException::Type::PROTOCOL_ERROR,
                    QStringLiteral("Bad version identifier"));
            }

            messageType = static_cast<ThriftMessageType>(sz & 0x000000ff);
            result += readString(name);
            result += readI32(seqid);
        }
        else
        {
            if (m_strict) {
                throw ThriftException(
                    ThriftException::Type::PROTOCOL_ERROR,
                    QStringLiteral(
                        "No version identifier... old protocol client in "
                        "strict mode?"));
            }
            else {
                // Handle pre-versioned input
                qint8 type;
                m_pos -= 4;
                result += readString(name);
                result += readByte(type);
                messageType = static_cast<ThriftMessageType>(type);
                result += readI32(seqid);
            }
        }

        return result;
    }

    quint32 readMessageEnd() { return 0; }

    inline quint32 readStructBegin(QString & name) { name.clear(); return 0; }
    inline quint32 readStructEnd() { return 0; }

    inline quint32 readFieldBegin(
        QString & name, ThriftFieldType & fieldType, qint16 & fieldId)
    {
        Q_UNUSED(name)
        quint32 result = 0;
        qint8 type;
        result += readByte(type);

        fieldType = static_cast<ThriftFieldType>(type);
        if (fieldType == ThriftFieldType::T_STOP) {
            fieldId = 0;
            return result;
        }

        result += readI16(fieldId);
        return result;
    }

    inline quint32 readFieldEnd() { return 0; }

    inline quint32 readMapBegin(
        ThriftFieldType & keyType, ThriftFieldType & valType, qint32 & size)
    {
        qint8 k, v;
        quint32 result = 0;
        qint32 sizei;
        result += readByte(k);
        keyType = static_cast<ThriftFieldType>(k);
        result += readByte(v);
        valType = static_cast<ThriftFieldType>(v);
        result += readI32(sizei);
        if (sizei < 0) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("Negative size!"));
        }
        else if (m_stringLimit > 0 && sizei > m_stringLimit) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("The size limit is exceeded."));
        }
        size = sizei;
        return result;
    }

    inline quint32 readMapEnd() {return 0;}

    inline quint32 readListBegin(ThriftFieldType & elemType, qint32 & size)
    {
        qint8 e;
        quint32 result = 0;
        qint32 sizei;
        result += readByte(e);
        elemType = static_cast<ThriftFieldType>(e);
        result += readI32(sizei);
        if (sizei < 0) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("Negative size!"));
        }
        else if (m_stringLimit > 0 && sizei > m_stringLimit) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("The size limit is exceeded."));
        }
        size = sizei;
        return result;
    }

    inline quint32 readListEnd() { return 0; }

    inline quint32 readSetBegin(ThriftFieldType & elemType, qint32 & size)
    {
        return readListBegin(elemType, size);
    }

    inline quint32 readSetEnd() { return 0; }

    inline quint32 readBool(bool & value)
    {
        quint8 b[1];
        read(b, 1);
        value = *(qint8*)b != 0;
        return 1;
    }

    inline quint32 readByte(qint8 & byte)
    {
        quint8 b[1];
        read(b, 1);
        byte = *(qint8*)b;
        return 1;
    }

    inline quint32 readI16(qint16 & i16)
    {
        union bytes {
            quint8 b[2];
            qint16 all;
        } theBytes;

        read(theBytes.b, 2);
        i16 = qFromBigEndian<qint16>(theBytes.all);
        return 2;
    }

    inline quint32 readI32(qint32 & i32)
    {
        union bytes {
            quint8 b[4];
            qint32 all;
        } theBytes;

        read(theBytes.b, 4);
        i32 = qFromBigEndian<qint32>(theBytes.all);
        return 4;
    }

    inline quint32 readI64(qint64 & i64)
    {
        union bytes {
            quint8 b[8];
            qint64 all;
        } theBytes;

        read(theBytes.b, 8);
        i64 = qFromBigEndian<qint64>(theBytes.all);
        return 8;
    }

    inline quint32 readDouble(double & dub)
    {
        Q_STATIC_ASSERT_X(
            sizeof(double) == sizeof(qint64) &&
            std::numeric_limits<double>::is_iec559,
            "incompatible double type");

        union bytes {
            quint8 b[8];
            qint64 all;
        } theBytes;

        read(theBytes.b, 8);
        theBytes.all = qFromBigEndian<qint64>(theBytes.all);
        dub = bitwise_cast<double>(theBytes.all);
        return 8;
    }

    inline quint32 readString(QString & str)
    {
        quint32 result;
        qint32 size;
        result = readI32(size);

        if (size < 0) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("Negative size!"));
        }

        if (m_stringLimit > 0 && size > m_stringLimit) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("The size limit is exceeded."));
        }

        // Catch empty string case
        if (size == 0) {
            str.clear();
            return result;
        }

        if ((m_pos + size) > m_buf.length()) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("Unexpected end of data"));
        }

        str = QString::fromUtf8(m_buf.constData() + m_pos, size);
        m_pos += size;
        result += static_cast<quint32>(size);

        return result;
    }

    inline quint32 readBinary(QByteArray& str)
    {
        quint32 result;
        qint32 size;
        result = readI32(size);

        if (size < 0) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("Negative size!"));
        }

        if (m_stringLimit > 0 && size > m_stringLimit) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("The size limit is exceeded."));
        }

        // Catch empty string case
        if (size == 0) {
            str.clear();
            return result;
        }

        if ((m_pos + size) > m_buf.length()) {
            throw ThriftException(
                ThriftException::Type::PROTOCOL_ERROR,
                QStringLiteral("Unexpected end of data"));
        }

        str = m_buf.mid(m_pos, size);
        m_pos += size;
        result += static_cast<quint32>(size);

        return result;
    }

    inline quint32 skip(ThriftFieldType type)
    {
        switch (type) {
        case ThriftFieldType::T_BOOL:
        {
            bool boolv;
            return readBool(boolv);
        }
        case ThriftFieldType::T_BYTE:
        {
            qint8 bytev;
            return readByte(bytev);
        }
        case ThriftFieldType::T_I16:
        {
            qint16 i16;
            return readI16(i16);
        }
        case ThriftFieldType::T_I32:
        {
            qint32 i32;
            return readI32(i32);
        }
        case ThriftFieldType::T_I64:
        {
            qint64 i64;
            return readI64(i64);
        }
        case ThriftFieldType::T_DOUBLE:
        {
            double dub;
            return readDouble(dub);
        }
        case ThriftFieldType::T_STRING:
        {
            QByteArray str;
            return readBinary(str);
        }
        case ThriftFieldType::T_STRUCT:
        {
            quint32 result = 0;
            QString name;
            qint16 fid;
            ThriftFieldType ftype;
            result += readStructBegin(name);
            while (true)
            {
                result += readFieldBegin(name, ftype, fid);
                if (ftype == ThriftFieldType::T_STOP) {
                    break;
                }

                result += skip(ftype);
                result += readFieldEnd();
            }
            result += readStructEnd();
            return result;
        }
        case ThriftFieldType::T_MAP:
        {
            quint32 result = 0;
            ThriftFieldType keyType;
            ThriftFieldType valType;
            qint32 i, size;
            result += readMapBegin(keyType, valType, size);
            for(i = 0; i < size; i++) {
                result += skip(keyType);
                result += skip(valType);
            }
            result += readMapEnd();
            return result;
        }
        case ThriftFieldType::T_SET:
        {
            quint32 result = 0;
            ThriftFieldType elemType;
            qint32 i, size;
            result += readSetBegin(elemType, size);
            for(i = 0; i < size; i++) {
                result += skip(elemType);
            }
            result += readSetEnd();
            return result;
        }
        case ThriftFieldType::T_LIST:
        {
            quint32 result = 0;
            ThriftFieldType elemType;
            qint32 i, size;
            result += readListBegin(elemType, size);
            for(i = 0; i < size; i++) {
                result += skip(elemType);
            }
            result += readListEnd();
            return result;
        }
        case ThriftFieldType::T_STOP:
        case ThriftFieldType::T_VOID:
        case ThriftFieldType::T_U64:
        case ThriftFieldType::T_UTF8:
        case ThriftFieldType::T_UTF16:
            break;
        }
        return 0;
    }
};

} // namespace qevercloud

#endif // QEVERCLOUD_THRIFT_H
