Editor/Effigy/MeshHoleRepairFragment.cs
using System;
using System.Collections.Generic;

namespace Effigy;

/// <summary>
/// Closing a mouth that lies inside a surface which has already been taken apart — cutting a body
/// that has been cut before.
///
/// WHY THE OTHER THREE ALL DECLINE IT. The single-face repair closes a hole by triangulating the
/// face around it, so the surface it fixed is a fan afterwards. Cut into that same surface again
/// and the second mouth lands across a dozen coplanar triangles: no single face contains it, there
/// are far more than two candidates, and the crossings where it passes from one triangle to the
/// next are ordinary points on those triangles' edges rather than vertices — which is the one thing
/// MeshHoleRepairCurved refuses without, because inventing a crossing means splitting a face it was
/// not asked to touch.
///
/// THE ANSWER IS TO STOP TREATING THE FRAGMENTS AS FACES. They are one surface that a previous
/// repair happened to leave in pieces, and the mouth is a hole in that SURFACE. So the whole
/// coplanar group is taken as one region — outer contour, plus whatever holes it already has — the
/// new mouth is added as one more hole, and the group is rebuilt from the loops. No crossing has to
/// be named, because the mouth never crosses anything: it is strictly inside the region.
///
/// THAT MAKES THIS THE LAST RESORT AND IT IS ORDERED LAST FOR A REASON. Rebuilding the group throws
/// away the partition the fragments had. Where the earlier repairs apply, they preserve it — the
/// span repair notches two faces and leaves them two faces — and this one would flatten it. So it
/// only ever sees loops all three of the others walked away from.
///
/// IT REFUSES THE SAME WAY THEY DO. The group must be coplanar with the loop, share one material,
/// have exactly one outer contour, and contain the mouth strictly inside it and outside every hole
/// it already has. And the result is measured before it is kept — see CloseLoopsInFragments.
/// </summary>
public static class MeshHoleRepairFragment
{
	const float PlaneTolerance = 1e-3f;
	const float NormalTolerance = 0.999f;

	/// <summary>
	/// Close every mouth lying inside a fragmented coplanar surface, and return how many.
	///
	/// Run LAST. Each repair is applied to a copy and rolled back unless it strictly reduced the
	/// open boundary without making anything non-manifold — the same guarantee MeshHoleRepairCurved
	/// gives, and for the same reason: rebuilding a group of faces is far too big a hammer to swing
	/// on trust.
	/// </summary>
	public static int CloseLoopsInFragments( PolyMesh mesh )
	{
		if ( mesh is null || mesh.FaceCount == 0 )
			return 0;

		var closed = 0;

		while ( true )
		{
			var loops = BoundaryLoops( mesh );
			var progressed = false;

			foreach ( var loop in loops )
			{
				if ( loop.Count < 3 )
					continue;

				var before = MeshValidator.Validate( mesh );
				var snapshot = mesh.Faces;

				mesh.Faces = CloneFaces( snapshot );

				if ( !TryCloseInGroup( mesh, loop ) )
				{
					mesh.Faces = snapshot;
					continue;
				}

				var after = MeshValidator.Validate( mesh );

				if ( after.BoundaryEdges >= before.BoundaryEdges
					|| after.NonManifoldEdges > before.NonManifoldEdges
					|| !after.IsValid )
				{
					mesh.Faces = snapshot;
					continue;
				}

				closed++;
				progressed = true;
				break;
			}

			if ( !progressed )
				return closed;
		}
	}

	static List<Face> CloneFaces( List<Face> faces )
	{
		var copy = new List<Face>( faces.Count );

		foreach ( var f in faces )
			copy.Add( new Face( (int[])f.Indices.Clone(), (Vec2[])f.UVs.Clone(), f.Material ) );

		return copy;
	}

	// --- the repair -------------------------------------------------------------------------------

	static bool TryCloseInGroup( PolyMesh mesh, List<int> loop )
	{
		var normal = LoopNormal( mesh, loop );

		// A mouth with no plane of its own is the curved case, and belongs to the repair that reads
		// its answer off the wall rather than off a plane.
		if ( normal.LengthSquared < 1e-20f )
			return false;

		normal = normal.Normal;

		Basis( normal, out var u, out var v );

		var plane = Vec3.Dot( mesh.Positions[loop[0]], normal );
		var group = CoplanarGroup( mesh, loop, normal, plane, out var material );

		// One face is the single-face repair's own case, and it does it better: it splices the hole
		// into that face and leaves the rest of the mesh alone.
		if ( group.Count < 2 )
			return false;

		if ( !GroupBoundary( mesh, group, out var rings ) )
			return false;

		// Flattened once, here, so every containment test below is asking about the same numbers.
		var flat = new List<List<Vec2>>( rings.Count );

		foreach ( var ring in rings )
		{
			var polygon = new List<Vec2>( ring.Count );

			foreach ( var index in ring )
				polygon.Add( Flatten( mesh.Positions[index], u, v ) );

			flat.Add( polygon );
		}

		// The biggest ring is the outer contour and the rest are holes the surface already had. A
		// group with no ring big enough to hold the others is not one surface.
		var outer = 0;

		for ( var i = 1; i < flat.Count; i++ )
		{
			if ( MathF.Abs( SignedArea( flat[i] ) ) > MathF.Abs( SignedArea( flat[outer] ) ) )
				outer = i;
		}

		var mouth = new List<Vec2>( loop.Count );

		foreach ( var index in loop )
			mouth.Add( Flatten( mesh.Positions[index], u, v ) );

		// STRICTLY INSIDE THE CONTOUR AND OUTSIDE EVERY EXISTING HOLE. A mouth failing either is not
		// a hole in this surface, and closing it would seal something that belongs elsewhere.
		foreach ( var p in mouth )
		{
			if ( !PointInPolygon( flat[outer], p ) )
				return false;
		}

		for ( var i = 0; i < flat.Count; i++ )
		{
			if ( i == outer )
				continue;

			foreach ( var p in mouth )
			{
				if ( PointInPolygon( flat[i], p ) )
					return false;
			}
		}

		// The loops, in the order the triangulator indexes them: outer first, then every hole the
		// surface already had, then the new mouth.
		var holeRings = new List<List<int>>();
		var holePolygons = new List<IReadOnlyList<Vec2>>();

		for ( var i = 0; i < flat.Count; i++ )
		{
			if ( i == outer )
				continue;

			holeRings.Add( rings[i] );
			holePolygons.Add( flat[i] );
		}

		holeRings.Add( loop );
		holePolygons.Add( mouth );

		var combined = new List<int>( rings[outer] );

		foreach ( var ring in holeRings )
			combined.AddRange( ring );

		// N-GONS RATHER THAN A FAN WHERE THE SHAPE ALLOWS IT, for the reason Triangulate.SplitWithHoles
		// gives: a Face is the unit of selection and of material assignment, so a surface returned as
		// thirty triangles is a surface you paint a thirtieth of per click. Falling back to the ear
		// clipper when it will not split is right - coarse and correct beats refusing.
		var faces = new List<int[]>();
		var split = Triangulate.SplitWithHoles( flat[outer], holePolygons );

		if ( split is { Count: > 0 } )
		{
			foreach ( var ring in split )
				faces.Add( Map( ring, combined ) );
		}
		else
		{
			var triangles = Triangulate.WithHoles( flat[outer], holePolygons );

			if ( triangles.Count == 0 )
				return false;

			foreach ( var (a, b, c) in triangles )
				faces.Add( Map( new List<int> { a, b, c }, combined ) );
		}

		var uvs = KnownUVs( mesh, group );

		Basis( normal, out var mapU, out var mapV );

		if ( !UVMap( mesh, group, mapU, mapV, out var origin, out var originUv, out var scaleU, out var scaleV ) )
			return false;

		var rebuilt = new List<Face>( mesh.FaceCount + faces.Count );
		var members = new HashSet<int>( group );
		var written = false;

		for ( var fi = 0; fi < mesh.FaceCount; fi++ )
		{
			if ( !members.Contains( fi ) )
			{
				rebuilt.Add( mesh.Faces[fi] );
				continue;
			}

			if ( written )
				continue;

			written = true;

			foreach ( var indices in faces )
			{
				if ( indices.Length < 3 || !Distinct( indices ) )
					return false;

				var corners = new Vec2[indices.Length];

				for ( var i = 0; i < indices.Length; i++ )
				{
					if ( uvs.TryGetValue( indices[i], out var existing ) )
					{
						corners[i] = existing;
						continue;
					}

					var p = Flatten( mesh.Positions[indices[i]], mapU, mapV ) - origin;
					corners[i] = new Vec2( originUv.x + p.x * scaleU, originUv.y + p.y * scaleV );
				}

				rebuilt.Add( new Face( indices, corners, material ) );
			}
		}

		mesh.Faces = rebuilt;
		return true;
	}

	static int[] Map( List<int> ring, List<int> combined )
	{
		var mapped = new int[ring.Count];

		for ( var i = 0; i < ring.Count; i++ )
			mapped[i] = combined[ring[i]];

		return mapped;
	}

	static bool Distinct( int[] indices )
	{
		var seen = new HashSet<int>();

		foreach ( var i in indices )
		{
			if ( !seen.Add( i ) )
				return false;
		}

		return true;
	}

	/// <summary>
	/// The connected run of faces sharing the loop's plane and one material slot.
	///
	/// Connected THROUGH SHARED EDGES, because that is what makes it one surface rather than two
	/// that happen to be level with each other — a lid and a pocket floor two units below it are
	/// coplanar in normal and nothing else, and welding them would be nonsense. Material is part of
	/// the identity for the reason CoplanarMerge gives: two coplanar neighbours painted differently
	/// are two faces because somebody made them two faces.
	/// </summary>
	static List<int> CoplanarGroup( PolyMesh mesh, List<int> loop, Vec3 normal, float plane, out int material )
	{
		material = 0;

		var loopEdges = new HashSet<EdgeKey>();

		for ( var i = 0; i < loop.Count; i++ )
			loopEdges.Add( new EdgeKey( loop[i], loop[(i + 1) % loop.Count] ) );

		var eligible = new List<int>();

		for ( var fi = 0; fi < mesh.FaceCount; fi++ )
		{
			var face = mesh.Faces[fi];

			if ( face.Count < 3 )
				continue;

			var faceNormal = mesh.FaceNormal( face );

			// The SIGNED dot, not its magnitude: a face pointing the other way is the far side of a
			// zero-thickness sliver, never part of this surface.
			if ( Vec3.Dot( faceNormal, normal ) < NormalTolerance )
				continue;

			if ( MathF.Abs( Vec3.Dot( mesh.Positions[face.Indices[0]], normal ) - plane ) > PlaneTolerance )
				continue;

			var isWall = false;

			for ( var i = 0; i < face.Count; i++ )
			{
				if ( loopEdges.Contains( new EdgeKey( face.Indices[i], face.Indices[(i + 1) % face.Count] ) ) )
				{
					isWall = true;
					break;
				}
			}

			if ( !isWall )
				eligible.Add( fi );
		}

		if ( eligible.Count == 0 )
			return eligible;

		// Which of the eligible faces the mouth actually sits in decides the group, so the walk
		// starts from the one containing the loop's first vertex and spreads through shared edges.
		Basis( normal, out var u, out var v );

		var seed = -1;
		var point = Flatten( mesh.Positions[loop[0]], u, v );

		foreach ( var fi in eligible )
		{
			var polygon = Polygon( mesh, mesh.Faces[fi], u, v );

			if ( PointInPolygon( polygon, point ) || OnBoundary( polygon, point ) )
			{
				seed = fi;
				break;
			}
		}

		if ( seed < 0 )
			return new List<int>();

		material = mesh.Faces[seed].Material;

		var pool = new HashSet<int>();

		foreach ( var fi in eligible )
		{
			if ( mesh.Faces[fi].Material == material )
				pool.Add( fi );
		}

		var edgeFaces = mesh.BuildEdgeFaces();
		var group = new List<int>();
		var visited = new HashSet<int> { seed };
		var queue = new Queue<int>();

		queue.Enqueue( seed );

		while ( queue.Count > 0 )
		{
			var fi = queue.Dequeue();
			group.Add( fi );

			var face = mesh.Faces[fi];

			for ( var i = 0; i < face.Count; i++ )
			{
				var key = new EdgeKey( face.Indices[i], face.Indices[(i + 1) % face.Count] );

				if ( !edgeFaces.TryGetValue( key, out var users ) )
					continue;

				foreach ( var other in users )
				{
					if ( other == fi || !pool.Contains( other ) || !visited.Add( other ) )
						continue;

					queue.Enqueue( other );
				}
			}
		}

		group.Sort();
		return group;
	}

	/// <summary>
	/// The group's own boundary: the edges exactly one of its faces uses, chained into loops.
	///
	/// Internal edges — the ones two group faces share — are what the fragmentation is made of and
	/// are exactly what this discards. What is left is the surface's real outline plus the holes it
	/// already had. The mouth is not among them: its edges belong to the tunnel wall, which is not
	/// in the group.
	/// </summary>
	static bool GroupBoundary( PolyMesh mesh, List<int> group, out List<List<int>> rings )
	{
		rings = new List<List<int>>();

		var count = new Dictionary<EdgeKey, int>();
		var directed = new Dictionary<int, List<int>>();

		foreach ( var fi in group )
		{
			var face = mesh.Faces[fi];

			for ( var i = 0; i < face.Count; i++ )
			{
				var a = face.Indices[i];
				var b = face.Indices[(i + 1) % face.Count];
				var key = new EdgeKey( a, b );

				count[key] = count.TryGetValue( key, out var n ) ? n + 1 : 1;
			}
		}

		// Kept DIRECTED, so the rings come out wound the way the surface is rather than whichever
		// way the walk happened to go. A hole wound like its contour triangulates inside out.
		foreach ( var fi in group )
		{
			var face = mesh.Faces[fi];

			for ( var i = 0; i < face.Count; i++ )
			{
				var a = face.Indices[i];
				var b = face.Indices[(i + 1) % face.Count];

				if ( count[new EdgeKey( a, b )] != 1 )
					continue;

				if ( !directed.TryGetValue( a, out var list ) )
					directed[a] = list = new List<int>();

				list.Add( b );
			}
		}

		var used = new HashSet<(int, int)>();

		foreach ( var start in directed.Keys )
		{
			foreach ( var first in directed[start] )
			{
				if ( used.Contains( (start, first) ) )
					continue;

				var ring = new List<int> { start };
				var current = first;

				used.Add( (start, current) );

				while ( current != start )
				{
					ring.Add( current );

					if ( !directed.TryGetValue( current, out var outgoing ) )
						return false;

					var next = -1;

					foreach ( var candidate in outgoing )
					{
						if ( used.Contains( (current, candidate) ) )
							continue;

						next = candidate;
						break;
					}

					if ( next < 0 )
						return false;

					used.Add( (current, next) );
					current = next;

					// A walk longer than the boundary itself is a bookkeeping fault, not a shape.
					if ( ring.Count > count.Count + 1 )
						return false;
				}

				if ( ring.Count >= 3 )
					rings.Add( ring );
			}
		}

		return rings.Count >= 1;
	}

	/// <summary>The UV every vertex of the group already carries, taken from the first face that
	/// names it. A vertex with two different UVs across the group is a seam, and a surface with a
	/// seam through it is not one this should be welding into a single region — but keeping the
	/// first is the same thing the group's own faces were already showing.</summary>
	static Dictionary<int, Vec2> KnownUVs( PolyMesh mesh, List<int> group )
	{
		var known = new Dictionary<int, Vec2>();

		foreach ( var fi in group )
		{
			var face = mesh.Faces[fi];

			for ( var i = 0; i < face.Count; i++ )
				known.TryAdd( face.Indices[i], face.UVs[i] );
		}

		return known;
	}

	/// <summary>The linear plane-to-UV map the group's own corners already follow, for the mouth's
	/// vertices which have no UV of their own. Same rule as the other repairs.</summary>
	static bool UVMap( PolyMesh mesh, List<int> group, Vec3 u, Vec3 v,
		out Vec2 origin, out Vec2 originUv, out float scaleU, out float scaleV )
	{
		origin = default;
		originUv = default;
		scaleU = 0f;
		scaleV = 0f;

		var face = mesh.Faces[group[0]];

		origin = Flatten( mesh.Positions[face.Indices[0]], u, v );
		originUv = face.UVs[0];

		foreach ( var fi in group )
		{
			var f = mesh.Faces[fi];

			for ( var i = 0; i < f.Count; i++ )
			{
				var p = Flatten( mesh.Positions[f.Indices[i]], u, v ) - origin;
				var duv = new Vec2( f.UVs[i].x - originUv.x, f.UVs[i].y - originUv.y );

				if ( MathF.Abs( p.x ) > 1e-6f && scaleU == 0f )
					scaleU = duv.x / p.x;

				if ( MathF.Abs( p.y ) > 1e-6f && scaleV == 0f )
					scaleV = duv.y / p.y;
			}
		}

		return true;
	}

	// --- shared geometry, kept here so this file stands alone -------------------------------------

	static List<List<int>> BoundaryLoops( PolyMesh mesh )
	{
		var atVertex = new Dictionary<int, List<int>>();

		foreach ( var (key, faces) in mesh.BuildEdgeFaces() )
		{
			if ( faces.Count != 1 )
				continue;

			Link( key.A, key.B );
			Link( key.B, key.A );
		}

		var loops = new List<List<int>>();
		var used = new HashSet<EdgeKey>();

		foreach ( var start in atVertex.Keys )
		{
			if ( atVertex[start].Count != 2 )
				continue;

			var loop = new List<int>();
			var current = start;
			var previous = -1;

			while ( true )
			{
				loop.Add( current );

				var next = -1;

				foreach ( var candidate in atVertex[current] )
				{
					if ( candidate == previous || used.Contains( new EdgeKey( current, candidate ) ) )
						continue;

					next = candidate;
					break;
				}

				if ( next < 0 )
					break;

				used.Add( new EdgeKey( current, next ) );
				previous = current;
				current = next;

				if ( current == start )
					break;
			}

			if ( loop.Count >= 3 && current == start )
				loops.Add( loop );
		}

		return loops;

		void Link( int from, int to )
		{
			if ( !atVertex.TryGetValue( from, out var list ) )
			{
				list = new List<int>( 2 );
				atVertex[from] = list;
			}

			list.Add( to );
		}
	}

	static Vec3 LoopNormal( PolyMesh mesh, List<int> loop )
	{
		var n = new Vec3( 0, 0, 0 );

		for ( var i = 0; i < loop.Count; i++ )
		{
			var a = mesh.Positions[loop[i]];
			var b = mesh.Positions[loop[(i + 1) % loop.Count]];

			n += new Vec3(
				(a.y - b.y) * (a.z + b.z),
				(a.z - b.z) * (a.x + b.x),
				(a.x - b.x) * (a.y + b.y) );
		}

		// A loop bounding a hole is wound against the face it is a hole in, so its own normal points
		// the other way. Everything here is expressed in the SURFACE's direction.
		return n;
	}

	static void Basis( Vec3 normal, out Vec3 u, out Vec3 v )
	{
		var n = normal.Normal;
		var seed = MathF.Abs( n.z ) < 0.9f ? new Vec3( 0, 0, 1 ) : new Vec3( 1, 0, 0 );

		u = Vec3.Cross( seed, n ).Normal;
		v = Vec3.Cross( n, u ).Normal;
	}

	static Vec2 Flatten( Vec3 p, Vec3 u, Vec3 v ) => new( Vec3.Dot( p, u ), Vec3.Dot( p, v ) );

	static List<Vec2> Polygon( PolyMesh mesh, Face face, Vec3 u, Vec3 v )
	{
		var polygon = new List<Vec2>( face.Count );

		foreach ( var index in face.Indices )
			polygon.Add( Flatten( mesh.Positions[index], u, v ) );

		return polygon;
	}

	static float SignedArea( List<Vec2> polygon )
	{
		var area = 0f;

		for ( int i = 0, j = polygon.Count - 1; i < polygon.Count; j = i++ )
			area += (polygon[j].x + polygon[i].x) * (polygon[j].y - polygon[i].y);

		return area * 0.5f;
	}

	static bool OnBoundary( List<Vec2> polygon, Vec2 p )
	{
		for ( var i = 0; i < polygon.Count; i++ )
		{
			var a = polygon[i];
			var b = polygon[(i + 1) % polygon.Count];
			var along = b - a;
			var length = along.Length;

			if ( length < 1e-9f )
				continue;

			var cross = along.x * (p.y - a.y) - along.y * (p.x - a.x);

			if ( MathF.Abs( cross ) / length > 1e-4f )
				continue;

			var t = ((p.x - a.x) * along.x + (p.y - a.y) * along.y) / (length * length);

			if ( t >= -1e-5f && t <= 1f + 1e-5f )
				return true;
		}

		return false;
	}

	static bool PointInPolygon( List<Vec2> polygon, Vec2 point )
	{
		var inside = false;

		for ( int i = 0, j = polygon.Count - 1; i < polygon.Count; j = i++ )
		{
			var a = polygon[i];
			var b = polygon[j];

			if ( a.y > point.y != b.y > point.y
				&& point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x )
			{
				inside = !inside;
			}
		}

		return inside;
	}
}