Editor/Road/ArchRoadPart.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public enum RoadSide {
	None,
	Left,
	Right,
	Both
}

public enum KerbStyle {
	Square,
	Bevelled,
	Rolled
}

public enum CrossingKind {
	Driveway,
	Footpath,
	Zebra
}

// A unit of its own, standing beside the buildings rather than under one.
public sealed class ArchRoadPart : ArchUnit, IArchPainted {
	public ArchRoadPart() {
		Name = "Road";
		Kind = ArchKind.Road;
	}

	public List<ArchCurveNode> Nodes { get; set; } = new();
	public bool Closed { get; set; }

	public float Width { get; set; } = 288f;
	public int Lanes { get; set; } = 2;
	public float Thickness { get; set; } = 6f;
	public float Camber { get; set; } = 1.5f;

	public float Precision { get; set; } = 48f;
	public bool Simplify { get; set; } = true;
	public float StraightThreshold { get; set; } = 1f;
	public int MinimumStraightRun { get; set; } = 3;

	public RoadSide Pavements { get; set; } = RoadSide.Both;
	public float PavementWidth { get; set; } = 96f;
	public float KerbHeight { get; set; } = 6f;
	public KerbStyle Kerb { get; set; } = KerbStyle.Bevelled;
	public float KerbBevel { get; set; } = 1.5f;
	// Without it the kerb is a knife edge with nowhere for its material.
	public float KerbWidth { get; set; } = 5f;
	// Cheap - adds columns to a section that is swept anyway.
	public int PavementCourses { get; set; } = 2;
	// Off by default - a pitch rings the kerb face as well as the field.
	public float PaverSize { get; set; }

	public bool Markings { get; set; } = true;
	public string CentreLine { get; set; } = "dashed_white";
	public string LaneLine { get; set; } = "";
	public string EdgeLine { get; set; } = "";

	public RoadSide Barriers { get; set; } = RoadSide.None;
	public ArchBarrierSpec Barrier { get; set; } = new() { Style = BarrierStyle.Guardrail };
	// From the outer edge of whatever it stands on - pavement, else carriageway.
	public float BarrierInset { get; set; } = 10f;

	public bool Conform { get; set; } = true;
	// Draping and carving are separate answers - a bridge is laid level THROUGH terrain.
	public bool Stamp { get; set; } = true;
	public List<ArchRoadCrossing> Crossings { get; set; } = new();
	public List<ArchBridgePart> Bridges { get; set; } = new();
	public List<ArchTunnelPart> Tunnels { get; set; } = new();

	// A road is nothing without a centreline; a single dropped node leaves no run to build.
	public override bool HasContent => Nodes.Count >= 2;

	public ArchCurve Curve() => ArchCurve.Of( Nodes, Closed );

	public float HalfWidth => MathF.Max( 24f, Width ) * 0.5f;

	public bool Carries( RoadSide setting, bool right ) {
		return setting == RoadSide.Both || setting == (right ? RoadSide.Right : RoadSide.Left);
	}

	// Only the CARRIAGEWAY takes the node's width scale - a pavement does not narrow with the road.
	public float Reach( bool right, float widthScale = 1f ) {
		return HalfWidth * MathF.Max( 0.01f, widthScale )
			+ (Carries( Pavements, right ) ? MathF.Max( 0f, PavementWidth ) : 0f);
	}

	public float Reach() => MathF.Max( Reach( false ), Reach( true ) );

	// ONE number the verge, a drive's landing and the ground carve all read: how far the pavement's back edge
	// stands above the crown.
	public float Back( ArchKit kit ) {
		return -MathF.Max( 0f, Camber ) + MathF.Max( 0f, KerbHeight ) + MathF.Max( 0f, kit?.PavementFall ?? 0f );
	}

	// Where the crown seats against the grade it is laid on. A road is cut INTO the ground rather than stood on it,
	// so wherever a pavement is carried this is negative: the pavement's back edge lands flush with grade and the
	// carriageway sits a kerb below it. Without one the carriageway itself is the top, and only the camber is owed.
	public float Seat( ArchKit kit ) {
		return Pavements == RoadSide.None ? MathF.Max( 0f, Camber ) : -Back( kit );
	}

	// Listed once here - a field left off one list silently stops applying.
	public ArchRoadPart Copy() {
		return new ArchRoadPart {
			Width = Width,
			Lanes = Lanes,
			Thickness = Thickness,
			Camber = Camber,
			Precision = Precision,
			Simplify = Simplify,
			StraightThreshold = StraightThreshold,
			MinimumStraightRun = MinimumStraightRun,
			Pavements = Pavements,
			PavementWidth = PavementWidth,
			KerbHeight = KerbHeight,
			Kerb = Kerb,
			KerbBevel = KerbBevel,
			KerbWidth = KerbWidth,
			PavementCourses = PavementCourses,
			PaverSize = PaverSize,
			Markings = Markings,
			CentreLine = CentreLine,
			LaneLine = LaneLine,
			EdgeLine = EdgeLine,
			Barriers = Barriers,
			Barrier = Barrier.Copy(),
			BarrierInset = BarrierInset,
			Conform = Conform,
			Stamp = Stamp
		};
	}
}

// The pavement is never broken, only lowered.
public sealed class ArchRoadCrossing {
	public int Id { get; set; }
	public string Name { get; set; } = "Crossing";
	public CrossingKind Kind { get; set; } = CrossingKind.Driveway;
	public float Distance { get; set; }
	public float Width { get; set; } = 132f;
	public RoadSide Side { get; set; } = RoadSide.Right;
	// Linked, it follows the driveway - move the garage and the kerb moves.
	public int ApproachId { get; set; }
	public bool Apron { get; set; } = true;
	// Splayed so a car can turn into it off a moving lane.
	public float Splay { get; set; } = 36f;
	// 0 takes the kit's default.
	public int SplaySegments { get; set; }

	// Only the station changes when runs are joined.
	public ArchRoadCrossing Copy( float distance ) {
		return new ArchRoadCrossing {
			Id = Id,
			Name = Name,
			Kind = Kind,
			Distance = distance,
			Width = Width,
			Side = Side,
			ApproachId = ApproachId,
			Apron = Apron,
			Splay = Splay,
			SplaySegments = SplaySegments
		};
	}
}