Editor/Output/ArchAudit.Faces.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchFaceDetail {
	public string Piece { get; init; }
	public int Index { get; init; }
	public int HandleIndex { get; init; }
	public int LayerId { get; init; }
	public string Material { get; init; }
	public string Role { get; init; }
	public string Plane { get; init; }
	public Vector3 Normal { get; init; }
	public Vector3 Centre { get; init; }
	public Vector3 TextureAxisU { get; init; }
	public Vector3 TextureAxisV { get; init; }
	public float Area { get; init; }
	public BBox Bounds { get; init; }
	public Vector3[] Corners { get; init; } = Array.Empty<Vector3>();

	public string Size {
		get {
			var sides = new[] { Bounds.Size.x, Bounds.Size.y, Bounds.Size.z }
				.OrderByDescending( side => side )
				.ToArray();

			return $"{sides[0]:0.#} x {sides[1]:0.#} in";
		}
	}

	public string Label => $"face {Index} · {Role} · {Size} · {Plane}";

	public string Id => $"{LayerId}:{Piece.Split( '/' )[^1]}:{Index}";
}

public sealed class ArchFaceGroup {
	public string Name { get; init; }
	public BBox Bounds { get; init; }
	public List<ArchFaceGroup> Children { get; init; } = new();
	public List<ArchFaceDetail> Faces { get; init; } = new();

	public int Count => Faces.Count + Children.Sum( child => child.Count );

	public IEnumerable<ArchFaceDetail> Every => Faces.Concat( Children.SelectMany( child => child.Every ) );

	public bool Holds( ArchFaceDetail face ) => Faces.Contains( face ) || Children.Any( child => child.Holds( face ) );
}

public sealed class ArchFacePiece {
	public string Name { get; init; }
	public int LayerId { get; init; }
	public BBox Bounds { get; init; }
	public List<ArchFaceDetail> Faces { get; init; } = new();

	public List<ArchFaceGroup> Groups { get; init; } = new();
}

public static partial class ArchAudit {
	public static Dictionary<int, List<MeshComponent>> BuiltByLayer( Scene scene ) {
		var built = new Dictionary<int, List<MeshComponent>>();

		foreach ( var renderer in ArchScene.FindRoots( scene )
			.SelectMany( root => root.Components.GetAll<MeshComponent>( FindMode.EverythingInSelfAndDescendants ) )
			.Where( renderer => renderer.Mesh is not null ) ) {
			var layer = LayerOf( Path( renderer.GameObject ) );

			if ( layer == 0 ) {
				continue;
			}

			if ( !built.TryGetValue( layer, out var pieces ) ) {
				pieces = new List<MeshComponent>();
				built[layer] = pieces;
			}

			pieces.Add( renderer );
		}

		return built;
	}

	public static List<ArchFacePiece> Faces( ArchTool tool, ArchTarget target ) {
		return Faced( ArchReport.RolesByMaterial( tool, target ), Collect( target ) );
	}

	public static List<ArchFacePiece> Faces( ArchTool tool, IEnumerable<MeshComponent> meshes ) {
		var pieces = meshes
			.Where( renderer => renderer.IsValid() && renderer.Mesh is not null )
			.Select( renderer => Of( Path( renderer.GameObject ), renderer.Mesh, renderer.WorldTransform ) )
			.ToList();

		return Faced( ArchReport.RolesByMaterial( tool, ArchLocate.Find( tool, "plan" ) ), pieces );
	}

	static List<ArchFacePiece> Faced( IReadOnlyDictionary<string, string> roles, List<Piece> pieces ) {
		return pieces
			.Select( piece => Detailed( piece, roles ) )
			.Select( faces => new ArchFacePiece {
				Name = faces.Count > 0 ? faces[0].Piece : "",
				LayerId = faces.Count > 0 ? faces[0].LayerId : 0,
				Bounds = Extent( faces ),
				Faces = faces,
				Groups = Grouped( faces )
			} )
			.Where( piece => piece.Faces.Count > 0 )
			.ToList();
	}

	static List<ArchFaceGroup> Grouped( List<ArchFaceDetail> faces ) {
		var lumps = Lumps( faces );

		if ( lumps.Count < 2 ) {
			return new List<ArchFaceGroup>();
		}

		var bounds = Extent( faces );
		var groups = new List<ArchFaceGroup>();

		foreach ( var side in lumps.GroupBy( lump => Side( Extent( lump ), bounds ) ).OrderBy( side => side.Key ) ) {
			var ordered = side
				.OrderByDescending( lump => Extent( lump ).Size.Length )
				.ToList();
			var name = $"{side.Key} {Roled( ordered[0] )}";

			if ( ordered.Count == 1 ) {
				groups.Add( new ArchFaceGroup { Name = name, Bounds = Extent( ordered[0] ), Faces = ordered[0] } );

				continue;
			}

			var run = new ArchFaceGroup { Name = "Run", Bounds = Extent( ordered[0] ), Faces = ordered[0] };
			var group = new ArchFaceGroup { Name = name, Bounds = Extent( side.SelectMany( lump => lump ).ToList() ) };

			group.Children.Add( run );

			var along = Longest( run.Bounds );
			var fittings = ordered
				.Skip( 1 )
				.OrderBy( lump => Component( Extent( lump ).Center, along ) )
				.ToList();

			for ( var index = 0; index < fittings.Count; index++ ) {
				group.Children.Add( new ArchFaceGroup {
					Name = $"Fitting {index + 1}",
					Bounds = Extent( fittings[index] ),
					Faces = fittings[index]
				} );
			}

			groups.Add( group );
		}

		return groups;
	}

	static List<List<ArchFaceDetail>> Lumps( List<ArchFaceDetail> faces ) {
		var owner = Enumerable.Range( 0, faces.Count ).ToArray();
		var edges = new Dictionary<(long, long), int>();

		int Root( int index ) {
			while ( owner[index] != index ) {
				owner[index] = owner[owner[index]];
				index = owner[index];
			}

			return index;
		}

		for ( var index = 0; index < faces.Count; index++ ) {
			var corners = faces[index].Corners;

			for ( var corner = 0; corner < corners.Length; corner++ ) {
				var from = Key( corners[corner] );
				var to = Key( corners[(corner + 1) % corners.Length] );

				if ( from == to ) {
					continue;
				}

				var edge = from < to ? (from, to) : (to, from);

				if ( !edges.TryGetValue( edge, out var other ) ) {
					edges[edge] = index;

					continue;
				}

				var left = Root( other );
				var right = Root( index );

				if ( left != right ) {
					owner[right] = left;
				}
			}
		}

		return faces
			.Select( ( face, index ) => (Face: face, Lump: Root( index )) )
			.GroupBy( entry => entry.Lump )
			.Select( group => group.Select( entry => entry.Face ).ToList() )
			.ToList();
	}

	public static List<List<ArchFaceDetail>> Connected( IEnumerable<ArchFaceDetail> faces ) {
		return faces
			.GroupBy( face => face.Piece )
			.SelectMany( piece => Lumps( piece.ToList() ) )
			.ToList();
	}

	public static List<List<ArchFaceDetail>> PhysicallyConnected( IEnumerable<ArchFaceDetail> faces ) {
		return Lumps( faces.ToList() );
	}

	static string Side( BBox lump, BBox piece ) {
		var offset = lump.Center - piece.Center;
		var size = piece.Size;
		var share = new Vector3(
			size.x > 1f ? offset.x / size.x : 0f,
			size.y > 1f ? offset.y / size.y : 0f,
			size.z > 1f ? offset.z / size.z : 0f );

		if ( MathF.Abs( share.x ) >= MathF.Abs( share.y ) && MathF.Abs( share.x ) >= MathF.Abs( share.z ) ) {
			return share.x >= 0f ? "East" : "West";
		}

		if ( MathF.Abs( share.y ) >= MathF.Abs( share.z ) ) {
			return share.y >= 0f ? "North" : "South";
		}

		return share.z >= 0f ? "Upper" : "Lower";
	}

	static string Roled( List<ArchFaceDetail> lump ) {
		return lump
			.GroupBy( face => face.Role )
			.OrderByDescending( role => role.Count() )
			.Select( role => role.Key )
			.First();
	}

	static BBox Extent( List<ArchFaceDetail> faces ) {
		var extent = new ArchExtent();

		foreach ( var corner in faces.SelectMany( face => face.Corners ) ) {
			extent.Add( corner );
		}

		return extent.Box;
	}

	static int Longest( BBox bounds ) {
		if ( bounds.Size.x >= bounds.Size.y && bounds.Size.x >= bounds.Size.z ) {
			return 0;
		}

		return bounds.Size.y >= bounds.Size.z ? 1 : 2;
	}

	static float Component( Vector3 point, int axis ) => axis switch { 0 => point.x, 1 => point.y, _ => point.z };

	// All faces under the cursor, nearest first — returns both halves of z-fights
	public static List<ArchFaceDetail> FacesAt( IEnumerable<ArchFacePiece> pieces, Ray ray ) {
		var hits = new List<(float Along, ArchFaceDetail Face)>();

		foreach ( var face in pieces.SelectMany( piece => piece.Faces ) ) {
			var facing = Vector3.Dot( face.Normal, ray.Forward );

			if ( MathF.Abs( facing ) < 0.001f ) {
				continue;
			}

			var along = Vector3.Dot( face.Centre - ray.Position, face.Normal ) / facing;

			if ( along <= 1f ) {
				continue;
			}

			if ( !Inside( face, ray.Position + ray.Forward * along ) ) {
				continue;
			}

			hits.Add( (along, face) );
		}

		return hits
			.OrderBy( hit => hit.Along )
			.Select( hit => hit.Face )
			.ToList();
	}

	static bool Inside( ArchFaceDetail face, Vector3 at ) {
		Basis( ArchFaceOverlap.Canonical( face.Normal ), out var right, out var up );

		return Contains( Flatten( face.Corners, face.Centre, right, up ), Flatten( new[] { at }, face.Centre, right, up )[0] );
	}

	public static List<ArchFaceDetail> Fighting( IEnumerable<ArchFacePiece> pieces, ArchFaceDetail face ) {
		var plane = ArchFaceOverlap.PlaneKey( face.Normal, face.Centre );

		return pieces
		.SelectMany( piece => piece.Faces )
		.Where( other => other.Id != face.Id
			&& ArchFaceOverlap.PlaneKey( other.Normal, other.Centre ) == plane
			&& Overlapping( face.Corners, other.Corners, face.Normal, face.Centre ) )
		.ToList();
	}

	public static string FaceDebug( ArchTool tool, IReadOnlyList<ArchFaceDetail> faces ) {
		if ( faces is not { Count: > 0 } ) {
			return "no faces picked - open a layer's GEOMETRY rows in Plan Layers and select one or more faces";
		}

		var context = tool.EveryBuiltPiece().ToList();
		var rows = new List<string[]> { new[] { "face", "piece", "role", "n", "x", "y", "z" } };

		foreach ( var face in faces ) {
			rows.Add( new[]
			{
				face.Id, Short( face.Piece ), face.Role, Say( face.Normal ),
				Span( face.Bounds.Mins.x, face.Bounds.Maxs.x ), Span( face.Bounds.Mins.y, face.Bounds.Maxs.y ), Span( face.Bounds.Mins.z, face.Bounds.Maxs.z )
			} );
		}

		var lines = new List<string> { $"arch faces {faces.Count} (in)" };

		lines.AddRange( Tabled( rows ) );

		foreach ( var face in faces ) {
			foreach ( var other in Fighting( context, face ) ) {
				var facing = Vector3.Dot( face.Normal, other.Normal ) < 0f ? "back-to-back" : "stacked";

				lines.Add( $"zfight {face.Id} x {other.Id} ({other.Role}) {facing}" );
			}

			foreach ( var piece in Through( face, context ) ) {
				lines.Add( $"inside {face.Id} in {Short( piece )}" );
			}
		}

		lines.Add( "" );
		lines.Add( "User Mentions:" );
		lines.Add( "" );

		return string.Join( "\n", lines );
	}

	static IEnumerable<string> Tabled( List<string[]> rows ) {
		var widths = new int[rows[0].Length];

		foreach ( var row in rows ) {
			for ( var column = 0; column < row.Length; column++ ) {
				widths[column] = Math.Max( widths[column], row[column].Length );
			}
		}

		return rows.Select( row => string.Join( "  ", row.Select( ( cell, column ) => cell.PadRight( widths[column] ) ) ).TrimEnd() );
	}

	static string Short( string piece ) {
		var segments = piece.Split( '/', StringSplitOptions.RemoveEmptyEntries );

		return string.Join( "/", segments.Skip( Math.Max( 0, segments.Length - 2 ) ) );
	}

	static string Span( float min, float max ) {
		return max - min < 0.05f ? $"{min:0.#}" : $"{min:0.#}..{max:0.#}";
	}

	public static string FaceBriefing( ArchTool tool, IReadOnlyList<ArchFaceDetail> faces ) {
		if ( faces is not { Count: > 0 } ) {
			return "no faces picked - pick them in the viewport with Selection > Geometry, or in the Plan Layers stack";
		}

		var context = tool.EveryBuiltPiece().ToList();
		var fights = new List<string>();
		var inside = new List<string>();
		var rows = new List<string>();
		var paired = new HashSet<string>();

		foreach ( var face in faces ) {
			rows.Add( $"{face.Id} · {face.Role} · {face.Plane} · {face.Size} · c {Say( face.Centre )} · n {Say( face.Normal )} · {face.Area:0.#} sq in" );

			foreach ( var other in Fighting( context, face ) ) {
				if ( !paired.Add( string.CompareOrdinal( face.Id, other.Id ) < 0 ? $"{face.Id}|{other.Id}" : $"{other.Id}|{face.Id}" ) ) {
					continue;
				}

				var facing = Vector3.Dot( face.Normal, other.Normal ) < 0f ? "back-to-back" : "stacked";

				fights.Add( $"{face.Id} × {other.Id} · {facing} on {face.Plane} · {other.Role} · {other.Area:0.#} sq in" );
			}

			foreach ( var piece in Through( face, context ) ) {
				inside.Add( $"{face.Id} inside {piece}" );
			}
		}

		var lines = new List<string>
		{
			"# ARCH FACE BRIEFING",
			"",
			"WHAT THIS IS. A measurement report from the Risk of Pain Architecture tool - the editor-only",
			"building generator in the Sunless arch libraries. Someone picked these faces in the editor because",
			"something about them looks wrong on screen, and pasted this to you. Alongside it they will",
			"usually say what they are seeing, or show a picture; this is the measured half of that. Every",
			"number is read off the geometry standing in the open scene, not inferred.",
			"",
			$"SCOPE. {faces.Count} face(s) picked, out of {context.Count} built pieces in the scene.",
			$"VERDICT: {Verdict( fights.Count, inside.Count )}",
			"",
			"HOW TO READ IT",
			"- FACES: one line per picked face. The id is layer:piece:index and can be asked about again.",
			"  plane is the single number locating an axis-aligned face - what two fighting faces have in",
			"  common. size is its two real extents, c its centre, n its normal. Units are inches.",
			"- SHARES A PLANE: two faces lying on one plane AND overlapping there. That is a z-fight: it",
			"  flickers as the camera moves. back-to-back means they face away from each other, stacked",
			"  means the same way. Every bite and inset constant in the generators exists to avoid this.",
			"- STANDS INSIDE ANOTHER PART: this face's centre falls within another piece's extent, so it is",
			"  buried or driving through it. Not proof on its own, but no finished surface should be there.",
			"- LAYERS: where each face's layer sits in the authored plan, as a path.",
			"",
			"START HERE, IN THIS ORDER",
			"1. Load the `architecture` skill. It is the map to this generator; without it you will read a",
			"   lot of files to learn what one page already says.",
			"2. Re-query these exact faces by id over the editor's MCP server (registered as `sbox`) - do",
			"   not go looking for them yourself, they are already identified:",
			$"      arch_faces target={string.Join( ",", faces.Select( face => face.Id ) )}",
			"   That returns this same report, live, and every id in it can be asked about the same way.",
			"3. Only then widen, and only with measurements:",
			"      arch_audit   target=<part> checks=coplanar,gaps,intersections",
			"      arch_measure target=<part>   planned extent beside built extent, per role",
			"   Targets take 'wall:18', 'room:17', 'walkway:17', a kind keyword like 'walkways', or",
			"   'x,y,z@radius' for everything near a point. Call arch_activate first if a call comes back",
			"   saying the tool is not active.",
			"",
			"DO NOT render pictures of your own to work this out - arch_view exists but the numbers here are",
			"the evidence, and a render only shows what you framed. Generate an image only if the person",
			"explicitly asks to see one.",
			"",
			"The fix belongs in whatever GENERATOR emitted the face, in the arch libraries, never in the",
			"scene: the scene is regenerated from the plan and hand edits are thrown away.",
			"",
			"FACES  id · role · plane · size · centre · normal · area"
		};

		lines.AddRange( rows );

		lines.Add( "" );
		lines.Add( "SHARES A PLANE (z-fights)" );
		lines.AddRange( fights.Count > 0 ? fights : new List<string> { "none" } );

		lines.Add( "" );
		lines.Add( "STANDS INSIDE ANOTHER PART" );
		lines.AddRange( inside.Count > 0 ? inside : new List<string> { "none" } );

		lines.Add( "" );
		lines.Add( "LAYERS" );
		lines.AddRange( faces
			.Select( face => face.LayerId )
			.Distinct()
			.Select( layer => $"{layer} · {Breadcrumb( tool, layer )}" ) );

		return string.Join( "\n", lines );
	}

	static string Verdict( int fights, int inside ) {
		if ( fights == 0 && inside == 0 ) {
			return "nothing shares a plane with these and none stands inside another part - whatever is wrong here is not a z-fight";
		}

		var said = new List<string>();

		if ( fights > 0 ) {
			said.Add( $"{fights} coplanar overlap(s) - these WILL flicker" );
		}

		if ( inside > 0 ) {
			said.Add( $"{inside} face(s) standing inside another part - buried, or driving through it" );
		}

		return string.Join( ", ", said );
	}

	static IEnumerable<string> Through( ArchFaceDetail face, List<ArchFacePiece> context ) {
		foreach ( var piece in context ) {
			if ( piece.Name == face.Piece || !piece.Bounds.Grow( -Closing ).Contains( face.Centre ) ) {
				continue;
			}

			yield return piece.Name;
		}
	}

	static List<ArchFaceDetail> Detailed( Piece piece, IReadOnlyDictionary<string, string> roles ) {
		var faces = new List<ArchFaceDetail>();
		var layer = LayerOf( piece.Name );

		for ( var index = 0; index < piece.Faces.Count; index++ ) {
			var face = piece.Faces[index];
			var extent = new ArchExtent();

			foreach ( var corner in face.Corners ) {
				extent.Add( corner );
			}

			faces.Add( new ArchFaceDetail {
				Piece = piece.Name,
				Index = index,
				HandleIndex = face.HandleIndex,
				LayerId = layer,
				Material = face.Material,
				Role = roles.TryGetValue( face.Material, out var role ) ? role : "unbound",
				Plane = Stated( face.Normal, face.Centre ),
				Normal = face.Normal,
				Centre = face.Centre,
				TextureAxisU = face.TextureAxisU,
				TextureAxisV = face.TextureAxisV,
				Area = face.Area,
				Bounds = extent.Box,
				Corners = face.Corners
			} );
		}

		return faces;
	}

	static string Stated( Vector3 normal, Vector3 centre ) {
		if ( MathF.Abs( normal.x ) > 0.999f ) {
			return $"x = {centre.x:0.##}";
		}

		if ( MathF.Abs( normal.y ) > 0.999f ) {
			return $"y = {centre.y:0.##}";
		}

		if ( MathF.Abs( normal.z ) > 0.999f ) {
			return $"z = {centre.z:0.##}";
		}

		return $"n {Say( normal )} · d {Vector3.Dot( normal, centre ):0.##}";
	}

	public static int LayerOf( string piece ) {
		var segments = (piece ?? string.Empty).Split( '/', StringSplitOptions.RemoveEmptyEntries );

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

		return 0;
	}

	static string Breadcrumb( ArchTool tool, int layerId ) {
		return tool.LayerTree.Find( layerId ) is { } node ? tool.LayerTree.Breadcrumb( node ) : "not in the plan";
	}

	public static ArchFaceDetail FaceById( ArchTool tool, string id ) {
		var parts = (id ?? string.Empty).Split( ':' );

		if ( parts.Length != 3 || !int.TryParse( parts[0], out var layer ) || !int.TryParse( parts[2], out var index ) ) {
			return null;
		}

		return tool.BuiltFaces( layer )
			.Where( piece => piece.Name.Split( '/' )[^1] == parts[1] )
			.SelectMany( piece => piece.Faces )
			.FirstOrDefault( face => face.Index == index );
	}
}