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

namespace Sunless.Architecture;

public partial class ArchTool {
	public const float FlashSeconds = 1.6f;

	const string IsolateKey = "arch.layers.isolate";
	const string GeometryKey = "arch.layers.geometry";

	readonly HashSet<int> hiddenLayers = new();
	readonly HashSet<string> hiddenPieces = new();
	readonly HashSet<int> affected = new();

	RealTimeSince affectedAt = FlashSeconds;

	bool affectedNamed;

	public static bool IsolateGroups {
		get => EditorCookie.Get( IsolateKey, true );
		set => EditorCookie.Set( IsolateKey, value );
	}

	public static bool ShowGeometry {
		get => EditorCookie.Get( GeometryKey, true );
		set => EditorCookie.Set( GeometryKey, value );
	}

	public float Flash( int itemId ) {
		if ( !affected.Contains( itemId ) ) {
			return 0f;
		}

		return Math.Clamp( 1f - affectedAt / FlashSeconds, 0f, 1f );
	}

	public bool Flashing => affectedAt < FlashSeconds && affected.Count > 0;

	public void Affect( int itemId ) => Affect( new[] { itemId } );

	// Clears before filling — bulk callers must pass all ids at once
	public void Affect( IReadOnlyList<int> itemIds ) {
		var wanted = itemIds?.Where( id => id != 0 ).ToList();

		if ( wanted is not { Count: > 0 } ) {
			return;
		}

		affected.Clear();
		affectedAt = 0f;
		affectedNamed = true;

		foreach ( var itemId in wanted ) {
			foreach ( var id in LayerTree.DirtyClosure( itemId ) ) {
				affected.Add( id );
			}

			if ( ArchLayerGroups.Holding( Plan, itemId ) is { } group ) {
				affected.Add( group.Id );

				foreach ( var member in group.Children ) {
					affected.Add( member );
				}
			}
		}

		EditedGroupId = ArchLayerGroups.Holding( Plan, wanted[^1] )?.Id ?? 0;
	}

	bool ConsumeAffected() {
		var named = affectedNamed;

		affectedNamed = false;

		return named;
	}

	public int EditedGroupId { get; set; }

	public bool Muted( ArchLayerNode node ) {
		if ( !IsolateGroups || EditedGroupId == 0 || node?.Ref is not { } layer ) {
			return false;
		}

		if ( layer.ItemId == EditedGroupId ) {
			return false;
		}

		var holding = ArchLayerGroups.Holding( Plan, layer.ItemId )
			?? ArchLayerGroups.Holding( Plan, Root( node )?.Ref?.ItemId ?? 0 );

		return holding is not null && holding.Id != EditedGroupId;
	}

	static ArchLayerNode Root( ArchLayerNode node ) {
		while ( node?.Parent is { } parent ) {
			node = parent;
		}

		return node;
	}

	// Walks up — a group has no scene object, so hiding propagates through the tree
	public bool LayerHidden( int itemId ) {
		if ( hiddenLayers.Count == 0 ) {
			return false;
		}

		if ( hiddenLayers.Contains( itemId ) ) {
			return true;
		}

		for ( var node = LayerTree.Find( itemId )?.Parent; node is not null; node = node.Parent ) {
			if ( node.Ref is { } layer && hiddenLayers.Contains( layer.ItemId ) ) {
				return true;
			}
		}

		return false;
	}

	public bool LayerHiddenSelf( int itemId ) => hiddenLayers.Contains( itemId );

	public bool PieceHidden( int owner, string piece ) => hiddenPieces.Contains( ArchParts.Key( owner, piece ) );

	public void ToggleLayerHidden( int itemId ) {
		SetLayerHidden( itemId, !LayerHiddenSelf( itemId ) );

		ApplyVisibility();
	}

	// No walk — caller must call ApplyVisibility once after bulk changes
	public void SetLayerHidden( int itemId, bool hidden ) {
		if ( hidden ) {
			hiddenLayers.Add( itemId );
		} else {
			hiddenLayers.Remove( itemId );
		}
	}

	public void TogglePieceHidden( int owner, string piece ) {
		var key = ArchParts.Key( owner, piece );

		if ( !hiddenPieces.Add( key ) ) {
			hiddenPieces.Remove( key );
		}

		ApplyVisibility();
	}

	public void ShowEveryLayer() {
		hiddenLayers.Clear();
		hiddenPieces.Clear();
		ApplyVisibility();
	}

	public void ToggleLayerLocked( ArchLayerRef layer ) {
		if ( LayerTree.Find( layer.ItemId ) is not { } node || node.Payload is null ) {
			return;
		}

		var locked = !node.Locked;

		SetLayerLocked( layer, locked );

		// Banked, not committed — a lock moves no vertex
		Bank( locked ? $"Lock {node.Name}" : $"Unlock {node.Name}" );
	}

	// No commit — bulk callers set every row then commit once
	public void SetLayerLocked( ArchLayerRef layer, bool locked ) {
		if ( LayerTree.Find( layer.ItemId ) is not { } node || node.Payload is null ) {
			return;
		}

		LayerTree.Record( Plan, node ).Locked = locked;
	}

	public ArchSiteAssembly GroupLayers( IEnumerable<int> members, string name = null ) {
		var wanted = members.Where( id => id != 0 ).Distinct().ToList();

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

		var group = ArchLayerGroups.Create( Plan, name ?? $"Group {Plan.Assemblies.Count + 1}", ArchAssemblyKind.Group, wanted );

		Affect( group.Id );

		Touch( "Group Layers" );

		return group;
	}

	public void UngroupLayer( ArchSiteAssembly group ) {
		if ( group is null ) {
			return;
		}

		ArchLayerGroups.Dissolve( Plan, group );

		if ( EditedGroupId == group.Id ) {
			EditedGroupId = 0;
		}

		Commit( "Ungroup Layers" );
	}

	public void BeginChild( ArchLayerRef parent, ArchKind kind ) {
		InsertionTarget = parent;

		if ( ArchKindsAsked.Subtool( kind ) is { Length: > 0 } subtool ) {
			EditorToolManager.SetSubTool( subtool );
		}
	}

	public ArchBuilding CarryRequest { get; set; }

	public void BeginCarry( ArchBuilding building ) {
		CarryRequest = building;

		EditorToolManager.SetSubTool( nameof( ArchSelectSubtool ) );
	}

	public void SelectLayer( ArchLayerNode node ) {
		if ( node?.Payload is null ) {
			return;
		}

		Select( new ArchSelection { Item = node.Payload, Room = node.Room, Building = node.Building } );

		// The standing tool keeps focus if it can host this pick
		if ( CurrentTool is ArchSubtool standing && (standing is ArchSelectSubtool || standing.Hosts( node.Payload )) ) {
			return;
		}

		EditorToolManager.SetSubTool( nameof( ArchSelectSubtool ) );
	}
}