Editor/Barriers/ArchBarrier.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public readonly struct ArchBarrierPost {
	public ArchFrame Frame { get; init; }
	public float Seat { get; init; }
	public float Foot { get; init; }
	public float Lean { get; init; }
	public bool End { get; init; }

	public Vector2 Flat => new( Frame.Position.x, Frame.Position.y );
}

// Seats differ only on a raking run - a stepped run levels them.
public sealed class ArchBarrierBayShape {
	const float StrandChord = 48f;

	public ArchBarrierPost From { get; init; }
	public ArchBarrierPost To { get; init; }
	public float SeatFrom { get; init; }
	public float SeatTo { get; init; }
	public float Ground { get; init; }
	public int Courses { get; init; }
	public bool Gate { get; init; }
	public bool Missing { get; init; }
	public float Lean { get; init; }
	public float Slip { get; init; }
	public float Sag { get; init; }
	public int Index { get; init; }

	public float Span => (To.Flat - From.Flat).Length;

	// The one sagging line. The drop was resolved once, on the bay, so the ghost walks exactly the points the
	// generator builds and neither can re-derive a curve of its own.
	public List<Vector3> Strand( float from, float to ) {
		var chords = Math.Max( 1, ArchDivide.AtMost( Span, StrandChord ).Count );
		var path = new List<Vector3>();
		var span = To.Flat - From.Flat;

		for ( var chord = 0; chord <= chords; chord++ ) {
			var fraction = chord / (float)chords;
			var flat = From.Flat + span * fraction;
			var level = MathX.Lerp( from, to, fraction );

			path.Add( new Vector3( flat.x, flat.y, level - Sag * 4f * fraction * (1f - fraction) ) );
		}

		return path;
	}

	public Vector2 Along {
		get {
			var span = To.Flat - From.Flat;

			return span.Length < 0.05f ? new Vector2( 1f, 0f ) : span.Normal;
		}
	}

	public float SeatAt( float fraction ) => MathX.Lerp( SeatFrom, SeatTo, fraction );
}

// One resolve, read by the generator AND the placement ghost, so the posts drawn are the posts built.
public sealed class ArchBarrierShape {
	public List<ArchBarrierPost> Posts { get; init; } = new();
	public List<ArchBarrierBayShape> Bays { get; init; } = new();

	public bool IsUsable => Bays.Count > 0;

	public static ArchBarrierShape Resolve( ArchCurve curve, ArchBarrierSpec spec, float offset = 0f, float lift = 0f, ArchKit kit = null ) {
		var shape = new ArchBarrierShape();

		if ( curve is null || !curve.IsUsable || spec is null ) {
			return shape;
		}

		var length = curve.Length;

		if ( length < 4f ) {
			return shape;
		}

		var stations = Stations( curve, spec, length, offset, lift );

		if ( stations.Count < 2 ) {
			return shape;
		}

		var seats = Seats( stations, spec );
		var gate = spec.HasGate ? (spec.GateAt, spec.GateAt + spec.GateWidth) : (0f, 0f);

		for ( var index = 0; index < stations.Count; index++ ) {
			var neighbours = new List<float> { seats[Math.Max( 0, index - 1 )], seats[Math.Min( seats.Count - 1, index )] };

			shape.Posts.Add( new ArchBarrierPost {
				Frame = stations[index],
				Seat = neighbours.Max(),
				Foot = MathF.Min( stations[index].Position.z, neighbours.Min() ),
				Lean = Signed( spec.Seed, index, 11 ) * spec.Wear * 2.5f,
				End = !curve.Closed && (index == 0 || index == stations.Count - 1)
			} );
		}

		for ( var index = 0; index < seats.Count; index++ ) {
			var from = shape.Posts[index];
			var to = shape.Posts[index + 1];
			var authored = spec.Bays.ElementAtOrDefault( index );
			var midpoint = (from.Frame.Distance + to.Frame.Distance) * 0.5f;

			shape.Bays.Add( new ArchBarrierBayShape {
				From = from,
				To = to,
				SeatFrom = spec.Ground == BarrierGround.Follow ? from.Frame.Position.z : seats[index],
				SeatTo = spec.Ground == BarrierGround.Follow ? to.Frame.Position.z : seats[index],
				Ground = MathF.Min( from.Frame.Position.z, to.Frame.Position.z ),
				Courses = Courses( spec, index, authored ),
				Gate = authored?.Gate == true || (spec.HasGate && midpoint >= gate.Item1 && midpoint <= gate.Item2),
				Missing = spec.Broken > 0.001f && Noise( spec.Seed, index, 23 ) < spec.Broken,
				Lean = Signed( spec.Seed, index, 31 ) * spec.Wear * 3.5f,
				// Down only - a panel settles; a lifted one would top the wall's height line.
				Slip = -Noise( spec.Seed, index, 47 ) * spec.Wear * 2.5f,
				Sag = Sagged( spec, kit, (to.Flat - from.Flat).Length, index ),
				Index = index
			} );
		}

		return shape;
	}

	static List<ArchFrame> Stations( ArchCurve curve, ArchBarrierSpec spec, float length, float offset, float lift ) {
		var distances = Distances( spec, length );
		var stations = new List<ArchFrame>();

		foreach ( var distance in distances ) {
			if ( !curve.Sample( Math.Clamp( distance, 0f, length ), out var frame ) ) {
				continue;
			}

			stations.Add( offset == 0f && lift == 0f ? frame : Shifted( frame, offset, lift ) );
		}

		return stations;
	}

	static List<float> Distances( ArchBarrierSpec spec, float length ) {
		var panel = MathF.Max( 12f, spec.PanelLength );

		if ( spec.Bays.Count == 0 ) {
			return spec.EvenBays
				? ArchDivide.AtMost( length, panel ).Nodes.ToList()
				: ArchDivide.Fixed( length, panel );
		}

		var distances = new List<float> { 0f };
		var travelled = 0f;

		foreach ( var bay in spec.Bays ) {
			travelled += MathF.Max( 12f, bay.Span > 0.5f ? bay.Span : panel );

			if ( travelled >= length - 6f ) {
				break;
			}

			distances.Add( travelled );
		}

		// A run extended past its own bay list carries on at the panel length rather than ending in one enormous bay.
		while ( length - travelled > panel * 1.15f ) {
			travelled += panel;
			distances.Add( travelled );
		}

		distances.Add( length );

		return distances;
	}

	// Sits on the higher end; the plinth makes up the difference.
	static List<float> Seats( IReadOnlyList<ArchFrame> stations, ArchBarrierSpec spec ) {
		var seats = new List<float>();
		var highest = stations.Max( station => station.Position.z );

		for ( var index = 0; index < stations.Count - 1; index++ ) {
			seats.Add( spec.Ground switch {
				BarrierGround.Level => highest,
				BarrierGround.Step => MathF.Max( stations[index].Position.z, stations[index + 1].Position.z ),
				_ => MathF.Max( stations[index].Position.z, stations[index + 1].Position.z )
			} );
		}

		return seats;
	}

	// A FRACTION of the span, so one kit number sags a short spur and a long crossing alike, and wear deepens it.
	static float Sagged( ArchBarrierSpec spec, ArchKit kit, float span, int index ) {
		if ( spec.Style != BarrierStyle.Catenary ) {
			return 0f;
		}

		var fraction = MathF.Max( 0f, kit?.CatenarySag ?? 0f );

		return span * fraction * (1f + Noise( spec.Seed, index, 113 ) * spec.Wear);
	}

	// Every emitted vertex canonicalizes onto the finest rung, so a strand thinner than TWO rungs has both
	// sides of its section land on one of them and the whole prism collapses to nothing. The kit may ask for
	// a hair-thin wire; the mesh cannot hold one, and a line nobody can see is worse than one a touch fat.
	public static float WireHalf( ArchKit kit ) {
		return MathF.Max( ArchGridService.FinestSize, (kit?.WireThickness ?? 0f) * 0.5f );
	}

	static int Courses( ArchBarrierSpec spec, int index, ArchBarrierBay authored ) {
		if ( authored is { Courses: > 0 } ) {
			return authored.Courses;
		}

		var courses = Math.Max( 1, spec.Courses );

		if ( spec.CourseVary <= 0 ) {
			return courses;
		}

		// Down only - Courses is the height line; a bay falls short, never over.
		return Math.Max( 1, courses - (int)MathF.Round( Noise( spec.Seed, index, 59 ) * spec.CourseVary ) );
	}

	static ArchFrame Shifted( ArchFrame frame, float offset, float lift ) {
		return new ArchFrame {
			Position = frame.Side( offset, lift ),
			Along = frame.Along,
			Across = frame.Across,
			Up = frame.Up,
			Distance = frame.Distance,
			WidthScale = frame.WidthScale
		};
	}

	// Seeded, never Game.Random: a run that reshuffled itself on every hotload could not be reviewed.
	public static float Noise( int seed, int index, int salt ) {
		var hash = unchecked((uint)(seed * 374761393 + index * 668265263 + salt * 2246822519));

		hash ^= hash >> 13;
		hash = unchecked(hash * 1274126177);
		hash ^= hash >> 16;

		return (hash & 0xFFFFFF) / (float)0xFFFFFF;
	}

	public static float Signed( int seed, int index, int salt ) => Noise( seed, index, salt ) * 2f - 1f;
}