/*
 * Copyright 2023 Bloomberg LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <buildboxcommon_logging.h>
#include <buildboxcommon_streamingstandardoutputifstreamfilemonitor.h>
#include <cstring>
#include <thread>
#include <utility>

namespace buildboxcommon {

StreamingStandardOutputIfstreamFileMonitor::
    StreamingStandardOutputIfstreamFileMonitor(
        const std::string &path, DataReadyCallback dataReadyCallback,
        int64_t pollIntervalMillieconds, int64_t writeTimeoutMilliseconds,
        size_t readBufferSize)
    : d_filePath(path), d_dataReadyCallback(std::move(dataReadyCallback)),
      d_outputStream(std::ifstream(path, std::ios::binary)),
      d_pollInterval(
          std::min(pollIntervalMillieconds, writeTimeoutMilliseconds)),
      d_writeTimeout(writeTimeoutMilliseconds),
      d_readBufferSize(readBufferSize),
      d_monitoringThread(&StreamingStandardOutputIfstreamFileMonitor::monitor,
                         this)
{
    if (readBufferSize <= 0) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error, "Read buffer can only have positive size");
    }
}

StreamingStandardOutputIfstreamFileMonitor::
    ~StreamingStandardOutputIfstreamFileMonitor()
{
    stop();
    if (d_monitoringThread.joinable()) {
        d_monitoringThread.join();
    }
    d_outputStream.close();
}

void StreamingStandardOutputIfstreamFileMonitor::stop()
{
    {
        // from reference:
        // https://en.cppreference.com/w/cpp/thread/condition_variable "Even if
        // the shared variable is atomic, it must be modified while owning the
        // mutex to correctly publish the modification to the waiting thread."
        std::scoped_lock<std::mutex> lock(d_mutex);
        d_stopRequested = true;
    }

    d_cv.notify_all();
}

void StreamingStandardOutputIfstreamFileMonitor::monitor()
{
    if (!d_outputStream) {
        BUILDBOX_LOG_ERROR(
            "Failed to open output file to stream: " << d_filePath);
        return;
    }

    std::unique_ptr<char[]> readBuffer =
        std::make_unique<char[]>(d_readBufferSize);
    size_t readBufferOffset = 0;
    std::streamsize outputStreamOffset = 0;
    auto lastWriteTime = std::chrono::system_clock::now();
    bool last_iter =
        false; // when stopped, try to read and send remaining data
    while (!d_stopRequested || !last_iter) {
        last_iter |= d_stopRequested;
        // Read and send as much data from file as possible until EOF
        d_outputStream.seekg(outputStreamOffset);
        while (d_outputStream.good()) {
            const std::streamsize readSize =
                readChunkFromOutput(readBuffer.get(), readBufferOffset);
            if (readSize > 0) {
                readBufferOffset += readSize;
                outputStreamOffset += readSize;
            }
            if (shouldWriteChunk(readBufferOffset, lastWriteTime)) {
                if (!d_dataReadyCallback(
                        FileChunk(readBuffer.get(), readBufferOffset))) {
                    d_stopRequested = true;
                    return;
                }
                readBufferOffset = 0;
                lastWriteTime = std::chrono::system_clock::now();
            }
        }
        // non-EOF error
        if (!d_outputStream && !d_outputStream.eof()) {
            BUILDBOX_LOG_ERROR(
                "Failed to read chunk from output file. strerror="
                << std::strerror(errno));
            return;
        }
        // Clear EOF
        d_outputStream.clear();

        if (!d_stopRequested) {
            std::unique_lock lock(d_mutex);
            d_cv.wait_for(lock, d_pollInterval,
                          [&] { return d_stopRequested.load(); });
            lock.unlock();
        }
    }
}

std::streamsize
StreamingStandardOutputIfstreamFileMonitor::readChunkFromOutput(
    char *buffer, const size_t bufferOffset)
{
    const auto remainingBufferSize = d_readBufferSize - bufferOffset;
    d_outputStream.read(
        buffer + // NOLINT (cppcoreguidelines-pro-bounds-pointer-arithmetic)
            bufferOffset,
        static_cast<std::streamsize>(remainingBufferSize));
    return d_outputStream.gcount();
}

bool StreamingStandardOutputIfstreamFileMonitor::shouldWriteChunk(
    const size_t bufferOffset,
    std::chrono::time_point<std::chrono::system_clock> &lastWriteTime)
{
    // buffer must be non-empty and either:
    // 1. read buffer is full
    // 2. timeout is reached
    // 3. stop is requested

    const bool nonempty = bufferOffset > 0;
    const bool bufferFull = bufferOffset == d_readBufferSize;
    const bool timeout =
        (std::chrono::system_clock::now() - lastWriteTime) > d_writeTimeout;
    return nonempty && (bufferFull || d_stopRequested || timeout);
}

} // namespace buildboxcommon
