using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityObject = UnityEngine.Object;

namespace UnityEditor.Rendering
{
    /// <summary>
    /// Flags describing usage of an asset by its dependents, when that asset might have serialized shader property names.
    /// </summary>
    [Flags]
    enum SerializedShaderPropertyUsage : byte
    {
        /// <summary>
        /// Asset's usage is unknown.
        /// </summary>
        Unknown = 0,
        /// <summary>
        /// Asset contains no serialized shader properties.
        /// </summary>
        NoShaderProperties = 1,
        /// <summary>
        /// Asset is used by objects that have materials which have been upgraded.
        /// </summary>
        UsedByUpgraded = 2,
        /// <summary>
        /// Asset is used by objects that have materials which were not upgraded.
        /// </summary>
        UsedByNonUpgraded = 4,
        /// <summary>
        /// Asset is used by objects that have materials which may have been upgraded, but there is no unambiguous upgrade path.
        /// </summary>
        UsedByAmbiguouslyUpgraded = 8,
    }

    /// <summary>
    /// Class containing utility methods for upgrading assets affected by render pipeline migration.
    /// </summary>
    static class UpgradeUtility
    {
        /// <summary>
        /// Stable, unique identifier for some asset.
        /// </summary>
        internal struct UID
        {
            public string Value;
            public static implicit operator string(UID uid) => uid.Value;
            public static implicit operator UID(string id) => new UID { Value = id };
        }

        internal interface IMaterial
        {
            UID ID { get; }
            string ShaderName { get; }
        }

        internal struct MaterialProxy : IMaterial
        {
            public MaterialProxy(Material material, UnityObject[] allAssetsAtPath)
            {
                m_ID = $"{allAssetsAtPath}{Array.IndexOf(allAssetsAtPath, material)}";
                m_Material = material;
            }

            UID m_ID;
            Material m_Material;
            public UID ID => m_ID;
            public string ShaderName => m_Material.shader.name;
            public static implicit operator Material(MaterialProxy proxy) => proxy.m_Material;
            public static implicit operator MaterialProxy(Material material) =>
                new MaterialProxy(material, AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(material)));
            public override string ToString() => m_Material.ToString();
        }

        /// <summary>
        /// Create A table of new shader names and all known upgrade paths to them in the target pipeline.
        /// </summary>
        /// <param name="upgraders">The set of <see cref="MaterialUpgrader"/> from which to build the table.</param>
        /// <returns>A table of new shader names and all known upgrade paths to them in the target pipeline.</returns>
        public static Dictionary<string, IReadOnlyList<MaterialUpgrader>> GetAllUpgradePathsToShaders(
            IEnumerable<MaterialUpgrader> upgraders
        )
        {
            var upgradePathBuilder = new Dictionary<string, List<MaterialUpgrader>>();
            foreach (var upgrader in upgraders)
            {
                // skip over upgraders that do not rename shaders or have not been initialized
                if (upgrader.NewShaderPath == null)
                    continue;

                if (!upgradePathBuilder.TryGetValue(upgrader.NewShaderPath, out var allPaths))
                    upgradePathBuilder[upgrader.NewShaderPath] = allPaths = new List<MaterialUpgrader>();
                allPaths.Add(upgrader);
            }
            return upgradePathBuilder.ToDictionary(kv => kv.Key, kv => kv.Value as IReadOnlyList<MaterialUpgrader>);
        }

        /// <summary>
        /// Gets the new name for a serialized shader property, which needs to be applied to a material that has been upgraded.
        /// </summary>
        /// <remarks>
        /// Some assets serialize shader property names, in order to apply modifications to a material at run-time.
        /// Use this method's return value to determine whether the serialized property name can be safely substituted,
        /// based on a material the host object intends to apply it to.
        /// </remarks>
        /// <param name="shaderPropertyName">A shader property name serialized on a host object.</param>
        /// <param name="material">
        /// The target material to which some shader property modification will be applied.
        /// It is presumed to have already been upgraded.
        /// </param>
        /// <param name="materialPropertyType">What type of property <paramref name="shaderPropertyName"/> is.</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="newPropertyName">
        /// The new name for <paramref name="shaderPropertyName"/>.
        /// Its value is only guaranteed to be unambiguous if the method returns
        /// <see cref="SerializedShaderPropertyUsage.UsedByUpgraded"/>.
        /// </param>
        /// <returns>
        /// Usage flags indicating how <paramref name="shaderPropertyName"/> relates to <paramref name="material"/>.
        /// </returns>
        public static SerializedShaderPropertyUsage GetNewPropertyName(
            string shaderPropertyName,
            IMaterial material,
            MaterialUpgrader.MaterialPropertyType materialPropertyType,
            IReadOnlyDictionary<string, IReadOnlyList<MaterialUpgrader>> allUpgradePathsToNewShaders,
            IReadOnlyDictionary<UID, MaterialUpgrader> upgradePathsUsedByMaterials,
            out string newPropertyName
        )
        {
            var result = SerializedShaderPropertyUsage.Unknown;

            // we want to find out if we should rename the property
            newPropertyName = shaderPropertyName;

            // first check if we already know how this material was upgraded
            if (upgradePathsUsedByMaterials != null && upgradePathsUsedByMaterials.TryGetValue(material.ID, out var upgrader))
            {
                result |= SerializedShaderPropertyUsage.UsedByUpgraded;

                var propertyRenameTable = upgrader.GetPropertyRenameMap(materialPropertyType);
                propertyRenameTable.TryGetValue(shaderPropertyName, out newPropertyName);
            }

            // otherwise, try to guess whether it might have been upgraded
            if (newPropertyName == shaderPropertyName)
            {
                // get possible known upgrade paths material might have taken
                allUpgradePathsToNewShaders.TryGetValue(material.ShaderName, out var possibleUpgraders);

                // if there are none, then assume this material was not upgraded
                if ((possibleUpgraders?.Count ?? 0) == 0)
                {
                    result |= SerializedShaderPropertyUsage.UsedByNonUpgraded;
                }
                // otherwise, see if there are any possible upgrade paths
                else
                {
                    // narrow possible upgraders to those which specify a rename for the bound property
                    var matchingUpgraders = possibleUpgraders.Where(
                        u => u.GetPropertyRenameMap(materialPropertyType).ContainsKey(shaderPropertyName)
                        ).ToList();

                    // if there are any, assume the material has been upgraded
                    if (matchingUpgraders.Any())
                    {
                        result |= SerializedShaderPropertyUsage.UsedByUpgraded;

                        // if there are many possible upgrade paths to take, mark the upgrade as ambiguous
                        newPropertyName = matchingUpgraders[0].GetPropertyRenameMap(materialPropertyType)[shaderPropertyName];
                        var name = newPropertyName; // cannot use out param inside lambda
                        if (matchingUpgraders.Any(u => u.GetPropertyRenameMap(materialPropertyType)[shaderPropertyName] != name))
                            result |= SerializedShaderPropertyUsage.UsedByAmbiguouslyUpgraded;
                    }
                    else
                    {
                        var alreadyUpgraded = possibleUpgraders.Any(u => u.GetPropertyRenameMap(materialPropertyType).Values.Contains(shaderPropertyName));

                        if (alreadyUpgraded)
                        {
                            result |= SerializedShaderPropertyUsage.UsedByUpgraded;
                        }
                    }
                }
            }

            return result;
        }
    }
}