Editor/Output/ArchCollision.cs
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public enum ArchCollisionMode {
	None,
	Solids,
	Convex,
	Complex
}

public interface IArchCollides {
	ArchCollisionMode? Collision { get; set; }
}

public static class ArchCollision {
	// Resolves: part → inherited up the layer stack → scope → kit
	public static ArchCollisionMode Resolve( ArchBuiltPart part, ArchKit kit, ArchLayerTree layers ) {
		var wanted = part.Asked ?? Inherited( part.Source, layers ) ?? part.Scope ?? kit?.Physics ?? ArchCollisionMode.Solids;

		if ( wanted != ArchCollisionMode.Solids ) {
			return wanted;
		}

		// Solids only valid when the generator fully described the part with them
		if ( part.Canvas.Describes && part.Canvas.Solids.Any( solid => solid.IsUsable ) ) {
			return ArchCollisionMode.Solids;
		}

		return ArchCollisionMode.Complex;
	}

	static ArchCollisionMode? Inherited( object source, ArchLayerTree layers ) {
		return source is not null && layers?.Find( source ) is { } node ? Above( node ) : null;
	}

	public static ArchCollisionMode? Above( ArchLayerNode node ) {
		for ( var above = node?.Parent; above is not null; above = above.Parent ) {
			if ( above.Payload is IArchCollides { Collision: { } inherited } ) {
				return inherited;
			}
		}

		return null;
	}

	public static ArchCollisionMode Showing( ArchLayerNode node, ArchKit kit, out bool own ) {
		var mine = (node?.Payload as IArchCollides)?.Collision;

		own = mine.HasValue;

		return mine ?? Above( node ) ?? kit?.Physics ?? ArchCollisionMode.Solids;
	}

	public static void Write( GameObject node, MeshComponent renderer, ArchMesh canvas, ArchCollisionMode mode ) {
		renderer.Collision = mode switch {
			ArchCollisionMode.Complex => MeshComponent.CollisionType.Mesh,
			ArchCollisionMode.Convex => MeshComponent.CollisionType.Hull,
			_ => MeshComponent.CollisionType.None
		};

		Clear( node );

		if ( mode != ArchCollisionMode.Solids ) {
			return;
		}

		foreach ( var solid in canvas.Solids.Where( solid => solid.IsUsable ) ) {
			Shape( node, solid );
		}
	}

	static void Clear( GameObject node ) {
		foreach ( var collider in node.Components.GetAll<HullCollider>( FindMode.EverythingInSelf ).ToList() ) {
			collider.Destroy();
		}
	}

	static void Shape( GameObject node, ArchSolid solid ) {
		var collider = node.Components.Create<HullCollider>();

		switch ( solid.Form ) {
			case ArchSolidForm.Box:
				collider.Type = HullCollider.PrimitiveType.Box;
				collider.Center = solid.Centre;
				collider.BoxSize = solid.Size;
				break;

			case ArchSolidForm.Cylinder:
				collider.Type = HullCollider.PrimitiveType.Cylinder;
				collider.Center = solid.Centre;
				collider.Radius = solid.Radius;
				collider.Height = solid.Height;
				collider.Slices = solid.Slices;
				break;

			default:
				collider.Type = HullCollider.PrimitiveType.Points;
				collider.Points = solid.Points.ToList();
				break;
		}
	}

	public static string Describe( ArchCollisionMode mode ) => mode switch {
		ArchCollisionMode.None => "No collision — nothing to walk into",
		ArchCollisionMode.Solids => "Convex pieces — one shape per solid the generator made",
		ArchCollisionMode.Convex => "One hull round the whole part — fills its holes",
		_ => "Exact mesh — every triangle, the expensive one"
	};

	public static string Glyph( ArchCollisionMode mode ) => mode switch {
		ArchCollisionMode.None => "block",
		ArchCollisionMode.Solids => "view_module",
		ArchCollisionMode.Convex => "crop_square",
		_ => "grain"
	};
}