namespace Spectre.Console;

/// <summary>
/// Representation of a file system path.
/// </summary>
public sealed class TextPath : IRenderable, IHasJustification
{
    private const string Ellipsis = "...";
    private const string UnicodeEllipsis = "…";

    private readonly string[] _parts;
    private readonly bool _rooted;
    private readonly bool _windows;

    /// <summary>
    /// Gets or sets the root style.
    /// </summary>
    public Style? RootStyle { get; set; }

    /// <summary>
    /// Gets or sets the separator style.
    /// </summary>
    public Style? SeparatorStyle { get; set; }

    /// <summary>
    /// Gets or sets the stem style.
    /// </summary>
    public Style? StemStyle { get; set; }

    /// <summary>
    /// Gets or sets the leaf style.
    /// </summary>
    public Style? LeafStyle { get; set; }

    /// <summary>
    /// Gets or sets the alignment.
    /// </summary>
    public Justify? Justification { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="TextPath"/> class.
    /// </summary>
    /// <param name="path">The path to render.</param>
    public TextPath(string path)
    {
        // Normalize the path
        path ??= string.Empty;
        path = path.Replace('\\', '/');
        path = path.TrimEnd('/').Trim();

        // Get the distinct parts
        _parts = path.Split(['/'], StringSplitOptions.RemoveEmptyEntries);

        // Rooted Unix path?
        if (path.StartsWith("/", StringComparison.Ordinal))
        {
            _rooted = true;
            _parts = new[] { "/" }.Concat(_parts).ToArray();
        }
        else if (_parts.Length > 0 && _parts[0].EndsWith(":", StringComparison.Ordinal))
        {
            // Rooted Windows path
            _rooted = true;
            _windows = true;
        }
    }

    /// <inheritdoc/>
    public Measurement Measure(RenderOptions options, int maxWidth)
    {
        var fitted = Fit(options, maxWidth);
        var separatorCount = fitted.Length - 1;
        var length = fitted.Sum(f => f.Length) + separatorCount;

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

    /// <inheritdoc/>
    public IEnumerable<Segment> Render(RenderOptions options, int maxWidth)
    {
        var rootStyle = RootStyle ?? Style.Plain;
        var separatorStyle = SeparatorStyle ?? Style.Plain;
        var stemStyle = StemStyle ?? Style.Plain;
        var leafStyle = LeafStyle ?? Style.Plain;

        var fitted = Fit(options, maxWidth);
        var parts = new List<Segment>();
        foreach (var (_, first, last, item) in fitted.Enumerate())
        {
            // Leaf?
            if (last)
            {
                parts.Add(new Segment(item, leafStyle));
            }
            else
            {
                if (first && _rooted)
                {
                    // Root
                    parts.Add(new Segment(item, rootStyle));

                    if (_windows)
                    {
                        // Windows root has a slash
                        parts.Add(new Segment("/", separatorStyle));
                    }
                }
                else
                {
                    // Normal path segment
                    parts.Add(new Segment(item, stemStyle));
                    parts.Add(new Segment("/", separatorStyle));
                }
            }
        }

        // Align the result
        Aligner.Align(parts, Justification, maxWidth);

        return parts;
    }

    private string[] Fit(RenderOptions options, int maxWidth)
    {
        // No parts?
        if (_parts.Length == 0)
        {
            return _parts;
        }

        // Will it fit as is?
        if (_parts.Sum(Cell.GetCellLength) + (_parts.Length - 1) <= maxWidth)
        {
            return _parts;
        }

        var ellipsis = options.Unicode ? UnicodeEllipsis : Ellipsis;
        var ellipsisLength = Cell.GetCellLength(ellipsis);

        if (_parts.Length >= 2)
        {
            var skip = _rooted ? 1 : 0;
            var separatorCount = _rooted ? 2 : 1;
            var rootLength = _rooted ? Cell.GetCellLength(_parts[0]) : 0;

            // Try popping parts until it fits
            var queue = new Queue<string>(_parts.Skip(skip).Take(_parts.Length - separatorCount));
            while (queue.Count > 0)
            {
                // Remove the first item
                queue.Dequeue();

                // Get the current queue width in cells
                var queueWidth =
                        rootLength // Root (if rooted)
                        + ellipsisLength // Ellipsis
                        + queue.Sum(Cell.GetCellLength) // Middle
                        + Cell.GetCellLength(_parts.Last()) // Last
                        + queue.Count + separatorCount; // Separators

                // Will it fit?
                if (maxWidth >= queueWidth)
                {
                    var result = new List<string>();

                    if (_rooted)
                    {
                        // Add the root
                        result.Add(_parts[0]);
                    }

                    result.Add(ellipsis);
                    result.AddRange(queue);
                    result.Add(_parts.Last());

                    return result.ToArray();
                }
            }
        }

        // Just trim the last part so it fits
        var last = _parts.Last();
        var take = Math.Min(last.Length, Math.Max(0, maxWidth - ellipsisLength));
        var start = Math.Max(0, last.Length - take);

        return [string.Concat(ellipsis, last.Substring(start, take))];
    }
}

/// <summary>
/// Contains extension methods for <see cref="TextPath"/>.
/// </summary>
public static class TextPathExtensions
{
    /// <summary>
    /// Sets the separator style.
    /// </summary>
    /// <param name="obj">The path.</param>
    /// <param name="style">The separator style to set.</param>
    /// <returns>The same instance so that multiple calls can be chained.</returns>
    public static TextPath SeparatorStyle(this TextPath obj, Style style)
    {
        ArgumentNullException.ThrowIfNull(obj);

        obj.SeparatorStyle = style;
        return obj;
    }

    /// <summary>
    /// Sets the separator color.
    /// </summary>
    /// <param name="obj">The path.</param>
    /// <param name="color">The separator color.</param>
    /// <returns>The same instance so that multiple calls can be chained.</returns>
    public static TextPath SeparatorColor(this TextPath obj, Color color)
    {
        return SeparatorStyle(obj, new Style(foreground: color));
    }

    /// <summary>
    /// Sets the root style.
    /// </summary>
    /// <param name="obj">The path.</param>
    /// <param name="style">The root style to set.</param>
    /// <returns>The same instance so that multiple calls can be chained.</returns>
    public static TextPath RootStyle(this TextPath obj, Style style)
    {
        ArgumentNullException.ThrowIfNull(obj);

        obj.RootStyle = style;
        return obj;
    }

    /// <summary>
    /// Sets the root color.
    /// </summary>
    /// <param name="obj">The path.</param>
    /// <param name="color">The root color.</param>
    /// <returns>The same instance so that multiple calls can be chained.</returns>
    public static TextPath RootColor(this TextPath obj, Color color)
    {
        return RootStyle(obj, new Style(foreground: color));
    }

    /// <summary>
    /// Sets the stem style.
    /// </summary>
    /// <param name="obj">The path.</param>
    /// <param name="style">The stem style to set.</param>
    /// <returns>The same instance so that multiple calls can be chained.</returns>
    public static TextPath StemStyle(this TextPath obj, Style style)
    {
        ArgumentNullException.ThrowIfNull(obj);

        obj.StemStyle = style;
        return obj;
    }

    /// <summary>
    /// Sets the stem color.
    /// </summary>
    /// <param name="obj">The path.</param>
    /// <param name="color">The stem color.</param>
    /// <returns>The same instance so that multiple calls can be chained.</returns>
    public static TextPath StemColor(this TextPath obj, Color color)
    {
        return StemStyle(obj, new Style(foreground: color));
    }

    /// <summary>
    /// Sets the leaf style.
    /// </summary>
    /// <param name="obj">The path.</param>
    /// <param name="style">The stem leaf to set.</param>
    /// <returns>The same instance so that multiple calls can be chained.</returns>
    public static TextPath LeafStyle(this TextPath obj, Style style)
    {
        ArgumentNullException.ThrowIfNull(obj);

        obj.LeafStyle = style;
        return obj;
    }

    /// <summary>
    /// Sets the leaf color.
    /// </summary>
    /// <param name="obj">The path.</param>
    /// <param name="color">The leaf color.</param>
    /// <returns>The same instance so that multiple calls can be chained.</returns>
    public static TextPath LeafColor(this TextPath obj, Color color)
    {
        return LeafStyle(obj, new Style(foreground: color));
    }
}