using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using JetBrains.Annotations;
using Unity.Profiling;
using UnityEngine;
using VRC.SDKBase;
using VRC.SDK3;
using VRC.SDK3.Components;
using VRC.SDK3.UdonNetworkCalling;
using VRC.Udon.ClientBindings.Interfaces;
using VRC.Udon.Common;
using VRC.Udon.Common.Attributes;
using VRC.Udon.Common.Enums;
using VRC.Udon.Common.Interfaces;
using VRC.Udon.Security;
using VRC.Udon.Serialization.OdinSerializer;
using VRC.Udon.VM;
using Logger = VRC.Core.Logger;
using Object = UnityEngine.Object;
using SyncType = VRC.SDKBase.Networking.SyncType;


#if VRC_CLIENT
using VRC.Core;
#endif

#if UNITY_EDITOR && !VRC_CLIENT
using UnityEditor.SceneManagement;
#endif

namespace VRC.Udon
{
    public sealed class UdonBehaviour : AbstractUdonBehaviour, ISerializationCallbackReceiver
#if VRC_CLIENT
        , VRC.Core.Networking.INetworkReadyReceiver
#endif
    {
        #region Odin Serialized Fields

        [PublicAPI]
        public IUdonVariableTable publicVariables = new UdonVariableTable();

        #endregion

        #region Serialized Public Fields

        [Obsolete("Use VRCObjectSync instead")]
        [PublicAPI]
        // ReSharper disable once InconsistentNaming
        public bool SynchronizePosition;

        // ReSharper disable once InconsistentNaming
        [PublicAPI]
        public readonly bool SynchronizeAnimation = false; //We don't support animation sync yet, coming soon.

        // ReSharper disable once InconsistentNaming

        [Obsolete("Use VRCObjectSync instead")]
        [PublicAPI]
        public bool AllowCollisionOwnershipTransfer = true;

        // ReSharper disable once InconsistentNaming
        [HideInInspector, Obsolete("Use SyncMethod instead")]
        public bool Reliable = false;

        // ReSharper disable once InconsistentNaming
        [SerializeField]
        private SyncType _syncMethod = SyncType.Unknown;

        public override SyncType SyncMethod
        {
            get 
            {
                // Old Scene?
                if(_syncMethod == SyncType.Unknown)
                {
#pragma warning disable 618
                    _syncMethod = Reliable ? SyncType.Manual : SyncType.Continuous;
#pragma warning restore 618
                }

                return _syncMethod;
            }
            set
            {
                _syncMethod = value;
                if(value == SyncType.None)
                {
                    return;
                }

                // All synced UdonBehaviours on one GameObject must use the same sync method.
#if VRC_CLIENT
                using(gameObject.GetComponentsPooled(out List<UdonBehaviour> behaviours))
#else
                UdonBehaviour[] behaviours = gameObject.GetComponents<UdonBehaviour>();
#endif
                {
                    foreach(UdonBehaviour ub in behaviours)
                    {
                        if(ub != null && ub._syncMethod != SyncType.None)
                        {
                            ub._syncMethod = value;
                        }
                    }
                }
            }
        }

        public bool HasDoneStart => _hasDoneStart;

        public bool HasError => _hasError;

        public bool SyncIsContinuous => SyncMethod == SyncType.Continuous;
        public bool SyncIsManual => SyncMethod == SyncType.Manual;

        #endregion

        #region Serialized Private Fields

        [SerializeField]
        private AbstractSerializedUdonProgramAsset serializedProgramAsset;

#if UNITY_EDITOR && !VRC_CLIENT
        [SerializeField]
        public AbstractUdonProgramSource programSource;
#endif

        #endregion

        #region Public Fields and Properties

        [PublicAPI]
        public static Action<UdonBehaviour, IUdonProgram> OnInit { get; set; } = null;
                
        [PublicAPI]
        public static Action<UdonBehaviour> RequestSerializationHook { get; set; } = null;
        
        [PublicAPI]
        public override bool DisableInteractive { get; set; }

        [PublicAPI]
        [ExcludeFromUdonWrapper]
        public override bool IsNetworkingSupported
        {
            get => _isNetworkingSupported;
            set
            {
                if (_initialized)
                {
                    throw new InvalidOperationException(
                        "IsNetworkingSupported cannot be changed after the UdonBehaviour has been initialized.");
                }

                _isNetworkingSupported = value;
            }
        }

        public override bool IsInteractive => _hasInteractiveEvents && !DisableInteractive;
        
        // ReSharper disable once InconsistentNaming
        public const string ReturnVariableName = "__returnValue";

        internal int UpdateOrder => _program?.UpdateOrder ?? 0;

        [PublicAPI]
        public override bool DisableEventProcessing { get; set; } = false;

        public int ProgramId => serializedProgramAsset != null ? serializedProgramAsset.GetInstanceID() : 0;

        public ulong ProgramSize => serializedProgramAsset != null ? serializedProgramAsset.GetSerializedProgramSize() : 0L;

        public override NetworkCallingEntrypointMetadata[] GetNetworkCallingMetadata() =>
            serializedProgramAsset != null ? serializedProgramAsset.GetNetworkCallingMetadata() : null;
        public override NetworkCallingEntrypointMetadata GetNetworkCallingMetadata(string entrypoint) =>
            serializedProgramAsset != null ? serializedProgramAsset.GetNetworkCallingMetadata(entrypoint) : null;

        public override bool TryGetEntrypointNameFromHash(uint hash, out string entrypoint)
        {
            if (serializedProgramAsset == null)
            {
                entrypoint = null;
                return false;
            }
            return serializedProgramAsset.TryGetEntrypointNameFromHash(hash, out entrypoint);
        }
        public override bool TryGetEntrypointHashFromName(string entrypoint, out uint hash)
        {
            if (serializedProgramAsset == null)
            {
                hash = 0;
                return false;
            }
            return serializedProgramAsset.TryGetEntrypointHashFromName(entrypoint, out hash);
        }

        public bool IsInitialized => _initialized;

        // a component index that doesn't change at runtime so it can be used as an identifier
        [NonSerialized] private int _componentIndexFixed = -1;
        public override int GetComponentIndexFixed()
        {
            if (_componentIndexFixed == -1)
                _componentIndexFixed = GetComponentIndex();
            return _componentIndexFixed;
        }

        #endregion

        #region Private Fields and Properties

        private UdonManager _udonManager;
        private IUdonProgram _program;
        private IUdonVM _udonVM;
        private bool _isReady;
        private string _categoryName;
        private bool _hasError;
        private bool _hasDoneStart;
        private bool _initialized;
        private bool _isNetworkingSupported = false;
        
        private bool _hasInteractiveEvents;
        private bool _hasUpdateEvent;
        private bool _hasLateUpdateEvent;
        private bool _hasFixedUpdateEvent;
        private bool _hasPostLateUpdateEvent;
        private readonly Dictionary<string, List<uint>> _eventTable = new Dictionary<string, List<uint>>();

        private readonly Dictionary<(string eventName, string symbolName), string> _symbolNameCache =
            new Dictionary<(string, string), string>();

        private static ProfilerMarker _managedUpdateProfilerMarker =
            new ProfilerMarker("UdonBehaviour.ManagedUpdate()");

        private static ProfilerMarker _managedLateUpdateProfilerMarker =
            new ProfilerMarker("UdonBehaviour.ManagedLateUpdate()");

        private static ProfilerMarker _managedFixedUpdateProfilerMarker =
            new ProfilerMarker("UdonBehaviour.ManagedFixedUpdate()");
			
        private static ProfilerMarker _postLateUpdateProfilerMarker =
            new ProfilerMarker("UdonBehaviour.PostLateUpdate()");

        private readonly SortedDictionary<uint, (uint, uint)> _variableToChangeEvent = new SortedDictionary<uint, (uint, uint)>();

        private readonly List<AbstractUdonBehaviourEventProxy> _eventProxies = new List<AbstractUdonBehaviourEventProxy>();
        
        #endregion

        #region Editor Only

#if UNITY_EDITOR && !VRC_CLIENT
        public void RunEditorUpdate(ref bool dirty)
        {
            if (programSource == null)
            {
                return;
            }

            programSource.RunEditorUpdate(this, ref dirty);

            if (!dirty)
            {
                return;
            }

            EditorSceneManager.MarkSceneDirty(gameObject.scene);
        }

#endif

        #endregion

        #region Private Methods

        private bool LoadProgram(IUdonSignatureVerifier signatureVerifier)
        {
            if (serializedProgramAsset == null)
            {
                return false;
            }

            if(_program == null)
            {
                bool verificationResult = signatureVerifier.VerifySignature(serializedProgramAsset as IUdonSignatureHolder);
                if (!verificationResult)
                {
                    Logger.LogError($"Program signature verification failed for {name}", _categoryName, this);
                    return false;
                }

                _program = serializedProgramAsset.RetrieveProgram();
            }

            IUdonSymbolTable symbolTable = _program?.SymbolTable;
            IUdonHeap heap = _program?.Heap;
            if (symbolTable == null || heap == null)
            {
                return false;
            }

            foreach (string variableSymbol in publicVariables.VariableSymbols)
            {
                if (!symbolTable.HasAddressForSymbol(variableSymbol))
                {
                    continue;
                }

                uint symbolAddress = symbolTable.GetAddressFromSymbol(variableSymbol);

                if (!publicVariables.TryGetVariableType(variableSymbol, out Type declaredType))
                {
                    continue;
                }

                publicVariables.TryGetVariableValue(variableSymbol, out object value);
                if (declaredType == typeof(GameObject) || declaredType == typeof(UdonBehaviour) ||
                    declaredType == typeof(Transform))
                {
                    if (value == null)
                    {
                        value = new UdonGameObjectComponentHeapReference(declaredType);
                        declaredType = typeof(UdonGameObjectComponentHeapReference);
                    }
                }

                heap.SetHeapVariable(symbolAddress, value, declaredType);
            }

            return true;
        }
        
        private void RegisterEventProxy<T>() where T : AbstractUdonBehaviourEventProxy
        {
            // Exit early if we already have a match.
            foreach (AbstractUdonBehaviourEventProxy existingProxy in _eventProxies)
            {
                if (!(existingProxy is T existingProxyAsT))
                {
                    continue;
                }

                if (existingProxyAsT.EventReceiver.Equals(this))
                {
                    return;
                }
            }

            AbstractUdonBehaviourEventProxy proxy = gameObject.AddComponent<T>();
#if UNITY_EDITOR
            proxy.hideFlags = HideFlags.HideInInspector | 
                              HideFlags.DontSaveInEditor | 
                              HideFlags.DontSaveInBuild;
#endif
            _udonManager.Blacklist(proxy, false);
            proxy.EventReceiver = this;
            proxy.enabled = enabled;

            _eventProxies.Add(proxy);
        }

        private void ProcessEntryPoints()
        {
            if (_program.EntryPoints.HasExportedSymbol("_interact"))
            {
                _hasInteractiveEvents = true;
            }

            if (_program.EntryPoints.HasExportedSymbol("_update"))
            {
                _hasUpdateEvent = true;
            }

            if (_program.EntryPoints.HasExportedSymbol("_lateUpdate"))
            {
                _hasLateUpdateEvent = true;
            }

            if (_program.EntryPoints.HasExportedSymbol("_fixedUpdate"))
            {
                _hasFixedUpdateEvent = true;
            }
            
            if (_program.EntryPoints.HasExportedSymbol("_postLateUpdate"))
            {
                _hasPostLateUpdateEvent = true;
            }

            DetectExistingProxies();
            if (_program.EntryPoints.HasExportedSymbol("_onRenderObject"))
            {
                RegisterEventProxy<OnRenderObjectProxy>();
            }

            if (_program.EntryPoints.HasExportedSymbol("_onWillRenderObject"))
            {
                RegisterEventProxy<OnWillRenderObjectProxy>();
            }

            if (_program.EntryPoints.HasExportedSymbol("_onTriggerStay") ||
                _program.EntryPoints.HasExportedSymbol("_onPlayerTriggerStay") ||
                _program.EntryPoints.HasExportedSymbol("_onDroneTriggerStay")
                )
            {
                RegisterEventProxy<OnTriggerStayProxy>();
            }

            if (_program.EntryPoints.HasExportedSymbol("_onCollisionStay") ||
                _program.EntryPoints.HasExportedSymbol("_onPlayerCollisionStay"))
            {
                RegisterEventProxy<OnCollisionStayProxy>();
            }
            
            if (_program.EntryPoints.HasExportedSymbol("_onAnimatorMove"))
            {
                RegisterEventProxy<OnAnimatorMoveProxy>();
            }

            if(_program.EntryPoints.HasExportedSymbol("_onAudioFilterRead"))
            {
                RegisterEventProxy<OnAudioFilterReadProxy>();
            }

            RegisterUpdate();

            _eventTable.Clear();
            foreach (string entryPoint in _program.EntryPoints.GetExportedSymbols())
            {
                uint address = _program.EntryPoints.GetAddressFromSymbol(entryPoint);

                if (!_eventTable.ContainsKey(entryPoint))
                {
                    _eventTable.Add(entryPoint, new List<uint>());
                }

                _eventTable[entryPoint].Add(address);

                _udonManager.RegisterInput(this, entryPoint, true);
                
                // check whether this is a variableChangedEvent
                if (entryPoint.StartsWith(VariableChangedEvent.EVENT_PREFIX))
                {
                    string variableName = entryPoint.Remove(0, VariableChangedEvent.EVENT_PREFIX.Length);
                    // ensure the variable with the matching name exists
                    if (_program.SymbolTable.TryGetAddressFromSymbol(variableName, out uint variableAddress))
                    {
                        // the old variable is only added if it's used, so just store default if it's not
                        _program.SymbolTable.TryGetAddressFromSymbol(string.Concat(VariableChangedEvent.OLD_VALUE_PREFIX, variableName), out uint oldVariableAddress);
                        
                        // add variable > event address lookup
                        _variableToChangeEvent.Add(variableAddress, (address, oldVariableAddress));
                    }
                }
            }
        }
        
        // GameObjects may sometimes already have proxies for this UdonBehaviour.
        // For example if the GameObject was cloned from a scene GameObject,
        // or the component was for some reason added in the editor (this is not an expected workflow).
        // In either case Unity will serialize the reference from the proxy to this UdonBehaviour,
        // but will not serialize the contents of the _eventProxies List so we need to build it.
        // If we don't do this then we may create a proxy where one already exists and events will run twice.
        private void DetectExistingProxies()
        {
            GetComponents(_eventProxies);
            for(int i = _eventProxies.Count - 1; i >= 0; i--)
            {
                AbstractUdonBehaviourEventProxy proxy = _eventProxies[i];
                if(proxy == null)
                {
                    _eventProxies.RemoveAt(i);
                    continue;
                }

                UdonBehaviour proxyEventReceiver = proxy.EventReceiver;

                // Destroy and remove all copied proxy components which don't have an EventReceiver.
                if(proxyEventReceiver == null)
                {
                    Destroy(proxy);
                    _eventProxies.RemoveAt(i);
                    continue;
                }

                // Remove all copied proxy components which aren't for this UdonBehaviour.
                if(proxyEventReceiver == this)
                {
                    continue;
                }

                _eventProxies.RemoveAt(i);
            }
        }

        private bool ResolveUdonHeapReferences(IUdonSymbolTable symbolTable, IUdonHeap heap)
        {
            bool success = true;
            foreach (string symbolName in symbolTable.GetSymbols())
            {
                try
                {
                    uint symbolAddress = symbolTable.GetAddressFromSymbol(symbolName);
                    object heapValue = heap.GetHeapVariable(symbolAddress);
                    if (!(heapValue is UdonBaseHeapReference udonBaseHeapReference))
                    {
                        continue;
                    }

                    if (!ResolveUdonHeapReference(heap, symbolAddress, udonBaseHeapReference))
                    {
                        success = false;
                    }
                }
                catch (Exception e)
                {
#if UNITY_EDITOR
                    Logger.LogError($"{e.Message}\n{e.StackTrace}");
#else
                    Logger.LogError(e.Message);
#endif
                    success = false;
                }
            }

            return success;
        }

        private bool ResolveUdonHeapReference(IUdonHeap heap, uint symbolAddress,
            UdonBaseHeapReference udonBaseHeapReference)
        {
            switch (udonBaseHeapReference)
            {
                case UdonGameObjectComponentHeapReference udonGameObjectComponentHeapReference:
                {
                    Type referenceType = udonGameObjectComponentHeapReference.type;
                    if (referenceType == typeof(GameObject))
                    {
                        heap.SetHeapVariable(symbolAddress, gameObject);
                        return true;
                    }
                    else if (referenceType == typeof(Transform))
                    {
                        heap.SetHeapVariable(symbolAddress, gameObject.transform);
                        return true;
                    }
                    else if (referenceType == typeof(UdonBehaviour) || referenceType == typeof(IUdonBehaviour))
                    {
                        heap.SetHeapVariable(symbolAddress, this);
                        return true;
                    }
                    else if (referenceType == typeof(Object))
                    {
                        heap.SetHeapVariable(symbolAddress, this);
                        return true;
                    }
                    else
                    {
                        Logger.Log(
                            $"Unsupported GameObject/Component reference type: {udonBaseHeapReference.GetType().Name}. Only GameObject, Transform, and UdonBehaviour are supported.",
                            _categoryName,
                            this);

                        return false;
                    }
                }
                default:
                {
                    Logger.Log(
                        $"Unknown heap reference type: {udonBaseHeapReference.GetType().Name}",
                        _categoryName,
                        this);

                    return false;
                }
            }
        }

        #endregion

        #region Managed Unity Events

        internal void ManagedUpdate()
        {
            using (_managedUpdateProfilerMarker.Auto())
            {
                if (!_hasDoneStart && _isReady)
                {
                    _hasDoneStart = true;
                    RunEvent("_onEnable");
                    RunEvent("_start");
                    if (!_hasUpdateEvent)
                    {
                        _udonManager.UnregisterUdonBehaviourUpdate(this);
                    }
                }

                RunEvent("_update");
            }
        }

        internal void ManagedLateUpdate()
        {
            using (_managedLateUpdateProfilerMarker.Auto())
            {
                RunEvent("_lateUpdate");
            }
        }

        internal void ManagedFixedUpdate()
        {
            using (_managedFixedUpdateProfilerMarker.Auto())
            {
                RunEvent("_fixedUpdate");
            }
        }

        internal void PostLateUpdate()
        {
            using (_postLateUpdateProfilerMarker.Auto())
            {
                RunEvent("_postLateUpdate");
            }
        }

        #endregion

        #region Unity Events

        public void OnAnimatorIK(int layerIndex)
        {
            RunEvent("_onAnimatorIK", ("layerIndex", layerIndex));
        }

        internal void ProxyOnAnimatorMove()
        {
            RunEvent("_onAnimatorMove");
        }

        internal void ProxyOnAudioFilterRead(float[] data, int channels)
        {
            RunEvent("_onAudioFilterRead", ("data", data), ("channels", channels));
        }

        public void OnBecameInvisible()
        {
            RunEvent("_onBecameInvisible");
        }

        public void OnBecameVisible()
        {
            RunEvent("_onBecameVisible");
        }

        public void OnCollisionEnter(Collision other)
        {
            var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject);
            if (player != null)
            {
                RunEvent("_onPlayerCollisionEnter", ("player", player));
            }
             else if (!UdonManager.Instance.IsBlacklisted(other)) 
            {
                RunEvent("_onCollisionEnter", ("other", other));
            }
        }

        public void OnCollisionEnter2D(Collision2D other)
        {
            if (!UdonManager.Instance.IsBlacklisted(other))
            {
                RunEvent("_onCollisionEnter2D", ("other", other));
            }
        }

        public void OnCollisionExit(Collision other)
        {
            var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject);
            if (player != null)
            {
                RunEvent("_onPlayerCollisionExit", ("player", player));
            }
            else if (!UdonManager.Instance.IsBlacklisted(other))
            {
                RunEvent("_onCollisionExit", ("other", other));
            }
        }

        public void OnCollisionExit2D(Collision2D other)
        {
            if (!UdonManager.Instance.IsBlacklisted(other))
            {
                RunEvent("_onCollisionExit2D", ("other", other));
            }
        }

        internal void ProxyOnCollisionStay(Collision other)
        {
            var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject);
            if (player != null)
            {
                RunEvent("_onPlayerCollisionStay", ("player", player));
            }
            else if (!UdonManager.Instance.IsBlacklisted(other))
            {
                RunEvent("_onCollisionStay", ("other", other));
            }
        }

        public void OnCollisionStay2D(Collision2D other)
        {
            if (!UdonManager.Instance.IsBlacklisted(other))
            {
                RunEvent("_onCollisionStay2D", ("other", other));
            }
        }

        public void OnDestroy()
        {
            serializedPublicVariablesBytesString = null;
            publicVariablesUnityEngineObjects = null;
            
            foreach (AbstractUdonBehaviourEventProxy proxy in _eventProxies)
            {
                if (proxy)
                {
                    Destroy(proxy);
                }
            }

            _eventProxies.Clear();

            if(_program == null)
            {
                return;
            }
            
            foreach (string entryPoint in _program.EntryPoints.GetExportedSymbols())
            {
                _udonManager.RegisterInput(this, entryPoint, false);
            }

            RunEvent("_onDestroy");

            _udonVM = null;
            _program = null;
        }

        public void OnDisable()
        {
            UnregisterUpdate();

            RunEvent("_onDisable");
        }

        public void OnDrawGizmos()
        {
            RunEvent("_onDrawGizmos");
        }

        public void OnDrawGizmosSelected()
        {
            RunEvent("_onDrawGizmosSelected");
        }

        public void OnEnable()
        {
            if (_initialized)
            {
                RegisterUpdate();
            }

            RunEvent("_onEnable");
        }

        public void OnJointBreak(float breakForce)
        {
            RunEvent("_onJointBreak", ("force", breakForce));
        }

        public void OnJointBreak2D(Joint2D brokenJoint)
        {
            RunEvent("_onJointBreak2D", ("joint", brokenJoint));
        }

        public void OnMouseDown()
        {
            RunEvent("_onMouseDown");
        }

        public void OnMouseDrag()
        {
            RunEvent("_onMouseDrag");
        }

        public void OnMouseEnter()
        {
            RunEvent("_onMouseEnter");
        }

        public void OnMouseExit()
        {
            RunEvent("_onMouseExit");
        }

        public void OnMouseOver()
        {
            RunEvent("_onMouseOver");
        }

        public void OnMouseUp()
        {
            RunEvent("_onMouseUp");
        }

        public void OnMouseUpAsButton()
        {
            RunEvent("_onMouseUpAsButton");
        }

        public void OnParticleCollision(GameObject other)
        {
            var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject);
            if (player != null)
            {
                RunEvent("_onPlayerParticleCollision", ("player", player));
            }
            else
            {
            RunEvent("_onParticleCollision", ("other", other));
        }
        }

        public void OnParticleTrigger()
        {
            RunEvent("_onParticleTrigger");
        }

        public void OnPostRender()
        {
            RunEvent("_onPostRender");
        }

        public void OnPreCull()
        {
            RunEvent("_onPreCull");
        }

        public void OnPreRender()
        {
            RunEvent("_onPreRender");
        }

        public void OnRenderImage(RenderTexture src, RenderTexture dest)
        {
            if (!_eventTable.ContainsKey("_onRenderImage") || _eventTable["_onRenderImage"].Count == 0)
            {
                Graphics.Blit(src, dest);
                return;
            }

            RunEvent("_onRenderImage", ("src", src), ("dest", dest));
        }

        internal void ProxyOnRenderObject()
        {
            RunEvent("_onRenderObject");
        }

        public void OnTransformChildrenChanged()
        {
            RunEvent("_onTransformChildrenChanged");
        }

        public void OnTransformParentChanged()
        {
            RunEvent("_onTransformParentChanged");
        }

        public void OnTriggerEnter(Collider other)
        {
            var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject);
            if (player != null)
            {
                RunEvent("_onPlayerTriggerEnter", ("player", player));
                return;
            }

            if(VRCDroneApi.TryGetDroneFromGameObject(other.gameObject, out var drone))
            {
                RunEvent(
                    "_onDroneTriggerEnter", 
                    ("drone", drone)
                );
                return;
            }
            
            // Try to process the event using registered consumers (e.g., VRCPropApi)
            if (UdonManager.Instance.TryNotifyOnTriggerEnterConsumers(this, other))
            {
                return;
            }
            
            if (!UdonManager.Instance.IsBlacklisted(other))
            {
                RunEvent("_onTriggerEnter", ("other", other));
            }
        }

        public void OnTriggerEnter2D(Collider2D other)
        {
            if (!UdonManager.Instance.IsBlacklisted(other))
            {
                RunEvent("_onTriggerEnter2D", ("other", other));
            }
        }

        public void OnTriggerExit(Collider other)
        {
            var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject);
            if (player != null)
            {
                RunEvent("_onPlayerTriggerExit", ("player", player));
                return;
            }
            
            if(VRCDroneApi.TryGetDroneFromGameObject(other.gameObject, out var drone))
            {
                RunEvent(
                    "_onDroneTriggerExit", 
                    ("drone", drone)
                );
                return;
            }
            
            // Try to process the event using registered consumers (e.g., VRCPropApi)
            if (UdonManager.Instance.TryNotifyOnTriggerExitConsumers(this, other))
            {
                return;
            }
            
            if (!UdonManager.Instance.IsBlacklisted(other))
            {
                RunEvent("_onTriggerExit", ("other", other));
            }
        }

        public void OnTriggerExit2D(Collider2D other)
        {
            if (!UdonManager.Instance.IsBlacklisted(other))
            {
                RunEvent("_onTriggerExit2D", ("other", other));
            }
        }

        internal void ProxyOnTriggerStay(Collider other)
        {
            var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject);
            if (player != null)
            {
                RunEvent("_onPlayerTriggerStay", ("player", player));
                return;
            }
            
            if(VRCDroneApi.TryGetDroneFromGameObject(other.gameObject, out var drone))
            {
                RunEvent(
                    "_onDroneTriggerStay", 
                    ("drone", drone)
                );
                return;
            }
            
            // Try to process the event using registered consumers (e.g., VRCPropApi)
            if (UdonManager.Instance.TryNotifyOnTriggerStayConsumers(this, other))
            {
                return;
            }
            
            if (!UdonManager.Instance.IsBlacklisted(other))
            {
                RunEvent("_onTriggerStay", ("other", other));
            }
        }

        public void OnTriggerStay2D(Collider2D other)
        {
            if (!UdonManager.Instance.IsBlacklisted(other))
            {
                RunEvent("_onTriggerStay2D", ("other", other));
            }
        }

        public void OnControllerColliderHit(ControllerColliderHit hit)
        {
            if(hit.gameObject == null) return;

            var tempPlayer = VRCPlayerApi.GetPlayerByGameObject(hit.gameObject);
            if(Utilities.IsValid(tempPlayer))
            {
                ControllerColliderPlayerHit playerHit = new ControllerColliderPlayerHit()
                {
                    player = tempPlayer,
                    moveDirection = hit.moveDirection,
                    moveLength = hit.moveLength,
                    normal = hit.normal,
                    point = hit.point,
                };

                RunEvent("_onControllerColliderHitPlayer", ("hit", playerHit));
            }
            else if(!UdonManager.Instance.IsBlacklisted(hit.gameObject))
            {
                RunEvent("_onControllerColliderHit", ("hit", hit));
            }
        }

        public void OnValidate()
        {
            RunEvent("_onValidate");
        }

        internal void ProxyOnWillRenderObject()
        {
            RunEvent("_onWillRenderObject");
        }

        #endregion

        #region VRCSDK Events

#if VRC_CLIENT
        [PublicAPI]
        public void OnNetworkReady()
        {
            _isReady = true;
        }
#endif

        //Called through Interactable interface
        public override void Interact()
        {
            RunEvent("_interact");
        }

        public override void OnDrop()
        {
            RunEvent("_onDrop");
        }

        public override void OnPickup()
        {
            RunEvent("_onPickup");
        }

        public override void OnPickupUseDown()
        {
            RunEvent("_onPickupUseDown");
        }

        public override void OnPickupUseUp()
        {
            RunEvent("_onPickupUseUp");
        }

        //Called via delegate by UdonSync
        [PublicAPI]
        public void OnPreSerialization()
        {
            if(_syncMethod == SyncType.None)
            {
                return;
            }

            if (!RunEvent(UdonManager.UDON_EVENT_ONPRESERIALIZATION)
                && !_hasError
                && _eventTable.ContainsKey(UdonManager.UDON_EVENT_ONPRESERIALIZATION))
                Logger.LogErrorFormat("OnPreSerialization event failed for {0}", gameObject.name);
        }

        //Called via delegate by UdonSync
        [PublicAPI]
        public void OnPostSerialization(SerializationResult result)
        {
            if(_syncMethod == SyncType.None)
            {
                return;
            }

            if (!RunEvent(UdonManager.UDON_EVENT_ONPOSTSERIALIZATION, ("result", result))
                && !_hasError
                && _eventTable.ContainsKey(UdonManager.UDON_EVENT_ONPOSTSERIALIZATION))
                Logger.LogErrorFormat("OnPostSerialization event failed for {0}", gameObject.name);
        }

        //Called via delegate by UdonSync
        [PublicAPI]
        public void OnDeserialization(DeserializationResult result)
        {
            if(_syncMethod == SyncType.None)
            {
                return;
            }

            if (!RunEvent(UdonManager.UDON_EVENT_ONDESERIALIZATION, ("result", result))
                && !_hasError
                && _eventTable.ContainsKey(UdonManager.UDON_EVENT_ONDESERIALIZATION))
                Logger.LogErrorFormat("OnDeserialization event failed for {0}", gameObject.name);
        }

        #endregion

        #region RunProgram Methods

        [PublicAPI]
        public override void RunProgram(string eventName)
        {
            if (_program == null)
            {
                return;
            }

            if(!_program.EntryPoints.HasExportedSymbol(eventName))
            {
                return;
            }

            uint address = _program.EntryPoints.GetAddressFromSymbol(eventName);
            RunProgram(address);
        }

        private void RunProgram(uint entryPoint)
        {
            if (_hasError)
            {
                return;
            }

            if (_udonVM == null)
            {
                return;
            }

            uint originalAddress = _udonVM.GetProgramCounter();
            UdonBehaviour originalExecuting = _udonManager.currentlyExecuting;

            _udonVM.SetProgramCounter(entryPoint);
            _udonManager.currentlyExecuting = this;

            _udonVM.DebugLogging = _udonManager.DebugLogging;

            try
            {
                _udonManager.IncrementDepthCount();
                uint result = _udonVM.Interpret();
                if (result != 0)
                {
                    Logger.LogError(
                        $"Udon VM execution errored, this UdonBehaviour will be halted.",
                        _categoryName,
                        this);

                    _hasError = true;
                    enabled = false;
                }
            }
            catch (UdonVMException error)
            {
                Logger.LogError(
                    "An exception occurred during Udon execution, this UdonBehaviour will be halted.\n" + error,
                    _categoryName,
                    this);

                _hasError = true;
                enabled = false;
            }
            finally
            {
                _udonManager.DecrementDepthCount();
            }

            // pi: note that _udonVM can be null here if the UdonBehaviour destroys itself during the Interpret call
            _udonManager.currentlyExecuting = originalExecuting;
            if (originalAddress < 0xFFFFFFFC && _udonVM != null)
            {
                _udonVM.SetProgramCounter(originalAddress);
            }
        }

        [PublicAPI]
        public ImmutableArray<string> GetPrograms()
        {
            return _program?.EntryPoints.GetExportedSymbols() ?? ImmutableArray<string>.Empty;
        }

        #endregion

        #region Serialization

        [SerializeField]
        private string serializedPublicVariablesBytesString;

        [SerializeField]
        private List<Object> publicVariablesUnityEngineObjects;

        [SerializeField]
        private DataFormat publicVariablesSerializationDataFormat = DataFormat.Binary;

        void ISerializationCallbackReceiver.OnAfterDeserialize()
        {
            DeserializePublicVariables();
        }

        private void DeserializePublicVariables()
        {
            byte[] serializedPublicVariablesBytes =
                Convert.FromBase64String(serializedPublicVariablesBytesString ?? "");

            publicVariables = SerializationUtility.DeserializeValue<IUdonVariableTable>(
                serializedPublicVariablesBytes,
                publicVariablesSerializationDataFormat,
                publicVariablesUnityEngineObjects
            ) ?? new UdonVariableTable();

            // Validate that the type of the value can actually be cast to the declaredType to avoid InvalidCastExceptions later.
            foreach (string publicVariableSymbol in publicVariables.VariableSymbols.ToArray())
            {
                if (!publicVariables.TryGetVariableValue(publicVariableSymbol, out object value))
                {
                    continue;
                }

                if (value == null)
                {
                    continue;
                }

                if (!publicVariables.TryGetVariableType(publicVariableSymbol, out Type declaredType))
                {
                    continue;
                }

                if (declaredType.IsInstanceOfType(value))
                {
                    continue;
                }

                if (declaredType.IsValueType)
                {
                    publicVariables.TrySetVariableValue(publicVariableSymbol, Activator.CreateInstance(declaredType));
                }
                else
                {
                    publicVariables.TrySetVariableValue(publicVariableSymbol, null);
                }
            }
        }

        void ISerializationCallbackReceiver.OnBeforeSerialize()
        {
            SerializePublicVariables();
        }

        private void SerializePublicVariables()
        {
            byte[] serializedPublicVariablesBytes = SerializationUtility.SerializeValue(
                publicVariables,
                publicVariablesSerializationDataFormat,
                out publicVariablesUnityEngineObjects);

            serializedPublicVariablesBytesString = Convert.ToBase64String(serializedPublicVariablesBytes);
        }

        #endregion

        #region IUdonBehaviour Interface

        [PublicAPI]
        public bool TryToInterrogateUdon<T0, T1>(string eventName, out object returnValue, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1)
        {
            return TryToInterrogateUdon<object, T0, T1>(eventName, out returnValue, parameter0, parameter1);
        }

        private bool TryToInterrogateUdon<TOut, T0, T1>(string eventName, out TOut returnValue, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1)
        {
            if (!_initialized || !enabled || !_hasDoneStart)
            {
#if VRC_CLIENT || UNITY_EDITOR
                if (UdonManager.Instance.DebugLogging)
                {
                    Logger.Log($"{gameObject.name} not ready to respond to {eventName}: initialized={_initialized} enabled={enabled} hasStarted={_hasDoneStart}", _categoryName);
                }
#endif

                returnValue = default;
                return false;
            }
            
            if (!_eventTable.ContainsKey(eventName))
            {
#if VRC_CLIENT || UNITY_EDITOR
                if (UdonManager.Instance.DebugLogging)
                {
                    Logger.Log($"{gameObject.name} will not respond to {eventName}", _categoryName);
                }
#endif

                returnValue = default;
                return false;
            }

            if(!RunEvent(eventName, parameter0, parameter1))
            {
#if VRC_CLIENT || UNITY_EDITOR
                if (UdonManager.Instance.DebugLogging)
                {
                    Logger.LogError($"{gameObject.name} failed to respond to {eventName}", _categoryName);
                }
#endif

                returnValue = default;
                return false;
            }

            returnValue = GetProgramVariable<TOut>(ReturnVariableName);
            return true;
        }

        public override bool RunEvent(string eventName) => RunEventAdvanced(eventName, canRunBeforeStart: false);
        public override bool RunEventAdvanced(string eventName, bool canRunBeforeStart)
        {
            if(DisableEventProcessing)
            {
                return false;
            }

            if(!_isReady)
            {
                return false;
            }

            if(!_hasDoneStart && !canRunBeforeStart)
            {
                return false;
            }

            if(_hasError)
            {
                return false;
            }

            if(_udonVM == null)
            {
                return false;
            }

            if(!_eventTable.TryGetValue(eventName, out List<uint> entryPoints))
            {
                return false;
            }

            foreach(uint entryPoint in entryPoints)
            {
                RunProgram(entryPoint);
            }

            return true;
        }

        #region RunEvent Parameter Overloads

        public override bool RunEvent<T0>(string eventName, (string symbolName, T0 value) parameter0) => RunEventAdvanced<T0>(eventName, true, false, parameter0);
        public override bool RunEventAdvanced<T0>(string eventName, bool mangleParameterNames, bool canRunBeforeStart, (string symbolName, T0 value) parameter0)
        {
            if(DisableEventProcessing)
            {
                return false;
            }

            if(!_isReady)
            {
                return false;
            }

            if(!_hasDoneStart && !canRunBeforeStart)
            {
                return false;
            }

            if(_hasError)
            {
                return false;
            }

            if(_udonVM == null)
            {
                return false;
            }

            if(!_eventTable.TryGetValue(eventName, out List<uint> entryPoints))
            {
                return false;
            }

            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter0.symbolName) : parameter0.symbolName, parameter0.value);
            foreach(uint entryPoint in entryPoints)
            {
                RunProgram(entryPoint);
            }

            return true;
        }

        public override bool RunEvent<T0, T1>(string eventName, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1) => RunEventAdvanced<T0, T1>(eventName, true, false, parameter0, parameter1);
        public override bool RunEventAdvanced<T0, T1>(string eventName, bool mangleParameterNames, bool canRunBeforeStart, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1)
        {
            if(DisableEventProcessing)
            {
                return false;
            }

            if(!_isReady)
            {
                return false;
            }

            if(!_hasDoneStart && !canRunBeforeStart)
            {
                return false;
            }

            if(_hasError)
            {
                return false;
            }

            if(_udonVM == null)
            {
                return false;
            }

            if(!_eventTable.TryGetValue(eventName, out List<uint> entryPoints))
            {
                return false;
            }

            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter0.symbolName) : parameter0.symbolName, parameter0.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter1.symbolName) : parameter1.symbolName, parameter1.value);
            foreach(uint entryPoint in entryPoints)
            {
                RunProgram(entryPoint);
            }

            return true;
        }

        public override bool RunEvent<T0, T1, T2>(string eventName, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1,
            (string symbolName, T2 value) parameter2) => RunEventAdvanced<T0, T1, T2>(eventName, true, false, parameter0, parameter1, parameter2);
        public override bool RunEventAdvanced<T0, T1, T2>(string eventName, bool mangleParameterNames, bool canRunBeforeStart, (string symbolName, T0 value) parameter0,
            (string symbolName, T1 value) parameter1, (string symbolName, T2 value) parameter2)
        {
            if(DisableEventProcessing)
            {
                return false;
            }

            if(!_isReady)
            {
                return false;
            }

            if(!_hasDoneStart && !canRunBeforeStart)
            {
                return false;
            }

            if(_hasError)
            {
                return false;
            }

            if(_udonVM == null)
            {
                return false;
            }

            if(!_eventTable.TryGetValue(eventName, out List<uint> entryPoints))
            {
                return false;
            }

            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter0.symbolName) : parameter0.symbolName, parameter0.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter1.symbolName) : parameter1.symbolName, parameter1.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter2.symbolName) : parameter2.symbolName, parameter2.value);
            foreach(uint entryPoint in entryPoints)
            {
                RunProgram(entryPoint);
            }

            return true;
        }

        public override bool RunEvent<T0, T1, T2, T3>(string eventName, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1,
            (string symbolName, T2 value) parameter2, (string symbolName, T3 value) parameter3) => RunEventAdvanced<T0, T1, T2, T3>(eventName, true, false, parameter0, parameter1, parameter2, parameter3);
        public override bool RunEventAdvanced<T0, T1, T2, T3>(string eventName, bool mangleParameterNames, bool canRunBeforeStart, (string symbolName, T0 value) parameter0,
            (string symbolName, T1 value) parameter1, (string symbolName, T2 value) parameter2, (string symbolName, T3 value) parameter3)
        {
            if(DisableEventProcessing)
            {
                return false;
            }

            if(!_isReady)
            {
                return false;
            }

            if(!_hasDoneStart && !canRunBeforeStart)
            {
                return false;
            }

            if(_hasError)
            {
                return false;
            }

            if(_udonVM == null)
            {
                return false;
            }

            if(!_eventTable.TryGetValue(eventName, out List<uint> entryPoints))
            {
                return false;
            }

            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter0.symbolName) : parameter0.symbolName, parameter0.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter1.symbolName) : parameter1.symbolName, parameter1.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter2.symbolName) : parameter2.symbolName, parameter2.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter3.symbolName) : parameter3.symbolName, parameter3.value);
            foreach(uint entryPoint in entryPoints)
            {
                RunProgram(entryPoint);
            }

            return true;
        }

        public override bool RunEvent<T0, T1, T2, T3, T4>(string eventName, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1,
            (string symbolName, T2 value) parameter2, (string symbolName, T3 value) parameter3, (string symbolName, T4 value) parameter4) => RunEventAdvanced<T0, T1, T2, T3, T4>(eventName, true, false, parameter0, parameter1, parameter2, parameter3, parameter4);
        public override bool RunEventAdvanced<T0, T1, T2, T3, T4>(string eventName, bool mangleParameterNames, bool canRunBeforeStart, (string symbolName, T0 value) parameter0,
            (string symbolName, T1 value) parameter1, (string symbolName, T2 value) parameter2, (string symbolName, T3 value) parameter3, (string symbolName, T4 value) parameter4)
        {
            if(DisableEventProcessing)
            {
                return false;
            }

            if(!_isReady)
            {
                return false;
            }

            if(!_hasDoneStart && !canRunBeforeStart)
            {
                return false;
            }

            if(_hasError)
            {
                return false;
            }

            if(_udonVM == null)
            {
                return false;
            }

            if(!_eventTable.TryGetValue(eventName, out List<uint> entryPoints))
            {
                return false;
            }

            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter0.symbolName) : parameter0.symbolName, parameter0.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter1.symbolName) : parameter1.symbolName, parameter1.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter2.symbolName) : parameter2.symbolName, parameter2.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter3.symbolName) : parameter3.symbolName, parameter3.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter4.symbolName) : parameter4.symbolName, parameter4.value);
            foreach(uint entryPoint in entryPoints)
            {
                RunProgram(entryPoint);
            }

            return true;
        }

        public override bool RunEvent<T0, T1, T2, T3, T4, T5>(string eventName, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1,
            (string symbolName, T2 value) parameter2, (string symbolName, T3 value) parameter3, (string symbolName, T4 value) parameter4, (string symbolName, T5 value) parameter5) 
            => RunEventAdvanced<T0, T1, T2, T3, T4, T5>(eventName, true, false, parameter0, parameter1, parameter2, parameter3, parameter4, parameter5);
        public override bool RunEventAdvanced<T0, T1, T2, T3, T4, T5>(string eventName, bool mangleParameterNames, bool canRunBeforeStart, 
            (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1, (string symbolName, T2 value) parameter2, 
            (string symbolName, T3 value) parameter3, (string symbolName, T4 value) parameter4, (string symbolName, T5 value) parameter5)
        {
            if(DisableEventProcessing)
            {
                return false;
            }

            if(!_isReady)
            {
                return false;
            }

            if(!_hasDoneStart && !canRunBeforeStart)
            {
                return false;
            }

            if(_hasError)
            {
                return false;
            }

            if(_udonVM == null)
            {
                return false;
            }

            if(!_eventTable.TryGetValue(eventName, out List<uint> entryPoints))
            {
                return false;
            }

            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter0.symbolName) : parameter0.symbolName, parameter0.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter1.symbolName) : parameter1.symbolName, parameter1.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter2.symbolName) : parameter2.symbolName, parameter2.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter3.symbolName) : parameter3.symbolName, parameter3.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter4.symbolName) : parameter4.symbolName, parameter4.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter5.symbolName) : parameter5.symbolName, parameter5.value);
            foreach(uint entryPoint in entryPoints)
            {
                RunProgram(entryPoint);
            }

            return true;
        }

        public override bool RunEvent<T0, T1, T2, T3, T4, T5, T6>(string eventName, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1,
            (string symbolName, T2 value) parameter2, (string symbolName, T3 value) parameter3, (string symbolName, T4 value) parameter4, 
            (string symbolName, T5 value) parameter5, (string symbolName, T6 value) parameter6) 
            => RunEventAdvanced<T0, T1, T2, T3, T4, T5, T6>(eventName, true, false, parameter0, parameter1, parameter2, parameter3, parameter4, parameter5, parameter6);
        public override bool RunEventAdvanced<T0, T1, T2, T3, T4, T5, T6>(string eventName, bool mangleParameterNames, bool canRunBeforeStart, 
            (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1, (string symbolName, T2 value) parameter2, 
            (string symbolName, T3 value) parameter3, (string symbolName, T4 value) parameter4, (string symbolName, T5 value) parameter5, 
            (string symbolName, T6 value) parameter6)
        {
            if(DisableEventProcessing)
            {
                return false;
            }

            if(!_isReady)
            {
                return false;
            }

            if(!_hasDoneStart && !canRunBeforeStart)
            {
                return false;
            }

            if(_hasError)
            {
                return false;
            }

            if(_udonVM == null)
            {
                return false;
            }

            if(!_eventTable.TryGetValue(eventName, out List<uint> entryPoints))
            {
                return false;
            }

            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter0.symbolName) : parameter0.symbolName, parameter0.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter1.symbolName) : parameter1.symbolName, parameter1.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter2.symbolName) : parameter2.symbolName, parameter2.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter3.symbolName) : parameter3.symbolName, parameter3.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter4.symbolName) : parameter4.symbolName, parameter4.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter5.symbolName) : parameter5.symbolName, parameter5.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter6.symbolName) : parameter6.symbolName, parameter6.value);
            foreach(uint entryPoint in entryPoints)
            {
                RunProgram(entryPoint);
            }

            return true;
        }

        public override bool RunEvent<T0, T1, T2, T3, T4, T5, T6, T7>(string eventName, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1,
            (string symbolName, T2 value) parameter2, (string symbolName, T3 value) parameter3, (string symbolName, T4 value) parameter4, 
            (string symbolName, T5 value) parameter5, (string symbolName, T6 value) parameter6, (string symbolName, T7 value) parameter7) 
            => RunEventAdvanced<T0, T1, T2, T3, T4, T5, T6, T7>(eventName, true, false, parameter0, parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7);
        public override bool RunEventAdvanced<T0, T1, T2, T3, T4, T5, T6, T7>(string eventName, bool mangleParameterNames, bool canRunBeforeStart, 
            (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1, (string symbolName, T2 value) parameter2, 
            (string symbolName, T3 value) parameter3, (string symbolName, T4 value) parameter4, (string symbolName, T5 value) parameter5, 
            (string symbolName, T6 value) parameter6, (string symbolName, T7 value) parameter7)
        {
            if(DisableEventProcessing)
            {
                return false;
            }

            if(!_isReady)
            {
                return false;
            }

            if(!_hasDoneStart && !canRunBeforeStart)
            {
                return false;
            }

            if(_hasError)
            {
                return false;
            }

            if(_udonVM == null)
            {
                return false;
            }

            if(!_eventTable.TryGetValue(eventName, out List<uint> entryPoints))
            {
                return false;
            }

            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter0.symbolName) : parameter0.symbolName, parameter0.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter1.symbolName) : parameter1.symbolName, parameter1.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter2.symbolName) : parameter2.symbolName, parameter2.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter3.symbolName) : parameter3.symbolName, parameter3.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter4.symbolName) : parameter4.symbolName, parameter4.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter5.symbolName) : parameter5.symbolName, parameter5.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter6.symbolName) : parameter6.symbolName, parameter6.value);
            SetProgramVariable(mangleParameterNames ? GetEventParameterName(eventName, parameter7.symbolName) : parameter7.symbolName, parameter7.value);
            foreach(uint entryPoint in entryPoints)
            {
                RunProgram(entryPoint);
            }

            return true;
        }

        public override bool RunEvent(string eventName, params (string symbolName, object value)[] programVariables) => RunEventAdvanced(eventName, true, false, programVariables);
        public override bool RunEventAdvanced(string eventName, bool mangleParameterNames, bool canRunBeforeStart, params (string symbolName, object value)[] programVariables)
        {
            if(DisableEventProcessing)
            {
                return false;
            }

            if(!_isReady)
            {
                return false;
            }

            if (!_hasDoneStart && !canRunBeforeStart)
            {
                return false;
            }

            if (_hasError)
            {
                return false;
            }

            if (_udonVM == null)
            {
                return false;
            }

            if (!_eventTable.TryGetValue(eventName, out List<uint> entryPoints))
            {
                return false;
            }

            //TODO: Replace with a non-boxing interface before exposing to users
            foreach ((string symbolName, object value) in programVariables)
            {
                var newSymbolName = mangleParameterNames ? GetEventParameterName(eventName, symbolName) : symbolName;
                SetProgramVariable(newSymbolName, value);
            }

            foreach (uint entryPoint in entryPoints)
            {
                RunProgram(entryPoint);
            }

            return true;
        }

        #endregion

        public override void RunInputEvent(string eventName, UdonInputEventArgs args)
        {
            if (!_isReady)
            {
                return;
            }

            if (!_hasDoneStart)
            {
                return;
            }

            if(!_program.EntryPoints.HasExportedSymbol(eventName))
            {
                return;
            }

            // Set value arg
            switch (args.eventType)
            {
                case UdonInputEventType.AXIS:
                    SetProgramVariable(GetEventParameterName(eventName, "floatValue"), args.floatValue);
                    break;
                case UdonInputEventType.BUTTON:
                    SetProgramVariable(GetEventParameterName(eventName, "boolValue"), args.boolValue);
                    break;
            }

            // Set event args
            SetProgramVariable(GetEventParameterName(eventName, "args"), args);
            RunProgram(eventName);
        }

        private string GetEventParameterName(string eventName, string symbolName)
        {
            if (!_symbolNameCache.TryGetValue((eventName, symbolName), out string newSymbolName))
            {
                newSymbolName = $"{eventName.Substring(1)}{char.ToUpper(symbolName.First())}{symbolName.Substring(1)}";
                _symbolNameCache.Add((eventName, symbolName), newSymbolName);
            }

            return newSymbolName;
        }

        private ProfilerMarker _preloadUdonProgramProfilerMarker = new ProfilerMarker("UdonBehaviour.PreloadUdonProgram");

        public void PreloadUdonProgram(IUdonSignatureVerifier signatureVerifier)
        {
            using(_preloadUdonProgramProfilerMarker.Auto())
            {
                if(serializedProgramAsset == null)
                {
                    return;
                }

                if(_program != null)
                {
                    return;
                }

                signatureVerifier.VerifySignature(serializedProgramAsset as IUdonSignatureHolder);
                _program = serializedProgramAsset.RetrieveProgram();
            }
        }

        private ProfilerMarker _initializeUdonContentProfilerMarker = new ProfilerMarker("UdonBehaviour.InitializeUdonContent");

        private T SearchUdonInterface<T>() where T : class
        {
            // while a scene is loading we do not allow searching the hierarchy
            if (_udonManager.IsSceneLoading)
                return _udonManager as T;
            var foundInParent = GetComponentInParent<T>();
            if (foundInParent == null || (foundInParent as Component) == null)
                return _udonManager as T;
            return foundInParent;
        }

        public override void InitializeUdonContent()
        {
            using(_initializeUdonContentProfilerMarker.Auto())
            {
                if(_initialized)
                {
                    return;
                }

                SetupLogging();

                _udonManager = UdonManager.Instance;
                if(_udonManager == null)
                {
                    enabled = false;
                    Logger.LogError(
                        $"Could not find the UdonManager; the UdonBehaviour on '{gameObject.name}' will not run.",
                        _categoryName,
                        this);

                    return;
                }

                IUdonSignatureVerifier signatureVerifier = SearchUdonInterface<IUdonSignatureVerifier>();
                if(!LoadProgram(signatureVerifier))
                {
                    enabled = false;
                    Logger.Log(
                        $"Could not load the program; the UdonBehaviour on '{gameObject.name}' will not run.",
                        _categoryName,
                        this);

                    return;
                }

                // Let UdonManager apply any processing or scans.
                _udonManager.ProcessUdonProgram(_program);

                IUdonSymbolTable symbolTable = _program?.SymbolTable;
                IUdonHeap heap = _program?.Heap;
                if(symbolTable == null || heap == null)
                {
                    enabled = false;
                    Logger.Log(
                        $"Invalid program; the UdonBehaviour on '{gameObject.name}' will not run.",
                        _categoryName,
                        this);

                    return;
                }

                if(!ResolveUdonHeapReferences(symbolTable, heap))
                {
                    enabled = false;
                    Logger.Log(
                        $"Failed to resolve a GameObject/Component Reference; the UdonBehaviour on '{gameObject.name}' will not run.",
                        _categoryName,
                        this);

                    return;
                }

                IUdonClientInterface clientInterface = SearchUdonInterface<IUdonClientInterface>();
                _udonVM = clientInterface.ConstructUdonVM();

                if(_udonVM == null)
                {
                    enabled = false;
                    Logger.LogError(
                        $"No UdonVM; the UdonBehaviour on '{gameObject.name}' will not run.",
                        _categoryName,
                        this);

                    return;
                }

                _udonVM.LoadProgram(_program);

                ProcessEntryPoints();

                #if !VRC_CLIENT
                _isReady = true;
                #else
                if(!_isNetworkingSupported)
                {
                    _isReady = true;
                }
                #endif

                _initialized = true;

                RunOnInit();
            }
        }

        [PublicAPI]
        public void RunOnInit()
        {
            if (OnInit == null)
            {
                return;
            }

            try
            {
                OnInit(this, _program);
            }
            catch (Exception exception)
            {
                enabled = false;
                Logger.LogError(
                    $"An exception '{exception.Message}' occurred during initialization; the UdonBehaviour on '{gameObject.name}' will not run. Exception:\n{exception}",
                    _categoryName,
                    this
                );
            }
        }

        private void RegisterUpdate()
        {
            if (_udonManager == null)
            {
                return;
            }

            if (!isActiveAndEnabled)
            {
                return;
            }

            if (_hasUpdateEvent || !_hasDoneStart)
            {
                _udonManager.RegisterUdonBehaviourUpdate(this);
            }

            if (_hasLateUpdateEvent)
            {
                _udonManager.RegisterUdonBehaviourLateUpdate(this);
            }

            if (_hasFixedUpdateEvent)
            {
                _udonManager.RegisterUdonBehaviourFixedUpdate(this);
            }

            if (_hasPostLateUpdateEvent)
            {
                _udonManager.RegisterUdonBehaviourPostLateUpdate(this);
            }
            
            foreach (AbstractUdonBehaviourEventProxy proxy in _eventProxies)
            {
                proxy.enabled = true;
            }
        }

        private void UnregisterUpdate()
        {
            if (_udonManager == null)
            {
                return;
            }

            if (_hasUpdateEvent)
            {
                _udonManager.UnregisterUdonBehaviourUpdate(this);
            }

            if (_hasLateUpdateEvent)
            {
                _udonManager.UnregisterUdonBehaviourLateUpdate(this);
            }

            if (_hasFixedUpdateEvent)
            {
                _udonManager.UnregisterUdonBehaviourFixedUpdate(this);
            }

            if (_hasPostLateUpdateEvent)
            {
                _udonManager.UnregisterUdonBehaviourPostLateUpdate(this);
            }
            
            foreach (AbstractUdonBehaviourEventProxy proxy in _eventProxies)
            {
                proxy.enabled = false;
            }
        }

        #region IUdonEventReceiver and IUdonSyncTarget Interface

        #region IUdonEventReceiver Only

        public override void SendCustomEvent(string eventName)
        {
            RunProgram(eventName);
        }

        public override void SendCustomNetworkEvent(NetworkEventTarget target, string eventName)
            => NetworkCalling.SendCustomNetworkEvent(this, target, eventName);
        public override void SendCustomNetworkEvent(NetworkEventTarget target, string eventName, object parameter0)
            => NetworkCalling.SendCustomNetworkEvent(this, target, eventName, parameter0);
        public override void SendCustomNetworkEvent(NetworkEventTarget target, string eventName, object parameter0, object parameter1)
            => NetworkCalling.SendCustomNetworkEvent(this, target, eventName, parameter0, parameter1);
        public override void SendCustomNetworkEvent(NetworkEventTarget target, string eventName, object parameter0, object parameter1, object parameter2)
            => NetworkCalling.SendCustomNetworkEvent(this, target, eventName, parameter0, parameter1, parameter2);
        public override void SendCustomNetworkEvent(NetworkEventTarget target, string eventName, object parameter0, object parameter1, object parameter2, object parameter3)
            => NetworkCalling.SendCustomNetworkEvent(this, target, eventName, parameter0, parameter1, parameter2, parameter3);
        public override void SendCustomNetworkEvent(NetworkEventTarget target, string eventName, object parameter0, object parameter1, object parameter2, object parameter3, object parameter4)
            => NetworkCalling.SendCustomNetworkEvent(this, target, eventName, parameter0, parameter1, parameter2, parameter3, parameter4);
        public override void SendCustomNetworkEvent(NetworkEventTarget target, string eventName, object parameter0, object parameter1, object parameter2, object parameter3, object parameter4, object parameter5)
            => NetworkCalling.SendCustomNetworkEvent(this, target, eventName, parameter0, parameter1, parameter2, parameter3, parameter4, parameter5);
        public override void SendCustomNetworkEvent(NetworkEventTarget target, string eventName, object parameter0, object parameter1, object parameter2, object parameter3, object parameter4, object parameter5, object parameter6)
            => NetworkCalling.SendCustomNetworkEvent(this, target, eventName, parameter0, parameter1, parameter2, parameter3, parameter4, parameter5, parameter6);
        public override void SendCustomNetworkEvent(NetworkEventTarget target, string eventName, object parameter0, object parameter1, object parameter2, object parameter3, object parameter4, object parameter5, object parameter6, object parameter7)
            => NetworkCalling.SendCustomNetworkEvent(this, target, eventName, parameter0, parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7);

        public override void RequestSerialization()
        {
            RequestSerializationHook?.Invoke(this);
        }
        
        public override void SendCustomEventDelayedSeconds(string eventName, float delaySeconds, EventTiming eventTiming = EventTiming.Update)
        {
            UdonManager.Instance.ScheduleDelayedEvent(this, eventName, delaySeconds, eventTiming);
        }

        public override void SendCustomEventDelayedFrames(string eventName, int delayFrames, EventTiming eventTiming = EventTiming.Update)
        {
            UdonManager.Instance.ScheduleDelayedEvent(this, eventName, delayFrames, eventTiming);
        }

        public override string InteractionText
        {
            get => interactText;
            set => interactText = value;
        }

        #endregion

        #region IUdonSyncTarget

        public override IUdonSyncMetadataTable SyncMetadataTable => _program?.SyncMetadataTable;

        #endregion

        #region Shared

        public override Type GetProgramVariableType(string symbolName)
        {
            if (!_program.SymbolTable.HasAddressForSymbol(symbolName))
            {
                return null;
            }

            uint symbolAddress = _program.SymbolTable.GetAddressFromSymbol(symbolName);
            return _program.Heap.GetHeapVariableType(symbolAddress);
        }

        public override void SetProgramVariable<T>(string symbolName, T value)
        {
            if (_program == null)
            {
                return;
            }

            if (!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress))
            {
                return;
            }

            SetHeapVariable(symbolAddress, value);
        }

        public override void SetProgramVariable(string symbolName, object value)
        {
            if (_program == null)
            {
                return;
            }

            if (!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress))
            {
                return;
            }

            SetHeapVariable( symbolAddress, value);
        }

        private void SetHeapVariable<T>(uint symbolAddress, T newValue)
        {
            if (_variableToChangeEvent.TryGetValue(symbolAddress, out (uint eventAddress, uint oldVariableAddress) data))
            {
                // cache value before changing
                T value = _program.Heap.GetHeapVariable<T>(symbolAddress);

                // check for change and trigger event
                if(!value?.Equals(newValue) ?? newValue != null)
                {
                    // change the variable on the heap
                    _program.Heap.SetHeapVariable(symbolAddress, newValue);
                    
                    // change the old variable on the heap
                    if (data.oldVariableAddress != uint.MaxValue)
                    {
                        _program.Heap.SetHeapVariable(data.oldVariableAddress, value);
                    }

                    // trigger the event
                    RunProgram(data.eventAddress);
                }
            }
            else
            {
                // just change the variable on the heap
                _program.Heap.SetHeapVariable(symbolAddress, newValue);
            }
        }

        public override T GetProgramVariable<T>(string symbolName)
        {
            if (_program == null)
            {
                return default;
            }

            if (!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress))
            {
                return default;
            }

            return _program.Heap.GetHeapVariable<T>(symbolAddress);
        }

        public override object GetProgramVariable(string symbolName)
        {
            if (_program == null)
            {
                return null;
            }

            if (!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress))
            {
#if UNITY_EDITOR
                Logger.LogError($"Could not find symbol {symbolName}; available: [{string.Join(",", _program.SymbolTable.GetSymbols())}]", _categoryName);
#endif
                return null;
            }

            return _program.Heap.GetHeapVariable(symbolAddress);
        }

        public override bool TryGetProgramVariable<T>(string symbolName, out T value)
        {
            value = default;
            if (_program == null)
            {
                return false;
            }

            if (!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress))
            {
                return false;
            }

            return _program.Heap.TryGetHeapVariable(symbolAddress, out value);
        }

        public override bool TryGetProgramVariable(string symbolName, out object value)
        {
            value = null;
            if (_program == null)
            {
                return false;
            }

            if (!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress))
            {
                return false;
            }

            return _program.Heap.TryGetHeapVariable(symbolAddress, out value);
        }

        #endregion

        #endregion

        #endregion

        #region Logging Methods

        private void SetupLogging()
        {
            _categoryName = "UdonBehaviour";
            if (Logger.CategoryIsDescribed(_categoryName))
            {
                return;
            }

            Logger.DescribeCategory(_categoryName);
            Logger.EnableCategory(_categoryName);
        }

        #endregion

        #region Manual Initialization Methods

        [PublicAPI]
        public void AssignProgramAndVariables(AbstractSerializedUdonProgramAsset compiledAsset,
            IUdonVariableTable variables)
        {
            serializedProgramAsset = compiledAsset;
            publicVariables = variables;
        }

        #endregion
    }
}
