Editor/Carve/ArchCarveQuads.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Stitches a ring (shape inside shape) corner-to-corner; falls back to sweep if it can't tile exactly
static class ArchCarveQuads {
	const int BreakSalt = 71;

	const float LeastBite = ArchGridService.FinestSize * 2f;

	// Ear-clip into triangles, then fuse pairs into the squarest quads available
	public static IEnumerable<List<Vector2>> Split( List<Vector2> loop ) {
		if ( loop.Count <= 4 ) {
			return new[] { loop };
		}

		var wedges = Wedges( loop );

		return wedges.Count == 0 ? new[] { loop } : Fused( wedges );
	}

	static List<List<Vector2>> Wedges( List<Vector2> loop ) {
		var remaining = new List<Vector2>( loop );
		var wedges = new List<List<Vector2>>();

		while ( remaining.Count > 3 ) {
			var clipped = Clipped( remaining );

			if ( clipped < 0 ) {
				return new List<List<Vector2>>();
			}

			var behind = remaining[(clipped - 1 + remaining.Count) % remaining.Count];
			var ahead = remaining[(clipped + 1) % remaining.Count];

			wedges.Add( new List<Vector2> { behind, remaining[clipped], ahead } );
			remaining.RemoveAt( clipped );
		}

		wedges.Add( remaining );

		return wedges;
	}

	static int Clipped( List<Vector2> loop ) {
		for ( var index = 0; index < loop.Count; index++ ) {
			var behind = loop[(index - 1 + loop.Count) % loop.Count];
			var here = loop[index];
			var ahead = loop[(index + 1) % loop.Count];

			if ( Turn( behind, here, ahead ) < ArchCarve.Grain ) {
				continue;
			}

			var wedge = new List<Vector2> { behind, here, ahead };

			if ( loop.Where( corner => !Corner( wedge, corner ) ).Any( corner => ArchFootprint.Contains( wedge, corner ) ) ) {
				continue;
			}

			return index;
		}

		return -1;
	}

	static bool Corner( List<Vector2> wedge, Vector2 corner ) => wedge.Any( held => (held - corner).Length < ArchCarve.Grain );

	static float Turn( Vector2 behind, Vector2 here, Vector2 ahead ) {
		var into = here - behind;
		var away = ahead - here;

		return into.x * away.y - into.y * away.x;
	}

	static List<List<Vector2>> Fused( List<List<Vector2>> wedges ) {
		var taken = new bool[wedges.Count];
		var quads = new List<List<Vector2>>();

		var candidates = new List<(float Cost, int Left, int Right, List<Vector2> Quad)>();

		for ( var left = 0; left < wedges.Count; left++ ) {
			for ( var right = left + 1; right < wedges.Count; right++ ) {
				if ( Fuse( wedges[left], wedges[right] ) is { } quad ) {
					candidates.Add( (Skew( quad ), left, right, quad) );
				}
			}
		}

		foreach ( var (_, left, right, quad) in candidates.OrderBy( candidate => candidate.Cost ) ) {
			if ( taken[left] || taken[right] ) {
				continue;
			}

			taken[left] = true;
			taken[right] = true;

			quads.Add( quad );
		}

		for ( var index = 0; index < wedges.Count; index++ ) {
			if ( !taken[index] ) {
				quads.Add( wedges[index] );
			}
		}

		return quads;
	}

	static List<Vector2> Fuse( List<Vector2> left, List<Vector2> right ) {
		for ( var index = 0; index < left.Count; index++ ) {
			var from = left[index];
			var to = left[(index + 1) % left.Count];

			if ( !Corner( right, from ) || !Corner( right, to ) ) {
				continue;
			}

			var beyond = right.FirstOrDefault( corner => !Corner( new List<Vector2> { from, to }, corner ) );
			var behind = left[(index + 2) % left.Count];
			var quad = new List<Vector2> { behind, from, beyond, to };

			return Convex( quad ) ? quad : null;
		}

		return null;
	}

	static bool Convex( List<Vector2> quad ) {
		for ( var index = 0; index < quad.Count; index++ ) {
			var behind = quad[(index - 1 + quad.Count) % quad.Count];
			var ahead = quad[(index + 1) % quad.Count];

			if ( Turn( behind, quad[index], ahead ) < ArchCarve.Grain ) {
				return false;
			}
		}

		return true;
	}

	static float Skew( List<Vector2> quad ) {
		var skew = 0f;

		for ( var index = 0; index < quad.Count; index++ ) {
			var into = (quad[index] - quad[(index - 1 + quad.Count) % quad.Count]).Normal;
			var away = (quad[(index + 1) % quad.Count] - quad[index]).Normal;

			skew += MathF.Abs( Vector2.Dot( into, away ) );
		}

		return skew;
	}

	public static List<List<Vector2>> Ring( IReadOnlyList<ArchCarveVolume> volumes ) {
		if ( volumes.Count != 2 ) {
			return null;
		}

		var first = Wound( volumes[0].Footprint );
		var second = Wound( volumes[1].Footprint );

		if ( first is null || second is null ) {
			return null;
		}

		var host = Holds( first, second ) ? first : Holds( second, first ) ? second : null;

		if ( host is null ) {
			return null;
		}

		var standing = ReferenceEquals( host, first );
		var hole = Broken( host, standing ? second : first, standing ? volumes[1].Break : volumes[0].Break );
		var faces = Stitched( host, hole );

		return faces is not null && Tiles( faces, host ) ? faces : null;
	}

	static List<Vector2> Wound( IReadOnlyList<Vector2> footprint ) {
		return footprint is { Count: >= 3 } ? ArchFootprint.Wind( footprint.ToList() ) : null;
	}

	static bool Holds( List<Vector2> outer, List<Vector2> inner ) {
		if ( inner.Any( corner => !ArchFootprint.Contains( outer, corner ) ) ) {
			return false;
		}

		for ( var index = 0; index < inner.Count; index++ ) {
			var spans = ArchFootprint.Inside( outer, inner[index], inner[(index + 1) % inner.Count] ).ToList();

			if ( spans.Count != 1 || spans[0].From > 0.001f || spans[0].To < 0.999f ) {
				return false;
			}
		}

		return true;
	}

	// Pulls convex corners inward to simulate disrepair; reflex corners stay put
	static List<Vector2> Broken( List<Vector2> host, List<Vector2> hole, ArchCarveBreak breaking ) {
		if ( !breaking.Breaks ) {
			return hole;
		}

		var bite = Bite( host, hole, breaking.Jitter );

		if ( bite < LeastBite ) {
			return hole;
		}

		var broken = new List<Vector2>();

		for ( var index = 0; index < hole.Count; index++ ) {
			var behind = hole[(index - 1 + hole.Count) % hole.Count];
			var here = hole[index];
			var ahead = hole[(index + 1) % hole.Count];
			var inward = Turn( behind, here, ahead ) < ArchCarve.Grain ? Vector2.Zero : Inward( behind, here, ahead );
			var offset = bite * ArchBarrierShape.Noise( breaking.Seed, index, BreakSalt );

			var bitten = inward.Length > 0.5f && offset >= LeastBite;

			broken.Add( bitten ? ArchGridService.Fine( here + inward * offset ) : here );
		}

		return broken;
	}

	// Clamped to 1/3 of the ring's narrowest gap and 1/4 of the hole's narrowest span
	static float Bite( List<Vector2> host, List<Vector2> hole, float jitter ) {
		ArchFootprint.Bounds( hole, out var min, out var max );

		var gap = MathF.Min( Gap( host, hole ), Gap( hole, host ) );

		return MathF.Min( jitter, MathF.Min( gap / 3f, MathF.Min( max.x - min.x, max.y - min.y ) * 0.25f ) );
	}

	static float Gap( List<Vector2> loop, List<Vector2> against ) {
		var boundary = ArchRunPath.Of( loop, 0f, true );
		var gap = float.MaxValue;

		foreach ( var corner in against ) {
			if ( boundary.Nearest( corner, out _, out var reach ) ) {
				gap = MathF.Min( gap, reach );
			}
		}

		return gap;
	}

	static Vector2 Inward( Vector2 behind, Vector2 here, Vector2 ahead ) {
		var into = (here - behind).Normal;
		var away = (ahead - here).Normal;
		var bisector = new Vector2( -into.y, into.x ) + new Vector2( -away.y, away.x );

		return bisector.Length < 0.0001f ? Vector2.Zero : bisector.Normal;
	}

	static List<List<Vector2>> Stitched( List<Vector2> host, List<Vector2> hole ) {
		var reach = Reach( host, hole );

		if ( Around( reach, host.Count ) != host.Count ) {
			return null;
		}

		var faces = new List<List<Vector2>> { new( hole ) };

		for ( var index = 0; index < hole.Count; index++ ) {
			var next = (index + 1) % hole.Count;
			var corner = reach[index];
			var step = Step( reach[index], reach[next], host.Count );

			if ( step == 0 ) {
				faces.Add( new List<Vector2> { host[corner], hole[next], hole[index] } );

				continue;
			}

			faces.Add( new List<Vector2> { host[corner], host[(corner + 1) % host.Count], hole[next], hole[index] } );

			for ( var extra = 1; extra < step; extra++ ) {
				var at = (corner + extra) % host.Count;

				faces.Add( new List<Vector2> { host[at], host[(at + 1) % host.Count], hole[next] } );
			}
		}

		return faces;
	}

	static int Around( List<int> reach, int corners ) {
		var travelled = 0;

		for ( var index = 0; index < reach.Count; index++ ) {
			travelled += Step( reach[index], reach[(index + 1) % reach.Count], corners );
		}

		return travelled;
	}

	static int Step( int from, int to, int corners ) => (to - from + corners) % corners;

	// Equal corner counts pair by offset (shortest total); unequal pair by nearest angle
	static List<int> Reach( List<Vector2> host, List<Vector2> hole ) {
		if ( host.Count == hole.Count ) {
			return Paired( host, hole );
		}

		var centre = hole.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / hole.Count;

		return hole.Select( corner => Nearest( host, centre, corner ) ).ToList();
	}

	static List<int> Paired( List<Vector2> host, List<Vector2> hole ) {
		var best = 0;
		var shortest = float.MaxValue;

		for ( var offset = 0; offset < host.Count; offset++ ) {
			var total = 0f;

			for ( var index = 0; index < hole.Count; index++ ) {
				total += (host[(index + offset) % host.Count] - hole[index]).Length;
			}

			if ( total < shortest ) {
				shortest = total;
				best = offset;
			}
		}

		return Enumerable.Range( 0, hole.Count ).Select( index => (index + best) % host.Count ).ToList();
	}

	static int Nearest( List<Vector2> host, Vector2 centre, Vector2 corner ) {
		var wanted = MathF.Atan2( corner.y - centre.y, corner.x - centre.x );
		var best = 0;
		var closest = float.MaxValue;

		for ( var index = 0; index < host.Count; index++ ) {
			var angle = MathF.Atan2( host[index].y - centre.y, host[index].x - centre.x ) - wanted;
			var apart = MathF.Abs( MathF.Atan2( MathF.Sin( angle ), MathF.Cos( angle ) ) );

			if ( apart < closest ) {
				closest = apart;
				best = index;
			}
		}

		return best;
	}

	static bool Tiles( List<List<Vector2>> faces, List<Vector2> host ) {
		var wanted = MathF.Abs( ArchFootprint.SignedArea( host ) );
		var covered = 0f;

		foreach ( var face in faces ) {
			var area = ArchFootprint.SignedArea( face );
			var centre = face.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / face.Count;

			if ( area < ArchCarve.Grain || !ArchFootprint.Contains( host, centre ) ) {
				return false;
			}

			if ( face.Count > 3 && !ArchFootprint.IsSimple( face ) ) {
				return false;
			}

			covered += area;
		}

		return MathF.Abs( covered - wanted ) < MathF.Max( 1f, wanted * 0.0001f );
	}
}