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

namespace Sunless.Architecture;

public static class ArchCut {
	public const float MinRun = 18f;

	public static Vector2 WorldBand( ArchCutSegment segment, float lift ) {
		return new Vector2( segment.BaseHeight + lift, segment.TopHeight + lift );
	}

	// Memoised — every host a shaft reaches asks for the same volumes
	public static IReadOnlyList<ArchCarveVolume> Resolve( ArchCutPart cut, ArchKit kit ) {
		return ArchBuildMemo.Held( memo => memo.Volumes, cut?.Id ?? 0, () => Resolved( cut, kit ).ToList() );
	}

	static IEnumerable<ArchCarveVolume> Resolved( ArchCutPart cut, ArchKit kit ) {
		if ( cut is not { HasContent: true } ) {
			yield break;
		}

		if ( cut.IsDamage ) {
			foreach ( var volume in ArchDamage.Resolve( cut, kit ) ) {
				yield return volume;
			}

			yield break;
		}

		var legs = cut.Segments.Where( segment => segment.HasLoop || segment.Length > 1f ).ToList();
		var breaking = Breaking( cut, kit );

		if ( cut.Profile != CutProfile.Steps ) {
			foreach ( var leg in legs ) {
				yield return Bite( cut, leg ).Breaking( breaking );
			}

			yield break;
		}

		var going = cut.StepGoing > 0.5f ? cut.StepGoing : kit.StepGoing;
		var rise = cut.StepRise > 0.5f ? cut.StepRise : kit.StepRise;

		foreach ( var leg in legs.Where( leg => !leg.HasLoop ) ) {
			var climbed = leg.BaseHeight;
			var steps = Math.Max( 1, (int)MathF.Round( leg.Length / MathF.Max( 1f, going ) ) );
			var tread = leg.Length / steps;
			var axes = leg.Axes;
			var half = MathF.Max( 1f, leg.Width ) * 0.5f;

			for ( var step = 0; step < steps; step++ ) {
				var footprint = axes.Rect( step * tread, (step + 1) * tread, -half, half );

				climbed += rise;

				yield return ArchCarveVolume.Over( footprint, climbed, leg.TopHeight ).Breaking( breaking );
			}
		}
	}

	public static ArchCarveVolume Bite( ArchCutPart cut, ArchCutSegment leg ) {
		return ArchRamp.Rakes( cut )
			? ArchCarveVolume.Above( leg.Outline(), Floor( cut, leg ), leg.TopHeight )
			: ArchCarveVolume.Over( leg.Outline(), leg.BaseHeight, leg.TopHeight );
	}

	public static ArchCarvePlane Floor( ArchCutPart cut, ArchCutSegment leg ) {
		return ArchRamp.Rakes( cut )
			? ArchRamp.Deck( cut, leg.Outline(), leg.TopHeight, leg.TopHeight - leg.BaseHeight )
			: ArchCarvePlane.Level( leg.BaseHeight );
	}

	public static float Toe( ArchCutPart cut, ArchCutSegment leg ) {
		return leg.TopHeight - ArchRamp.Fall( cut, leg.TopHeight - leg.BaseHeight );
	}

	// Seeded by id so break edges are stable across hotloads
	static ArchCarveBreak Breaking( ArchCutPart cut, ArchKit kit ) {
		return cut.BreakEdges ? new ArchCarveBreak { Seed = cut.Id, Jitter = kit?.BreakJitter ?? 0f } : default;
	}

	public static bool Affects( ArchCutPart cut, ArchCutAffects target ) => (cut.Affects & target) == target;

	public static IEnumerable<ArchFloorCutout> Stored( ArchBuilding building, int level ) {
		return building.Cutouts.Where( cutout => cutout.Level == level && ArchLayerGate.Owned( cutout.OwnerId ) );
	}

	public static IEnumerable<ArchFloorCutout> Holes(
		ArchPlan plan,
		int level,
		ArchKit kit,
		float bottom,
		float top,
		int hostId,
		ArchCutAffects target = ArchCutAffects.Floors,
		int standing = 0 ) {
		foreach ( var (cut, volume) in Reaching( plan, level, kit, bottom, top, hostId, target, standing ) ) {
			var hole = new ArchFloorCutout { Level = level, Name = cut.Name, Break = volume.Break };

			hole.Reshape( volume.Footprint );

			yield return hole;
		}
	}

	public static IEnumerable<List<Vector2>> Guarded( ArchPlan plan, int level, ArchKit kit, float bottom, float top, int hostId ) {
		var low = MathF.Min( bottom, top );
		var high = MathF.Max( bottom, top );

		foreach ( var (cut, volume) in Reaching( plan, level, kit, bottom, top, hostId, ArchCutAffects.Floors ) ) {
			if ( cut.GuardsOpenedEdges && Pierces( volume, low, high ) ) {
				yield return volume.Footprint.ToList();
			}
		}
	}

	public static IEnumerable<ArchCarveVolume> Volumes(
		ArchPlan plan,
		int level,
		ArchKit kit,
		float bottom,
		float top,
		int hostId,
		ArchCutAffects target,
		int standing = 0 ) {
		return Reaching( plan, level, kit, bottom, top, hostId, target, standing ).Select( found => found.Volume );
	}

	public static IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Damage(
		ArchPlan plan,
		int level,
		ArchKit kit,
		float bottom,
		float top,
		int hostId,
		ArchCutAffects target,
		int standing = 0 ) {
		return Reaching( plan, level, kit, bottom, top, hostId, target, standing, ArchZoneMode.Damage );
	}

	public static IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Extrusions(
		ArchPlan plan,
		int level,
		ArchKit kit,
		float bottom,
		float top,
		int hostId,
		ArchCutAffects target,
		int standing = 0 ) {
		return Reaching( plan, level, kit, bottom, top, hostId, target, standing, ArchZoneMode.Extrude );
	}

	static IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Reaching(
		ArchPlan plan,
		int level,
		ArchKit kit,
		float bottom,
		float top,
		int hostId,
		ArchCutAffects target,
		int standing = 0,
		ArchZoneMode mode = ArchZoneMode.Carve ) {
		if ( plan is null ) {
			yield break;
		}

		var low = MathF.Min( bottom, top );
		var high = MathF.Max( bottom, top );

		foreach ( var cut in Cuts( plan, level, hostId, target, standing, mode ) ) {
			foreach ( var volume in Resolve( cut, kit ) ) {
				if ( Reaches( volume, low, high ) ) {
					yield return (cut, volume);
				}
			}
		}
	}

	// Only reaches layers that were STANDING at the time this cut was authored (ArchLayerOrder)
	static IEnumerable<ArchCutPart> Cuts( ArchPlan plan, int level, int hostId, ArchCutAffects target, int standing = 0, ArchZoneMode mode = ArchZoneMode.Carve ) {
		foreach ( var building in plan.Buildings ) {
			foreach ( var cut in Filed( plan, building.Cuts, building.Id, level, hostId, target, standing, mode ) ) {
				yield return cut;
			}
		}

		foreach ( var road in plan.Roads() ) {
			foreach ( var cut in Filed( plan, road.Cuts, road.Id, level, hostId, target, standing, mode ) ) {
				yield return cut;
			}
		}
	}

	static IEnumerable<ArchCutPart> Filed(
		ArchPlan plan,
		IReadOnlyList<ArchCutPart> cuts,
		int ownerId,
		int level,
		int hostId,
		ArchCutAffects target,
		int standing,
		ArchZoneMode mode ) {
		foreach ( var cut in cuts ) {
			if ( cut.Mode != mode || cut.Level > level || !cut.HasContent || !ArchLayerGate.On( cut ) || !Affects( cut, target ) ) {
				continue;
			}

			if ( !ArchLayerOrder.Applies( plan, cut.Id, standing ) ) {
				continue;
			}

			var reach = ArchLayerGroups.Reach( plan, cut.Id, ownerId );

			if ( hostId == 0 || reach is null || reach.Contains( hostId ) ) {
				yield return cut;
			}
		}
	}

	public static bool Reaches( ArchCarveVolume volume, float low, float high ) {
		var floor = float.MaxValue;
		var ceiling = float.MinValue;

		foreach ( var corner in volume.Footprint ) {
			floor = MathF.Min( floor, volume.Floor.At( corner ) );
			ceiling = MathF.Max( ceiling, volume.Ceiling.At( corner ) );
		}

		return ceiling > low + ArchCarve.Grain && floor < high - ArchCarve.Grain;
	}

	// True when the volume spans the entire band — a well, not a recess
	public static bool Pierces( ArchCarveVolume volume, float low, float high ) {
		var floor = float.MinValue;
		var ceiling = float.MaxValue;

		foreach ( var corner in volume.Footprint ) {
			floor = MathF.Max( floor, volume.Floor.At( corner ) );
			ceiling = MathF.Min( ceiling, volume.Ceiling.At( corner ) );
		}

		return floor <= low + ArchCarve.Grain && ceiling >= high - ArchCarve.Grain;
	}

	public static IEnumerable<(float From, float To)> Outside(
		ArchPlan plan,
		ArchKit kit,
		int level,
		int hostId,
		Vector2 from,
		Vector2 to,
		float bottom,
		float top,
		ArchCutAffects target = ArchCutAffects.WallFittings,
		int standing = 0 ) {
		var blocked = new List<(float From, float To)>();

		if ( plan is not null && (to - from).Length > ArchCarve.Grain ) {
			foreach ( var cut in Cuts( plan, level, hostId, target, standing ) ) {
				foreach ( var volume in Resolve( cut, kit ).Where( volume => Reaches( volume, bottom, top ) ) ) {
					blocked.AddRange( ArchFootprint.Inside( volume.Footprint, from, to ) );
				}
			}
		}

		var marks = new List<float> { 0f, 1f };

		foreach ( var range in blocked ) {
			marks.Add( Math.Clamp( range.From, 0f, 1f ) );
			marks.Add( Math.Clamp( range.To, 0f, 1f ) );
		}

		marks = marks.Distinct().OrderBy( value => value ).ToList();

		for ( var index = 0; index + 1 < marks.Count; index++ ) {
			var start = marks[index];
			var finish = marks[index + 1];
			var middle = (start + finish) * 0.5f;

			if ( finish - start > 0.001f && !blocked.Any( range => middle > range.From && middle < range.To ) ) {
				yield return (start, finish);
			}
		}
	}

	public static IEnumerable<List<Vector3>> OutsidePath(
		ArchPlan plan,
		ArchKit kit,
		int level,
		int hostId,
		IReadOnlyList<Vector3> path,
		ArchCutAffects target ) {
		var runs = new List<List<Vector3>>();

		for ( var index = 0; index + 1 < (path?.Count ?? 0); index++ ) {
			var from = path[index];
			var to = path[index + 1];
			var flatFrom = new Vector2( from.x, from.y );
			var flatTo = new Vector2( to.x, to.y );

			var spans = Outside( plan, kit, level, hostId, flatFrom, flatTo, MathF.Min( from.z, to.z ), MathF.Max( from.z, to.z ), target ).ToList();

			foreach ( var span in spans ) {
				var start = Vector3.Lerp( from, to, span.From );
				var finish = Vector3.Lerp( from, to, span.To );
				var current = runs.LastOrDefault();

				if ( current is null || (current[^1] - start).Length > 0.05f ) {
					current = new List<Vector3> { start };
					runs.Add( current );
				}

				if ( (current[^1] - finish).Length > 0.01f ) {
					current.Add( finish );
				}
			}

			if ( spans.Count == 0 || spans.Any( span => span.From > 0.001f || span.To < 0.999f ) ) {
				runs.Add( null );
			}
		}

		return runs.Where( run => run is { Count: >= 2 } );
	}

	public static IEnumerable<ArchRunPath> Runs(
		ArchPlan plan,
		ArchKit kit,
		int level,
		int hostId,
		ArchRunPath run,
		ArchCutAffects target ) {
		var path = run.Raised();

		if ( run.Closed && path.Count >= 3 ) {
			path.Add( path[0] );
		}

		foreach ( var surviving in OutsidePath( plan, kit, level, hostId, path, target ) ) {
			yield return ArchRunPath.Normalized( surviving, run.Height );
		}
	}

	public static IEnumerable<(List<Vector3> Points, bool Closed)> Surviving(
		ArchPlan plan,
		ArchKit kit,
		int level,
		int hostId,
		IReadOnlyList<Vector3> path,
		ArchCutAffects target ) {
		foreach ( var surviving in OutsidePath( plan, kit, level, hostId, path, target ) ) {
			var closed = ArchRunPath.IsClosed( surviving );

			yield return (closed ? surviving.Take( surviving.Count - 1 ).ToList() : surviving, closed);
		}
	}

	public static bool Covers( ArchPlan plan, ArchKit kit, int level, int hostId, Vector3 from, Vector3 to, ArchCutAffects target ) {
		var at = new Vector2( (from.x + to.x) * 0.5f, (from.y + to.y) * 0.5f );

		return Volumes( plan, level, kit, MathF.Min( from.z, to.z ), MathF.Max( from.z, to.z ), hostId, target )
			.Any( volume => volume.Covers( at ) );
	}

	public static IEnumerable<ArchCutPart> Over(
		ArchPlan plan,
		int level,
		IReadOnlyList<Vector2> outline,
		int hostId,
		ArchCutAffects target = ArchCutAffects.Platforms,
		int standing = 0 ) {
		if ( plan is null || outline is not { Count: >= 3 } ) {
			yield break;
		}

		foreach ( var cut in Cuts( plan, level, hostId, target, standing ) ) {
			if ( cut.Outlines().Any( loop => ArchFootprint.Overlaps( loop, outline ) ) ) {
				yield return cut;
			}
		}
	}

	public static ArchCutSegment Extend( ArchCutPart cut, Vector2 from, Vector2 to, float width, float baseHeight, float topHeight ) {
		var segment = Next( cut, from, to, width, baseHeight, topHeight );

		if ( segment is null ) {
			return null;
		}

		cut.Segments.Add( segment );

		return segment;
	}

	public static ArchCutSegment Next( ArchCutPart cut, Vector2 from, Vector2 to, float width, float baseHeight, float topHeight, bool? snapAngle = null ) {
		var last = cut?.Segments.Count > 0 ? cut.Segments[^1] : null;
		var lift = last is null ? 0f : cut.Rise;

		return Sketch(
			last?.End ?? from,
			to,
			width,
			(last?.BaseHeight ?? baseHeight) + lift,
			(last?.TopHeight ?? topHeight) + lift,
			snapAngle ?? cut?.SnapAngle ?? true );
	}

	// Loop legs derive Start/End from their loop outline — keeps shape and handles in sync
	public static void Fit( ArchCutSegment segment, float yaw ) {
		if ( segment is null || !segment.HasLoop ) {
			return;
		}

		var axes = new ArchStairAxes { Yaw = yaw };
		var along = axes.Along;
		var centre = segment.Loop.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / segment.Loop.Count;
		var reach = segment.Loop.Select( point => Vector2.Dot( point - centre, along ) ).ToList();

		segment.Start = centre + along * reach.Min();
		segment.End = centre + along * reach.Max();
	}

	public static void Shift( ArchCutSegment segment, Vector2 by ) {
		if ( segment is null || by.Length < ArchGridService.FinestSize ) {
			return;
		}

		segment.Start += by;
		segment.End += by;

		if ( segment.HasLoop ) {
			segment.Loop = segment.Loop.Select( point => point + by ).ToList();
		}
	}

	// Applies a delta swing, not an absolute angle — preserves existing yaw
	public static void Turn( ArchCutSegment segment, Vector2 about, float degrees, bool snapAngle ) {
		if ( segment is null ) {
			return;
		}

		var swing = snapAngle ? ArchGridService.Angle( degrees ) : degrees;
		var wanted = segment.Yaw + swing;

		if ( MathF.Abs( swing ) < 0.001f ) {
			return;
		}

		if ( segment.HasLoop ) {
			segment.Loop = ArchFootprint.Turned( segment.Loop, about, swing );
			Fit( segment, wanted );

			return;
		}

		var ends = ArchFootprint.Turned( new[] { segment.Start, segment.End }, about, swing );

		segment.Start = ends[0];
		segment.End = ends[1];
	}

	public static bool Reshape( ArchCutSegment segment, Vector2 from, Vector2 to, bool snapAngle = true ) {
		if ( segment is { HasLoop: true } ) {
			Shift( segment, from - segment.Start );

			return true;
		}

		if ( segment is null || Sketch( from, to, segment.Width, segment.BaseHeight, segment.TopHeight, snapAngle ) is not { } redrawn ) {
			return false;
		}

		segment.Start = redrawn.Start;
		segment.End = redrawn.End;

		return true;
	}

	public static void Relink( ArchCutPart cut ) {
		if ( cut is null ) {
			return;
		}

		for ( var index = 1; index < cut.Segments.Count; index++ ) {
			var previous = cut.Segments[index - 1];
			var leg = cut.Segments[index];

			Reshape( leg, previous.End, previous.End + (leg.End - leg.Start), cut.SnapAngle );
		}
	}

	public static ArchCutSegment Sketch( Vector2 from, Vector2 to, float width, float baseHeight, float topHeight, bool snapAngle = true ) {
		var span = to - from;

		if ( span.Length < MinRun ) {
			return null;
		}

		var yaw = ArchGridService.Snap( MathF.Atan2( span.y, span.x ).RadianToDegree(), snapAngle ? ArchGridService.AngleStep : 1f ).DegreeToRadian();
		var along = new Vector2( MathF.Cos( yaw ), MathF.Sin( yaw ) );

		return new ArchCutSegment {
			Start = from,
			End = from + along * ArchGridService.Fine( Vector2.Dot( span, along ) ),
			Width = MathF.Max( 1f, width ),
			BaseHeight = MathF.Min( baseHeight, topHeight ),
			TopHeight = MathF.Max( baseHeight, topHeight )
		};
	}
}