// Asset Naming Manager — tiny glob engine for path filters.
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

namespace AssetNamingManager
{
    /// <summary>
    /// Matches Unity asset paths against shell-style patterns so rules can say things like
    /// <c>Assets/Art/**/Textures</c> or <c>**/ThirdParty/**</c> without anyone writing a regex.
    /// </summary>
    /// <remarks>
    /// Supported syntax:
    /// <list type="bullet">
    /// <item><description><c>*</c> — any run of characters except <c>/</c></description></item>
    /// <item><description><c>**</c> — any run of characters including <c>/</c></description></item>
    /// <item><description><c>?</c> — exactly one character except <c>/</c></description></item>
    /// </list>
    /// A pattern with no wildcard is treated as a "contains this folder segment" test, which is
    /// what people intuitively expect when they type <c>Art/Textures</c> into a rule.
    /// </remarks>
    public static class GlobMatcher
    {
        static readonly Dictionary<string, Regex> Cache = new Dictionary<string, Regex>(StringComparer.Ordinal);

        /// <summary>Normalises separators and trims the trailing slash. Always call before comparing paths.</summary>
        public static string NormalizePath(string path)
        {
            if (string.IsNullOrEmpty(path)) return string.Empty;
            string p = path.Replace('\\', '/').Trim();
            while (p.Length > 1 && p.EndsWith("/", StringComparison.Ordinal)) p = p.Substring(0, p.Length - 1);
            return p;
        }

        /// <summary>True when <paramref name="path"/> matches <paramref name="pattern"/>.</summary>
        public static bool IsMatch(string path, string pattern)
        {
            if (string.IsNullOrWhiteSpace(pattern)) return false;

            string p = NormalizePath(path);
            string g = NormalizePath(pattern);

            if (g.IndexOf('*') < 0 && g.IndexOf('?') < 0)
            {
                // Literal pattern: treat it as a path fragment test on segment boundaries so that
                // "Art/Textures" matches "Assets/Game/Art/Textures/Hero/T_Hero.png".
                return ContainsSegment(p, g);
            }

            return GetRegex(g).IsMatch(p);
        }

        /// <summary>True when <paramref name="path"/> matches any pattern in the list.</summary>
        public static bool IsMatchAny(string path, IList<string> patterns)
        {
            if (patterns == null) return false;
            for (int i = 0; i < patterns.Count; i++)
            {
                if (IsMatch(path, patterns[i])) return true;
            }
            return false;
        }

        static bool ContainsSegment(string path, string fragment)
        {
            if (fragment.Length == 0) return false;
            if (string.Equals(path, fragment, StringComparison.OrdinalIgnoreCase)) return true;

            int idx = path.IndexOf(fragment, StringComparison.OrdinalIgnoreCase);
            while (idx >= 0)
            {
                bool leftOk = idx == 0 || path[idx - 1] == '/';
                int end = idx + fragment.Length;
                bool rightOk = end == path.Length || path[end] == '/';
                if (leftOk && rightOk) return true;
                idx = path.IndexOf(fragment, idx + 1, StringComparison.OrdinalIgnoreCase);
            }
            return false;
        }

        static Regex GetRegex(string pattern)
        {
            Regex rx;
            if (Cache.TryGetValue(pattern, out rx)) return rx;

            rx = new Regex(Translate(pattern), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
            if (Cache.Count > 512) Cache.Clear();      // keep the cache from growing forever
            Cache[pattern] = rx;
            return rx;
        }

        /// <summary>Converts a glob into an anchored regular expression. Public so tests can read it.</summary>
        public static string Translate(string pattern)
        {
            // A pattern ending in "/**" means "this folder and everything under it", so it has to
            // match the folder itself as well as its contents. Without this, an ignore entry such as
            // "Assets/Plugins/**" hides every asset inside Plugins but leaves the folder visible —
            // and since nothing inside it was scanned, the folder then reports as empty.
            bool wholeSubtree = pattern.EndsWith("/**", StringComparison.Ordinal);
            string core = wholeSubtree ? pattern.Substring(0, pattern.Length - 3) : pattern;

            var sb = new StringBuilder("^");

            for (int i = 0; i < core.Length; i++)
            {
                char c = core[i];
                switch (c)
                {
                    case '*':
                        bool doubleStar = i + 1 < core.Length && core[i + 1] == '*';
                        if (doubleStar)
                        {
                            i++;
                            // "**/" should also match zero folders, so Assets/**/T_*.png hits Assets/T_a.png.
                            if (i + 1 < core.Length && core[i + 1] == '/')
                            {
                                i++;
                                sb.Append("(?:.*/)?");
                            }
                            else
                            {
                                sb.Append(".*");
                            }
                        }
                        else
                        {
                            sb.Append("[^/]*");
                        }
                        break;

                    case '?':
                        sb.Append("[^/]");
                        break;

                    default:
                        sb.Append(Regex.Escape(c.ToString()));
                        break;
                }
            }

            if (wholeSubtree) sb.Append("(?:/.*)?");
            sb.Append("$");
            return sb.ToString();
        }

        /// <summary>Clears the compiled-pattern cache. Called when a rule set is edited.</summary>
        public static void ClearCache()
        {
            Cache.Clear();
        }
    }
}
