Gun.cs
using Sandbox;
using Sandbox.Citizen;
using System;
using System.Diagnostics;
using System.Linq;
using System.Net.WebSockets;
using static Sandbox.Component;

public enum GunType
{
	Magazine,
	Shotgun,
	Bolt,
}

public sealed class Gun : Component, INetworkListener
{
	/// <summary>Magazine = timed full reload; Shotgun = shell-by-shell reload stance.</summary>
	[Property] public GunType Type { get; set; } = GunType.Magazine;

	/// <summary>Seconds between shots. Lower = faster fire rate.</summary>
	[Property] private float FireRate { get; set; } = 0.1f;

	[Property] private GameObject Muzzleflash { get; set; }
	[Property] private SoundPointComponent SoundPoint { get; set; }

	/// <summary>Seconds the world muzzle flash stays visible for other players.</summary>
	[Property] private float MuzzleFlashDuration { get; set; } = 0.05f;

	/// <summary>Prefab cloned for each pellet on every shot.</summary>
	[Property] public GameObject BulletPrefab { get; set; }

	/// <summary>Transform at the gun barrel; pellets spawn here in VR.</summary>
	[Property] public GameObject MuzzleTransform { get; set; }

	/// <summary>Projectile speed in world units per second.</summary>
	[Property] public float BulletSpeed { get; set; } = 3000f;

	/// <summary>Projectiles spawned per shot. 1 = single bullet (default).</summary>
	[Property] public int PelletCount { get; set; } = 1;

	/// <summary>Max random cone half-angle in degrees per pellet. 0 = no spread.</summary>
	[Property] public float SpreadAngle { get; set; } = 0f;

	/// <summary>Maximum bullets in the magazine; refilled on each reload.</summary>
	[Property] private int MagazineSize { get; set; } = 30;

	/// <summary>Seconds before firing is allowed again after a magazine reload.</summary>
	[Property, Group("Reload"), HideIf( nameof( Type ), GunType.Shotgun )] private float ReloadTime { get; set; } = 2f;
	[Property, Group( "Reload" ), HideIf( nameof( Type ), GunType.Shotgun )] private float MagOutTime { get; set; }
	[Property, Group( "Reload" ), HideIf( nameof( Type ), GunType.Shotgun )] private SoundEvent MagOutSound { get; set; }
	[Property, Group( "Reload" ), HideIf( nameof( Type ), GunType.Shotgun )] private float MagInTime { get; set; }
	[Property, Group( "Reload" ), HideIf( nameof( Type ), GunType.Shotgun )] private SoundEvent MagInSound { get; set; }

	/// <summary>Seconds between each shell insert during a shotgun reload.</summary>
	[Property, Group( "Reload" ), ShowIf(nameof(Type), GunType.Shotgun)] private float ShellInsertInterval { get; set; } = 0.6f;
	/// <summary>Seconds into each shell insert interval when the insert sound plays.</summary>
	[Property, Group( "Reload" ), ShowIf( nameof( Type ), GunType.Shotgun )] private float ShellInsertSoundTime { get; set; } = 0.4f;
	[Property, Group( "Reload" ), ShowIf( nameof( Type ), GunType.Shotgun )] private SoundEvent ShellInsertSound { get; set; }
	/// <summary> If true, the weapon will play the unloaded deploy/idle animation when the magazine is empty. </summary>
	[Property, Group( "Reload" )] private bool HasUnloadedAnimation { get; set; } = false;

	/// <summary>Seconds before firing is allowed after pickup (desktop draw animation). 0 disables.</summary>
	[Property] private float EquipTime { get; set; } = 1f;

	/// <summary>Per-shot aim/crosshair recoil pattern. Leave empty for no recoil.</summary>
	[Property, Group( "Recoil" )] public RecoilPattern Recoil { get; set; }

	/// <summary>Tick for weapons designed for two hands (e.g. rifles); firing one-handed in VR amplifies recoil.</summary>
	[Property, Group( "Recoil" )] public bool IsTwoHanded { get; set; } = false;

	/// <summary>Degrees of VR gun kick per normalized recoil unit. Higher = stronger visible kick.</summary>
	[Property, Group( "Recoil" )] private float VrRecoilScale { get; set; } = 60f;

	/// <summary>Recoil magnitude multiplier applied when a two-handed gun is held with only one hand in VR.</summary>
	[Property, Group( "Recoil" )] private float OneHandedRecoilMultiplier { get; set; } = 2f;

	/// <summary>Recoil magnitude multiplier when firing from the hip (not aiming down sights). 1 = same as ironsights.</summary>
	[Property, Group( "Recoil" )] private float HipFireRecoilMultiplier { get; set; } = 1.75f;

	/// <summary>Scales how long each shot's VR rumble lasts. 1 is the default HardImpact length.</summary>
	[Property, Group( "Haptics" )] private float HapticLengthScale { get; set; } = 1f;

	/// <summary>Scales VR rumble frequency. Higher feels sharper.</summary>
	[Property, Group( "Haptics" )] private float HapticFrequencyScale { get; set; } = 1f;

	/// <summary>Scales VR rumble intensity. 1 is full strength.</summary>
	[Property, Group( "Haptics" )] private float HapticAmplitudeScale { get; set; } = 1f;

	[Sync] private bool Firing { get; set; } = false;
	[Sync] private int FireSequence { get; set; }
	[Sync] private int CurrentBullets { get; set; }
	[Sync] private bool ShellReloadActive { get; set; }

	private bool IsReloading { get; set; }
	private bool IsEquipping { get; set; }
	private bool _lastShellReloadActive;
	private float _nextShellAmmoTime;
	private bool _shellInsertSoundPlayed;
	private bool _shellReloadCancelPending;
	private bool _fireWhenShellReloadEnds;

	// Scan from this weapon's own object, not GameObject.Root: a stowed weapon is
	// parented to the player, making Root resolve to the player and match other weapons.
	private bool IsHeld =>
		GameObject.Components.GetAll<GrabPoint>( FindMode.InDescendants ).Any( g => g.IsGrabbed );

	private int HeldGripCount =>
		GameObject.Components.GetAll<GrabPoint>( FindMode.InDescendants ).Count( g => g.IsGrabbed );

	private bool BlocksFireFromReload => Type != GunType.Shotgun && IsReloading;

	/// <summary>
	/// Current recoil expressed as a gun-local rotation kick for VR. Amplified when a
	/// two-handed weapon is gripped with only one hand.
	/// </summary>
	public Rotation VrRecoilRotation
	{
		get
		{
			float multiplier = (IsTwoHanded && HeldGripCount == 1) ? OneHandedRecoilMultiplier : 1f;
			return Rotation.From(
				-_currentRecoil.y * VrRecoilScale * multiplier,
				_currentRecoil.x * VrRecoilScale * multiplier,
				0f );
		}
	}

	/// <summary>
	/// Aim/viewmodel recoil after hip-fire scaling. Ironsights use the raw pattern; hip fire is amplified.
	/// </summary>
	private Vector2 EffectiveRecoil
	{
		get
		{
			bool inIronsights = !Game.IsRunningInVR && PlayerViewModel.Instance is { Ironsights: true };
			return _currentRecoil * (inIronsights ? 1f : HipFireRecoilMultiplier);
		}
	}

	private bool CanFire => Time.Now >= _nextFireTime && !BlocksFireFromReload && !IsEquipping && CurrentBullets > 0;
	private float _nextFireTime = 0f;
	private float _reloadEndTime;
	private float _equipEndTime;
	private bool _wasHeld;
	private int _recoilIndex;
	private Vector2 _currentRecoil;
	private float _lastFireTime = float.NegativeInfinity;
	private float _burstStartTime = float.NegativeInfinity;
	private int _lastProcessedFireSequence;
	private float _worldMuzzleHideTime;
	private ReloadBar _reloadBar;
	private float _reloadBoltTime;
	private bool _magOutPlayed;
	private bool _magInPlayed;

	protected override void OnStart()
	{
		if ( !IsProxy )
			CurrentBullets = MagazineSize;

		_lastShellReloadActive = ShellReloadActive;
	}

	protected override void OnUpdate()
	{
		UpdateShellReloadVisuals();

		if ( !IsProxy )
		{
			UpdateEquipState();
			UpdateReloadingState();

			UpdateRecoil();

			if ( !Game.IsRunningInVR )
			{
				if ( IsHeld )
				{
					UpdateViewModelAnimation();
				}
			}

			HandleFiring();

			if ( Game.IsRunningInVR )
			{
				UpdateProxyShotEffects();
			}
			else
			{
				HandleReloadBolt();
			}

			if (PlayerViewModel.Instance is not null && IsHeld)
			{
				PlayerViewModel.Instance?.SetLoadedState( !HasUnloadedAnimation || CurrentBullets > 0 );
			}
		}
		else
		{
			UpdateProxyShotEffects();
		}
	}

	private void UpdateShellReloadVisuals()
	{
		if ( ShellReloadActive == _lastShellReloadActive )
			return;

		_lastShellReloadActive = ShellReloadActive;
	}

	private void HandleReloadBolt()
	{
		if ( _reloadBoltTime > 0 && Time.Now >= _reloadBoltTime )
		{
			_reloadBoltTime = 0;
			PlayerViewModel.Instance.PlayReloadBolt();
		}
	}

	private void UpdateRecoil()
	{
		if ( Recoil is null || !Recoil.HasPoints )
		{
			_currentRecoil = Vector2.Lerp( _currentRecoil, Vector2.Zero, Recoil?.RecoverySpeed ?? 8f * Time.Delta );
			return;
		}

		if ( Time.Now - _lastFireTime > FireRate * 2f )
		{
			_recoilIndex = 0;
			_currentRecoil = Vector2.Lerp( _currentRecoil, Vector2.Zero, Recoil.RecoverySpeed * Time.Delta );
			return;
		}

		var target = _recoilIndex > 0 ? Recoil.GetPoint( _recoilIndex - 1 ) : Vector2.Zero;
		_currentRecoil = Vector2.Lerp( _currentRecoil, target, Recoil.ApproachSpeed * Time.Delta );
	}

	private void UpdateViewModelAnimation()
	{
		if ( PlayerViewModel.Instance is null || !PlayerViewModel.Instance.HasActiveWeapon )
			return;

		bool inBurst = Time.Now - _lastFireTime <= FireRate * 2f;
		float attackHold = inBurst ? Math.Clamp( (Time.Now - _burstStartTime) / 0.3f, 0f, 1f ) : 0f;

		PlayerViewModel.Instance.UpdateWeaponAnimation( attackHold, EffectiveRecoil );
	}

	private void HandleFiring()
	{
		if ( Firing && CanFire )
		{
			PlayerViewModel.Instance?.ShowMuzzleFlash();
			PlayerViewModel.Instance?.PlayShotSound();
			SpawnProjectiles();
			CurrentBullets--;
			PlayerViewModel.Instance?.PlayAttack();
			PlayerAnimationBroadcast.CurrentPlayer.TriggerAttack();

			bool startingBurst = Time.Now - _lastFireTime > FireRate * 2f;
			_lastFireTime = Time.Now;
			if ( startingBurst )
				_burstStartTime = Time.Now;

			if ( Recoil is not null && Recoil.HasPoints )
				_recoilIndex++;

			if ( Type == GunType.Bolt )
				_reloadBoltTime = Time.Now + 0.5f;

			FireSequence++;
			_nextFireTime = Time.Now + FireRate;

			if ( Game.IsRunningInVR )
				TriggerHeldHandRumble();
		}

		Firing = false;
	}

	private void TriggerHeldHandRumble()
	{
		foreach ( var grab in GameObject.Components.GetAll<GrabPoint>( FindMode.InDescendants ) )
		{
			if ( grab.IsGrabbed )
				grab.TriggerHaptic( HapticEffect.HardImpact, HapticLengthScale, HapticFrequencyScale, HapticAmplitudeScale );
		}
	}

	private void UpdateProxyShotEffects()
	{
		if ( FireSequence != _lastProcessedFireSequence )
		{
			_lastProcessedFireSequence = FireSequence;
			TriggerWorldMuzzleFlash();
			SoundPoint?.StartSound();
		}

		UpdateWorldMuzzleFlashVisibility();
	}

	private void TriggerWorldMuzzleFlash()
	{
		if ( Muzzleflash is null )
			return;

		Muzzleflash.Enabled = false;
		Muzzleflash.Enabled = true;
		_worldMuzzleHideTime = Time.Now + MuzzleFlashDuration;
	}

	private void UpdateWorldMuzzleFlashVisibility()
	{
		if ( Muzzleflash is null || !Muzzleflash.Enabled )
			return;

		if ( Time.Now >= _worldMuzzleHideTime )
			Muzzleflash.Enabled = false;
	}

	private void SpawnProjectiles()
	{
		if ( IsProxy || BulletPrefab is null )
			return;

		Vector3 spawnPosition;
		Vector3 shootDirection;

		if ( Game.IsRunningInVR )
		{
			spawnPosition = MuzzleTransform.WorldPosition;
			shootDirection = MuzzleTransform.WorldRotation.Forward;
		}
		else
		{
			var ray = PlayerViewModel.Instance.GetAimRay( EffectiveRecoil );
			spawnPosition = ray.Position;
			shootDirection = ray.Forward;
		}

		int pelletCount = Math.Max( 1, PelletCount );
		for ( int i = 0; i < pelletCount; i++ )
		{
			var pelletDirection = ApplySpread( shootDirection, SpreadAngle );
			var bullet = BulletPrefab.Clone( spawnPosition, Rotation.LookAt( pelletDirection ) );

			var projectileScript = bullet.Components.Get<Projectile>();
			if ( projectileScript is not null )
			{
				projectileScript.Velocity = pelletDirection * BulletSpeed;
				projectileScript.WeaponObject = GameObject;
			}

			if ( projectileScript.StayInWorld )
			{
				bullet.NetworkSpawn();
			}
		}
	}

	private static Vector3 ApplySpread( Vector3 direction, float spreadAngle )
	{
		if ( spreadAngle <= 0f )
			return direction.Normal;

		var angles = Rotation.LookAt( direction.Normal ).Angles();
		angles.pitch += Game.Random.Float( -spreadAngle, spreadAngle );
		angles.yaw += Game.Random.Float( -spreadAngle, spreadAngle );
		return Rotation.From( angles ).Forward;
	}

	public void Fire()
	{
		if ( ShellReloadActive )
		{
			_shellReloadCancelPending = true;
			_fireWhenShellReloadEnds = true;
			return;
		}

		if ( BlocksFireFromReload || IsEquipping || CurrentBullets <= 0 )
			return;

		Firing = true;
	}

	public void Reload()
	{
		if ( IsReloading || CurrentBullets >= MagazineSize || IsEquipping || ShellReloadActive )
			return;

		if ( Type == GunType.Shotgun )
		{
			BeginShellReload();
			return;
		}

		IsReloading = true;
		_reloadEndTime = Time.Now + ReloadTime;
		_magOutPlayed = false;
		_magInPlayed = false;
		PlayerViewModel.Instance?.PlayReload();
		PlayerAnimationBroadcast.CurrentPlayer.TriggerReload();

		if ( Game.IsRunningInVR )
			CreateVrReloadBar();
	}

	private void BeginShellReload()
	{
		ShellReloadActive = true;
		_shellReloadCancelPending = false;
		_fireWhenShellReloadEnds = false;
		PlayerViewModel.Instance?.BeginShellReload();
		BeginNextShellInsert();

		if ( Game.IsRunningInVR )
			CreateVrReloadBar();
	}

	private void BeginNextShellInsert()
	{
		PlayShellInsertAnimation();
		_nextShellAmmoTime = Time.Now + ShellInsertInterval;
		_shellInsertSoundPlayed = false;
	}

	private void PlayShellInsertAnimation()
	{
		PlayerViewModel.Instance?.InsertShell();
	}

	private void EndShellReload()
	{
		if ( !ShellReloadActive )
			return;

		ShellReloadActive = false;
		_shellReloadCancelPending = false;
		PlayerViewModel.Instance?.EndShellReload();
		DestroyVrReloadBar();
	}

	private void FinishShellReload( bool tryFire )
	{
		bool shouldFire = tryFire && _fireWhenShellReloadEnds && CurrentBullets > 0;
		_fireWhenShellReloadEnds = false;
		EndShellReload();

		if ( shouldFire && !IsEquipping )
			Firing = true;
	}

	private void UpdateShellReload()
	{
		if ( Type != GunType.Shotgun || !ShellReloadActive )
			return;

		if ( !IsHeld )
		{
			_fireWhenShellReloadEnds = false;
			EndShellReload();
			return;
		}

		if ( CurrentBullets >= MagazineSize )
		{
			FinishShellReload( tryFire: false );
			return;
		}

		var shellInsertStartTime = _nextShellAmmoTime - ShellInsertInterval;
		var elapsed = Time.Now - shellInsertStartTime;

		if ( !_shellInsertSoundPlayed && elapsed >= ShellInsertSoundTime )
		{
			_shellInsertSoundPlayed = true;
			PlayReloadSound( ShellInsertSound );
		}

		if ( Time.Now < _nextShellAmmoTime )
			return;

		CurrentBullets++;

		if ( _shellReloadCancelPending || CurrentBullets >= MagazineSize )
		{
			FinishShellReload( tryFire: true );
			return;
		}

		BeginNextShellInsert();
	}

	private void UpdateEquipState()
	{
		bool isHeld = IsHeld;

		if ( isHeld && !_wasHeld )
			BeginEquip();

		if ( !isHeld && _wasHeld )
			CancelEquip();

		_wasHeld = isHeld;

		if ( IsEquipping && Time.Now >= _equipEndTime )
			IsEquipping = false;
	}

	private void UpdateReloadingState()
	{
		UpdateShellReload();
		UpdateShellReloadBar();

		if ( !IsReloading )
			return;

		if ( !IsHeld )
		{
			IsReloading = false;
			DestroyVrReloadBar();
			return;
		}

		var reloadStartTime = _reloadEndTime - ReloadTime;
		var elapsed = Time.Now - reloadStartTime;

		if ( !_magOutPlayed && elapsed >= MagOutTime )
		{
			_magOutPlayed = true;
			PlayReloadSound( MagOutSound );
		}

		if ( !_magInPlayed && elapsed >= MagInTime )
		{
			_magInPlayed = true;
			PlayReloadSound( MagInSound );
		}

		if ( _reloadBar is not null )
			_reloadBar.Progress = elapsed / ReloadTime;
		if ( Time.Now >= _reloadEndTime )
		{
			IsReloading = false;
			CurrentBullets = MagazineSize;
			DestroyVrReloadBar();

			if ( !Game.IsRunningInVR )
			{
				PlayerAnimationBroadcast.CurrentPlayer.DisableSecondaryGrip = false;
			}
		}
	}

	[Rpc.Broadcast]
	private void PlayReloadSound( SoundEvent sound )
	{
		if ( sound is null )
			return;

		var isWorldSound = IsProxy || Game.IsRunningInVR;
		sound.UI = !isWorldSound;

		if (!sound.UI)
		{
			sound.Distance = 2000f;
		}

		Sound.Play( sound, WorldPosition );
	}

	private void CreateVrReloadBar()
	{
		DestroyVrReloadBar();

		var worldPanelGO = new GameObject( parent: GameObject, name: "ReloadBar" )
		{
			NetworkMode = NetworkMode.Never
		};
		var worldPanel = worldPanelGO.AddComponent<WorldPanel>();
		worldPanel.PanelSize = new Vector2( 300f, 100f );
		worldPanel.LookAtCamera = true;
		_reloadBar = worldPanelGO.AddComponent<ReloadBar>();
		_reloadBar.Progress = 0f;
	}

	private void DestroyVrReloadBar()
	{
		_reloadBar?.Destroy();
		_reloadBar = null;
	}

	private void UpdateShellReloadBar()
	{
		if ( !Game.IsRunningInVR || _reloadBar is null || !ShellReloadActive )
			return;

		var shellInsertStartTime = _nextShellAmmoTime - ShellInsertInterval;
		_reloadBar.Progress = Math.Clamp( (Time.Now - shellInsertStartTime) / ShellInsertInterval, 0f, 1f );
	}

	private void BeginEquip()
	{
		if ( Game.IsRunningInVR || EquipTime <= 0f )
			return;

		IsEquipping = true;
		_equipEndTime = Time.Now + EquipTime;
	}

	private void CancelEquip()
	{
		IsEquipping = false;
	}
}