Editor/WeaponImporter/Tool/UI/Preview/WeaponViewport.Overlays.cs
using Editor;
using Sandbox;
using WeaponImporter.Core.Geometry;
using WeaponImporter.Core.Setup;
using N = System.Numerics;

namespace WeaponImporter.Tool.UI;

public sealed partial class WeaponViewport
{
	private static readonly Color Bones = new( 1f, .58f, .16f );
	private static readonly Color RightColor = new( .3f, 1f, .4f );
	private static readonly Color LeftColor = new( .25f, .6f, 1f );
	private static readonly Color Highlight = new( 1f, .35f, .8f );
	private static readonly Color Muzzle = new( 1f, .3f, .25f );
	private static readonly Color Eject = new( 1f, .85f, .2f );

	private Vector2 _hoverPosition;
	private N.Vector3 _probePoint = new( float.NaN );
	private GripSurface _probe;

	/// <summary>A ray from a viewport pixel into canonical weapon space, hitting the weapon's triangles.</summary>
	public (N.Vector3 Point, N.Vector3 Normal)? RaycastWeapon( Vector2 local )
	{
		var analysis = _c.Analysis;
		if ( analysis is null || !_weaponObject.IsValid() || !Camera.IsValid() )
			return null;
		var ray = GetRay( local );
		var weapon = WeaponWorld;
		var origin = weapon.PointToLocal( ray.Position );
		var direction = (weapon.Rotation.Inverse * ray.Forward).Normal;
		var hit = analysis.WeaponBvh.Raycast( origin.ToCore(), direction.ToCore(), 10000f );
		if ( hit is not { } h )
			return null;
		var normal = h.Normal;
		if ( N.Vector3.Dot( normal, direction.ToCore() ) > 0 )
			normal = -normal;
		return (h.Point, normal);
	}

	private void DrawOverlays()
	{
		var session = _c.Session;
		UpdateGizmoInputs( false );
		using var scope = Gizmo.Scope( "weapon-importer" );
		Gizmo.Transform = Transform.Zero;
		Gizmo.Draw.IgnoreDepth = false;
		Gizmo.Draw.LineThickness = 1f;
		if ( !OwnArmsView )
			DrawFloor();
		if ( session?.Setup is null )
			return;
		// First person with the file's own arms is view only: the same markers, following the
		// weapon as its animation moves it, without picking or IK targets.
		var own = OwnArmsView;
		if ( !own && !_weaponObject.IsValid() )
			return;

		var setup = session.Setup;
		var weapon = WeaponWorld;
		Gizmo.Draw.IgnoreDepth = true;

		// Arms: orange while the hand is on the weapon for this action, gray while it plays its
		// own animation; the arm under the cursor lights up (click toggles it).
		var hoverArm = !own && _c.Pick == PickTarget.None && IsUnderMouse && HitMarker( _hoverPosition ) == PickTarget.None ? HitArm( _hoverPosition ) : null;
		if ( own )
		{
			if ( _c.ShowSkeleton )
				DrawOwnArms();
		}
		else if ( (_c.ShowSkeleton || hoverArm is not null) && _body.IsValid() && _body.SceneModel.IsValid() )
		{
			foreach ( var (bone, parent) in _armBones )
			{
				var side = SideOf( bone );
				var hovered = side is not null && side == hoverArm;
				if ( !_c.ShowSkeleton && !hovered )
					continue;
				var released = side is { } s && _c.IsHandReleased( _c.Role, s );
				Gizmo.Draw.LineThickness = hovered ? 3f : 1.5f;
				Gizmo.Draw.Color = hovered ? Color.White : released ? Theme.TextLight.WithAlpha( .6f ) : Bones.WithAlpha( .85f );
				var a = _body.SceneModel.GetBoneWorldTransform( parent ).Position;
				var b = _body.SceneModel.GetBoneWorldTransform( bone ).Position;
				Gizmo.Draw.Line( a, b );
			}
		}

		if ( (_c.ShowRegions || _c.HighlightRegion is not null) && _c.Analysis is { } regionsOf )
			DrawRegions( weapon, regionsOf );

		if ( _c.ShowContacts || _c.ShowNormals )
		{
			DrawGrip( weapon, setup.Primary, _c.RightSurface, RightColor, true );
			if ( setup.UseSupportHand && setup.Support is not null )
				DrawGrip( weapon, setup.Support, _c.LeftSurface, LeftColor, true );
		}

		if ( !own && _c.ShowIkTargets && _hold.IsValid() )
		{
			if ( WeaponHold.TryParse( _hold.RightHand, out var r ) )
				Triad( weapon.ToWorld( r ), 3f );
			if ( setup.UseSupportHand && WeaponHold.TryParse( _hold.LeftHand, out var l ) )
				Triad( weapon.ToWorld( l ), 3f );
		}

		if ( _c.ShowAttachments || _c.Pick is PickTarget.Muzzle or PickTarget.Eject )
		{
			var hoverMarker = !own && _c.Pick == PickTarget.None && IsUnderMouse ? HitMarker( _hoverPosition ) : PickTarget.None;
			DrawPoint( weapon, setup.Muzzle, Muzzle, 5f, _c.Pick == PickTarget.Muzzle, hoverMarker == PickTarget.Muzzle );
			DrawPoint( weapon, setup.Eject, Eject, 3.5f, _c.Pick == PickTarget.Eject, hoverMarker == PickTarget.Eject );
		}

		if ( !string.IsNullOrEmpty( _c.HighlightBone ) && _c.Analysis is { } analysis )
		{
			var skeleton = analysis.Asset.Skeleton;
			var index = skeleton.IndexOf( _c.HighlightBone );
			if ( index >= 0 )
			{
				// First person shows the bone where the animation has it.
				Vector3 At( int bone ) => own && FirstPersonBone( bone ) is { } animated ? animated : weapon.PointToWorld( skeleton.RestWorld[bone].Pos.ToEngine() );
				var p = At( index );
				Gizmo.Draw.Color = Highlight;
				Gizmo.Draw.LineThickness = 2f;
				Gizmo.Draw.LineSphere( p, 0.8f, 8 );
				var parent = skeleton[index].ParentIndex;
				if ( parent >= 0 )
					Gizmo.Draw.Line( At( parent ), p );
				Gizmo.Draw.ScreenText( _c.HighlightBone, p, new Vector2( 10, -8 ), "Roboto", 12, TextFlag.LeftTop );
			}
		}

		if ( own )
		{
			DrawFirstPersonHud();
			return;
		}
		if ( _c.Pick != PickTarget.None )
			DrawPickProbe( weapon );

		DrawHud();
	}

	/// <summary>The file's own first-person arms as bone lines (orange, like the character's).</summary>
	private void DrawOwnArms()
	{
		var analysis = _c.Analysis;
		if ( analysis is null )
			return;
		var skeleton = analysis.Asset.Skeleton;
		Gizmo.Draw.LineThickness = 1.5f;
		Gizmo.Draw.Color = Bones.WithAlpha( .85f );
		if ( _fpArmSegments.Count > 0 )
		{
			foreach ( var (parent, bone) in _fpArmSegments )
				if ( FirstPersonBone( parent ) is { } a && FirstPersonBone( bone ) is { } b )
					Gizmo.Draw.Line( a, b );
			return;
		}
		foreach ( var bone in analysis.ArmBones )
		{
			var parent = skeleton[bone].ParentIndex;
			if ( parent < 0 || !analysis.ArmBones.Contains( parent ) || !IsOwnArmDisplayBone( bone ) || !IsOwnArmDisplayBone( parent ) )
				continue;
			if ( FirstPersonBone( parent ) is { } a && FirstPersonBone( bone ) is { } b )
				Gizmo.Draw.Line( a, b );
		}
	}

	public static Color RegionColor( Core.Grip.GripRegionKind kind ) => kind switch
	{
		Core.Grip.GripRegionKind.PrimaryGrip => new Color( .3f, 1f, .4f ),
		Core.Grip.GripRegionKind.SecondaryGrip => new Color( .25f, .6f, 1f ),
		Core.Grip.GripRegionKind.Trigger => new Color( 1f, .85f, .2f ),
		Core.Grip.GripRegionKind.Support => new Color( 1f, .58f, .16f ),
		_ => new Color( 1f, .3f, .3f ),
	};

	/// <summary>Semantic regions as boxes on the weapon; the highlighted one is drawn stronger.</summary>
	private void DrawRegions( Transform weapon, Core.Analysis.WeaponAnalysis analysis )
	{
		foreach ( var region in Core.Grip.GripRegions.Of( analysis ) )
		{
			var highlighted = _c.HighlightRegion == region.Kind;
			if ( !_c.ShowRegions && !highlighted )
				continue;
			var c = region.Center.ToEngine();
			var h = region.HalfExtents.ToEngine();
			var color = RegionColor( region.Kind );
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.LineThickness = highlighted ? 2.5f : 1f;
			Gizmo.Draw.Color = color.WithAlpha( highlighted ? 1f : .55f );
			using ( Gizmo.Scope( $"region-{region.Kind}", weapon ) )
				Gizmo.Draw.LineBBox( new BBox( c - h, c + h ) );
			Gizmo.Draw.IgnoreDepth = false;
		}
	}

	private void DrawGrip( Transform weapon, GripSetup grip, Core.Analysis.GripCandidate measured, Color color, bool ring )
	{
		if ( grip is null || !grip.Enabled )
			return;
		var contact = weapon.PointToWorld( V.Of( grip.Contact ).ToEngine() );
		var normal = weapon.Rotation * V.Of( grip.Normal ).ToEngine();
		if ( _c.ShowContacts )
		{
			Gizmo.Draw.Color = color;
			Gizmo.Draw.SolidSphere( contact, 0.35f, 8, 8 );
			if ( ring && measured?.Surface is { } s )
			{
				Gizmo.Draw.LineThickness = 1.5f;
				Gizmo.Draw.Color = color.WithAlpha( .7f );
				var axis = weapon.Rotation * s.Axis.ToEngine();
				Gizmo.Draw.LineCircle( weapon.PointToWorld( s.Center.ToEngine() ), axis, s.Radius, 0, 360, 24 );
			}
		}
		if ( _c.ShowNormals )
		{
			Gizmo.Draw.LineThickness = 1.5f;
			Gizmo.Draw.Color = color;
			Gizmo.Draw.Arrow( contact, contact + normal * 3f, 0.8f, 0.35f );
		}
	}

	private void DrawPoint( Transform weapon, PointSetup point, Color color, float length, bool selected, bool hovered )
	{
		// First person: the point rides on its own bone (a slide, a bolt) as the animation moves it.
		if ( point is not null && OwnArmsView && _c.Analysis?.Asset.Skeleton.IndexOf( point.Bone ) is { } pointBone && FirstPersonBoneFrame( pointBone ) is { } frame )
			weapon = frame;
		if ( point is not null && (selected || hovered) )
		{
			// Selected (or about to be): a ring that faces the camera around the marker.
			var at = weapon.PointToWorld( V.Of( point.Canonical ).ToEngine() );
			Gizmo.Draw.LineThickness = selected ? 2.5f : 1.5f;
			Gizmo.Draw.Color = selected ? Color.White : color.WithAlpha( .8f );
			Gizmo.Draw.LineCircle( at, (Camera.WorldPosition - at).Normal, 0.9f, 0, 360, 24 );
		}
		if ( point is null || _c.Analysis is not { } analysis )
			return;
		var skeleton = analysis.Asset.Skeleton;
		var bone = skeleton.IndexOf( point.Bone );
		var rest = bone >= 0 ? skeleton.RestWorld[bone] : Core.Maths.XForm.Identity;
		// Only the direction is needed from the bone-local transform; the position is stored in canonical space too.
		var canonical = Core.Maths.XForm.Compose( rest, new Core.Maths.XForm( N.Vector3.Zero, V.Q( point.Rotation ) ) );
		var origin = weapon.PointToWorld( V.Of( point.Canonical ).ToEngine() );
		var direction = weapon.Rotation * N.Vector3.Transform( N.Vector3.UnitX, canonical.Rot ).ToEngine();
		Gizmo.Draw.LineThickness = 2f;
		Gizmo.Draw.Color = color;
		Gizmo.Draw.SolidSphere( origin, 0.3f, 6, 6 );
		Gizmo.Draw.Arrow( origin, origin + direction * length, 1.2f, 0.5f );
	}

	/// <summary>A quiet floor grid (one foot squares) fading out towards the edges, without colored axes.</summary>
	private static void DrawFloor()
	{
		const int cells = 12;
		const float step = 12f;
		const float extent = cells * step;
		Gizmo.Draw.LineThickness = 1f;
		for ( var i = -cells; i <= cells; i++ )
		{
			var fade = 1f - MathF.Abs( i ) / (cells + 1f);
			Gizmo.Draw.Color = Color.White.WithAlpha( (i == 0 ? .1f : .05f) * fade + .01f );
			var o = i * step;
			Gizmo.Draw.Line( new Vector3( o, -extent, 0 ), new Vector3( o, extent, 0 ) );
			Gizmo.Draw.Line( new Vector3( -extent, o, 0 ), new Vector3( extent, o, 0 ) );
		}
	}

	private static void Triad( Transform t, float size )
	{
		Gizmo.Draw.LineThickness = 2f;
		Gizmo.Draw.Color = Color.Red;
		Gizmo.Draw.Line( t.Position, t.Position + t.Rotation.Forward * size );
		Gizmo.Draw.Color = Color.Green;
		Gizmo.Draw.Line( t.Position, t.Position + t.Rotation.Left * size );
		Gizmo.Draw.Color = Color.Blue;
		Gizmo.Draw.Line( t.Position, t.Position + t.Rotation.Up * size );
	}

	private void DrawPickProbe( Transform weapon )
	{
		if ( !IsUnderMouse )
			return;
		var hit = RaycastWeapon( _hoverPosition );
		if ( hit is not { } h )
			return;
		var picking = _c.Pick;
		var color = picking switch
		{
			PickTarget.LeftGrip => LeftColor,
			PickTarget.Muzzle => Muzzle,
			PickTarget.Eject => Eject,
			_ => RightColor,
		};
		var p = weapon.PointToWorld( h.Point.ToEngine() );
		var n = weapon.Rotation * h.Normal.ToEngine();
		Gizmo.Draw.Color = color;
		Gizmo.Draw.SolidSphere( p, 0.3f, 8, 8 );
		Gizmo.Draw.LineThickness = 1.5f;
		Gizmo.Draw.Arrow( p, p + n * 3f, 0.8f, 0.35f );
		if ( picking is PickTarget.RightGrip or PickTarget.LeftGrip && _c.Analysis is { } analysis )
		{
			if ( N.Vector3.DistanceSquared( _probePoint, h.Point ) > 0.04f )
			{
				_probePoint = h.Point;
				var hint = picking == PickTarget.RightGrip ? N.Vector3.UnitZ : N.Vector3.UnitX;
				_probe = SurfaceProbe.FromPoint( analysis.WeaponBvh, h.Point + h.Normal * 0.05f, hint );
			}
			if ( _probe is { } s )
			{
				Gizmo.Draw.Color = color.WithAlpha( .8f );
				Gizmo.Draw.LineCircle( weapon.PointToWorld( s.Center.ToEngine() ), weapon.Rotation * s.Axis.ToEngine(), s.Radius, 0, 360, 28 );
				Gizmo.Draw.ScreenText( $"r {s.Radius:0.00} in", p, new Vector2( 12, 6 ), "Roboto", 12, TextFlag.LeftTop );
			}
		}
	}

	private void DrawHud()
	{
		var lines = new List<(string Text, Color Color)>();
		if ( _c.Solving )
			lines.Add( ("Fitting hands…", Theme.Yellow) );
		if ( _c.Pick != PickTarget.None )
			lines.Add( (ImporterController.PickHint( _c.Pick ), Theme.Blue) );
		var y = 10f;
		foreach ( var (text, color) in lines )
		{
			Gizmo.Draw.Color = color;
			Gizmo.Draw.ScreenText( text, new Vector2( 12, y ), "Roboto", 13, TextFlag.LeftTop );
			y += 18f;
		}
	}
}