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

namespace UnityEditor.Rendering.Universal
{
    internal static class MaterialReferenceBuilder
    {
        public static readonly Dictionary<Type, List<MethodInfo>> MaterialReferenceLookup;

        static MaterialReferenceBuilder()
        {
            MaterialReferenceLookup = GetMaterialReferenceLookup();
        }

        private static Dictionary<Type, List<MethodInfo>> GetMaterialReferenceLookup()
        {
            var result = new Dictionary<Type, List<MethodInfo>>();

            var allObjectsWithMaterialProperties = TypeCache.GetTypesDerivedFrom<Object>()
                .Where(type => type.GetProperties().Any(HasMaterialProperty));

            foreach (var property in allObjectsWithMaterialProperties)
            {
                if (!result.ContainsKey(property))
                {
                    result.Add(property, new List<MethodInfo>());
                }

                var materialProps = GetMaterialPropertiesWithoutLeaking(property);
                foreach (var prop in materialProps)
                {
                    result[property].Add(prop.GetGetMethod());
                }
            }

            return result;
        }

        private static bool HasMaterialProperty(PropertyInfo prop)
        {
            return prop.PropertyType == typeof(Material) || prop.PropertyType == typeof(Material[]);
        }

        private static List<Material> GetMaterials(Object obj)
        {
            var result = new List<Material>();

            var allMaterialProperties = obj.GetType().GetMaterialPropertiesWithoutLeaking();
            foreach (var property in allMaterialProperties)
            {
                var value = property.GetGetMethod().GetMaterialFromMethod(obj, (methodName, objectName) =>
                    $"The method {methodName} was not found on {objectName}. This property will not be indexed.");

                if (value is Material materialResult)
                {
                    result.Add(materialResult);
                }
                else if (value is Material[] materialList)
                {
                    result.AddRange(materialList);
                }
            }

            return result;
        }

        /// <summary>
        /// Gets all of the types in the Material Reference lookup that are components. Used to determine whether to run the
        /// method directly or on the component
        /// </summary>
        /// <returns>List of types that are components</returns>
        public static List<Type> GetComponentTypes()
        {
            return MaterialReferenceLookup.Keys.Where(key => typeof(Component).IsAssignableFrom(key)).ToList();
        }

        /// <summary>
        /// Gets all material properties from an object or a component of an object
        /// </summary>
        /// <param name="obj">The GameObject or Scriptable Object</param>
        /// <returns>List of Materials</returns>
        public static List<Material> GetMaterialsFromObject(Object obj)
        {
            var result = new List<Material>();

            if (obj is GameObject go)
            {
                foreach (var key in GetComponentTypes())
                {
                    var components = go.GetComponentsInChildren(key);
                    foreach (var component in components)
                    {
                        result.AddRange(GetMaterials(component));
                    }
                }
            }
            else
            {
                result.AddRange(GetMaterials(obj));
            }

            return result.Distinct().ToList();
        }

        /// <summary>
        /// Text Mesh pro will sometimes be missing the GetFontSharedMaterials method, even though the property is supposed
        /// to have that method. This gracefully handles that case.
        /// </summary>
        /// <param name="method">The Method being invoked</param>
        /// <param name="obj">The Unity Object the method is invoked upon</param>
        /// <param name="generateErrorString">The function that takes the method name and object name and produces an error string</param>
        /// <returns>The resulting object from invoking the method on the Object</returns>
        /// <exception cref="Exception">Any exception that is not the missing method exception</exception>
        public static object GetMaterialFromMethod(this MethodInfo method,
            Object obj,
            Func<string, string, string> generateErrorString)
        {
            object result = null;
            try
            {
                result = method.Invoke(obj, null);
            }
            catch (Exception e)
            {
                // swallow the missing method exception, there's nothing we can do about it at this point
                // and we've already checked for other possible null exceptions here
                if ((e.InnerException is NullReferenceException))
                {
                    Debug.LogWarning(generateErrorString(method.Name, obj.name));
                }
                else
                {
                    throw e;
                }
            }

            return result;
        }

        /// <summary>
        /// Gets the SharedMaterial(s) properties when there are shared materials so that we don't leak material instances into the scene
        /// </summary>
        /// <param name="property">The property Type that we are getting the SharedMaterial(s) properties from</param>
        /// <returns>List of shared material properties and other material properties that won't leak material instances</returns>
        public static IEnumerable<PropertyInfo> GetMaterialPropertiesWithoutLeaking(this Type property)
        {
            var materialProps = property.GetProperties().Where(HasMaterialProperty).ToList();

            // if there is a sharedMaterial property or sharedMaterials property, remove the property that will leak materials
            var sharedMaterialProps =
                materialProps.Where(prop => prop.Name.ToLowerInvariant().Contains("shared")).ToList();

            var propsToRemove = sharedMaterialProps
                .Select(prop => prop.Name.ToLowerInvariant().Replace("shared", string.Empty))
                .ToList();
            materialProps.RemoveAll(prop => propsToRemove.Contains(prop.Name.ToLowerInvariant()));

            // also remove any property which has no setter
            materialProps.RemoveAll(prop => prop.SetMethod == null);

            return materialProps;
        }

        /// <summary>
        /// Get whether or not a Material is considered readonly (Built In Resource)
        /// </summary>
        /// <param name="material">The Material to test</param>
        /// <returns>Boolean of whether or not that Material is considered readonly</returns>
        public static bool GetIsReadonlyMaterial(Material material)
        {
            var assetPath = AssetDatabase.GetAssetPath(material);

            return string.IsNullOrEmpty(assetPath) || assetPath.Equals(@"Resources/unity_builtin_extra", StringComparison.OrdinalIgnoreCase);
        }
    }
}