Editor/Tool/ArchElevation.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// The role colours the piece; Item is what the pick highlights against.
public enum ArchElevationRole {
	Wall,
	Opening,
	Floor,
	Platform,
	Cut,
	Roof
}

public sealed class ArchElevationPiece {
	public ArchElevationRole Role { get; init; }
	public object Item { get; init; }
	public int Floor { get; init; }
	// Along the drawn axis - y in Front, x in Side.
	public float From { get; init; }
	public float To { get; init; }
	public float Foot { get; init; }
	public float Head { get; init; }
	// Both surfaces sampled; null when the piece is a plain band. A wedge has no single thickness.
	public List<Vector2> Profile { get; init; }
	public List<Vector2> Soffit { get; init; }

	public bool Rakes => Profile is { Count: >= 2 } && Soffit is { Count: >= 2 };
}

// Front cuts at x and draws in y; Side the reverse.
public readonly struct ArchElevationPlane {
	public ArchViewAxis Axis { get; init; }
	public float Depth { get; init; }

	public bool IsElevation => Axis is ArchViewAxis.Front or ArchViewAxis.Side;

	public float Across( Vector2 point ) => Axis == ArchViewAxis.Front ? point.y : point.x;

	public float Along( Vector2 point ) => Axis == ArchViewAxis.Front ? point.x : point.y;

	public Vector2 Plan( float across ) => Axis == ArchViewAxis.Front ? new Vector2( Depth, across ) : new Vector2( across, Depth );

	public Vector3 At( float across, float height ) {
		var plan = Plan( across );

		return new Vector3( plan.x, plan.y, height );
	}
}

// Resolves one building, every storey — the section shows the stack.
public static class ArchElevation {
	const float Grain = ArchCarve.Grain;

	// Fine enough that a deck break lands near a sample, coarse enough that a long roof stays cheap.
	const int Samples = 48;

	// Takes plan+kit, not the tool — keeps the resolve reachable from the harness.
	public static ArchElevationCut Cut( ArchPlan plan, ArchKit kit, ArchBuilding building, ArchViewAxis axis, float depth ) {
		var plane = new ArchElevationPlane { Axis = axis, Depth = depth };
		var pieces = new List<ArchElevationPiece>();

		if ( building is not null && plane.IsElevation ) {
			var lift = ArchAsks.Lift( plan, building, kit );

			foreach ( var room in building.Rooms ) {
				Rooms( pieces, plane, room, lift, kit );
			}

			foreach ( var roof in building.Roofs ) {
				Roofs( pieces, plane, roof, lift );
			}

			foreach ( var platform in building.Platforms ) {
				Solid( pieces, plane, platform.Outline(), ArchElevationRole.Platform, platform, platform.Level,
					ArchCarvePlane.Level( platform.GradeHeight ), ArchRamp.Deck( platform ) );
			}

			foreach ( var cut in building.Cuts ) {
				foreach ( var segment in cut.Legs() ) {
					Solid( pieces, plane, segment.Outline(), ArchElevationRole.Cut, cut, cut.Level,
						ArchCut.Floor( cut, segment ), ArchCarvePlane.Level( segment.TopHeight ) );
				}
			}
		}

		return new ArchElevationCut {
			Building = building,
			Axis = axis,
			Depth = depth,
			Pieces = pieces
		};
	}

	static void Rooms( List<ArchElevationPiece> pieces, ArchElevationPlane plane, ArchRoom room, float lift, ArchKit kit ) {
		var floor = room.BaseHeight + lift;

		if ( room.HasFloor ) {
			Add( pieces, plane, ArchFloorGen.Footprint( room ), ArchElevationRole.Floor, room, room.Floor,
				floor - MathF.Max( 1f, kit.FloorThickness ), floor );
		}

		foreach ( var wall in room.Walls ) {
			if ( wall.Length < 0.5f ) {
				continue;
			}

			var head = floor + ArchWallSection.Height( wall, room, kit );

			if ( !Add( pieces, plane, ArchWallSection.Band( wall, kit ), ArchElevationRole.Wall, wall, room.Floor, floor, head ) ) {
				continue;
			}

			Openings( pieces, plane, wall, room, floor, kit );
		}
	}

	// Only the plane through the HOLE shows the opening - an inch over shows solid wall.
	static void Openings( List<ArchElevationPiece> pieces, ArchElevationPlane plane, ArchWall wall, ArchRoom room, float floor, ArchKit kit ) {
		if ( !Meets( wall, plane, out var along ) ) {
			return;
		}

		foreach ( var opening in wall.Openings.Where( entry => along >= entry.Left && along <= entry.Right ) ) {
			Add( pieces, plane, ArchWallSection.Band( wall, kit ), ArchElevationRole.Opening, opening, room.Floor,
				floor + opening.SillHeight, floor + opening.Top );
		}
	}

	// A wall parallel to the plane never crosses it - the case a straight interpolation would divide by zero on.
	static bool Meets( ArchWall wall, ArchElevationPlane plane, out float along ) {
		along = 0f;

		var start = plane.Along( wall.Start ) - plane.Depth;
		var end = plane.Along( wall.End ) - plane.Depth;

		if ( MathF.Abs( start - end ) < Grain ) {
			return false;
		}

		var fraction = start / (start - end);

		if ( fraction < 0f || fraction > 1f ) {
			return false;
		}

		along = fraction * wall.Length;

		return true;
	}

	static void Roofs( List<ArchElevationPiece> pieces, ArchElevationPlane plane, ArchRoofPart roof, float lift ) {
		var outline = roof.Outline();

		if ( !Crosses( outline, plane, out var from, out var to ) ) {
			return;
		}

		var thickness = MathF.Max( 1f, roof.Thickness );
		var profile = Sampled( plane, from, to, across => ArchRoofPlane.At( roof, plane.Plan( across ) ) + lift );
		var soffit = profile.Select( sample => sample.WithY( sample.y - thickness ) ).ToList();

		Raking( pieces, ArchElevationRole.Roof, roof, roof.Level, from, to, soffit, profile );
	}

	// A body between two SURFACES. Level on both, it is the plain band every other piece is; raked on either, it
	// carries both traces so a section draws the wedge rather than the box it fits inside.
	static void Solid(
		List<ArchElevationPiece> pieces,
		ArchElevationPlane plane,
		IReadOnlyList<Vector2> loop,
		ArchElevationRole role,
		object item,
		int floor,
		ArchCarvePlane under,
		ArchCarvePlane over ) {
		if ( !under.Rakes && !over.Rakes ) {
			Add( pieces, plane, loop, role, item, floor, under.Datum, over.Datum );

			return;
		}

		if ( !Crosses( loop, plane, out var from, out var to ) ) {
			return;
		}

		Raking( pieces, role, item, floor, from, to,
			Sampled( plane, from, to, across => under.At( plane.Plan( across ) ) ),
			Sampled( plane, from, to, across => over.At( plane.Plan( across ) ) ) );
	}

	static void Raking(
		List<ArchElevationPiece> pieces,
		ArchElevationRole role,
		object item,
		int floor,
		float from,
		float to,
		List<Vector2> soffit,
		List<Vector2> profile ) {
		pieces.Add( new ArchElevationPiece {
			Role = role,
			Item = item,
			Floor = floor,
			From = from,
			To = to,
			Foot = soffit.Min( point => point.y ),
			Head = profile.Max( point => point.y ),
			Profile = profile,
			Soffit = soffit
		} );
	}

	static List<Vector2> Sampled( ArchElevationPlane plane, float from, float to, Func<float, float> height ) {
		var samples = new List<Vector2>();

		for ( var step = 0; step <= Samples; step++ ) {
			var across = MathX.Lerp( from, to, step / (float)Samples );

			samples.Add( new Vector2( across, height( across ) ) );
		}

		return samples;
	}

	static bool Add(
		List<ArchElevationPiece> pieces,
		ArchElevationPlane plane,
		IReadOnlyList<Vector2> loop,
		ArchElevationRole role,
		object item,
		int floor,
		float foot,
		float head ) {
		if ( !Crosses( loop, plane, out var from, out var to ) ) {
			return false;
		}

		pieces.Add( new ArchElevationPiece {
			Role = role,
			Item = item,
			Floor = floor,
			From = from,
			To = to,
			Foot = MathF.Min( foot, head ),
			Head = MathF.Max( foot, head )
		} );

		return true;
	}

	// An edge lying ON the plane contributes both ends, or a wall exactly at the section depth vanishes.
	public static bool Crosses( IReadOnlyList<Vector2> loop, ArchElevationPlane plane, out float from, out float to ) {
		from = float.MaxValue;
		to = float.MinValue;

		if ( loop is not { Count: >= 2 } ) {
			return false;
		}

		for ( var index = 0; index < loop.Count; index++ ) {
			var a = loop[index];
			var b = loop[(index + 1) % loop.Count];
			var here = plane.Along( a ) - plane.Depth;
			var next = plane.Along( b ) - plane.Depth;

			if ( MathF.Abs( here ) < Grain && MathF.Abs( next ) < Grain ) {
				Take( ref from, ref to, plane.Across( a ) );
				Take( ref from, ref to, plane.Across( b ) );

				continue;
			}

			if ( here > 0f == next > 0f ) {
				continue;
			}

			Take( ref from, ref to, MathX.Lerp( plane.Across( a ), plane.Across( b ), here / (here - next) ) );
		}

		return to - from > Grain;
	}

	static void Take( ref float from, ref float to, float value ) {
		from = MathF.Min( from, value );
		to = MathF.Max( to, value );
	}
}

// Held between frames - re-cutting the whole house sixty times a second is the resolve the overlay refuses.
public sealed class ArchElevationCut {
	public ArchBuilding Building { get; init; }
	public ArchViewAxis Axis { get; init; }
	public float Depth { get; init; }
	public List<ArchElevationPiece> Pieces { get; init; } = new();

	public ArchElevationPlane Plane => new() { Axis = Axis, Depth = Depth };

	// Smallest area first, so a door beats its wall - the more specific thing wins.
	public ArchElevationPiece At( float across, float height ) {
		return Pieces
			.Where( piece => across >= piece.From && across <= piece.To && height >= piece.Foot && height <= piece.Head )
			.OrderBy( piece => (piece.To - piece.From) * (piece.Head - piece.Foot) )
			.FirstOrDefault();
	}

	public bool Matches( ArchBuilding building, ArchViewAxis axis, float depth ) {
		return ReferenceEquals( Building, building ) && Axis == axis && MathF.Abs( Depth - depth ) < 0.01f;
	}
}