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

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Management.Automation.Internal;
using System.Management.Automation.Runspaces;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;

using Dbg = System.Management.Automation.Diagnostics;
#if !UNIX
using ConsoleHandle = Microsoft.Win32.SafeHandles.SafeFileHandle;
#endif

namespace Microsoft.PowerShell
{
    using PowerShell = System.Management.Automation.PowerShell;

    /// <summary>
    /// ConsoleHostUserInterface implements console-mode user interface for powershell.
    /// </summary>
    [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")]
    internal partial class ConsoleHostUserInterface : System.Management.Automation.Host.PSHostUserInterface
    {
        /// <summary>
        /// This is the char that is echoed to the console when the input is masked. This not localizable.
        /// </summary>
        private const char PrintToken = '*';

        /// <summary>
        /// Command completion implementation object.
        /// </summary>
        private PowerShell _commandCompletionPowerShell;

        /// <summary>
        /// This is a test hook for programmatically reading and writing ConsoleHost I/O.
        /// </summary>
        private static PSHostUserInterface s_h = null;

        /// <summary>
        /// Return true if the console supports a VT100 like virtual terminal.
        /// </summary>
        public override bool SupportsVirtualTerminal { get; }

        /// <summary>
        /// Constructs an instance.
        /// </summary>
        /// <param name="parent"></param>
        /// <exception/>
        internal ConsoleHostUserInterface(ConsoleHost parent)
        {
            Dbg.Assert(parent != null, "parent may not be null");

            _parent = parent;
            _rawui = new ConsoleHostRawUserInterface(this);

#if UNIX
            SupportsVirtualTerminal = true;
#else
            try
            {
                // Turn on virtual terminal if possible.

                // This might throw - not sure how exactly (no console), but if it does, we shouldn't fail to start.
                var handle = ConsoleControl.GetActiveScreenBufferHandle();
                var m = ConsoleControl.GetMode(handle);
                if (ConsoleControl.NativeMethods.SetConsoleMode(handle.DangerousGetHandle(), (uint)(m | ConsoleControl.ConsoleModes.VirtualTerminal)))
                {
                    // We only know if vt100 is supported if the previous call actually set the new flag, older
                    // systems ignore the setting.
                    m = ConsoleControl.GetMode(handle);
                    this.SupportsVirtualTerminal = (m & ConsoleControl.ConsoleModes.VirtualTerminal) != 0;
                }
            }
            catch
            {
            }
#endif

            _isInteractiveTestToolListening = false;
        }

        /// <summary>
        /// Supplies an implementation of PSHostRawUserInterface that provides low-level console mode UI facilities.
        /// </summary>
        /// <value></value>
        /// <exception/>
        public override PSHostRawUserInterface RawUI
        {
            get
            {
                Dbg.Assert(_rawui != null, "rawui should have been created by ctor");

                // no locking because this is read-only, and allocated in the ctor.

                return _rawui;
            }
        }

        // deadcode; but could be needed in the future.
        ///// <summary>
        ///// gets the PSHost instance that uses this ConsoleHostUserInterface instance
        ///// </summary>
        ///// <value></value>
        ///// <exception/>

        // internal
        // PSHost
        // Parent
        // {
        //    get
        //    {
        //        using (tracer.TraceProperty())
        //        {
        //            // no locking because this is read-only and set in the ctor.

        //            return parent;
        //        }
        //    }
        // }

        /// <summary>
        /// True if command completion is currently running.
        /// </summary>
        internal bool IsCommandCompletionRunning
        {
            get
            {
                return _commandCompletionPowerShell != null &&
                       _commandCompletionPowerShell.InvocationStateInfo.State == PSInvocationState.Running;
            }
        }

        /// <summary>
        /// True if the Read* functions should read from the stdin stream instead of from the win32 console.
        /// </summary>
        internal bool ReadFromStdin { get; set; }

        /// <summary>
        /// True if the host shouldn't write out prompts.
        /// </summary>
        internal bool NoPrompt { get; set; }

        #region Line-oriented interaction

        /// <summary>
        /// See base class.
        /// </summary>
        /// <returns></returns>
        /// <exception cref="HostException">
        /// If Win32's SetConsoleMode fails
        ///    OR
        ///    Win32's ReadConsole fails
        ///    OR
        ///    obtaining information about the buffer failed
        ///    OR
        ///    Win32's SetConsoleCursorPosition failed
        /// </exception>
        public override string ReadLine()
        {
            HandleThrowOnReadAndPrompt();

            // call our internal version such that it does not end input on a tab
            ReadLineResult unused;

            return ReadLine(false, string.Empty, out unused, true, true);
        }

        /// <summary>
        /// See base class.
        /// </summary>
        /// <returns></returns>
        /// <exception cref="HostException">
        /// If obtaining a handle to the active screen buffer failed
        ///    OR
        ///    Win32's setting input buffer mode to disregard window and mouse input failed
        ///    OR
        ///    Win32's ReadConsole failed
        /// </exception>
        /// <exception cref="PipelineStoppedException">
        /// If Ctrl-C is entered by user
        /// </exception>
        public override SecureString ReadLineAsSecureString()
        {
            HandleThrowOnReadAndPrompt();

            // we lock here so that multiple threads won't interleave the various reads and writes here.

            object result = null;
            lock (_instanceLock)
            {
                result = ReadLineSafe(true, PrintToken);
            }

            SecureString secureResult = result as SecureString;
            System.Management.Automation.Diagnostics.Assert(secureResult != null, "ReadLineSafe did not return a SecureString");

            return secureResult;
        }

        /// <summary>
        /// Implementation based on NT CredUI's GetPasswdStr.
        /// Use Win32.ReadConsole to construct a SecureString. The advantage of ReadConsole over ReadKey is
        /// Alt-ddd where d is {0-9} is allowed.
        /// It also manages the cursor as keys are entered and "backspaced". However, it is possible that
        /// while this method is running, the console buffer contents could change. Then, its cursor mgmt
        /// will likely be messed up.
        ///
        /// Secondary implementation for Unix based on Console.ReadKey(), where
        /// the advantage is portability through abstraction. Does not support
        /// arrow key movement, but supports backspace.
        /// </summary>
        ///<param name="isSecureString">
        /// True to specify reading a SecureString; false reading a string
        /// </param>
        /// <param name="printToken">
        /// string for output echo
        /// </param>
        /// <returns></returns>
        /// <exception cref="HostException">
        /// If obtaining a handle to the active screen buffer failed
        ///    OR
        ///    Win32's setting input buffer mode to disregard window and mouse input failed
        ///    OR
        ///    Win32's ReadConsole failed
        ///    OR
        ///    obtaining information about the buffer failed
        ///    OR
        ///    Win32's SetConsoleCursorPosition failed
        /// </exception>
        /// <exception cref="PipelineStoppedException">
        /// If Ctrl-C is entered by user
        /// </exception>
        private object ReadLineSafe(bool isSecureString, char? printToken)
        {
            // Don't lock (instanceLock) in here -- the caller needs to do that...

            PreRead();
            string printTokenString = printToken.HasValue ?
                printToken.ToString() :
                null;
            SecureString secureResult = new SecureString();
            StringBuilder result = new StringBuilder();
#if UNIX
            bool treatControlCAsInput = Console.TreatControlCAsInput;
#else
            ConsoleHandle handle = ConsoleControl.GetConioDeviceHandle();
            ConsoleControl.ConsoleModes originalMode = ConsoleControl.GetMode(handle);
            bool isModeChanged = true; // assume ConsoleMode is changed so that if ReadLineSetMode
            // fails to return the value correctly, the original mode is
            // restored.
#endif

            try
            {
#if UNIX
                Console.TreatControlCAsInput = true;
#else
                // Ensure that we're in the proper line-input mode.

                const ConsoleControl.ConsoleModes DesiredMode =
                    ConsoleControl.ConsoleModes.Extended |
                    ConsoleControl.ConsoleModes.QuickEdit;

                ConsoleControl.ConsoleModes m = originalMode;
                bool shouldUnsetEchoInput = shouldUnsetMode(ConsoleControl.ConsoleModes.EchoInput, ref m);
                bool shouldUnsetLineInput = shouldUnsetMode(ConsoleControl.ConsoleModes.LineInput, ref m);
                bool shouldUnsetMouseInput = shouldUnsetMode(ConsoleControl.ConsoleModes.MouseInput, ref m);
                bool shouldUnsetProcessInput = shouldUnsetMode(ConsoleControl.ConsoleModes.ProcessedInput, ref m);

                if ((m & DesiredMode) != DesiredMode ||
                    shouldUnsetMouseInput ||
                    shouldUnsetEchoInput ||
                    shouldUnsetLineInput ||
                    shouldUnsetProcessInput)
                {
                    m |= DesiredMode;
                    ConsoleControl.SetMode(handle, m);
                }
                else
                {
                    isModeChanged = false;
                }

                _rawui.ClearKeyCache();
#endif

                Coordinates originalCursorPos = _rawui.CursorPosition;

                while (true)
                {
                    //
                    // read one char at a time so that we don't
                    // end up having a immutable string holding the
                    // secret in memory.
                    //
#if UNIX
                    ConsoleKeyInfo keyInfo = Console.ReadKey(true);
#else
                    const int CharactersToRead = 1;
#pragma warning disable CA2014
                    Span<char> inputBuffer = stackalloc char[CharactersToRead + 1];
#pragma warning restore CA2014
                    string key = ConsoleControl.ReadConsole(handle, initialContentLength: 0, inputBuffer, charactersToRead: CharactersToRead, endOnTab: false, out _);
#endif

#if UNIX
                    // Handle Ctrl-C ending input
                    if (keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
#else
                    if (string.IsNullOrEmpty(key) || (char)3 == key[0])
#endif
                    {
                        PipelineStoppedException e = new PipelineStoppedException();
                        throw e;
                    }
#if UNIX
                    if (keyInfo.Key == ConsoleKey.Enter)
#else
                    if ((char)13 == key[0])
#endif
                    {
                        //
                        // we are done if user presses ENTER key
                        //
                        break;
                    }
#if UNIX
                    if (keyInfo.Key == ConsoleKey.Backspace)
#else
                    if ((char)8 == key[0])
#endif
                    {
                        //
                        // for backspace, remove last char appended
                        //
                        if (isSecureString && secureResult.Length > 0)
                        {
                            secureResult.RemoveAt(secureResult.Length - 1);
                            WriteBackSpace(originalCursorPos);
                        }
                        else if (result.Length > 0)
                        {
                            result.Remove(result.Length - 1, 1);
                            WriteBackSpace(originalCursorPos);
                        }
                    }
#if UNIX
                    else if (char.IsControl(keyInfo.KeyChar))
                    {
                        // deny list control characters
                        continue;
                    }
#endif
                    else
                    {
                        //
                        // append the char to our string
                        //
                        if (isSecureString)
                        {
#if UNIX
                            secureResult.AppendChar(keyInfo.KeyChar);
#else
                            secureResult.AppendChar(key[0]);
#endif
                        }
                        else
                        {
#if UNIX
                            result.Append(keyInfo.KeyChar);
#else
                            result.Append(key);
#endif
                        }

                        if (!string.IsNullOrEmpty(printTokenString))
                        {
                            WritePrintToken(printTokenString, ref originalCursorPos);
                        }
                    }
                }
            }
#if UNIX
            catch (InvalidOperationException)
            {
                // ReadKey() failed so we stop
                throw new PipelineStoppedException();
            }
#endif
            finally
            {
#if UNIX
                Console.TreatControlCAsInput = treatControlCAsInput;
#else
                if (isModeChanged)
                {
                    ConsoleControl.SetMode(handle, originalMode);
                }
#endif
            }

            WriteLineToConsole();
            PostRead(result.ToString());
            if (isSecureString)
            {
                return secureResult;
            }
            else
            {
                return result;
            }
        }

        /// <summary>
        /// Handle writing print token with proper cursor adjustment for ReadLineSafe.
        /// </summary>
        /// <param name="printToken">
        /// token output for each char input. It must be a one-char string
        /// </param>
        /// <param name="originalCursorPosition">
        /// it is the cursor position where ReadLineSafe begins
        /// </param>
        /// <exception cref="HostException">
        /// If obtaining information about the buffer failed
        ///    OR
        ///    Win32's SetConsoleCursorPosition failed
        /// </exception>
        private void WritePrintToken(
            string printToken,
            ref Coordinates originalCursorPosition)
        {
            Dbg.Assert(!string.IsNullOrEmpty(printToken),
                "Calling WritePrintToken with printToken being null or empty");
            Dbg.Assert(printToken.Length == 1,
                "Calling WritePrintToken with printToken's Length being " + printToken.Length);
            Size consoleBufferSize = _rawui.BufferSize;
            Coordinates currentCursorPosition = _rawui.CursorPosition;

            // if the cursor is currently at the lower right corner, this write will cause the screen buffer to
            // scroll up. So, it is necessary to adjust the original cursor position one row up.
            if (currentCursorPosition.Y >= consoleBufferSize.Height - 1 && // last row
                currentCursorPosition.X >= consoleBufferSize.Width - 1)  // last column
            {
                if (originalCursorPosition.Y > 0)
                {
                    originalCursorPosition.Y--;
                }
            }

            WriteToConsole(printToken, false);
        }

        /// <summary>
        /// Handle backspace with proper cursor adjustment for ReadLineSafe.
        /// </summary>
        /// <param name="originalCursorPosition">
        /// it is the cursor position where ReadLineSafe begins
        /// </param>
        /// <exception cref="HostException">
        /// If obtaining information about the buffer failed
        ///    OR
        ///    Win32's SetConsoleCursorPosition failed
        /// </exception>
        private void WriteBackSpace(Coordinates originalCursorPosition)
        {
            Coordinates cursorPosition = _rawui.CursorPosition;
            if (cursorPosition == originalCursorPosition)
            {
                // at originalCursorPosition, don't move
                return;
            }

            if (cursorPosition.X == 0)
            {
                if (cursorPosition.Y <= originalCursorPosition.Y)
                {
                    return;
                }
                // BufferSize.Width is 1 larger than cursor position
                cursorPosition.X = _rawui.BufferSize.Width - 1;
                cursorPosition.Y--;
                BlankAtCursor(cursorPosition);
            }
            else if (cursorPosition.X > 0)
            {
                cursorPosition.X--;
                BlankAtCursor(cursorPosition);
            }
            // do nothing if cursorPosition.X is left of screen
        }

        /// <summary>
        /// Blank out at and move rawui.CursorPosition to <paramref name="cursorPosition"/>
        /// </summary>
        /// <param name="cursorPosition">Position to blank out.</param>
        private void BlankAtCursor(Coordinates cursorPosition)
        {
            _rawui.CursorPosition = cursorPosition;
            WriteToConsole(" ", true);
            _rawui.CursorPosition = cursorPosition;
        }

#if !UNIX
        /// <summary>
        /// If <paramref name="m"/> is set on <paramref name="flagToUnset"/>, unset it and return true;
        /// otherwise return false.
        /// </summary>
        /// <param name="flagToUnset">
        /// a flag in ConsoleControl.ConsoleModes to be unset in <paramref name="m"/>
        /// </param>
        /// <param name="m">
        /// </param>
        /// <returns>
        /// true if <paramref name="m"/> is set on <paramref name="flagToUnset"/>
        /// false otherwise
        /// </returns>
        private static bool shouldUnsetMode(
            ConsoleControl.ConsoleModes flagToUnset,
            ref ConsoleControl.ConsoleModes m)
        {
            if ((m & flagToUnset) > 0)
            {
                m &= ~flagToUnset;
                return true;
            }

            return false;
        }
#endif

        #region WriteToConsole

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal void WriteToConsole(char c, bool transcribeResult)
        {
            ReadOnlySpan<char> value = stackalloc char[1] { c };
            WriteToConsole(value, transcribeResult);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal void WriteToConsole(ReadOnlySpan<char> value, bool transcribeResult)
        {
            WriteToConsole(value, transcribeResult, newLine: false);
        }

        private void WriteToConsole(ReadOnlySpan<char> value, bool transcribeResult, bool newLine)
        {
#if !UNIX
            ConsoleHandle handle = ConsoleControl.GetActiveScreenBufferHandle();

            // Ensure that we're in the proper line-output mode.  We don't lock here as it does not matter if we
            // attempt to set the mode from multiple threads at once.
            ConsoleControl.ConsoleModes m = ConsoleControl.GetMode(handle);

            const ConsoleControl.ConsoleModes DesiredMode =
                ConsoleControl.ConsoleModes.ProcessedOutput
                | ConsoleControl.ConsoleModes.WrapEndOfLine;

            if ((m & DesiredMode) != DesiredMode)
            {
                m |= DesiredMode;
                ConsoleControl.SetMode(handle, m);
            }
#endif

            PreWrite();

            // This is atomic, so we don't lock here...
#if !UNIX
            ConsoleControl.WriteConsole(handle, value, newLine);
#else
            ConsoleOutWriteHelper(value, newLine);
#endif

            if (_isInteractiveTestToolListening && Console.IsOutputRedirected)
            {
                ConsoleOutWriteHelper(value, newLine);
            }

            if (transcribeResult)
            {
                PostWrite(value, newLine);
            }
            else
            {
                PostWrite();
            }
        }

        private void WriteToConsole(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string text, bool newLine = false)
        {
            // Sync access so that we don't conflict on color settings if called from multiple threads.
            lock (_instanceLock)
            {
                ConsoleColor fg = RawUI.ForegroundColor;
                ConsoleColor bg = RawUI.BackgroundColor;

                RawUI.ForegroundColor = foregroundColor;
                RawUI.BackgroundColor = backgroundColor;

                try
                {
                    WriteToConsole(text, transcribeResult: true, newLine);
                }
                finally
                {
                    RawUI.ForegroundColor = fg;
                    RawUI.BackgroundColor = bg;
                }
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void ConsoleOutWriteHelper(ReadOnlySpan<char> value, bool newLine)
        {
            if (newLine)
            {
                Console.Out.WriteLine(value);
            }
            else
            {
                Console.Out.Write(value);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal void WriteLineToConsole(ReadOnlySpan<char> value, bool transcribeResult)
        {
            WriteToConsole(value, transcribeResult, newLine: true);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void WriteLineToConsole(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string text)
        {
            WriteToConsole(foregroundColor, backgroundColor, text, newLine: true);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void WriteLineToConsole(string text)
        {
            WriteLineToConsole(text, transcribeResult: true);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void WriteLineToConsole()
        {
            WriteToConsole(Environment.NewLine, transcribeResult: true, newLine: false);
        }

        #endregion WriteToConsole

        /// <summary>
        /// See base class.
        /// </summary>
        /// <param name="value"></param>
        /// <exception cref="HostException">
        /// If Win32's CreateFile fails
        ///    OR
        ///    Win32's GetConsoleMode fails
        ///    OR
        ///    Win32's SetConsoleMode fails
        ///    OR
        ///    Win32's WriteConsole fails
        /// </exception>
        public override void Write(string value)
        {
            WriteImpl(value, newLine: false);
        }

        private void WriteImpl(string value, bool newLine)
        {
            if (string.IsNullOrEmpty(value) && !newLine)
            {
                return;
            }

            // If the test hook is set, write to it and continue.
            if (s_h != null)
            {
                if (newLine)
                {
                    s_h.WriteLine(value);
                }
                else
                {
                    s_h.Write(value);
                }
            }

            TextWriter writer = Console.IsOutputRedirected ? Console.Out : _parent.ConsoleTextWriter;

            if (_parent.IsRunningAsync)
            {
                Dbg.Assert(writer == _parent.OutputSerializer.textWriter, "writers should be the same");

                _parent.OutputSerializer.Serialize(value);

                if (newLine)
                {
                    _parent.OutputSerializer.Serialize(Environment.NewLine);
                }
            }
            else
            {
                if (newLine)
                {
                    writer.WriteLine(value);
                }
                else
                {
                    writer.Write(value);
                }
            }
        }

        /// <summary>
        /// See base class.
        /// </summary>
        /// <param name="foregroundColor"></param>
        /// <param name="backgroundColor"></param>
        /// <param name="value"></param>
        /// <exception cref="HostException">
        /// If obtaining information about the buffer failed
        ///    OR
        ///    Win32's SetConsoleTextAttribute
        ///    OR
        ///    Win32's CreateFile fails
        ///    OR
        ///    Win32's GetConsoleMode fails
        ///    OR
        ///    Win32's SetConsoleMode fails
        ///    OR
        ///    Win32's WriteConsole fails
        /// </exception>
        public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
        {
            Write(foregroundColor, backgroundColor, value, newLine: false);
        }

        /// <summary>
        /// See base class.
        /// </summary>
        /// <param name="foregroundColor"></param>
        /// <param name="backgroundColor"></param>
        /// <param name="value"></param>
        /// <exception cref="HostException">
        /// If obtaining information about the buffer failed
        ///    OR
        ///    Win32's SetConsoleTextAttribute
        ///    OR
        ///    Win32's CreateFile fails
        ///    OR
        ///    Win32's GetConsoleMode fails
        ///    OR
        ///    Win32's SetConsoleMode fails
        ///    OR
        ///    Win32's WriteConsole fails
        /// </exception>
        public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
        {
            Write(foregroundColor, backgroundColor, value, newLine: true);
        }

        private void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value, bool newLine)
        {
            // Sync access so that we don't conflict on color settings if called from multiple threads.
            lock (_instanceLock)
            {
                ConsoleColor fg = RawUI.ForegroundColor;
                ConsoleColor bg = RawUI.BackgroundColor;

                RawUI.ForegroundColor = foregroundColor;
                RawUI.BackgroundColor = backgroundColor;

                try
                {
                    this.WriteImpl(value, newLine);
                }
                finally
                {
                    RawUI.ForegroundColor = fg;
                    RawUI.BackgroundColor = bg;
                }
            }
        }

        /// <summary>
        /// See base class.
        /// </summary>
        /// <param name="value"></param>
        /// <exception cref="HostException">
        ///    Win32's CreateFile fails
        ///    OR
        ///    Win32's GetConsoleMode fails
        ///    OR
        ///    Win32's SetConsoleMode fails
        ///    OR
        ///    Win32's WriteConsole fails
        /// </exception>
        public override void WriteLine(string value)
        {
            this.WriteImpl(value, newLine: true);
        }

        /// <summary>
        /// See base class.
        /// </summary>
        /// <exception cref="HostException">
        ///    Win32's CreateFile fails
        ///    OR
        ///    Win32's GetConsoleMode fails
        ///    OR
        ///    Win32's SetConsoleMode fails
        ///    OR
        ///    Win32's WriteConsole fails
        /// </exception>
        public override void WriteLine()
        {
            this.WriteImpl(Environment.NewLine, newLine: false);
        }

        #region Word Wrapping

        /// <summary>
        /// This is a poor-man's word-wrapping routine.  It breaks a single string into segments small enough to fit within a
        /// given number of cells.  A break is determined by the last occurrence of whitespace that allows all prior characters
        /// on a line to be written within a given number of cells.  If there is no whitespace found within that span, then the
        /// largest span that will fit in the bounds is used.
        ///
        /// The problem is complicated by the fact that a single character may consume more than one cell.  Conceptually, this
        /// is the same case as placing an upper bound on the length of a line while also having a strlen function that
        /// arbitrarily considers the length of any single character to be 1 or greater.
        /// </summary>
        /// <param name="text">
        /// Text to be emitted.
        /// Each tab character in the text is replaced with a space in the results.
        /// </param>
        /// <param name="maxWidthInBufferCells">
        /// Max width, in buffer cells, of a single line.  Note that a single character may consume more than one cell.  The
        /// number of cells consumed is determined by calling ConsoleHostRawUserInterface.LengthInBufferCells.
        /// </param>
        /// <returns>
        /// A list of strings representing the text broken into "lines" each of which are guaranteed not to exceed
        /// maxWidthInBufferCells.
        /// </returns>
        internal List<string> WrapText(string text, int maxWidthInBufferCells)
        {
            List<string> result = new List<string>();

            List<Word> words = ChopTextIntoWords(text, maxWidthInBufferCells);
            if (words.Count < 1)
            {
                return result;
            }

            IEnumerator<Word> e = words.GetEnumerator();
            bool valid = false;
            int cellCounter = 0;
            StringBuilder line = new StringBuilder();
            string l = null;

            do
            {
                valid = e.MoveNext();
                if (!valid)
                {
                    if (line.Length > 0)
                    {
                        l = line.ToString();
                        Dbg.Assert(RawUI.LengthInBufferCells(l) <= maxWidthInBufferCells, "line is too long");
                        result.Add(l);
                    }

                    break;
                }

                if ((e.Current.Flags & WordFlags.IsNewline) > 0)
                {
                    l = line.ToString();
                    Dbg.Assert(RawUI.LengthInBufferCells(l) <= maxWidthInBufferCells, "line is too long");
                    result.Add(l);

                    // skip the newline "words"

                    line = new StringBuilder();
                    cellCounter = 0;
                    continue;
                }

                // will the word fit?

                if (cellCounter + e.Current.CellCount <= maxWidthInBufferCells)
                {
                    // yes, add it to the line.

                    line.Append(e.Current.Text);
                    cellCounter += e.Current.CellCount;
                }
                else
                {
                    // no: too long.  Either start a new line, or pick off as much whitespace as we need.

                    if ((e.Current.Flags & WordFlags.IsWhitespace) == 0)
                    {
                        l = line.ToString();
                        Dbg.Assert(RawUI.LengthInBufferCells(l) <= maxWidthInBufferCells, "line is too long");
                        result.Add(l);

                        line = new StringBuilder(e.Current.Text);
                        cellCounter = e.Current.CellCount;
                        continue;
                    }

                    // chop the whitespace into bits.

                    int w = maxWidthInBufferCells - cellCounter;
                    Dbg.Assert(w < e.Current.CellCount, "width remaining should be less than size of word");

                    line.Append(e.Current.Text.Substring(0, w));

                    l = line.ToString();
                    Dbg.Assert(RawUI.LengthInBufferCells(l) == maxWidthInBufferCells, "line should exactly fit");
                    result.Add(l);

                    string remaining = e.Current.Text.Substring(w);
                    line = new StringBuilder(remaining);
                    cellCounter = RawUI.LengthInBufferCells(remaining);
                }
            } while (valid);

            return result;
        }

        /// <summary>
        /// Struct used by WrapText.
        /// </summary>
        [Flags]
        internal enum WordFlags
        {
            IsWhitespace = 0x01,
            IsNewline = 0x02
        }

        internal struct Word
        {
            internal int CellCount;
            internal string Text;
            internal WordFlags Flags;
        }

        /// <summary>
        /// Chops text into "words," where a word is defined to be a sequence of whitespace characters, or a sequence of
        /// non-whitespace characters, each sequence being no longer than a given maximum.  Therefore, in the text "this is a
        /// string" there are 7 words: 4 sequences of non-whitespace characters and 3 sequences of whitespace characters.
        ///
        /// Whitespace is considered to be spaces or tabs.  Each tab character is replaced with a single space.
        /// </summary>
        /// <param name="text">
        /// The text to be chopped up.
        /// </param>
        /// <param name="maxWidthInBufferCells">
        /// The maximum number of buffer cells that each word may consume.
        /// </param>
        /// <returns>
        /// A list of words, in the same order they appear in the source text.
        /// </returns>
        /// <remarks>
        /// This can be made faster by, instead of creating little strings for each word, creating indices of the start and end
        /// range of a word.  That would reduce the string allocations.
        /// </remarks>
        internal List<Word> ChopTextIntoWords(string text, int maxWidthInBufferCells)
        {
            List<Word> result = new List<Word>();

            if (string.IsNullOrEmpty(text))
            {
                return result;
            }

            if (maxWidthInBufferCells < 1)
            {
                return result;
            }

            text = text.Replace('\t', ' ');

            result = new List<Word>();

            // a "word" is a span of characters delimited by whitespace.  Contiguous whitespace, too, is a word.

            int startIndex = 0;
            int wordEnd = 0;
            bool inWs = false;

            while (wordEnd < text.Length)
            {
                if (text[wordEnd] == '\n')
                {
                    if (startIndex < wordEnd)
                    {
                        // the span up to this point needs to be saved off

                        AddWord(text, startIndex, wordEnd, maxWidthInBufferCells, inWs, ref result);
                    }

                    // add a nl word

                    Word w = new Word();
                    w.Flags = WordFlags.IsNewline;
                    result.Add(w);

                    // skip the nl

                    ++wordEnd;
                    startIndex = wordEnd;

                    inWs = false;
                    continue;
                }
                else if (text[wordEnd] == ' ')
                {
                    if (!inWs)
                    {
                        // span from startIndex..(wordEnd - 1) is a word

                        AddWord(text, startIndex, wordEnd, maxWidthInBufferCells, inWs, ref result);
                        startIndex = wordEnd;
                    }

                    inWs = true;
                }
                else
                {
                    // not whitespace

                    if (inWs)
                    {
                        AddWord(text, startIndex, wordEnd, maxWidthInBufferCells, inWs, ref result);
                        startIndex = wordEnd;
                    }

                    inWs = false;
                }

                ++wordEnd;
            }

            if (startIndex != wordEnd)
            {
                AddWord(text, startIndex, text.Length, maxWidthInBufferCells, inWs, ref result);
            }

            return result;
        }

        /// <summary>
        /// Helper for ChopTextIntoWords.  Takes a span of characters in a string and adds it to the word list, further
        /// subdividing the span as needed so that each subdivision fits within the limit.
        /// </summary>
        /// <param name="text">
        /// The string of characters in which the span is to be extracted.
        /// </param>
        /// <param name="startIndex">
        /// index into text of the start of the word to be added.
        /// </param>
        /// <param name="endIndex">
        /// index of the char after the last char to be included in the word.
        /// </param>
        /// <param name="maxWidthInBufferCells">
        /// The maximum number of buffer cells that each word may consume.
        /// </param>
        /// <param name="isWhitespace">
        /// true if the span is whitespace, false if not.
        /// </param>
        /// <param name="result">
        /// The list into which the words will be added.
        /// </param>
        internal void AddWord(string text, int startIndex, int endIndex,
            int maxWidthInBufferCells, bool isWhitespace, ref List<Word> result)
        {
            Dbg.Assert(endIndex >= startIndex, "startIndex must be before endIndex");
            Dbg.Assert(endIndex >= 0, "endIndex must be positive");
            Dbg.Assert(startIndex >= 0, "startIndex must be positive");
            Dbg.Assert(startIndex < text.Length, "startIndex must be within the string");
            Dbg.Assert(endIndex <= text.Length, "endIndex must be within the string");

            while (startIndex < endIndex)
            {
                int i = Math.Min(endIndex, startIndex + maxWidthInBufferCells);
                Word w = new Word();
                if (isWhitespace)
                {
                    w.Flags = WordFlags.IsWhitespace;
                }

                while (true)
                {
                    w.Text = text.Substring(startIndex, i - startIndex);
                    w.CellCount = RawUI.LengthInBufferCells(w.Text);
                    if (w.CellCount <= maxWidthInBufferCells)
                    {
                        // the segment from start..i fits

                        break;
                    }
                    else
                    {
                        // The segment does not fit, back off a tad until it does

                        --i;
                    }
                }

                Dbg.Assert(RawUI.LengthInBufferCells(w.Text) <= maxWidthInBufferCells, "word should not exceed max");
                result.Add(w);

                startIndex = i;
            }
        }

        internal string WrapToCurrentWindowWidth(string text)
        {
            StringBuilder sb = new StringBuilder();

            // we leave a 1-cell margin on the end because if the very last character butts up against the
            // edge of the screen buffer, then the console will wrap the line.

            List<string> lines = WrapText(text, RawUI.WindowSize.Width - 1);
            int count = 0;
            foreach (string s in lines)
            {
                sb.Append(s);
                if (++count != lines.Count)
                {
                    sb.Append(Environment.NewLine);
                }
            }

            return sb.ToString();
        }

        #endregion Word Wrapping

        /// <summary>
        /// See base class.
        /// </summary>
        /// <param name="message"></param>
        /// <exception cref="HostException">
        /// If obtaining information about the buffer failed
        ///    OR
        ///    Win32's SetConsoleTextAttribute
        ///    OR
        ///    Win32's CreateFile fails
        ///    OR
        ///    Win32's GetConsoleMode fails
        ///    OR
        ///    Win32's SetConsoleMode fails
        ///    OR
        ///    Win32's WriteConsole fails
        /// </exception>
        public override void WriteDebugLine(string message)
        {
            // don't lock here as WriteLine is already protected.
            bool unused;
            message = HostUtilities.RemoveGuidFromMessage(message, out unused);

            // We should write debug to error stream only if debug is redirected.)
            if (_parent.ErrorFormat == Serialization.DataFormat.XML)
            {
                _parent.ErrorSerializer.Serialize(message, "debug");
            }
            else
            {
                // NTRAID#Windows OS Bugs-1061752-2004/12/15-sburns should read a skin setting here...
                WriteLine(
                    DebugForegroundColor,
                    DebugBackgroundColor,
                    StringUtil.Format(ConsoleHostUserInterfaceStrings.DebugFormatString, message));
            }
        }

        /// <summary>
        /// See base class.
        /// </summary>
        /// <param name="record"></param>
        public override void WriteInformation(InformationRecord record)
        {
            // We should write information to error stream only if redirected.)
            if (_parent.ErrorFormat == Serialization.DataFormat.XML)
            {
                _parent.ErrorSerializer.Serialize(record, "information");
            }
            else
            {
                // Do nothing. The information stream is not visible by default
            }
        }

        /// <summary>
        /// See base class.
        /// </summary>
        /// <param name="message"></param>
        /// <exception cref="HostException">
        /// If obtaining information about the buffer failed
        ///    OR
        ///    Win32's SetConsoleTextAttribute
        ///    OR
        ///    Win32's CreateFile fails
        ///    OR
        ///    Win32's GetConsoleMode fails
        ///    OR
        ///    Win32's SetConsoleMode fails
        ///    OR
        ///    Win32's WriteConsole fails
        /// </exception>
        public override void WriteVerboseLine(string message)
        {
            // don't lock here as WriteLine is already protected.
            bool unused;
            message = HostUtilities.RemoveGuidFromMessage(message, out unused);

            // NTRAID#Windows OS Bugs-1061752-2004/12/15-sburns should read a skin setting here...)
            if (_parent.ErrorFormat == Serialization.DataFormat.XML)
            {
                _parent.ErrorSerializer.Serialize(message, "verbose");
            }
            else
            {
                WriteLine(
                    VerboseForegroundColor,
                    VerboseBackgroundColor,
                    StringUtil.Format(ConsoleHostUserInterfaceStrings.VerboseFormatString, message));
            }
        }

        /// <summary>
        /// See base class.
        /// </summary>
        /// <param name="message"></param>
        /// <exception cref="HostException">
        /// If obtaining information about the buffer failed
        ///    OR
        ///    Win32's SetConsoleTextAttribute
        ///    OR
        ///    Win32's CreateFile fails
        ///    OR
        ///    Win32's GetConsoleMode fails
        ///    OR
        ///    Win32's SetConsoleMode fails
        ///    OR
        ///    Win32's WriteConsole fails
        /// </exception>
        public override void WriteWarningLine(string message)
        {
            // don't lock here as WriteLine is already protected.
            bool unused;
            message = HostUtilities.RemoveGuidFromMessage(message, out unused);

            // NTRAID#Windows OS Bugs-1061752-2004/12/15-sburns should read a skin setting here...)
            if (_parent.ErrorFormat == Serialization.DataFormat.XML)
            {
                _parent.ErrorSerializer.Serialize(message, "warning");
            }
            else
            {
                WriteLine(
                    WarningForegroundColor,
                    WarningBackgroundColor,
                    StringUtil.Format(ConsoleHostUserInterfaceStrings.WarningFormatString, message));
            }
        }

        /// <summary>
        /// Invoked by CommandBase.WriteProgress to display a progress record.
        /// </summary>
        public override void WriteProgress(Int64 sourceId, ProgressRecord record)
        {
            if (record == null)
            {
                Dbg.Assert(false, "WriteProgress called with null ProgressRecord");
            }
            else
            {
                bool matchPattern;
                string currentOperation = HostUtilities.RemoveIdentifierInfoFromMessage(record.CurrentOperation, out matchPattern);
                if (matchPattern)
                {
                    record = new ProgressRecord(record) { CurrentOperation = currentOperation };
                }

                // We allow only one thread at a time to update the progress state.)
                if (_parent.ErrorFormat == Serialization.DataFormat.XML)
                {
                    PSObject obj = new PSObject();
                    obj.Properties.Add(new PSNoteProperty("SourceId", sourceId));
                    obj.Properties.Add(new PSNoteProperty("Record", record));
                    _parent.ErrorSerializer.Serialize(obj, "progress");
                }
                else
                {
                    lock (_instanceLock)
                    {
                        HandleIncomingProgressRecord(sourceId, record);
                    }
                }
            }
        }

        public override void WriteErrorLine(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                return;
            }

            TextWriter writer = (!Console.IsErrorRedirected || _parent.IsInteractive)
                ? _parent.ConsoleTextWriter
                : Console.Error;

            if (_parent.ErrorFormat == Serialization.DataFormat.XML)
            {
                Dbg.Assert(writer == _parent.ErrorSerializer.textWriter, "writers should be the same");

                _parent.ErrorSerializer.Serialize(value + Environment.NewLine);
            }
            else
            {
                if (writer == _parent.ConsoleTextWriter)
                    WriteLine(ErrorForegroundColor, ErrorBackgroundColor, value);
                else
                    Console.Error.WriteLine(value);
            }
        }

        // Format colors
        public ConsoleColor FormatAccentColor { get; set; } = ConsoleColor.Green;

        // Error colors
        public ConsoleColor ErrorAccentColor { get; set; } = ConsoleColor.Cyan;
        public ConsoleColor ErrorForegroundColor { get; set; } = ConsoleColor.Red;
        public ConsoleColor ErrorBackgroundColor { get; set; } = Console.BackgroundColor;

        // Warning colors
        public ConsoleColor WarningForegroundColor { get; set; } = ConsoleColor.Yellow;
        public ConsoleColor WarningBackgroundColor { get; set; } = Console.BackgroundColor;

        // Debug colors
        public ConsoleColor DebugForegroundColor { get; set; } = ConsoleColor.Yellow;
        public ConsoleColor DebugBackgroundColor { get; set; } = Console.BackgroundColor;

        // Verbose colors
        public ConsoleColor VerboseForegroundColor { get; set; } = ConsoleColor.Yellow;
        public ConsoleColor VerboseBackgroundColor { get; set; } = Console.BackgroundColor;

        // Progress colors
        public ConsoleColor ProgressForegroundColor { get; set; } = ConsoleColor.Black;
        public ConsoleColor ProgressBackgroundColor { get; set; } = ConsoleColor.Yellow;

        #endregion Line-oriented interaction

        #region implementation

        // We use System.Environment.NewLine because we are platform-agnostic

        internal static readonly string Crlf = System.Environment.NewLine;

        private const string Tab = "\x0009";

        internal enum ReadLineResult
        {
            endedOnEnter = 0,
            endedOnTab = 1,
            endedOnShiftTab = 2,
            endedOnBreak = 3
        }

        private const int MaxInputLineLength = 1024;

        /// <summary>
        /// Reads a line of input from the console.  Returns when the user hits enter, a break key, a break event occurs.  In
        /// the case that stdin has been redirected, reads from the stdin stream instead of the console.
        /// </summary>
        /// <param name="endOnTab">
        /// true to end input when the user hits the tab or shift-tab keys, false to only end on the enter key (or a break
        /// event). Ignored if not reading from the console device.
        /// </param>
        /// <param name="initialContent">
        /// The initial contents of the input buffer.  Nice if you want to have a default result. Ignored if not reading from the
        /// console device.
        /// </param>
        /// <param name="result">
        /// Receives an enum value indicating how input was ended.
        /// </param>
        /// <param name="calledFromPipeline">
        /// TBD
        /// </param>
        /// <param name="transcribeResult">
        /// true to include the results in any transcription that might be happening.
        /// </param>
        /// <returns>
        /// The string read from either the console or the stdin stream.  null if:
        /// - stdin was read and EOF was reached on the stream, or
        /// - the console was read, and input was terminated with Ctrl-C, Ctrl-Break, or Close.
        /// </returns>
        /// <exception cref="HostException">
        /// If Win32's SetConsoleMode fails
        ///    OR
        ///    Win32's ReadConsole fails
        ///    OR
        ///    obtaining information about the buffer failed
        ///    OR
        ///    Win32's SetConsoleCursorPosition failed
        /// </exception>
        internal string ReadLine(bool endOnTab, string initialContent, out ReadLineResult result, bool calledFromPipeline, bool transcribeResult)
        {
            result = ReadLineResult.endedOnEnter;

            // If the test hook is set, read from it.
            if (s_h != null) return s_h.ReadLine();

            string restOfLine = null;

            string s = ReadFromStdin
                ? ReadLineFromFile(initialContent)
                : ReadLineFromConsole(endOnTab, initialContent, calledFromPipeline, ref restOfLine, ref result);

            if (transcribeResult)
            {
                PostRead(s);
            }
            else
            {
                PostRead();
            }

            if (restOfLine != null)
                s += restOfLine;

            return s;
        }

        private string ReadLineFromFile(string initialContent)
        {
            var sb = new StringBuilder();
            if (!string.IsNullOrEmpty(initialContent))
            {
                sb.Append(initialContent);
                sb.Append('\n');
            }

            var consoleIn = _parent.ConsoleIn.Value;
            while (true)
            {
                var inC = consoleIn.Read();
                if (inC == -1)
                {
                    // EOF - we return null which tells our caller to exit
                    // but only if we don't have any input, we could have
                    // input and then stdin was closed, but never saw a newline.
                    return sb.Length == 0 ? null : sb.ToString();
                }

                var c = unchecked((char)inC);
                if (!NoPrompt) Console.Out.Write(c);

                if (c == '\r')
                {
                    // Treat as newline, but consume \n if there is one.
                    if (consoleIn.Peek() == '\n')
                    {
                        if (!NoPrompt) Console.Out.Write('\n');
                        consoleIn.Read();
                    }

                    break;
                }

                if (c == '\n')
                {
                    break;
                }

                // If NoPrompt is true, we are in a sort of server mode where we shouldn't
                // do anything like edit the command line - every character is part of the input.
                if (c == '\b' && !NoPrompt)
                {
                    sb.Remove(sb.Length - 1, 1);
                }
                else
                {
                    sb.Append(c);
                }
            }

            return sb.ToString();
        }

        private string ReadLineFromConsole(bool endOnTab, string initialContent, bool calledFromPipeline, ref string restOfLine, ref ReadLineResult result)
        {
            PreRead();
            // Ensure that we're in the proper line-input mode.

#if !UNIX
            ConsoleHandle handle = ConsoleControl.GetConioDeviceHandle();
            ConsoleControl.ConsoleModes m = ConsoleControl.GetMode(handle);

            const ConsoleControl.ConsoleModes DesiredMode =
                ConsoleControl.ConsoleModes.LineInput
                | ConsoleControl.ConsoleModes.EchoInput
                | ConsoleControl.ConsoleModes.ProcessedInput;

            if ((m & DesiredMode) != DesiredMode || (m & ConsoleControl.ConsoleModes.MouseInput) > 0)
            {
                m &= ~ConsoleControl.ConsoleModes.MouseInput;
                m |= DesiredMode;
                ConsoleControl.SetMode(handle, m);
            }
#endif

            // If more characters are typed than you asked, then the next call to ReadConsole will return the
            // additional characters beyond those you requested.
            //
            // If input is terminated with a tab key, then the buffer returned will have a tab (ascii 0x9) at the
            // position where the tab key was hit.  If the user has arrowed backward over existing input in the line
            // buffer, the tab will overwrite whatever character was in that position. That character will be lost in
            // the input buffer, but since we echo each character the user types, it's still in the active screen buffer
            // and we can read the console output to get that character.
            //
            // If input is terminated with an enter key, then the buffer returned will have ascii 0x0D and 0x0A
            // (Carriage Return and Line Feed) as the last two characters of the buffer.
            //
            // If input is terminated with a break key (Ctrl-C, Ctrl-Break, Close, etc.), then the buffer will be
            // the empty string.

#if UNIX
            // For Unix systems, we implement a basic readline loop around Console.ReadKey(), that
            // supports backspace, arrow keys, Ctrl-C, and Ctrl-D. This readline is only used for
            // interactive prompts (like Read-Host), otherwise it is assumed that PSReadLine is
            // available. Therefore this explicitly does not support history or tab completion.

            bool treatControlCAsInput = Console.TreatControlCAsInput;

            try
            {

                ConsoleKeyInfo keyInfo;
                string s = string.Empty;
                int index = 0;
                int cursorLeft = Console.CursorLeft;
                int cursorCurrent = cursorLeft;
                bool insertMode = true;
                Console.TreatControlCAsInput = true;
#else
            _rawui.ClearKeyCache();
            uint keyState = 0;
            string s = string.Empty;
            Span<char> inputBuffer = stackalloc char[MaxInputLineLength + 1];
            if (initialContent.Length > 0)
            {
                initialContent.AsSpan().CopyTo(inputBuffer);
            }

#endif
            while (true)
            {
#if UNIX
                    keyInfo = Console.ReadKey(true);
#else
                s += ConsoleControl.ReadConsole(handle, initialContent.Length, inputBuffer, MaxInputLineLength, endOnTab, out keyState);
                Dbg.Assert(s != null, "s should never be null");
#endif

#if UNIX
                    // Handle Ctrl-C ending input
                    if (keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
#else
                if (s.Length == 0)
#endif
                {
                    result = ReadLineResult.endedOnBreak;
                    s = null;

                    if (calledFromPipeline)
                    {
                        // make sure that the pipeline that called us is stopped

                        throw new PipelineStoppedException();
                    }

                    break;
                }

#if UNIX
                    if (keyInfo.Key == ConsoleKey.Enter)
#else
                if (s.EndsWith(Environment.NewLine, StringComparison.Ordinal))
#endif
                {
                    result = ReadLineResult.endedOnEnter;
#if UNIX
                        // We're intercepting characters, so we need to echo the newline
                        Console.Out.WriteLine();
#else
                    s = s.Remove(s.Length - Environment.NewLine.Length);
#endif
                    break;
                }

#if UNIX
                    if (keyInfo.Key == ConsoleKey.Tab)
                    {
                        // This is unsupported
                        continue;
                    }
#else
                int i = s.IndexOf(Tab, StringComparison.Ordinal);

                if (endOnTab && i != -1)
                {
                    // then the tab we found is the completion character.  bit 0x10 is set if the shift key was down
                    // when the key was hit.

                    if ((keyState & 0x10) == 0)
                    {
                        result = ReadLineResult.endedOnTab;
                    }
                    else if ((keyState & 0x10) > 0)
                    {
                        result = ReadLineResult.endedOnShiftTab;
                    }
                    else
                    {
                        // do nothing: leave the result state as it was. This is the circumstance when we've have to
                        // do more than one iteration and the input ended on a tab or shift-tab, or the user hit
                        // enter, or the user hit ctrl-c
                    }

                    // also clean up the screen -- if the cursor was positioned somewhere before the last character
                    // in the input buffer, then the characters from the tab to the end of the buffer need to be
                    // erased.
                    int leftover = RawUI.LengthInBufferCells(s.Substring(i + 1));

                    if (leftover > 0)
                    {
                        Coordinates c = RawUI.CursorPosition;

                        // before cleaning up the screen, read the active screen buffer to retrieve the character that
                        // is overridden by the tab
                        char charUnderCursor = GetCharacterUnderCursor(c);

                        Write(StringUtil.Padding(leftover));
                        RawUI.CursorPosition = c;

                        restOfLine = s[i] + (charUnderCursor + s.Substring(i + 1));
                    }
                    else
                    {
                        restOfLine += s[i];
                    }

                    s = s.Remove(i);

                    break;
                }
#endif
#if UNIX
                    if (keyInfo.Key == ConsoleKey.Backspace)
                    {
                        if (index > 0)
                        {
                            int length = s.Length;
                            s = s.Remove(index - 1, 1);
                            index--;
                            cursorCurrent = Console.CursorLeft;
                            Console.CursorLeft = cursorLeft;
                            Console.Out.Write(s.PadRight(length));
                            Console.CursorLeft = cursorCurrent - 1;
                        }

                        continue;
                    }

                    if (keyInfo.Key == ConsoleKey.Delete
                        || (keyInfo.Key == ConsoleKey.D && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)))
                    {
                        if (index < s.Length)
                        {
                            int length = s.Length;
                            s = s.Remove(index, 1);
                            cursorCurrent = Console.CursorLeft;
                            Console.CursorLeft = cursorLeft;
                            Console.Out.Write(s.PadRight(length));
                            Console.CursorLeft = cursorCurrent;
                        }

                        continue;
                    }

                    if (keyInfo.Key == ConsoleKey.LeftArrow
                        || (keyInfo.Key == ConsoleKey.B && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)))
                    {
                        if (Console.CursorLeft > cursorLeft)
                        {
                            Console.CursorLeft--;
                            index--;
                        }

                        continue;
                    }

                    if (keyInfo.Key == ConsoleKey.RightArrow
                        || (keyInfo.Key == ConsoleKey.F && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)))
                    {
                        if (Console.CursorLeft < cursorLeft + s.Length)
                        {
                            Console.CursorLeft++;
                            index++;
                        }

                        continue;
                    }

                    if (keyInfo.Key == ConsoleKey.UpArrow
                        || keyInfo.Key == ConsoleKey.DownArrow
                        || keyInfo.Key == ConsoleKey.PageUp
                        || keyInfo.Key == ConsoleKey.PageDown)
                    {
                        // Arrow/Page Up/down is unimplemented, so fail gracefully
                        continue;
                    }

                    if (keyInfo.Key == ConsoleKey.Home
                        || (keyInfo.Key == ConsoleKey.A && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)))
                    {
                        Console.CursorLeft = cursorLeft;
                        index = 0;
                        continue;
                    }

                    if (keyInfo.Key == ConsoleKey.End
                        || (keyInfo.Key == ConsoleKey.E && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)))
                    {
                        Console.CursorLeft = cursorLeft + s.Length;
                        index = s.Length;
                        continue;
                    }

                    if (keyInfo.Key == ConsoleKey.Escape)
                    {
                        Console.CursorLeft = cursorLeft;
                        index = s.Length;
                        s = string.Empty;
                        continue;
                    }

                    if (keyInfo.Key == ConsoleKey.Insert)
                    {
                        // Toggle insert/overwrite mode
                        insertMode = !insertMode;
                        continue;
                    }

                    if (char.IsControl(keyInfo.KeyChar))
                    {
                        // deny list control characters
                        continue;
                    }

                    // Handle case where terminal gets reset and the index is outside of the buffer
                    if (index > s.Length)
                    {
                        index = s.Length;
                    }

                    // Modify string
                    if (!insertMode && index < s.Length) // then overwrite mode
                    {
                        s = s.Remove(index, 1);
                    }

                    s = s.Insert(index, keyInfo.KeyChar.ToString());
                    index++;

                    // Redisplay string
                    cursorCurrent = Console.CursorLeft;
                    Console.CursorLeft = cursorLeft;
                    Console.Out.Write(s);
                    Console.CursorLeft = cursorCurrent + 1;
#endif
            }

            Dbg.Assert(
                       (s == null && result == ReadLineResult.endedOnBreak)
                       || (s != null && result != ReadLineResult.endedOnBreak),
                       "s should only be null if input ended with a break");

            return s;
#if UNIX
            }
            finally
            {
                Console.TreatControlCAsInput = treatControlCAsInput;
            }
#endif
        }

#if !UNIX
        /// <summary>
        /// Get the character at the cursor when the user types 'tab' in the middle of line.
        /// </summary>
        /// <param name="cursorPosition">The cursor position where 'tab' is hit.</param>
        /// <returns></returns>
        private char GetCharacterUnderCursor(Coordinates cursorPosition)
        {
            Rectangle region = new Rectangle(0, cursorPosition.Y, RawUI.BufferSize.Width - 1, cursorPosition.Y);
            BufferCell[,] content = RawUI.GetBufferContents(region);

            for (int index = 0, column = 0; column <= cursorPosition.X; index++)
            {
                BufferCell cell = content[0, index];
                if (cell.BufferCellType == BufferCellType.Complete || cell.BufferCellType == BufferCellType.Leading)
                {
                    if (column == cursorPosition.X)
                    {
                        return cell.Character;
                    }

                    column += ConsoleControl.LengthInBufferCells(cell.Character);
                }
            }

            Dbg.Assert(false, "the character at the cursor should be retrieved, never gets to here");
            return '\0';
        }
#endif

        /// <summary>
        /// Strip nulls from a string...
        /// </summary>
        /// <param name="input">The string to process.</param>
        /// <returns>The string with any \0 characters removed...</returns>
        private string RemoveNulls(string input)
        {
            if (input.Contains('\0'))
            {
                return input;
            }

            StringBuilder sb = new StringBuilder();
            foreach (char c in input)
            {
                if (c != '\0')
                    sb.Append(c);
            }

            return sb.ToString();
        }

        /// <summary>
        /// Reads a line, and completes the input for the user if they hit tab.
        /// </summary>
        /// <param name="exec">
        /// The Executor instance on which to run any pipelines that are needed to find matches
        /// </param>
        /// <returns>
        /// null on a break event
        /// the completed line otherwise
        /// </returns>
        internal string ReadLineWithTabCompletion(Executor exec)
        {
            string input = null;
            string lastInput = string.Empty;

            ReadLineResult rlResult = ReadLineResult.endedOnEnter;

#if !UNIX
            ConsoleHandle handle = ConsoleControl.GetActiveScreenBufferHandle();

            string lastCompletion = string.Empty;
            Size screenBufferSize = RawUI.BufferSize;

            // Save the cursor position at the end of the prompt string so that we can restore it later to write the
            // completed input.

            Coordinates endOfPromptCursorPos = RawUI.CursorPosition;

            CommandCompletion commandCompletion = null;
            string completionInput = null;
#endif

            while (true)
            {
                if (TryInvokeUserDefinedReadLine(out input))
                {
                    break;
                }

                input = ReadLine(true, lastInput, out rlResult, false, false);

                if (input == null)
                {
                    break;
                }

                if (rlResult == ReadLineResult.endedOnEnter)
                {
                    break;
                }

#if UNIX // Portable code only ends on enter (or no input), so tab is not processed
                throw new PlatformNotSupportedException("This readline state is unsupported in portable code!");
#else

                Coordinates endOfInputCursorPos = RawUI.CursorPosition;
                string completedInput = null;

                if (rlResult == ReadLineResult.endedOnTab || rlResult == ReadLineResult.endedOnShiftTab)
                {
                    int tabIndex = input.IndexOf(Tab, StringComparison.Ordinal);
                    Dbg.Assert(tabIndex != -1, "tab should appear in the input");

                    string restOfLine = string.Empty;
                    int leftover = input.Length - tabIndex - 1;
                    if (leftover > 0)
                    {
                        // We are reading from the console (not redirected, b/c we don't end on tab when redirected)
                        // If the cursor is at the end of a line, there is actually a space character at the cursor's position and when we type tab
                        // at the end of a line, that space character is replaced by the tab. But when we type tab at the middle of a line, the space
                        // character at the end is preserved, we should remove that space character because it's not provided by the user.
                        input = input.Remove(input.Length - 1);
                        restOfLine = input.Substring(tabIndex + 1);
                    }

                    input = input.Remove(tabIndex);

                    if (input != lastCompletion || commandCompletion == null)
                    {
                        completionInput = input;
                        commandCompletion = GetNewCompletionResults(input);
                    }

                    var completionResult = commandCompletion.GetNextResult(rlResult == ReadLineResult.endedOnTab);
                    if (completionResult != null)
                    {
                        completedInput = completionInput.Substring(0, commandCompletion.ReplacementIndex)
                                         + completionResult.CompletionText;
                    }
                    else
                    {
                        completedInput = completionInput;
                    }

                    if (restOfLine != string.Empty)
                    {
                        completedInput += restOfLine;
                    }

                    if (completedInput.Length > (MaxInputLineLength - 2))
                    {
                        completedInput = completedInput.Substring(0, MaxInputLineLength - 2);
                    }

                    // Remove any nulls from the string...
                    completedInput = RemoveNulls(completedInput);

                    // adjust the saved cursor position if the buffer scrolled as the user was typing (i.e. the user
                    // typed past the end of the buffer).

                    int linesOfInput = (endOfPromptCursorPos.X + input.Length) / screenBufferSize.Width;
                    endOfPromptCursorPos.Y = endOfInputCursorPos.Y - linesOfInput;

                    // replace the displayed input with the new input
                    try
                    {
                        RawUI.CursorPosition = endOfPromptCursorPos;
                    }
                    catch (PSArgumentOutOfRangeException)
                    {
                        // If we go a range exception, it's because
                        // there's no room in the buffer for the completed
                        // line so we'll just pretend that there was no match...
                        break;
                    }

                    // When the string is written to the console, a space character is actually appended to the string
                    // and the cursor will flash at the position of that space character.
                    WriteToConsole(completedInput, false);

                    Coordinates endOfCompletionCursorPos = RawUI.CursorPosition;

                    // adjust the starting cursor position if the screen buffer has scrolled as a result of writing the
                    // completed input (i.e. writing the completed input ran past the end of the buffer).

                    int linesOfCompletedInput = (endOfPromptCursorPos.X + completedInput.Length) / screenBufferSize.Width;
                    endOfPromptCursorPos.Y = endOfCompletionCursorPos.Y - linesOfCompletedInput;

                    // blank out any "leftover" old input.  That's everything between the cursor position at the time
                    // the user hit tab up to the current cursor position after writing the completed text.

                    int deltaInput =
                        (endOfInputCursorPos.Y * screenBufferSize.Width + endOfInputCursorPos.X)
                        - (endOfCompletionCursorPos.Y * screenBufferSize.Width + endOfCompletionCursorPos.X);

                    if (deltaInput > 0)
                    {
                        ConsoleControl.FillConsoleOutputCharacter(handle, ' ', deltaInput, endOfCompletionCursorPos);
                    }

                    if (restOfLine != string.Empty)
                    {
                        lastCompletion = completedInput.Remove(completedInput.Length - restOfLine.Length);
                        SendLeftArrows(restOfLine.Length);
                    }
                    else
                    {
                        lastCompletion = completedInput;
                    }

                    lastInput = completedInput;
                }
#endif
            }

            // Since we did not transcribe any call to ReadLine, transcribe the results here.

            if (_parent.IsTranscribing)
            {
                // Reads always terminate with the enter key, so add that.

                _parent.WriteLineToTranscript(input);
            }

            return input;
        }

#if !UNIX
        private void SendLeftArrows(int length)
        {
            var inputs = new ConsoleControl.INPUT[length * 2];
            for (int i = 0; i < length; i++)
            {
                var down = new ConsoleControl.INPUT();
                down.Type = (UInt32)ConsoleControl.InputType.Keyboard;
                down.Data.Keyboard = new ConsoleControl.KeyboardInput();
                down.Data.Keyboard.Vk = (UInt16)ConsoleControl.VirtualKeyCode.Left;
                down.Data.Keyboard.Scan = 0;
                down.Data.Keyboard.Flags = 0;
                down.Data.Keyboard.Time = 0;
                down.Data.Keyboard.ExtraInfo = IntPtr.Zero;

                var up = new ConsoleControl.INPUT();
                up.Type = (UInt32)ConsoleControl.InputType.Keyboard;
                up.Data.Keyboard = new ConsoleControl.KeyboardInput();
                up.Data.Keyboard.Vk = (UInt16)ConsoleControl.VirtualKeyCode.Left;
                up.Data.Keyboard.Scan = 0;
                up.Data.Keyboard.Flags = (UInt32)ConsoleControl.KeyboardFlag.KeyUp;
                up.Data.Keyboard.Time = 0;
                up.Data.Keyboard.ExtraInfo = IntPtr.Zero;

                inputs[2 * i] = down;
                inputs[2 * i + 1] = up;
            }

            ConsoleControl.MimicKeyPress(inputs);
        }
#endif

        private CommandCompletion GetNewCompletionResults(string input)
        {
            try
            {
                var runspace = _parent.Runspace;
                var debugger = runspace.Debugger;

                if ((debugger != null) && debugger.InBreakpoint)
                {
                    // If in debug stop mode do command completion though debugger process command.
                    try
                    {
                        return CommandCompletion.CompleteInputInDebugger(input, input.Length, null, debugger);
                    }
                    catch (PSInvalidOperationException)
                    { }
                }

                if (runspace is LocalRunspace &&
                    runspace.ExecutionContext.EngineHostInterface.NestedPromptCount > 0)
                {
                    _commandCompletionPowerShell = PowerShell.Create(RunspaceMode.CurrentRunspace);
                }
                else
                {
                    _commandCompletionPowerShell = PowerShell.Create();
                    _commandCompletionPowerShell.SetIsNested(_parent.IsNested);
                    _commandCompletionPowerShell.Runspace = runspace;
                }

                return CommandCompletion.CompleteInput(input, input.Length, null, _commandCompletionPowerShell);
            }
            finally
            {
                _commandCompletionPowerShell = null;
            }
        }

        private const string CustomReadlineCommand = "PSConsoleHostReadLine";

        private bool TryInvokeUserDefinedReadLine(out string input)
        {
            // We're using GetCommands instead of GetCommand so we don't auto-load a module should the command exist, but isn't loaded.
            // The idea is that if someone hasn't defined the command (say because they started -noprofile), we shouldn't auto-load
            // this function.

            var runspace = _parent.LocalRunspace;
            if (runspace != null &&
                runspace.Engine.Context.EngineIntrinsics.InvokeCommand.GetCommands(CustomReadlineCommand,
                    CommandTypes.Function | CommandTypes.Cmdlet, nameIsPattern: false).Any())
            {
                try
                {
                    PowerShell ps;
                    if ((runspace.ExecutionContext.EngineHostInterface.NestedPromptCount > 0) &&
                        (Runspace.DefaultRunspace != null))
                    {
                        ps = PowerShell.Create(RunspaceMode.CurrentRunspace);
                    }
                    else
                    {
                        ps = PowerShell.Create();
                        ps.Runspace = runspace;
                    }

                    var result = ps.AddCommand(CustomReadlineCommand).Invoke();
                    if (result.Count == 1)
                    {
                        input = PSObject.Base(result[0]) as string;
                        return true;
                    }
                }
                catch (Exception)
                {
                }
            }

            input = null;
            return false;
        }

        #endregion implementation

        // used to serialize access to instance data

        private object _instanceLock = new object();

        // If this is true, class throws on read or prompt method which require
        // access to console.
        internal bool ThrowOnReadAndPrompt
        {
            set
            {
                _throwOnReadAndPrompt = value;
            }
        }

        private bool _throwOnReadAndPrompt;

        internal void HandleThrowOnReadAndPrompt()
        {
            if (_throwOnReadAndPrompt)
            {
                throw PSTraceSource.NewInvalidOperationException(ConsoleHostUserInterfaceStrings.ReadFailsOnNonInteractiveFlag);
            }
        }

        // this is a test hook for the ConsoleInteractiveTestTool, which sets this field to true.

        private bool _isInteractiveTestToolListening;

        // This instance data is "read-only" and need not have access serialized.

        private ConsoleHostRawUserInterface _rawui;
        private ConsoleHost _parent;

        [TraceSourceAttribute("ConsoleHostUserInterface", "Console host's subclass of S.M.A.Host.Console")]
        private static
        PSTraceSource s_tracer = PSTraceSource.GetTracer("ConsoleHostUserInterface", "Console host's subclass of S.M.A.Host.Console");
    }
}   // namespace

