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

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Globalization;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Text;
using System.Text.RegularExpressions;

namespace Microsoft.PowerShell.Commands.Internal.Format
{
    /// <summary>
    /// Writer class to handle Complex Object formatting.
    /// </summary>
    internal sealed class ComplexWriter
    {
        /// <summary>
        /// Initialization method to be called before any other operation.
        /// </summary>
        /// <param name="lineOutput">LineOutput interfaces to write to.</param>
        /// <param name="numberOfTextColumns">Number of columns used to write out.</param>
        internal void Initialize(LineOutput lineOutput, int numberOfTextColumns)
        {
            _lo = lineOutput;
            _textColumns = numberOfTextColumns;
        }

        /// <summary>
        /// Writes a string.
        /// </summary>
        /// <param name="s"></param>
        internal void WriteString(string s)
        {
            _indentationManager.Clear();

            AddToBuffer(s);

            WriteToScreen();
        }

        /// <summary>
        /// It interprets a list of format value tokens and outputs it.
        /// </summary>
        /// <param name="formatValueList">List of FormatValue tokens to interpret.</param>
        internal void WriteObject(List<FormatValue> formatValueList)
        {
            // we always start with no indentation
            _indentationManager.Clear();

            foreach (FormatEntry fe in formatValueList)
            {
                // operate on each directive inside the list,
                // carrying the indentation from invocation to invocation
                GenerateFormatEntryDisplay(fe, 0);
            }
            // make sure that, if we have pending text in the buffer it gets flushed
            WriteToScreen();
        }

        /// <summary>
        /// Operate on a single entry.
        /// </summary>
        /// <param name="fe">Entry to process.</param>
        /// <param name="currentDepth">Current depth of recursion.</param>
        private void GenerateFormatEntryDisplay(FormatEntry fe, int currentDepth)
        {
            foreach (object obj in fe.formatValueList)
            {
                if (obj is FormatEntry feChild)
                {
                    if (currentDepth < maxRecursionDepth)
                    {
                        if (feChild.frameInfo != null)
                        {
                            // if we have frame information, we need to push it on the
                            // indentation stack
                            using (_indentationManager.StackFrame(feChild.frameInfo))
                            {
                                GenerateFormatEntryDisplay(feChild, currentDepth + 1);
                            }
                        }
                        else
                        {
                            // no need here of activating an indentation stack frame
                            GenerateFormatEntryDisplay(feChild, currentDepth + 1);
                        }
                    }

                    continue;
                }

                if (obj is FormatNewLine)
                {
                    this.WriteToScreen();
                    continue;
                }

                if (obj is FormatTextField ftf)
                {
                    this.AddToBuffer(ftf.text);
                    continue;
                }

                if (obj is FormatPropertyField fpf)
                {
                    this.AddToBuffer(fpf.propertyValue);
                }
            }
        }

        /// <summary>
        /// Add a string to the current buffer, waiting for a FlushBuffer()
        /// </summary>
        /// <param name="s">String to add to buffer.</param>
        private void AddToBuffer(string s)
        {
            _stringBuffer.Append(s);
        }

        /// <summary>
        /// Write to the output interface.
        /// </summary>
        private void WriteToScreen()
        {
            int leftIndentation = _indentationManager.LeftIndentation;
            int rightIndentation = _indentationManager.RightIndentation;
            int firstLineIndentation = _indentationManager.FirstLineIndentation;

            // VALIDITY CHECKS:

            // check the useful ("active") width
            int usefulWidth = _textColumns - rightIndentation - leftIndentation;
            if (usefulWidth <= 0)
            {
                // fatal error, there is nothing to write to the device
                // just clear the buffer and return
                _stringBuffer = new StringBuilder();
            }

            // check indentation or hanging is not larger than the active width
            int indentationAbsoluteValue = (firstLineIndentation > 0) ? firstLineIndentation : -firstLineIndentation;
            if (indentationAbsoluteValue >= usefulWidth)
            {
                // value too big, we reset it to zero
                firstLineIndentation = 0;
            }

            // compute the first line indentation or hanging
            int firstLineWidth = _textColumns - rightIndentation - leftIndentation;
            int followingLinesWidth = firstLineWidth;

            if (firstLineIndentation >= 0)
            {
                // the first line has an indentation
                firstLineWidth -= firstLineIndentation;
            }
            else
            {
                // the first line is hanging
                followingLinesWidth += firstLineIndentation;
            }

            // error checking on invalid values

            // generate the lines using the computed widths
            StringCollection sc = StringManipulationHelper.GenerateLines(_lo.DisplayCells, _stringBuffer.ToString(),
                                        firstLineWidth, followingLinesWidth);

            // compute padding
            int firstLinePadding = leftIndentation;
            int followingLinesPadding = leftIndentation;
            if (firstLineIndentation >= 0)
            {
                // the first line has an indentation
                firstLinePadding += firstLineIndentation;
            }
            else
            {
                // the first line is hanging
                followingLinesPadding -= firstLineIndentation;
            }

            // now write the lines on the screen
            bool firstLine = true;
            foreach (string s in sc)
            {
                if (firstLine)
                {
                    firstLine = false;
                    _lo.WriteLine(StringManipulationHelper.PadLeft(s, firstLinePadding));
                }
                else
                {
                    _lo.WriteLine(StringManipulationHelper.PadLeft(s, followingLinesPadding));
                }
            }

            _stringBuffer = new StringBuilder();
        }

        /// <summary>
        /// Helper object to manage the frame-based indentation and margins.
        /// </summary>
        private readonly IndentationManager _indentationManager = new IndentationManager();

        /// <summary>
        /// Buffer to accumulate partially constructed text.
        /// </summary>
        private StringBuilder _stringBuffer = new StringBuilder();

        /// <summary>
        /// Interface to write to.
        /// </summary>
        private LineOutput _lo;

        /// <summary>
        /// Number of columns for the output device.
        /// </summary>
        private int _textColumns;

        private const int maxRecursionDepth = 50;
    }

    internal sealed class IndentationManager
    {
        private sealed class IndentationStackFrame : IDisposable
        {
            internal IndentationStackFrame(IndentationManager mgr)
            {
                _mgr = mgr;
            }

            public void Dispose()
            {
                _mgr?.RemoveStackFrame();
            }

            private readonly IndentationManager _mgr;
        }

        internal void Clear()
        {
            _frameInfoStack.Clear();
        }

        internal IDisposable StackFrame(FrameInfo frameInfo)
        {
            IndentationStackFrame frame = new IndentationStackFrame(this);
            _frameInfoStack.Push(frameInfo);
            return frame;
        }

        private void RemoveStackFrame()
        {
            _frameInfoStack.Pop();
        }

        internal int RightIndentation
        {
            get
            {
                return ComputeRightIndentation();
            }
        }

        internal int LeftIndentation
        {
            get
            {
                return ComputeLeftIndentation();
            }
        }

        internal int FirstLineIndentation
        {
            get
            {
                if (_frameInfoStack.Count == 0)
                    return 0;
                return _frameInfoStack.Peek().firstLine;
            }
        }

        private int ComputeRightIndentation()
        {
            int val = 0;
            foreach (FrameInfo fi in _frameInfoStack)
            {
                val += fi.rightIndentation;
            }

            return val;
        }

        private int ComputeLeftIndentation()
        {
            int val = 0;
            foreach (FrameInfo fi in _frameInfoStack)
            {
                val += fi.leftIndentation;
            }

            return val;
        }

        private readonly Stack<FrameInfo> _frameInfoStack = new Stack<FrameInfo>();
    }

    /// <summary>
    /// Result of GetWords.
    /// </summary>
    internal struct GetWordsResult
    {
        internal string Word;
        internal string Delim;
    }

    /// <summary>
    /// Collection of helper functions for string formatting.
    /// </summary>
    internal sealed class StringManipulationHelper
    {
        private const char SoftHyphen = '\u00AD';
        private const char HardHyphen = '\u2011';
        private const char NonBreakingSpace = '\u00A0';

        private static readonly Collection<string> s_cultureCollection = new Collection<string>();

        static StringManipulationHelper()
        {
            s_cultureCollection.Add("en");        // English
            s_cultureCollection.Add("fr");        // French
            s_cultureCollection.Add("de");        // German
            s_cultureCollection.Add("it");        // Italian
            s_cultureCollection.Add("pt");        // Portuguese
            s_cultureCollection.Add("es");        // Spanish
        }

        /// <summary>
        /// Breaks a string into a collection of words
        /// TODO: we might be able to improve this function in the future
        /// so that we do not break paths etc.
        /// </summary>
        /// <param name="s">Input string.</param>
        /// <returns>A collection of words.</returns>
        private static IEnumerable<GetWordsResult> GetWords(string s)
        {
            StringBuilder sb = new StringBuilder();
            StringBuilder vtSeqs = null;
            Dictionary<int, int> vtRanges = null;

            var valueStrDec = new ValueStringDecorated(s);
            if (valueStrDec.IsDecorated)
            {
                vtSeqs = new StringBuilder();
                vtRanges = valueStrDec.EscapeSequenceRanges;
            }

            bool wordHasVtSeqs = false;
            for (int i = 0; i < s.Length; i++)
            {
                if (vtRanges?.TryGetValue(i, out int len) == true)
                {
                    var vtSpan = s.AsSpan(i, len);
                    sb.Append(vtSpan);
                    vtSeqs.Append(vtSpan);

                    wordHasVtSeqs = true;
                    i += len - 1;
                    continue;
                }

                string delimiter = null;
                if (s[i] is ' ' or '\t' or SoftHyphen)
                {
                    // Soft hyphen = \u00AD - Should break, and add a hyphen if needed.
                    // If not needed for a break, hyphen should be absent.
                    delimiter = new string(s[i], 1);
                }
                else if (s[i] is HardHyphen or NonBreakingSpace)
                {
                    // Non-breaking space = \u00A0 - ideally shouldn't wrap.
                    // Hard hyphen = \u2011 - Should not break.
                    delimiter = string.Empty;
                }

                if (delimiter is not null)
                {
                    if (wordHasVtSeqs && !sb.EndsWith(PSStyle.Instance.Reset))
                    {
                        sb.Append(PSStyle.Instance.Reset);
                    }

                    var result = new GetWordsResult()
                    {
                        Word = sb.ToString(),
                        Delim = delimiter
                    };

                    sb.Clear().Append(vtSeqs);
                    yield return result;
                }
                else
                {
                    sb.Append(s[i]);
                }
            }

            if (wordHasVtSeqs)
            {
                if (sb.Length == vtSeqs.Length)
                {
                    // This indicates 'sb' only contains all VT sequences, which may happen when the string ends with a word delimiter.
                    // For a word that contains VT sequence only, it's the same as an empty string to the formatting system,
                    // because nothing will actually be rendered.
                    // So, we use an empty string in this case to avoid unneeded string allocations.
                    sb.Clear();
                }
                else if (!sb.EndsWith(PSStyle.Instance.Reset))
                {
                    sb.Append(PSStyle.Instance.Reset);
                }
            }

            yield return new GetWordsResult() { Word = sb.ToString(), Delim = string.Empty };
        }

        internal static StringCollection GenerateLines(DisplayCells displayCells, string val, int firstLineLen, int followingLinesLen)
        {
            if (s_cultureCollection.Contains(CultureInfo.CurrentCulture.TwoLetterISOLanguageName))
            {
                return GenerateLinesWithWordWrap(displayCells, val, firstLineLen, followingLinesLen);
            }
            else
            {
                return GenerateLinesWithoutWordWrap(displayCells, val, firstLineLen, followingLinesLen);
            }
        }

        private static StringCollection GenerateLinesWithoutWordWrap(DisplayCells displayCells, string val, int firstLineLen, int followingLinesLen)
        {
            StringCollection retVal = new StringCollection();

            if (string.IsNullOrEmpty(val))
            {
                // if null or empty, just add and we are done
                retVal.Add(val);
                return retVal;
            }

            // break string on newlines and process each line separately
            List<string> lines = SplitLines(val);

            for (int k = 0; k < lines.Count; k++)
            {
                string currentLine = lines[k];

                if (currentLine == null || displayCells.Length(currentLine) <= firstLineLen)
                {
                    // we do not need to split further, just add
                    retVal.Add(currentLine);
                    continue;
                }

                // the string does not fit, so we have to wrap around on multiple lines
                // for each of these lines in the string, the first line will have
                // a (potentially) different length (indentation or hanging)

                // for each line, start a new state
                SplitLinesAccumulator accumulator = new SplitLinesAccumulator(retVal, firstLineLen, followingLinesLen);

                int offset = 0; // offset into the line we are splitting

                while (offset < currentLine.Length)
                {
                    // acquire the current active display line length (it can very from call to call)
                    int currentDisplayLen = accumulator.ActiveLen;

                    // determine if the current tail would fit or not

                    // for the remaining part of the string, determine its display cell count
                    int currentCellsToFit = displayCells.Length(currentLine, offset);

                    // determine if we fit into the line
                    int excessCells = currentCellsToFit - currentDisplayLen;

                    if (excessCells > 0)
                    {
                        // we are not at the end of the string, select a sub string
                        // that would fit in the remaining display length
                        int charactersToAdd = displayCells.TruncateTail(currentLine, offset, currentDisplayLen);

                        if (charactersToAdd <= 0)
                        {
                            // corner case: we have a two cell character and the current
                            // display length is one.
                            // add a single cell arbitrary character instead of the original
                            // one and keep going
                            charactersToAdd = 1;
                            accumulator.AddLine("?");
                        }
                        else
                        {
                            // of the given length, add it to the accumulator
                            accumulator.AddLine(currentLine.VtSubstring(offset, charactersToAdd));
                        }

                        // increase the offset by the # of characters added
                        offset += charactersToAdd;
                    }
                    else
                    {
                        // we reached the last (partial) line, we add it all
                        accumulator.AddLine(currentLine.VtSubstring(offset));
                        break;
                    }
                }
            }

            return retVal;
        }

        private sealed class SplitLinesAccumulator
        {
            internal SplitLinesAccumulator(StringCollection retVal, int firstLineLen, int followingLinesLen)
            {
                _retVal = retVal;
                _firstLineLen = firstLineLen;
                _followingLinesLen = followingLinesLen;
            }

            internal void AddLine(string s)
            {
                if (!_addedFirstLine)
                {
                    _addedFirstLine = true;
                }

                _retVal.Add(s);
            }

            internal int ActiveLen
            {
                get
                {
                    if (_addedFirstLine)
                        return _followingLinesLen;
                    return _firstLineLen;
                }
            }

            private readonly StringCollection _retVal;
            private bool _addedFirstLine;
            private readonly int _firstLineLen;
            private readonly int _followingLinesLen;
        }

        private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCells, string val, int firstLineLen, int followingLinesLen)
        {
            StringCollection retVal = new StringCollection();

            if (string.IsNullOrEmpty(val))
            {
                // if null or empty, just add and we are done
                retVal.Add(val);
                return retVal;
            }

            // break string on newlines and process each line separately
            List<string> lines = SplitLines(val);

            for (int k = 0; k < lines.Count; k++)
            {
                if (lines[k] == null || displayCells.Length(lines[k]) <= firstLineLen)
                {
                    // we do not need to split further, just add
                    retVal.Add(lines[k]);
                    continue;
                }

                int spacesLeft = firstLineLen;
                int lineWidth = firstLineLen;
                bool firstLine = true;
                StringBuilder singleLine = new StringBuilder();
                string resetStr = PSStyle.Instance.Reset;

                foreach (GetWordsResult word in GetWords(lines[k]))
                {
                    string wordToAdd = word.Word;
                    string suffix = null;

                    // Handle soft hyphen
                    if (word.Delim.Length == 1 && word.Delim[0] is SoftHyphen)
                    {
                        int wordWidthWithHyphen = displayCells.Length(wordToAdd) + displayCells.Length(SoftHyphen);

                        // Add hyphen only if necessary
                        if (wordWidthWithHyphen == spacesLeft)
                        {
                            suffix = "-";
                        }
                    }
                    else if (!string.IsNullOrEmpty(word.Delim))
                    {
                        suffix = word.Delim;
                    }

                    if (suffix is not null)
                    {
                        wordToAdd = wordToAdd.EndsWith(resetStr)
                            ? wordToAdd.Insert(wordToAdd.Length - resetStr.Length, suffix)
                            : wordToAdd + suffix;
                    }

                    int wordWidth = displayCells.Length(wordToAdd);

                    // Handle zero width
                    if (lineWidth == 0)
                    {
                        if (firstLine)
                        {
                            firstLine = false;
                            lineWidth = followingLinesLen;
                        }

                        if (lineWidth == 0)
                        {
                            break;
                        }

                        spacesLeft = lineWidth;
                    }

                    // Word is wider than a single line
                    if (wordWidth > lineWidth)
                    {
                        Dictionary<int, int> vtRanges = null;
                        StringBuilder vtSeqs = null;

                        var valueStrDec = new ValueStringDecorated(wordToAdd);
                        if (valueStrDec.IsDecorated)
                        {
                            vtSeqs = new StringBuilder();
                            vtRanges = valueStrDec.EscapeSequenceRanges;
                        }

                        bool hasEscSeqs = false;
                        for (int i = 0; i < wordToAdd.Length; i++)
                        {
                            if (vtRanges?.TryGetValue(i, out int len) == true)
                            {
                                var vtSpan = wordToAdd.AsSpan(i, len);
                                singleLine.Append(vtSpan);
                                vtSeqs.Append(vtSpan);

                                hasEscSeqs = true;
                                i += len - 1;
                                continue;
                            }

                            char charToAdd = wordToAdd[i];
                            int charWidth = displayCells.Length(charToAdd);

                            // Corner case: we have a two cell character and the current display length is one.
                            // Add a single cell arbitrary character instead of the original one and keep going.
                            if (charWidth > lineWidth)
                            {
                                charToAdd = '?';
                                charWidth = 1;
                            }

                            if (charWidth > spacesLeft)
                            {
                                if (hasEscSeqs && !singleLine.EndsWith(resetStr))
                                {
                                    singleLine.Append(resetStr);
                                }

                                retVal.Add(singleLine.ToString());
                                singleLine.Clear().Append(vtSeqs).Append(charToAdd);

                                if (firstLine)
                                {
                                    firstLine = false;
                                    lineWidth = followingLinesLen;
                                }

                                spacesLeft = lineWidth - charWidth;
                            }
                            else
                            {
                                singleLine.Append(charToAdd);
                                spacesLeft -= charWidth;
                            }
                        }
                    }
                    else
                    {
                        if (wordWidth > spacesLeft)
                        {
                            retVal.Add(singleLine.ToString());
                            singleLine.Clear().Append(wordToAdd);

                            if (firstLine)
                            {
                                firstLine = false;
                                lineWidth = followingLinesLen;
                            }

                            spacesLeft = lineWidth - wordWidth;
                        }
                        else
                        {
                            singleLine.Append(wordToAdd);
                            spacesLeft -= wordWidth;
                        }
                    }
                }

                retVal.Add(singleLine.ToString());
            }

            return retVal;
        }

        /// <summary>
        /// Split a multiline string into an array of strings
        /// by honoring both \n and \r\n.
        /// </summary>
        /// <param name="s">String to split.</param>
        /// <returns>String array with the values.</returns>
        internal static List<string> SplitLines(string s)
        {
            if (string.IsNullOrEmpty(s) || !s.Contains('\n'))
            {
                return new List<string>(capacity: 1) { s?.Replace("\r", string.Empty) };
            }

            StringBuilder sb = new StringBuilder();
            List<string> list = new List<string>();

            StringBuilder vtSeqs = null;
            Dictionary<int, int> vtRanges = null;

            var valueStrDec = new ValueStringDecorated(s);
            if (valueStrDec.IsDecorated)
            {
                vtSeqs = new StringBuilder();
                vtRanges = valueStrDec.EscapeSequenceRanges;
            }

            bool hasVtSeqs = false;
            for (int i = 0; i < s.Length; i++)
            {
                if (vtRanges?.TryGetValue(i, out int len) == true)
                {
                    var vtSpan = s.AsSpan(i, len);
                    sb.Append(vtSpan);
                    vtSeqs.Append(vtSpan);

                    hasVtSeqs = true;
                    i += len - 1;
                    continue;
                }

                char c = s[i];
                if (c == '\n')
                {
                    if (hasVtSeqs && !sb.EndsWith(PSStyle.Instance.Reset))
                    {
                        sb.Append(PSStyle.Instance.Reset);
                    }

                    list.Add(sb.ToString());
                    sb.Clear().Append(vtSeqs);
                }
                else if (c != '\r')
                {
                    sb.Append(c);
                }
            }

            if (hasVtSeqs)
            {
                if (sb.Length == vtSeqs.Length)
                {
                    // This indicates 'sb' only contains all VT sequences, which may happen when the string ends with '\n'.
                    // For a sub-string that contains VT sequence only, it's the same as an empty string to the formatting
                    // system, because nothing will actually be rendered.
                    // So, we use an empty string in this case to avoid unneeded string allocations.
                    sb.Clear();
                }
                else if (!sb.EndsWith(PSStyle.Instance.Reset))
                {
                    sb.Append(PSStyle.Instance.Reset);
                }
            }

            list.Add(sb.ToString());
            return list;
        }

        internal static string TruncateAtNewLine(string s)
        {
            if (string.IsNullOrEmpty(s))
            {
                return string.Empty;
            }

            int lineBreak = s.AsSpan().IndexOfAny('\n', '\r');

            if (lineBreak < 0)
            {
                return s;
            }

            return s.Substring(0, lineBreak) + PSObjectHelper.Ellipsis;
        }

        internal static string PadLeft(string val, int count)
        {
            return StringUtil.Padding(count) + val;
        }
    }
}
