Code/RoadComponent/RoadComponent.Bridge.cs
using System;
using System.Collections.Generic;
using System.Linq;
using HalfEdgeMesh;
using Sandbox;

namespace RedSnail.RoadTool;

public enum BridgePillarShape
{
	Square,
	Cylinder
}

public partial class RoadComponent
{
	private bool m_DoesBridgeNeedRebuild = false;

	private const string BridgeTag = "road_bridge";

	[Property, FeatureEnabled("Bridge", Icon = "architecture", Tint = EditorTint.Blue)]
	private bool HasBridge { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = false;

	/// <summary>
	/// This will prevent the bridge from being rebuilt if any property is edited or if the road component get disable and re-enabled.
	/// Really useful if you plan to edit the mesh with the mapping tool so you don't accidently erase/rebuild the bridge.
	/// </summary>
	[Property(Title = "🔒 Locked"), Feature("Bridge")]
	private bool IsLocked { get; set; } = false;

	/// <summary>
	/// This is your bridge material you wanna use.
	/// I recommend using a tileable texture for better result.
	/// </summary>
	[Property(Title = "Material"), Feature("Bridge"), Group("Texturing"), Order(1)]
	private Material BridgeMaterial { get; set { field = value; m_DoesBridgeNeedRebuild = true; } }

	[Property(Title = "Texture Repeat"), Feature("Bridge"), Group("Texturing"), Order(1), Step(1)]
	private float BridgeTextureRepeat { get; set { field = value.Clamp(10.0f, 10000.0f); m_DoesBridgeNeedRebuild = true; } } = 500.0f;

	[Property(Title = "Border Width"), Feature("Bridge"), Group("Shape"), Order(0), Range(10.0f, 500.0f)]
	private float BridgeBorderWidth { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;

	[Property(Title = "Border Height"), Feature("Bridge"), Group("Shape"), Range(10.0f, 500.0f)]
	private float BridgeBorderHeight { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;

	[Property(Title = "Bottom Depth"), Feature("Bridge"), Group("Shape"), Range(10.0f, 500.0f)]
	private float BridgeBottomDepth { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;

	[Property(Title = "Close Caps"), Feature("Bridge"), Group("Shape")]
	private bool BridgeCloseCaps { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;

	[Property(Title = "Pillars"), Feature("Bridge"), ToggleGroup("Pillars"), Order(2)]
	private bool Pillars { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;

	[Property(Title = "Shape"), Feature("Bridge"), ToggleGroup("Pillars")]
	private BridgePillarShape BridgePillarType { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = BridgePillarShape.Square;

	[Property(Title = "Size"), Feature("Bridge"), ToggleGroup("Pillars"), Range(10.0f, 1000.0f)]
	private float BridgePillarSize { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 200.0f;

	[Property(Title = "Height"), Feature("Bridge"), ToggleGroup("Pillars"), Range(10.0f, 5000.0f)]
	private float BridgePillarHeight { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 600.0f;

	[Property(Title = "Spacing"), Feature("Bridge"), ToggleGroup("Pillars"), Range(100.0f, 10000.0f)]
	private float BridgePillarSpacing { get; set { field = value.Clamp(100.0f, 100000.0f); m_DoesBridgeNeedRebuild = true; } } = 1200.0f;

	[Property(Title = "Inset"), Feature("Bridge"), ToggleGroup("Pillars"), Range(0.0f, 200.0f)]
	private float BridgePillarInset { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 20.0f;

	[Property(Title = "Segments"), Feature("Bridge"), ToggleGroup("Pillars"), Range(3, 24), ShowIf(nameof(BridgePillarType), BridgePillarShape.Cylinder)]
	private int BridgePillarRoundSegments { get; set { field = value.Clamp(3, 64); m_DoesBridgeNeedRebuild = true; } } = 12;

	/// <summary>
	/// Does the pillars follow world up vector or follow the road shape ?
	/// </summary>
	[Property(Title = "Keep Vertical (World Up)"), Feature("Bridge"), ToggleGroup("Pillars")]
	private bool BridgePillarsKeepVertical { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;



	private void CreateBridgeMesh(string _Name, Material _Material, Transform[] _Frames, int _SegmentCount, float _InnerOffset, float _HeightOffset, float _TextureRepeat)
	{
		var polygonMesh = new PolygonMesh();
		int frameCount = _SegmentCount + 1;
		const int verticesPerFrame = 10;
		var positions = new Vector3[frameCount * verticesPerFrame];

		float outerOffset = _InnerOffset + BridgeBorderWidth;
		float topHeight = _HeightOffset + BridgeBorderHeight;
		float bottomHeight = _HeightOffset - BridgeBottomDepth;
		float underRoadOffset = MathF.Max(0.0f, _InnerOffset);

		for (int i = 0; i < frameCount; i++)
		{
			var frame = _Frames[i];
			var position = frame.Position;
			var right = frame.Rotation.Right;
			var up = frame.Rotation.Up;

			positions[i * verticesPerFrame] = position - right * _InnerOffset;
			positions[i * verticesPerFrame + 1] = position - right * _InnerOffset + up * topHeight;
			positions[i * verticesPerFrame + 2] = position - right * outerOffset + up * topHeight;
			positions[i * verticesPerFrame + 3] = position - right * outerOffset + up * bottomHeight;
			positions[i * verticesPerFrame + 4] = position - right * underRoadOffset + up * bottomHeight;
			positions[i * verticesPerFrame + 5] = position + right * underRoadOffset + up * bottomHeight;
			positions[i * verticesPerFrame + 6] = position + right * outerOffset + up * bottomHeight;
			positions[i * verticesPerFrame + 7] = position + right * outerOffset + up * topHeight;
			positions[i * verticesPerFrame + 8] = position + right * _InnerOffset + up * topHeight;
			positions[i * verticesPerFrame + 9] = position + right * _InnerOffset;
		}

		var vertices = polygonMesh.AddVertices(positions);

		int profileEdgeCount = verticesPerFrame - 1;
		var profileDistances = new float[verticesPerFrame];
		for (int i = 0; i < profileEdgeCount; i++)
		{
			var current = positions[i];
			var next = positions[i + 1];
			profileDistances[i + 1] = profileDistances[i] + Vector3.DistanceBetween(current, next) / _TextureRepeat;
		}

		float splineDistance = 0f;

		for (int segmentIndex = 0; segmentIndex < _SegmentCount; segmentIndex++)
		{
			float segmentTravel = Vector3.DistanceBetween(_Frames[segmentIndex].Position, _Frames[segmentIndex + 1].Position);
			float v0 = splineDistance / _TextureRepeat;
			float v1 = (splineDistance + segmentTravel) / _TextureRepeat;

			for (int edgeIndex = 0; edgeIndex < profileEdgeCount; edgeIndex++)
			{
				int currentIndex0 = segmentIndex * verticesPerFrame + edgeIndex;
				int nextIndex0 = currentIndex0 + 1;
				int currentIndex1 = (segmentIndex + 1) * verticesPerFrame + edgeIndex;
				int nextIndex1 = currentIndex1 + 1;
				float u0 = profileDistances[edgeIndex];
				float u1 = profileDistances[edgeIndex + 1];

				MeshUtility.AddTexturedQuad(
					polygonMesh,
					_Material,
					vertices[currentIndex0],
					vertices[currentIndex1],
					vertices[nextIndex1],
					vertices[nextIndex0],
					new Vector2(u0, v0),
					new Vector2(u0, v1),
					new Vector2(u1, v1),
					new Vector2(u1, v0)
				);
			}

			splineDistance += segmentTravel;
		}

		if (BridgeCloseCaps)
		{
			AddBridgeCap(polygonMesh, vertices, positions, 0, verticesPerFrame, _Material, _TextureRepeat, _Frames[0].Rotation.Right, _Frames[0].Rotation.Up, _IsStartCap: true);
			AddBridgeCap(polygonMesh, vertices, positions, (frameCount - 1) * verticesPerFrame, verticesPerFrame, _Material, _TextureRepeat, _Frames[frameCount - 1].Rotation.Right, _Frames[frameCount - 1].Rotation.Up, _IsStartCap: false);
		}

		CreateBridgeChild(_Name, polygonMesh);

		if (Pillars)
			CreateBridgePillars($"{_Name}_Pillars", _Material, _Frames, bottomHeight, _TextureRepeat);
	}



	private void CreateBridgePillars(string _Name, Material _Material, Transform[] _Frames, float _BridgeBottomHeight, float _TextureRepeat)
	{
		if (_Frames.Length < 2)
			return;

		float pillarHeight = Math.Max(1.0f, BridgePillarHeight);
		float halfSize = Math.Max(1.0f, BridgePillarSize) * 0.5f;
		float spacing = Math.Max(1.0f, BridgePillarSpacing);

		var sampledFrames = new List<(Transform frame, float distance)>(_Frames.Length);
		float totalDistance = 0.0f;
		sampledFrames.Add((_Frames[0], 0.0f));

		for (int i = 1; i < _Frames.Length; i++)
		{
			totalDistance += Vector3.DistanceBetween(_Frames[i - 1].Position, _Frames[i].Position);
			sampledFrames.Add((_Frames[i], totalDistance));
		}

		if (totalDistance <= 0.0f)
			return;

		var polygonMesh = new PolygonMesh();
		var pillarDistances = new List<float>();

		if (totalDistance <= spacing)
		{
			pillarDistances.Add(totalDistance * 0.5f);
		}
		else
		{
			for (float distance = spacing * 0.5f; distance < totalDistance; distance += spacing)
				pillarDistances.Add(distance);
		}

		if (pillarDistances.Count == 0)
			return;

		foreach (float distance in pillarDistances)
		{
			Transform frame = InterpolateFrameAtDistance(sampledFrames, distance);
			Vector3 pillarUp = BridgePillarsKeepVertical ? Vector3.Up : frame.Rotation.Up;
			Vector3 pillarForward = BridgePillarsKeepVertical ? frame.Rotation.Forward.WithZ(0).Normal : frame.Rotation.Forward;

			if (pillarForward.IsNearZeroLength)
				pillarForward = Vector3.Forward;

			Vector3 pillarRight = Vector3.Cross(pillarForward, pillarUp).Normal;

			if (pillarRight.IsNearZeroLength)
				pillarRight = Vector3.Right;

			pillarForward = Vector3.Cross(pillarUp, pillarRight).Normal;

			float pillarTopOffset = _BridgeBottomHeight + (BridgePillarsKeepVertical ? BridgePillarInset : 0.0f);
			Vector3 topCenter = frame.Position + pillarUp * pillarTopOffset;

			if (BridgePillarType == BridgePillarShape.Cylinder)
				AddRoundBridgePillar(polygonMesh, _Material, topCenter, pillarRight, pillarForward, pillarUp, halfSize, pillarHeight, _TextureRepeat);
			else
				AddSquareBridgePillar(polygonMesh, _Material, topCenter, pillarRight, pillarForward, pillarUp, halfSize, pillarHeight, _TextureRepeat);
		}

		CreateBridgeChild(_Name, polygonMesh);
	}



	private static void AddSquareBridgePillar(PolygonMesh _PolygonMesh, Material _Material, Vector3 _TopCenter, Vector3 _Right, Vector3 _Forward, Vector3 _Up, float _HalfSize, float _Height, float _TextureRepeat)
	{
		var topCorners = new[]
		{
			_TopCenter - _Right * _HalfSize - _Forward * _HalfSize,
			_TopCenter + _Right * _HalfSize - _Forward * _HalfSize,
			_TopCenter + _Right * _HalfSize + _Forward * _HalfSize,
			_TopCenter - _Right * _HalfSize + _Forward * _HalfSize
		};

		var bottomCorners = new Vector3[4];

		for (int i = 0; i < 4; i++)
			bottomCorners[i] = topCorners[i] - _Up * _Height;

		var positions = new Vector3[8];
		Array.Copy(topCorners, 0, positions, 0, 4);
		Array.Copy(bottomCorners, 0, positions, 4, 4);
		var vertices = _PolygonMesh.AddVertices(positions);

		for (int side = 0; side < 4; side++)
		{
			int next = (side + 1) % 4;
			float edgeLength = Vector3.DistanceBetween(topCorners[side], topCorners[next]) / _TextureRepeat;
			float vLength = _Height / _TextureRepeat;

			MeshUtility.AddTexturedQuad(
				_PolygonMesh,
				_Material,
				vertices[side],
				vertices[side + 4],
				vertices[next + 4],
				vertices[next],
				new Vector2(0, 0),
				new Vector2(0, vLength),
				new Vector2(edgeLength, vLength),
				new Vector2(edgeLength, 0)
			);
		}
	}



	private void AddRoundBridgePillar(PolygonMesh _PolygonMesh, Material _Material, Vector3 _TopCenter, Vector3 _Right, Vector3 _Forward, Vector3 _Up, float _Radius, float _Height, float _TextureRepeat)
	{
		int segmentCount = Math.Max(3, BridgePillarRoundSegments);
		var positions = new Vector3[segmentCount * 2];

		for (int i = 0; i < segmentCount; i++)
		{
			float angle = (MathF.PI * 2.0f * i) / segmentCount;
			Vector3 radial = _Right * MathF.Cos(angle) + _Forward * MathF.Sin(angle);
			Vector3 top = _TopCenter + radial * _Radius;

			positions[i] = top;
			positions[i + segmentCount] = top - _Up * _Height;
		}

		var vertices = _PolygonMesh.AddVertices(positions);
		float circumference = MathF.PI * 2.0f * _Radius;

		for (int side = 0; side < segmentCount; side++)
		{
			int next = (side + 1) % segmentCount;
			float u0 = (circumference * side / segmentCount) / _TextureRepeat;
			float u1 = (circumference * (side + 1) / segmentCount) / _TextureRepeat;
			float v1 = _Height / _TextureRepeat;

			MeshUtility.AddTexturedQuad(
				_PolygonMesh,
				_Material,
				vertices[side],
				vertices[side + segmentCount],
				vertices[next + segmentCount],
				vertices[next],
				new Vector2(u0, 0),
				new Vector2(u0, v1),
				new Vector2(u1, v1),
				new Vector2(u1, 0)
			);
		}
	}



	private static void AddBridgeCap(PolygonMesh _PolygonMesh, VertexHandle[] _Vertices, Vector3[] _Positions, int _StartIndex, int _VerticesPerFrame, Material _Material, float _TextureRepeat, Vector3 _CapRight, Vector3 _CapUp, bool _IsStartCap)
	{
		var remainingIndices = new List<int>(_VerticesPerFrame);
		var capPoints = new Vector2[_VerticesPerFrame];
		Vector3 origin = _Positions[_StartIndex];

		for (int i = 0; i < _VerticesPerFrame; i++)
		{
			remainingIndices.Add(i);
			Vector3 position = _Positions[_StartIndex + i];
			Vector3 local = position - origin;
			capPoints[i] = new Vector2(Vector3.Dot(local, _CapRight), Vector3.Dot(local, _CapUp));
		}

		if (GetSignedArea(capPoints) < 0.0f)
			remainingIndices.Reverse();

		while (remainingIndices.Count > 2)
		{
			bool foundEar = false;

			for (int i = 0; i < remainingIndices.Count; i++)
			{
				int previous = remainingIndices[(i - 1 + remainingIndices.Count) % remainingIndices.Count];
				int current = remainingIndices[i];
				int next = remainingIndices[(i + 1) % remainingIndices.Count];

				if (!IsBridgeEar(previous, current, next, remainingIndices, capPoints))
					continue;

				var face = _IsStartCap
					? _PolygonMesh.AddFace(_Vertices[_StartIndex + previous], _Vertices[_StartIndex + current], _Vertices[_StartIndex + next])
					: _PolygonMesh.AddFace(_Vertices[_StartIndex + previous], _Vertices[_StartIndex + next], _Vertices[_StartIndex + current]);

				if (face.IsValid)
				{
					_PolygonMesh.SetFaceMaterial(face, _Material);

					var uvA = capPoints[previous] / _TextureRepeat;
					var uvB = capPoints[current] / _TextureRepeat;
					var uvC = capPoints[next] / _TextureRepeat;

					var uvs = _IsStartCap
						? new List<Vector2> { uvA, uvB, uvC }
						: new List<Vector2> { uvA, uvC, uvB };

					_PolygonMesh.SetFaceTextureCoords(face, uvs);
				}

				remainingIndices.RemoveAt(i);
				foundEar = true;
				break;
			}

			if (foundEar)
				continue;

			break;
		}
	}



	private static float GetSignedArea(Vector2[] _Points)
	{
		float area = 0.0f;

		for (int i = 0; i < _Points.Length; i++)
		{
			Vector2 a = _Points[i];
			Vector2 b = _Points[(i + 1) % _Points.Length];

			area += (a.x * b.y) - (b.x * a.y);
		}

		return area * 0.5f;
	}



	private static bool IsBridgeEar(int _Previous, int _Current, int _Next, List<int> _Indices, Vector2[] _Points)
	{
		Vector2 a = _Points[_Previous];
		Vector2 b = _Points[_Current];
		Vector2 c = _Points[_Next];

		if (CrossBridge2D(b - a, c - b) <= 0.0f)
			return false;

		foreach (var testIndex in _Indices)
		{
			if (testIndex == _Previous || testIndex == _Current || testIndex == _Next)
				continue;

			if (IsPointInBridgeTriangle(_Points[testIndex], a, b, c))
				return false;
		}

		return true;
	}



	private static float CrossBridge2D(Vector2 _A, Vector2 _B) => _A.x * _B.y - _A.y * _B.x;



	private static bool IsPointInBridgeTriangle(Vector2 _Point, Vector2 _A, Vector2 _B, Vector2 _C)
	{
		float ab = CrossBridge2D(_B - _A, _Point - _A);
		float bc = CrossBridge2D(_C - _B, _Point - _B);
		float ca = CrossBridge2D(_A - _C, _Point - _C);

		bool hasNegative = ab < 0.0f || bc < 0.0f || ca < 0.0f;
		bool hasPositive = ab > 0.0f || bc > 0.0f || ca > 0.0f;

		return !(hasNegative && hasPositive);
	}



	private void CreateBridgeChild(string _Name, PolygonMesh _PolygonMesh)
	{
		var child = new GameObject(GameObject, true, _Name);
		child.Tags.Add(BridgeTag);

		var meshComponent = child.AddComponent<MeshComponent>();
		meshComponent.Mesh = _PolygonMesh;
		meshComponent.SmoothingAngle = 40.0f;
	}



	private void UpdateBridge()
	{
		if (m_DoesBridgeNeedRebuild)
		{
			CreateBridge();

			m_DoesBridgeNeedRebuild = false;
		}
	}



	private void EnsureBridgeMeshExist()
	{
		if (SandboxUtility.IsInPlayMode)
			return;

		if (IsLocked)
			return;

		if (HasGeneratedMeshChildren(BridgeTag))
			return;

		CreateBridge();
	}



	private void CreateBridge()
	{
		if (IsLocked)
			return;

		if (!Scene.IsEditor)
			return;

		RemoveBridge();

		if (!HasBridge)
			return;

		GetSplineFrameData(out var sampledFrames, out var segmentsToKeep);
		var frames = segmentsToKeep.Select(index => sampledFrames[index]).ToArray();
		int totalSegments = frames.Length - 1;

		if (totalSegments <= 0)
			return;

		float roadEdgeOffset = RoadWidth * 0.5f;
		float sidewalkOffset = HasSidewalk ? SidewalkWidth : 0.0f;
		float sidewalkUp = HasSidewalk ? SidewalkHeight : 0.0f;
		float baseInnerOffset = roadEdgeOffset + sidewalkOffset;

		float textureRepeat = Math.Max(1.0f, BridgeTextureRepeat);
		var material = BridgeMaterial ?? Material.Load("materials/dev/reflectivity_50.vmat");

		CreateBridgeMesh("Bridge", material, frames, totalSegments, baseInnerOffset, sidewalkUp, textureRepeat);
	}



	private void RemoveBridge()
	{
		if (IsLocked)
			return;

		if (!Scene.IsEditor)
			return;

		RemoveGeneratedMeshChildren(BridgeTag);
	}
}