Editor/Output/ArchBuildCache.cs
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

sealed class ArchBuiltRecord {
	public string Path { get; set; }

	public ulong Content { get; set; }
	public ulong Settled { get; set; }
	public ulong Settings { get; set; }
	public Dictionary<(long, long, long, long), BBox> Reach { get; set; } = new();
}

public sealed class ArchBuildSettlement {
	public IReadOnlySet<int> Write { get; init; }

	// Settings changed but geometry unchanged — take flags, keep model
	public IReadOnlySet<int> Dressed { get; init; } = new HashSet<int>();

	// Gated-out parts — live but not this build's business; prune must not delete them
	public IReadOnlySet<string> Kept { get; init; } = new HashSet<string>();

	public int Reused { get; init; }
	public int Contacted { get; init; }

	// A gated scope hid half of a coplanar pair — caller must rebuild cold
	public bool Escaped { get; init; }
}

// Filed by stable path (author names stripped) so renames don't invalidate the cache
public sealed class ArchBuildCache {
	readonly Dictionary<string, ArchBuiltRecord> parts = new();
	readonly Dictionary<(long, long, long, long), HashSet<string>> byPlane = new();
	readonly Dictionary<int, ulong> scopes = new();

	readonly Dictionary<int, ulong> interiors = new();

	// Scopes whose gating hid a coplanar pair — refused permanently to prevent repeat escapes
	readonly HashSet<int> refused = new();

	public int Count => parts.Count;

	public ArchBuildSettlement Last { get; private set; }

	public void Forget() {
		Restart();
		refused.Clear();
	}

	public void Restart() {
		parts.Clear();
		byPlane.Clear();
		scopes.Clear();
		interiors.Clear();
	}

	internal bool Lined( int wallId, ulong content ) {
		if ( interiors.TryGetValue( wallId, out var held ) && held == content ) {
			return true;
		}

		interiors[wallId] = content;

		return false;
	}

	internal void Unlined( int wallId ) => interiors.Remove( wallId );

	public ArchBuildGate Gate( ArchPlan plan, ArchKit kit, int terrain, IReadOnlySet<int> rebuild = null ) {
		return ArchLayerHash.Worth( plan ) || rebuild is not null
			? new ArchBuildGate( plan, kit, terrain, scopes, refused, rebuild )
			: null;
	}

	// Null cache = cold build (everything compared and written)
	public static ArchBuildSettlement Settle( ArchArchitectureBuild build, ArchBuildCache cache, ArchBuildGate gate = null ) {
		if ( cache is null ) {
			var all = Enumerable.Range( 0, build.Parts.Count ).ToHashSet();

			new ArchFaceOverlap().RemoveCoveredFaces( build.Parts );

			return new ArchBuildSettlement { Write = all, Contacted = all.Count };
		}

		return cache.Last = cache.Settled( build, gate );
	}

	ArchBuildSettlement Settled( ArchArchitectureBuild build, ArchBuildGate gate ) {
		var built = build.Parts;
		var kept = Kept( build.Standing );
		var closing = new ArchOverlapClosure( this );

		var keys = new string[built.Count];

		for ( var index = 0; index < built.Count; index++ ) {
			keys[index] = ArchNames.Stable( built[index].Path );
		}

		for ( var index = 0; index < built.Count; index++ ) {
			if ( parts.TryGetValue( keys[index], out var held ) && held.Content == built[index].Key ) {
				continue;
			}

			closing.Enrol( built, index );

			if ( held is not null ) {
				closing.Sight( held.Reach );
			}
		}

		var emitted = keys.ToHashSet();

		foreach ( var gone in parts.Where( entry => !emitted.Contains( entry.Key ) && !kept.Contains( entry.Key ) ) ) {
			closing.Sight( gone.Value.Reach );
		}

		closing.Close( built, keys );

		if ( Escaped( kept, closing ) is { Count: > 0 } escapees ) {
			foreach ( var scope in escapees ) {
				refused.Add( scope );
			}

			return new ArchBuildSettlement { Write = new HashSet<int>(), Escaped = true };
		}

		new ArchFaceOverlap().Resolve( closing.Faces() );

		var write = new HashSet<int>();
		var dressed = new HashSet<int>();

		for ( var index = 0; index < built.Count; index++ ) {
			var content = built[index].Key;

			parts.TryGetValue( keys[index], out var held );

			// Outside the closure nothing near it moved, so its settled key is the one it already had.
			var settled = closing.Holds( index ) ? Surviving( built[index], content ) : held?.Settled ?? content;

			if ( held is null || held.Settled != settled ) {
				write.Add( index );
			} else if ( held.Settings != built[index].Settings ) {
				dressed.Add( index );
			}

			Record( keys[index], built[index].Path, content, settled, built[index].Settings, closing.Reach( index ) ?? held?.Reach );
		}

		foreach ( var gone in parts.Keys.Where( path => !emitted.Contains( path ) && !kept.Contains( path ) ).ToList() ) {
			Drop( gone );
		}

		foreach ( var scope in gate?.Keys ?? new Dictionary<int, ulong>() ) {
			scopes[scope.Key] = scope.Value;
		}

		return new ArchBuildSettlement {
			Write = write,
			Dressed = dressed,
			Kept = Standing( kept ),
			Reused = built.Count - write.Count,
			Contacted = closing.Count
		};
	}

	// Converts stable keys back to scene paths for the prune
	IReadOnlySet<string> Standing( IReadOnlySet<string> kept ) {
		var standing = new HashSet<string>();

		foreach ( var key in kept ) {
			if ( parts.TryGetValue( key, out var held ) && held.Path is not null ) {
				standing.Add( held.Path );
			}
		}

		return standing;
	}

	internal IEnumerable<string> Standing( (long, long, long, long) plane ) {
		return byPlane.TryGetValue( plane, out var held ) ? held.ToList() : Enumerable.Empty<string>();
	}

	internal ArchBuiltRecord Held( string path ) => parts.TryGetValue( path, out var held ) ? held : null;

	List<int> Escaped( IReadOnlySet<string> kept, ArchOverlapClosure closing ) {
		var escapees = new List<int>();

		foreach ( var path in kept ) {
			if ( !parts.TryGetValue( path, out var held ) || !closing.Meets( held.Reach ) ) {
				continue;
			}

			if ( ArchNames.TrySourceId( Segment( path ), out var scope ) ) {
				escapees.Add( scope );
			}
		}

		return escapees;
	}

	IReadOnlySet<string> Kept( IReadOnlyList<string> segments ) {
		var kept = new HashSet<string>();

		if ( segments.Count == 0 ) {
			return kept;
		}

		var standing = segments.Select( ArchNames.Stable ).ToList();

		foreach ( var path in parts.Keys ) {
			if ( standing.Any( segment => path == segment || path.StartsWith( $"{segment}/" ) ) ) {
				kept.Add( path );
			}
		}

		return kept;
	}

	static string Segment( string path ) {
		var end = path.IndexOf( '/' );

		return end < 0 ? path : path[..end];
	}

	// Folds surviving face handles onto the emitted key — same geometry + same culled faces = same part
	static ulong Surviving( ArchBuiltPart part, ulong content ) {
		var mesh = part.Canvas.Finish();
		var hash = content;

		foreach ( var handle in mesh.FaceHandles ) {
			hash = ArchHash.Fold( hash, handle.Index );
		}

		return hash;
	}

	void Record( string key, string path, ulong content, ulong settled, ulong settings, Dictionary<(long, long, long, long), BBox> reach ) {
		if ( !parts.TryGetValue( key, out var held ) ) {
			held = parts[key] = new ArchBuiltRecord();
		}

		held.Path = path;
		held.Content = content;
		held.Settled = settled;
		held.Settings = settings;

		if ( reach is null || ReferenceEquals( reach, held.Reach ) ) {
			return;
		}

		Unfile( key, held.Reach );

		held.Reach = reach;

		foreach ( var plane in reach.Keys ) {
			(byPlane.TryGetValue( plane, out var into ) ? into : byPlane[plane] = new HashSet<string>()).Add( key );
		}
	}

	void Drop( string path ) {
		if ( !parts.TryGetValue( path, out var held ) ) {
			return;
		}

		Unfile( path, held.Reach );

		parts.Remove( path );
	}

	void Unfile( string path, Dictionary<(long, long, long, long), BBox> reach ) {
		foreach ( var plane in reach.Keys ) {
			if ( byPlane.TryGetValue( plane, out var standing ) && standing.Remove( path ) && standing.Count == 0 ) {
				byPlane.Remove( plane );
			}
		}
	}
}

// Incrementally grows the set of parts the contact pass must re-compare via plane adjacency
sealed class ArchOverlapClosure {
	readonly ArchBuildCache cache;
	readonly Dictionary<int, List<ArchOverlapFace>> faces = new();
	readonly Dictionary<int, Dictionary<(long, long, long, long), BBox>> reaches = new();
	readonly Dictionary<(long, long, long, long), BBox> touched = new();
	readonly Queue<(long, long, long, long)> frontier = new();

	public ArchOverlapClosure( ArchBuildCache cache ) {
		this.cache = cache;
	}

	public int Count => faces.Count;

	public bool Holds( int index ) => faces.ContainsKey( index );

	public Dictionary<(long, long, long, long), BBox> Reach( int index ) {
		return reaches.TryGetValue( index, out var held ) ? held : null;
	}

	public List<ArchOverlapFace> Faces() {
		return faces.Keys.OrderBy( index => index ).SelectMany( index => faces[index] ).ToList();
	}

	public void Enrol( IReadOnlyList<ArchBuiltPart> built, int index ) {
		if ( faces.ContainsKey( index ) ) {
			return;
		}

		var collected = ArchFaceOverlap.Faces( built[index], index ).ToList();
		var reach = new Dictionary<(long, long, long, long), BBox>();

		foreach ( var face in collected ) {
			var bounds = ArchFaceOverlap.Bounds( face );

			reach[face.Plane] = reach.TryGetValue( face.Plane, out var held ) ? Union( held, bounds ) : bounds;
		}

		faces[index] = collected;
		reaches[index] = reach;

		Sight( reach );
	}

	// Grown extents are re-queued — monotonic growth guarantees convergence
	public void Sight( Dictionary<(long, long, long, long), BBox> reach ) {
		foreach ( var entry in reach ) {
			if ( touched.TryGetValue( entry.Key, out var held ) ) {
				var grown = Union( held, entry.Value );

				if ( grown.Mins == held.Mins && grown.Maxs == held.Maxs ) {
					continue;
				}

				touched[entry.Key] = grown;
			} else {
				touched[entry.Key] = entry.Value;
			}

			frontier.Enqueue( entry.Key );
		}
	}

	public void Close( IReadOnlyList<ArchBuiltPart> built, string[] keys ) {
		var byPath = new Dictionary<string, int>();

		for ( var index = 0; index < built.Count; index++ ) {
			byPath[keys[index]] = index;
		}

		while ( frontier.Count > 0 ) {
			var plane = frontier.Dequeue();

			if ( !touched.TryGetValue( plane, out var extent ) ) {
				continue;
			}

			foreach ( var path in cache.Standing( plane ) ) {
				if ( !byPath.TryGetValue( path, out var index ) || faces.ContainsKey( index ) ) {
					continue;
				}

				if ( cache.Held( path )?.Reach.TryGetValue( plane, out var held ) == true
					&& ArchFaceOverlap.Touches( extent, held ) ) {
					Enrol( built, index );
				}
			}
		}
	}

	public bool Meets( Dictionary<(long, long, long, long), BBox> reach ) {
		foreach ( var entry in reach ) {
			if ( touched.TryGetValue( entry.Key, out var extent ) && ArchFaceOverlap.Touches( extent, entry.Value ) ) {
				return true;
			}
		}

		return false;
	}

	static BBox Union( BBox left, BBox right ) {
		return new BBox( Vector3.Min( left.Mins, right.Mins ), Vector3.Max( left.Maxs, right.Maxs ) );
	}
}