using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;

namespace Spectre.Console
{
    /// <summary>
    /// A paragraph of text where different parts
    /// of the paragraph can have individual styling.
    /// </summary>
    [DebuggerDisplay("{_text,nq}")]
    public sealed class Paragraph : Renderable, IAlignable, IOverflowable
    {
        private readonly List<SegmentLine> _lines;

        /// <summary>
        /// Gets or sets the alignment of the whole paragraph.
        /// </summary>
        public Justify? Alignment { get; set; }

        /// <summary>
        /// Gets or sets the text overflow strategy.
        /// </summary>
        public Overflow? Overflow { get; set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="Paragraph"/> class.
        /// </summary>
        public Paragraph()
        {
            _lines = new List<SegmentLine>();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Paragraph"/> class.
        /// </summary>
        /// <param name="text">The text.</param>
        /// <param name="style">The style of the text.</param>
        public Paragraph(string text, Style? style = null)
            : this()
        {
            if (text is null)
            {
                throw new ArgumentNullException(nameof(text));
            }

            Append(text, style);
        }

        /// <summary>
        /// Appends some text to this paragraph.
        /// </summary>
        /// <param name="text">The text to append.</param>
        /// <param name="style">The style of the appended text.</param>
        /// <returns>The same instance so that multiple calls can be chained.</returns>
        public Paragraph Append(string text, Style? style = null)
        {
            if (text is null)
            {
                throw new ArgumentNullException(nameof(text));
            }

            foreach (var (_, first, last, part) in text.SplitLines().Enumerate())
            {
                var current = part;

                if (first)
                {
                    var line = _lines.LastOrDefault();
                    if (line == null)
                    {
                        _lines.Add(new SegmentLine());
                        line = _lines.Last();
                    }

                    if (string.IsNullOrEmpty(current))
                    {
                        line.Add(Segment.Empty);
                    }
                    else
                    {
                        foreach (var span in current.SplitWords())
                        {
                            line.Add(new Segment(span, style ?? Style.Plain));
                        }
                    }
                }
                else
                {
                    var line = new SegmentLine();

                    if (string.IsNullOrEmpty(current))
                    {
                        line.Add(Segment.Empty);
                    }
                    else
                    {
                        foreach (var span in current.SplitWords())
                        {
                            line.Add(new Segment(span, style ?? Style.Plain));
                        }
                    }

                    _lines.Add(line);
                }
            }

            return this;
        }

        /// <inheritdoc/>
        protected override Measurement Measure(RenderContext context, int maxWidth)
        {
            if (_lines.Count == 0)
            {
                return new Measurement(0, 0);
            }

            var min = _lines.Max(line => line.Max(segment => segment.CellLength(context)));
            var max = _lines.Max(x => x.CellWidth(context));

            return new Measurement(min, Math.Min(max, maxWidth));
        }

        /// <inheritdoc/>
        protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
        {
            if (context is null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (_lines.Count == 0)
            {
                return Array.Empty<Segment>();
            }

            var lines = SplitLines(context, maxWidth);

            // Justify lines
            var justification = context.Justification ?? Alignment ?? Justify.Left;
            foreach (var (_, _, last, line) in lines.Enumerate())
            {
                var length = line.Sum(l => l.StripLineEndings().CellLength(context));
                if (length < maxWidth)
                {
                    // Justify right side
                    if (justification == Justify.Right)
                    {
                        var diff = maxWidth - length;
                        line.Prepend(new Segment(new string(' ', diff)));
                    }
                    else if (justification == Justify.Center)
                    {
                        // Left side.
                        var diff = (maxWidth - length) / 2;
                        line.Prepend(new Segment(new string(' ', diff)));

                        // Right side
                        line.Add(new Segment(new string(' ', diff)));
                        var remainder = (maxWidth - length) % 2;
                        if (remainder != 0)
                        {
                            line.Add(new Segment(new string(' ', remainder)));
                        }
                    }
                }
            }

            return new SegmentLineEnumerator(lines);
        }

        private List<SegmentLine> Clone()
        {
            var result = new List<SegmentLine>();

            foreach (var line in _lines)
            {
                var newLine = new SegmentLine();
                foreach (var segment in line)
                {
                    newLine.Add(segment);
                }

                result.Add(newLine);
            }

            return result;
        }

        private List<SegmentLine> SplitLines(RenderContext context, int maxWidth)
        {
            if (_lines.Max(x => x.CellWidth(context)) <= maxWidth)
            {
                return Clone();
            }

            var lines = new List<SegmentLine>();
            var line = new SegmentLine();

            var newLine = true;

            using var iterator = new SegmentLineIterator(_lines);
            var queue = new Queue<Segment>();
            while (true)
            {
                var current = (Segment?)null;
                if (queue.Count == 0)
                {
                    if (!iterator.MoveNext())
                    {
                        break;
                    }

                    current = iterator.Current;
                }
                else
                {
                    current = queue.Dequeue();
                }

                if (current == null)
                {
                    throw new InvalidOperationException("Iterator returned empty segment.");
                }

                newLine = false;

                if (current.IsLineBreak)
                {
                    lines.Add(line);
                    line = new SegmentLine();
                    newLine = true;
                    continue;
                }

                var length = current.CellLength(context);
                if (length > maxWidth)
                {
                    // The current segment is longer than the width of the console,
                    // so we will need to crop it up, into new segments.
                    var segments = Segment.SplitOverflow(current, Overflow, context, maxWidth);
                    if (segments.Count > 0)
                    {
                        if (line.CellWidth(context) + segments[0].CellLength(context) > maxWidth)
                        {
                            lines.Add(line);
                            line = new SegmentLine();
                            newLine = true;

                            segments.ForEach(s => queue.Enqueue(s));
                            continue;
                        }
                        else
                        {
                            // Add the segment and push the rest of them to the queue.
                            line.Add(segments[0]);
                            segments.Skip(1).ForEach(s => queue.Enqueue(s));
                            continue;
                        }
                    }
                }
                else
                {
                    if (line.CellWidth(context) + length > maxWidth)
                    {
                        line.Add(Segment.Empty);
                        lines.Add(line);
                        line = new SegmentLine();
                        newLine = true;
                    }
                }

                if (newLine && current.IsWhiteSpace)
                {
                    continue;
                }

                newLine = false;

                line.Add(current);
            }

            // Flush remaining.
            if (line.Count > 0)
            {
                lines.Add(line);
            }

            return lines;
        }
    }
}
