Editor/Output/ArchArchitectureBuilder.cs

Editor builder for architectural output. Walks a plan, resolves connections, and invokes drawer/filer components to produce parts and root transforms for buildings, roads, rooms and other architectural elements.

File AccessNetworking
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchArchitectureBuild
{
	public IReadOnlyList<ArchBuiltPart> Parts { get; init; } = Array.Empty<ArchBuiltPart>();
	public IReadOnlyDictionary<string, Transform> RootTransforms { get; init; } = new Dictionary<string, Transform>();

	// Object-path segments of the scopes this build did not touch. Their parts are still standing and still
	// correct, so nothing here describes them - the cache remembers what they are, and the prune must not read
	// their absence from this list as a deletion.
	public IReadOnlyList<string> Standing { get; init; } = Array.Empty<string>();
}

// The story of one rebuild: resolve the plan's connections, then walk every building and every road, asking
// the drawer for each thing it stands on to fill a canvas. Nothing here names a generator - a role nobody answers
// simply builds nothing.
public sealed class ArchArchitectureBuilder
{
	readonly ArchGenerationServices services;
	readonly ArchOutputService output;
	readonly Dictionary<string, Transform> rootTransforms = new();
	readonly bool worldSpace;

	ArchDrawers drawers;
	ArchAnswers answers;
	ArchBuildGate gate;

	public ArchArchitectureBuilder( ArchPlan plan, ArchKit kit, Scene scene = null, ArchGenerationServices services = null )
	{
		this.services = services ?? new ArchGenerationServices( plan, kit, scene );
		output = new ArchOutputService( this.services.Plan );
		drawers = ArchDrawers.Load();
		answers = ArchAnswers.Load();
		worldSpace = scene is null;
	}

	// The generation gate for the whole layer system: a disabled layer draws nothing.
	static bool On( object payload ) => ArchLayerGate.On( payload );

	// Draws with a hand-picked table instead of the written-out one, which is the only way to stand in front of
	// the tool as it looks with one role unanswered.
	public ArchArchitectureBuilder Drawing( ArchDrawers with )
	{
		drawers = with;

		return this;
	}

	// The same, for the questions the builder asks between drawings - what a building's grade is, what storeys it
	// stands, whether a room is a link.
	public ArchArchitectureBuilder Answering( ArchAnswers with )
	{
		answers = with;

		return this;
	}

	// A scope nothing could have touched is not generated at all - the parts it made last build are still standing
	// and still correct, so the cheapest thing the generator can do about them is nothing.
	public ArchArchitectureBuilder Through( ArchBuildGate through )
	{
		gate = through;

		return this;
	}

	// The object-path segment of each scope left standing, which is the name the scene and the cache agree on -
	// never the plan id, because a junction's node name parses as a road id and would be filed under it.
	readonly List<string> skipped = new();

	bool Standing( int itemId, string segment )
	{
		if ( gate is null || !gate.Skips( itemId ) )
		{
			return false;
		}

		skipped.Add( segment );

		return true;
	}

	public ArchArchitectureBuilder WithConnections()
	{
		services.ResolveConnections();

		return this;
	}

	public ArchArchitectureBuilder WithBuildings()
	{
		foreach ( var building in services.Plan.Buildings )
		{
			if ( On( building ) && !Standing( building.Id, ArchNames.Building( building ) ) )
			{
				BuildBuilding( building );
			}
		}

		return this;
	}

	public ArchArchitectureBuilder WithRoads()
	{
		if ( !services.ConnectionsResolved )
		{
			WithConnections();
		}

		foreach ( var road in services.Plan.Roads() )
		{
			if ( !On( road ) || Standing( road.Id, ArchNames.Road( road ) ) )
			{
				continue;
			}

			File( ArchNames.Road( road ), road, ArchFiles.Road );

			DrawShafts( ArchNames.Road( road ), road.Cuts, road.Palette );
		}

		foreach ( var junction in services.Connections.Junctions )
		{
			File( junction.Name, junction.Subject, ArchFiles.Junction );
		}

		return this;
	}

	// The contact pass is NOT run here any more: it decides what survives by comparing coplanar neighbours, so
	// whether it has to run at all is a question about which parts moved - and that is ArchBuildCache's.
	public ArchArchitectureBuild Create()
	{
		var transforms = new Dictionary<string, Transform>( rootTransforms );

		// A part that put its whole body into named pieces files no mesh of its own, so the node those pieces
		// hang under takes its frame from here or they are drawn in the wrong space.
		foreach ( var frame in output.Frames )
		{
			transforms[frame.Key] = frame.Value;
		}

		return new ArchArchitectureBuild
		{
			Parts = output.Create(),
			RootTransforms = transforms,
			Standing = skipped.ToList()
		};
	}

	// The one place a generator is reached. A role nothing answers draws nothing at all, rather than filing an
	// empty node the cache would read as a part that vanished.
	void Draw( string path, Transform nodeTransform, Transform space, object subject, bool welded, string role, params object[] hosts )
	{
		if ( drawers.For( role ) is not IArchDrawer drawer )
		{
			return;
		}

		output.Draw( path, nodeTransform, space, subject, welded, ( canvas, source, part ) => drawer.Draw( new ArchDrawing
		{
			Canvas = canvas,
			Subject = source,
			Part = part,
			Services = services,
			Hosts = hosts
		} ) );
	}

	// The same, for a generator that opens more than one part out of a single layer and so files its own.
	void File( string path, object subject, string role, params object[] hosts )
	{
		if ( drawers.Filing( role ) is not IArchFiler filer )
		{
			return;
		}

		filer.File( new ArchFiling
		{
			Output = output,
			Path = path,
			Subject = subject,
			Services = services,
			Hosts = hosts
		} );
	}

	void BuildBuilding( ArchBuilding building )
	{
		var root = ArchNames.Building( building );
		var lift = answers.Lift( services.Plan, building, services.Kit );
		rootTransforms[root] = new Transform( Vector3.Up * lift );

		// The group holding it first, then the unit's own answer over the top - the same scope order everything else
		// in the stack reads.
		using var held = output.Within( ArchLayerGroups.Holding( services.Plan, building.Id )?.Collision );
		using var collision = output.Within( building.Collision );

		DrawStoreys( root, building );

		foreach ( var room in building.Rooms )
		{
			if ( On( room ) )
			{
				DrawRoom( root, room, building );
			}
		}

		DrawRoofs( root, building );
		DrawDownpipes( root, building );
		DrawPipeRuns( root, building );
		DrawBrackets( root, building );
		DrawLadders( root, building );
		DrawBalconies( root, building );
		DrawExteriorStairs( root, building );
		DrawFences( root, building, lift );
		DrawShafts( root, building.Cuts, building.Palette, BuildingSpace( building ) );
		DrawPlatforms( root, building, lift );
	}

	void DrawRoom( string root, ArchRoom room, ArchBuilding building )
	{
		var roomPath = $"{root}/{ArchNames.Room( room )}";

		using var collision = output.Within( room.Collision );

		DrawOverhang( roomPath, room, building );
		DrawShell( roomPath, room, building );
		DrawPillars( roomPath, room, building );
		DrawSpans( roomPath, room, building );
		DrawBeams( roomPath, room, building );
		DrawStairs( roomPath, room, building );
		DrawTrims( roomPath, room, building );
		DrawPorches( roomPath, room, building );
		DrawApproaches( roomPath, room, building );
	}

	void DrawStoreys( string root, ArchBuilding building )
	{
		foreach ( var storey in answers.Storeys( building, services.Kit, services.Style ) )
		{
			var floor = $"{root}/{storey.Name}";

			Draw( $"{floor}/Slab", Transform.Zero, BuildingSpace( building ), storey.Subject, false, ArchDraws.Storey, building );
			Draw( $"{floor}/Foundation", Transform.Zero, BuildingSpace( building ), storey.Subject, false, ArchDraws.Foundation, building );
		}
	}

	// Only a rising link draws its own walls - they rake between the mouths, which no vertical wall
	// generator can say. A flat link's passage walls are ordinary walls, so they take the ordinary path
	// and everything an opening wears comes with them.
	void DrawShell( string roomPath, ArchRoom room, ArchBuilding building )
	{
		if ( answers.Rising( room ) )
		{
			DrawRakedShell( roomPath, room, building );

			return;
		}

		foreach ( var wall in room.Walls.Where( wall => !ArchWallJoins.CoveredBy( building, room, wall ) && On( wall ) ) )
		{
			var frame = new Transform( new Vector3( wall.Start.x, wall.Start.y, room.BaseHeight ), Rotation.FromYaw( ArchWallJoins.AngleDegrees( wall ) ) );

			Draw( $"{roomPath}/{ArchNames.Wall( wall )}", frame, WallSpace( building, frame ), wall, true, ArchDraws.Wall, room, building );
		}

		if ( answers.IsWalkway( room ) )
		{
			DrawWalkwayBoards( roomPath, room, building );
		}
	}

	void DrawRakedShell( string roomPath, ArchRoom room, ArchBuilding building )
	{
		Draw( $"{roomPath}/WalkwayShell", Transform.Zero, BuildingSpace( building ), room, false, ArchDraws.WalkwayRising, building );
	}

	// The link's own boards, drawn apart from its walls so they die square at the mouth corners.
	void DrawWalkwayBoards( string roomPath, ArchRoom room, ArchBuilding building )
	{
		Draw( $"{roomPath}/WalkwayBoards", Transform.Zero, BuildingSpace( building ), room, false, ArchDraws.WalkwayBoards, building );
	}

	// The channel hangs on the eave that feeds it, so it is filed beside the fascia and soffit of ITS roof rather
	// than in one heap at the foot of the house - a building with two roofs had one Gutters node for both.
	void DrawRoofs( string root, ArchBuilding building )
	{
		foreach ( var roof in building.Roofs )
		{
			if ( !On( roof ) )
			{
				continue;
			}

			var runs = new List<ArchRunPath>();
			var roofPath = $"{root}/{ArchNames.Roof( roof )}";

			Draw( roofPath, Transform.Zero, BuildingSpace( building ), roof, false, ArchDraws.Roof,
				building, building.GuttersEnabled ? runs : null );

			DrawGutterChannel( roofPath, runs, building );
			DrawRoofWalls( roofPath, roof, building );
		}
	}

	// A parapet that varies, a party wall between two rooftops: an ordinary wall through the ordinary generator, so
	// everything a wall wears comes with it. The host room and its joins are ArchRoofWalls' single answer - there is
	// no room on a deck, and a second stand-in made here would disagree with the ghost about the seat.
	void DrawRoofWalls( string roofPath, ArchRoofPart roof, ArchBuilding building )
	{
		if ( roof.Walls.Count == 0 )
		{
			return;
		}

		var deck = ArchRoofWalls.Deck( roof, services.Kit );

		foreach ( var wall in roof.Walls )
		{
			if ( !On( wall ) )
			{
				continue;
			}

			var frame = new Transform( new Vector3( wall.Start.x, wall.Start.y, deck.Seat ), Rotation.FromYaw( ArchWallJoins.AngleDegrees( wall ) ) );

			Draw( $"{roofPath}/{ArchNames.Wall( wall )}", frame, WallSpace( building, frame ), wall, true, ArchDraws.Wall,
				deck.Room, building, deck.Connections );
		}
	}

	void DrawGutterChannel( string roofPath, List<ArchRunPath> runs, ArchBuilding building )
	{
		if ( runs.Count == 0 )
		{
			return;
		}

		File( roofPath, runs, ArchFiles.GutterChannel, building, BuildingSpace( building ) );
	}

	void DrawDownpipes( string root, ArchBuilding building )
	{
		foreach ( var pipe in building.Downpipes )
		{
			if ( !On( pipe ) )
			{
				continue;
			}

			Draw( $"{root}/{ArchNames.Downpipe( pipe )}", Transform.Zero, BuildingSpace( building ), pipe, false,
				ArchDraws.Downpipe, building );
		}
	}

	// The hangers go in AFTER the runs, because a bracket volume reads the lane paths the runs resolve - and
	// both resolve through their own shape rather than off anything the other one built, so the order here is
	// only the order the rows read in, never a dependency.
	void DrawPipeRuns( string root, ArchBuilding building )
	{
		foreach ( var run in building.Pipes )
		{
			if ( !On( run ) )
			{
				continue;
			}

			Draw( $"{root}/{ArchNames.Pipe( run )}", Transform.Zero, BuildingSpace( building ), run, false,
				ArchDraws.Pipe, building );
		}
	}

	void DrawBrackets( string root, ArchBuilding building )
	{
		foreach ( var bracket in building.Brackets )
		{
			if ( !On( bracket ) )
			{
				continue;
			}

			Draw( $"{root}/{ArchNames.Bracket( bracket )}", Transform.Zero, BuildingSpace( building ), bracket, false,
				ArchDraws.Bracket, building );
		}
	}

	void DrawLadders( string root, ArchBuilding building )
	{
		foreach ( var ladder in building.Ladders )
		{
			if ( !On( ladder ) )
			{
				continue;
			}

			Draw( $"{root}/{ArchNames.Ladder( ladder )}", Transform.Zero, BuildingSpace( building ), ladder, false,
				ArchDraws.Ladder, building );
		}
	}

	void DrawBalconies( string root, ArchBuilding building )
	{
		foreach ( var balcony in building.Balconies )
		{
			if ( !On( balcony ) )
			{
				continue;
			}

			Draw( $"{root}/{ArchNames.Balcony( balcony )}", Transform.Zero, BuildingSpace( building ), balcony, false,
				ArchDraws.Balcony, building );
		}
	}

	void DrawExteriorStairs( string root, ArchBuilding building )
	{
		foreach ( var flight in building.ExteriorStairs )
		{
			if ( !On( flight ) )
			{
				continue;
			}

			Draw( $"{root}/{ArchNames.ExteriorStair( flight )}", Transform.Zero, BuildingSpace( building ), flight, false,
				ArchDraws.ExteriorStair, building );
		}
	}

	void DrawFences( string root, ArchBuilding building, float lift )
	{
		foreach ( var fence in building.Fences )
		{
			if ( !On( fence ) )
			{
				continue;
			}

			Draw( $"{root}/{ArchNames.Fence( fence )}", new Transform( Vector3.Up * -lift ), Transform.Zero, fence, true,
				ArchDraws.Fence, building );
		}
	}

	// A cut that merely carves is the absence in what it passed through - no geometry, nothing to delete. What it
	// DOES build is its enclosure and its head, and those read the same whether it was filed on a building or on a
	// road: a service chamber off a bore is a shaft with walls round it.
	void DrawShafts( string root, IReadOnlyList<ArchCutPart> cuts, ArchPalette owner, Transform space = default )
	{
		foreach ( var cut in cuts.Where( entry => entry.Builds ) )
		{
			if ( !On( cut ) )
			{
				continue;
			}

			Draw( $"{root}/{ArchNames.Cut( cut )}", Transform.Zero, space, cut, false, ArchDraws.Cut, owner );
		}
	}

	// On grade, like a fence: the lift belongs to the floor, not a platform poured beside it.
	void DrawPlatforms( string root, ArchBuilding building, float lift )
	{
		foreach ( var platform in building.Platforms )
		{
			if ( !On( platform ) )
			{
				continue;
			}

			Draw( $"{root}/{ArchNames.Platform( platform )}", new Transform( Vector3.Up * -lift ), Transform.Zero, platform, false,
				ArchDraws.Platform, building );
		}
	}

	void DrawOverhang( string roomPath, ArchRoom room, ArchBuilding building )
	{
		Draw( $"{roomPath}/Overhang", Transform.Zero, BuildingSpace( building ), room, false, ArchDraws.Overhang, building );
	}

	void DrawPillars( string roomPath, ArchRoom room, ArchBuilding building )
	{
		foreach ( var pillar in room.Pillars )
		{
			if ( !On( pillar ) )
			{
				continue;
			}

			Draw( $"{roomPath}/{ArchNames.Pillar( pillar )}", Transform.Zero, BuildingSpace( building ), pillar, false,
				ArchDraws.Pillar, room, building );
		}
	}

	void DrawSpans( string roomPath, ArchRoom room, ArchBuilding building )
	{
		foreach ( var span in room.PierSpans )
		{
			if ( !On( span ) )
			{
				continue;
			}

			Draw( $"{roomPath}/{ArchNames.Span( span )}", Transform.Zero, BuildingSpace( building ), span, false,
				ArchDraws.Span, room, building );
		}
	}

	void DrawBeams( string roomPath, ArchRoom room, ArchBuilding building )
	{
		foreach ( var beam in room.Beams )
		{
			if ( !On( beam ) )
			{
				continue;
			}

			Draw( $"{roomPath}/{ArchNames.Beam( beam )}", Transform.Zero, BuildingSpace( building ), beam, false,
				ArchDraws.Beam, room, building );
		}
	}

	void DrawStairs( string roomPath, ArchRoom room, ArchBuilding building )
	{
		foreach ( var stair in room.Stairs )
		{
			if ( !On( stair ) )
			{
				continue;
			}

			Draw( $"{roomPath}/{ArchNames.Stair( stair )}", Transform.Zero, BuildingSpace( building ), stair, false,
				ArchDraws.Stair, room, building );
		}
	}

	void DrawTrims( string roomPath, ArchRoom room, ArchBuilding building )
	{
		foreach ( var trim in room.Trims )
		{
			if ( !On( trim ) )
			{
				continue;
			}

			Draw( $"{roomPath}/{ArchNames.Trim( trim )}", Transform.Zero, BuildingSpace( building ), trim, false,
				ArchDraws.Trim, room, building );
		}
	}

	void DrawPorches( string roomPath, ArchRoom room, ArchBuilding building )
	{
		foreach ( var porch in room.Porches )
		{
			if ( !On( porch ) )
			{
				continue;
			}

			var porchPath = $"{roomPath}/{ArchNames.Porch( porch )}";

			Draw( porchPath, Transform.Zero, BuildingSpace( building ), porch, false, ArchDraws.Porch, room, building );

			DrawPorchParts( porchPath, porch, room, building );
		}
	}

	// A porch is a HOST, so what stands on its deck is drawn under it - its own object, its own row, its own
	// eye and its own collision - exactly as a room's flights, columns and runs are drawn under the room.
	void DrawPorchParts( string porchPath, ArchPorchPart porch, ArchRoom room, ArchBuilding building )
	{
		foreach ( var stair in porch.Stairs )
		{
			if ( !On( stair ) )
			{
				continue;
			}

			Draw( $"{porchPath}/{ArchNames.Stair( stair )}", Transform.Zero, BuildingSpace( building ), stair, false,
				ArchDraws.Stair, room, building );
		}

		foreach ( var pillar in porch.Pillars )
		{
			if ( !On( pillar ) )
			{
				continue;
			}

			Draw( $"{porchPath}/{ArchNames.Pillar( pillar )}", Transform.Zero, BuildingSpace( building ), pillar, false,
				ArchDraws.Pillar, room, building );
		}

		foreach ( var trim in porch.Trims )
		{
			if ( !On( trim ) )
			{
				continue;
			}

			Draw( $"{porchPath}/{ArchNames.Trim( trim )}", Transform.Zero, BuildingSpace( building ), trim, false,
				ArchDraws.Trim, room, building );
		}
	}

	void DrawApproaches( string roomPath, ArchRoom room, ArchBuilding building )
	{
		foreach ( var approach in room.Approaches )
		{
			if ( !On( approach ) )
			{
				continue;
			}

			Draw( $"{roomPath}/{ArchNames.Approach( approach )}", Transform.Zero, BuildingSpace( building ), approach, false,
				ArchDraws.Approach, room, building );
		}
	}

	Transform BuildingSpace( ArchBuilding building )
	{
		return worldSpace ? new Transform( Vector3.Up * answers.Lift( services.Plan, building, services.Kit ) ) : Transform.Zero;
	}

	Transform WallSpace( ArchBuilding building, Transform frame )
	{
		if ( !worldSpace )
		{
			return frame;
		}

		return new Transform( Vector3.Up * answers.Lift( services.Plan, building, services.Kit ) + frame.Position, frame.Rotation );
	}
}