// Asset Naming Manager — the config asset your whole team shares.
using System.Collections.Generic;
using UnityEngine;

namespace AssetNamingManager
{
    /// <summary>
    /// The project's naming convention, written down as an asset instead of living in one
    /// person's head. Drop it in version control and the whole team is on the same rules.
    /// </summary>
    [CreateAssetMenu(
        fileName = "NamingRules",
        menuName = "Asset Naming Manager/Naming Rule Set",
        order = 320)]
    public class NamingRuleSet : ScriptableObject
    {
        /// <summary>Bumped when the serialized layout changes so old assets can be migrated.</summary>
        public const int CurrentVersion = 1;

        [HideInInspector] public int version = CurrentVersion;

        /// <summary>
        /// Name of the preset this rule set was last seeded from, so the window can show which one
        /// you are on. It is a record of where the rules came from, not a live link — edit a rule
        /// and this still says whatever you loaded last.
        /// </summary>
        [HideInInspector] public string lastAppliedPreset = string.Empty;

        // ------------------------------------------------------------- scope

        [Header("Where to look")]
        [Tooltip("Folders the scan walks. Keep this tight in a big project — it is the single " +
                 "biggest lever on scan speed.")]
        public string[] scanRoots = { "Assets" };

        [Tooltip("Paths the scan skips, as globs. Third-party art and imported plugins do not " +
                 "follow your convention and should never appear in your report.")]
        public string[] ignoredPaths =
        {
            // The tool's own code, docs and settings are never the user's problem. Demo/ is
            // deliberately NOT ignored: its badly-named samples are what make the first scan
            // show something. Delete that folder, or add it here, once you have seen it work.
            "Assets/AssetNamingManager/Editor/**",
            "Assets/AssetNamingManager/Documentation/**",
            "Assets/AssetNamingManager/Settings/**",
            "Assets/Plugins/**",
            "Assets/ThirdParty/**",
            "Assets/StreamingAssets/**",
            "**/Editor Default Resources/**",
            "**.dll",
            "**.asmdef",
            "**.asmref"
        };

        // ------------------------------------------------------------- rules

        [Header("Rules")]
        [Tooltip("Checked top to bottom. When several rules match one asset, the most specific one wins.")]
        public List<NamingRule> rules = new List<NamingRule>();

        // ------------------------------------------------------------- project-wide checks

        [Header("Characters")]
        [Tooltip("Characters that may never appear in an asset name. Spaces are the big one: they " +
                 "break command-line tooling and some build pipelines.")]
        public string forbiddenCharacters = " !@#$%^&*()+=[]{};'\",<>?`~";

        [Tooltip("Also flag any character outside plain ASCII. Turn this on if your build farm or " +
                 "asset bundles have ever choked on an accented file name.")]
        public bool asciiOnly = true;

        [Tooltip("How loudly to report a banned character.")]
        public NamingSeverity illegalCharacterSeverity = NamingSeverity.Error;

        [Header("Duplicates")]
        [Tooltip("Find two assets with the same name. This is how a texture quietly ships twice.")]
        public bool checkDuplicateNames = true;

        [Tooltip("How closely two assets must match before they count as duplicates.")]
        public DuplicateScope duplicateScope = DuplicateScope.NameAndExtension;

        public NamingSeverity duplicateSeverity = NamingSeverity.Warning;

        [Header("Housekeeping")]
        [Tooltip("Flag assets sitting loose in the Assets root instead of a category folder.")]
        public bool checkLooseRootAssets = true;

        public NamingSeverity looseRootSeverity = NamingSeverity.Warning;

        [Tooltip("Flag folders that contain no assets. They are usually leftovers from a refactor.")]
        public bool checkEmptyFolders = true;

        public NamingSeverity emptyFolderSeverity = NamingSeverity.Info;

        [Tooltip("Flag junk words such as copy, final, new, temp, WIP inside an otherwise fine name.")]
        public bool checkRedundantTokens = true;

        [Tooltip("The junk words to look for. Edit freely — your team probably has its own.")]
        public string[] redundantTokens = NameUtility.DefaultRedundantTokens;

        public NamingSeverity redundantTokenSeverity = NamingSeverity.Warning;

        [Header("Folders")]
        [Tooltip("Also check folder names against a convention.")]
        public bool checkFolderNaming = false;

        public NamingConvention folderConvention = NamingConvention.PascalCase;

        public NamingSeverity folderNamingSeverity = NamingSeverity.Info;

        [Header("Numbering")]
        [Tooltip("Require trailing numbers to be zero padded, so Run_2 sorts after Run_10 correctly.")]
        public bool checkNumberPadding = false;

        [Tooltip("How many digits a trailing number must have. 2 means Run_01, 3 means Run_001.")]
        [Range(2, 5)]
        public int numberPaddingWidth = 2;

        public NamingSeverity numberPaddingSeverity = NamingSeverity.Info;

        [Header("Path length")]
        [Tooltip("Windows still has a 260 character path limit in plenty of build tools. " +
                 "Catching long paths early beats debugging a broken build.")]
        public bool checkPathLength = true;

        [Range(80, 250)]
        public int maxPathLength = 180;

        public NamingSeverity pathLengthSeverity = NamingSeverity.Warning;

        // ------------------------------------------------------------- automation

        [Header("Automation")]
        [Tooltip("Check assets as they are imported and warn in the console straight away.")]
        public bool checkOnImport = false;

        [Tooltip("Severity at which a command-line run returns a non-zero exit code and fails CI.")]
        public NamingSeverity ciFailOn = NamingSeverity.Error;

        [Tooltip("Where exported reports are written, relative to the project root (next to Assets). " +
                 "Keep it out of Assets so Unity does not re-import your reports.")]
        public string reportFolder = "NamingManagerReports";

        // ------------------------------------------------------------- helpers

        /// <summary>
        /// Returns the rule responsible for an asset, or null when no rule claims it.
        /// When several rules match, the most specific one wins; ties go to the topmost rule.
        /// </summary>
        public NamingRule ResolveRule(string assetPath, string extension, string typeName)
        {
            NamingRule best = null;
            int bestScore = -1;

            for (int i = 0; i < rules.Count; i++)
            {
                var rule = rules[i];
                if (rule == null || !rule.AppliesTo(assetPath, extension, typeName)) continue;

                int score = rule.Specificity;
                if (score > bestScore)
                {
                    best = rule;
                    bestScore = score;
                }
            }

            return best;
        }

        /// <summary>True when the path is inside one of the ignored globs.</summary>
        public bool IsIgnored(string assetPath)
        {
            return GlobMatcher.IsMatchAny(assetPath, ignoredPaths);
        }

        /// <summary>Scan roots with separators normalised and empties dropped.</summary>
        public string[] ResolvedScanRoots()
        {
            var list = new List<string>();
            if (scanRoots != null)
            {
                foreach (var root in scanRoots)
                {
                    if (string.IsNullOrWhiteSpace(root)) continue;
                    list.Add(GlobMatcher.NormalizePath(root));
                }
            }
            if (list.Count == 0) list.Add("Assets");
            return list.ToArray();
        }

        /// <summary>Collects every configuration mistake in this asset. Empty list means all good.</summary>
        public List<string> Validate()
        {
            var problems = new List<string>();

            if (rules == null || rules.Count == 0)
            {
                problems.Add("This rule set has no rules. Load a preset from the Rules tab to get started.");
                return problems;
            }

            var seenNames = new HashSet<string>();
            foreach (var rule in rules)
            {
                if (rule == null)
                {
                    problems.Add("The rule list contains an empty entry.");
                    continue;
                }

                rule.Validate(problems);

                if (!string.IsNullOrWhiteSpace(rule.displayName) && !seenNames.Add(rule.displayName))
                    problems.Add("Two rules are both called '" + rule.displayName + "'. Reports will be confusing.");
            }

            if (forbiddenCharacters != null && forbiddenCharacters.IndexOf('/') >= 0)
                problems.Add("'/' is in the forbidden character list. It is a path separator and can never appear in a file name anyway.");

            if (minRuleSeverityIsInfo() && ciFailOn == NamingSeverity.Error)
                problems.Add("No rule is set to Error, so a CI gate set to 'Error' can never fail. Consider lowering the gate to Warning.");

            return problems;
        }

        bool minRuleSeverityIsInfo()
        {
            foreach (var rule in rules)
            {
                if (rule != null && rule.enabled && rule.severity == NamingSeverity.Error) return false;
            }
            return illegalCharacterSeverity != NamingSeverity.Error;
        }

        void OnValidate()
        {
            // A rule set edit can change what a glob means, so drop the compiled pattern cache.
            GlobMatcher.ClearCache();
            if (numberPaddingWidth < 2) numberPaddingWidth = 2;
            if (maxPathLength < 80) maxPathLength = 80;
            version = CurrentVersion;
        }
    }
}
