Editor/FenceLibrary/FenceDefinitionAssetPreview.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Editor.Assets;

namespace Editor;

[AssetPreview( "fencedef" )]
public sealed class FenceDefinitionAssetPreview : AssetPreview
{
	private static readonly Color GuideColor = Color.Parse( "#f8a64c" ) ?? Color.Orange;

	private FenceDefinition definition;
	private GameObject previewRoot;
	private readonly List<FenceGuideSceneObject> guides = [];
	private int lastHash;

	public FenceDefinitionAssetPreview( Asset asset ) : base( asset )
	{
	}

	public override float PreviewWidgetCycleSpeed => 0.0f;

	public override async Task InitializeAsset()
	{
		await base.InitializeAsset();

		definition = Asset.LoadResource<FenceDefinition>();
		if ( definition is null )
			return;

		using ( Scene.Push() )
		{
			RebuildPreview();
		}
	}

	public override void UpdateScene( float cycle, float timeStep )
	{
		if ( definition is not null )
		{
			var currentHash = FenceDefinitionEditorUtility.ComputeDefinitionHash( definition );
			if ( currentHash != lastHash )
			{
				using ( Scene.Push() )
				{
					RebuildPreview();
				}
			}
		}

		using ( Scene.Push() )
		{
			var rotation = new Angles( 18.0f, 225.0f, 0.0f ).ToRotation();
			var distance = MathX.SphereCameraDistance( Math.Max( SceneSize.Length * 0.5f, 1.0f ), Camera.FieldOfView );
			var aspect = ScreenSize.y > 0 ? (float)ScreenSize.x / ScreenSize.y : 1.0f;
			if ( aspect > 1.0f )
			{
				distance *= aspect;
			}

			Camera.WorldRotation = rotation;
			Camera.WorldPosition = SceneCenter + rotation.Forward * -distance;
		}

		TickScene( timeStep );
	}

	public override void Dispose()
	{
		using ( Scene?.Push() )
		{
			ClearPreview();
		}

		base.Dispose();
	}

	private void RebuildPreview()
	{
		FenceGuideSceneObject.EnsureAssetsLoaded();
		ClearPreview();

		previewRoot = new GameObject( true, "Fence Preview" );
		PrimaryObject = previewRoot;
		lastHash = FenceDefinitionEditorUtility.ComputeDefinitionHash( definition );

		var prototypes = FenceDefinitionEditorUtility.BuildPrototypes( definition );
		var cursor = 0.0f;
		const float gap = 16.0f;
		var hasBounds = false;
		var bounds = BBox.FromPositionAndSize( Vector3.Zero, 8.0f );

		foreach ( var prototype in prototypes )
		{
			var segmentRoot = SpawnPreviewSegment( prototype );
			if ( !segmentRoot.IsValid() )
				continue;

			segmentRoot.SetParent( previewRoot, true );

			var range = FenceDefinitionEditorUtility.MeasureProjectedRange( segmentRoot.GetBounds(), Vector3.Forward );
			segmentRoot.WorldPosition += Vector3.Forward * (cursor - range.Min);

			var segmentBounds = segmentRoot.GetBounds();
			var guideZ = segmentBounds.Mins.z - 2.0f;
			var guideStart = new Vector3( cursor, segmentBounds.Center.y, guideZ );
			var guideEnd = guideStart + Vector3.Forward * prototype.CanonicalLength;
			guides.Add( new FenceGuideSceneObject( Scene.SceneWorld, guideStart, guideEnd, GuideColor ) );

			if ( !hasBounds )
			{
				bounds = segmentBounds;
				hasBounds = true;
			}
			else
			{
				bounds = bounds.AddBBox( segmentBounds );
			}

			bounds = bounds.AddPoint( guideStart );
			bounds = bounds.AddPoint( guideEnd );
			cursor += prototype.CanonicalLength + gap;
		}

		SceneCenter = hasBounds ? bounds.Center : Vector3.Zero;
		SceneSize = hasBounds ? bounds.Size : new Vector3( 64.0f, 64.0f, 64.0f );
	}

	private GameObject SpawnPreviewSegment( FencePrototype prototype )
	{
		if ( prototype is null )
			return null;

		var plan = new FencePlacementPlan
		{
			RootName = prototype.DisplayName,
			Origin = Vector3.Zero,
			Direction = Vector3.Forward,
			TotalLength = prototype.CanonicalLength,
			Segments =
			[
				new FencePlacementSegment
				{
					Prototype = prototype,
					LinePoint = Vector3.Zero,
					SurfacePoint = Vector3.Zero,
					Direction = Vector3.Forward,
					Up = Vector3.Up,
					Length = prototype.CanonicalLength,
					StartDistance = 0.0f,
					EndDistance = prototype.CanonicalLength
				}
			]
		};

		var placed = FenceDefinitionEditorUtility.CommitPlacementPlan( plan );
		if ( !placed.IsValid() )
			return null;

		var segment = placed.Children.Count > 0 ? placed.Children[0] : null;
		if ( segment.IsValid() )
		{
			segment.Parent = null;
		}
		placed.Destroy();

		return segment;
	}

	private void ClearPreview()
	{
		FenceDefinitionEditorUtility.DestroyHierarchy( previewRoot );
		previewRoot = null;
		PrimaryObject = null;

		foreach ( var guide in guides )
		{
			guide?.Delete();
		}

		guides.Clear();
	}
}

internal sealed class FenceGuideSceneObject : SceneCustomObject
{
	private static Material lineMaterial;
	private readonly Vertex[] vertices;

	internal static void EnsureAssetsLoaded()
	{
		lineMaterial ??= Material.Load( "materials/gizmo/line.vmat" );
	}

	public FenceGuideSceneObject( SceneWorld world, Vector3 start, Vector3 end, Color color ) : base( world )
	{
		var direction = (end - start).Normal;
		var arrowLength = Math.Max( (end - start).Length * 0.12f, 4.0f );
		var arrowBase = end - direction * arrowLength;
		var side = Vector3.Cross( direction, Vector3.Up ).Normal * Math.Max( arrowLength * 0.45f, 1.5f );

		vertices =
		[
			new Vertex( start, color ),
			new Vertex( end, color ),
			new Vertex( end, color ),
			new Vertex( arrowBase + side, color ),
			new Vertex( end, color ),
			new Vertex( arrowBase - side, color )
		];

		var bounds = BBox.FromPositionAndSize( start, 1.0f );
		bounds = bounds.AddPoint( end );
		bounds = bounds.AddPoint( arrowBase + side );
		bounds = bounds.AddPoint( arrowBase - side );
		Bounds = bounds;
	}

	public override void RenderSceneObject()
	{
		if ( lineMaterial is null )
			return;

		Graphics.Draw( vertices.AsSpan(), vertices.Length, lineMaterial, Attributes, Graphics.PrimitiveType.Lines );
	}
}