// Asset Naming Manager — catches a bad name the moment the asset lands in the project.
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace AssetNamingManager
{
    /// <summary>
    /// Runs the scanner over the handful of assets Unity just imported or moved, so a badly named
    /// file gets called out while the person who made it is still looking at it. Report-only:
    /// nothing here renames, moves or writes anything. <see cref="NamingFixer"/> is the only code
    /// that changes an asset, and it only ever runs from a button somebody pressed.
    /// </summary>
    /// <remarks>
    /// Silent unless <see cref="NamingRuleSet.checkOnImport"/> is ticked. This class sits in the
    /// path of every single import in the buyer's project, so the switched-off case is written to
    /// cost a string compare and nothing more.
    /// </remarks>
    public class NamingManagerImportHook : AssetPostprocessor
    {
        /// <summary>
        /// Console lines one import may produce. Dragging in a 2,000 file art pack should start a
        /// conversation, not bury every other message in the console.
        /// </summary>
        const int MaxReportedPerImport = 20;

        static NamingRuleSet _ruleSet;
        static string _resolvedGuid;
        static bool _reportedFailure;

        // With Fast Enter Play Mode - the default for new Unity 6.6 projects - the domain is
        // not reloaded, so statics survive between Play sessions. Clear the cache here so a
        // session never starts holding something the previous one left behind.
        [InitializeOnEnterPlayMode]
        static void ResetOnEnterPlayMode()
        {
            _ruleSet = null;
            _resolvedGuid = null;
            _reportedFailure = false;
        }

        // Unity dispatches this five-argument form on every version the package supports, and
        // didDomainReload is the only reliable way to tell a real import from the sweep that
        // follows a recompile — that sweep hands us the whole project and would re-report all of it.
        static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets,
                                           string[] movedAssets, string[] movedFromAssetPaths,
                                           bool didDomainReload)
        {
            // An exception thrown out of here aborts the buyer's import. Whatever goes wrong inside,
            // the import wins.
            try
            {
                if (didDomainReload) return;

                // Deletions are the whole story for a lot of batches, and a deleted asset has
                // nothing left to check.
                if (importedAssets.Length == 0 && movedAssets.Length == 0) return;

                // A domain reload is seconds away and will take these statics with it, so anything
                // logged now is noise nobody will act on.
                if (EditorApplication.isCompiling) return;

                NamingRuleSet ruleSet = ActiveRuleSet(importedAssets);
                if (ruleSet == null || !ruleSet.checkOnImport) return;

                Report(ruleSet, Collect(ruleSet, importedAssets, movedAssets));
            }
            catch (Exception e)
            {
                if (!_reportedFailure)
                {
                    // Once is enough: a hook that failed will fail again on the next import, and the
                    // buyer only needs telling that the check is not running.
                    _reportedFailure = true;
                    Debug.LogWarning(NamingManagerAbout.LogPrefix +
                                     "The import check hit a problem and was skipped: " + e.Message);
                }
            }
        }

        // ---------------------------------------------------------------- rule set

        // Resolved once and then held, because the cached reference IS the live asset — ticking
        // checkOnImport in the Inspector takes effect on the very next import with nothing to
        // invalidate. The GUID pref is a plain EditorPrefs read and catches the one thing the
        // reference cannot: the user switching to a different rule set in the window.
        static NamingRuleSet ActiveRuleSet(string[] imported)
        {
            string guid = NamingManagerSettings.ActiveRuleSetGuid;

            if (guid == _resolvedGuid)
            {
                if (_ruleSet != null) return _ruleSet;

                // We already know this project has no rule set. Searching for one again on every
                // import would tax every project that never bought into the tool, and only a new
                // .asset arriving can change the answer.
                if (!ContainsRuleSetCandidate(imported)) return null;
            }

            _ruleSet = NamingManagerSettings.ActiveRuleSet;

            // Auto-discovery writes the pref, so read back what it settled on rather than the value
            // we walked in with. Assigned last: this field is the cache guard, and a throw above
            // must leave us unresolved rather than confidently wrong.
            _resolvedGuid = NamingManagerSettings.ActiveRuleSetGuid;
            return _ruleSet;
        }

        // A rule set is a ScriptableObject, so only a .asset landing can turn "none" into "one".
        static bool ContainsRuleSetCandidate(string[] imported)
        {
            for (int i = 0; i < imported.Length; i++)
            {
                if (imported[i].EndsWith(".asset", StringComparison.OrdinalIgnoreCase)) return true;
            }
            return false;
        }

        // ---------------------------------------------------------------- collection

        // Only the paths that now exist: movedFromAssetPaths point at where an asset used to be,
        // and deletedAssets never reach this method at all.
        static List<string> Collect(NamingRuleSet ruleSet, string[] imported, string[] moved)
        {
            string[] roots = ruleSet.ResolvedScanRoots();
            var paths = new List<string>(imported.Length + moved.Length);

            Gather(paths, imported, roots);
            Gather(paths, moved, roots);
            return paths;
        }

        static void Gather(List<string> into, string[] candidates, string[] roots)
        {
            for (int i = 0; i < candidates.Length; i++)
            {
                string path = GlobMatcher.NormalizePath(candidates[i]);
                if (path.Length > 0 && IsUnderAnyRoot(path, roots)) into.Add(path);
            }
        }

        // An import carries paths a full scan would never look at: a file under Packages/, or a
        // folder the buyer deliberately left out of scanRoots. Honouring the roots here is what
        // keeps the hook and the full scan agreeing about which assets are in scope. Ignore globs —
        // including the ones covering this package's own Editor, Documentation and Settings
        // folders — are left to the scanner, which applies them to every path it is handed.
        static bool IsUnderAnyRoot(string path, string[] roots)
        {
            for (int i = 0; i < roots.Length; i++)
            {
                string root = roots[i];
                if (path.Length > root.Length &&
                    path[root.Length] == '/' &&
                    path.StartsWith(root, StringComparison.Ordinal)) return true;
            }
            return false;
        }

        // ---------------------------------------------------------------- reporting

        static void Report(NamingRuleSet ruleSet, List<string> paths)
        {
            if (paths.Count == 0) return;

            var options = new ScanOptions
            {
                explicitPaths = paths.ToArray(),
                includeFolders = true,

                // A batch of five files cannot see the other nine thousand assets, so it would miss
                // exactly the clash that matters. Duplicates stay with the full scan.
                includeDuplicates = false
            };

            ScanReport report = NamingScanner.Scan(ruleSet, options);

            int logged = 0;
            int suppressed = 0;

            foreach (var violation in report.violations)
            {
                // Every folder is empty for the moment between creating it and putting the first
                // thing in it. Reporting that at import time is a complaint about nothing.
                if (violation.kind == ViolationKind.EmptyFolder) continue;

                if (logged >= MaxReportedPerImport) { suppressed++; continue; }

                UnityEngine.Object context = AssetDatabase.LoadMainAssetAtPath(violation.assetPath);
                string message = NamingManagerAbout.LogPrefix + violation.assetPath + " — " + violation.message;

                // A warning even for an Error rule. An import-time finding is a note to whoever just
                // dragged the file in; a console error would fail a batch-mode import that has
                // nothing to do with naming, and CI is where the severity is supposed to bite.
                if (violation.severity == NamingSeverity.Info) Debug.Log(message, context);
                else Debug.LogWarning(message, context);

                logged++;
            }

            if (suppressed > 0)
            {
                Debug.Log(NamingManagerAbout.LogPrefix + suppressed + " more naming problems arrived in that " +
                          "import. Open " + NamingManagerAbout.ProductName + " and run a full scan to see them all.");
            }
        }
    }
}
