// Asset Naming Manager — one rule row inside a rule set.
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;

namespace AssetNamingManager
{
    /// <summary>
    /// One naming rule: "assets that look like <em>this</em> must be named like <em>that</em>
    /// and live over <em>there</em>."
    /// </summary>
    /// <remarks>
    /// A rule is picked for an asset when every populated matcher agrees. Empty matchers are
    /// wildcards, so a rule with only <see cref="extensions"/> set applies to every asset with
    /// those extensions anywhere in the project.
    /// </remarks>
    [Serializable]
    public class NamingRule
    {
        // ------------------------------------------------------------- identity

        [Tooltip("Shown in the report and in the rules list. Keep it short: 'Textures', 'UI Sprites'.")]
        public string displayName = "New Rule";

        [Tooltip("Turn a rule off without deleting it. Disabled rules are skipped entirely.")]
        public bool enabled = true;

        [Tooltip("How loudly this rule complains. Only Error fails a CI gate set to 'Error'.")]
        public NamingSeverity severity = NamingSeverity.Warning;

        [Tooltip("Optional note for your team. Shown in the details pane next to every finding.")]
        [TextArea(1, 3)]
        public string note = "";

        // ------------------------------------------------------------- matchers

        [Tooltip("File extensions this rule covers, comma separated. Example: .png, .jpg, .tga\n" +
                 "Leave empty to match any extension.")]
        public string extensions = "";

        [Tooltip("Unity type names this rule covers, comma separated. Example: Texture2D, Sprite\n" +
                 "Leave empty to match any type.")]
        public string typeNames = "";

        [Tooltip("Only look at assets whose path matches one of these globs.\n" +
                 "Example: Assets/_Game/Art/**\nLeave empty to look everywhere.")]
        public string[] pathFilters = new string[0];

        // ------------------------------------------------------------- name checks

        [Tooltip("Required start of the file name. Several alternatives allowed: T_, TEX_\n" +
                 "Leave empty to skip the prefix check.")]
        public string requiredPrefix = "";

        [Tooltip("Required end of the file name, before the extension. Several alternatives allowed.\n" +
                 "Leave empty to skip the suffix check.")]
        public string requiredSuffix = "";

        [Tooltip("Casing the name must use, checked after the prefix and suffix are stripped off.")]
        public NamingConvention convention = NamingConvention.PascalCase;

        [Tooltip("Shortest acceptable name, prefix included. 0 disables the check.")]
        public int minLength = 3;

        [Tooltip("Longest acceptable name, prefix included. 0 disables the check.")]
        public int maxLength = 64;

        [Tooltip("Optional regular expression applied to the whole file name (no extension).")]
        public string customRegex = "";

        [Tooltip("On: the name must match the regex. Off: the name must NOT match it.")]
        public bool regexMustMatch = true;

        // ------------------------------------------------------------- folder checks

        [Tooltip("Folders this asset type is allowed to live in, as globs.\n" +
                 "Example: Assets/_Game/Art/Textures/**\nLeave empty to allow any folder.")]
        public string[] allowedFolders = new string[0];

        [Tooltip("Folders this asset type must never live in, as globs.")]
        public string[] forbiddenFolders = new string[0];

        [Tooltip("When a folder violation is found, offer to move the asset into this folder.\n" +
                 "Leave empty to report the problem without offering a move.")]
        public string preferredFolder = "";

        // ------------------------------------------------------------- fixing

        [Tooltip("Off: this rule reports problems but never proposes an automatic rename or move.")]
        public bool allowAutoFix = true;

        // ------------------------------------------------------------- runtime caches

        [NonSerialized] Regex _compiledRegex;
        [NonSerialized] string _compiledRegexSource;
        [NonSerialized] string[] _extensionCache;
        [NonSerialized] string _extensionCacheSource;
        [NonSerialized] string[] _typeCache;
        [NonSerialized] string _typeCacheSource;
        [NonSerialized] string[] _prefixCache;
        [NonSerialized] string _prefixCacheSource;
        [NonSerialized] string[] _suffixCache;
        [NonSerialized] string _suffixCacheSource;

        /// <summary>Extensions this rule matches, already split and lower-cased.</summary>
        public string[] Extensions
        {
            get
            {
                if (_extensionCache == null || _extensionCacheSource != extensions)
                {
                    var raw = NameUtility.SplitList(extensions);
                    var list = new List<string>(raw.Length);
                    foreach (var r in raw)
                    {
                        string e = r.ToLowerInvariant();
                        if (!e.StartsWith(".", StringComparison.Ordinal)) e = "." + e;
                        list.Add(e);
                    }
                    _extensionCache = list.ToArray();
                    _extensionCacheSource = extensions;
                }
                return _extensionCache;
            }
        }

        /// <summary>Unity type names this rule matches.</summary>
        public string[] TypeNames
        {
            get
            {
                if (_typeCache == null || _typeCacheSource != typeNames)
                {
                    _typeCache = NameUtility.SplitList(typeNames);
                    _typeCacheSource = typeNames;
                }
                return _typeCache;
            }
        }

        /// <summary>Accepted prefixes. Any one of them satisfies the rule.</summary>
        public string[] Prefixes
        {
            get
            {
                if (_prefixCache == null || _prefixCacheSource != requiredPrefix)
                {
                    _prefixCache = NameUtility.SplitList(requiredPrefix);
                    _prefixCacheSource = requiredPrefix;
                }
                return _prefixCache;
            }
        }

        /// <summary>Accepted suffixes. Any one of them satisfies the rule.</summary>
        public string[] Suffixes
        {
            get
            {
                if (_suffixCache == null || _suffixCacheSource != requiredSuffix)
                {
                    _suffixCache = NameUtility.SplitList(requiredSuffix);
                    _suffixCacheSource = requiredSuffix;
                }
                return _suffixCache;
            }
        }

        /// <summary>Compiled <see cref="customRegex"/>, or null when it is empty or invalid.</summary>
        public Regex CompiledRegex
        {
            get
            {
                if (string.IsNullOrWhiteSpace(customRegex)) return null;
                if (_compiledRegex == null || _compiledRegexSource != customRegex)
                {
                    try
                    {
                        _compiledRegex = new Regex(customRegex, RegexOptions.CultureInvariant);
                    }
                    catch (ArgumentException)
                    {
                        _compiledRegex = null;   // surfaced by Validate(), not thrown at scan time
                    }
                    _compiledRegexSource = customRegex;
                }
                return _compiledRegex;
            }
        }

        // ------------------------------------------------------------- matching

        /// <summary>
        /// True when this rule is responsible for the given asset. Every populated matcher must
        /// agree; empty matchers are treated as "anything".
        /// </summary>
        public bool AppliesTo(string assetPath, string extension, string typeName)
        {
            if (!enabled) return false;

            var exts = Extensions;
            if (exts.Length > 0)
            {
                bool hit = false;
                for (int i = 0; i < exts.Length && !hit; i++)
                    hit = string.Equals(exts[i], extension, StringComparison.OrdinalIgnoreCase);
                if (!hit) return false;
            }

            var types = TypeNames;
            if (types.Length > 0)
            {
                bool hit = false;
                for (int i = 0; i < types.Length && !hit; i++)
                    hit = string.Equals(types[i], typeName, StringComparison.OrdinalIgnoreCase);
                if (!hit) return false;
            }

            if (pathFilters != null && pathFilters.Length > 0 && !GlobMatcher.IsMatchAny(assetPath, pathFilters))
                return false;

            return true;
        }

        /// <summary>How specific this rule is. The scanner picks the highest score when several match.</summary>
        public int Specificity
        {
            get
            {
                int score = 0;
                if (Extensions.Length > 0) score += 4;
                if (TypeNames.Length > 0) score += 4;
                if (pathFilters != null && pathFilters.Length > 0) score += 8;
                return score;
            }
        }

        /// <summary>Returns the prefix this name already uses, or null when none of them fit.</summary>
        public string MatchedPrefix(string name)
        {
            foreach (var p in Prefixes)
            {
                if (name.StartsWith(p, StringComparison.Ordinal)) return p;
            }
            return null;
        }

        /// <summary>Returns the suffix this name already uses, or null when none of them fit.</summary>
        public string MatchedSuffix(string name)
        {
            foreach (var s in Suffixes)
            {
                if (name.EndsWith(s, StringComparison.Ordinal)) return s;
            }
            return null;
        }

        /// <summary>Collects configuration mistakes in this rule (bad regex, impossible lengths).</summary>
        public void Validate(List<string> problems)
        {
            if (string.IsNullOrWhiteSpace(displayName))
                problems.Add("A rule has no name.");

            if (!string.IsNullOrWhiteSpace(customRegex) && CompiledRegex == null)
                problems.Add("Rule '" + displayName + "' has an invalid regular expression.");

            if (minLength > 0 && maxLength > 0 && minLength > maxLength)
                problems.Add("Rule '" + displayName + "' has a minimum length larger than its maximum.");

            if (!string.IsNullOrWhiteSpace(preferredFolder) &&
                (allowedFolders == null || allowedFolders.Length == 0))
                problems.Add("Rule '" + displayName + "' has a preferred folder but no allowed folders, so it will never move anything.");
        }

        /// <summary>Deep copy, used by the preset picker and the "duplicate rule" button.</summary>
        public NamingRule Clone()
        {
            var copy = (NamingRule)MemberwiseClone();
            copy.pathFilters = pathFilters == null ? new string[0] : (string[])pathFilters.Clone();
            copy.allowedFolders = allowedFolders == null ? new string[0] : (string[])allowedFolders.Clone();
            copy.forbiddenFolders = forbiddenFolders == null ? new string[0] : (string[])forbiddenFolders.Clone();
            return copy;
        }
    }
}
