Editor/Layers/ArchLayerIds.cs
using System.Text.Json.Nodes;

namespace Sunless.Architecture;

// Every plan id in a saved subtree, moved as one block so the copy lands above whatever the plan already
// holds. Walked over the JSON and not the typed tree, because a unit carries payloads for kinds this build
// has no type for: an id left behind in those bytes is a stamped copy standing on the original's ids, which
// in the scene is one object where two were meant to be.
//
// Moving the whole range by one delta is what keeps the references honest. Every id inside the subtree is
// unique within it, so a constant shift preserves every pointer between them without this having to know
// which properties are pointers.
public static class ArchLayerIds {
	// Named on something the copy does not bring with it - a shifted road id points at somebody else's kerb.
	static readonly HashSet<string> Foreign = new() { "RoadId", "ApproachId" };

	public static bool Range( string json, out int lowest, out int highest ) {
		var low = int.MaxValue;
		var high = 0;

		lowest = 0;
		highest = 0;

		if ( JsonNode.Parse( json ) is not { } root ) {
			return false;
		}

		Walk( root, ( property, value ) => {
			if ( Foreign.Contains( property ) ) {
				return value;
			}

			low = Math.Min( low, value );
			high = Math.Max( high, value );

			return value;
		} );

		lowest = low;
		highest = high;

		return high > 0;
	}

	public static HashSet<int> Owned( string json ) => new( Every( json ) );

	// The same ids WITH their repeats. Owned answers whether an id is in the document, which is a question that
	// cannot see two parts standing under one - and two parts under one id is the failure this file exists for.
	public static List<int> Every( string json ) {
		var every = new List<int>();

		if ( JsonNode.Parse( json ) is not { } root ) {
			return every;
		}

		Walk( root, ( property, value ) => {
			if ( property == "Id" ) {
				every.Add( value );
			}

			return value;
		} );

		return every;
	}

	public static string Shifted( string json, int delta ) {
		if ( JsonNode.Parse( json ) is not { } root ) {
			return json;
		}

		Walk( root, ( property, value ) => Foreign.Contains( property ) ? 0 : value + delta );

		return root.ToJsonString();
	}

	static void Walk( JsonNode node, Func<string, int, int> visit ) {
		switch ( node ) {
			case JsonArray array:
				foreach ( var entry in array ) {
					if ( entry is not null ) {
						Walk( entry, visit );
					}
				}

				return;

			case JsonObject: break;

			default: return;
		}

		var holder = (JsonObject)node;

		// Over a copy of the names: assigning back into the object while enumerating it throws.
		foreach ( var property in holder.Select( pair => pair.Key ).ToList() ) {
			var value = holder[property];

			if ( value is JsonObject or JsonArray ) {
				Walk( value, visit );

				continue;
			}

			if ( !property.EndsWith( "Id", StringComparison.Ordinal ) || value is not JsonValue held || !held.TryGetValue<int>( out var id ) || id == 0 ) {
				continue;
			}

			holder[property] = JsonValue.Create( visit( property, id ) );
		}
	}
}