// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#include "precomp.h"
#include "readDataCooked.hpp"
#include "dbcs.h"
#include "stream.h"
#include "misc.h"
#include "_stream.h"
#include "inputBuffer.hpp"
#include "cmdline.h"
#include "../types/inc/GlyphWidth.hpp"
#include "../types/inc/convert.hpp"

#include "../interactivity/inc/ServiceLocator.hpp"

#define LINE_INPUT_BUFFER_SIZE (256 * sizeof(WCHAR))

using Microsoft::Console::Interactivity::ServiceLocator;

// Routine Description:
// - Constructs cooked read data class to hold context across key presses while a user is modifying their 'input line'.
// Arguments:
// - pInputBuffer - Buffer that data will be read from.
// - pInputReadHandleData - Context stored across calls from the same input handle to return partial data appropriately.
// - screenInfo - Output buffer that will be used for 'echoing' the line back to the user so they can see/manipulate it
// - BufferSize -
// - BytesRead -
// - CurrentPosition -
// - BufPtr -
// - BackupLimit -
// - UserBufferSize - The byte count of the buffer presented by the client
// - UserBuffer - The buffer that was presented by the client for filling with input data on read conclusion/return from server/host.
// - OriginalCursorPosition -
// - NumberOfVisibleChars
// - CtrlWakeupMask - Special client parameter to interrupt editing, end the wait, and return control to the client application
// - Echo -
// - InsertMode -
// - Processed -
// - Line -
// - pTempHandle - A handle to the output buffer to prevent it from being destroyed while we're using it to present 'edit line' text.
// - initialData - any text data that should be prepopulated into the buffer
// - pClientProcess - Attached process handle object
// Return Value:
// - THROW: Throws E_INVALIDARG for invalid pointers.
COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
                                   _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData,
                                   SCREEN_INFORMATION& screenInfo,
                                   _In_ size_t UserBufferSize,
                                   _In_ char* UserBuffer,
                                   _In_ ULONG CtrlWakeupMask,
                                   _In_ const std::wstring_view exeName,
                                   _In_ const std::string_view initialData,
                                   _In_ ConsoleProcessHandle* const pClientProcess) :
    ReadData(pInputBuffer, pInputReadHandleData),
    _screenInfo{ screenInfo },
    _bytesRead{ 0 },
    _currentPosition{ 0 },
    _userBufferSize{ UserBufferSize },
    _userBuffer{ UserBuffer },
    _tempHandle{ nullptr },
    _exeName{ exeName },
    _pdwNumBytes{ nullptr },

    _commandHistory{ CommandHistory::s_Find(pClientProcess) },
    _controlKeyState{ 0 },
    _ctrlWakeupMask{ CtrlWakeupMask },
    _visibleCharCount{ 0 },
    _originalCursorPosition{ -1, -1 },
    _beforeDialogCursorPosition{ 0, 0 },

    _echoInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_ECHO_INPUT) },
    _lineInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_LINE_INPUT) },
    _processedInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) },
    _insertMode{ ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode() },
    _unicode{ false },
    _clientProcess{ pClientProcess }
{
#ifndef UNIT_TESTING
    THROW_IF_FAILED(screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output,
                                                                GENERIC_WRITE,
                                                                FILE_SHARE_READ | FILE_SHARE_WRITE,
                                                                _tempHandle));
#endif

    // to emulate OS/2 KbdStringIn, we read into our own big buffer
    // (256 bytes) until the user types enter.  then return as many
    // chars as will fit in the user's buffer.
    _bufferSize = std::max(UserBufferSize, LINE_INPUT_BUFFER_SIZE);
    _buffer = std::make_unique<byte[]>(_bufferSize);
    _backupLimit = reinterpret_cast<wchar_t*>(_buffer.get());
    _bufPtr = reinterpret_cast<wchar_t*>(_buffer.get());

    // Initialize the user's buffer to spaces. This is done so that
    // moving in the buffer via cursor doesn't do strange things.
    std::fill_n(_bufPtr, _bufferSize / sizeof(wchar_t), UNICODE_SPACE);

    if (!initialData.empty())
    {
        memcpy_s(_bufPtr, _bufferSize, initialData.data(), initialData.size());

        _bytesRead += initialData.size();

        const auto cchInitialData = initialData.size() / sizeof(wchar_t);
        VisibleCharCount() = cchInitialData;
        _bufPtr += cchInitialData;
        _currentPosition = cchInitialData;

        OriginalCursorPosition() = screenInfo.GetTextBuffer().GetCursor().GetPosition();
        OriginalCursorPosition().x -= (til::CoordType)_currentPosition;

        const auto sScreenBufferSizeX = screenInfo.GetBufferSize().Width();
        while (OriginalCursorPosition().x < 0)
        {
            OriginalCursorPosition().x += sScreenBufferSizeX;
            OriginalCursorPosition().y -= 1;
        }
    }

    // TODO MSFT:11285829 find a better way to manage the lifetime of this object in relation to gci
}

// Routine Description:
// - Destructs a read data class.
// - Decrements count of readers waiting on the given handle.
COOKED_READ_DATA::~COOKED_READ_DATA()
{
    CommandLine::Instance().EndAllPopups();
}

std::span<wchar_t> COOKED_READ_DATA::SpanWholeBuffer()
{
    return std::span{ _backupLimit, (_bufferSize / sizeof(wchar_t)) };
}

std::span<wchar_t> COOKED_READ_DATA::SpanAtPointer()
{
    auto wholeSpan = SpanWholeBuffer();
    return wholeSpan.subspan(_bufPtr - _backupLimit);
}

bool COOKED_READ_DATA::HasHistory() const noexcept
{
    return _commandHistory != nullptr;
}

CommandHistory& COOKED_READ_DATA::History() noexcept
{
    return *_commandHistory;
}

const size_t& COOKED_READ_DATA::VisibleCharCount() const noexcept
{
    return _visibleCharCount;
}

size_t& COOKED_READ_DATA::VisibleCharCount() noexcept
{
    return _visibleCharCount;
}

SCREEN_INFORMATION& COOKED_READ_DATA::ScreenInfo() noexcept
{
    return _screenInfo;
}

til::point COOKED_READ_DATA::OriginalCursorPosition() const noexcept
{
    return _originalCursorPosition;
}

til::point& COOKED_READ_DATA::OriginalCursorPosition() noexcept
{
    return _originalCursorPosition;
}

til::point& COOKED_READ_DATA::BeforeDialogCursorPosition() noexcept
{
    return _beforeDialogCursorPosition;
}

bool COOKED_READ_DATA::IsEchoInput() const noexcept
{
    return _echoInput;
}

bool COOKED_READ_DATA::IsInsertMode() const noexcept
{
    return _insertMode;
}

void COOKED_READ_DATA::SetInsertMode(const bool mode) noexcept
{
    _insertMode = mode;
}

bool COOKED_READ_DATA::IsUnicode() const noexcept
{
    return _unicode;
}

// Routine Description:
// - gets the size of the user buffer
// Return Value:
// - the size of the user buffer in bytes
size_t COOKED_READ_DATA::UserBufferSize() const noexcept
{
    return _userBufferSize;
}

// Routine Description:
// - gets a pointer to the beginning of the prompt storage
// Return Value:
// - pointer to the first char in the internal prompt storage array
wchar_t* COOKED_READ_DATA::BufferStartPtr() noexcept
{
    return _backupLimit;
}

// Routine Description:
// - gets a pointer to where the next char will be inserted into the prompt storage
// Return Value:
// - pointer to the current insertion point of the prompt storage array
wchar_t* COOKED_READ_DATA::BufferCurrentPtr() noexcept
{
    return _bufPtr;
}

// Routine Description:
// - Set the location of the next char insert into the prompt storage to be at
// ptr. ptr must point into a valid portion of the internal prompt storage array
// Arguments:
// - ptr - the new char insertion location
void COOKED_READ_DATA::SetBufferCurrentPtr(wchar_t* ptr) noexcept
{
    _bufPtr = ptr;
}

// Routine Description:
// - gets the number of bytes read so far into the prompt buffer
// Return Value:
// - the number of bytes read
const size_t& COOKED_READ_DATA::BytesRead() const noexcept
{
    return _bytesRead;
}

// Routine Description:
// - gets the number of bytes read so far into the prompt buffer
// Return Value:
// - the number of bytes read
size_t& COOKED_READ_DATA::BytesRead() noexcept
{
    return _bytesRead;
}

// Routine Description:
// - gets the index for the current insertion point of the prompt
// Return Value:
// - the index of the current insertion point
const size_t& COOKED_READ_DATA::InsertionPoint() const noexcept
{
    return _currentPosition;
}

// Routine Description:
// - gets the index for the current insertion point of the prompt
// Return Value:
// - the index of the current insertion point
size_t& COOKED_READ_DATA::InsertionPoint() noexcept
{
    return _currentPosition;
}

// Routine Description:
// - sets the number of bytes that will be reported when this read block completes its read
// Arguments:
// - count - the number of bytes to report
void COOKED_READ_DATA::SetReportedByteCount(const size_t count) noexcept
{
    FAIL_FAST_IF_NULL(_pdwNumBytes);
    *_pdwNumBytes = count;
}

// Routine Description:
// - resets the prompt to be as if it was erased
void COOKED_READ_DATA::Erase() noexcept
{
    _bufPtr = _backupLimit;
    _bytesRead = 0;
    _currentPosition = 0;
    _visibleCharCount = 0;
}

// Routine Description:
// - This routine is called to complete a cooked read that blocked in ReadInputBuffer.
// - The context of the read was saved in the CookedReadData structure.
// - This routine is called when events have been written to the input buffer.
// - It is called in the context of the writing thread.
// - It may be called more than once.
// Arguments:
// - TerminationReason - if this routine is called because a ctrl-c or ctrl-break was seen, this argument
//                      contains CtrlC or CtrlBreak. If the owning thread is exiting, it will have ThreadDying. Otherwise 0.
// - fIsUnicode - Whether to convert the final data to A (using Console Input CP) at the end or treat everything as Unicode (UCS-2)
// - pReplyStatus - The status code to return to the client application that originally called the API (before it was queued to wait)
// - pNumBytes - The number of bytes of data that the server/driver will need to transmit back to the client process
// - pControlKeyState - For certain types of reads, this specifies which modifier keys were held.
// - pOutputData - not used
// Return Value:
// - true if the wait is done and result buffer/status code can be sent back to the client.
// - false if we need to continue to wait until more data is available.
bool COOKED_READ_DATA::Notify(const WaitTerminationReason TerminationReason,
                              const bool fIsUnicode,
                              _Out_ NTSTATUS* const pReplyStatus,
                              _Out_ size_t* const pNumBytes,
                              _Out_ DWORD* const pControlKeyState,
                              _Out_ void* const /*pOutputData*/)
{
    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();

    // this routine should be called by a thread owning the same
    // lock on the same console as we're reading from.
    FAIL_FAST_IF(!gci.IsConsoleLocked());

    *pNumBytes = 0;
    *pControlKeyState = 0;

    *pReplyStatus = STATUS_SUCCESS;

    FAIL_FAST_IF(_pInputReadHandleData->IsInputPending());

    // this routine should be called by a thread owning the same lock on the same console as we're reading from.
    FAIL_FAST_IF(_pInputReadHandleData->GetReadCount() == 0);

    // if ctrl-c or ctrl-break was seen, terminate read.
    if (WI_IsAnyFlagSet(TerminationReason, (WaitTerminationReason::CtrlC | WaitTerminationReason::CtrlBreak)))
    {
        *pReplyStatus = STATUS_ALERTED;
        gci.SetCookedReadData(nullptr);
        return true;
    }

    // See if we were called because the thread that owns this wait block is exiting.
    if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying))
    {
        *pReplyStatus = STATUS_THREAD_IS_TERMINATING;
        gci.SetCookedReadData(nullptr);
        return true;
    }

    // We must see if we were woken up because the handle is being closed. If
    // so, we decrement the read count. If it goes to zero, we wake up the
    // close thread. Otherwise, we wake up any other thread waiting for data.

    if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing))
    {
        *pReplyStatus = STATUS_ALERTED;
        gci.SetCookedReadData(nullptr);
        return true;
    }

    // If we get to here, this routine was called either by the input thread
    // or a write routine. Both of these callers grab the current console
    // lock.

    // MSFT:13994975 This is REALLY weird.
    // When we're doing cooked reading for popups, we come through this method
    //   twice. Once when we press F7 to bring up the popup, then again when we
    //   press enter to input the selected command.
    // The first time, there is no popup, and we go to CookedRead. We pass into
    //   CookedRead `pNumBytes`, which is passed to us as the address of the
    //   stack variable dwNumBytes, in ConsoleWaitBlock::Notify.
    // CookedRead sets this->_pdwNumBytes to that value, and starts the popup,
    //   which returns all the way up, and pops the ConsoleWaitBlock::Notify
    //   stack frame containing the address we're pointing at.
    // Then on the second time  through this function, we hit this if block,
    //   because there is a popup to get input from.
    // However, pNumBytes is now the address of a different stack frame, and not
    //   necessarily the same as before (presumably not at all). The
    //   Callback would try and write the number of bytes read to the
    //   value in _pdwNumBytes, and then we'd return up to ConsoleWaitBlock::Notify,
    //   who's dwNumBytes had nothing in it.
    // To fix this, when we hit this with a popup, we're going to make sure to
    //   refresh the value of _pdwNumBytes to the current address we want to put
    //   the out value into.
    // It's still really weird, but limits the potential fallout of changing a
    //   piece of old spaghetti code.
    if (_commandHistory)
    {
        if (CommandLine::Instance().HasPopup())
        {
            // (see above comment, MSFT:13994975)
            // Make sure that the popup writes the dwNumBytes to the right place
            if (pNumBytes)
            {
                _pdwNumBytes = pNumBytes;
            }

            auto& popup = CommandLine::Instance().GetPopup();
            *pReplyStatus = popup.Process(*this);
            if (*pReplyStatus == CONSOLE_STATUS_READ_COMPLETE ||
                (*pReplyStatus != CONSOLE_STATUS_WAIT && *pReplyStatus != CONSOLE_STATUS_WAIT_NO_BLOCK))
            {
                *pReplyStatus = S_OK;
                gci.SetCookedReadData(nullptr);
                return true;
            }
            return false;
        }
    }

    *pReplyStatus = Read(fIsUnicode, *pNumBytes, *pControlKeyState);
    if (*pReplyStatus != CONSOLE_STATUS_WAIT)
    {
        gci.SetCookedReadData(nullptr);
        return true;
    }
    else
    {
        return false;
    }
}

bool COOKED_READ_DATA::AtEol() const noexcept
{
    return _bytesRead == (_currentPosition * 2);
}

// Routine Description:
// - Method that actually retrieves a character/input record from the buffer (key press form)
//   and determines the next action based on the various possible cooked read modes.
// - Mode options include the F-keys popup menus, keyboard manipulation of the edit line, etc.
// - This method also does the actual copying of the final manipulated data into the return buffer.
// Arguments:
// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done.
// - numBytes - On in, the number of bytes available in the client
// buffer. On out, the number of bytes consumed in the client buffer.
// - controlKeyState - For some types of reads, this is the modifier key state with the last button press.
[[nodiscard]] HRESULT COOKED_READ_DATA::Read(const bool isUnicode,
                                             size_t& numBytes,
                                             ULONG& controlKeyState) noexcept
{
    controlKeyState = 0;

    auto Status = _readCharInputLoop(isUnicode, numBytes);

    // if the read was completed (status != wait), free the cooked read
    // data.  also, close the temporary output handle that was opened to
    // echo the characters read.
    if (Status != CONSOLE_STATUS_WAIT)
    {
        Status = _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState);
    }

    return Status;
}

void COOKED_READ_DATA::ProcessAliases(DWORD& lineCount)
{
    Alias::s_MatchAndCopyAliasLegacy(_backupLimit,
                                     _bytesRead,
                                     _backupLimit,
                                     _bufferSize,
                                     _bytesRead,
                                     _exeName,
                                     lineCount);
}

// Routine Description:
// - This method handles the various actions that occur on the edit line like pressing keys left/right/up/down, paging, and
//   the final ENTER key press that will end the wait and finally return the data.
// Arguments:
// - pCookedReadData - Pointer to cooked read data information (edit line, client buffer, etc.)
// - wch - The most recently pressed/retrieved character from the input buffer (keystroke)
// - keyState - Modifier keys/state information with the pressed key/character
// - status - The return code to pass to the client
// Return Value:
// - true if read is completed. false if we need to keep waiting and be called again with the user's next keystroke.
bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig,
                                    const DWORD keyState,
                                    NTSTATUS& status)
{
    const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
    size_t NumSpaces = 0;
    til::CoordType ScrollY = 0;
    size_t NumToWrite;
    auto wch = wchOrig;
    bool fStartFromDelim;

    status = STATUS_SUCCESS;
    if (_bytesRead >= (_bufferSize - (2 * sizeof(WCHAR))) && wch != UNICODE_CARRIAGERETURN && wch != UNICODE_BACKSPACE)
    {
        return false;
    }

    if (_ctrlWakeupMask != 0 && wch < L' ' && (_ctrlWakeupMask & (1 << wch)))
    {
        *_bufPtr = wch;
        _bytesRead += sizeof(WCHAR);
        _bufPtr += 1;
        _currentPosition += 1;
        _controlKeyState = keyState;
        return true;
    }

    if (wch == EXTKEY_ERASE_PREV_WORD)
    {
        wch = UNICODE_BACKSPACE;
    }

    if (AtEol())
    {
        // If at end of line, processing is relatively simple. Just store the character and write it to the screen.
        if (wch == UNICODE_BACKSPACE2)
        {
            wch = UNICODE_BACKSPACE;
        }

        if (wch != UNICODE_BACKSPACE || _bufPtr != _backupLimit)
        {
            fStartFromDelim = IsWordDelim(_bufPtr[-1]);

            auto loop = true;
            while (loop)
            {
                loop = false;
                if (_echoInput)
                {
                    NumToWrite = sizeof(WCHAR);
                    status = WriteCharsLegacy(_screenInfo,
                                              _backupLimit,
                                              _bufPtr,
                                              &wch,
                                              &NumToWrite,
                                              &NumSpaces,
                                              _originalCursorPosition.x,
                                              WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
                                              &ScrollY);
                    if (SUCCEEDED_NTSTATUS(status))
                    {
                        _originalCursorPosition.y += ScrollY;
                    }
                    else
                    {
                        RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status);
                    }
                }

                _visibleCharCount += NumSpaces;
                if (wch == UNICODE_BACKSPACE && _processedInput)
                {
                    _bytesRead -= sizeof(WCHAR);
                    // clang-format off
#pragma prefast(suppress: __WARNING_POTENTIAL_BUFFER_OVERFLOW_HIGH_PRIORITY, "This access is fine")
                    // clang-format on
                    *_bufPtr = (WCHAR)' ';
                    _bufPtr -= 1;
                    _currentPosition -= 1;

                    // Repeat until it hits the word boundary
                    if (wchOrig == EXTKEY_ERASE_PREV_WORD &&
                        _bufPtr != _backupLimit &&
                        fStartFromDelim ^ !IsWordDelim(_bufPtr[-1]))
                    {
                        loop = true;
                    }
                }
                else
                {
                    *_bufPtr = wch;
                    _bytesRead += sizeof(WCHAR);
                    _bufPtr += 1;
                    _currentPosition += 1;
                }
            }
        }
    }
    else
    {
        auto CallWrite = true;
        const auto sScreenBufferSizeX = _screenInfo.GetBufferSize().Width();

        // processing in the middle of the line is more complex:

        // calculate new cursor position
        // store new char
        // clear the current command line from the screen
        // write the new command line to the screen
        // update the cursor position

        if (wch == UNICODE_BACKSPACE && _processedInput)
        {
            // for backspace, use writechars to calculate the new cursor position.
            // this call also sets the cursor to the right position for the
            // second call to writechars.

            if (_bufPtr != _backupLimit)
            {
                fStartFromDelim = IsWordDelim(_bufPtr[-1]);

                auto loop = true;
                while (loop)
                {
                    loop = false;
                    // we call writechar here so that cursor position gets updated
                    // correctly.  we also call it later if we're not at eol so
                    // that the remainder of the string can be updated correctly.

                    if (_echoInput)
                    {
                        NumToWrite = sizeof(WCHAR);
                        status = WriteCharsLegacy(_screenInfo,
                                                  _backupLimit,
                                                  _bufPtr,
                                                  &wch,
                                                  &NumToWrite,
                                                  nullptr,
                                                  _originalCursorPosition.x,
                                                  WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
                                                  nullptr);
                        if (FAILED_NTSTATUS(status))
                        {
                            RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status);
                        }
                    }
                    _bytesRead -= sizeof(WCHAR);
                    _bufPtr -= 1;
                    _currentPosition -= 1;
                    memmove(_bufPtr,
                            _bufPtr + 1,
                            _bytesRead - (_currentPosition * sizeof(WCHAR)));
                    {
                        auto buf = (PWCHAR)((PBYTE)_backupLimit + _bytesRead);
                        *buf = (WCHAR)' ';
                    }
                    NumSpaces = 0;

                    // Repeat until it hits the word boundary
                    if (wchOrig == EXTKEY_ERASE_PREV_WORD &&
                        _bufPtr != _backupLimit &&
                        fStartFromDelim ^ !IsWordDelim(_bufPtr[-1]))
                    {
                        loop = true;
                    }
                }
            }
            else
            {
                CallWrite = false;
            }
        }
        else
        {
            // store the char
            if (wch == UNICODE_CARRIAGERETURN)
            {
                _bufPtr = (PWCHAR)((PBYTE)_backupLimit + _bytesRead);
                *_bufPtr = wch;
                _bufPtr += 1;
                _bytesRead += sizeof(WCHAR);
                _currentPosition += 1;
            }
            else
            {
                auto fBisect = false;

                if (_echoInput)
                {
                    if (CheckBisectProcessW(_screenInfo,
                                            _backupLimit,
                                            _currentPosition + 1,
                                            sScreenBufferSizeX - _originalCursorPosition.x,
                                            _originalCursorPosition.x,
                                            TRUE))
                    {
                        fBisect = true;
                    }
                }

                if (_insertMode)
                {
                    memmove(_bufPtr + 1,
                            _bufPtr,
                            _bytesRead - (_currentPosition * sizeof(WCHAR)));
                    _bytesRead += sizeof(WCHAR);
                }
                *_bufPtr = wch;
                _bufPtr += 1;
                _currentPosition += 1;

                // calculate new cursor position
                if (_echoInput)
                {
                    NumSpaces = RetrieveNumberOfSpaces(_originalCursorPosition.x,
                                                       _backupLimit,
                                                       _currentPosition - 1);
                    if (NumSpaces > 0 && fBisect)
                        NumSpaces--;
                }
            }
        }

        if (_echoInput && CallWrite)
        {
            til::point CursorPosition;

            // save cursor position
            CursorPosition = _screenInfo.GetTextBuffer().GetCursor().GetPosition();
            CursorPosition.x = (til::CoordType)(CursorPosition.x + NumSpaces);

            // clear the current command line from the screen
            // clang-format off
#pragma prefast(suppress: __WARNING_BUFFER_OVERFLOW, "Not sure why prefast doesn't like this call.")
            // clang-format on
            DeleteCommandLine(*this, FALSE);

            // write the new command line to the screen
            NumToWrite = _bytesRead;

            DWORD dwFlags = WC_DESTRUCTIVE_BACKSPACE | WC_PRINTABLE_CONTROL_CHARS;
            if (wch == UNICODE_CARRIAGERETURN)
            {
                dwFlags |= WC_KEEP_CURSOR_VISIBLE;
            }
            status = WriteCharsLegacy(_screenInfo,
                                      _backupLimit,
                                      _backupLimit,
                                      _backupLimit,
                                      &NumToWrite,
                                      &_visibleCharCount,
                                      _originalCursorPosition.x,
                                      dwFlags,
                                      &ScrollY);
            if (FAILED_NTSTATUS(status))
            {
                RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status);
                _bytesRead = 0;
                return true;
            }

            // update cursor position
            if (wch != UNICODE_CARRIAGERETURN)
            {
                if (CheckBisectProcessW(_screenInfo,
                                        _backupLimit,
                                        _currentPosition + 1,
                                        sScreenBufferSizeX - _originalCursorPosition.x,
                                        _originalCursorPosition.x,
                                        TRUE))
                {
                    if (CursorPosition.x == (sScreenBufferSizeX - 1))
                    {
                        CursorPosition.x++;
                    }
                }

                // adjust cursor position for WriteChars
                _originalCursorPosition.y += ScrollY;
                CursorPosition.y += ScrollY;
                status = AdjustCursorPosition(_screenInfo, CursorPosition, TRUE, nullptr);
                if (FAILED_NTSTATUS(status))
                {
                    _bytesRead = 0;
                    return true;
                }
            }
        }
    }

    // in cooked mode, enter (carriage return) is converted to
    // carriage return linefeed (0xda).  carriage return is always
    // stored at the end of the buffer.
    if (wch == UNICODE_CARRIAGERETURN)
    {
        if (_processedInput)
        {
            if (_bytesRead < _bufferSize)
            {
                *_bufPtr = UNICODE_LINEFEED;
                if (_echoInput)
                {
                    NumToWrite = sizeof(WCHAR);
                    status = WriteCharsLegacy(_screenInfo,
                                              _backupLimit,
                                              _bufPtr,
                                              _bufPtr,
                                              &NumToWrite,
                                              nullptr,
                                              _originalCursorPosition.x,
                                              WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
                                              nullptr);
                    if (FAILED_NTSTATUS(status))
                    {
                        RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status);
                    }
                }
                _bytesRead += sizeof(WCHAR);
                _bufPtr++;
                _currentPosition += 1;
            }
        }
        // reset the cursor back to 25% if necessary
        if (_lineInput)
        {
            if (_insertMode != gci.GetInsertMode())
            {
                // Make cursor small.
                LOG_IF_FAILED(CommandLine::Instance().ProcessCommandLine(*this, VK_INSERT, 0));
            }

            status = STATUS_SUCCESS;
            return true;
        }
    }

    return false;
}

// Routine Description:
// - Writes string to current position in prompt line. can overwrite text to the right of the cursor.
// Arguments:
// - wstr - the string to write
// Return Value:
// - The number of chars written
size_t COOKED_READ_DATA::Write(const std::wstring_view wstr)
{
    auto end = wstr.end();
    const auto charsRemaining = (_bufferSize / sizeof(wchar_t)) - (_bufPtr - _backupLimit);
    if (wstr.size() > charsRemaining)
    {
        end = std::next(wstr.begin(), charsRemaining);
    }

    std::copy(wstr.begin(), end, _bufPtr);
    const size_t charsInserted = end - wstr.begin();
    auto bytesInserted = charsInserted * sizeof(wchar_t);
    _currentPosition += charsInserted;
    _bytesRead += bytesInserted;

    if (IsEchoInput())
    {
        size_t NumSpaces = 0;
        til::CoordType ScrollY = 0;

        FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(ScreenInfo(),
                                                      _backupLimit,
                                                      _bufPtr,
                                                      _bufPtr,
                                                      &bytesInserted,
                                                      &NumSpaces,
                                                      OriginalCursorPosition().x,
                                                      WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
                                                      &ScrollY));
        OriginalCursorPosition().y += ScrollY;
        VisibleCharCount() += NumSpaces;
    }
    _bufPtr += charsInserted;

    return charsInserted;
}

// Routine Description:
// - saves data in the prompt buffer to the outgoing user buffer
// Arguments:
// - cch - the number of chars to write to the user buffer
// Return Value:
// - the number of bytes written to the user buffer
size_t COOKED_READ_DATA::SavePromptToUserBuffer(const size_t cch)
{
    size_t bytesToWrite = 0;
    const auto hr = SizeTMult(cch, sizeof(wchar_t), &bytesToWrite);
    if (FAILED(hr))
    {
        return 0;
    }

    memmove(_userBuffer, _backupLimit, bytesToWrite);

    if (!IsUnicode())
    {
        try
        {
            const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
            const auto wstr = ConvertToW(gci.CP, { _userBuffer, cch });
            const auto copyAmount = std::min(wstr.size() * sizeof(wchar_t), _userBufferSize);
            std::copy_n(reinterpret_cast<const char*>(wstr.data()), copyAmount, _userBuffer);
            return copyAmount;
        }
        CATCH_LOG();
    }
    return bytesToWrite;
}

// Routine Description:
// - saves data in the prompt buffer as pending input
// Arguments:
// - index - the index of what wchar to start the saving
// - multiline - whether the pending input should be saved as multiline or not
void COOKED_READ_DATA::SavePendingInput(const size_t index, const bool multiline)
{
    auto& inputReadHandleData = *GetInputReadHandleData();
    const std::wstring_view pending{ _backupLimit + index,
                                     BytesRead() / sizeof(wchar_t) - index };
    if (multiline)
    {
        inputReadHandleData.SaveMultilinePendingInput(pending);
    }
    else
    {
        inputReadHandleData.SavePendingInput(pending);
    }
}

// Routine Description:
// - saves data in the prompt buffer as pending input
// Arguments:
// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done.
// - numBytes - On in, the number of bytes available in the client
// buffer. On out, the number of bytes consumed in the client buffer.
// Return Value:
// - Status code that indicates success, wait, etc.
[[nodiscard]] NTSTATUS COOKED_READ_DATA::_readCharInputLoop(const bool isUnicode, size_t& numBytes) noexcept
{
    auto Status = STATUS_SUCCESS;

    while (_bytesRead < _bufferSize)
    {
        auto wch = UNICODE_NULL;
        auto commandLineEditingKeys = false;
        DWORD keyState = 0;

        // This call to GetChar may block.
        Status = GetChar(_pInputBuffer,
                         &wch,
                         true,
                         &commandLineEditingKeys,
                         nullptr,
                         &keyState);
        if (FAILED_NTSTATUS(Status))
        {
            if (Status != CONSOLE_STATUS_WAIT)
            {
                _bytesRead = 0;
            }
            break;
        }

        // we should probably set these up in GetChars, but we set them
        // up here because the debugger is multi-threaded and calls
        // read before outputting the prompt.

        if (_originalCursorPosition.x == -1)
        {
            _originalCursorPosition = _screenInfo.GetTextBuffer().GetCursor().GetPosition();
        }

        if (commandLineEditingKeys)
        {
            // TODO: this is super weird for command line popups only
            _unicode = isUnicode;

            _pdwNumBytes = &numBytes;

            Status = CommandLine::Instance().ProcessCommandLine(*this, wch, keyState);
            if (Status == CONSOLE_STATUS_READ_COMPLETE || Status == CONSOLE_STATUS_WAIT)
            {
                break;
            }
            if (FAILED_NTSTATUS(Status))
            {
                if (Status == CONSOLE_STATUS_WAIT_NO_BLOCK)
                {
                    Status = CONSOLE_STATUS_WAIT;
                }
                else
                {
                    _bytesRead = 0;
                }
                break;
            }
        }
        else
        {
            if (ProcessInput(wch, keyState, Status))
            {
                auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
                gci.Flags |= CONSOLE_IGNORE_NEXT_KEYUP;
                break;
            }
        }
    }
    return Status;
}

// Routine Description:
// - handles any tasks that need to be completed after the read input loop finishes
// Arguments:
// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done.
// - numBytes - On in, the number of bytes available in the client
// buffer. On out, the number of bytes consumed in the client buffer.
// - controlKeyState - For some types of reads, this is the modifier key state with the last button press.
// Return Value:
// - Status code that indicates success, out of memory, etc.
[[nodiscard]] NTSTATUS COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept
{
    std::span writer{ _userBuffer, _userBufferSize };
    std::wstring_view input{ _backupLimit, _bytesRead / sizeof(wchar_t) };
    DWORD LineCount = 1;

    if (_echoInput)
    {
        const auto idx = input.find(UNICODE_CARRIAGERETURN);
        if (idx != decltype(input)::npos)
        {
            if (_commandHistory)
            {
                auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
                LOG_IF_FAILED(_commandHistory->Add({ _backupLimit, idx }, WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP)));
            }

            Tracing::s_TraceCookedRead(_clientProcess, _backupLimit, base::saturated_cast<ULONG>(idx));

            // Don't be fooled by ProcessAliases only taking one argument. It rewrites multiple
            // class members on return, including `_bytesRead`, requiring us to reconstruct `input`.
            ProcessAliases(LineCount);
            input = { _backupLimit, _bytesRead / sizeof(wchar_t) };

            // The exact reasons for this are unclear to me (the one writing this comment), but this code used to
            // split the contents of a multiline alias (for instance `doskey test=echo foo$Techo bar$Techo baz`)
            // into multiple separate read outputs, ensuring that the client receives them line by line.
            //
            // This code first truncates the `input` to only contain the first line, so that Consume() below only
            // writes that line into the user buffer. We'll later store the remainder in SaveMultilinePendingInput().
            if (LineCount > 1)
            {
                // ProcessAliases() is supposed to end each line with \r\n. If it doesn't we might as well fail-fast.
                const auto firstLineEnd = input.find(UNICODE_LINEFEED) + 1;
                input = input.substr(0, std::min(input.size(), firstLineEnd));
            }
        }
    }

    const auto inputSizeBefore = input.size();
    GetInputBuffer()->Consume(isUnicode, input, writer);

    if (LineCount > 1)
    {
        // This is a continuation of the above identical if condition.
        // We've truncated the `input` slice and now we need to restore it.
        const auto inputSizeAfter = input.size();
        const auto amountConsumed = inputSizeBefore - inputSizeAfter;
        input = { _backupLimit, _bytesRead / sizeof(wchar_t) };
        input = input.substr(std::min(input.size(), amountConsumed));
        GetInputReadHandleData()->SaveMultilinePendingInput(input);
    }
    else if (!input.empty())
    {
        GetInputReadHandleData()->SavePendingInput(input);
    }

    numBytes = _userBufferSize - writer.size();
    controlKeyState = _controlKeyState;
    return STATUS_SUCCESS;
}

void COOKED_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer)
{
    // See the comment in WaitBlock.cpp for more information.
    if (_userBuffer == oldBuffer)
    {
        _userBuffer = static_cast<char*>(newBuffer);
    }
}
