/********************************************************************++
Copyright (c) Microsoft Corporation.  All rights reserved.
--********************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Management.Automation.Language;
using Microsoft.PowerShell.Internal;

namespace Microsoft.PowerShell
{
    public partial class PSConsoleReadLine
    {
#if UNIX
        private const ConsoleColor UnknownColor = (ConsoleColor) (-1);
#endif
        private BufferChar[] _consoleBuffer;
        private int _initialX;
        private int _initialY;
        private int _bufferWidth;
        private ConsoleColor _initialBackgroundColor;
        private ConsoleColor _initialForegroundColor;
        private BufferChar _space;
        private int _current;
        private int _emphasisStart;
        private int _emphasisLength;

        private class SavedTokenState
        {
            internal Token[] Tokens { get; set; }
            internal int Index { get; set; }
            internal ConsoleColor BackgroundColor { get; set; }
            internal ConsoleColor ForegroundColor { get; set; }
        }

        private void MaybeParseInput()
        {
            if (_tokens == null)
            {
                ParseInput();
            }
        }

        private string ParseInput()
        {
            var text = _buffer.ToString();
            _ast = Parser.ParseInput(text, out _tokens, out _parseErrors);
            return text;
        }

        private void ClearStatusMessage(bool render)
        {
            _statusBuffer.Clear();
            _statusLinePrompt = null;
            _statusIsErrorMessage = false;
            if (render)
            {
                Render();
            }
        }

        private void Render()
        {
            // If there are a bunch of keys queued up, skip rendering if we've rendered
            // recently.
            if (_queuedKeys.Count > 10 && (_lastRenderTime.ElapsedMilliseconds < 50))
            {
                // We won't render, but most likely the tokens will be different, so make
                // sure we don't use old tokens.
                _tokens = null;
                _ast = null;
                return;
            }

            ReallyRender();
        }

        [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults")]
        private void ReallyRender()
        {
            var text = ParseInput();

            int statusLineCount = GetStatusLineCount();
            int j = _initialX + (_bufferWidth * Options.ExtraPromptLineCount);
            var backgroundColor = _initialBackgroundColor;
            var foregroundColor = _initialForegroundColor;
            bool afterLastToken = false;
            int totalBytes = j;
            int bufferWidth = _console.BufferWidth;

            var tokenStack = new Stack<SavedTokenState>();
            tokenStack.Push(new SavedTokenState
            {
                Tokens = _tokens,
                Index = 0,
                BackgroundColor = _initialBackgroundColor,
                ForegroundColor = _initialForegroundColor
            });

            int bufferLineCount;

            try
            {
                _console.StartRender();
                bufferLineCount = ConvertOffsetToCoordinates(text.Length).Y - _initialY + 1 + statusLineCount;
                if (_consoleBuffer.Length != bufferLineCount * bufferWidth)
                {
                    var newBuffer = new BufferChar[bufferLineCount * bufferWidth];
                    Array.Copy(_consoleBuffer, newBuffer, _initialX + (Options.ExtraPromptLineCount * _bufferWidth));
                    if (_consoleBuffer.Length > bufferLineCount * bufferWidth)
                    {
                        int consoleBufferOffset = ConvertOffsetToConsoleBufferOffset(text.Length, _initialX + (Options.ExtraPromptLineCount * _bufferWidth));
                        // Need to erase the extra lines that we won't draw again
                        for (int i = consoleBufferOffset; i < _consoleBuffer.Length; i++)
                        {
                            _consoleBuffer[i] = _space;
                        }
                        _console.WriteBufferLines(_consoleBuffer, ref _initialY);
                    }
                    _consoleBuffer = newBuffer;
                }

                for (int i = 0; i < text.Length; i++)
                {
                    totalBytes = totalBytes % bufferWidth;
                    if (!afterLastToken)
                    {
                        // Figure out the color of the character - if it's in a token,
                        // use the tokens color otherwise use the initial color.
                        var state = tokenStack.Peek();
                        var token = state.Tokens[state.Index];

                        if (i == token.Extent.EndOffset)
                        {
                            if (token == state.Tokens[state.Tokens.Length - 1])
                            {
                                tokenStack.Pop();
                                if (tokenStack.Count == 0)
                                {
                                    afterLastToken = true;
                                    token = null;
                                    foregroundColor = _initialForegroundColor;
                                    backgroundColor = _initialBackgroundColor;
                                }
                                else
                                {
                                    state = tokenStack.Peek();
                                }
                            }

                            if (!afterLastToken)
                            {
                                foregroundColor = state.ForegroundColor;
                                backgroundColor = state.BackgroundColor;
                                token = state.Tokens[++state.Index];
                            }
                        }

                        if (!afterLastToken && i == token.Extent.StartOffset)
                        {
                            GetTokenColors(token, out foregroundColor, out backgroundColor);

                            var stringToken = token as StringExpandableToken;
                            if (stringToken != null)
                            {
                                // We might have nested tokens.
                                if (stringToken.NestedTokens != null && stringToken.NestedTokens.Any())
                                {
                                    var tokens = new Token[stringToken.NestedTokens.Count + 1];
                                    stringToken.NestedTokens.CopyTo(tokens, 0);
                                    // NestedTokens doesn't have an "EOS" token, so we use
                                    // the string literal token for that purpose.
                                    tokens[tokens.Length - 1] = stringToken;

                                    tokenStack.Push(new SavedTokenState
                                    {
                                        Tokens = tokens,
                                        Index = 0,
                                        BackgroundColor = backgroundColor,
                                        ForegroundColor = foregroundColor
                                    });

                                    if (i == tokens[0].Extent.StartOffset)
                                    {
                                        GetTokenColors(tokens[0], out foregroundColor, out backgroundColor);
                                    }
                                }
                            }
                        }
                    }

                    var charToRender = text[i];
                    if (charToRender == '\n')
                    {
                        while ((j % bufferWidth) != 0)
                        {
                            _consoleBuffer[j++] = _space;
                        }

                        for (int k = 0; k < Options.ContinuationPrompt.Length; k++, j++)
                        {
                            _consoleBuffer[j].UnicodeChar = Options.ContinuationPrompt[k];
                            _consoleBuffer[j].ForegroundColor = Options.ContinuationPromptForegroundColor;
                            _consoleBuffer[j].BackgroundColor = Options.ContinuationPromptBackgroundColor;
                        }
                    }
                    else
                    {
                        int size = LengthInBufferCells(charToRender);
                        totalBytes += size;

                        //if there is no enough space for the character at the edge, fill in spaces at the end and
                        //put the character to next line.
                        int filling = totalBytes > bufferWidth ? (totalBytes - bufferWidth) % size : 0;
                        for (int f = 0; f < filling; f++)
                        {
                            _consoleBuffer[j++] = _space;
                            totalBytes++;
                        }

                        if (char.IsControl(charToRender))
                        {
                            _consoleBuffer[j].UnicodeChar = '^';
                            MaybeEmphasize(ref _consoleBuffer[j++], i, foregroundColor, backgroundColor);
                            _consoleBuffer[j].UnicodeChar = (char)('@' + charToRender);
                            MaybeEmphasize(ref _consoleBuffer[j++], i, foregroundColor, backgroundColor);

                        }
#if !UNIX
                        else if (size > 1)
                        {
                            _consoleBuffer[j].UnicodeChar = charToRender;
                            _consoleBuffer[j].IsLeadByte = true;
                            MaybeEmphasize(ref _consoleBuffer[j++], i, foregroundColor, backgroundColor);
                            _consoleBuffer[j].UnicodeChar = charToRender;
                            _consoleBuffer[j].IsTrailByte = true;
                            MaybeEmphasize(ref _consoleBuffer[j++], i, foregroundColor, backgroundColor);
                        }
#endif
                        else
                        {
                            _consoleBuffer[j].UnicodeChar = charToRender;
                            MaybeEmphasize(ref _consoleBuffer[j++], i, foregroundColor, backgroundColor);
                        }
                    }
                }
            }
            finally
            {
                _console.EndRender();
            }

            for (; j < (_consoleBuffer.Length - (statusLineCount * _bufferWidth)); j++)
            {
                _consoleBuffer[j] = _space;
            }

            if (_statusLinePrompt != null)
            {
                foregroundColor = _statusIsErrorMessage ? Options.ErrorForegroundColor : _console.ForegroundColor;
                backgroundColor = _statusIsErrorMessage ? Options.ErrorBackgroundColor : _console.BackgroundColor;

                for (int i = 0; i < _statusLinePrompt.Length; i++, j++)
                {
                    _consoleBuffer[j].UnicodeChar = _statusLinePrompt[i];
                    _consoleBuffer[j].ForegroundColor = foregroundColor;
                    _consoleBuffer[j].BackgroundColor = backgroundColor;
                }
                for (int i = 0; i < _statusBuffer.Length; i++, j++)
                {
                    _consoleBuffer[j].UnicodeChar = _statusBuffer[i];
                    _consoleBuffer[j].ForegroundColor = foregroundColor;
                    _consoleBuffer[j].BackgroundColor = backgroundColor;
                }

                for (; j < _consoleBuffer.Length; j++)
                {
                    _consoleBuffer[j] = _space;
                }
            }

            bool rendered = false;
            if (_parseErrors.Length > 0)
            {
                int promptChar = _initialX - 1 + (_bufferWidth * Options.ExtraPromptLineCount);

                while (promptChar >= 0)
                {
                    var c = _consoleBuffer[promptChar].UnicodeChar;
                    if (char.IsWhiteSpace(c))
                    {
                        promptChar -= 1;
                        continue;
                    }

                    ConsoleColor prevColor = _consoleBuffer[promptChar].ForegroundColor;
                    _consoleBuffer[promptChar].ForegroundColor = ConsoleColor.Red;
                    _console.WriteBufferLines(_consoleBuffer, ref _initialY);
                    rendered = true;
                    _consoleBuffer[promptChar].ForegroundColor = prevColor;
                    break;
                }
            }

            if (!rendered)
            {
                _console.WriteBufferLines(_consoleBuffer, ref _initialY);
            }

            PlaceCursor();

            if ((_initialY + bufferLineCount) > (_console.WindowTop + _console.WindowHeight))
            {
#if !UNIX // TODO: verify this isn't necessary for LINUX
                _console.WindowTop = _initialY + bufferLineCount - _console.WindowHeight;
#endif
            }

            _lastRenderTime.Restart();
        }

        private int LengthInBufferCells(char c)
        {
            int length = Char.IsControl(c) ? 1 : 0;
            if (c < 256)
            {
                return length + 1;
            }
            return _console.LengthInBufferCells(c);
        }

        private static void WriteBlankLines(int count, int top)
        {
            var console = _singleton._console;
            var blanks = new BufferChar[count * console.BufferWidth];
            for (int i = 0; i < blanks.Length; i++)
            {
                blanks[i].BackgroundColor = console.BackgroundColor;
                blanks[i].ForegroundColor = console.ForegroundColor;
                blanks[i].UnicodeChar = ' ';
            }
            console.WriteBufferLines(blanks, ref top);
        }

        private static BufferChar[] ReadBufferLines(int top, int count)
        {
            return _singleton._console.ReadBufferLines(top, count);
        }

        private void GetTokenColors(Token token, out ConsoleColor foregroundColor, out ConsoleColor backgroundColor)
        {
            switch (token.Kind)
            {
            case TokenKind.Comment:
                foregroundColor = _options.CommentForegroundColor;
                backgroundColor = _options.CommentBackgroundColor;
                return;

            case TokenKind.Parameter:
                foregroundColor = _options.ParameterForegroundColor;
                backgroundColor = _options.ParameterBackgroundColor;
                return;

            case TokenKind.Variable:
            case TokenKind.SplattedVariable:
                foregroundColor = _options.VariableForegroundColor;
                backgroundColor = _options.VariableBackgroundColor;
                return;

            case TokenKind.StringExpandable:
            case TokenKind.StringLiteral:
            case TokenKind.HereStringExpandable:
            case TokenKind.HereStringLiteral:
                foregroundColor = _options.StringForegroundColor;
                backgroundColor = _options.StringBackgroundColor;
                return;

            case TokenKind.Number:
                foregroundColor = _options.NumberForegroundColor;
                backgroundColor = _options.NumberBackgroundColor;
                return;
            }

            if ((token.TokenFlags & TokenFlags.CommandName) != 0)
            {
                foregroundColor = _options.CommandForegroundColor;
                backgroundColor = _options.CommandBackgroundColor;
                return;
            }

            if ((token.TokenFlags & TokenFlags.Keyword) != 0)
            {
                foregroundColor = _options.KeywordForegroundColor;
                backgroundColor = _options.KeywordBackgroundColor;
                return;
            }

            if ((token.TokenFlags & (TokenFlags.BinaryOperator | TokenFlags.UnaryOperator | TokenFlags.AssignmentOperator)) != 0)
            {
                foregroundColor = _options.OperatorForegroundColor;
                backgroundColor = _options.OperatorBackgroundColor;
                return;
            }

            if ((token.TokenFlags & TokenFlags.TypeName) != 0)
            {
                foregroundColor = _options.TypeForegroundColor;
                backgroundColor = _options.TypeBackgroundColor;
                return;
            }

            if ((token.TokenFlags & TokenFlags.MemberName) != 0)
            {
                foregroundColor = _options.MemberForegroundColor;
                backgroundColor = _options.MemberBackgroundColor;
                return;
            }

            foregroundColor = _options.DefaultTokenForegroundColor;
            backgroundColor = _options.DefaultTokenBackgroundColor;
        }

        private void GetRegion(out int start, out int length)
        {
            if (_mark < _current)
            {
                start = _mark;
                length = _current - start;
            }
            else
            {
                start = _current;
                length = _mark - start;
            }
        }

        private bool InRegion(int i)
        {
            int start, end;
            if (_mark > _current)
            {
                start = _current;
                end = _mark;
            }
            else
            {
                start = _mark;
                end = _current;
            }
            return i >= start && i < end;
        }

        private void MaybeEmphasize(ref BufferChar charInfo, int i, ConsoleColor foregroundColor, ConsoleColor backgroundColor)
        {
            if (i >= _emphasisStart && i < (_emphasisStart + _emphasisLength))
            {
                backgroundColor = _options.EmphasisBackgroundColor;
                foregroundColor = _options.EmphasisForegroundColor;
            }
            else if (_visualSelectionCommandCount > 0 && InRegion(i))
            {
                // We can't quite emulate real console selection because it inverts
                // based on actual screen colors, our palette is limited.  The choice
                // to invert only the lower 3 bits to change the color is somewhat
                // but looks best with the 2 default color schemes - starting PowerShell
                // from it's shortcut or from a cmd shortcut.
#if UNIX // TODO: set Inverse attribute and let render choose what to do.
                ConsoleColor tempColor = (foregroundColor == UnknownColor) ? ConsoleColor.White : foregroundColor;
                foregroundColor = (backgroundColor == UnknownColor) ? ConsoleColor.Black : backgroundColor;
                backgroundColor = tempColor;
#else
                foregroundColor = (ConsoleColor)((int)foregroundColor ^ 7);
                backgroundColor = (ConsoleColor)((int)backgroundColor ^ 7);
#endif
            }

            charInfo.ForegroundColor = foregroundColor;
            charInfo.BackgroundColor = backgroundColor;
        }

        private void PlaceCursor(int x, ref int y)
        {
            int statusLineCount = GetStatusLineCount();
            if ((y + statusLineCount) >= _console.BufferHeight)
            {
                _console.ScrollBuffer((y + statusLineCount) - _console.BufferHeight + 1);
                y = _console.BufferHeight - 1;
            }
            _console.SetCursorPosition(x, y);
        }

        private void PlaceCursor()
        {
            var coordinates = ConvertOffsetToCoordinates(_current);
            int y = coordinates.Y;
            PlaceCursor(coordinates.X, ref y);
        }

        private COORD ConvertOffsetToCoordinates(int offset)
        {
            int x = _initialX;
            int y = _initialY + Options.ExtraPromptLineCount;

            int bufferWidth = _console.BufferWidth;
            var continuationPromptLength = Options.ContinuationPrompt.Length;

            for (int i = 0; i < offset; i++)
            {
                char c = _buffer[i];
                if (c == '\n')
                {
                    y += 1;
                    x = continuationPromptLength;
                }
                else
                {
                    int size = LengthInBufferCells(c);
                    x += size;
                    // Wrap?  No prompt when wrapping
                    if (x >= bufferWidth)
                    {
                        int offsize = x - bufferWidth;
                        if (offsize % size == 0)
                        {
                            x -= bufferWidth;
                        }
                        else
                        {
                            x = size;
                        }
                        y += 1;
                    }
                }
            }

            //if the next character has bigger size than the remain space on this line,
            //the cursor goes to next line where the next character is.
            if (_buffer.Length > offset)
            {
                int size = LengthInBufferCells(_buffer[offset]);
                // next one is Wrapped to next line
                if (x + size > bufferWidth && (x + size - bufferWidth) % size != 0)
                {
                    x = 0;
                    y++;
                }
            }

            return new COORD {X = (short)x, Y = (short)y};
        }

        private int ConvertOffsetToConsoleBufferOffset(int offset, int startIndex)
        {
            int j = startIndex;
            for (int i = 0; i < offset; i++)
            {
                var c = _buffer[i];
                if (c == '\n')
                {
                    for (int k = 0; k < Options.ContinuationPrompt.Length; k++)
                    {
                        j++;
                    }
                }
                else if (LengthInBufferCells(c) > 1)
                {
                    j += 2;
                }
                else
                {
                    j++;
                }
            }
            return j;
        }

        private int ConvertLineAndColumnToOffset(COORD coord)
        {
            int offset;
            int x = _initialX;
            int y = _initialY + Options.ExtraPromptLineCount;

            int bufferWidth = _console.BufferWidth;
            var continuationPromptLength = Options.ContinuationPrompt.Length;
            for (offset = 0; offset < _buffer.Length; offset++)
            {
                // If we are on the correct line, return when we find
                // the correct column
                if (coord.Y == y && coord.X <= x)
                {
                    return offset;
                }
                char c = _buffer[offset];
                if (c == '\n')
                {
                    // If we are about to move off of the correct line,
                    // the line was shorter than the column we wanted so return.
                    if (coord.Y == y)
                    {
                        return offset;
                    }
                    y += 1;
                    x = continuationPromptLength;
                }
                else
                {
                    int size = LengthInBufferCells(c);
                    x += size;
                    // Wrap?  No prompt when wrapping
                    if (x >= bufferWidth)
                    {
                        int offsize = x - bufferWidth;
                        if (offsize % size == 0)
                        {
                            x -= bufferWidth;
                        }
                        else
                        {
                            x = size;
                        }
                        y += 1;
                    }
                }
            }

            // Return -1 if y is out of range, otherwise the last line was shorter
            // than we wanted, but still in range so just return the last offset.
            return (coord.Y == y) ? offset : -1;
        }

        private bool LineIsMultiLine()
        {
            for (int i = 0; i < _buffer.Length; i++)
            {
                if (_buffer[i] == '\n')
                    return true;
            }
            return false;
        }

        private int GetStatusLineCount()
        {
            if (_statusLinePrompt == null)
                return 0;

            return (_statusLinePrompt.Length + _statusBuffer.Length) / _console.BufferWidth + 1;
        }

        [ExcludeFromCodeCoverage]
        void IPSConsoleReadLineMockableMethods.Ding()
        {
            switch (Options.BellStyle)
            {
            case BellStyle.None:
                break;
            case BellStyle.Audible:
#if UNIX
                Console.Beep();
#else
                Console.Beep(Options.DingTone, Options.DingDuration);
#endif
                break;
            case BellStyle.Visual:
                // TODO: flash prompt? command line?
                break;
            }
        }

        /// <summary>
        /// Notify the user based on their preference for notification.
        /// </summary>
        public static void Ding()
        {
            _singleton._mockableMethods.Ding();
        }

        private bool PromptYesOrNo(string s)
        {
            _statusLinePrompt = s;
            Render();

            var key = ReadKey();

            _statusLinePrompt = null;
            Render();
            return key.Key == ConsoleKey.Y;
        }

#region Screen scrolling

#if !UNIX
        /// <summary>
        /// Scroll the display up one screen.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
        public static void ScrollDisplayUp(ConsoleKeyInfo? key = null, object arg = null)
        {
            int numericArg;
            TryGetArgAsInt(arg, out numericArg, +1);
            var console = _singleton._console;
            var newTop = console.WindowTop - (numericArg * console.WindowHeight);
            if (newTop < 0)
            {
                newTop = 0;
            }
            console.SetWindowPosition(0, newTop);
        }

        /// <summary>
        /// Scroll the display up one line.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
        public static void ScrollDisplayUpLine(ConsoleKeyInfo? key = null, object arg = null)
        {
            int numericArg;
            TryGetArgAsInt(arg, out numericArg, +1);
            var console = _singleton._console;
            var newTop = console.WindowTop - numericArg;
            if (newTop < 0)
            {
                newTop = 0;
            }
            console.SetWindowPosition(0, newTop);
        }

        /// <summary>
        /// Scroll the display down one screen.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
        public static void ScrollDisplayDown(ConsoleKeyInfo? key = null, object arg = null)
        {
            int numericArg;
            TryGetArgAsInt(arg, out numericArg, +1);
            var console = _singleton._console;
            var newTop = console.WindowTop + (numericArg * console.WindowHeight);
            if (newTop > (console.BufferHeight - console.WindowHeight))
            {
                newTop = (console.BufferHeight - console.WindowHeight);
            }
            console.SetWindowPosition(0, newTop);
        }

        /// <summary>
        /// Scroll the display down one line.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
        public static void ScrollDisplayDownLine(ConsoleKeyInfo? key = null, object arg = null)
        {
            int numericArg;
            TryGetArgAsInt(arg, out numericArg, +1);
            var console = _singleton._console;
            var newTop = console.WindowTop + numericArg;
            if (newTop > (console.BufferHeight - console.WindowHeight))
            {
                newTop = (console.BufferHeight - console.WindowHeight);
            }
            console.SetWindowPosition(0, newTop);
        }

        /// <summary>
        /// Scroll the display to the top.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
        public static void ScrollDisplayTop(ConsoleKeyInfo? key = null, object arg = null)
        {
            _singleton._console.SetWindowPosition(0, 0);
        }

        /// <summary>
        /// Scroll the display to the cursor.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
        public static void ScrollDisplayToCursor(ConsoleKeyInfo? key = null, object arg = null)
        {
            // Ideally, we'll put the last input line at the bottom of the window
            var coordinates = _singleton.ConvertOffsetToCoordinates(_singleton._buffer.Length);

            var console = _singleton._console;
            var newTop = coordinates.Y - console.WindowHeight + 1;

            // If the cursor is already visible, and we're on the first
            // page-worth of the buffer, then just scroll to the top (we can't
            // scroll to before the beginning of the buffer).
            //
            // Note that we don't want to just return, because the window may
            // have been scrolled way past the end of the content, so we really
            // do need to set the new window top to 0 to bring it back into
            // view.
            if (newTop < 0)
            {
                newTop = 0;
            }

            // But if the cursor won't be visible, make sure it is.
            if (newTop > console.CursorTop)
            {
                // Add 10 for some extra context instead of putting the
                // cursor on the bottom line.
                newTop = console.CursorTop - console.WindowHeight + 10;
            }

            // But we can't go past the end of the buffer.
            if (newTop > (console.BufferHeight - console.WindowHeight))
            {
                newTop = (console.BufferHeight - console.WindowHeight);
            }
            console.SetWindowPosition(0, newTop);
        }

#endif
#endregion Screen scrolling
    }
}
