Editor/Wall/ArchOpeningRunAffector.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// A bay rhythm is one setting on the wall, not forty records - but the units it stands ARE records, because a
// wall's hole is an authored ArchOpening and nothing else wears a frame, a sash and a sill.
//
// So they are an effect, and effects obey the contract: re-derived from the rhythm as it stands NOW on every
// commit, and closed the moment it stops standing. The wall owns them, so retyping the preset, dragging an end
// or changing the bay all hand back exactly what the rhythm currently describes.
public static class ArchOpeningRunAffector {
	// The masonry between two units, and between a unit and something placed by hand. Below this the rhythm
	// is a strip window, which is a preset rather than a rhythm.
	const float LeastPier = 8f;

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

		foreach ( var room in plan.AllRooms().ToList() ) {
			foreach ( var wall in plan.Filed<ArchWall>( room, kinds ).ToList() ) {
				if ( Restand( plan, kit, room, wall ) ) {
					stood++;
				}
			}
		}

		return stood;
	}

	// Slotted by the wall and the ordinal on it, so a bay nudged along the same wall re-dresses the units it
	// already owns there rather than closing them and allocating new ids for the same rhythm.
	static bool Restand( ArchPlan plan, ArchKit kit, ArchRoom room, ArchWall wall ) {
		var preset = Named( kit, wall.OpeningRun );
		var wanted = Wanted( kit, room, wall, preset );

		return ArchOwned.Settle( plan, ArchOwned.Openings, wall.Id,
			wanted.Select( unit => Unit( wall, preset, unit ) ).ToList(), () => new ArchOpening() );
	}

	// Dressed off the preset in full, not just where it sits: retyping the rhythm's preset has to reach a unit
	// that is already standing, and every field but identity comes from the preset.
	static ArchWant<ArchWall, ArchOpening> Unit( ArchWall wall, ArchOpeningPreset preset, ArchOpeningShape unit ) {
		return new ArchWant<ArchWall, ArchOpening>( wall, opening => {
			preset.Dress( opening, unit.Offset );

			opening.Width = unit.Width;
			opening.Height = unit.Height;
			opening.SillHeight = unit.SillHeight;
		} );
	}

	// Exact, never FindOpening's fallback: a rhythm naming a preset the kit has lost would quietly stand
	// twelve of whatever happened to be first.
	static ArchOpeningPreset Named( ArchKit kit, ArchOpeningRun run ) {
		return run.Stands
			? kit.Openings.FirstOrDefault( preset => string.Equals( preset.Name, run.Preset, StringComparison.OrdinalIgnoreCase ) )
			: null;
	}

	// Stations come off ArchDivide over the wall's USABLE run, so the end units sit inside the margin instead of
	// stacking on it, and each one resolves through the placement service a drag uses.
	static List<ArchOpeningShape> Wanted( ArchKit kit, ArchRoom room, ArchWall wall, ArchOpeningPreset preset ) {
		var wanted = new List<ArchOpeningShape>();

		if ( preset is null || wall.Length < 1f ) {
			return wanted;
		}

		var run = wall.OpeningRun;

		ArchOpeningSeats.Usable( wall, preset, kit, true, ArchGridService.FinestSize, out var runFrom, out var runTo );

		var usable = runTo - runFrom;
		var width = ArchGridService.Fine( run.Width > 1f ? run.Width : preset.Width );
		var fits = (int)MathF.Floor( usable / MathF.Max( 1f, width + LeastPier ) );

		// Asking for more than the elevation holds is answered with as many as it holds - openings that
		// overlap are one broken panel, not a tighter rhythm.
		if ( fits < 1 ) {
			return wanted;
		}

		var asked = run.Count > 0 ? run.Count : ArchDivide.AtMost( usable, run.Bay ).Count;
		var division = ArchDivide.Into( usable, Math.Min( asked, fits ) );
		var wallHeight = ArchWallSection.Height( wall, room, kit );

		foreach ( var centre in division.Centres ) {
			var unit = ArchOpeningSeats.Stationed( wall, preset, kit, run, runFrom + centre, wallHeight );

			if ( Clear( wall, unit ) ) {
				wanted.Add( unit );
			}
		}

		return wanted;
	}

	// A hand-placed unit keeps its place and the rhythm parts around it. That is what an individual override is
	// here: drag the door you want and the windows make room for it, rather than editing one of twelve clones.
	static bool Clear( ArchWall wall, ArchOpeningShape unit ) {
		var left = unit.Offset - unit.Width * 0.5f;
		var right = unit.Offset + unit.Width * 0.5f;

		return !wall.Openings
			.Where( opening => opening.OwnerId != wall.Id )
			.Any( opening => right > opening.Left - LeastPier && left < opening.Right + LeastPier );
	}

	// The other half of being able to re-stand. Only the rhythm's own units - a hand-placed one is nobody
	// else's to close.
	public static void Close( ArchWall wall ) {
		wall.Openings.RemoveAll( opening => opening.OwnerId == wall.Id );
	}
}