Editor/Output/ArchDrawers.cs

Editor helper types and registry for architecture drawing and filing. Defines ArchDrawing and ArchFiling context holders, role name lists, interfaces IArchDrawer/IArchFiler, and a loader/registry ArchDrawers that enrolls concrete drawer/filer implementations and exposes lookup and exclusion helpers.

Reflection
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// What one generator is handed when the builder asks it to draw: the canvas to fill, the layer payload it was
// asked about, the part being filed, and the ancestry it stands in. A generator reaches for what it needs by
// type, so the builder can hand a roof deck's connections to the wall generator without a signature saying so.
public sealed class ArchDrawing
{
	public ArchMesh Canvas { get; init; }
	public object Subject { get; init; }
	public ArchBuiltPart Part { get; init; }
	public ArchGenerationServices Services { get; init; }
	public IReadOnlyList<object> Hosts { get; init; } = Array.Empty<object>();

	public ArchPlan Plan => Services.Plan;
	public ArchKit Kit => Services.Kit;
	public ArchStyle Style => Services.Style;

	public T Drawn<T>() => Subject is T subject ? subject : default;

	public T Within<T>() => Hosts.OfType<T>().FirstOrDefault();
}

// What a generator that files its OWN parts is handed: the output service, the path to file under, and the same
// subject and ancestry a canvas drawer gets. A road comes out as an apron, a kerb and a line - many parts from
// one layer - so it opens them itself rather than filling a canvas the builder files.
public sealed class ArchFiling
{
	public ArchOutputService Output { get; init; }
	public string Path { get; init; }
	public object Subject { get; init; }
	public ArchGenerationServices Services { get; init; }
	public IReadOnlyList<object> Hosts { get; init; } = Array.Empty<object>();

	public ArchPlan Plan => Services.Plan;
	public ArchKit Kit => Services.Kit;
	public ArchStyle Style => Services.Style;

	public T Drawn<T>() => Subject is T subject ? subject : default;

	public T Within<T>() => Hosts.OfType<T>().FirstOrDefault();
}

static class ArchRoleNames
{
	public static IReadOnlyList<string> Of( Type holder )
	{
		return holder.GetFields()
			.Where( field => field.IsLiteral && field.FieldType == typeof( string ) )
			.Select( field => (string)field.GetRawConstantValue() )
			.ToList();
	}
}

// The names the builder asks a canvas by. A role is not a payload type: a room is drawn as an overhang, as a
// rising shell and as boards, and a floor group is drawn as both a slab and a footing.
public static class ArchDraws
{
	public const string Storey = "floor.storey";
	public const string Foundation = "floor.foundation";
	public const string Wall = "wall";
	public const string WalkwayRising = "walkway.rising";
	public const string WalkwayBoards = "walkway.boards";
	public const string Roof = "roof";
	public const string Downpipe = "downpipe";
	public const string Pipe = "pipe";
	public const string Bracket = "bracket";
	public const string Ladder = "ladder";
	public const string Balcony = "balcony";
	public const string ExteriorStair = "stair.exterior";
	public const string Fence = "fence";
	public const string Cut = "cut";
	public const string Platform = "platform";
	public const string Overhang = "overhang";
	public const string Pillar = "pillar";
	public const string Span = "span";
	public const string Beam = "beam";
	public const string Stair = "stair";
	public const string Trim = "trim";
	public const string Porch = "porch";
	public const string Approach = "approach";

	public static IReadOnlyList<string> All { get; } = ArchRoleNames.Of( typeof( ArchDraws ) );
}

// The names the builder asks a whole set of parts by.
public static class ArchFiles
{
	public const string Road = "road";
	public const string Junction = "junction";
	public const string GutterChannel = "gutter.channel";

	public static IReadOnlyList<string> All { get; } = ArchRoleNames.Of( typeof( ArchFiles ) );
}

// One kind of thing the builder can be asked to draw. The implementation lives beside the generator it calls, so
// reading what a subtool draws never means reading the builder.
public interface IArchDrawer
{
	string Draws { get; }

	void Draw( ArchDrawing drawing );
}

public interface IArchFiler
{
	string Files { get; }

	void File( ArchFiling filing );
}

// Never cached across builds: hotload carries statics over, and a stale table would draw a scene with the
// generator code the editor had before the edit.
public sealed class ArchDrawers
{
	readonly Dictionary<string, IArchDrawer> drawers = new();
	readonly Dictionary<string, IArchFiler> filers = new();

	public static ArchDrawers Load()
	{
		var built = new ArchDrawers();

		built.Enrol( new ArchWallDrawer() );
		built.Enrol( new ArchTrimDrawer() );
		built.Enrol( new ArchBalconyDrawer() );
		built.Enrol( new ArchLadderDrawer() );
		built.Enrol( new ArchStoreyDrawer() );
		built.Enrol( new ArchFoundationDrawer() );
		built.Enrol( new ArchRoofDrawer() );
		built.Enrol( new ArchDownpipeDrawer() );
		built.Enrol( new ArchStairDrawer() );
		built.Enrol( new ArchApproachDrawer() );
		built.Enrol( new ArchExteriorStairDrawer() );
		built.Enrol( new ArchPillarDrawer() );
		built.Enrol( new ArchSpanDrawer() );
		built.Enrol( new ArchPlatformDrawer() );
		built.Enrol( new ArchBeamDrawer() );
		built.Enrol( new ArchCutDrawer() );
		built.Enrol( new ArchFenceDrawer() );
		built.Enrol( new ArchPipeDrawer() );
		built.Enrol( new ArchBracketDrawer() );
		built.Enrol( new ArchPorchDrawer() );
		built.Enrol( new ArchOverhangDrawer() );
		built.Enrol( new ArchWalkwayRisingDrawer() );
		built.Enrol( new ArchWalkwayBoardsDrawer() );

		built.Enrol( new ArchGutterChannelFiler() );
		built.Enrol( new ArchRoadFiler() );
		built.Enrol( new ArchJunctionFiler() );

		return built;
	}

	public ArchDrawers Enrol( IArchDrawer drawer )
	{
		drawers[drawer.Draws] = drawer;

		return this;
	}

	public ArchDrawers Enrol( IArchFiler filer )
	{
		filers[filer.Files] = filer;

		return this;
	}

	// The same table with one role gone, which is the negative control a drawer's absence is proved against.
	public ArchDrawers Except( string role )
	{
		var built = new ArchDrawers();

		foreach ( var enrolled in drawers.Where( entry => entry.Key != role ) )
		{
			built.drawers[enrolled.Key] = enrolled.Value;
		}

		foreach ( var enrolled in filers.Where( entry => entry.Key != role ) )
		{
			built.filers[enrolled.Key] = enrolled.Value;
		}

		return built;
	}

	// A role nothing answers draws nothing rather than erroring, which is what a negative control stands on.
	public IArchDrawer For( string role ) => drawers.TryGetValue( role, out var drawer ) ? drawer : null;

	public IArchFiler Filing( string role ) => filers.TryGetValue( role, out var filer ) ? filer : null;

	public IEnumerable<string> Roles => drawers.Keys.Concat( filers.Keys );
}