Editor/Tool/ArchToolVisibility.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// Name-matched on generated roots; a rebuild reapplies toggles.
public partial class ArchTool {
	public void ApplyVisibility() {
		var root = ArchScene.FindRoot( Scene );

		if ( root.IsValid() ) {
			ApplyVisibility( root );
		}
	}

	// Skip the walk when nothing is toggled off.
	bool Hiding => HideRoofs || HideWalls || HideFloors || HideFoundation || HideTrim || OnlyCurrentLevel
		|| hiddenLayers.Count > 0 || hiddenPieces.Count > 0;

	// Remembered so the last toggle-off still gets one restore walk.
	bool hid;

	void ApplyVisibility( GameObject root ) {
		// Ahead of the early return, because a bake is switched by the toggle and not by anything Hiding knows about.
		// Gated on a merged copy actually standing: a build prunes one it did not emit, and asking for the baked half
		// when there is no longer a baked half is how every source stays switched off with nothing to show for it.
		ArchBake.Show( root, ShowBaked && ArchBake.Standing( root ) );

		if ( !Hiding && !hid ) {
			return;
		}

		hid = Hiding;
		roofsById = null;

		foreach ( var building in root.Children ) {
			if ( MutedLayer( building ) ) {
				building.Enabled = false;
				continue;
			}

			building.Enabled = true;

			foreach ( var node in building.Children ) {
				if ( MutedLayer( node ) ) {
					node.Enabled = false;
					continue;
				}

				var isRoof = node.Name.StartsWith( "Roof", StringComparison.Ordinal )
					|| node.Name.StartsWith( "Downpipe", StringComparison.Ordinal );

				if ( isRoof ) {
					// Gutters and downpipes hang off a roof, so they follow the section's storey too.
					var section = RoofOf( node );

					node.Enabled = !HideRoofs && !(OnlyCurrentLevel && section is not null && section.Level != Level);

					if ( node.Enabled ) {
						Reveal( node, ArchNames.TrySourceId( node.Name, out var roofed ) ? roofed : 0, "" );
					}

					continue;
				}

				// A floor is filed under its level, not a room, so it answers the level hide itself.
				var storey = ArchNames.TryParseId( node.Name, "Floor", out var floored ) ? floored : (int?)null;
				var room = Plan.FindRoom( RoomIdOf( node ) );

				var levelHidden = OnlyCurrentLevel && (storey ?? room?.Floor ?? Level) != Level;

				node.Enabled = !levelHidden;

				if ( levelHidden ) {
					continue;
				}

				Reveal( node, ArchNames.TrySourceId( node.Name, out var owned ) ? owned : 0, "" );
			}
		}
	}

	// EVERY generated object below a building, not the two ranks the walk used to reach: a fascia is three deep
	// and the eye beside its row has to reach it. A name carrying a plan id starts a new layer, so what an eye
	// holds below that point is a PIECE of it - and hiding an object takes everything under it with it, which is
	// exactly what hiding a group of pieces means.
	void Reveal( GameObject node, int owner, string piece ) {
		foreach ( var child in node.Children ) {
			var layer = owner;
			var named = piece;

			if ( ArchNames.TrySourceId( child.Name, out var id ) ) {
				layer = id;
				named = "";
			} else {
				named = ArchParts.Joined( named, child.Name );
			}

			child.Enabled = !Hidden( child.Name ) && !LayerHidden( layer )
				&& (named.Length == 0 || !PieceHidden( layer, named ));

			if ( child.Enabled ) {
				Reveal( child, layer, named );
			}
		}
	}

	// The stack's eye, resolved through the id every generated name carries. Presentation only - the
	// layer still generates, so unhiding it costs nothing and a rebuild reapplies the toggle.
	bool MutedLayer( GameObject node ) {
		return ArchNames.TrySourceId( node.Name, out var id ) && LayerHidden( id );
	}

	bool Hidden( string name ) {
		if ( name.StartsWith( "Wall", StringComparison.Ordinal ) ) {
			return HideWalls;
		}

		if ( name is "Slab" or "Overhang" ) {
			return HideFloors;
		}

		if ( name == "Foundation" ) {
			return HideFoundation;
		}

		if ( name.StartsWith( "Trim", StringComparison.Ordinal ) ) {
			return HideTrim;
		}

		return false;
	}

	static int RoomIdOf( GameObject node ) {
		return ArchNames.TryParseId( node.Name, "Room", out var id ) ? id : 0;
	}

	Dictionary<int, ArchRoofPart> roofsById;

	// Indexed once per walk: this was a SelectMany over every roof in the plan, per roof node in the scene.
	ArchRoofPart RoofOf( GameObject node ) {
		if ( !ArchNames.TryParseId( node.Name, "Roof", out var id ) ) {
			return null;
		}

		if ( roofsById is null ) {
			roofsById = new Dictionary<int, ArchRoofPart>();

			foreach ( var roof in Plan.Buildings.SelectMany( building => building.Roofs ) ) {
				roofsById[roof.Id] = roof;
			}
		}

		return roofsById.TryGetValue( id, out var found ) ? found : null;
	}
}