// Copyright (C) 2016 The Qt Company Ltd.
// Copyright (C) 2021 Alex Trotsenko <alex1973tr@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qwindowspipereader_p.h"
#include <qcoreapplication.h>
#include <QMutexLocker>
#include <QPointer>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

static const DWORD minReadBufferSize = 4096;

QWindowsPipeReader::QWindowsPipeReader(QObject *parent)
    : QObject(parent),
      handle(INVALID_HANDLE_VALUE),
      eventHandle(CreateEvent(NULL, FALSE, FALSE, NULL)),
      syncHandle(CreateEvent(NULL, TRUE, FALSE, NULL)),
      waitObject(NULL),
      readBufferMaxSize(0),
      actualReadBufferSize(0),
      pendingReadBytes(0),
      lastError(ERROR_SUCCESS),
      state(Stopped),
      readSequenceStarted(false),
      pipeBroken(true),
      readyReadPending(false),
      winEventActPosted(false)
{
    ZeroMemory(&overlapped, sizeof(OVERLAPPED));
    overlapped.hEvent = eventHandle;
    waitObject = CreateThreadpoolWait(waitCallback, this, NULL);
    if (waitObject == NULL)
        qErrnoWarning("QWindowsPipeReader: CreateThreadpollWait failed.");
}

QWindowsPipeReader::~QWindowsPipeReader()
{
    stop();

    // Wait for thread pool callback to complete, as it can be still
    // executing some completion code.
    WaitForThreadpoolWaitCallbacks(waitObject, FALSE);
    CloseThreadpoolWait(waitObject);
    CloseHandle(eventHandle);
    CloseHandle(syncHandle);
}

/*!
    Sets the handle to read from. The handle must be valid.
    Do not call this function while the pipe is running.
 */
void QWindowsPipeReader::setHandle(HANDLE hPipeReadEnd)
{
    readBuffer.clear();
    actualReadBufferSize = 0;
    readyReadPending = false;
    pendingReadBytes = 0;
    handle = hPipeReadEnd;
    pipeBroken = false;
    lastError = ERROR_SUCCESS;
}

/*!
    Stops the asynchronous read sequence.
    If the read sequence is running then the I/O operation is canceled.
 */
void QWindowsPipeReader::stop()
{
    cancelAsyncRead(Stopped);
    pipeBroken = true;
}

/*!
    Stops the asynchronous read sequence.
    Reads all pending bytes into the internal buffer.
 */
void QWindowsPipeReader::drainAndStop()
{
    cancelAsyncRead(Draining);
    pipeBroken = true;
}

/*!
    Stops the asynchronous read sequence.
    Clears the internal buffer and discards any pending data.
 */
void QWindowsPipeReader::stopAndClear()
{
    cancelAsyncRead(Stopped);
    readBuffer.clear();
    actualReadBufferSize = 0;
    // QLocalSocket is supposed to write data in the 'Closing'
    // state, so we don't set 'pipeBroken' flag here. Also, avoid
    // setting this flag in checkForReadyRead().
    lastError = ERROR_SUCCESS;
}

/*!
    Stops the asynchronous read sequence.
 */
void QWindowsPipeReader::cancelAsyncRead(State newState)
{
    if (state != Running)
        return;

    mutex.lock();
    state = newState;
    if (readSequenceStarted) {
        // This can legitimately fail due to the GetOverlappedResult()
        // in the callback not being locked. We ignore ERROR_NOT_FOUND
        // in this case.
        if (!CancelIoEx(handle, &overlapped)) {
            const DWORD dwError = GetLastError();
            if (dwError != ERROR_NOT_FOUND) {
                qErrnoWarning(dwError, "QWindowsPipeReader: CancelIoEx on handle %p failed.",
                              handle);
            }
        }

        // Wait for callback to complete.
        do {
            mutex.unlock();
            waitForNotification();
            mutex.lock();
        } while (readSequenceStarted);
    }
    mutex.unlock();

    // Finish reading to keep the class state consistent. Note that
    // signals are not emitted in the call below, as the caller is
    // expected to do that synchronously.
    consumePending();
}

/*!
    Sets the size of internal read buffer.
 */
void QWindowsPipeReader::setMaxReadBufferSize(qint64 size)
{
    QMutexLocker locker(&mutex);
    readBufferMaxSize = size;
}

/*!
    Returns \c true if async operation is in progress, there is
    pending data to read, or a read error is pending.
*/
bool QWindowsPipeReader::isReadOperationActive() const
{
    QMutexLocker locker(&mutex);
    return readSequenceStarted || readyReadPending
           || (lastError != ERROR_SUCCESS && !pipeBroken);
}

/*!
    Returns the number of bytes we've read so far.
 */
qint64 QWindowsPipeReader::bytesAvailable() const
{
    return actualReadBufferSize;
}

/*!
    Copies at most \c{maxlen} bytes from the internal read buffer to \c{data}.
 */
qint64 QWindowsPipeReader::read(char *data, qint64 maxlen)
{
    QMutexLocker locker(&mutex);
    qint64 readSoFar;

    // If startAsyncRead() has read data, copy it to its destination.
    if (maxlen == 1 && actualReadBufferSize > 0) {
        *data = readBuffer.getChar();
        actualReadBufferSize--;
        readSoFar = 1;
    } else {
        readSoFar = readBuffer.read(data, qMin(actualReadBufferSize, maxlen));
        actualReadBufferSize -= readSoFar;
    }

    if (!pipeBroken) {
        startAsyncReadHelper(&locker);
        if (readSoFar == 0)
            return -2;      // signal EWOULDBLOCK
    }

    return readSoFar;
}

/*!
    Reads a line from the internal buffer, but no more than \c{maxlen}
    characters. A terminating '\0' byte is always appended to \c{data},
    so \c{maxlen} must be larger than 1.
 */
qint64 QWindowsPipeReader::readLine(char *data, qint64 maxlen)
{
    QMutexLocker locker(&mutex);
    qint64 readSoFar = 0;

    if (actualReadBufferSize > 0) {
        readSoFar = readBuffer.readLine(data, qMin(actualReadBufferSize + 1, maxlen));
        actualReadBufferSize -= readSoFar;
    }

    if (!pipeBroken) {
        startAsyncReadHelper(&locker);
        if (readSoFar == 0)
            return -2;      // signal EWOULDBLOCK
    }

    return readSoFar;
}

/*!
    Skips up to \c{maxlen} bytes from the internal read buffer.
 */
qint64 QWindowsPipeReader::skip(qint64 maxlen)
{
    QMutexLocker locker(&mutex);

    const qint64 skippedSoFar = readBuffer.skip(qMin(actualReadBufferSize, maxlen));
    actualReadBufferSize -= skippedSoFar;

    if (!pipeBroken) {
        startAsyncReadHelper(&locker);
        if (skippedSoFar == 0)
            return -2;      // signal EWOULDBLOCK
    }

    return skippedSoFar;
}

/*!
    Returns \c true if a complete line of data can be read from the buffer.
 */
bool QWindowsPipeReader::canReadLine() const
{
    QMutexLocker locker(&mutex);
    return readBuffer.indexOf('\n', actualReadBufferSize) >= 0;
}

/*!
    Starts an asynchronous read sequence on the pipe.
 */
void QWindowsPipeReader::startAsyncRead()
{
    QMutexLocker locker(&mutex);
    startAsyncReadHelper(&locker);
}

void QWindowsPipeReader::startAsyncReadHelper(QMutexLocker<QMutex> *locker)
{
    if (readSequenceStarted || lastError != ERROR_SUCCESS)
        return;

    state = Running;
    startAsyncReadLocked();

    // Do not post the event, if the read operation will be completed asynchronously.
    if (!readyReadPending && lastError == ERROR_SUCCESS)
        return;

    if (!winEventActPosted) {
        winEventActPosted = true;
        locker->unlock();
        QCoreApplication::postEvent(this, new QEvent(QEvent::WinEventAct));
    } else {
        locker->unlock();
    }

    SetEvent(syncHandle);
}

/*!
    Starts a new read sequence. Thread-safety should be ensured
    by the caller.
 */
void QWindowsPipeReader::startAsyncReadLocked()
{
    // Determine the number of bytes to read.
    qint64 bytesToRead = qMax(checkPipeState(), state == Running ? minReadBufferSize : 0);

    // This can happen only while draining; just do nothing in this case.
    if (bytesToRead == 0)
        return;

    while (lastError == ERROR_SUCCESS) {
        if (readBufferMaxSize && bytesToRead > (readBufferMaxSize - readBuffer.size())) {
            bytesToRead = readBufferMaxSize - readBuffer.size();
            if (bytesToRead <= 0) {
                // Buffer is full. User must read data from the buffer
                // before we can read more from the pipe.
                return;
            }
        }

        char *ptr = readBuffer.reserve(bytesToRead);

        // ReadFile() returns true, if the read operation completes synchronously.
        // We don't need to call GetOverlappedResult() additionally, because
        // 'numberOfBytesRead' is valid in this case.
        DWORD numberOfBytesRead;
        DWORD errorCode = ERROR_SUCCESS;
        if (!ReadFile(handle, ptr, bytesToRead, &numberOfBytesRead, &overlapped)) {
            errorCode = GetLastError();
            if (errorCode == ERROR_IO_PENDING) {
                Q_ASSERT(state == Running);
                // Operation has been queued and will complete in the future.
                readSequenceStarted = true;
                SetThreadpoolWait(waitObject, eventHandle, NULL);
                return;
            }
        }

        if (!readCompleted(errorCode, numberOfBytesRead))
            return;

        // In the 'Draining' state, we have to get all the data with one call
        // to ReadFile(). Note that message mode pipes are not supported here.
        if (state == Draining) {
            Q_ASSERT(bytesToRead == qint64(numberOfBytesRead));
            return;
        }

        // We need to loop until all pending data has been read and an
        // operation is queued for asynchronous completion.
        // If the pipe is configured to work in message mode, we read
        // the data in chunks.
        bytesToRead = qMax(checkPipeState(), minReadBufferSize);
    }
}

/*!
    \internal

    Thread pool callback procedure.
 */
void QWindowsPipeReader::waitCallback(PTP_CALLBACK_INSTANCE instance, PVOID context,
                                      PTP_WAIT wait, TP_WAIT_RESULT waitResult)
{
    Q_UNUSED(instance);
    Q_UNUSED(wait);
    Q_UNUSED(waitResult);
    QWindowsPipeReader *pipeReader = reinterpret_cast<QWindowsPipeReader *>(context);

    // Get the result of the asynchronous operation.
    DWORD numberOfBytesTransfered = 0;
    DWORD errorCode = ERROR_SUCCESS;
    if (!GetOverlappedResult(pipeReader->handle, &pipeReader->overlapped,
                             &numberOfBytesTransfered, FALSE))
        errorCode = GetLastError();

    pipeReader->mutex.lock();

    pipeReader->readSequenceStarted = false;

    // Do not overwrite error code, if error has been detected by
    // checkPipeState() in waitForPipeClosed(). Also, if the reader was
    // stopped, the only reason why this function can be called is the
    // completion of a cancellation. No signals should be emitted, and
    // no new read sequence should be started in this case.
    if (pipeReader->lastError == ERROR_SUCCESS && pipeReader->state != Stopped) {
        // Ignore ERROR_OPERATION_ABORTED. We have canceled the I/O operation
        // specifically for flushing the pipe.
        if (pipeReader->state == Draining && errorCode == ERROR_OPERATION_ABORTED)
            errorCode = ERROR_SUCCESS;

        if (pipeReader->readCompleted(errorCode, numberOfBytesTransfered))
            pipeReader->startAsyncReadLocked();

        if (pipeReader->state == Running && !pipeReader->winEventActPosted) {
            pipeReader->winEventActPosted = true;
            pipeReader->mutex.unlock();
            QCoreApplication::postEvent(pipeReader, new QEvent(QEvent::WinEventAct));
        } else {
            pipeReader->mutex.unlock();
        }
    } else {
        pipeReader->mutex.unlock();
    }

    // We set the event only after unlocking to avoid additional context
    // switches due to the released thread immediately running into the lock.
    SetEvent(pipeReader->syncHandle);
}

/*!
    Will be called whenever the read operation completes. Returns \c true if
    no error occurred; otherwise returns \c false.
 */
bool QWindowsPipeReader::readCompleted(DWORD errorCode, DWORD numberOfBytesRead)
{
    // ERROR_MORE_DATA is not an error. We're connected to a message mode
    // pipe and the message didn't fit into the pipe's system
    // buffer. We will read the remaining data in the next call.
    if (errorCode == ERROR_SUCCESS || errorCode == ERROR_MORE_DATA) {
        readyReadPending = true;
        pendingReadBytes += numberOfBytesRead;
        readBuffer.truncate(actualReadBufferSize + pendingReadBytes);
        return true;
    }

    lastError = errorCode;
    return false;
}

/*!
    Receives notification that the read operation has completed.
 */
bool QWindowsPipeReader::event(QEvent *e)
{
    if (e->type() == QEvent::WinEventAct) {
        consumePendingAndEmit(true);
        return true;
    }
    return QObject::event(e);
}

/*!
    Updates the read buffer size and emits pending signals in the main thread.
    Returns \c true, if readyRead() was emitted.
 */
bool QWindowsPipeReader::consumePendingAndEmit(bool allowWinActPosting)
{
    ResetEvent(syncHandle);
    mutex.lock();

    // Enable QEvent::WinEventAct posting.
    if (allowWinActPosting)
        winEventActPosted = false;

    const bool emitReadyRead = consumePending();
    const DWORD dwError = lastError;

    mutex.unlock();

    // Trigger 'pipeBroken' only once. This flag must be updated before
    // emitting the readyRead() signal. Otherwise, the read sequence will
    // be considered not finished, and we may hang if a slot connected
    // to readyRead() calls waitForReadyRead().
    const bool emitPipeClosed = (dwError != ERROR_SUCCESS && !pipeBroken);
    if (emitPipeClosed)
        pipeBroken = true;

    // Disable any further processing, if the pipe was stopped.
    // We are not allowed to emit signals in either 'Stopped'
    // or 'Draining' state.
    if (state != Running)
        return false;

    if (!emitPipeClosed) {
        if (emitReadyRead)
            emit readyRead();
    } else {
        QPointer<QWindowsPipeReader> alive(this);
        if (emitReadyRead)
            emit readyRead();
        if (alive && dwError != ERROR_BROKEN_PIPE && dwError != ERROR_PIPE_NOT_CONNECTED)
            emit winError(dwError, "QWindowsPipeReader::consumePendingAndEmit"_L1);
        if (alive)
            emit pipeClosed();
    }

    return emitReadyRead;
}

/*!
    Updates the read buffer size. Returns \c true, if readyRead()
    should be emitted. Thread-safety should be ensured by the caller.
 */
bool QWindowsPipeReader::consumePending()
{
    if (readyReadPending) {
        readyReadPending = false;
        actualReadBufferSize += pendingReadBytes;
        pendingReadBytes = 0;
        return true;
    }

    return false;
}

/*!
    Returns the number of available bytes in the pipe.
 */
DWORD QWindowsPipeReader::checkPipeState()
{
    DWORD bytes;
    if (PeekNamedPipe(handle, nullptr, 0, nullptr, &bytes, nullptr))
        return bytes;

    lastError = GetLastError();
    return 0;
}

bool QWindowsPipeReader::waitForNotification()
{
    DWORD waitRet;
    do {
        waitRet = WaitForSingleObjectEx(syncHandle, INFINITE, TRUE);
        if (waitRet == WAIT_OBJECT_0)
            return true;

        // Some I/O completion routine was called. Wait some more.
    } while (waitRet == WAIT_IO_COMPLETION);

    return false;
}

QT_END_NAMESPACE
