Editor/Carve/ArchExtrudeAffector.cs
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// A Pier zone's columns. Every other extrude kind is work the WALL carries, so the wall generator dresses it and
// nothing has to be stored. A pier cannot be: it is a real dressed ArchPillarPart with a footing, a plinth, a
// banded shaft and a capital off an authored type, and it belongs to the room rather than to the wall.
//
// So they are an effect, and effects obey the contract: re-derived from where the zone stands NOW on every commit,
// scoped to the group the zone was placed in, and closed plan-wide whatever the scope. Drag the zone and the piers
// follow it; disable it and ArchLayerGate stops them being read without deleting anything.
public static class ArchExtrudeAffector {
	// The narrowest stretch worth a column of its own - below this the zone is clipping a wall's corner.
	const float LeastPier = 6f;

	public static int Resolve( ArchPlan plan, ArchKit kit ) {
		var stood = 0;

		foreach ( var building in plan.Buildings.ToList() ) {
			foreach ( var zone in building.Cuts.ToList() ) {
				if ( zone.Extrude == ArchExtrudeKind.Pier && Restand( plan, kit, building, zone ) ) {
					stood++;
				}
			}
		}

		return stood;
	}

	static bool Restand( ArchPlan plan, ArchKit kit, ArchBuilding home, ArchCutPart zone ) {
		var wanted = Wanted( plan, kit, home, zone );

		// Compared by where the column STANDS, not by which wall carries it - a zone slid along the same wall wants
		// the same wall and a different station, and an id check would skip the re-stand.
		if ( Standing( plan, zone ).SetEquals( wanted.Select( Signature ) ) ) {
			return false;
		}

		Close( plan, zone );

		var type = Dressing( zone );

		foreach ( var pier in wanted ) {
			var part = type is null
				? new ArchPillarPart()
				: type.Standing( pier.Wall.PointAt( pier.Along ), pier.Foot, pier.Rise );

			part.Id = plan.AllocateId();
			part.OwnerId = zone.Id;
			part.Name = $"Pier {pier.Wall.Id}";
			part.Placement = PillarPlacement.Pilaster;
			part.WallId = pier.Wall.Id;
			part.WallOffset = pier.Along;
			part.Origin = pier.Wall.PointAt( pier.Along );
			part.BaseHeight = pier.Foot;
			part.Height = pier.Rise;
			part.PilasterWidth = pier.Width;
			part.PilasterProtrusion = ArchExtrude.Reach( zone );
			part.PilasterBothFaces = zone.ExtrudeBothFaces;
			part.Palette = zone.Palette;

			if ( zone.ExtrudeFooting is { } founding ) {
				part.Footing = founding;
			}

			pier.Room.Pillars.Add( part );
		}

		return true;
	}

	// Only the storey the zone's FOOT stands on. A zone crossing three floors is one support, not one per storey -
	// which is the whole point of dragging it tall - so the column takes the band's own rise and the storeys above
	// are left to the wall behind it.
	static List<Pier> Wanted( ArchPlan plan, ArchKit kit, ArchBuilding home, ArchCutPart zone ) {
		var piers = new List<Pier>();

		if ( !zone.HasContent || !ArchLayerGate.On( zone ) || !ArchCut.Affects( zone, ArchCutAffects.Walls ) ) {
			return piers;
		}

		var reach = ArchLayerGroups.Reach( plan, zone.Id, home.Id );

		foreach ( var room in plan.AllRooms().Where( room => room.Floor == zone.Level ) ) {
			if ( reach is not null && plan.OwnerOf( room ) is { } owner && !reach.Contains( owner.Id ) ) {
				continue;
			}

			var building = plan.OwnerOf( room );
			var height = ArchFloorGen.WallHeight( room, kit );

			foreach ( var wall in room.Walls ) {
				var join = new ArchConnections( building, kit )
					.JoinWall( wall, room, ArchWallSection.Thickness( wall, kit ) );

				foreach ( var stretch in ArchWallExtrude.Stretches( plan, kit, building, room, wall, join.StartExtend, join.EndExtend, height ) ) {
					if ( stretch.Zone.Id != zone.Id || stretch.Width < LeastPier ) {
						continue;
					}

					piers.Add( new Pier {
						Room = room,
						Wall = wall,
						Along = stretch.Along,
						Width = stretch.Width,
						// The zone's OWN foot, not the stretch the storey clipped to its floor: a footing is buried
						// by definition, so a column held to the slab it stands on could never show one.
						Foot = Foot( zone ),
						// And the zone's whole band, not the stretch either, so one gesture climbs past every floor
						// line it crosses.
						Rise = Band( zone )
					} );
				}
			}
		}

		return piers;
	}

	static float Band( ArchCutPart zone ) {
		var rise = 0f;

		foreach ( var leg in zone.Legs() ) {
			rise = MathF.Max( rise, leg.TopHeight - leg.BaseHeight );
		}

		return MathF.Max( 1f, rise );
	}

	static float Foot( ArchCutPart zone ) {
		var foot = float.MaxValue;

		foreach ( var leg in zone.Legs() ) {
			foot = MathF.Min( foot, leg.BaseHeight );
		}

		return foot >= float.MaxValue ? 0f : foot;
	}

	static ArchPillarType Dressing( ArchCutPart zone ) {
		var shelf = ArchAsks.PillarTypes();

		if ( shelf.Count == 0 ) {
			return null;
		}

		return shelf.FirstOrDefault( type => type.Name == zone.ExtrudePillarType ) ?? shelf[0];
	}

	public static void Close( ArchPlan plan, ArchCutPart zone ) {
		foreach ( var room in plan.AllRooms() ) {
			room.Pillars.RemoveAll( pillar => pillar.OwnerId == zone.Id );
		}
	}

	static HashSet<string> Standing( ArchPlan plan, ArchCutPart zone ) {
		return plan.AllRooms()
			.SelectMany( room => room.Pillars )
			.Where( pillar => pillar.OwnerId == zone.Id )
			.Select( pillar => $"{pillar.WallId}:{pillar.WallOffset:0.##}:{pillar.PilasterWidth:0.##}:{pillar.BaseHeight:0.##}:{pillar.Height:0.##}" )
			.ToHashSet();
	}

	static string Signature( Pier pier ) {
		return $"{pier.Wall.Id}:{pier.Along:0.##}:{pier.Width:0.##}:{pier.Foot:0.##}:{pier.Rise:0.##}";
	}

	sealed class Pier {
		public ArchRoom Room { get; init; }
		public ArchWall Wall { get; init; }
		public float Along { get; init; }
		public float Width { get; init; }
		public float Foot { get; init; }
		public float Rise { get; init; }
	}
}