Editor/Services/ArchRowPlacement.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// One unit of a street row: a base-grid rectangle, and everything the seed decided about it.
public sealed class ArchRowBay {
	public int Index { get; init; }
	public Vector2 Min { get; init; }
	public Vector2 Max { get; init; }
	public int Storeys { get; init; }
	public bool Parapet { get; init; }

	public Vector2 Size => Max - Min;
}

// One resolve, read by the ghost, the commit and the report - a preview that divided the frontage again would
// draw a different row than the one it stands.
public sealed class ArchRowShape {
	public List<ArchRowBay> Bays { get; init; } = new();
	public bool AlongX { get; init; }
	public float Frontage { get; init; }
	public float Depth { get; init; }
	public int RoadId { get; init; }
	public int Blocked { get; init; }
	public int Seed { get; init; }

	public bool IsUsable => Bays.Count > 0;
}

public sealed class ArchRowResult {
	public ArchSiteAssembly Group { get; init; }
	public List<ArchBuilding> Buildings { get; init; } = new();
}

// Staircase of grid-aligned rectangles, not splayed plots — grid snapping outranks the kink.
public static class ArchRowPlacement {
	const float Sway = 0.3f;
	const float LeastShare = 0.75f;
	const int LeastStoreys = 1;
	const int MostStoreys = 3;
	const int WidthSalt = 5;
	const int StoreySalt = 17;
	const int ParapetSalt = 37;

	// How near the drag has to run to a road before the row takes that road's verge as its frontage.
	public static float Verge( ArchRoadPart road ) => road.Reach() * 1.5f;

	// The drag START, not the whole rectangle: seeding off both corners reshuffles every bay the author has
	// already reviewed as the drag grows.
	public static int Seeded( Vector2 start ) {
		return unchecked((int)start.x * 73856093 ^ (int)start.y * 19349663);
	}

	public static ArchRowShape Resolve(
		ArchPlan plan,
		ArchKit kit,
		ArchArchetype archetype,
		int level,
		Vector2 from,
		Vector2 to,
		ArchRoadPart road,
		ArchCurve centreline,
		bool parapets ) {
		if ( plan is null || kit is null || archetype is null ) {
			return new ArchRowShape();
		}

		var grid = new ArchGridService();
		var unit = MathF.Max( grid.BaseSize, archetype.MinimumPlot.x );
		var least = MathF.Max( grid.BaseSize, grid.Base( unit * LeastShare ) );
		var seed = Seeded( grid.Base( from ) );

		var drafted = road is null
			? Straight( grid, from, to, unit, least, seed, parapets )
			: Verged( grid, road, centreline ?? road.Curve(), from, to, unit, least, seed, parapets );

		return Vacant( plan, kit, level, drafted );
	}

	public static ArchRowResult Stand(
		ArchPlan plan,
		ArchKit kit,
		ArchRowShape row,
		ArchArchetype archetype,
		ArchSectionRules rules,
		int level,
		float baseHeight,
		RoofStyle? style,
		RidgeRun ridge ) {
		if ( plan is null || kit is null || rules is null || row is null || !row.IsUsable ) {
			return null;
		}

		var standing = ArchArchetypeRules.Standing( rules, kit );
		var storey = standing + kit.FloorThickness;
		var floor = rules.Floor ?? true;
		var gutters = rules.Gutters ?? true;
		var stood = new List<ArchBuilding>();

		foreach ( var bay in row.Bays ) {
			var building = new ArchBuilding {
				Id = plan.AllocateId(),
				Name = $"{archetype?.Title ?? "Unit"}{plan.Buildings.Count + 1}",
				Archetype = archetype?.Name ?? "",
				GuttersEnabled = gutters && !bay.Parapet
			};

			var shell = ArchBuild.Shell(
				plan, building, kit, level, baseHeight, bay.Min, bay.Max,
				floor, style, building.GuttersEnabled, ridge );

			if ( shell is null ) {
				continue;
			}

			ArchArchetypeRules.Apply( shell, rules, kit );
			Capped( shell.Roof, bay );

			plan.Units.Add( building );
			Stacked( plan, kit, building, bay, rules, level, baseHeight, storey, standing, floor, style, ridge );

			stood.Add( building );
		}

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

		return new ArchRowResult {
			Group = ArchLayerGroups.Create( plan, ArchLayerGroups.Named( plan ), ArchAssemblyKind.Group, stood.Select( unit => unit.Id ) ),
			Buildings = stood
		};
	}

	static void Stacked(
		ArchPlan plan,
		ArchKit kit,
		ArchBuilding building,
		ArchRowBay bay,
		ArchSectionRules rules,
		int level,
		float baseHeight,
		float storey,
		float standing,
		bool floor,
		RoofStyle? style,
		RidgeRun ridge ) {
		for ( var above = 1; above < bay.Storeys; above++ ) {
			var section = ArchBuild.Storey(
				plan, building, kit, level + above, baseHeight + storey * above, bay.Min, bay.Max,
				floor, style, building.GuttersEnabled, ridge );

			if ( section is null ) {
				return;
			}

			ArchArchetypeRules.Apply( section, rules, kit );

			// A lifted deck keeps the storey Storey measured, so the row restates the one it authored.
			section.Room.WallHeight = standing;

			if ( section.Roof is { } lifted ) {
				lifted.BaseHeight = section.Room.BaseHeight + standing;
			}

			Capped( section.Roof, bay );
		}
	}

	// A parapet stands on the wall line, so the eave trims that would hang off it go with it.
	static void Capped( ArchRoofPart roof, ArchRowBay bay ) {
		if ( roof is null || !bay.Parapet ) {
			return;
		}

		roof.Parapet = true;
		roof.Overhang = 0f;
		roof.Fascia = false;
		roof.Soffit = false;
		roof.Gutters = false;
	}

	static ArchRowShape Straight( ArchGridService grid, Vector2 from, Vector2 to, float unit, float least, int seed, bool parapets ) {
		var lo = grid.Base( Vector2.Min( from, to ) );
		var hi = grid.Base( Vector2.Max( from, to ) );
		var span = hi - lo;
		var alongX = span.x >= span.y;
		var frontage = alongX ? span.x : span.y;
		var depth = alongX ? span.y : span.x;

		if ( frontage < least || depth < grid.BaseSize ) {
			return new ArchRowShape { AlongX = alongX, Frontage = frontage, Depth = depth, Seed = seed };
		}

		var division = ArchDivide.AtLeast( frontage, unit );
		var stations = Stations( division, seed, least );
		var start = alongX ? lo.x : lo.y;
		var edges = Rising( stations.Select( station => start + grid.Base( station ) ).ToList() );
		var near = alongX ? lo.y : lo.x;
		var bays = new List<ArchRowBay>();

		for ( var index = 0; index < edges.Count - 1; index++ ) {
			if ( edges[index + 1] - edges[index] < grid.BaseSize ) {
				continue;
			}

			bays.Add( Bay( index, seed, alongX, edges[index], edges[index + 1], near, near + depth, parapets ) );
		}

		return new ArchRowShape { Bays = bays, AlongX = alongX, Frontage = frontage, Depth = depth, Seed = seed };
	}

	static ArchRowShape Verged(
		ArchGridService grid,
		ArchRoadPart road,
		ArchCurve curve,
		Vector2 from,
		Vector2 to,
		float unit,
		float least,
		int seed,
		bool parapets ) {
		if ( curve is null || !curve.IsUsable
			|| !curve.Nearest( from, out var head, out var headGap )
			|| !curve.Nearest( to, out var tail, out var tailGap ) ) {
			return Straight( grid, from, to, unit, least, seed, parapets );
		}

		var furthest = headGap >= tailGap ? head : tail;
		var pull = (headGap >= tailGap ? from : to) - furthest.Flat;
		var right = Vector2.Dot( pull, Across( furthest ) ) >= 0f;
		var reach = road.Reach( right );
		var near = MathF.Min( head.Distance, tail.Distance );
		var far = MathF.Max( head.Distance, tail.Distance );
		var run = far - near;
		var depth = MathF.Max( grid.BaseSize, grid.Base( MathF.Max( headGap, tailGap ) - reach ) );

		if ( run < least || !curve.Sample( near, out var opening ) || !curve.Sample( far, out var closing ) ) {
			return new ArchRowShape { Frontage = run, Depth = depth, RoadId = road.Id, Seed = seed };
		}

		var chord = closing.Flat - opening.Flat;
		var alongX = MathF.Abs( chord.x ) >= MathF.Abs( chord.y );
		var forward = (alongX ? chord.x : chord.y) >= 0f;
		var outward = (alongX ? pull.y : pull.x) < 0f ? -1f : 1f;
		var division = ArchDivide.AtLeast( run, unit );
		var stations = Stations( division, seed, least );

		float Stationed( float offset ) => forward ? near + offset : far - offset;

		var edges = Rising( stations.Select( station => grid.Base( Edge( curve, Stationed( station ), alongX ) ) ).ToList() );
		var bays = new List<ArchRowBay>();

		for ( var index = 0; index < edges.Count - 1; index++ ) {
			if ( edges[index + 1] - edges[index] < grid.BaseSize ) {
				continue;
			}

			if ( !curve.Sample( Stationed( (stations[index] + stations[index + 1]) * 0.5f ), out var frame ) ) {
				continue;
			}

			var kerb = frame.Flat + Across( frame ) * (right ? reach : -reach);
			var verge = grid.Base( alongX ? kerb.y : kerb.x );

			bays.Add( Bay( index, seed, alongX, edges[index], edges[index + 1], verge, verge + outward * depth, parapets ) );
		}

		return new ArchRowShape { Bays = bays, AlongX = alongX, Frontage = run, Depth = depth, RoadId = road.Id, Seed = seed };
	}

	static ArchRowBay Bay( int index, int seed, bool alongX, float opening, float closing, float verge, float back, bool parapets ) {
		var near = MathF.Min( verge, back );
		var far = MathF.Max( verge, back );

		return new ArchRowBay {
			Index = index,
			Min = alongX ? new Vector2( opening, near ) : new Vector2( near, opening ),
			Max = alongX ? new Vector2( closing, far ) : new Vector2( far, closing ),
			Storeys = Storeyed( seed, index ),
			Parapet = parapets && ArchBarrierShape.Noise( seed, index, ParapetSalt ) < 0.5f
		};
	}

	static int Storeyed( int seed, int index ) {
		var reach = MostStoreys - LeastStoreys + 1;

		return Math.Clamp( LeastStoreys + (int)MathF.Floor( ArchBarrierShape.Noise( seed, index, StoreySalt ) * reach ), LeastStoreys, MostStoreys );
	}

	// The party wall between two units is ONE node used by both, so a jittered width can never open a gap.
	static List<float> Stations( ArchDivision division, int seed, float least ) {
		var stations = new List<float>();

		for ( var index = 0; index <= division.Count; index++ ) {
			var station = division.At( index );

			if ( index > 0 && index < division.Count ) {
				station += ArchBarrierShape.Signed( seed, index, WidthSalt ) * division.Step * Sway;
			}

			stations.Add( station );
		}

		for ( var index = 1; index < stations.Count - 1; index++ ) {
			var earliest = stations[index - 1] + least;
			var latest = MathF.Max( earliest, division.Span - least * (stations.Count - 1 - index) );

			stations[index] = Math.Clamp( stations[index], earliest, latest );
		}

		return stations;
	}

	// A street that doubles back would hand the same edge out twice; the bay between them collapses and drops.
	static List<float> Rising( List<float> edges ) {
		for ( var index = 1; index < edges.Count; index++ ) {
			edges[index] = MathF.Max( edges[index], edges[index - 1] );
		}

		return edges;
	}

	static float Edge( ArchCurve curve, float distance, bool alongX ) {
		if ( !curve.Sample( distance, out var frame ) ) {
			return 0f;
		}

		return alongX ? frame.Flat.x : frame.Flat.y;
	}

	static Vector2 Across( ArchFrame frame ) {
		var across = new Vector2( frame.Across.x, frame.Across.y );

		return across.Length < 0.01f ? new Vector2( 0f, 1f ) : across.Normal;
	}

	// A trimmed bay loses the party wall it shares and steps out of the staircase, so occupied ground drops the
	// whole bay rather than shrinking it.
	static ArchRowShape Vacant( ArchPlan plan, ArchKit kit, int level, ArchRowShape drafted ) {
		if ( !drafted.IsUsable ) {
			return drafted;
		}

		var boundary = new ArchBoundaryPlacement( plan, kit );
		var standing = new List<ArchRowBay>();
		var blocked = 0;

		foreach ( var bay in drafted.Bays ) {
			var placement = boundary.Outside( level, bay.Min, bay.Max );

			if ( !placement.IsUsable || placement.Min != bay.Min || placement.Max != bay.Max ) {
				blocked++;
				continue;
			}

			standing.Add( bay );
		}

		return new ArchRowShape {
			Bays = standing,
			AlongX = drafted.AlongX,
			Frontage = drafted.Frontage,
			Depth = drafted.Depth,
			RoadId = drafted.RoadId,
			Blocked = blocked,
			Seed = drafted.Seed
		};
	}
}