#include "threadedfile.h"

#include <balancer/kernel/memory/chunks.h>
#include <balancer/kernel/thread/threadedqueue.h>

#include <util/system/file.h>
#include <util/system/fstat.h>

using namespace NSrvKernel;

namespace {
    class TThreadedFile : public TNonCopyable, public TThreadedQueue::ICallback {
    public:
        using TAction = void (TThreadedFile::*)();

        TAction Action = nullptr;

        TThreadedFile(TString name) noexcept
            : Name_(std::move(name))
            , File_(INVALID_FHANDLE)
        {}

        void DoRun() noexcept override {
            Y_ASSERT(Action);
            (this->*Action)();
        }

        const TString& Name() const noexcept {
            return Name_;
        }

        const TFileStat& Stat() const noexcept {
            return Stat_;
        }

    public:
        ui64 NextLength = 0;
        ui64 Offset = 0;
        TBuffer Buffer;

    public:
        void Error() noexcept {
            Y_ASSERT(false);
        }

        void Open() noexcept {
            TFileHandle(Name_, OpenExisting | RdOnly).Swap(File_);

            if (File_.IsOpen()) {
                Stat_ = TFileStat(File_);
                Action = nullptr;
            } else {
                Action = &TThreadedFile::Error;
            }
        }

        void Close() noexcept {
            const bool ret = File_.Close();

            if (ret) {
                Action = nullptr;
            } else {
                Action = &TThreadedFile::Error;
            }
        }

        void Pread() noexcept {
            if (Awaited()) {
                i32 readed = 0;

                if (NextLength) {
                    Buffer.Reserve(Buffer.Size() + NextLength);
                    readed = File_.Pread(Buffer.Data() + Buffer.Size(), Buffer.Avail(), Offset);
                }

                if (readed > 0) {
                    Buffer.Advance(readed);
                    Offset += readed;
                    NextLength -= readed;
                } else if (readed == 0) {
                    Action = nullptr;
                } else {
                    Action = &TThreadedFile::Error;
                }
            }
        }

    private:
        const TString Name_;
        TFileHandle File_;
        TFileStat Stat_;
    };

    TError InitThreadedFile(
        THolder<TThreadedFile>& file, TThreadedQueue* queue) noexcept
    {
        file->Action = &TThreadedFile::Open;

        file = queue->Run(file.Release(), TInstant::Max());

        Y_REQUIRE(file,
                  TThreadedFileError{} << "failed to init");

        Y_REQUIRE(file->Action != &TThreadedFile::Error,
                  TThreadedFileError{} << "failed to open file " << file->Name());
        return {};
    }

    TErrorOr<TChunkPtr> ThreadedFilePread(
        ui64 len, ui64 offset, TInstant deadline, THolder<TThreadedFile>& file,
        TThreadedQueue* queue) noexcept
    {
        file->NextLength = len;
        file->Offset = offset;

        file->Action = &TThreadedFile::Pread;

        do {
            file = queue->Run(file.Release(), deadline);
        } while (file && file->Action && file->Action != &TThreadedFile::Error);

        if (!file) {
            return nullptr;
        }

        Y_REQUIRE(file->Action != &TThreadedFile::Error,
                  TThreadedFileError{} << "failed to read " << len
                                       << " bytes at offset " << file->Offset << " from file "
                                       << file->Name());

        return NewChunk(std::move(file->Buffer));
    }
}

TErrorOr<TChunkPtr> NSrvKernel::ThreadedFileReadAll(
    TString name, NSrvKernel::TThreadedQueue* queue, ui64 maxLength, TInstant deadline) noexcept
{
    auto file = MakeHolder<TThreadedFile>(std::move(name));
    Y_PROPAGATE_ERROR(InitThreadedFile(file, queue));

    const ui64 length = file->Stat().Size;

    if (length > maxLength) {
        return nullptr;
    }

    return ThreadedFilePread(length, 0, deadline, file, queue);
}

TErrorOr<ui64> NSrvKernel::ThreadedFileSend(
    TString name, NSrvKernel::TThreadedQueue* queue, IIoOutput* output) noexcept
{
    auto file = MakeHolder<TThreadedFile>(std::move(name));
    Y_PROPAGATE_ERROR(InitThreadedFile(file, queue));

    ui64 offset = 0;

    while (true) {
        TChunkPtr chunk;
        Y_PROPAGATE_ERROR(
            ThreadedFilePread(8192, offset, TInstant::Max(), file, queue).AssignTo(chunk));

        Y_REQUIRE(chunk,
                  TThreadedFileError{} << "reading file failed: "
                  "fs thread is overloaded or coro with read request is canceled");

        if (!chunk->Length()) {
            break;
        }

        offset += chunk->Length();

        Y_PROPAGATE_ERROR(output->Send(TChunkList(std::move(chunk)), TInstant::Max()));
    }
    return offset;
}
