Editor/Layers/ArchLayerHash.cs
using System.Collections.Generic;
using System.Linq;

namespace Sunless.Architecture;

// WHAT MAKES A LAYER THE SAME LAYER - asked here and nowhere else, the way ArchLayerOrder is the one answer about
// order. Identity is the SERIALISED form, because a fold written field by field forgets the field added next month
// and a forgotten field is a change that draws nothing.
public static class ArchLayerHash {
	public static ulong Of( object payload ) {
		return payload is null ? ArchHash.Seed : ArchHash.Of( ArchStorage.Keyed( payload ) );
	}

	// With no group in the plan every scope is ungrouped, so its own form is already in the plan-wide term and its key
	// moves whenever it does. The gate could never skip one, and asking costs a serialise per rebuild.
	public static bool Worth( ArchPlan plan ) => plan is { Assemblies.Count: > 0 };

	// Order-dependent, because a group is a scope over an ORDERED operation stack: the same two members swapped
	// build different geometry, so they must not roll up to the same key. Nested groups fold whole.
	//
	// `held` is where a build remembers what it has already worked out. Every member of a group asks for the SAME
	// roll-up, so without one a group of sixteen serialises seventeen payloads sixteen times over.
	public static ulong Group( ArchPlan plan, ArchSiteAssembly group, Dictionary<int, ulong> held = null ) {
		if ( held is not null && held.TryGetValue( group.Id, out var remembered ) ) {
			return remembered;
		}

		var hash = ArchHash.Fold( ArchHash.Seed, group.Id );

		hash = ArchHash.Fold( hash, Of( group ) );

		foreach ( var childId in group.Children ) {
			if ( ArchLayerGroups.Find( plan, childId ) is { } nested ) {
				hash = ArchHash.Fold( hash, Group( plan, nested, held ) );

				continue;
			}

			hash = ArchHash.Fold( hash, Of( Payload( plan, childId ) ) );
		}

		if ( held is not null ) {
			held[group.Id] = hash;
		}

		return hash;
	}

	// Everything that reaches OUT of every scope, so a scope may only be skipped when none of it moved. A platform is
	// in here because a pad sets a grade lift ArchCarveHosts looks for across every building; an ungrouped layer is
	// because its reach is the whole plan by definition. Conservative on purpose - folding in too much costs a
	// rebuild, leaving something out costs a wrong building.
	public static ulong Wide( ArchPlan plan, ArchKit kit, int terrain ) {
		var hash = ArchHash.Fold( ArchHash.Seed, terrain );

		hash = ArchHash.Fold( hash, Of( kit ) );

		foreach ( var record in plan.Layers ) {
			hash = ArchHash.Fold( hash, Of( record ) );
		}

		// A piece's mode lives on the plan rather than on the payload, so nothing else in here would move when
		// one changes and the gate would skip the scope holding it.
		foreach ( var record in plan.Parts ) {
			hash = ArchHash.Fold( hash, Of( record ) );
		}

		foreach ( var link in plan.Links ) {
			hash = ArchHash.Fold( hash, Of( link ) );
		}

		foreach ( var instance in plan.Instances ) {
			hash = ArchHash.Fold( hash, Of( instance ) );
		}

		foreach ( var group in plan.Assemblies ) {
			hash = ArchHash.Fold( hash, ArchHash.Fold( ArchHash.Seed, group.Id ) );
			hash = ArchHash.Fold( hash, Of( group ) );
		}

		foreach ( var platform in plan.Buildings.SelectMany( building => building.Platforms ) ) {
			hash = ArchHash.Fold( hash, Of( platform ) );
		}

		// A services corridor reaches out of whatever scope holds it by definition: what it steps over is every
		// other corridor in the plan, so one moving inside its own group changes a run in somebody else's. Left
		// out, the gate skipped that other scope and it kept a hump over a bundle that had gone.
		foreach ( var run in plan.Buildings.SelectMany( building => building.Pipes ) ) {
			hash = ArchHash.Fold( hash, Of( run ) );
		}

		// And a hanger is sized off those runs, so it moves whenever they do.
		foreach ( var bracket in plan.Buildings.SelectMany( building => building.Brackets ) ) {
			hash = ArchHash.Fold( hash, Of( bracket ) );
		}

		foreach ( var scope in plan.Buildings.Where( building => ArchLayerGroups.Holding( plan, building.Id ) is null ) ) {
			hash = ArchHash.Fold( hash, Of( scope ) );
		}

		foreach ( var road in plan.Roads().Where( road => ArchLayerGroups.Holding( plan, road.Id ) is null ) ) {
			hash = ArchHash.Fold( hash, Of( road ) );
		}

		return hash;
	}

	// One building's or one road's key: what reaches everywhere, plus the scope it stands in. Held by a group,
	// that is the group's roll-up; held by none, its own form is already inside the wide term, so the two
	// together say "nothing anywhere in the plan that could touch this has moved".
	public static ulong Scope( ulong wide, ArchPlan plan, int itemId, Dictionary<int, ulong> held = null ) {
		var hash = ArchHash.Fold( wide, itemId );

		if ( ArchLayerGroups.Holding( plan, itemId ) is { } group ) {
			return ArchHash.Fold( hash, Group( plan, group, held ) );
		}

		return ArchHash.Fold( hash, Of( Payload( plan, itemId ) ) );
	}

	// Only the roots a scope can be: a group's member list holds ids of any kind, and what is not a building or a
	// road is folded through the tree instead of hunted for here.
	static object Payload( ArchPlan plan, int itemId ) {
		if ( plan.FindBuilding( itemId ) is { } building ) {
			return building;
		}

		if ( plan.Roads().FirstOrDefault( road => road.Id == itemId ) is { } road ) {
			return road;
		}

		return plan.AllRooms().FirstOrDefault( room => room.Id == itemId );
	}
}