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

namespace Sunless.Architecture;

// One shallow room behind one opening on a facade nobody can walk into. DERIVED, so it carries no id and no
// layer record - exactly as slabs, ceilings, roof decks and platforms carry none.
public sealed class ArchFalseInterior {
	public ArchBuilding Building { get; init; }
	public ArchRoom Room { get; init; }
	public ArchWall Wall { get; init; }
	public ArchOpening Opening { get; init; }
	// The room face of the wall, in the wall's own frame - the plane the box is measured off.
	public float Face { get; init; }
	public float Depth { get; init; }

	public float Back => Face + Depth;
}

public static partial class ArchCull {
	// No way in and nothing above, asked of a whole unit rather than one of its roofs: a shell with no door
	// and no archway, capped everywhere, is a facade. Anything else is a room, or becoming one.
	public static bool Unenterable( ArchBuilding building, ArchPlan plan, ArchKit kit ) {
		if ( building is null ) {
			return false;
		}

		var walls = building.Rooms.SelectMany( room => room.Walls.Select( wall => (Room: room, Wall: wall) ) ).ToList();

		if ( walls.Count == 0 ) {
			return false;
		}

		// A single-sided shell emits no room face at all, so there is nothing in there to be in.
		if ( walls.All( standing => ArchWallSection.SingleSided( standing.Wall, standing.Room ) ) ) {
			return true;
		}

		if ( walls.SelectMany( standing => standing.Wall.Openings ).Any( Entered ) ) {
			return false;
		}

		return building.Roofs.Count > 0 && building.Roofs.All( roof => Sealed( roof, building, plan, kit ) );
	}

	// A leaf hangs in a way in and an archway is a hole walked straight through. A window is neither.
	static bool Entered( ArchOpening opening ) {
		return opening.Kind.Hangs() || opening.Kind == OpeningKind.Archway;
	}

	// Every box the plan asks for, resolved without a scene, so the pass, the report and the test all read one
	// answer. A unit ArchCull calls enterable contributes none: behind a real room the box would z-fight the
	// room it is standing inside.
	public static List<ArchFalseInterior> Behind( ArchPlan plan, ArchKit kit ) {
		var found = new List<ArchFalseInterior>();

		if ( plan is null ) {
			return found;
		}

		var depth = MathF.Max( 1f, kit.FalseInteriorDepth );

		foreach ( var building in plan.Buildings.Where( unit => Unenterable( unit, plan, kit ) ) ) {
			foreach ( var room in building.Rooms ) {
				foreach ( var wall in room.Walls.Where( wall => !ArchWallJoins.CoveredBy( building, room, wall ) ) ) {
					var face = ArchWallSection.Thickness( wall, kit ) * 0.5f;

					found.AddRange( Showing( wall ).Select( opening => new ArchFalseInterior {
						Building = building,
						Room = room,
						Wall = wall,
						Opening = opening,
						Face = face,
						Depth = depth
					} ) );
				}
			}
		}

		return found;
	}

	// The units the wall generator actually cut a hole for - the same filter it runs, because a box behind a
	// hole nothing opened is a box hanging in a solid wall.
	static IEnumerable<ArchOpening> Showing( ArchWall wall ) {
		return wall.Openings
			.Where( opening => opening.Width > 0.5f && opening.Height > 0.5f )
			.Where( opening => ArchLayerGate.On( opening ) && ArchLayerGate.Owned( opening.OwnerId ) )
			.Where( opening => opening.SwallowedBy == 0 || !ArchLayerGate.Owned( opening.SwallowedBy ) );
	}

	// The box, in the wall's own frame, wound to face the hole. Its front IS the hole: a face on the wall's
	// room plane would be the sticker this exists to remove.
	public static void Line( ArchMesh canvas, ArchFalseInterior interior, ArchBrush brush ) {
		var opening = interior.Opening;
		var left = opening.Left;
		var right = opening.Right;
		var bottom = MathF.Max( 0f, opening.SillHeight );
		var top = opening.Top;
		var face = interior.Face;
		var back = interior.Back;

		if ( right - left < 0.5f || top - bottom < 0.5f || interior.Depth < 0.5f ) {
			return;
		}

		canvas.Quad(
			new Vector3( left, back, bottom ),
			new Vector3( right, back, bottom ),
			new Vector3( right, back, top ),
			new Vector3( left, back, top ),
			brush );

		canvas.Quad(
			new Vector3( left, face, bottom ),
			new Vector3( left, back, bottom ),
			new Vector3( left, back, top ),
			new Vector3( left, face, top ),
			brush );

		canvas.Quad(
			new Vector3( right, back, bottom ),
			new Vector3( right, face, bottom ),
			new Vector3( right, face, top ),
			new Vector3( right, back, top ),
			brush );

		canvas.Quad(
			new Vector3( left, face, top ),
			new Vector3( left, back, top ),
			new Vector3( right, back, top ),
			new Vector3( right, face, top ),
			brush );

		canvas.Quad(
			new Vector3( left, back, bottom ),
			new Vector3( left, face, bottom ),
			new Vector3( right, face, bottom ),
			new Vector3( right, back, bottom ),
			brush );
	}

	// The pass on its own, for when the faces have already been cleaned. A rebuild prunes what it emits, the
	// same way it takes back the faces Clean removed.
	public static int Interiors( Scene scene, ArchPlan plan, ArchKit kit ) {
		var root = ArchScene.FindRoot( scene );

		if ( !root.IsValid() ) {
			Log.Warning( "Architecture: nothing to line - no generated root in this scene." );

			return 0;
		}

		using ( SceneEditorSession.Active.UndoScope( "Line False Interiors" ).WithGameObjectChanges( root, GameObjectUndoFlags.All ).Push() ) {
			return Lined( root, plan, kit );
		}
	}

	// Re-derived every run - a door added to a facade makes it a room, and the box behind its window has to GO rather
	// than be left standing inside it - but only HANDED OVER where the box actually changed. Drawing one is five
	// quads; giving it to the engine cooks a collision hull, a physics mesh and a trace mesh, and this pass reaches
	// every window on every facade in the plan.
	//
	// A null cache lines them all, which is what the menu action and a scene nobody has built through mean.
	public static int Lined( GameObject root, ArchPlan plan, ArchKit kit, ArchBuildCache cache = null ) {
		var nodes = Walls( root );
		var style = new ArchStyle( kit );
		var wanted = Behind( plan, kit ).GroupBy( interior => interior.Wall.Id ).ToDictionary( group => group.Key, group => group.ToList() );
		var lined = 0;

		foreach ( var bare in nodes.Where( entry => !wanted.ContainsKey( entry.Key ) ) ) {
			Strip( bare.Value );
			cache?.Unlined( bare.Key );
		}

		foreach ( var group in wanted ) {
			if ( !nodes.TryGetValue( group.Key, out var node ) ) {
				continue;
			}

			// The wall's own frame, so the boxes are drawn in the coordinates the wall generator uses and the
			// projection puts them on the same grain as the room face they hang behind.
			var canvas = new ArchMesh( node.WorldTransform );

			foreach ( var interior in group.Value ) {
				Line( canvas, interior, style.Brush(
					ArchSurface.WallInterior, interior.Wall.Palette, interior.Room.Palette, interior.Building.Palette ) );

				lined++;
			}

			// Asked of the box's own key, the way a part is - and of the scene as well, because a hand-delete leaves
			// the cache saying something is standing that is not.
			if ( cache?.Lined( group.Key, canvas.Content ) == true && Standing( node ) ) {
				continue;
			}

			Strip( node );
			Fit( node, canvas );
		}

		return lined;
	}

	static void Strip( GameObject wall ) {
		if ( wall.Children.FirstOrDefault( child => child.Name == ArchPieces.Interior ) is { } standing ) {
			standing.DestroyImmediate();
		}
	}

	static bool Standing( GameObject wall ) {
		return wall.Children.Any( child => child.Name == ArchPieces.Interior );
	}

	static Dictionary<int, GameObject> Walls( GameObject root ) {
		var nodes = new Dictionary<int, GameObject>();

		foreach ( var node in ArchScene.Descendants( root ) ) {
			if ( ArchNames.TryParseId( node.Name, "Wall", out var id ) ) {
				nodes[id] = node;
			}
		}

		return nodes;
	}

	static void Fit( GameObject wall, ArchMesh canvas ) {
		if ( canvas.IsEmpty ) {
			return;
		}

		var node = wall.Scene.CreateObject();

		node.Name = ArchPieces.Interior;
		node.SetParent( wall, false );
		node.Tags.Add( ArchScene.GeneratedTag );

		var renderer = node.Components.GetOrCreate<MeshComponent>();

		renderer.Color = Color.White;
		renderer.SmoothingAngle = 0f;

		// It must not be traceable: the cleaning pass reads cover by ray, and a box behind a window it could
		// hit would have the elevation in front of it stripped as buried.
		ArchCollision.Write( node, renderer, canvas, ArchCollisionMode.None );

		renderer.Mesh = canvas.Finish();
	}
}