#if UNITY_EDITOR
using System;
using System.Linq.Expressions;
using System.Reflection;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;
namespace Kamgam.SkyClouds.URP
{ 
    /// 
    /// Allows you to register a callback before compilation
    /// which is then executed automatically after compilation.
    /// 
    /// Methods registered are usually executed in FIFO order. Though there
    /// is no guarantee that this will always be the case.
    /// 
    /// CrossCompileCallbacks.RegisterCallback(testCallbackA); // It can find the type automatically.
    /// CrossCompileCallbacks.RegisterCallback(typeof(YourClass), "testCallbackA");
    /// 
    /// 
    public static class CrossCompileCallbacks
    {
        /// 
        /// If set to true then the callbacks will not be called immediately but
        /// within the next editor update cycle.
        /// Use this to avoid "Calling ... from assembly reloading callbacks are not supported." errors.
        /// 
        public static bool DelayExecutionAfterCompilation
        {
            get => SessionState.GetBool(typeName() + ".DelayExecution", false);
            set => SessionState.SetBool(typeName() + ".DelayExecution", value);
        }
        static string typeName() => typeof(CrossCompileCallbacks).FullName;
        const string _maxIndexKey = ".MaxIndex";
        static string maxIndexKey() => typeName() + _maxIndexKey;
        const string _lastReleasedIndexKey = ".LastReleasedIndex";
        static string lastReleasedIndexKey() => typeName() + _lastReleasedIndexKey;
        const string _indexTypeKey = ".Index[{0}].Type";
        static string indexTypeKey(int index) => string.Format(typeName() + _indexTypeKey, index);
        const string _indexMethodKey = ".Index[{0}].Method";
        static string indexMethodKey(int index) => string.Format(typeName() + _indexMethodKey, index);
        static int getMaxIndex()
        {
            return SessionState.GetInt(maxIndexKey(), -1);
        }
        static int getNextIndex()
        {
            int maxIndex;
            // Try to reuse an old index (update max index if necessary)
            int reusableIndex = SessionState.GetInt(lastReleasedIndexKey(), -1);
            if (reusableIndex >= 0)
            {
                SessionState.SetInt(lastReleasedIndexKey(), -1);
                maxIndex = getMaxIndex();
                if(maxIndex < reusableIndex)
                    SessionState.SetInt(maxIndexKey(), reusableIndex);
                return reusableIndex;
            }
            // New index needed (increase max index).
            maxIndex = SessionState.GetInt(maxIndexKey(), -1);
            maxIndex++;
            SessionState.SetInt(maxIndexKey(), maxIndex);
            return maxIndex;
        }
        public static void ReleaseIndex(int index)
        {
            if (index < 0)
                return;
            SessionState.SetInt(lastReleasedIndexKey(), index);
            SessionState.EraseString(indexTypeKey(index));
            SessionState.EraseString(indexMethodKey(index));
            // Decrease or erase max index if needed.
            int maxIndex = getMaxIndex();
            if(index == maxIndex)
            {
                maxIndex--;
                if(maxIndex < 0)
                    SessionState.EraseInt(maxIndexKey());
                else
                    SessionState.SetInt(maxIndexKey(), maxIndex);
            }
        }
        public static void ReleaseAllOnType(Type type)
        {
            if (type == null)
                return;
            int maxIndex = getMaxIndex();
            for (int i = maxIndex; i >= 0; i--)
            {
                string typeName;
                GetCallbackInfo(i, out typeName, out _);
                if(typeName == type.FullName)
                {
                    ReleaseIndex(i);
                }
            }
        }
        /// 
        /// Registers a callback and returns an index >= 0 on success and -1 on failure.
        /// 
        /// A static method without any parameters.
        /// 
        public static int RegisterCallback(System.Action callback)
        {
            if (callback == null)
                return -1;
            var methodInfo = callback.GetMethodInfo();
            if (methodInfo == null)
                return -1;
            if (!methodInfo.IsStatic)
            {
                Debug.Log("Method needs to be static.");
                return -1;
            }
            return RegisterCallback(methodInfo.DeclaringType, methodInfo.Name);
        }
        /// 
        /// Registers a callback and returns an index >= 0 on success and -1 on failure.
        /// 
        /// 
        /// A static method without any parameters.
        /// 
        public static int RegisterCallback(Type type, string staticMethodName)
        {
            if (type == null || string.IsNullOrEmpty(staticMethodName))
            {
                Debug.Assert(type != null);
                Debug.Assert(staticMethodName != null);
                return -1;
            }
            // Check if methods has any parameters (that's not supported)
            try
            {
                var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
                var methodInfo = type.GetMethod(staticMethodName, flags);
                if (methodInfo == null)
                {
                    Debug.LogError("No static method '" + staticMethodName + "' found in '" + type.FullName + "'.");
                    return -1;
                }
                if (methodInfo.GetParameters().Length > 0)
                {
                    Debug.Assert(methodInfo.GetParameters().Length == 0);
                    return -1;
                } 
            }
            catch (System.Exception e)
            {
                Debug.LogError($"CrossCompileCallbacks: Error while checking '{staticMethodName}' method parameters. Error:\n" + e.Message);
            }
            int index = getNextIndex();
            SessionState.SetString(indexTypeKey(index), type.FullName);
            SessionState.SetString(indexMethodKey(index), staticMethodName);
            return index;
        }
        public static void GetCallbackInfo(int index, out string typeName, out string methodName)
        {
            typeName = SessionState.GetString(indexTypeKey(index), null);
            methodName = SessionState.GetString(indexMethodKey(index), null);
        }
        [DidReloadScripts(-1)]
        static void onAfterCompilation()
        {
            if (DelayExecutionAfterCompilation)
            {
                EditorApplication.delayCall -= delayedExecuteRegisteredCallbacks;
                EditorApplication.delayCall += delayedExecuteRegisteredCallbacks;
            }
            else
            {
                executeRegisteredCallbacks();
            }
        }
        static void delayedExecuteRegisteredCallbacks()
        {
            EditorApplication.delayCall -= delayedExecuteRegisteredCallbacks;
            executeRegisteredCallbacks();
        }
        static void executeRegisteredCallbacks()
        {
            int maxIndex = getMaxIndex();
            for (int i = maxIndex; i >= 0; i--)
            {
                string typeName;
                string methodName;
                GetCallbackInfo(i, out typeName, out methodName);
                try 
                {
                    ReleaseIndex(i);
                    if (string.IsNullOrEmpty(typeName) || string.IsNullOrEmpty(methodName))
                        continue;
                    var methodInfo = findStaticMethod(typeName, methodName);
                    methodInfo.Invoke(null, null);
                }
                catch (System.Exception e)
                {
                    string errorMsg = e.Message;
                    if(errorMsg.Contains("invocation") && e.InnerException != null)
                    {
                        errorMsg += "\n" + e.InnerException.Message;
                    }
                    Debug.LogError($"CrossCompileCallbacks: Calling '{typeName}.{methodName}' failed. Error:\n" + errorMsg);
                }
            }
        }
        static MethodInfo findStaticMethod(string fullTypeName, string methodName)
        {
            var type = findType(fullTypeName);
            if (type == null)
                return null;
            var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
            var methodInfo = type.GetMethod(methodName, flags);
            return methodInfo;
        }
        static Type findType(string fullTypeName)
        {
            Debug.Assert(fullTypeName != null);
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (var assembly in assemblies)
            {
                Type t = assembly.GetType(fullTypeName, throwOnError: false);
                if (t != null)
                    return t;
            }
            throw new ArgumentException("Type " + fullTypeName + " doesn't exist in the current app domain.");
        }
        /// 
        /// Utility method to store a static parameterless Action
        /// in the SessionState for retrieval at a later time.
        /// 
        /// 
        /// 
        /// 
        public static bool StoreAction(string sessionStorageKey, System.Action action)
        {
            if (action == null)
                return false;
            var methodInfo = action.GetMethodInfo();
            if (methodInfo == null)
                return false;
            if (!methodInfo.IsStatic)
            {
                Debug.Log("Method '"+ methodInfo.Name + "'needs to be static.");
                return false;
            }
            SessionState.SetString(sessionStorageKey + ".Type", methodInfo.DeclaringType.FullName);
            SessionState.SetString(sessionStorageKey + ".Method", methodInfo.Name);
            return true;
        }
        /// 
        /// Retrieves the Action from the SessionState.
        /// 
        /// 
        /// 
        public static System.Action GetStoredAction(string sessionStorageKey)
        {
            var typeName = SessionState.GetString(sessionStorageKey + ".Type", null);
            var methodName = SessionState.GetString(sessionStorageKey + ".Method", null);
            if (string.IsNullOrEmpty(typeName) || string.IsNullOrEmpty(methodName))
            {
                return null;
            }
            var type = findType(typeName);
            if (type == null)
                return null;
            var methodInfo = findStaticMethod(typeName, methodName);
            if (methodInfo == null)
                return null;
            return (Action) Delegate.CreateDelegate(typeof(Action), methodInfo);
        }
        public static void ClearStoredAction(string sessionStorageKey)
        {
            SessionState.EraseString(sessionStorageKey + ".Type");
            SessionState.EraseString(sessionStorageKey + ".Method");
        }
        // Testing
        /*
        [DidReloadScripts]
        static void StartTest()
        {
            Debug.Log("CrossCompileCallbacks: Starting test.");
            RegisterCallback(testCallbackA);
            RegisterCallback(typeof(CrossCompileCallbacks), "testCallbackB");
            var action = GetStoredAction("storedActionA");
            ClearStoredAction("storedActionA");
            if (action != null)
                action.Invoke();
            StoreAction("storedActionA", storedActionA);            
        }
        static void testCallbackA()
        {
            Debug.Log("Test callback A executed."); 
        }
        static void testCallbackB()
        {
            Debug.Log("Test callback B executed."); 
        }
        static void storedActionA()
        {
            Debug.Log("Stored action A executed.");
        }
        //*/
    }
}
#endif