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

namespace Sunless.Architecture;

// Per-piece override — a piece nobody touched carries no record and follows whatever stands above it.
public sealed class ArchPartRecord {
	public int ItemId { get; set; }
	public string Piece { get; set; } = "";
	public ArchCollisionMode? Collision { get; set; }

	// Layer's own answer is the record at Piece == ""; shadows live here because no payload declares an interface for it.
	public bool? Shadows { get; set; }
}

// Piece path algebra — "Gutters/Fascia" etc. All piece questions answered here and nowhere else.
public static class ArchParts {
	public static string Joined( string piece, string name ) {
		return string.IsNullOrEmpty( piece ) ? name : $"{piece}/{name}";
	}

	public static string Above( string piece ) {
		var at = (piece ?? "").LastIndexOf( '/' );

		return at < 0 ? "" : piece[..at];
	}

	public static string Name( string piece ) {
		var at = (piece ?? "").LastIndexOf( '/' );

		return at < 0 ? piece ?? "" : piece[(at + 1)..];
	}

	// The editor-only key the eye hides by: a piece has no id of its own, so it is named by the layer that
	// built it and where it hangs on that layer.
	public static string Key( int itemId, string piece ) => $"{itemId}:{piece}";

	// Deepest segment carrying a plan id is the owner; everything below it is the piece path.
	public static void Split( string path, out int owner, out string piece ) {
		var segments = (path ?? "").Split( '/', System.StringSplitOptions.RemoveEmptyEntries );

		for ( var index = segments.Length - 1; index >= 0; index-- ) {
			if ( !ArchNames.TrySourceId( segments[index], out owner ) ) {
				continue;
			}

			piece = string.Join( "/", segments[(index + 1)..] );

			return;
		}

		owner = 0;
		piece = "";
	}

	public static ArchPartRecord Find( ArchPlan plan, int itemId, string piece ) {
		return plan?.Parts.FirstOrDefault( record => record.ItemId == itemId && record.Piece == piece );
	}

	public static ArchPartRecord Record( ArchPlan plan, int itemId, string piece ) {
		if ( Find( plan, itemId, piece ) is { } held ) {
			return held;
		}

		var record = new ArchPartRecord { ItemId = itemId, Piece = piece };

		plan.Parts.Add( record );

		return record;
	}

	// The nearest answer at or above this piece, stopping short of the layer itself - a fascia follows the
	// gutters it hangs on before it ever follows the roof they hang on.
	public static ArchCollisionMode? Asked( ArchPlan plan, int itemId, string piece ) {
		if ( plan is null || plan.Parts.Count == 0 ) {
			return null;
		}

		for ( var walked = piece; !string.IsNullOrEmpty( walked ); walked = Above( walked ) ) {
			if ( Find( plan, itemId, walked )?.Collision is { } mode ) {
				return mode;
			}
		}

		return null;
	}

	// The same walk, carried one step further than the collision one: it reads the layer's own record at the empty
	// piece, because that is where a layer's answer is kept rather than on its payload.
	public static bool? AskedShadows( ArchPlan plan, int itemId, string piece ) {
		if ( plan is null || plan.Parts.Count == 0 ) {
			return null;
		}

		for ( var walked = piece ?? ""; ; walked = Above( walked ) ) {
			if ( Find( plan, itemId, walked )?.Shadows is { } casts ) {
				return casts;
			}

			if ( string.IsNullOrEmpty( walked ) ) {
				return null;
			}
		}
	}

	public static bool ShowingShadows( ArchPlan plan, ArchKit kit, ArchLayerTree layers, int itemId, string piece, out bool own ) {
		var mine = Find( plan, itemId, piece )?.Shadows;

		own = mine.HasValue;

		return mine
			?? AskedShadows( plan, itemId, Above( piece ) )
			?? ArchShadows.Above( plan, layers?.Find( itemId ) )
			?? kit?.Shadows
			?? true;
	}

	public static string FollowingShadows( ArchPlan plan, ArchKit kit, ArchLayerTree layers, int itemId, string piece ) {
		if ( Above( piece ) is { Length: > 0 } above && AskedShadows( plan, itemId, above ) is { } inherited ) {
			return $"Follow {Name( above )} — {ArchShadows.Describe( inherited )}";
		}

		var node = layers?.Find( itemId );

		return node is null
			? $"Follow the kit — {ArchShadows.Describe( kit?.Shadows ?? true )}"
			: $"Follow {node.Name} — {ArchShadows.Describe( ArchShadows.Showing( plan, kit, node, out _ ) )}";
	}

	// What a row shows and what its menu says it is following, the way ArchCollision.Showing answers it for a
	// layer - one answer, so the badge and the menu cannot disagree about whether this piece owns its mode.
	public static ArchCollisionMode Showing( ArchPlan plan, ArchKit kit, ArchLayerTree layers, int itemId, string piece, out bool own ) {
		var mine = Find( plan, itemId, piece )?.Collision;

		own = mine.HasValue;

		return mine
			?? Asked( plan, itemId, Above( piece ) )
			?? (layers?.Find( itemId )?.Payload as IArchCollides)?.Collision
			?? ArchCollision.Above( layers?.Find( itemId ) )
			?? kit?.Physics
			?? ArchCollisionMode.Solids;
	}

	// Where a piece with no answer of its own gets one from, named so clearing an override says what will
	// answer instead.
	public static string Following( ArchPlan plan, ArchKit kit, ArchLayerTree layers, int itemId, string piece ) {
		if ( Above( piece ) is { Length: > 0 } above && Asked( plan, itemId, above ) is { } inherited ) {
			return $"Follow {Name( above )} — {ArchCollision.Describe( inherited )}";
		}

		var node = layers?.Find( itemId );
		var showing = ArchCollision.Showing( node, kit, out _ );

		return node is null
			? $"Follow the kit — {ArchCollision.Describe( kit?.Physics ?? ArchCollisionMode.Solids )}"
			: $"Follow {node.Name} — {ArchCollision.Describe( showing )}";
	}
}