// Asset Naming Manager — the only class allowed to touch your files.
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace AssetNamingManager
{
    /// <summary>One asset's worth of pending change, after every finding on it has been merged.</summary>
    public class FixPlan
    {
        /// <summary>Where the asset lives today.</summary>
        public string sourcePath;

        /// <summary>New file name without extension, or null to keep the current one.</summary>
        public string targetName;

        /// <summary>New folder, or null to keep the current one.</summary>
        public string targetFolder;

        /// <summary>Extension carried over from the source, including the dot.</summary>
        public string extension;

        /// <summary>Human list of the problems this change resolves.</summary>
        public readonly List<string> reasons = new List<string>();

        /// <summary>Set by <see cref="NamingFixer.Apply"/> when a collision forced a different name.</summary>
        public bool nameWasDeduplicated;

        /// <summary>The path the asset will have once this plan is applied.</summary>
        public string TargetPath
        {
            get
            {
                string folder = targetFolder ?? ParentFolder(sourcePath);
                string name = targetName ?? NamingScanner.GetFileNameWithoutExtension(sourcePath);
                return folder.Length == 0 ? name + extension : folder + "/" + name + extension;
            }
        }

        /// <summary>False when the plan would not actually change anything.</summary>
        public bool IsMeaningful
        {
            get { return !string.Equals(TargetPath, sourcePath, StringComparison.Ordinal); }
        }

        internal static string ParentFolder(string path)
        {
            int slash = path.LastIndexOf('/');
            return slash < 0 ? string.Empty : path.Substring(0, slash);
        }
    }

    /// <summary>Totals from one <see cref="NamingFixer.Apply"/> run.</summary>
    public class FixResult
    {
        public int renamed;
        public int moved;
        public int skipped;
        public int failed;
        public bool cancelled;

        /// <summary>Journal id you can pass to <see cref="FixJournal.Revert"/> to undo this batch.</summary>
        public string batchId = "";

        /// <summary>Per-asset failure messages, ready to print.</summary>
        public readonly List<string> problems = new List<string>();

        /// <summary>Number of assets actually changed.</summary>
        public int Changed { get { return renamed + moved; } }

        /// <summary>One-line summary for the console and the status bar.</summary>
        public string Summary()
        {
            return string.Format("{0} renamed, {1} moved, {2} skipped, {3} failed.", renamed, moved, skipped, failed);
        }
    }

    /// <summary>
    /// Turns findings into concrete renames and moves, and applies them through the AssetDatabase
    /// so that every prefab, scene and script reference follows the asset. Nothing here runs
    /// unless you ask for it.
    /// </summary>
    public static class NamingFixer
    {
        // ================================================================ naming

        /// <summary>
        /// The name this asset <em>should</em> have: illegal characters gone, junk words gone,
        /// numbers padded, casing corrected, prefix and suffix attached. Returns the original
        /// name when the rule set has nothing to say about it.
        /// </summary>
        public static string ComputeIdealName(NamingRuleSet ruleSet, NamingRule rule, string fileName)
        {
            if (ruleSet == null || string.IsNullOrEmpty(fileName)) return fileName;

            bool removed;
            string working = NameUtility.StripIllegalCharacters(
                fileName, ruleSet.forbiddenCharacters, ruleSet.asciiOnly, '_', out removed);

            if (ruleSet.checkRedundantTokens)
                working = NameUtility.RemoveRedundantTokens(working, ruleSet.redundantTokens);

            if (working.Length == 0) working = fileName;

            string prefix = string.Empty;
            string suffix = string.Empty;
            NamingConvention convention = NamingConvention.Any;
            int maxLength = 0;

            if (rule != null)
            {
                convention = rule.convention;
                maxLength = rule.maxLength;

                string matchedPrefix = rule.MatchedPrefix(working);
                if (matchedPrefix != null)
                {
                    prefix = matchedPrefix;
                    working = working.Substring(matchedPrefix.Length);
                }
                else if (rule.Prefixes.Length > 0)
                {
                    prefix = rule.Prefixes[0];
                }

                string matchedSuffix = rule.MatchedSuffix(working);
                if (matchedSuffix != null && working.Length > matchedSuffix.Length)
                {
                    suffix = matchedSuffix;
                    working = working.Substring(0, working.Length - matchedSuffix.Length);
                }
                else if (rule.Suffixes.Length > 0)
                {
                    suffix = rule.Suffixes[0];
                }
            }

            working = working.Trim('_', '-', ' ');
            if (working.Length == 0) working = NamingScanner.GetFileNameWithoutExtension(fileName);

            if (ruleSet.checkNumberPadding)
                working = NameUtility.PadTrailingNumber(working, ruleSet.numberPaddingWidth);

            working = NameUtility.Convert(working, convention);

            string result = NameUtility.CollapseSeparators(prefix + working + suffix);

            if (maxLength > 0 && result.Length > maxLength)
            {
                int room = maxLength - prefix.Length - suffix.Length;
                if (room > 0)
                {
                    string trimmedBody = working.Length > room ? working.Substring(0, room) : working;
                    result = NameUtility.CollapseSeparators(prefix + trimmedBody.TrimEnd('_', '-', ' ') + suffix);
                }
            }

            return result.Length == 0 ? fileName : result;
        }

        // ================================================================ planning

        /// <summary>
        /// Merges every finding on the same asset into one change. Findings without a concrete
        /// suggestion are dropped, and plans that would not change anything are filtered out.
        /// </summary>
        public static List<FixPlan> BuildPlans(IEnumerable<AssetNamingViolation> violations)
        {
            var byPath = new Dictionary<string, FixPlan>(StringComparer.Ordinal);
            var order = new List<string>();

            foreach (var v in violations)
            {
                if (v == null || !v.CanAutoFix) continue;

                FixPlan plan;
                if (!byPath.TryGetValue(v.assetPath, out plan))
                {
                    plan = new FixPlan
                    {
                        sourcePath = v.assetPath,
                        extension = v.extension
                    };
                    byPath[v.assetPath] = plan;
                    order.Add(v.assetPath);
                }

                if (plan.targetName == null && v.IsRename) plan.targetName = v.suggestedName;
                if (plan.targetFolder == null && v.IsMove) plan.targetFolder = GlobMatcher.NormalizePath(v.suggestedFolder);

                string reason = v.KindLabel + " — " + v.message;
                if (!plan.reasons.Contains(reason)) plan.reasons.Add(reason);
            }

            var plans = new List<FixPlan>(order.Count);
            foreach (var path in order)
            {
                var plan = byPath[path];
                if (plan.IsMeaningful) plans.Add(plan);
            }
            return plans;
        }

        // ================================================================ applying

        /// <summary>
        /// Applies plans through <see cref="AssetDatabase.MoveAsset"/>, which keeps every GUID and
        /// therefore every reference intact. Creates missing destination folders, resolves name
        /// collisions by appending _01, _02, and journals everything so the batch can be reverted.
        /// </summary>
        /// <param name="plans">What to change.</param>
        /// <param name="showProgress">Show a cancelable progress bar. Pass false in batch mode.</param>
        public static FixResult Apply(IList<FixPlan> plans, bool showProgress = true)
        {
            var result = new FixResult();
            if (plans == null || plans.Count == 0) return result;

            var batch = new List<FixJournal.Entry>(plans.Count);
            var claimed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            try
            {
                for (int i = 0; i < plans.Count; i++)
                {
                    var plan = plans[i];
                    if (plan == null) { result.skipped++; continue; }

                    if (showProgress &&
                        EditorUtility.DisplayCancelableProgressBar(
                            "Asset Naming Manager",
                            "Fixing " + NamingScanner.GetFileName(plan.sourcePath) + "  (" + (i + 1) + "/" + plans.Count + ")",
                            (float)i / plans.Count))
                    {
                        result.cancelled = true;
                        break;
                    }

                    if (string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(plan.sourcePath)))
                    {
                        result.skipped++;
                        result.problems.Add(plan.sourcePath + " — no longer exists, skipped.");
                        continue;
                    }

                    string targetFolder = plan.targetFolder ?? FixPlan.ParentFolder(plan.sourcePath);
                    if (!EnsureFolder(targetFolder))
                    {
                        result.failed++;
                        result.problems.Add(plan.sourcePath + " — could not create folder " + targetFolder);
                        continue;
                    }

                    string baseName = plan.targetName ?? NamingScanner.GetFileNameWithoutExtension(plan.sourcePath);
                    string targetPath = Combine(targetFolder, baseName, plan.extension);

                    if (string.Equals(targetPath, plan.sourcePath, StringComparison.Ordinal))
                    {
                        result.skipped++;
                        continue;
                    }

                    // Two assets can want the same corrected name. Give the later ones a counter.
                    if (claimed.Contains(targetPath) || PathIsTaken(targetPath, plan.sourcePath))
                    {
                        string unique = MakeUnique(targetFolder, baseName, plan.extension, plan.sourcePath, claimed);
                        if (unique == null)
                        {
                            result.failed++;
                            result.problems.Add(plan.sourcePath + " — could not find a free name near " + baseName);
                            continue;
                        }
                        targetPath = unique;
                        plan.nameWasDeduplicated = true;
                    }

                    string validation = AssetDatabase.ValidateMoveAsset(plan.sourcePath, targetPath);
                    if (!string.IsNullOrEmpty(validation))
                    {
                        result.failed++;
                        result.problems.Add(plan.sourcePath + " — " + validation);
                        continue;
                    }

                    string error = AssetDatabase.MoveAsset(plan.sourcePath, targetPath);
                    if (!string.IsNullOrEmpty(error))
                    {
                        result.failed++;
                        result.problems.Add(plan.sourcePath + " — " + error);
                        continue;
                    }

                    claimed.Add(targetPath);
                    batch.Add(new FixJournal.Entry { from = plan.sourcePath, to = targetPath });

                    bool folderChanged = !string.Equals(
                        FixPlan.ParentFolder(plan.sourcePath), targetFolder, StringComparison.Ordinal);
                    if (folderChanged) result.moved++; else result.renamed++;
                }
            }
            finally
            {
                if (showProgress) EditorUtility.ClearProgressBar();
            }

            if (batch.Count > 0)
            {
                AssetDatabase.SaveAssets();
                AssetDatabase.Refresh();
                result.batchId = FixJournal.Record(batch);
            }

            return result;
        }

        /// <summary>Convenience overload: plan and apply a set of findings in one call.</summary>
        public static FixResult Apply(IEnumerable<AssetNamingViolation> violations, bool showProgress = true)
        {
            return Apply(BuildPlans(violations), showProgress);
        }

        // ================================================================ helpers

        static string Combine(string folder, string name, string extension)
        {
            return string.IsNullOrEmpty(folder) ? name + extension : folder + "/" + name + extension;
        }

        static bool PathIsTaken(string candidate, string ownPath)
        {
            if (string.Equals(candidate, ownPath, StringComparison.OrdinalIgnoreCase)) return false;

            // Case-insensitive on purpose: Windows and macOS file systems collide on case alone.
            string guid = AssetDatabase.AssetPathToGUID(candidate);
            if (!string.IsNullOrEmpty(guid)) return true;

            return System.IO.File.Exists(ToAbsolute(candidate));
        }

        static string MakeUnique(string folder, string baseName, string extension, string ownPath, HashSet<string> claimed)
        {
            for (int n = 1; n <= 99; n++)
            {
                string candidate = Combine(folder, baseName + "_" + n.ToString("00"), extension);
                if (!claimed.Contains(candidate) && !PathIsTaken(candidate, ownPath)) return candidate;
            }
            return null;
        }

        static string ToAbsolute(string assetPath)
        {
            string projectRoot = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length);
            return projectRoot + assetPath;
        }

        /// <summary>Creates a folder chain such as Assets/Art/Textures/UI if it does not exist yet.</summary>
        public static bool EnsureFolder(string folder)
        {
            if (string.IsNullOrEmpty(folder)) return false;
            if (AssetDatabase.IsValidFolder(folder)) return true;

            string[] parts = folder.Split('/');
            if (parts.Length == 0 || parts[0] != "Assets") return false;

            string current = "Assets";
            for (int i = 1; i < parts.Length; i++)
            {
                if (string.IsNullOrEmpty(parts[i])) continue;

                string next = current + "/" + parts[i];
                if (!AssetDatabase.IsValidFolder(next))
                {
                    string guid = AssetDatabase.CreateFolder(current, parts[i]);
                    if (string.IsNullOrEmpty(guid)) return false;
                }
                current = next;
            }

            return AssetDatabase.IsValidFolder(folder);
        }
    }
}
