Editor/Output/ArchArchitectureBuilder.cs
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>();

	public IReadOnlyList<string> Standing { get; init; } = Array.Empty<string>();
}

public sealed class ArchArchitectureBuilder {
	readonly ArchBuildContext context;
	readonly ArchOutput output;
	readonly Dictionary<string, Transform> rootTransforms = new();

	ArchDrawers drawers;
	ArchAnswers answers;
	ArchBuildGate gate;

	public ArchArchitectureBuilder( ArchPlan plan, ArchKit kit, Scene scene = null, ArchBuildContext context = null ) {
		this.context = context ?? new ArchBuildContext( plan, kit, scene );
		output = new ArchOutput( this.context.Plan );
		drawers = ArchDrawers.Load();
		answers = ArchAnswers.Load();
	}

	static bool On( object payload ) => ArchLayerGate.On( payload );

	public ArchArchitectureBuilder Drawing( ArchDrawers with ) {
		drawers = with;

		return this;
	}

	public ArchArchitectureBuilder Answering( ArchAnswers with ) {
		answers = with;

		return this;
	}

	public ArchArchitectureBuilder Through( ArchBuildGate through ) {
		gate = through;

		return this;
	}

	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() {
		context.ResolveConnections();

		return this;
	}

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

		return this;
	}

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

		foreach ( var road in context.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, Transform.Zero );
		}

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

		return this;
	}

	public ArchArchitectureBuilder WithUnits() {
		foreach ( var unit in context.Plan.Units ) {
			if ( unit is ArchBuilding or ArchRoadPart || !On( unit ) ) {
				continue;
			}

			var root = ArchNames.Unit( unit );

			if ( Standing( unit.Id, root ) ) {
				continue;
			}

			using var collision = output.Within( unit.Collision );
			using var shadows = output.WithinShadows( unit.Id );

			Draw( root, Transform.Zero, Transform.Zero, unit, false, ArchDraws.Unit( unit.Kind ) );

			File( root, unit, ArchFiles.Unit( unit.Kind ) );

			DrawShafts( root, unit.Cuts, unit.Palette, Transform.Zero );
		}

		return this;
	}

	public ArchArchitectureBuild Create() {
		var transforms = new Dictionary<string, Transform>( rootTransforms );

		foreach ( var frame in output.Frames ) {
			transforms[frame.Key] = frame.Value;
		}

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

	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;
		}

		var from = ArchBuildProfile.Now;

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

		ArchBuildProfile.Ran( role, from );
	}

	void File( string path, object subject, string role, params object[] hosts ) {
		if ( drawers.Filing( role ) is not IArchFiler filer ) {
			return;
		}

		var from = ArchBuildProfile.Now;

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

		ArchBuildProfile.Ran( role, from );
	}

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

		var holding = ArchLayerGroups.Holding( context.Plan, building.Id );

		using var held = output.Within( holding?.Collision );
		using var heldShadows = output.WithinShadows( holding?.Id ?? 0 );
		using var collision = output.Within( building.Collision );
		using var shadows = output.WithinShadows( building.Id );

		DrawStoreys( root, building );

		foreach ( var room in building.Rooms ) {
			var roomPath = $"{root}/{ArchNames.Room( room )}";

			if ( On( room ) && !Standing( room.Id, roomPath ) ) {
				DrawRoom( roomPath, 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 roomPath, ArchRoom room, ArchBuilding building ) {
		using var collision = output.Within( room.Collision );
		using var shadows = output.WithinShadows( room.Id );

		DrawOverhang( roomPath, room, building );
		DrawShell( roomPath, room, building );
		DrawCornice( roomPath, room, building );
		DrawPillars( roomPath, room, building );
		DrawSpans( roomPath, room, building );
		DrawBeams( roomPath, room, building );
		DrawCabinets( 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, context.Kit, context.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 );
		}
	}

	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 );
	}

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

	void DrawRoofs( string root, ArchBuilding building ) {
		foreach ( var roof in building.Roofs ) {
			var roofPath = $"{root}/{ArchNames.Roof( roof )}";

			if ( !On( roof ) || Standing( roof.Id, roofPath ) ) {
				continue;
			}

			var runs = new List<ArchRunPath>();

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

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

	void DrawRoofWalls( string roofPath, ArchRoofPart roof, ArchBuilding building ) {
		if ( roof.Walls.Count == 0 ) {
			return;
		}

		var deck = ArchRoofWalls.Deck( roof, context.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 );
		}
	}

	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 );
		}
	}

	// `space` must be explicitly named — default(Transform) has zero scale and maps all UVs to one texel
	void DrawShafts( string root, IReadOnlyList<ArchCutPart> cuts, ArchPalette owner, Transform space ) {
		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 );
		}
	}

	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 );
	}

	// The storey's entablature is ONE ring round its own loop, so it hangs off the storey rather than off each wall.
	void DrawCornice( string roomPath, ArchRoom room, ArchBuilding building ) {
		Draw( $"{roomPath}/{ArchPieces.Cornice}", Transform.Zero, BuildingSpace( building ), room, false, ArchDraws.Cornice, building );
	}

	void DrawPillars( string roomPath, ArchRoom room, ArchBuilding building ) {
		foreach ( var pillar in room.Pillars ) {
			if ( !On( pillar ) || !ArchLayerGate.Owned( pillar.OwnerId ) ) {
				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 DrawCabinets( string roomPath, ArchRoom room, ArchBuilding building ) {
		foreach ( var cabinets in room.Cabinets ) {
			if ( !On( cabinets ) ) {
				continue;
			}

			Draw( $"{roomPath}/{ArchNames.Cabinets( cabinets )}", Transform.Zero, BuildingSpace( building ), cabinets, false,
				ArchDraws.Cabinet, 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 );
		}
	}

	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 );
		}
	}

	// The node's actual world space — UVs and contact pass both need this
	Transform BuildingSpace( ArchBuilding building ) {
		return new Transform( Vector3.Up * answers.Lift( context.Plan, building, context.Kit ) );
	}

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