#pragma once

#include <util/system/yassert.h>
#include <util/generic/utility.h>
#include <util/generic/ylimits.h>
#include <string.h>

struct IBinaryStream {
    virtual ~IBinaryStream() = default;

    inline i64 Write(const void* userBuffer, i64 size) {
        if (size <= Max<int>()) {
            return WriteImpl(userBuffer, static_cast<int>(size));
        } else {
            return LongWrite(userBuffer, size);
        }
    }

    inline i64 Read(void* userBuffer, i64 size) {
        if (size <= Max<int>()) {
            return ReadImpl(userBuffer, static_cast<int>(size));
        } else {
            return LongRead(userBuffer, size);
        }
    }

    virtual bool IsValid() const = 0;
    virtual bool IsFailed() const = 0;

private:
    virtual int WriteImpl(const void* userBuffer, int size) = 0;
    virtual int ReadImpl(void* userBuffer, int size) = 0;

    i64 LongRead(void* userBuffer, i64 size);
    i64 LongWrite(const void* userBuffer, i64 size);
};

template <int N_SIZE = 16384>
class TBufferedStream {
    char Buf[N_SIZE];
    i64 Pos, BufSize;
    IBinaryStream& Stream;
    bool bIsReading, bIsEof, bFailed;

    void ReadComplex(void* userBuffer, i64 size) {
        if (bIsEof) {
            memset(userBuffer, 0, size);
            return;
        }
        char* dst = (char*)userBuffer;
        i64 leftBytes = BufSize - Pos;
        memcpy(dst, Buf + Pos, leftBytes);
        dst += leftBytes;
        size -= leftBytes;
        Pos = BufSize = 0;
        if (size > N_SIZE) {
            i64 n = Stream.Read(dst, size);
            bFailed = Stream.IsFailed();
            if (n != size) {
                bIsEof = true;
                memset(dst + n, 0, size - n);
            }
        } else {
            BufSize = Stream.Read(Buf, N_SIZE);
            bFailed = Stream.IsFailed();
            if (BufSize == 0)
                bIsEof = true;
            Read(dst, size);
        }
    }

    void WriteComplex(const void* userBuffer, i64 size) {
        Flush();
        if (size >= N_SIZE) {
            Stream.Write(userBuffer, size);
            bFailed = Stream.IsFailed();
        } else
            Write(userBuffer, size);
    }

    void operator=(const TBufferedStream&) {
    }

public:
    TBufferedStream(bool bRead, IBinaryStream& stream)
        : Pos(0)
        , BufSize(0)
        , Stream(stream)
        , bIsReading(bRead)
        , bIsEof(false)
        , bFailed(false)
    {
    }
    ~TBufferedStream() {
        if (!bIsReading)
            Flush();
    }
    void Flush() {
        Y_ASSERT(!bIsReading);
        if (bIsReading)
            return;
        Stream.Write(Buf, Pos);
        bFailed = Stream.IsFailed();
        Pos = 0;
    }
    bool IsEof() const {
        return bIsEof;
    }
    inline void Read(void* userBuffer, i64 size) {
        Y_ASSERT(bIsReading);
        if (!bIsEof && size + Pos <= BufSize) {
            memcpy(userBuffer, Buf + Pos, size);
            Pos += size;
            return;
        }
        ReadComplex(userBuffer, size);
    }
    inline void Write(const void* userBuffer, i64 size) {
        Y_ASSERT(!bIsReading);
        if (Pos + size < N_SIZE) {
            memcpy(Buf + Pos, userBuffer, size);
            Pos += size;
            return;
        }
        WriteComplex(userBuffer, size);
    }
    bool IsValid() const {
        return Stream.IsValid();
    }
    bool IsFailed() const {
        return bFailed;
    }
};