#ifndef VARINT_INL_H_
#error "Direct inclusion of this file is not allowed, include varint.h"
// For the sake of sane code completion.
#include "varint.h"
#endif

#include "zig_zag.h"

#include <library/cpp/yt/exception/exception.h>

#include <array>

namespace NYT {

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

inline int WriteVarUint64(char* output, ui64 value)
{
    output[0] = static_cast<ui8>(value);
    if (Y_LIKELY(value < 0x80)) {
        return 1;
    }
    output[0] |= 0x80;
    value >>= 7;
    output[1] = static_cast<ui8>(value);
    if (Y_LIKELY(value < 0x80)) {
        return 2;
    }
    int count = 2;
    do {
        output[count - 1] |= 0x80;
        value >>= 7;
        output[count] = static_cast<ui8>(value);
        ++count;
    } while (Y_UNLIKELY(value >= 0x80));
    return count;
}

inline int WriteVarUint64(IOutputStream* output, ui64 value)
{
    std::array<char, MaxVarUint64Size> buffer;
    auto size = WriteVarUint64(buffer.data(), value);
    output->Write(buffer.data(), size);
    return size;
}

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

namespace NDetail {

template <class T, size_t N>
T ReadVarUintKnownSize(const char* buffer)
{
    auto result = static_cast<T>(static_cast<ui8>(buffer[N - 1])) << (7 * (N - 1));
    for (size_t i = 0, offset = 0; i < N - 1; i++, offset += 7) {
        result += static_cast<T>(static_cast<ui8>(buffer[i]) - 0x80) << offset;
    }
    return result;
}

} // namespace NDetail

#define XX(type, size) \
    if (static_cast<ui8>(input[size - 1]) < 0x80) { \
        *value = NYT::NDetail::ReadVarUintKnownSize<type, size>(input); \
        return size; \
    }

Y_FORCE_INLINE int ReadVarUint32(const char* input, ui32* value)
{
    XX(ui64, 1)
    XX(ui64, 2)
    XX(ui64, 3)
    XX(ui64, 4)
    XX(ui64, 5)
    throw TSimpleException("Value is too big for varuint32");
}

Y_FORCE_INLINE int ReadVarUint64(const char* input, ui64* value)
{
    XX(ui64, 1)
    XX(ui64, 2)
    XX(ui64, 3)
    XX(ui64, 4)
    XX(ui64, 5)
    XX(ui64, 6)
    XX(ui64, 7)
    XX(ui64, 8)
    XX(ui64, 9)
    XX(ui64, 10)
    throw TSimpleException("Value is too big for varuint64");
}

#undef XX

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

namespace NDetail {

template <class TOutput>
Y_FORCE_INLINE int WriteVarUint32Impl(TOutput output, ui32 value)
{
    return WriteVarUint64(output, static_cast<ui64>(value));
}

template <class TOutput>
Y_FORCE_INLINE int WriteVarInt32Impl(TOutput output, i32 value)
{
    return WriteVarUint64(output, ZigZagEncode32(value));
}

template <class TOutput>
Y_FORCE_INLINE int WriteVarInt64Impl(TOutput output, i64 value)
{
    return WriteVarUint64(output, ZigZagEncode64(value));
}

} // namespace NDetail

Y_FORCE_INLINE int WriteVarUint32(IOutputStream* output, ui32 value)
{
    return NYT::NDetail::WriteVarUint32Impl(output, value);
}

Y_FORCE_INLINE int WriteVarUint32(char* output, ui32 value)
{
    return NYT::NDetail::WriteVarUint32Impl(output, value);
}

Y_FORCE_INLINE int WriteVarInt32(IOutputStream* output, i32 value)
{
    return NYT::NDetail::WriteVarInt32Impl(output, value);
}

Y_FORCE_INLINE int WriteVarInt32(char* output, i32 value)
{
    return NYT::NDetail::WriteVarInt32Impl(output, value);
}

Y_FORCE_INLINE int WriteVarInt64(IOutputStream* output, i64 value)
{
    return NYT::NDetail::WriteVarInt64Impl(output, value);
}

Y_FORCE_INLINE int WriteVarInt64(char* output, i64 value)
{
    return NYT::NDetail::WriteVarInt64Impl(output, value);
}

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

namespace NDetail {

template <class TReadCallback>
int ReadVarUint64Impl(ui64* value, TReadCallback&& doRead)
{
    size_t count = 0;
    ui64 result = 0;

    ui8 byte;
    do {
        byte = doRead();
        result |= (static_cast<ui64> (byte & 0x7F)) << (7 * count);
        ++count;
        if (count > MaxVarUint64Size) {
            throw TSimpleException("Value is too big for varuint64");
        }
    } while (byte & 0x80);

    *value = result;
    return count;
}

inline int ReadVarUint64Fallback(const char* input, const char* end, ui64* value)
{
    return ReadVarUint64Impl(
        value,
        [&] {
            if (input == end) {
                throw TSimpleException("Premature end of data while reading varuint64");
            }
            return *input++;
        });
}

} // namespace NDetail

inline int ReadVarUint64(IInputStream* input, ui64* value)
{
    return NYT::NDetail::ReadVarUint64Impl(
        value,
        [&] {
            char byte;
            if (input->Read(&byte, 1) != 1) {
                throw TSimpleException("Premature end of stream while reading varuint64");
            }
            return byte;
        });
}

Y_FORCE_INLINE int ReadVarUint64(const char* input, const char* end, ui64* value)
{
    if (Y_LIKELY(static_cast<size_t>(end - input) >= MaxVarUint64Size)) {
        return ReadVarUint64(input, value);
    } else {
        return NYT::NDetail::ReadVarUint64Fallback(input, end, value);
    }
}

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

namespace NDetail {

template <class... TArgs>
Y_FORCE_INLINE int ReadVarUint32Impl(ui32* value, TArgs&&... args)
{
    ui64 varInt;
    int bytesRead = ReadVarUint64(std::forward<TArgs>(args)..., &varInt);
    if (varInt > std::numeric_limits<ui32>::max()) {
        throw TSimpleException("Value is too big for varuint32");
    }
    *value = static_cast<ui32>(varInt);
    return bytesRead;
}

} // namespace NDetail

Y_FORCE_INLINE int ReadVarUint32(IInputStream* input, ui32* value)
{
    return NYT::NDetail::ReadVarUint32Impl(value, input);
}

Y_FORCE_INLINE int ReadVarUint32(const char* input, const char* end, ui32* value)
{
    return NYT::NDetail::ReadVarUint32Impl(value, input, end);
}

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

namespace NDetail {

template <class... TArgs>
Y_FORCE_INLINE int ReadVarInt32Impl(i32* value, TArgs&&... args)
{
    ui64 varInt;
    int bytesRead = ReadVarUint64(std::forward<TArgs>(args)..., &varInt);
    if (varInt > std::numeric_limits<ui32>::max()) {
        throw TSimpleException("Value is too big for varint32");
    }
    *value = ZigZagDecode32(static_cast<ui32>(varInt));
    return bytesRead;
}

} // namespace NDetail

Y_FORCE_INLINE int ReadVarInt32(IInputStream* input, i32* value)
{
    return NYT::NDetail::ReadVarInt32Impl(value, input);
}

Y_FORCE_INLINE int ReadVarInt32(const char* input, i32* value)
{
    return NYT::NDetail::ReadVarInt32Impl(value, input);
}

Y_FORCE_INLINE int ReadVarInt32(const char* input, const char* end, i32* value)
{
    return NYT::NDetail::ReadVarInt32Impl(value, input, end);
}

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

namespace NDetail {

template <class... TArgs>
Y_FORCE_INLINE int ReadVarInt64Impl(i64* value, TArgs&&... args)
{
    ui64 varInt;
    int bytesRead = ReadVarUint64(std::forward<TArgs>(args)..., &varInt);
    *value = ZigZagDecode64(varInt);
    return bytesRead;
}

} // namespace NDetail

Y_FORCE_INLINE int ReadVarInt64(IInputStream* input, i64* value)
{
    return NYT::NDetail::ReadVarInt64Impl(value, input);
}

Y_FORCE_INLINE int ReadVarInt64(const char* input, i64* value)
{
    return NYT::NDetail::ReadVarInt64Impl(value, input);
}

Y_FORCE_INLINE int ReadVarInt64(const char* input, const char* end, i64* value)
{
    return NYT::NDetail::ReadVarInt64Impl(value, input, end);
}

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

} // namespace NYT