using System.Text.RegularExpressions;
using Beam.Abstractions;
using HtmlAgilityPack;

namespace Beam.Dynamic;

public enum SearchStrategy {
    DepthFirst,
    BreadthFirst,
}

public enum SearchStringDefaultSelection {
    First,
    Last,
    GreatestChildren,
    Any,
    None,
}

public class SearchStringOptions {
    public required bool SearchStringIsRegex { get; set; }
    public required bool SearchInBody { get; set; }
    public required string? SearchInAttribute { get; set; }
    
    /// <summary>
    /// Only used when both <see cref="SearchInBody"/> and <see cref="SearchInAttribute"/> are false/null, or no match is found for the search criteria.
    /// </summary>
    public SearchStringDefaultSelection DefaultSelection { get; set; } = SearchStringDefaultSelection.First;
    
    /// <summary>
    /// Only used when <see cref="SearchStringIsRegex"/> is false.
    /// </summary>
    public IEqualityComparer<string> UseComparer { get; set; } = StringComparer.CurrentCulture;
}

public class SelectDataProvider : IComposableDataProvider<HtmlNode?>, IManySelectionComposableDataProvider<HtmlNode?> {
    public SearchStrategy SearchStrategyType { get; set; } = SearchStrategy.DepthFirst;
    public SearchStringOptions SearchStringOptions { get; set; } = new SearchStringOptions() {
        SearchStringIsRegex = false,
        SearchInBody = true,
        SearchInAttribute = null
    };
    
    public string? SearchString { get; set; }
    public IBinding? Content { get; set; }
    
    /// <summary>
    /// Returns the first node that matches the search criteria.
    /// </summary>
    /// <param name="document"></param>
    /// <returns></returns>
    public HtmlNode? Get(HtmlDocument document) {
        return Select(document);
    }
    /// <summary>
    /// Returns the first child node that matches the search criteria.
    /// </summary>
    /// <param name="node"></param>
    /// <returns></returns>
    public HtmlNode? Get(HtmlNode node) {
        return Select(node);
    }

    public HtmlNode? Get(HtmlNode[] node) {
        throw new NotSupportedException();
    }
    
    public HtmlNode[]? _Select(HtmlNode node) {
        LinkedList<HtmlNode> searchSet = new();
        LinkedListNode<HtmlNode> currentNode = searchSet.AddLast(node);
        HashSet<HtmlNode> visited = [node];
        
        void breadthFirst(HtmlNode node) {
            foreach (var child in node.ChildNodes) {
                if (visited.Contains(child))
                    continue;
                searchSet.AddLast(child);
                visited.Add(child);
            }
        }

        void depthFirst(HtmlNode node) {
            foreach (var child in node.ChildNodes.Reverse()) {
                if (visited.Contains(child))
                    continue;
                searchSet.AddAfter(currentNode, child);
                visited.Add(child);
            }
        }

        Action<HtmlNode> enqueueStartegy = SearchStrategyType switch {
            SearchStrategy.BreadthFirst => breadthFirst,
            SearchStrategy.DepthFirst => depthFirst,
            _ => throw new NotSupportedException()
        };

        var bestCandidate = currentNode.Value;
        List<HtmlNode> selected = [];
        
        do {
            var n = currentNode.Value;
            if (SearchStringOptions.SearchInBody)
                if (SearchStringOptions.SearchStringIsRegex && Regex.IsMatch(n.InnerText ?? "", SearchString ?? ""))
                    selected.Add(n);
                else if (SearchStringOptions.UseComparer.Equals(n.InnerText, SearchString ?? ""))
                    selected.Add(n);
            if (SearchStringOptions.SearchInAttribute is not null)
                if (SearchStringOptions.SearchStringIsRegex && n.GetAttributeValue(SearchStringOptions.SearchInAttribute, null) != null &&
                    Regex.IsMatch(n.GetAttributeValue(SearchStringOptions.SearchInAttribute, ""), SearchString ?? ""))
                    selected.Add(n);
                else if (SearchStringOptions.UseComparer.Equals(n.GetAttributeValue(SearchStringOptions.SearchInAttribute, null), SearchString ?? ""))
                    selected.Add(n);

            switch (SearchStringOptions.DefaultSelection) {
                case SearchStringDefaultSelection.GreatestChildren:
                    if (n.ChildNodes.Count > bestCandidate.ChildNodes.Count)
                        bestCandidate = n;
                    break;
                case SearchStringDefaultSelection.Last:
                    bestCandidate = n;
                    break;
                case SearchStringDefaultSelection.Any:
                case SearchStringDefaultSelection.First:
                case SearchStringDefaultSelection.None:
                default:
                    break;
            }
            enqueueStartegy(n);

        } while ((currentNode = currentNode.Next!) != null);

        if (selected.Count == 0 && SearchStringOptions.DefaultSelection != SearchStringDefaultSelection.None)
            selected.Add(bestCandidate);
        
        return selected.ToArray();
    }

    public HtmlNode? Select(HtmlDocument document) {
        return Select(Content?.Select(document) ?? document.DocumentNode);
    }
    
    public HtmlNode? Select(HtmlNode node) {
        return _Select(node)?.FirstOrDefault();
    }

    public HtmlNode? ManyGet(HtmlNode[] node) {
        throw new NotSupportedException();
    }
    public HtmlNode[]? SelectMany(HtmlDocument doc) {
        return _Select(Content?.Select(doc) ?? doc.DocumentNode);
    }
    public HtmlNode[]? SelectMany(HtmlNode[] node) {
        return node.SelectMany(x => _Select(x) ?? []).ToArray();
    }
}