Editor/FenceLibrary/FenceDefinitionEditorUtility.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox.Volumes;

namespace Editor;

internal sealed class FencePrototype
{
	public FenceDefinitionEntry Entry { get; init; }
	public string DisplayName { get; init; }
	public Model Model { get; init; }
	public PrefabFile Prefab { get; init; }
	public Angles RotationOffset { get; init; }
	public float Weight { get; init; }
	public float CanonicalLength { get; init; }
}

internal sealed class FencePlacementSegment
{
	public FencePrototype Prototype { get; init; }
	public Vector3 LinePoint { get; init; }
	public Vector3 SurfacePoint { get; init; }
	public Vector3 Direction { get; init; }
	public Vector3 Up { get; init; }
	public Angles RandomRotationOffset { get; init; }
	public float Length { get; init; }
	public float StartDistance { get; init; }
	public float EndDistance { get; init; }
}

internal sealed class FencePlacementPlan
{
	public string RootName { get; set; }
	public Vector3 Origin { get; set; }
	public Vector3 Direction { get; set; }
	public float TotalLength { get; set; }
	public bool CreateBlockerVolumes { get; set; }
	public List<FencePlacementSegment> Segments { get; set; } = [];
}

internal enum FenceToolPlacementMode
{
	Line,
	Spline
}

internal readonly record struct FenceSplineControlPoint( Vector3 Position, Vector3 Normal );

internal readonly record struct FenceSplineSample( Vector3 Position, Vector3 Normal );

internal readonly record struct FenceSplineDistanceSample( Vector3 Position, Vector3 Normal, Vector3 Tangent );

internal sealed class FenceToolSettings
{
	/// <summary>
	/// Fence definition asset used for the next placement run.
	/// </summary>
	[Property]
	public FenceDefinition Definition { get; set; }

	/// <summary>
	/// Selects whether the tool places a straight line run or a surface-drawn spline run.
	/// </summary>
	[Property]
	public FenceToolPlacementMode Mode { get; set; } = FenceToolPlacementMode.Line;

	/// <summary>
	/// When enabled, each segment start is projected down onto scene geometry before placement.
	/// </summary>
	[Property]
	public bool SnapToGround { get; set; } = true;

	/// <summary>
	/// Vertical distance used for the downward scene trace when snapping fence segments to the ground.
	/// </summary>
	[Property]
	public float GroundTraceHeight { get; set; } = 256.0f;

	/// <summary>
	/// Optional scene tag filter for the ground trace. When empty, any visible scene geometry may be used.
	/// </summary>
	[Property]
	public string SurfaceTag { get; set; } = string.Empty;

	/// <summary>
	/// Keeps fence segments upright by ignoring vertical slope when resolving their forward direction.
	/// </summary>
	[Property]
	public bool KeepUpright { get; set; } = true;

	/// <summary>
	/// Minimum world-space distance between captured points while drawing spline fence runs.
	/// </summary>
	[Property]
	public float SplinePointSpacing { get; set; } = 24.0f;

	/// <summary>
	/// Number of interpolated curve samples generated between captured spline points for preview and placement.
	/// </summary>
	[Property]
	public int SplineInterpolationSteps { get; set; } = 6;

	/// <summary>
	/// Seed used for deterministic random selection when multiple entries fit a run segment.
	/// </summary>
	[Property]
	public int Seed { get; set; } = 1;

	/// <summary>
	/// Increments the seed after a successful placement so the next run can vary automatically.
	/// </summary>
	[Property]
	public bool IncrementSeedAfterPlacement { get; set; } = true;
}

internal readonly record struct FenceProjectionRange( float Min, float Max )
{
	public float Length => Max - Min;
}

internal sealed class FencePlacementCandidate
{
	public FencePrototype Prototype { get; init; }
	public float Length { get; init; }
}

internal static class FenceDefinitionEditorUtility
{
	internal const string PreviewTag = "__fence_preview";
	private const float MinimumSegmentLength = 0.01f;

	internal static int ComputeDefinitionHash( FenceDefinition definition )
	{
		if ( definition is null )
			return 0;

		var hash = new HashCode();
		hash.Add( definition.RootName ?? string.Empty );
		hash.Add( definition.UseWeightedRandom );
		hash.Add( definition.MaximumSegments );
		hash.Add( definition.SegmentGap );
		hash.Add( definition.RotationRandomizationMin.pitch );
		hash.Add( definition.RotationRandomizationMin.yaw );
		hash.Add( definition.RotationRandomizationMin.roll );
		hash.Add( definition.RotationRandomizationMax.pitch );
		hash.Add( definition.RotationRandomizationMax.yaw );
		hash.Add( definition.RotationRandomizationMax.roll );
		hash.Add( definition.DistanceRandomizationMin );
		hash.Add( definition.DistanceRandomizationMax );
		hash.Add( definition.CreateBlockerVolumes );
		hash.Add( definition.Entries?.Count ?? 0 );

		if ( definition.Entries is not null )
		{
			foreach ( var entry in definition.Entries )
			{
				if ( entry is null )
					continue;

				hash.Add( entry.DisplayName ?? string.Empty );
				hash.Add( entry.Model?.ResourcePath ?? string.Empty );
				hash.Add( entry.Prefab?.ResourcePath ?? string.Empty );
				hash.Add( entry.RotationOffset.pitch );
				hash.Add( entry.RotationOffset.yaw );
				hash.Add( entry.RotationOffset.roll );
				hash.Add( entry.Weight );
			}
		}

		return hash.ToHashCode();
	}

	internal static List<FencePrototype> BuildPrototypes( FenceDefinition definition )
	{
		var prototypes = new List<FencePrototype>();
		if ( definition?.Entries is null )
			return prototypes;

		foreach ( var entry in definition.Entries )
		{
			if ( entry is null || (entry.Prefab is null && entry.Model is null) )
				continue;

			var prototype = BuildPrototype( entry );
			if ( prototype is not null )
			{
				prototypes.Add( prototype );
			}
		}

		return prototypes;
	}

	internal static FencePlacementPlan BuildPlacementPlan( FenceDefinition definition, IReadOnlyList<FencePrototype> prototypes, FenceToolSettings settings, Scene scene, Vector3 startPoint, Vector3 endPoint )
	{
		var plan = new FencePlacementPlan
		{
			RootName = string.IsNullOrWhiteSpace( definition?.RootName ) ? "Fence" : definition.RootName,
			Origin = startPoint,
			Direction = Vector3.Forward,
			TotalLength = 0.0f
		};

		if ( definition is null || prototypes is null || prototypes.Count == 0 )
			return plan;

		var rawDirection = endPoint - startPoint;
		if ( rawDirection.Length < 1.0f )
			return plan;

		var runDirection = ResolveRunDirection( rawDirection, settings.KeepUpright, Vector3.Up );
		if ( runDirection.Length < 0.001f )
			return plan;

		var totalLength = Vector3.Dot( rawDirection, runDirection );
		if ( totalLength <= 0.0f )
			return plan;

		plan.Direction = runDirection;
		plan.TotalLength = totalLength;
		plan.CreateBlockerVolumes = definition.CreateBlockerVolumes;

		var rng = new Random( settings.Seed );
		var cursorDistance = 0.0f;
		var maximumSegments = Math.Max( definition.MaximumSegments, 1 );
		var baseGap = Math.Max( definition.SegmentGap, 0.0f );

		for ( var segmentIndex = 0; segmentIndex < maximumSegments; segmentIndex++ )
		{
			var remaining = totalLength - cursorDistance;
			if ( remaining <= MinimumSegmentLength )
				break;

			var linePoint = startPoint + runDirection * cursorDistance;
			TrySampleSurface( scene, settings, linePoint, out var surfacePoint, out var surfaceNormal );
			var up = settings.KeepUpright ? Vector3.Up : (surfaceNormal.Length < 0.001f ? Vector3.Up : surfaceNormal.Normal);
			var forward = ResolveRunDirection( runDirection, settings.KeepUpright, up );
			if ( forward.Length < 0.001f )
				break;

			var candidates = new List<FencePlacementCandidate>();

			foreach ( var prototype in prototypes )
			{
				if ( prototype.CanonicalLength <= remaining + 0.01f )
				{
					candidates.Add( new FencePlacementCandidate
					{
						Prototype = prototype,
						Length = prototype.CanonicalLength
					} );
				}
			}

			if ( candidates.Count == 0 )
				break;

			var chosen = PickCandidate( candidates, definition.UseWeightedRandom, rng );
			if ( chosen is null )
				break;

			var endDistance = cursorDistance + chosen.Length;
			plan.Segments.Add( new FencePlacementSegment
			{
				Prototype = chosen.Prototype,
				LinePoint = linePoint,
				SurfacePoint = surfacePoint,
				Direction = forward,
				Up = up,
				RandomRotationOffset = SampleRotationRandomization( definition, rng ),
				Length = chosen.Length,
				StartDistance = cursorDistance,
				EndDistance = endDistance
			} );

			cursorDistance = Math.Max( endDistance + SampleDistanceGap( definition, baseGap, rng ), cursorDistance + MinimumSegmentLength );
		}

		return plan;
	}

	internal static FencePlacementPlan BuildSplinePlacementPlan( FenceDefinition definition, IReadOnlyList<FencePrototype> prototypes, FenceToolSettings settings, Scene scene, IReadOnlyList<FenceSplineControlPoint> controlPoints )
	{
		var origin = controlPoints is not null && controlPoints.Count > 0 ? controlPoints[0].Position : Vector3.Zero;
		var plan = new FencePlacementPlan
		{
			RootName = string.IsNullOrWhiteSpace( definition?.RootName ) ? "Fence" : definition.RootName,
			Origin = origin,
			Direction = Vector3.Forward,
			TotalLength = 0.0f
		};

		if ( definition is null || prototypes is null || prototypes.Count == 0 || controlPoints is null || controlPoints.Count < 2 )
			return plan;

		var samples = BuildSplinePreviewPath( controlPoints, settings.SplineInterpolationSteps );
		var totalLength = MeasureSplineLength( samples );
		if ( totalLength < 1.0f )
			return plan;

		var initialSample = SampleSplineAtDistance( samples, 0.0f );
		var initialUp = settings.KeepUpright ? Vector3.Up : NormalizeNormal( initialSample.Normal );
		var initialDirection = ResolveRunDirection( initialSample.Tangent, settings.KeepUpright, initialUp );
		plan.Direction = initialDirection.Length < 0.001f ? Vector3.Forward : initialDirection;
		plan.TotalLength = totalLength;
		plan.CreateBlockerVolumes = definition.CreateBlockerVolumes;

		var rng = new Random( settings.Seed );
		var cursorDistance = 0.0f;
		var maximumSegments = Math.Max( definition.MaximumSegments, 1 );
		var baseGap = Math.Max( definition.SegmentGap, 0.0f );

		for ( var segmentIndex = 0; segmentIndex < maximumSegments; segmentIndex++ )
		{
			var remaining = totalLength - cursorDistance;
			if ( remaining <= MinimumSegmentLength )
				break;

			var candidates = new List<FencePlacementCandidate>();

			foreach ( var prototype in prototypes )
			{
				if ( prototype.CanonicalLength <= remaining + 0.01f )
				{
					candidates.Add( new FencePlacementCandidate
					{
						Prototype = prototype,
						Length = prototype.CanonicalLength
					} );
				}
			}

			if ( candidates.Count == 0 )
				break;

			var chosen = PickCandidate( candidates, definition.UseWeightedRandom, rng );
			if ( chosen is null )
				break;

			var startSample = SampleSplineAtDistance( samples, cursorDistance );
			var endDistance = cursorDistance + chosen.Length;
			var endSample = SampleSplineAtDistance( samples, Math.Min( endDistance, totalLength ) );
			var up = settings.KeepUpright ? Vector3.Up : NormalizeNormal( startSample.Normal );
			var chord = endSample.Position - startSample.Position;
			var forwardSource = chord.Length > 0.001f ? chord : startSample.Tangent;
			var forward = ResolveRunDirection( forwardSource, settings.KeepUpright, up );
			if ( forward.Length < 0.001f )
				break;

			plan.Segments.Add( new FencePlacementSegment
			{
				Prototype = chosen.Prototype,
				LinePoint = startSample.Position,
				SurfacePoint = startSample.Position,
				Direction = forward,
				Up = up,
				RandomRotationOffset = SampleRotationRandomization( definition, rng ),
				Length = chosen.Length,
				StartDistance = cursorDistance,
				EndDistance = endDistance
			} );

			cursorDistance = Math.Max( endDistance + SampleDistanceGap( definition, baseGap, rng ), cursorDistance + MinimumSegmentLength );
		}

		return plan;
	}

	internal static List<FenceSplineSample> BuildSplinePreviewPath( IReadOnlyList<FenceSplineControlPoint> controlPoints, int interpolationSteps )
	{
		var samples = new List<FenceSplineSample>();
		if ( controlPoints is null || controlPoints.Count == 0 )
			return samples;

		if ( controlPoints.Count == 1 )
		{
			var point = NormalizeSplinePoint( controlPoints[0] );
			samples.Add( new FenceSplineSample( point.Position, point.Normal ) );
			return samples;
		}

		var steps = Math.Clamp( interpolationSteps, 1, 32 );
		var first = NormalizeSplinePoint( controlPoints[0] );
		samples.Add( new FenceSplineSample( first.Position, first.Normal ) );

		for ( var index = 0; index < controlPoints.Count - 1; index++ )
		{
			var p0 = controlPoints[Math.Max( index - 1, 0 )];
			var p1 = controlPoints[index];
			var p2 = controlPoints[index + 1];
			var p3 = controlPoints[Math.Min( index + 2, controlPoints.Count - 1 )];

			for ( var step = 1; step <= steps; step++ )
			{
				var t = (float)step / steps;
				var position = CatmullRom( p0.Position, p1.Position, p2.Position, p3.Position, t );
				var normal = InterpolateNormal( p1.Normal, p2.Normal, t );

				if ( samples.Count > 0 && Vector3.DistanceBetween( samples[^1].Position, position ) < 0.001f )
					continue;

				samples.Add( new FenceSplineSample( position, normal ) );
			}
		}

		return samples;
	}

	internal static GameObject SpawnPreviewHierarchy( FencePlacementPlan plan )
	{
		if ( plan is null || plan.Segments.Count == 0 )
			return null;

		var root = new GameObject( true, $"{plan.RootName} Preview" );
		root.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.Hidden | GameObjectFlags.EditorOnly;
		root.Tags.Add( PreviewTag );

		foreach ( var segment in plan.Segments )
		{
			var placed = SpawnSegmentObject( segment.Prototype, previewOnly: true );
			if ( !placed.IsValid() )
				continue;

			ApplySegmentTransform( placed, segment );
			placed.SetParent( root, true );
			MarkHierarchyAsPreview( placed );
		}

		return root;
	}

	internal static GameObject CommitPlacementPlan( FencePlacementPlan plan )
	{
		if ( plan is null || plan.Segments.Count == 0 )
			return null;

		var root = new GameObject( true, string.IsNullOrWhiteSpace( plan.RootName ) ? "Fence" : plan.RootName );

		foreach ( var segment in plan.Segments )
		{
			var placed = SpawnSegmentObject( segment.Prototype, previewOnly: false );
			if ( !placed.IsValid() )
				continue;

			ApplySegmentTransform( placed, segment );
			placed.SetParent( root, true );

			if ( plan.CreateBlockerVolumes )
			{
				AddOrUpdateBlockerVolume( placed );
			}
		}

		return root;
	}

	internal static int AddOrUpdateBlockerVolumes( GameObject root )
	{
		if ( !root.IsValid() || IsDedicatedBlockerObject( root ) )
			return 0;

		var count = 0;
		foreach ( var target in ResolveBlockerTargets( root ) )
		{
			if ( AddOrUpdateBlockerVolume( target ) )
			{
				count++;
			}
		}

		return count;
	}

	internal static bool TryUpdatePreviewHierarchy( GameObject root, FencePlacementPlan plan )
	{
		if ( !root.IsValid() || plan is null )
			return false;

		if ( root.Children.Count != plan.Segments.Count )
			return false;

		for ( var index = 0; index < plan.Segments.Count; index++ )
		{
			var child = root.Children[index];
			var segment = plan.Segments[index];

			if ( !child.IsValid() || !string.Equals( child.Name, segment.Prototype.DisplayName, StringComparison.Ordinal ) )
				return false;
		}

		for ( var index = 0; index < plan.Segments.Count; index++ )
		{
			ApplySegmentTransform( root.Children[index], plan.Segments[index] );
		}

		return true;
	}

	internal static FenceProjectionRange MeasureProjectedRange( BBox bounds, Vector3 direction )
	{
		var normal = direction.Normal;
		var min = float.PositiveInfinity;
		var max = float.NegativeInfinity;

		foreach ( var corner in GetCorners( bounds ) )
		{
			var projection = Vector3.Dot( corner, normal );
			min = Math.Min( min, projection );
			max = Math.Max( max, projection );
		}

		if ( float.IsInfinity( min ) || float.IsInfinity( max ) )
			return new FenceProjectionRange( 0.0f, 0.0f );

		return new FenceProjectionRange( min, max );
	}

	internal static IEnumerable<Vector3> GetCorners( BBox bounds )
	{
		var mins = bounds.Mins;
		var maxs = bounds.Maxs;

		yield return new Vector3( mins.x, mins.y, mins.z );
		yield return new Vector3( mins.x, mins.y, maxs.z );
		yield return new Vector3( mins.x, maxs.y, mins.z );
		yield return new Vector3( mins.x, maxs.y, maxs.z );
		yield return new Vector3( maxs.x, mins.y, mins.z );
		yield return new Vector3( maxs.x, mins.y, maxs.z );
		yield return new Vector3( maxs.x, maxs.y, mins.z );
		yield return new Vector3( maxs.x, maxs.y, maxs.z );
	}

	internal static void DestroyHierarchy( GameObject root )
	{
		if ( root.IsValid() )
		{
			root.Destroy();
		}
	}

	private static FencePrototype BuildPrototype( FenceDefinitionEntry entry )
	{
		var displayName = ResolveDisplayName( entry );
		var tempObject = SpawnSegmentObject( entry, displayName, previewOnly: true );
		if ( !tempObject.IsValid() )
			return null;

		tempObject.WorldRotation = entry.RotationOffset.ToRotation();
		tempObject.WorldPosition = Vector3.Zero;

		var range = MeasureProjectedRange( tempObject.GetBounds(), Vector3.Forward );
		var canonicalLength = Math.Max( range.Length, 1.0f );

		tempObject.Destroy();

		return new FencePrototype
		{
			Entry = entry,
			DisplayName = displayName,
			Model = entry.Model,
			Prefab = entry.Prefab,
			RotationOffset = entry.RotationOffset,
			Weight = entry.Weight,
			CanonicalLength = canonicalLength
		};
	}

	private static FencePlacementCandidate PickCandidate( List<FencePlacementCandidate> candidates, bool useWeightedRandom, Random rng )
	{
		if ( candidates is null || candidates.Count == 0 )
			return null;

		if ( candidates.Count == 1 )
			return candidates[0];

		if ( !useWeightedRandom )
			return candidates[rng.Next( candidates.Count )];

		var totalWeight = 0.0f;
		foreach ( var candidate in candidates )
		{
			totalWeight += Math.Max( candidate.Prototype.Weight, 0.01f );
		}

		if ( totalWeight <= 0.0f )
			return candidates[rng.Next( candidates.Count )];

		var pick = (float)rng.NextDouble() * totalWeight;
		foreach ( var candidate in candidates )
		{
			pick -= Math.Max( candidate.Prototype.Weight, 0.01f );
			if ( pick <= 0.0f )
				return candidate;
		}

		return candidates[^1];
	}

	private static void TrySampleSurface( Scene scene, FenceToolSettings settings, Vector3 linePoint, out Vector3 surfacePoint, out Vector3 surfaceNormal )
	{
		surfacePoint = linePoint;
		surfaceNormal = Vector3.Up;

		if ( scene is null || !settings.SnapToGround )
			return;

		var traceHeight = Math.Max( settings.GroundTraceHeight, 1.0f );
		var traceStart = linePoint + Vector3.Up * traceHeight;
		var traceEnd = linePoint + Vector3.Down * traceHeight;

		var trace = scene.Trace.Ray( traceStart, traceEnd )
			.UseRenderMeshes( true )
			.UsePhysicsWorld( false )
			.WithoutTags( PreviewTag )
			.WithoutTags( "hidden" );

		var result = default(SceneTraceResult);
		if ( !string.IsNullOrWhiteSpace( settings.SurfaceTag ) )
		{
			result = trace.WithTag( settings.SurfaceTag ).Run();
		}

		if ( !result.Hit )
		{
			result = trace.Run();
		}

		if ( !result.Hit )
			return;

		surfacePoint = result.HitPosition;
		surfaceNormal = result.Normal;
	}

	private static Vector3 ResolveRunDirection( Vector3 direction, bool keepUpright, Vector3 up )
	{
		if ( keepUpright )
		{
			var flat = new Vector3( direction.x, direction.y, 0.0f );
			return flat.Length < 0.001f ? Vector3.Forward : flat.Normal;
		}

		var projected = direction - up * Vector3.Dot( direction, up );
		return projected.Length < 0.001f ? Vector3.Forward : projected.Normal;
	}

	private static float MeasureSplineLength( IReadOnlyList<FenceSplineSample> samples )
	{
		if ( samples is null || samples.Count < 2 )
			return 0.0f;

		var length = 0.0f;
		for ( var index = 1; index < samples.Count; index++ )
		{
			length += Vector3.DistanceBetween( samples[index - 1].Position, samples[index].Position );
		}

		return length;
	}

	private static FenceSplineDistanceSample SampleSplineAtDistance( IReadOnlyList<FenceSplineSample> samples, float distance )
	{
		if ( samples is null || samples.Count == 0 )
			return new FenceSplineDistanceSample( Vector3.Zero, Vector3.Up, Vector3.Forward );

		if ( samples.Count == 1 )
			return new FenceSplineDistanceSample( samples[0].Position, NormalizeNormal( samples[0].Normal ), Vector3.Forward );

		var remaining = Math.Max( distance, 0.0f );
		var lastTangent = Vector3.Forward;

		for ( var index = 1; index < samples.Count; index++ )
		{
			var previous = samples[index - 1];
			var current = samples[index];
			var delta = current.Position - previous.Position;
			var segmentLength = delta.Length;
			if ( segmentLength <= 0.001f )
				continue;

			lastTangent = delta.Normal;

			if ( remaining <= segmentLength )
			{
				var t = remaining / segmentLength;
				var position = previous.Position + delta * t;
				var normal = InterpolateNormal( previous.Normal, current.Normal, t );
				return new FenceSplineDistanceSample( position, normal, lastTangent );
			}

			remaining -= segmentLength;
		}

		var last = samples[^1];
		return new FenceSplineDistanceSample( last.Position, NormalizeNormal( last.Normal ), lastTangent );
	}

	private static FenceSplineControlPoint NormalizeSplinePoint( FenceSplineControlPoint point )
	{
		return new FenceSplineControlPoint( point.Position, NormalizeNormal( point.Normal ) );
	}

	private static Vector3 NormalizeNormal( Vector3 normal )
	{
		return normal.Length < 0.001f ? Vector3.Up : normal.Normal;
	}

	private static Vector3 InterpolateNormal( Vector3 start, Vector3 end, float t )
	{
		var normal = NormalizeNormal( start ) + (NormalizeNormal( end ) - NormalizeNormal( start )) * Math.Clamp( t, 0.0f, 1.0f );
		return NormalizeNormal( normal );
	}

	private static Vector3 CatmullRom( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t )
	{
		var t2 = t * t;
		var t3 = t2 * t;

		return (p1 * 2.0f
			+ (p2 - p0) * t
			+ (p0 * 2.0f - p1 * 5.0f + p2 * 4.0f - p3) * t2
			+ (Vector3.Zero - p0 + p1 * 3.0f - p2 * 3.0f + p3) * t3) * 0.5f;
	}

	private static GameObject SpawnSegmentObject( FencePrototype prototype, bool previewOnly )
	{
		if ( prototype is null )
			return null;

		return SpawnSegmentObject( prototype.Entry, prototype.DisplayName, previewOnly );
	}

	private static GameObject SpawnSegmentObject( FenceDefinitionEntry entry, string displayName, bool previewOnly )
	{
		if ( entry?.Prefab is not null )
		{
			var prefabScene = SceneUtility.GetPrefabScene( entry.Prefab );
			var clone = prefabScene?.Clone();
			if ( clone is null )
				return null;

			clone.Name = displayName;
			if ( previewOnly )
			{
				MarkHierarchyAsPreview( clone );
			}

			return clone;
		}

		if ( entry?.Model is null )
			return null;

		var root = new GameObject( true, displayName );
		if ( previewOnly )
		{
			MarkHierarchyAsPreview( root );
		}

		if ( !previewOnly && (entry.Model.Physics?.Parts.Count ?? 0) > 0 )
		{
			var prop = root.AddComponent<Prop>();
			prop.Model = entry.Model;
			prop.IsStatic = ResolveModelIsStatic( entry.Model );
		}
		else if ( entry.Model.BoneCount > 0 )
		{
			var skinnedRenderer = root.AddComponent<SkinnedModelRenderer>();
			skinnedRenderer.PlayAnimationsInEditorScene = true;
			skinnedRenderer.Model = entry.Model;
		}
		else
		{
			var renderer = root.AddComponent<ModelRenderer>();
			renderer.Model = entry.Model;
		}

		return root;
	}

	private static bool ResolveModelIsStatic( Model model )
	{
		if ( model is null || string.IsNullOrWhiteSpace( model.ResourcePath ) )
			return false;

		var asset = AssetSystem.FindByPath( model.ResourcePath );
		var archetype = asset?.FindStringEditInfo( "model_archetype_id" );
		return string.Equals( archetype, "static_prop_model", StringComparison.Ordinal );
	}

	private static void ApplySegmentTransform( GameObject placed, FencePlacementSegment segment )
	{
		if ( !placed.IsValid() || segment is null )
			return;

		var rotationOffset = CombineAngles( segment.Prototype.RotationOffset, segment.RandomRotationOffset );
		placed.WorldRotation = Rotation.LookAt( segment.Direction, segment.Up ) * rotationOffset.ToRotation();
		placed.WorldPosition = segment.SurfacePoint;

		var forwardRange = MeasureProjectedRange( placed.GetBounds(), segment.Direction );
		var targetStart = Vector3.Dot( segment.LinePoint, segment.Direction );
		placed.WorldPosition += segment.Direction * (targetStart - forwardRange.Min);

		var upRange = MeasureProjectedRange( placed.GetBounds(), segment.Up );
		var targetBottom = Vector3.Dot( segment.SurfacePoint, segment.Up );
		placed.WorldPosition += segment.Up * (targetBottom - upRange.Min);
	}

	internal static bool AddOrUpdateBlockerVolume( GameObject segmentRoot )
	{
		if ( !segmentRoot.IsValid() || !TryGetBlockerLocalBounds( segmentRoot, out var localBounds ) )
			return false;

		RemoveDirectBlockerObjects( segmentRoot );

		var area = segmentRoot.Components.Get<NavMeshArea>( FindMode.EverythingInSelf ) ?? segmentRoot.Components.Create<NavMeshArea>();
		area.SceneVolume = new SceneVolume
		{
			Type = SceneVolume.VolumeTypes.Box,
			Box = ClampBlockerBounds( localBounds )
		};
		area.IsBlocker = true;
		area.Enabled = true;
		return true;
	}

	private static IEnumerable<GameObject> ResolveBlockerTargets( GameObject root )
	{
		if ( !root.IsValid() || IsDedicatedBlockerObject( root ) )
			yield break;

		if ( HasOwnSegmentContent( root ) || HasOwnBlockerArea( root ) || root.Parent.IsValid() )
		{
			yield return root;
			yield break;
		}

		var yieldedChild = false;
		foreach ( var child in root.Children )
		{
			if ( !child.IsValid() || IsDedicatedBlockerObject( child ) )
				continue;

			yieldedChild = true;
			yield return child;
		}

		if ( !yieldedChild )
		{
			yield return root;
		}
	}

	private static bool TryGetBlockerLocalBounds( GameObject segmentRoot, out BBox localBounds )
	{
		localBounds = default;
		var hasBounds = false;

		foreach ( var gameObject in EnumerateHierarchy( segmentRoot ) )
		{
			if ( IsDedicatedBlockerObject( gameObject ) )
				continue;

			foreach ( var collider in gameObject.Components.GetAll<Collider>( FindMode.EverythingInSelf ) )
			{
				if ( collider is null )
					continue;

				var bounds = ConvertLocalBoundsToLocal( collider.LocalBounds, collider.GameObject.WorldTransform, segmentRoot.WorldTransform );
				localBounds = hasBounds ? localBounds.AddBBox( bounds ) : bounds;
				hasBounds = true;
			}
		}

		if ( hasBounds )
			return true;

		localBounds = ConvertWorldBoundsToLocal( segmentRoot.GetBounds(), segmentRoot.WorldTransform );
		return localBounds.Size.Length > 0.001f;
	}

	private static bool HasOwnSegmentContent( GameObject gameObject )
	{
		return gameObject.Components.Get<Collider>( FindMode.EverythingInSelf ) is not null
			|| gameObject.Components.Get<ModelRenderer>( FindMode.EverythingInSelf ) is not null
			|| gameObject.Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelf ) is not null
			|| gameObject.Components.Get<Prop>( FindMode.EverythingInSelf ) is not null;
	}

	private static bool HasOwnBlockerArea( GameObject gameObject )
	{
		return gameObject.Components.Get<NavMeshArea>( FindMode.EverythingInSelf ) is not null;
	}

	private static void RemoveDirectBlockerObjects( GameObject segmentRoot )
	{
		foreach ( var child in segmentRoot.Children.ToArray() )
		{
			if ( IsDedicatedBlockerObject( child ) )
			{
				child.Destroy();
			}
		}
	}

	private static bool IsDedicatedBlockerObject( GameObject gameObject )
	{
		return gameObject.IsValid() && string.Equals( gameObject.Name, "NavBlocker", StringComparison.Ordinal );
	}

	private static BBox ConvertLocalBoundsToLocal( BBox sourceBounds, Transform sourceSpace, Transform targetSpace )
	{
		var bounds = default(BBox);
		var hasPoint = false;

		foreach ( var corner in GetCorners( sourceBounds ) )
		{
			var targetPoint = targetSpace.PointToLocal( sourceSpace.PointToWorld( corner ) );
			if ( !hasPoint )
			{
				bounds = BBox.FromPositionAndSize( targetPoint, 0.0f );
				hasPoint = true;
			}
			else
			{
				bounds = bounds.AddPoint( targetPoint );
			}
		}

		return bounds;
	}

	private static BBox ConvertWorldBoundsToLocal( BBox worldBounds, Transform localSpace )
	{
		var bounds = default(BBox);
		var hasPoint = false;

		foreach ( var corner in GetCorners( worldBounds ) )
		{
			var localPoint = localSpace.PointToLocal( corner );
			if ( !hasPoint )
			{
				bounds = BBox.FromPositionAndSize( localPoint, 0.0f );
				hasPoint = true;
			}
			else
			{
				bounds = bounds.AddPoint( localPoint );
			}
		}

		return bounds;
	}

	private static BBox ClampBlockerBounds( BBox bounds )
	{
		var sourceSize = bounds.Size;
		var size = new Vector3(
			Math.Max( sourceSize.x, 1.0f ),
			Math.Max( sourceSize.y, 1.0f ),
			Math.Max( sourceSize.z, 1.0f ) );

		return BBox.FromPositionAndSize( bounds.Center, size );
	}

	private static void MarkHierarchyAsPreview( GameObject root )
	{
		foreach ( var gameObject in EnumerateHierarchy( root ) )
		{
			gameObject.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.Hidden | GameObjectFlags.EditorOnly;
			gameObject.Tags.Add( PreviewTag );
		}
	}

	private static IEnumerable<GameObject> EnumerateHierarchy( GameObject root )
	{
		if ( !root.IsValid() )
			yield break;

		yield return root;

		foreach ( var child in root.Children )
		{
			foreach ( var descendant in EnumerateHierarchy( child ) )
			{
				yield return descendant;
			}
		}
	}

	private static string ResolveDisplayName( FenceDefinitionEntry entry )
	{
		if ( entry is null )
			return "Fence Segment";

		if ( !string.IsNullOrWhiteSpace( entry.DisplayName ) )
			return entry.DisplayName.Trim();

		if ( entry.Prefab is not null )
			return System.IO.Path.GetFileNameWithoutExtension( entry.Prefab.ResourcePath );

		if ( entry.Model is not null )
			return System.IO.Path.GetFileNameWithoutExtension( entry.Model.ResourcePath );

		return "Fence Segment";
	}

	private static Angles SampleRotationRandomization( FenceDefinition definition, Random rng )
	{
		if ( definition is null )
			return Angles.Zero;

		return new Angles(
			SampleRange( definition.RotationRandomizationMin.pitch, definition.RotationRandomizationMax.pitch, rng ),
			SampleRange( definition.RotationRandomizationMin.yaw, definition.RotationRandomizationMax.yaw, rng ),
			SampleRange( definition.RotationRandomizationMin.roll, definition.RotationRandomizationMax.roll, rng ) );
	}

	private static float SampleDistanceGap( FenceDefinition definition, float baseGap, Random rng )
	{
		if ( definition is null )
			return baseGap;

		return baseGap + SampleRange( definition.DistanceRandomizationMin, definition.DistanceRandomizationMax, rng );
	}

	private static float SampleRange( float a, float b, Random rng )
	{
		var min = Math.Min( a, b );
		var max = Math.Max( a, b );
		if ( MathF.Abs( max - min ) < 0.001f )
			return min;

		return min + (max - min) * (float)rng.NextDouble();
	}

	private static Angles CombineAngles( Angles a, Angles b )
	{
		return new Angles(
			a.pitch + b.pitch,
			a.yaw + b.yaw,
			a.roll + b.roll );
	}
}