using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor.Animations;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Rendering;
using IMaterial = UnityEditor.Rendering.UpgradeUtility.IMaterial;
using UID = UnityEditor.Rendering.UpgradeUtility.UID;

namespace UnityEditor.Rendering
{
    /// <summary>
    /// A class containing static methods for updating <see cref="AnimationClip"/> assets with bindings for <see cref="Material"/> properties.
    /// </summary>
    /// <remarks>
    /// Animation clips store bindings for material properties by path name, but don't know whether those properties exist on their dependents.
    /// Because property names did not change uniformly in the material/shader upgrade process, it is not possible to patch path names indiscriminately.
    /// This class provides utilities for discovering how clips are used, so users can make decisions about whether or not to then update them.
    /// It has the limitation that it only knows about:
    /// - Clips that are directly referenced by an <see cref="Animation"/> component
    /// - Clips referenced by an <see cref="AnimatorController"/> used by an <see cref="Animator"/> component
    /// - Clips that are sub-assets of a <see cref="PlayableAsset"/> used by a <see cref="PlayableDirector"/> component with a single <see cref="Animator"/> binding
    /// It does not know about clips that might be referenced in other ways for run-time reassignment.
    /// Recommended usage is to call <see cref="DoUpgradeAllClipsMenuItem"/> from a menu item callback.
    /// The utility can also provide faster, more reliable results if it knows what <see cref="MaterialUpgrader"/> was used to upgrade specific materials.
    /// </remarks>
    static partial class AnimationClipUpgrader
    {
        static readonly Regex k_MatchMaterialPropertyName = new Regex(@"material.(\w+)(\.\w+)?", RegexOptions.Compiled);

        /// <summary>
        /// Determines whether the specified<see cref="EditorCurveBinding"/> is for a material property.
        /// </summary>
        /// <remarks>Internal only for testability.</remarks>
        /// <param name="b">An <see cref="EditorCurveBinding"/>.</param>
        /// <returns><c>true</c> if the binding is for a material property; <c>false</c> otherwise.</returns>
        internal static bool IsMaterialBinding(EditorCurveBinding b) =>
            (b.type?.IsSubclassOf(typeof(Renderer)) ?? false)
            && !string.IsNullOrEmpty(b.propertyName)
            && k_MatchMaterialPropertyName.IsMatch(b.propertyName);

        static readonly IReadOnlyCollection<string> k_ColorAttributeSuffixes =
            new HashSet<string>(new[] { ".r", ".g", ".b", ".a" });

        /// <summary>
        /// Infer a shader property name and type from an <see cref="EditorCurveBinding"/>.
        /// </summary>
        /// <remarks>Internal only for testability.</remarks>
        /// <param name="binding">A binding presumed to map to a material property. (See also <seealso cref="IsMaterialBinding"/>.)</param>
        /// <returns>A shader property name, and a guess of what type of shader property it targets.</returns>
        internal static (string Name, ShaderPropertyType Type) InferShaderProperty(EditorCurveBinding binding)
        {
            var match = k_MatchMaterialPropertyName.Match(binding.propertyName);
            var propertyName = match.Groups[1].Value;
            var propertyType = match.Groups[2].Value;
            return (propertyName,
                k_ColorAttributeSuffixes.Contains(propertyType) ? ShaderPropertyType.Color : ShaderPropertyType.Float);
        }

        /// <summary>
        /// Gets asset data for all clip assets at the specified paths, which contain bindings for material properties.
        /// (See also <seealso cref="GatherClipsUsageInDependentPrefabs"/> and <seealso cref="GatherClipsUsageInDependentScenes"/>.)
        /// </summary>
        /// <param name="clipPaths">Paths to assets containing <see cref="AnimationClip"/>.</param>
        /// <returns>
        /// Lookup table mapping <see cref="AnimationClip"/> to its asset path, bindings, property rename table, and usage.
        /// (Use <see cref="GatherClipsUsageInDependentPrefabs"/> to initialize rename table and usage.)
        /// </returns>
        internal static IDictionary<
            IAnimationClip,
            (ClipPath Path, EditorCurveBinding[] Bindings, SerializedShaderPropertyUsage Usage, IDictionary<EditorCurveBinding, string> PropertyRenames)
        > GetAssetDataForClipsFiltered(
            IEnumerable<ClipPath> clipPaths
        )
        {
            var result = new Dictionary<
                IAnimationClip,
                (ClipPath Path, EditorCurveBinding[] Bindings, SerializedShaderPropertyUsage Usage, IDictionary<EditorCurveBinding, string> PropertyRenames)
                >();
            foreach (var clipPath in clipPaths)
            {
                foreach (var asset in AssetDatabase.LoadAllAssetsAtPath(clipPath))
                {
                    if (!(asset is AnimationClip clip))
                        continue;

                    var bindings = AnimationUtility.GetCurveBindings(clip).Where(IsMaterialBinding).ToArray();
                    if (bindings.Length == 0)
                        continue;

                    result[(AnimationClipProxy)clip] =
                        (clipPath, bindings, SerializedShaderPropertyUsage.Unknown, new Dictionary<EditorCurveBinding, string>());
                }
            }

            return result;
        }

        /// <summary>
        /// Get dependency mappings between <see cref="AnimationClip"/> and their dependents.
        /// </summary>
        /// <param name="clips"> Paths to clips to consider. (See also <seealso cref="GetAssetDataForClipsFiltered"/>.)</param>
        /// <param name="assets">Paths to assets to consider.</param>
        /// <param name="clipToDependentAssets">Mapping of clip paths to paths of their dependents.</param>
        /// <param name="assetToClipDependencies">Mapping of asset paths to their clip dependencies.</param>
        /// <typeparam name="T">The type of asset path.</typeparam>
        internal static void GetClipDependencyMappings<T>(
            IEnumerable<ClipPath> clips,
            IEnumerable<T> assets,
            out IReadOnlyDictionary<ClipPath, IReadOnlyCollection<T>> clipToDependentAssets,
            out IReadOnlyDictionary<T, IReadOnlyCollection<ClipPath>> assetToClipDependencies
        ) where T : struct, IAssetPath
        {
            // ensure there are no duplicate keys
            clips = new HashSet<ClipPath>(clips);
            assets = new HashSet<T>(assets);

            // create mutable builders
            var clipsDependentsBuilder = clips.ToDictionary(c => c, c => new HashSet<T>());
            var assetsBuilder = new Dictionary<T, HashSet<ClipPath>>();

            // build dependency tables
            foreach (var asset in assets)
            {
                assetsBuilder[asset] = new HashSet<ClipPath>();

                foreach (var dependencyPath in AssetDatabase.GetDependencies(asset.Path))
                {
                    if (!clipsDependentsBuilder.TryGetValue(dependencyPath, out var dependents))
                        continue;

                    dependents.Add(asset);
                    assetsBuilder[asset].Add(dependencyPath);
                }
            }

            // return readonly results
            clipToDependentAssets =
                clipsDependentsBuilder.ToDictionary(kv => kv.Key, kv => kv.Value as IReadOnlyCollection<T>);
            assetToClipDependencies =
                assetsBuilder.ToDictionary(kv => kv.Key, kv => kv.Value as IReadOnlyCollection<ClipPath>);
        }

        // reusable buffers
        static readonly List<Animation> s_AnimationBuffer = new List<Animation>(8);
        static readonly List<Animator> s_AnimatorBuffer = new List<Animator>(8);
        static readonly List<IAnimationClipSource> s_CustomAnimationBuffer = new List<IAnimationClipSource>(8);
        static readonly List<PlayableDirector> s_PlayableDirectorBuffer = new List<PlayableDirector>(8);

        /// <summary>
        /// Get information about a clip's usage among its dependent scenes to determine whether or not it should be upgraded.
        /// </summary>
        /// <param name="clipDependents">
        /// A table mapping clip asset paths, to asset paths of their dependent prefabs.
        /// (See <seealso cref="GetClipDependencyMappings{T}"/>.)
        /// </param>
        /// <param name="assetDependencies">
        /// A table mapping prefab asset paths, to asset paths of their clip dependencies.
        /// (See <seealso cref="GetClipDependencyMappings{T}"/>.)
        /// </param>
        /// <param name="clipData">
        /// A table mapping clips to other data about them. (See also <seealso cref="GetAssetDataForClipsFiltered"/>.)
        /// </param>
        /// <param name="allUpgradePathsToNewShaders">
        /// A table of new shader names and all known upgrade paths to them in the target pipeline.
        /// (See also <seealso cref="UpgradeUtility.GetAllUpgradePathsToShaders"/>.)
        /// </param>
        /// <param name="upgradePathsUsedByMaterials">
        /// Optional table of materials known to have gone through a specific upgrade path.
        /// </param>
        /// <param name="progressFunctor">
        /// Optional functor to display a progress bar.
        /// </param>
        internal static void GatherClipsUsageInDependentPrefabs(
            IReadOnlyDictionary<ClipPath, IReadOnlyCollection<PrefabPath>> clipDependents,
            // TODO: right now, clip dependencies are gathered in Animation/Animator, so this may not be needed...
            IReadOnlyDictionary<PrefabPath, IReadOnlyCollection<ClipPath>> assetDependencies,
            IDictionary<
                IAnimationClip,
                (ClipPath Path, EditorCurveBinding[] Bindings, SerializedShaderPropertyUsage Usage, IDictionary<EditorCurveBinding, string> PropertyRenames)
            > clipData,
            IReadOnlyDictionary<string, IReadOnlyList<MaterialUpgrader>> allUpgradePathsToNewShaders,
            IReadOnlyDictionary<UID, MaterialUpgrader> upgradePathsUsedByMaterials = default,
            Func<string, float, bool> progressFunctor = null
        )
        {
            int clipIndex = 0;
            int totalNumberOfClips = clipDependents.Count;

            // check all dependents for usage
            foreach (var kv in clipDependents)
            {
                float currentProgress = (float)++clipIndex / totalNumberOfClips;
                if (progressFunctor != null && progressFunctor($"({clipIndex} of {totalNumberOfClips}) {kv.Key.Path}", currentProgress))
                    break;

                foreach (var prefabPath in kv.Value)
                {
                    var go = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
                    GatherClipsUsageForGameObject(go, clipData, allUpgradePathsToNewShaders, upgradePathsUsedByMaterials);
                }
            }
        }

        /// <summary>
        /// Get information about a clip's usage among its dependent scenes to determine whether or not it should be upgraded.
        /// </summary>
        /// <remarks>
        /// Because this method will open scenes to search for usages, it is recommended you first prompt for user input.
        /// It is also a good idea to first call <see cref="GatherClipsUsageInDependentPrefabs"/> to generate usage data.
        /// Clips that are already known to be unsafe for upgrading based on their prefab usage can be skipped here.
        /// </remarks>
        /// <param name="clipDependents">
        /// A table mapping clip asset paths, to asset paths of their dependent scenes.
        /// (See <seealso cref="GetClipDependencyMappings{T}"/>.)
        /// </param>
        /// <param name="assetDependencies">
        /// A table mapping scene asset paths, to asset paths of their clip dependencies.
        /// (See <seealso cref="GetClipDependencyMappings{T}"/>.)
        /// </param>
        /// <param name="clipData">
        /// A table mapping clips to other data about them. (See also <seealso cref="GetAssetDataForClipsFiltered"/>.)
        /// </param>
        /// <param name="allUpgradePathsToNewShaders">
        /// A table of new shader names and all known upgrade paths to them in the target pipeline.
        /// (See also <seealso cref="UpgradeUtility.GetAllUpgradePathsToShaders"/>.)
        /// </param>
        /// <param name="upgradePathsUsedByMaterials">
        /// Optional table of materials known to have gone through a specific upgrade path.
        /// </param>
        /// <param name="progressFunctor">
        /// Optional functor to display a progress bar.
        /// </param>
        internal static void GatherClipsUsageInDependentScenes(
            IReadOnlyDictionary<ClipPath, IReadOnlyCollection<ScenePath>> clipDependents,
            // TODO: right now, clip dependencies are gathered in Animation/Animator, so this may not be needed...
            IReadOnlyDictionary<ScenePath, IReadOnlyCollection<ClipPath>> assetDependencies,
            IDictionary<
                IAnimationClip,
                (ClipPath Path, EditorCurveBinding[] Bindings, SerializedShaderPropertyUsage Usage, IDictionary<EditorCurveBinding, string>
                    PropertyRenames)
            > clipData,
            IReadOnlyDictionary<string, IReadOnlyList<MaterialUpgrader>> allUpgradePathsToNewShaders,
            IReadOnlyDictionary<UID, MaterialUpgrader> upgradePathsUsedByMaterials = default,
            Func<string, float, bool> progressFunctor = null
        )
        {
            int clipIndex = 0;
            int totalNumberOfClips = clipDependents.Count;

            // check all dependents for usage
            foreach (var kv in clipDependents)
            {
                float currentProgress = (float)++clipIndex / totalNumberOfClips;
                if (progressFunctor != null && progressFunctor($"({clipIndex} of {totalNumberOfClips}) {kv.Key.Path}", currentProgress))
                    break;

                foreach (var scenePath in kv.Value)
                {
                    var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
                    foreach (var go in scene.GetRootGameObjects())
                        GatherClipsUsageForGameObject(go, clipData, allUpgradePathsToNewShaders, upgradePathsUsedByMaterials);
                }
            }
        }

        /// <summary>
        /// Update usage information about the specified clips in the clip data table.
        /// </summary>
        /// <param name="go">A prefab, or a <see cref="GameObject"/> in a scene.</param>
        /// <param name="clipData">
        /// A table mapping clips to other data about them. (See also <seealso cref="GetAssetDataForClipsFiltered"/>.)
        /// </param>
        /// <param name="allUpgradePathsToNewShaders">
        /// A table of new shader names and all known upgrade paths to them in the target pipeline.
        /// (See also <seealso cref="UpgradeUtility.GetAllUpgradePathsToShaders"/>.)
        /// </param>
        /// <param name="upgradePathsUsedByMaterials">
        /// Optional table of materials known to have gone through a specific upgrade path.
        /// </param>
        static void GatherClipsUsageForGameObject(
            GameObject go,
            IDictionary<
                IAnimationClip,
                (ClipPath Path, EditorCurveBinding[] Bindings, SerializedShaderPropertyUsage Usage, IDictionary<EditorCurveBinding, string> PropertyRenames)
            > clipData,
            IReadOnlyDictionary<string, IReadOnlyList<MaterialUpgrader>> allUpgradePathsToNewShaders,
            IReadOnlyDictionary<UID, MaterialUpgrader> upgradePathsUsedByMaterials = default
        )
        {
            go.GetComponentsInChildren(true, s_AnimationBuffer);
            go.GetComponentsInChildren(true, s_AnimatorBuffer);
            go.GetComponentsInChildren(true, s_CustomAnimationBuffer);

            // first check clip usage among GameObjects with legacy Animation
            var gameObjects = new HashSet<GameObject>(s_AnimationBuffer.Select(a => a.gameObject)
                .Union(s_AnimatorBuffer.Select(a => a.gameObject))
                .Union(s_CustomAnimationBuffer.Where(a => a is Component).Select(a => ((Component)a).gameObject)));

            foreach (var gameObject in gameObjects)
            {
                var clips = AnimationUtility.GetAnimationClips(gameObject).Select(clip =>  (IAnimationClip)(AnimationClipProxy)clip);

                GatherClipsUsageForAnimatedHierarchy(
                    gameObject.transform, clips, clipData, allUpgradePathsToNewShaders, upgradePathsUsedByMaterials
                );
            }

            // next check clip usage among GameObjects with PlayableDirector
            go.GetComponentsInChildren(true, s_PlayableDirectorBuffer);
            foreach (var playableDirector in s_PlayableDirectorBuffer)
            {
                var playableAsset = playableDirector.playableAsset;
                if (playableAsset == null)
                    continue;

                var assetPath = AssetDatabase.GetAssetPath(playableAsset);

                // get all clip sub-assets
                var clips = new HashSet<IAnimationClip>(
                    AssetDatabase.LoadAllAssetsAtPath(assetPath)
                        .Where(asset => asset is AnimationClip)
                        .Select(asset => (IAnimationClip)(AnimationClipProxy)(asset as AnimationClip))
                );

                // get all clip dependency-assets
                // this will not handle nested clips in FBX like assets, but these are less likely to be editable
                clips.UnionWith(AssetDatabase.GetDependencies(assetPath)
                    .Select(AssetDatabase.LoadAssetAtPath<AnimationClip>)
                    .Where(asset => asset is AnimationClip)
                    .Select(asset => (IAnimationClip)(AnimationClipProxy)asset));

                // check if the value of a binding is an animator, and examines clip usage relative to it
                // this is imprecise, but is suitable to catch the majority of cases (i.e., a single animator binding)
                using (var so = new SerializedObject(playableDirector))
                {
                    var clipsProp = so.FindProperty("m_SceneBindings");
                    for (int i = 0, count = clipsProp.arraySize; i < count; ++i)
                    {
                        var elementProp = clipsProp.GetArrayElementAtIndex(i);
                        var value = elementProp.FindPropertyRelative("value");
                        if (value.objectReferenceValue is Animator animator)
                        {
                            GatherClipsUsageForAnimatedHierarchy(
                                animator.transform, clips, clipData, allUpgradePathsToNewShaders, upgradePathsUsedByMaterials
                            );
                        }
                    }
                }
            }

            // release UnityObject references
            s_AnimationBuffer.Clear();
            s_AnimatorBuffer.Clear();
            s_CustomAnimationBuffer.Clear();
            s_PlayableDirectorBuffer.Clear();
        }

        // reusable buffers
        static readonly List<Renderer> s_RendererBuffer = new List<Renderer>(8);

        static readonly Dictionary<string, (IRenderer Renderer, List<IMaterial> Materials)> s_RenderersByPath =
            new Dictionary<string, (IRenderer Renderer, List<IMaterial> Materials)>();

        /// <summary>
        /// Update usage information about the specified clips in the clip data table.
        /// </summary>
        /// <param name="root">The root of the animated hierarchy (i.e., object with Animation or Animator).</param>
        /// <param name="clips">Collection of animation clips</param>
        /// <param name="clipData">
        /// A table mapping clips to other data about them. (See also <seealso cref="GetAssetDataForClipsFiltered"/>.)
        /// </param>
        /// <param name="allUpgradePathsToNewShaders">
        /// A table of new shader names and all known upgrade paths to them in the target pipeline.
        /// (See also <seealso cref="UpgradeUtility.GetAllUpgradePathsToShaders"/>.)
        /// </param>
        /// <param name="upgradePathsUsedByMaterials">
        /// Optional table of materials known to have gone through a specific upgrade path.
        /// </param>
        static void GatherClipsUsageForAnimatedHierarchy(
            Transform root,
            IEnumerable<IAnimationClip> clips,
            IDictionary<
                IAnimationClip,
                (ClipPath Path, EditorCurveBinding[] Bindings, SerializedShaderPropertyUsage Usage, IDictionary<EditorCurveBinding, string> PropertyRenames)
            > clipData,
            IReadOnlyDictionary<string, IReadOnlyList<MaterialUpgrader>> allUpgradePathsToNewShaders,
            IReadOnlyDictionary<UID, MaterialUpgrader> upgradePathsUsedByMaterials
        )
        {
            // TODO: report paths of specific assets that contribute to problematic results?

            // find all renderers in the animated hierarchy
            root.GetComponentsInChildren(true, s_RendererBuffer);
            foreach (var renderer in s_RendererBuffer)
            {
                var path = AnimationUtility.CalculateTransformPath(renderer.transform, root);
                var m = ListPool<IMaterial>.Get();
                var r = (RendererProxy)renderer;
                r.GetSharedMaterials(m);
                s_RenderersByPath[path] = (r, m);
            }

            // if there are any renderers, check all clips for usage
            if (s_RendererBuffer.Count > 0)
            {
                foreach (var clip in clips)
                    GatherClipUsage(clip, clipData, s_RenderersByPath, allUpgradePathsToNewShaders, upgradePathsUsedByMaterials);
            }

            // release UnityObject references
            s_RendererBuffer.Clear();
            foreach (var(_, materials) in s_RenderersByPath.Values)
                ListPool<IMaterial>.Release(materials);
            s_RenderersByPath.Clear();
        }

        /// <summary>
        /// Update usage information about the specified clip in the clip data table.
        /// </summary>
        /// <remarks>
        /// This method works by looking at shaders used by materials assigned to the specified renderers.
        /// Usage and property renames for the clip are updated, if a binding in the clip matches an upgrader.
        /// Internal only for testability.
        /// </remarks>
        /// <param name="clip">An animation clip.</param>
        /// <param name="clipData">
        /// A table mapping clips to other data about them. (See also <seealso cref="GetAssetDataForClipsFiltered"/>.)
        /// </param>
        /// <param name="renderersByPath">
        /// A table mapping transform paths of renderers to lists of the materials they use.
        /// </param>
        /// <param name="allUpgradePathsToNewShaders">
        /// A table of new shader names and all known upgrade paths to them in the target pipeline.
        /// (See also <seealso cref="UpgradeUtility.GetAllUpgradePathsToShaders"/>.)
        /// </param>
        /// <param name="upgradePathsUsedByMaterials">
        /// Optional table of materials known to have gone through a specific upgrade path.
        /// </param>
        internal static void GatherClipUsage(
            IAnimationClip clip,
            IDictionary<
                IAnimationClip,
                (ClipPath Path, EditorCurveBinding[] Bindings, SerializedShaderPropertyUsage Usage, IDictionary<EditorCurveBinding, string> PropertyRenames)
            > clipData,
            IReadOnlyDictionary<string, (IRenderer Renderer, List<IMaterial> Materials)> renderersByPath,
            IReadOnlyDictionary<string, IReadOnlyList<MaterialUpgrader>> allUpgradePathsToNewShaders,
            IReadOnlyDictionary<UID, MaterialUpgrader> upgradePathsUsedByMaterials
        )
        {
            // exit if clip is unknown; it may have been filtered at an earlier stage
            if (!clipData.TryGetValue(clip, out var data))
                return;

            // see if any animated material bindings in the clip refer to renderers in animated hierarchy
            foreach (var binding in data.Bindings)
            {
                // skip if binding is not for material, or refers to a nonexistent renderer
                if (
                    !IsMaterialBinding(binding)
                    || !renderersByPath.TryGetValue(binding.path, out var rendererData)
                ) continue;

                // determine the shader property name and type from the binding
                var shaderProperty = InferShaderProperty(binding);
                var renameType = shaderProperty.Type == ShaderPropertyType.Color
                    ? MaterialUpgrader.MaterialPropertyType.Color
                    : MaterialUpgrader.MaterialPropertyType.Float;

                // material property animations apply to all materials, so check shader usage in all of them
                foreach (var material in rendererData.Materials)
                {
                    var usage = UpgradeUtility.GetNewPropertyName(
                        shaderProperty.Name,
                        material,
                        renameType,
                        allUpgradePathsToNewShaders,
                        upgradePathsUsedByMaterials,
                        out var newPropertyName
                    );

                    // if the property has already been upgraded with a different name, mark the upgrade as ambiguous
                    if (
                        usage == SerializedShaderPropertyUsage.UsedByUpgraded
                        && data.PropertyRenames.TryGetValue(binding, out var propertyRename)
                        && propertyRename != newPropertyName
                    )
                        usage |= SerializedShaderPropertyUsage.UsedByAmbiguouslyUpgraded;

                    data.Usage |= usage;
                    data.PropertyRenames[binding] = newPropertyName;
                }
            }

            clipData[clip] = data;
        }

        /// <summary>
        /// Upgrade the specified clips using the associated property rename table.
        /// </summary>
        /// <param name="clipsToUpgrade">
        /// A table mapping clips to property renaming tables that can be safely applied to their bindings.
        /// </param>
        /// <param name="excludeFlags">Do not upgrade clips that have any of these flags set.</param>
        /// <param name="upgraded">Collector for all clips that are upgraded.</param>
        /// <param name="notUpgraded">Collector for all clips that are not upgraded.</param>
        /// <param name="progressFunctor">Optional functor to display a progress bar.</param>
        internal static void UpgradeClips(
            IDictionary<IAnimationClip, (ClipPath Path, EditorCurveBinding[] Bindings, SerializedShaderPropertyUsage Usage, IDictionary<EditorCurveBinding, string> PropertyRenames)> clipsToUpgrade,
            SerializedShaderPropertyUsage excludeFlags,
            HashSet<(IAnimationClip Clip, ClipPath Path, SerializedShaderPropertyUsage Usage)> upgraded,
            HashSet<(IAnimationClip Clip, ClipPath Path, SerializedShaderPropertyUsage Usage)> notUpgraded,
            Func<string, float, bool> progressFunctor = null
        )
        {
            upgraded.Clear();
            notUpgraded.Clear();

            int clipIndex = 0;
            int totalNumberOfClips = clipsToUpgrade.Count;

            foreach (var kv in clipsToUpgrade)
            {
                float currentProgress = (float)++clipIndex / totalNumberOfClips;
                if (progressFunctor != null && progressFunctor($"({clipIndex} of {totalNumberOfClips}) {kv.Value.Path.Path}", currentProgress))
                    break;

                if (kv.Value.Usage == SerializedShaderPropertyUsage.Unknown || (kv.Value.Usage & excludeFlags) != 0)
                {
                    notUpgraded.Add((kv.Key, kv.Value.Path, kv.Value.Usage));
                    continue;
                }

                var renames = kv.Value.PropertyRenames;
                var bindings = kv.Key.GetCurveBindings().Where(IsMaterialBinding).ToArray();
                if (bindings.Length > 0)
                {
                    var newBindings = new EditorCurveBinding[bindings.Length];

                    for (int i = 0; i < bindings.Length; ++i)
                    {
                        var binding = bindings[i];

                        newBindings[i] = binding;
                        if (renames.TryGetValue(binding, out var newName))
                            newBindings[i].propertyName = k_MatchMaterialPropertyName.Replace(newBindings[i].propertyName, $"material.{newName}$2");
                    }

                    kv.Key.ReplaceBindings(bindings, newBindings);
                }

                upgraded.Add((kv.Key, kv.Value.Path, kv.Value.Usage));
            }
        }

        /// <summary>
        /// A function to call from a menu item callback, which will upgrade all <see cref="AnimationClip"/> in the project.
        /// </summary>
        /// <param name="allUpgraders">All <see cref="MaterialUpgrader"/> for the current render pipeline.</param>
        /// <param name="knownUpgradePaths">
        /// Optional mapping of materials to upgraders they are known to have used.
        /// Without this mapping, the method makes inferences about how a material might have been upgraded.
        /// Making these inferences is slower and possibly not sensitive to ambiguous upgrade paths.
        /// </param>
        /// <param name="filterFlags">
        /// Optional flags to filter out clips that are used in ways that are not safe for upgrading.
        /// </param>
        public static void DoUpgradeAllClipsMenuItem(
            IEnumerable<MaterialUpgrader> allUpgraders,
            string progressBarName,
            IReadOnlyDictionary<UID, MaterialUpgrader> knownUpgradePaths = default,
            SerializedShaderPropertyUsage filterFlags = ~(SerializedShaderPropertyUsage.UsedByUpgraded | SerializedShaderPropertyUsage.UsedByNonUpgraded)
        )
        {
            var clipPaths = AssetDatabase.FindAssets("t:AnimationClip")
                .Select(p => (ClipPath)AssetDatabase.GUIDToAssetPath(p))
                .ToArray();
            DoUpgradeClipsMenuItem(clipPaths, allUpgraders, progressBarName, knownUpgradePaths, filterFlags);
        }

        static void DoUpgradeClipsMenuItem(
            ClipPath[] clipPaths,
            IEnumerable<MaterialUpgrader> allUpgraders,
            string progressBarName,
            IReadOnlyDictionary<UID, MaterialUpgrader> upgradePathsUsedByMaterials,
            SerializedShaderPropertyUsage filterFlags
        )
        {
            // exit early if no clips
            if (clipPaths?.Length == 0)
                return;

            // display dialog box
            var dialogMessage = L10n.Tr(
                "Upgrading Material curves in AnimationClips assumes you have already upgraded Materials and shaders as needed. " +
                "It also requires loading assets that use clips to inspect their usage, which can be a slow process. " +
                "Do you want to proceed?"
            );

            if (!EditorUtility.DisplayDialog(
                L10n.Tr("Upgrade AnimationClips"),
                dialogMessage,
                DialogText.proceed,
                DialogText.cancel))
                return;

            // only include scene paths if user requested it
            var prefabPaths = AssetDatabase.FindAssets("t:Prefab")
                .Select(p => (PrefabPath)AssetDatabase.GUIDToAssetPath(p))
                .ToArray();
            var scenePaths = AssetDatabase.FindAssets("t:Scene")
                .Select(p => (ScenePath)AssetDatabase.GUIDToAssetPath(p))
                .ToArray();

            // retrieve clip assets with material animation
            var clipData = GetAssetDataForClipsFiltered(clipPaths);

            const float kGatherInPrefabsTotalProgress = 0.33f;
            const float kGatherInScenesTotalProgress = 0.66f;
            const float kUpgradeClipsTotalProgress = 1f;

            // create table mapping all upgrade paths to new shaders
            var allUpgradePathsToNewShaders = UpgradeUtility.GetAllUpgradePathsToShaders(allUpgraders);

            // retrieve interdependencies with prefabs to figure out which clips can be safely upgraded
            GetClipDependencyMappings(clipPaths, prefabPaths, out var clipPrefabDependents, out var prefabDependencies);

            GatherClipsUsageInDependentPrefabs(
                clipPrefabDependents, prefabDependencies, clipData, allUpgradePathsToNewShaders, upgradePathsUsedByMaterials,
                (info, progress) => EditorUtility.DisplayCancelableProgressBar(progressBarName, $"Gathering from prefabs {info}", Mathf.Lerp(0f, kGatherInPrefabsTotalProgress, progress))
            );

            if (EditorUtility.DisplayCancelableProgressBar(progressBarName, "", kGatherInPrefabsTotalProgress))
            {
                EditorUtility.ClearProgressBar();
                return;
            }

            // if any scenes should be considered, do the same for clips used by scenes
            if (scenePaths.Any())
            {
                GetClipDependencyMappings(clipPaths, scenePaths, out var clipSceneDependents, out var sceneDependencies);
                GatherClipsUsageInDependentScenes(
                    clipSceneDependents, sceneDependencies, clipData, allUpgradePathsToNewShaders, upgradePathsUsedByMaterials,
                    (info, progress) => EditorUtility.DisplayCancelableProgressBar(progressBarName, $"Gathering from scenes {info}", Mathf.Lerp(kGatherInPrefabsTotalProgress, kGatherInScenesTotalProgress, progress))
                );
            }

            if (EditorUtility.DisplayCancelableProgressBar(progressBarName, "", kGatherInScenesTotalProgress))
            {
                EditorUtility.ClearProgressBar();
                return;
            }

            // patch clips that should be upgraded
            var upgraded = new HashSet<(IAnimationClip Clip, ClipPath Path, SerializedShaderPropertyUsage Usage)>();
            var notUpgraded = new HashSet<(IAnimationClip Clip, ClipPath Path, SerializedShaderPropertyUsage Usage)>();

            AssetDatabase.StartAssetEditing();
            UpgradeClips(
                clipData, filterFlags, upgraded, notUpgraded,
                (info, progress) => EditorUtility.DisplayCancelableProgressBar(progressBarName, $"Upgrading clips {info}", Mathf.Lerp(kGatherInScenesTotalProgress, kUpgradeClipsTotalProgress, progress))
            );
            AssetDatabase.SaveAssets();
            AssetDatabase.StopAssetEditing();

            EditorUtility.ClearProgressBar();

            // report results
            if (upgraded.Count > 0)
            {
                var successes = upgraded.Select(data => $"- {data.Path}: ({data.Usage})");
                Debug.Log(
                    "Upgraded the following clips:\n" +
                    $"{string.Join("\n", successes)}"
                );
            }

            if (notUpgraded.Count == clipData.Count)
            {
                Debug.LogWarning("No clips were upgraded. Did you remember to upgrade materials first?");
            }
            else if (notUpgraded.Count > 0)
            {
                var errors = notUpgraded.Select(data => $"- {data.Path}: ({data.Usage})");
                Debug.LogWarning(
                    $"Did not modify following clips because they they were used in ways other than {~filterFlags}:\n" +
                    $"{string.Join("\n", errors)}"
                );
            }
        }
    }
}