#include "log.h"

#include <contrib/libs/lz4/lz4.h>
#include <google/protobuf/messagext.h>

#include <util/generic/scope.h>

static constexpr int SyncLimit = 1 << 19;

void NSv::TLog::Reopen(THolder<IOutputStream> out) {
    Enabled_ = !!out;
    if (Flush_) {
        PushOwned(&Stop_);
        cone::uninterruptible([&] {
            Flush_->wait(cone::norethrow);
        });
    }
    if (!(Out_ = std::move(out))) {
        return;
    }
    Flush_ = cone::thread([this] {
        size_t flushes = 0;
        size_t unwritten = 0;
        google::protobuf::io::TOutputStreamProxy proxy(Out_.Get());
        google::protobuf::io::CopyingOutputStreamAdaptor adaptor(&proxy);
        NSv::TLogEvent syncMsg;
        NSv::TLogEvent packedHead;
        TString data;
        data.reserve(SyncLimit * 2);
        syncMsg.SetSync(TString::Join("\xFF", SyncField));

        auto appendMessage = [](const NProtoBuf::MessageLite& m, TString& out) {
            google::protobuf::io::StringOutputStream so(&out);
            google::protobuf::io::CodedOutputStream co(&so);
            co.WriteVarint64(m.ByteSizeLong());
            Y_PROTOBUF_SUPPRESS_NODISCARD m.SerializePartialToCodedStream(&co);
        };

        auto compress = [&]() {
            if (!unwritten) {
                return;
            }

            TString tmp;
            appendMessage(packedHead, tmp);
            data.prepend(tmp);
            auto out = syncMsg.MutableLZ4();
            out->resize(LZ4_COMPRESSBOUND(data.size()));
            out->resize(LZ4_compress_default(data.begin(), out->begin(), data.size(), out->size()));
            syncMsg.SetLZ4DecompressedSize(data.size());
            syncMsg.SetLZ4Time(packedHead.GetTime(0));
            syncMsg.MutableSync()->begin()[0] = 1
              + google::protobuf::io::CodedOutputStream::VarintSize32(syncMsg.ByteSizeLong())
              + google::protobuf::io::CodedOutputStream::StaticVarintSize32<NSv::TLogEvent::kSyncFieldNumber << 3 | 2>::value
              + google::protobuf::io::CodedOutputStream::StaticVarintSize32<4 + SyncField.size()>::value;

            static_assert(NSv::TLogEvent::kSyncFieldNumber == 1, "the `Sync` field should be serialized first");
            google::protobuf::io::CodedOutputStream co(&adaptor);
            co.WriteVarint32(syncMsg.ByteSizeLong());
            Y_PROTOBUF_SUPPRESS_NODISCARD syncMsg.SerializePartialToCodedStream(&co);
            packedHead.Clear();
            data.clear();
            unwritten = 0;
            flushes++;
        };

        cone::guard periodicFlush = [&]() {
            // This is the *average* delay; worst-case, an event generated right after this
            // loop goes to sleep will have to wait for two iterations.
            for (size_t c = flushes; cone::sleep_for(std::chrono::seconds(5)); c = flushes) {
                if (c == flushes) {
                    // messages are being generated too slowly to fill up compressed blocks
                    compress();
                }
                adaptor.Flush();
                Out_->Flush();
            }
            return false;
        };

        Y_DEFER { compress(); }; // flush pending items when terminating
        while (true) {
            IItem* next = nullptr;
            do {
                if (Head_ != &Stub_ && Head_ == Tail_.load(std::memory_order_acquire)) {
                    PushOwned(&Stub_); // ensure that `Head_->Next_` is non-null
                }
                while (!(next = Head_->Next_.load(std::memory_order_acquire))) {
                    if (!cone::sleep_for(std::chrono::milliseconds(5)) || !More_.wait_if([&] {
                        return !(next = Head_->Next_.load(std::memory_order_acquire));
                    })) {
                        return false;
                    }
                }
                std::swap(Head_, next);
                if (next == &Stop_) {
                    return true;
                }
            } while (next == &Stub_);
            // Another window where `Remaining_` overestimates the number of items (by 1).
            ++Remaining_;
            // Ownership of this object has been transferred to us.
            THolder<TItem> item = THolder<TItem>(static_cast<TItem*>(next));
            packedHead.AddTime(item->Time);
            packedHead.AddType(item->Data->GetDescriptor()->options().GetExtension(message_id));
            packedHead.AddCurrent(item->Current);
            packedHead.AddContext(item->Context);
            appendMessage(*item->Data, data);
            // The 32 is to compensate for 5 varints; don't bother computing the actual size.
            if ((unwritten += 32 + item->Data->ByteSizeLong()) >= SyncLimit) {
                compress();
            }
        }
    }, 1024 * 1024);
}
