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

using System;
using System.Collections;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Management.Automation.Internal;
using System.Text;

using Microsoft.PowerShell.Commands.Internal.Format;

using Dbg = System.Management.Automation.Diagnostics;

namespace Microsoft.PowerShell
{
    /// <summary>
    /// ProgressNode is an augmentation of the ProgressRecord type that adds extra fields for the purposes of tracking
    /// outstanding activities received by the host, and rendering them in the console.
    /// </summary>
    internal
    class
    ProgressNode : ProgressRecord
    {
        /// <summary>
        /// Indicates the various layouts for rendering a particular node.
        /// </summary>
        internal
        enum
        RenderStyle
        {
            Invisible = 0,
            Minimal = 1,
            Compact = 2,

            /// <summary>
            /// Allocate only one line for displaying the StatusDescription or the CurrentOperation,
            /// truncate the rest if the StatusDescription or CurrentOperation doesn't fit in one line.
            /// </summary>
            Full = 3,

            /// <summary>
            /// The node will be displayed the same as Full, plus, the whole StatusDescription and CurrentOperation will be displayed (in multiple lines if needed).
            /// </summary>
            FullPlus = 4,

            /// <summary>
            /// The node will be displayed using ANSI escape sequences.
            /// </summary>
            Ansi = 5,
        }

        /// <summary>
        /// Constructs an instance from a ProgressRecord.
        /// </summary>
        internal
        ProgressNode(long sourceId, ProgressRecord record)
            : base(record.ActivityId, record.Activity, record.StatusDescription)
        {
            Dbg.Assert(record.RecordType == ProgressRecordType.Processing, "should only create node for Processing records");

            this.ParentActivityId = record.ParentActivityId;
            this.CurrentOperation = record.CurrentOperation;
            this.PercentComplete = Math.Min(record.PercentComplete, 100);
            this.SecondsRemaining = record.SecondsRemaining;
            this.RecordType = record.RecordType;

            this.Style = IsMinimalProgressRenderingEnabled()
                ? RenderStyle.Ansi
                : this.Style = RenderStyle.FullPlus;

            this.SourceId = sourceId;
        }

        /// <summary>
        /// Renders a single progress node as strings of text according to that node's style. The text is appended to the
        /// supplied list of strings.
        /// </summary>
        /// <param name="strCollection">
        /// List of strings to which the node's rendering will be appended.
        /// </param>
        /// <param name="indentation">
        /// The indentation level (in BufferCells) at which the node should be rendered.
        /// </param>
        /// <param name="maxWidth">
        /// The maximum number of BufferCells that the rendering is allowed to consume.
        /// </param>
        /// <param name="rawUI">
        /// The PSHostRawUserInterface used to gauge string widths in the rendering.
        /// </param>
        internal
        void
        Render(ArrayList strCollection, int indentation, int maxWidth, PSHostRawUserInterface rawUI)
        {
            Dbg.Assert(strCollection != null, "strCollection should not be null");
            Dbg.Assert(indentation >= 0, "indentation is negative");
            Dbg.Assert(this.RecordType != ProgressRecordType.Completed, "should never render completed records");

            switch (Style)
            {
                case RenderStyle.FullPlus:
                    RenderFull(strCollection, indentation, maxWidth, rawUI, isFullPlus: true);
                    break;
                case RenderStyle.Full:
                    RenderFull(strCollection, indentation, maxWidth, rawUI, isFullPlus: false);
                    break;
                case RenderStyle.Compact:
                    RenderCompact(strCollection, indentation, maxWidth, rawUI);
                    break;
                case RenderStyle.Minimal:
                    RenderMinimal(strCollection, indentation, maxWidth, rawUI);
                    break;
                case RenderStyle.Ansi:
                    RenderAnsi(strCollection, indentation, maxWidth);
                    break;
                case RenderStyle.Invisible:
                    // do nothing
                    break;
                default:
                    Dbg.Assert(false, "unrecognized RenderStyle value");
                    break;
            }
        }

        /// <summary>
        /// Renders a node in the "Full" style.
        /// </summary>
        /// <param name="strCollection">
        /// List of strings to which the node's rendering will be appended.
        /// </param>
        /// <param name="indentation">
        /// The indentation level (in BufferCells) at which the node should be rendered.
        /// </param>
        /// <param name="maxWidth">
        /// The maximum number of BufferCells that the rendering is allowed to consume.
        /// </param>
        /// <param name="rawUI">
        /// The PSHostRawUserInterface used to gauge string widths in the rendering.
        /// </param>
        /// <param name="isFullPlus">
        /// Indicate if the full StatusDescription and CurrentOperation should be displayed.
        /// </param>
        private
        void
        RenderFull(ArrayList strCollection, int indentation, int maxWidth, PSHostRawUserInterface rawUI, bool isFullPlus)
        {
            string indent = StringUtil.Padding(indentation);

            // First line: the activity

            strCollection.Add(
                StringUtil.TruncateToBufferCellWidth(
                    rawUI, StringUtil.Format(" {0}{1} ", indent, this.Activity), maxWidth));

            indentation += 3;
            indent = StringUtil.Padding(indentation);

            // Second line: the status description

            RenderFullDescription(this.StatusDescription, indent, maxWidth, rawUI, strCollection, isFullPlus);

            // Third line: the percentage thermometer. The size of this is proportional to the width we're allowed
            // to consume. -2 for the whitespace, -2 again for the brackets around thermo, -5 to not be too big

            if (PercentComplete >= 0)
            {
                int thermoWidth = Math.Max(3, maxWidth - indentation - 2 - 2 - 5);
                int mercuryWidth = 0;
                mercuryWidth = PercentComplete * thermoWidth / 100;
                if (PercentComplete < 100 && mercuryWidth == thermoWidth)
                {
                    // back off a tad unless we're totally complete to prevent the appearance of completion before
                    // the fact.

                    --mercuryWidth;
                }

                strCollection.Add(
                    StringUtil.TruncateToBufferCellWidth(
                        rawUI,
                        StringUtil.Format(
                            " {0}[{1}{2}] ",
                            indent,
                            new string('o', mercuryWidth),
                            StringUtil.Padding(thermoWidth - mercuryWidth)),
                        maxWidth));
            }

            // Fourth line: the seconds remaining

            if (SecondsRemaining >= 0)
            {
                TimeSpan span = new TimeSpan(0, 0, this.SecondsRemaining);

                strCollection.Add(
                    StringUtil.TruncateToBufferCellWidth(
                        rawUI,
                        " "
                        + StringUtil.Format(
                            ProgressNodeStrings.SecondsRemaining,
                            indent,
                            span)
                        + " ",
                    maxWidth));
            }

            // Fifth and Sixth lines: The current operation

            if (!string.IsNullOrEmpty(CurrentOperation))
            {
                strCollection.Add(" ");
                RenderFullDescription(this.CurrentOperation, indent, maxWidth, rawUI, strCollection, isFullPlus);
            }
        }

        private static void RenderFullDescription(string description, string indent, int maxWidth, PSHostRawUserInterface rawUi, ArrayList strCollection, bool isFullPlus)
        {
            string oldDescription = StringUtil.Format(" {0}{1} ", indent, description);
            string newDescription;

            do
            {
                newDescription = StringUtil.TruncateToBufferCellWidth(rawUi, oldDescription, maxWidth);
                strCollection.Add(newDescription);

                if (oldDescription.Length == newDescription.Length)
                {
                    break;
                }
                else
                {
                    oldDescription = StringUtil.Format(" {0}{1}", indent, oldDescription.Substring(newDescription.Length));
                }
            } while (isFullPlus);
        }

        /// <summary>
        /// Renders a node in the "Compact" style.
        /// </summary>
        /// <param name="strCollection">
        /// List of strings to which the node's rendering will be appended.
        /// </param>
        /// <param name="indentation">
        /// The indentation level (in BufferCells) at which the node should be rendered.
        /// </param>
        /// <param name="maxWidth">
        /// The maximum number of BufferCells that the rendering is allowed to consume.
        /// </param>
        /// <param name="rawUI">
        /// The PSHostRawUserInterface used to gauge string widths in the rendering.
        /// </param>
        private
        void
        RenderCompact(ArrayList strCollection, int indentation, int maxWidth, PSHostRawUserInterface rawUI)
        {
            string indent = StringUtil.Padding(indentation);

            // First line: the activity

            strCollection.Add(
                StringUtil.TruncateToBufferCellWidth(
                    rawUI,
                    StringUtil.Format(" {0}{1} ", indent, this.Activity), maxWidth));

            indentation += 3;
            indent = StringUtil.Padding(indentation);

            // Second line: the status description with percentage and time remaining, if applicable.

            string percent = string.Empty;
            if (PercentComplete >= 0)
            {
                percent = StringUtil.Format("{0}% ", PercentComplete);
            }

            string secRemain = string.Empty;
            if (SecondsRemaining >= 0)
            {
                TimeSpan span = new TimeSpan(0, 0, SecondsRemaining);
                secRemain = span.ToString() + " ";
            }

            strCollection.Add(
                StringUtil.TruncateToBufferCellWidth(
                    rawUI,
                    StringUtil.Format(
                        " {0}{1}{2}{3} ",
                        indent,
                        percent,
                        secRemain,
                        StatusDescription),
                    maxWidth));

            // Third line: The current operation

            if (!string.IsNullOrEmpty(CurrentOperation))
            {
                strCollection.Add(
                    StringUtil.TruncateToBufferCellWidth(
                        rawUI,
                        StringUtil.Format(" {0}{1} ", indent, this.CurrentOperation), maxWidth));
            }
        }

        /// <summary>
        /// Renders a node in the "Minimal" style.
        /// </summary>
        /// <param name="strCollection">
        /// List of strings to which the node's rendering will be appended.
        /// </param>
        /// <param name="indentation">
        /// The indentation level (in BufferCells) at which the node should be rendered.
        /// </param>
        /// <param name="maxWidth">
        /// The maximum number of BufferCells that the rendering is allowed to consume.
        /// </param>
        /// <param name="rawUI">
        /// The PSHostRawUserInterface used to gauge string widths in the rendering.
        /// </param>
        private
        void
        RenderMinimal(ArrayList strCollection, int indentation, int maxWidth, PSHostRawUserInterface rawUI)
        {
            string indent = StringUtil.Padding(indentation);

            // First line: Everything mushed into one line

            string percent = string.Empty;
            if (PercentComplete >= 0)
            {
                percent = StringUtil.Format("{0}% ", PercentComplete);
            }

            string secRemain = string.Empty;
            if (SecondsRemaining >= 0)
            {
                TimeSpan span = new TimeSpan(0, 0, SecondsRemaining);
                secRemain = span.ToString() + " ";
            }

            strCollection.Add(
                StringUtil.TruncateToBufferCellWidth(
                    rawUI,
                    StringUtil.Format(
                        " {0}{1} {2}{3}{4} ",
                        indent,
                        Activity,
                        percent,
                        secRemain,
                        StatusDescription),
                    maxWidth));
        }

        internal static bool IsMinimalProgressRenderingEnabled()
        {
            return PSStyle.Instance.Progress.View == ProgressView.Minimal;
        }

        /// <summary>
        /// Renders a node in the "ANSI" style.
        /// </summary>
        /// <param name="strCollection">
        /// List of strings to which the node's rendering will be appended.
        /// </param>
        /// <param name="indentation">
        /// The indentation level in chars at which the node should be rendered.
        /// </param>
        /// <param name="maxWidth">
        /// The maximum number of chars that the rendering is allowed to consume.
        /// </param>
        private
        void
        RenderAnsi(ArrayList strCollection, int indentation, int maxWidth)
        {
            string indent = StringUtil.Padding(indentation);
            string secRemain = string.Empty;
            if (SecondsRemaining >= 0)
            {
                secRemain = SecondsRemaining.ToString() + "s";
            }

            int secRemainLength = secRemain.Length + 1;

            // limit progress bar to 120 chars as no need to render full width
            if (PSStyle.Instance.Progress.MaxWidth > 0 && maxWidth > PSStyle.Instance.Progress.MaxWidth)
            {
                maxWidth = PSStyle.Instance.Progress.MaxWidth;
            }

            // if the activity is really long, only use up to half the width
            string activity;
            if (Activity.Length > maxWidth / 2)
            {
                activity = Activity.Substring(0, maxWidth / 2) + PSObjectHelper.Ellipsis;
            }
            else
            {
                activity = Activity;
            }

            // 4 is for the extra space and square brackets below and one extra space
            int barWidth = maxWidth - activity.Length - indentation - 4;

            var sb = new StringBuilder();
            int padding = maxWidth + PSStyle.Instance.Progress.Style.Length + PSStyle.Instance.Reverse.Length + PSStyle.Instance.ReverseOff.Length;
            sb.Append(PSStyle.Instance.Reverse);

            int maxStatusLength = barWidth - secRemainLength - 1;
            if (maxStatusLength > 0 && StatusDescription.Length > barWidth - secRemainLength)
            {
                sb.Append(StatusDescription.AsSpan(0, barWidth - secRemainLength - 1));
                sb.Append(PSObjectHelper.Ellipsis);
            }
            else
            {
                sb.Append(StatusDescription);
            }

            int emptyPadLength = barWidth + PSStyle.Instance.Reverse.Length - sb.Length - secRemainLength;
            if (emptyPadLength > 0)
            {
                sb.Append(string.Empty.PadRight(emptyPadLength));
            }

            sb.Append(secRemain);

            if (PercentComplete >= 0 && PercentComplete < 100 && barWidth > 0)
            {
                int barLength = PercentComplete * barWidth / 100;
                if (barLength >= barWidth)
                {
                    barLength = barWidth - 1;
                }

                if (barLength < sb.Length)
                {
                    sb.Insert(barLength + PSStyle.Instance.Reverse.Length, PSStyle.Instance.ReverseOff);
                }
            }
            else
            {
                sb.Append(PSStyle.Instance.ReverseOff);
            }

            strCollection.Add(
                StringUtil.Format(
                    "{0}{1}{2} [{3}]{4}",
                    indent,
                    PSStyle.Instance.Progress.Style,
                    activity,
                    sb.ToString(),
                    PSStyle.Instance.Reset)
                .PadRight(padding));
        }

        /// <summary>
        /// The nodes that have this node as their parent.
        /// </summary>
        internal
        ArrayList
        Children;

        /// <summary>
        /// The "age" of the node.  A node's age is incremented by PendingProgress.Update each time a new ProgressRecord is
        /// received by the host. A node's age is reset when a corresponding ProgressRecord is received.  Thus, the age of
        /// a node reflects the number of ProgressRecord that have been received since the node was last updated.
        ///
        /// The age is used by PendingProgress.Render to determine which nodes should be rendered on the display, and how. As the
        /// display has finite size, it may be possible to have many more outstanding progress activities than will fit in that
        /// space. The rendering of nodes can be progressively "compressed" into a more terse format, or not rendered at all in
        /// order to fit as many nodes as possible in the available space. The oldest nodes are compressed or skipped first.
        /// </summary>
        internal
        int
        Age;

        /// <summary>
        /// The style in which this node should be rendered.
        /// </summary>
        internal
        RenderStyle
        Style = RenderStyle.FullPlus;

        /// <summary>
        /// Identifies the source of the progress record.
        /// </summary>
        internal
        long
        SourceId;

        /// <summary>
        /// The number of vertical BufferCells that are required to render the node in its current style.
        /// </summary>
        /// <value></value>
        internal int LinesRequiredMethod(PSHostRawUserInterface rawUi, int maxWidth)
        {
            Dbg.Assert(this.RecordType != ProgressRecordType.Completed, "should never render completed records");

            switch (Style)
            {
                case RenderStyle.FullPlus:
                    return LinesRequiredInFullStyleMethod(rawUi, maxWidth, isFullPlus: true);

                case RenderStyle.Full:
                    return LinesRequiredInFullStyleMethod(rawUi, maxWidth, isFullPlus: false);

                case RenderStyle.Compact:
                    return LinesRequiredInCompactStyle;

                case RenderStyle.Minimal:
                    return 1;

                case RenderStyle.Invisible:
                    return 0;

                case RenderStyle.Ansi:
                    return 1;

                default:
                    Dbg.Assert(false, "Unknown RenderStyle value");
                    break;
            }

            return 0;
        }

        /// <summary>
        /// The number of vertical BufferCells that are required to render the node in the Full style.
        /// </summary>
        /// <value></value>
        private int LinesRequiredInFullStyleMethod(PSHostRawUserInterface rawUi, int maxWidth, bool isFullPlus)
        {
            // Since the fields of this instance could have been changed, we compute this on-the-fly.

            // NTRAID#Windows OS Bugs-1062104-2004/12/15-sburns we assume 1 line for each field.  If we ever need to
            // word-wrap text fields, then this calculation will need updating.

            // Start with 1 for the Activity
            int lines = 1;
            // Use 5 spaces as the heuristic indent. 5 spaces stand for the indent for the CurrentOperation of the first-level child node
            var indent = StringUtil.Padding(5);
            var temp = new ArrayList();

            if (isFullPlus)
            {
                temp.Clear();
                RenderFullDescription(StatusDescription, indent, maxWidth, rawUi, temp, isFullPlus: true);
                lines += temp.Count;
            }
            else
            {
                // 1 for the Status
                lines++;
            }

            if (PercentComplete >= 0)
            {
                ++lines;
            }

            if (SecondsRemaining >= 0)
            {
                ++lines;
            }

            if (!string.IsNullOrEmpty(CurrentOperation))
            {
                if (isFullPlus)
                {
                    lines += 1;
                    temp.Clear();
                    RenderFullDescription(CurrentOperation, indent, maxWidth, rawUi, temp, isFullPlus: true);
                    lines += temp.Count;
                }
                else
                {
                    lines += 2;
                }
            }

            return lines;
        }

        /// <summary>
        /// The number of vertical BufferCells that are required to render the node in the Compact style.
        /// </summary>
        /// <value></value>
        private
        int
        LinesRequiredInCompactStyle
        {
            get
            {
                // Since the fields of this instance could have been changed, we compute this on-the-fly.

                // NTRAID#Windows OS Bugs-1062104-2004/12/15-sburns we assume 1 line for each field.  If we ever need to
                // word-wrap text fields, then this calculation will need updating.

                // Start with 1 for the Activity, and 1 for the Status.

                int lines = 2;
                if (!string.IsNullOrEmpty(CurrentOperation))
                {
                    ++lines;
                }

                return lines;
            }
        }
    }
}   // namespace
