﻿
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using UdonSharp.Compiler;
using UdonSharp.Compiler.Udon;
using UnityEditor;
using UnityEngine;
using VRC.Udon;

namespace UdonSharp
{
    internal static class UdonSharpUtils
    {
        // https://stackoverflow.com/questions/6386202/get-type-name-without-any-generics-info
        public static string GetNameWithoutGenericArity(this System.Type t)
        {
            if (!t.IsGenericType)
                return t.Name;

            string name = t.Name;
            int index = name.IndexOf('`');
            return index == -1 ? name : name.Substring(0, index);
        }

        private static readonly HashSet<System.Type> _unsignedTypes = new HashSet<System.Type>()
        {
            typeof(byte),
            typeof(ushort),
            typeof(uint),
            typeof(ulong),
        };

        private static readonly HashSet<System.Type> _signedTypes = new HashSet<System.Type>()
        {
            typeof(sbyte),
            typeof(short),
            typeof(int),
            typeof(long),
        };

        private static readonly HashSet<System.Type> _integerTypes = new HashSet<System.Type>()
        {
            typeof(byte),
            typeof(sbyte),
            typeof(short),
            typeof(ushort),
            typeof(int),
            typeof(uint),
            typeof(long),
            typeof(ulong),
        };

        private static readonly HashSet<System.Type> _floatTypes = new HashSet<System.Type>()
        {
            typeof(float),
            typeof(double),
            typeof(decimal),
        };

        public static bool IsSignedType(System.Type type)
        {
            return _signedTypes.Contains(type);
        }

        public static bool IsUnsignedType(System.Type type)
        {
            return _unsignedTypes.Contains(type);
        }

        public static bool IsIntegerType(System.Type type)
        {
            return _integerTypes.Contains(type);
        }

        public static bool IsFloatType(System.Type type)
        {
            return _floatTypes.Contains(type);
        }

        public static bool IsNumericType(System.Type type)
        {
            return IsIntegerType(type) || IsFloatType(type);
        }

        public static bool IsExplicitlyAssignableFrom(this System.Type targetType, System.Type assignee)
        {
            // Normal explicit assign
            if (targetType.IsAssignableFrom(assignee))
                return true;

            // Numeric conversions
            if (IsNumericType(targetType) && IsNumericType(assignee))
                return true;

            // Handle user-defined implicit conversion operators defined on both sides
            // Roughly follows https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/conversions#processing-of-user-defined-implicit-conversions

            // I doubt I'll ever deal with properly supporting nullable but ¯\_(ツ)_/¯
            if (System.Nullable.GetUnderlyingType(targetType) != null)
                targetType = System.Nullable.GetUnderlyingType(targetType);
            if (System.Nullable.GetUnderlyingType(assignee) != null)
                assignee = System.Nullable.GetUnderlyingType(assignee);

            List<System.Type> operatorTypes = new List<System.Type>();
            operatorTypes.Add(targetType);

            System.Type currentSourceType = assignee;
            while (currentSourceType != null)
            {
                operatorTypes.Add(currentSourceType);
                currentSourceType = currentSourceType.BaseType;
            }

            foreach (System.Type operatorType in operatorTypes)
            {
                IEnumerable<MethodInfo> methods = operatorType.GetMethods(BindingFlags.Public | BindingFlags.Static).Where(e => e.Name == "op_Implicit");

                foreach (MethodInfo methodInfo in methods)
                {
                    if (methodInfo.ReturnType == targetType && (methodInfo.GetParameters()[0].ParameterType == assignee || methodInfo.GetParameters()[0].ParameterType == typeof(UnityEngine.Object)))
                        return true;
                }
            }

            foreach (System.Type operatorType in operatorTypes)
            {
                IEnumerable<MethodInfo> methods = operatorType.GetMethods(BindingFlags.Public | BindingFlags.Static).Where(e => e.Name == "op_Explicit");

                foreach (MethodInfo methodInfo in methods)
                {
                    if (methodInfo.ReturnType == targetType && (methodInfo.GetParameters()[0].ParameterType == assignee || methodInfo.GetParameters()[0].ParameterType == typeof(UnityEngine.Object)))
                        return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Escapes property names into something Udon allows since C# will put '<' and '>' in property backing field names
        /// </summary>
        internal static string UnmanglePropertyFieldName(string propertyName)
        {
            return propertyName?.Replace('<', '_').Replace('>', '_');
        }

        public static bool IsUserDefinedBehaviour(System.Type type)
        {
            return type == typeof(UdonSharpBehaviour) ||
                   type.IsSubclassOf(typeof(UdonSharpBehaviour)) ||
                  (type.IsArray && (type.GetElementType() == typeof(UdonSharpBehaviour) || type.GetElementType().IsSubclassOf(typeof(UdonSharpBehaviour)) ||
                                    type.GetElementType() == typeof(UdonBehaviour) || type.GetElementType().IsSubclassOf(typeof(UdonBehaviour))));
        }
        
        public static bool IsUserJaggedArray(System.Type type)
        {
            return type.IsArray && type.GetElementType().IsArray;
        }

        public static bool IsUserDefinedEnum(System.Type type)
        {
            return (type.IsEnum && !CompilerUdonInterface.IsExternType(type)) || 
                   (type.IsArray && type.GetElementType().IsEnum && !CompilerUdonInterface.IsExternType(type.GetElementType()));
        }

        public static bool IsUserDefinedType(System.Type type)
        {
            return IsUserDefinedBehaviour(type) ||
                   IsUserJaggedArray(type) ||
                   IsUserDefinedEnum(type);
        }
        
        private static volatile Dictionary<Type, Type> _inheritedTypeMap;
        private static readonly object _inheritedTypeMapLock = new object();

        private static Dictionary<Type, Type> GetInheritedTypeMap()
        {
            if (_inheritedTypeMap != null)
                return _inheritedTypeMap;
            
            lock (_inheritedTypeMapLock)
            {
                if (_inheritedTypeMap != null)
                    return _inheritedTypeMap;
                
                Dictionary<Type, Type> typeMap = new Dictionary<Type, Type>();

                IEnumerable<Type> typeList = AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "VRCSDK3").GetTypes();

                foreach (Type childType in typeList)
                {
                    if (childType.BaseType?.Namespace != null &&
                        childType.BaseType.Namespace.StartsWith("VRC.SDKBase", StringComparison.Ordinal) && 
                        childType.BaseType.Name != "VRCNetworkBehaviour")
                    {
                        if (typeMap.ContainsKey(childType.BaseType))
                        {
                            LogWarning($"Inherited type map already contains an entry for {childType.BaseType}, existing pair: ({childType.BaseType}, {typeMap[childType.BaseType]})");
                            continue;
                        }
                        
                        typeMap.Add(childType.BaseType, childType);
                    }
                }

                typeMap.Add(typeof(VRC.SDK3.Video.Components.VRCUnityVideoPlayer), typeof(VRC.SDK3.Video.Components.Base.BaseVRCVideoPlayer));
                typeMap.Add(typeof(VRC.SDK3.Video.Components.AVPro.VRCAVProVideoPlayer), typeof(VRC.SDK3.Video.Components.Base.BaseVRCVideoPlayer));

                _inheritedTypeMap = typeMap;
            }

            return _inheritedTypeMap;
        }

        internal static Type RemapBaseType(Type type)
        {
            Dictionary<Type, Type> typeMap = GetInheritedTypeMap();

            int arrayDepth = 0;
            Type currentType = type;
            while (currentType.IsArray)
            {
                currentType = currentType.GetElementType();
                ++arrayDepth;
            }

            if (typeMap.ContainsKey(currentType))
            {
                type = typeMap[currentType];

                while (arrayDepth-- > 0)
                    type = type.MakeArrayType();
            }

            return type;
        }
        
        [ThreadStatic]
        private static Dictionary<Type, Type> _userTypeToUdonTypeCache;

        public static Type UserTypeToUdonType(Type type)
        {
            if (type == null)
                return null;

            if (_userTypeToUdonTypeCache == null)
                _userTypeToUdonTypeCache = new Dictionary<Type, Type>();

            if (_userTypeToUdonTypeCache.TryGetValue(type, out Type foundType))
                return foundType;
            
            Type udonType = null;

            if (IsUserDefinedType(type))
            {
                if (type.IsArray)
                {
                    if (!type.GetElementType().IsArray)
                    {
                        if (IsUserDefinedEnum(type))
                            udonType = type.GetElementType().GetEnumUnderlyingType().MakeArrayType();
                        else
                            udonType = typeof(UnityEngine.Component[]);// Hack because VRC doesn't expose the array type of UdonBehaviour
                    }
                    else // Jagged arrays
                    {
                        udonType = typeof(object[]);
                    }
                }
                else
                {
                    udonType = typeof(VRC.Udon.UdonBehaviour);
                }
            }

            if (udonType == null)
                udonType = type;

            udonType = RemapBaseType(udonType);
            
            _userTypeToUdonTypeCache.Add(type, udonType);

            return udonType;
        }

        public static void LogBuildError(string message, string filePath, int line, int character)
        {
            MethodInfo buildErrorLogMethod = typeof(Debug).GetMethod("LogPlayerBuildError", BindingFlags.NonPublic | BindingFlags.Static);

            string errorMessage = $"[<color=#FF00FF>UdonSharp</color>] {filePath}({line},{character}): {message}";

            buildErrorLogMethod.Invoke(null, new object[] {
                        errorMessage,
                        filePath,
                        line,
                        character });
        }

        public static void Log(object message)
        {
            Debug.Log($"[<color=#0c824c>UdonSharp</color>] {message}");
        }
        
        public static void Log(object message, UnityEngine.Object context)
        {
            Debug.Log($"[<color=#0c824c>UdonSharp</color>] {message}", context);
        }
        
        public static void LogWarning(object message)
        {
            Debug.LogWarning($"[<color=#FF00FF>UdonSharp</color>] {message}");
        }
        
        public static void LogWarning(object message, UnityEngine.Object context)
        {
            Debug.LogWarning($"[<color=#FF00FF>UdonSharp</color>] {message}", context);
        }
        
        public static void LogError(object message)
        {
            Debug.LogError($"[<color=#FF00FF>UdonSharp</color>] {message}");
        }
        
        public static void LogError(object message, UnityEngine.Object context)
        {
            Debug.LogError($"[<color=#FF00FF>UdonSharp</color>] {message}", context);
        }

        #if !UNITY_2022_3_OR_NEWER
        private static readonly MethodInfo _displayProgressBar = typeof(Editor).Assembly.GetTypes().FirstOrDefault(e => e.Name == "AsyncProgressBar")?.GetMethod("Display");
        private static readonly MethodInfo _clearProgressBar = typeof(Editor).Assembly.GetTypes().FirstOrDefault(e => e.Name == "AsyncProgressBar")?.GetMethod("Clear");
        #endif
        private static int _progressId;
        
        public static void ShowAsyncProgressBar(string text, float progress)
        {
            #if UNITY_2022_3_OR_NEWER
            if (_progressId == 0)
            {
                _progressId = Progress.Start("U# Compile", "Compiling UdonSharp scripts...");
            }
            Progress.Report(_progressId, progress, text);
            #else
            _displayProgressBar.Invoke(null, new object[] {text, progress});
            #endif
        }

        public static void ClearAsyncProgressBar()
        {
            #if UNITY_2022_3_OR_NEWER
            if (_progressId == 0) return;
            Progress.Remove(_progressId);
            _progressId = 0;
            #else
            _clearProgressBar.Invoke(null, null);
            #endif
        }

        public static void LogRuntimeError(string message, string prefix, string filePath, int line, int character)
        {
            MethodInfo buildErrorLogMethod = typeof(UnityEngine.Debug).GetMethod("LogPlayerBuildError", BindingFlags.NonPublic | BindingFlags.Static);

            string errorMessage = $"[<color=#FF00FF>UdonSharp</color>]{prefix} {filePath}({line + 1},{character}): {message}";

            buildErrorLogMethod.Invoke(null, new object[] {
                        errorMessage,
                        filePath,
                        line + 1,
                        character });
        }

        public static string ReadFileTextSync(string filePath, float timeoutSeconds = 2f)
        {
            bool sourceLoaded = false;

            string fileText = "";

            DateTime startTime = DateTime.Now;

            while (true)
            {
                System.IO.IOException exception = null;

                try
                {
                    fileText = System.IO.File.ReadAllText(filePath);
                    sourceLoaded = true;
                }
                catch (System.IO.IOException e)
                {
                    exception = e;

                    if (e is System.IO.FileNotFoundException ||
                        e is System.IO.DirectoryNotFoundException)
                        throw;
                }

                if (sourceLoaded)
                    break;
                
                System.Threading.Thread.Sleep(20);

                TimeSpan timeFromStart = DateTime.Now - startTime;

                if (timeFromStart.TotalSeconds > timeoutSeconds)
                {
                    Debug.LogError($"Timeout when attempting to read file {filePath}");
                    if (exception != null)
                        throw exception;
                }
            }

            return fileText;
        }

        internal static string HashString(string stringToHash)
        {
            using (SHA1Managed sha256 = new SHA1Managed())
            {
                return BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", "");
            }
        }
        
        internal static bool AllElementsMatch<T>(IEnumerable<T> collection)
        {
            IEnumerable<T> enumerable = collection as T[] ?? collection.ToArray();
            if (!enumerable.Any())
                return true;

            T firstValue = enumerable.First();

            return enumerable.All(e => e.Equals(firstValue));
        }

        /// <summary>
        /// Returns if a normal System.Object is null, and handles when a UnityEngine.Object referenced as a System.Object is null
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        internal static bool IsUnityObjectNull(this object value)
        {
            if (value == null)
                return true;

            if (value is UnityEngine.Object unityEngineObject && unityEngineObject == null)
                return true;

            return false;
        }

        internal static string[] GetProjectDefines(IEnumerable<string> defineList, bool editorBuild, BuildTarget buildTarget)
        {
            List<string> defines = new List<string>();

            foreach (string define in defineList)
            {
                if (!editorBuild)
                    if (define.StartsWith("UNITY_EDITOR"))
                        continue;

                if (FilterPerPlatformDefines(define, buildTarget, out var replacement))
                {
                    defines.Add(define);
                }
                else if (replacement != null)
                {
                    defines.Add(replacement);
                }
            }

            defines.Add("COMPILER_UDONSHARP");

            return defines.ToArray();
        }

        private static bool FilterPerPlatformDefines(string define, BuildTarget buildTarget, out string replacement)
        {
            replacement = null;
            switch (define)
            {
                case "UNITY_STANDALONE":
                {
                    switch (buildTarget)
                    {
                        case BuildTarget.StandaloneWindows64:
                            return true;
                        case BuildTarget.Android:
                            replacement = "UNITY_ANDROID";
                            return false;
                        case BuildTarget.iOS:
                            replacement = "UNITY_IOS";
                            return false;
                    }
                    break;
                }
                case "UNITY_ANDROID":
                {
                    switch (buildTarget)
                    {
                        case BuildTarget.StandaloneWindows64:
                            replacement = "UNITY_STANDALONE";
                            return false;
                        case BuildTarget.Android:
                            return true;
                        case BuildTarget.iOS:
                            replacement = "UNITY_IOS";
                            return false;
                    }
                    break;
                }
                case "UNITY_IOS":
                {
                    switch (buildTarget)
                    {
                        case BuildTarget.StandaloneWindows64:
                            replacement = "UNITY_STANDALONE";
                            return false;
                        case BuildTarget.Android:
                            replacement = "UNITY_ANDROID";
                            return false;
                        case BuildTarget.iOS:
                            return true;
                    }
                    break;
                }
            }
            
            return true;
        }

        internal static void ShowEditorNotification(string notificationString)
        {
            typeof(SceneView).GetMethod("ShowNotification", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { notificationString });
        }

        public static bool DoesUnityProjectHaveCompileErrors()
        {
            Type logEntryType = typeof(Editor).Assembly.GetType("UnityEditor.LogEntries");
            MethodInfo getLinesAndModeMethod = logEntryType.GetMethod("GetLinesAndModeFromEntryInternal", BindingFlags.Public | BindingFlags.Static);
            
            bool hasCompileError = false;
            
            int logEntryCount = (int)logEntryType.GetMethod("StartGettingEntries", BindingFlags.Public | BindingFlags.Static).Invoke(null, Array.Empty<object>());

            try
            {
                object[] getLinesParams = { 0, 1, 0, "" };

                for (int i = 0; i < logEntryCount; ++i)
                {
                    getLinesParams[0] = i;
                    getLinesAndModeMethod.Invoke(null, getLinesParams);

                    int mode = (int)getLinesParams[2];

                    // 1 << 11 == ConsoleWindow.Mode.ScriptCompileError
                    if ((mode & (1 << 11)) != 0)
                    {
                        hasCompileError = true;
                        break;
                    }
                }
            }
            finally
            {
                logEntryType.GetMethod("EndGettingEntries").Invoke(null, Array.Empty<object>());
            }

            return hasCompileError;
        }

        internal static void SetDirty(UnityEngine.Object obj)
        {
            EditorUtility.SetDirty(obj);
            PrefabUtility.RecordPrefabInstancePropertyModifications(obj);
        }

        private static PropertyInfo _getLoadedAssembliesProp;

        internal static IEnumerable<Assembly> GetLoadedEditorAssemblies()
        {
            if (_getLoadedAssembliesProp == null)
            {
                Assembly editorAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(e => e.GetName().Name == "UnityEditor");
                Type editorAssembliesType = editorAssembly.GetType("UnityEditor.EditorAssemblies");
                _getLoadedAssembliesProp = editorAssembliesType.GetProperty("loadedAssemblies", BindingFlags.Static | BindingFlags.NonPublic);
            }

            return (Assembly[])_getLoadedAssembliesProp.GetValue(null);
        }

        /// <summary>
        /// Used to prevent Odin's DefaultSerializationBinder from getting a callback to register an assembly in specific cases where it will explode due to https://github.com/mono/mono/issues/20968
        /// </summary>
        internal class UdonSharpAssemblyLoadStripScope : IDisposable
        {
            Delegate[] originalDelegates;

            public UdonSharpAssemblyLoadStripScope()
            {
                FieldInfo info = AppDomain.CurrentDomain.GetType().GetField("AssemblyLoad", BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance);

                AssemblyLoadEventHandler handler = info.GetValue(AppDomain.CurrentDomain) as AssemblyLoadEventHandler;

                originalDelegates = handler?.GetInvocationList();

                if (originalDelegates != null)
                {
                    foreach (Delegate del in originalDelegates)
                        AppDomain.CurrentDomain.AssemblyLoad -= (AssemblyLoadEventHandler)del;
                }
            }

            public void Dispose()
            {
                if (originalDelegates != null)
                {
                    foreach (Delegate del in originalDelegates)
                        AppDomain.CurrentDomain.AssemblyLoad += (AssemblyLoadEventHandler)del;
                }
            }
        }

        internal static void ResetAssemblyCaches()
        {
            CompilationContext.ResetAssemblyCaches();
        }
    }
}
