
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon.Common;
using UdonSharp;
using VRC.Udon.Wrapper.Modules;

namespace Superbstingray
{
	[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
	public class UdonPlatformHook : UdonSharp.UdonSharpBehaviour
{
	[Tooltip("Layers that the Player will move with.")]
	public LayerMask hookLayerMask;

	[Tooltip("Vertical distance between the Player and Platform before the Script Unhooks from Colliders. You may want to increase this value if your world has a higher than average jump impulse.")]
	public float hookDistance = 0.75F;

	[Tooltip("Will make the Player keep their Velocity + the Platforms Velocity when they detach from it.")]
	public bool inheritVelocity = true;

	[Tooltip("Will Immobilize(true) players when standing still on moving platforms to prevent Avatars from having IK drift / IK walk.")]
	public bool reduceIKDrift = true;

	[Tooltip("Try Detect if the Player has their Main Menu open and stop moving them. Will help with menu interactions.")]
	public bool mainMenuPause = true;

	[Tooltip("Try Detect if the Player has their Quick Menu open and stop moving them. Will help with menu interactions.")]
	public bool quickMenuPause = false;

	BoxCollider platformOverride;
	Collider[] colliderArray;
	Collider sceneCollider;
	Quaternion headRotation;
	RaycastHit hitInfo;	
	Transform PlayerOffset, playerTracker, hook;
	Vector3 playerVelocity, hookLastPos, hookOffsetPos, lastFramePos;
	VRCPlayerApi localPlayer;

	bool menuOpen;
	float InputMoveH, InputMoveV;
	int unhookThreshold, localColliders, intUI;

	[FieldChangeCallback(nameof(isHookedCallback))]
	bool isHooked;
	bool isHookedCallback
	{	set
		{	isHooked = value;
			if (isHooked) // isHooked=true functions
			{
				hook.localPosition = Vector3.zero; hook.eulerAngles = Vector3.zero;
				platformOverride.enabled = true;	
				PlayerOffset.SetPositionAndRotation(hook.position, hook.rotation);

				// When hooking count the number of PlayerLocal colliders the player has
				// as this will help us know when the Player enters a station.
				localColliders = Mathf.Clamp(Physics.OverlapSphere((localPlayer.GetPosition()), 1024f, 1024).Length, 1, 100);
			}
			else // isHooked=false functions
			{
				if(inheritVelocity) { localPlayer.SetVelocity(playerVelocity); } // Sets the players velocity to their actual worldspace velocity when they Unhook.

				OverridesOff();
                hook.localPosition = Vector3.zero; hook.eulerAngles = Vector3.zero;
				PlayerOffset.SetPositionAndRotation(hook.position, hook.rotation);
			}	
		}
	}

        void Start()
        {
            InitializeVariables();
            IgnoreSceneCollision();
        }

        void FixedUpdate()
        {
            FixedUpdateFunctions();
        }

        void LateUpdate()
        {
            LateUpdateFunctions();
        }

        void FixedUpdateFunctions() 
		{	
			if (!VRC.SDKBase.Utilities.IsValid(localPlayer)) { return; }

			if (isHooked && inheritVelocity) // Average the last X frames of the players global velocity.
			{
				playerVelocity = Vector3.ClampMagnitude((playerVelocity * 3f + (localPlayer.GetPosition() - lastFramePos) / Time.deltaTime) / 4f, 100f);
				lastFramePos = localPlayer.GetPosition();
			}

			if (!menuOpen)
			{
				// Override_Spherecast. Set position of override collider.
				Physics.SphereCast(localPlayer.GetPosition() + new Vector3(0F, .3f, 0f), 0.25f, Vector3.down, out hitInfo, 10f, hookLayerMask.value);
				platformOverride.center = hitInfo.point;

				// FixedUpdate_Spherecast. Check for valid platforms.
				// Add to the unhookThreshold if it misses a valid platform and unhook if unhookThreshold is greater than X.
				if (!Physics.SphereCast(localPlayer.GetPosition() + new Vector3(0f, .3f, 0f), 0.25f, Vector3.down, out hitInfo, hookDistance + .3f, hookLayerMask.value))
				{
					unhookThreshold++;
					if (unhookThreshold > 10 && isHooked)
					{
						hook.parent = transform;
						SetProgramVariable(nameof(isHooked), false);
						SendCustomEventDelayedSeconds(nameof(OverridesOff), 0.5f);
					}
				}
			}
		}

		void LateUpdateFunctions() 
		{
			if (!VRC.SDKBase.Utilities.IsValid(localPlayer)) { return; }
#if !UNITY_EDITOR
            if (isHooked) // Count the number of InterntalUI colliders as a means to know if the menu is open or not.
			{
				intUI = Physics.OverlapSphereNonAlloc(localPlayer.GetPosition(), 10f, colliderArray, 524288);
				menuOpen = (mainMenuPause && (intUI >= 7 && intUI <= 14)) || (quickMenuPause && (intUI >= 15 && intUI <= 19));
			}
			if (isHooked && !menuOpen) // Move the parented hook to the Players position
			{
				hookLastPos = hook.position;
				hook.position = localPlayer.GetPosition();
				hookOffsetPos = hook.position - hookLastPos;
			}
			if (isHooked && menuOpen) { localPlayer.SetVelocity(Vector3.zero); } // Override Player Velocity to make it easier to use their menu.

			// Teleport the player to the new offset position only if the players PlayerLocal collider count
			// didn't decrease otherwise assume the player entered a station.
			if (isHooked && !menuOpen)
			{
				playerTracker.SetPositionAndRotation(localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Origin).position, localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Origin).rotation);
				PlayerOffset.SetPositionAndRotation(hook.position, hook.rotation);

				if (Physics.OverlapSphere(localPlayer.GetPosition(), 1024f, 1024).Length >= localColliders)
				{
					localPlayer.TeleportTo(playerTracker.position - hookOffsetPos, playerTracker.rotation, VRC_SceneDescriptor.SpawnOrientation.AlignRoomWithSpawnPoint, true);
				}
			}
#else
            // "EditorOnly" duplicate funtion from above specifically for CyanEmu/ClientSim as Origin tracking does not behave the same in editor as in client.
            if (isHooked)
			{
				playerTracker.SetPositionAndRotation(localPlayer.GetPosition(), localPlayer.GetRotation());
				PlayerOffset.SetPositionAndRotation(hook.position, hook.rotation);

				if (Physics.OverlapSphere(localPlayer.GetPosition(), 1024f, 1024).Length >= localColliders)
				{
					localPlayer.TeleportTo(playerTracker.position - hookOffsetPos, playerTracker.rotation, VRC_SceneDescriptor.SpawnOrientation.AlignPlayerWithSpawnPoint, true);
					hookOffsetPos = Vector3.zero;
				}
			}
#endif
			// Players in Desktop or 3 Point tracking will have their Inverse Kinematics drag behind and "IK Walk" while being moved when they aren't intentionally locomoting.
			// This function is to prevent that from occuring by Immobilizing the player when they are hooked to a platform and aren't moving relative to the platform.
			// This is scuffed and I should optimize it.
			if(reduceIKDrift && isHooked)
			{
				headRotation = localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).rotation;
				localPlayer.Immobilize(!(InputMoveH * 0.1f + InputMoveV != 0f)
				&& Mathf.Abs(playerVelocity.x) + Mathf.Abs(playerVelocity.z) > .1f
				&& Mathf.Abs(localPlayer.GetVelocity().x) + Mathf.Abs(localPlayer.GetVelocity().z) < .01f
				&& Quaternion.Angle(new Quaternion(0f, headRotation.y, 0f, headRotation.w).normalized, localPlayer.GetRotation()) < 90f);
			}
			// LateUpdate_Spherecast. Check for valid platforms.
			if (Physics.SphereCast(localPlayer.GetPosition() + new Vector3(0f, .3f, 0f), 0.25f, Vector3.down, out hitInfo, hookDistance + .3f, hookLayerMask.value))
			{
				unhookThreshold = 0;
				if (unhookThreshold < 10 && (localPlayer.IsPlayerGrounded())) // Hook to the valid platform if the Player is grounded.
				{
					hook.parent = hitInfo.transform;
					SetProgramVariable(nameof(isHooked), true);
				}
			}
		}

		// INPUT EVENTS: MoveVertical, MoveHorizontal, InputJump.
		public override void InputMoveVertical(float Value, UdonInputEventArgs InputMoveVerticalArgs)
		{
			InputMoveV = InputMoveVerticalArgs.floatValue;
		}
		public override void InputMoveHorizontal(float Value, UdonInputEventArgs InputMoveHorizontalArgs)
		{
			InputMoveH = InputMoveHorizontalArgs.floatValue;
		}
		public override void InputJump(bool outputJumpBool, UdonInputEventArgs inputJumpArgs)
		{
			if (reduceIKDrift) { localPlayer.Immobilize(false); }
		}

		// Reset prefab state and call unhook on Respawn.
		public override void OnPlayerRespawn(VRCPlayerApi onPlayerRespawnPlayer) 
		{
			hook.parent = transform;
			unhookThreshold = System.Int32.MaxValue;
			localPlayer.SetVelocity(Vector3.zero);
			SetProgramVariable(nameof(isHooked), false);
			OverridesOff();
		}

		// The prefab uses an additional overriding collider to prevent the VRC player controller from being affected by moving world
		// colliders which can affect locomotion and animations, and this is to prevent the override collider from interacting with other colliders in the scene.
		public void IgnoreSceneCollision()
		{
			SendCustomEventDelayedSeconds(nameof(IgnoreSceneCollision), 60f);
			colliderArray = Physics.OverlapSphere(Vector3.zero, System.Single.MaxValue);
			for (int i = 0; i < colliderArray.Length; i++)
			{
				sceneCollider = colliderArray[i];
				if (VRC.SDKBase.Utilities.IsValid(sceneCollider))
				{
					Physics.IgnoreCollision(sceneCollider, platformOverride);
				}
			}
		}

        void InitializeVariables() // Set Prefab Variables 
        {
            localPlayer = Networking.LocalPlayer;
            if (!VRC.SDKBase.Utilities.IsValid(localPlayer)) { return; }
            lastFramePos = localPlayer.GetPosition();
            playerTracker = transform.GetChild(0).GetChild(0);
            PlayerOffset = transform.GetChild(0);
            hook = transform.GetChild(1);
            platformOverride = transform.GetComponent<BoxCollider>();
            platformOverride.size = new Vector3(0.5f, 0.05f, 0.5f);
            transform.position = Vector3.zero;

            if (hookLayerMask.value == -1) // Override if using "Everything" as a layermask to Everything -PlayerLocal,-MirrorReflection to prevent interference with the prefabs functionality.
            {
                hookLayerMask.value = -263369;
            }
        }

        // Disable Override Collider and set Immobilize state false.
        void OverridesOff()
		{
			if (!localPlayer.IsPlayerGrounded()) { platformOverride.enabled = false; }

			if (reduceIKDrift) { localPlayer.Immobilize(false); }
		}
	}
}